mirror of
https://github.com/dg/dibi.git
synced 2025-09-03 02:52:47 +02:00
Compare commits
11 Commits
master
...
translator
Author | SHA1 | Date | |
---|---|---|---|
|
09a974ed7a | ||
|
124d52139c | ||
|
78646e1790 | ||
|
cab1c5b5e6 | ||
|
94df6db03d | ||
|
0575f9ea17 | ||
|
76b8ed2108 | ||
|
4b1a2faa76 | ||
|
b931dbe13b | ||
|
a55e2a0cf8 | ||
|
6356f9f7a4 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,8 +1,8 @@
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.github export-ignore
|
||||
appveyor.yml export-ignore
|
||||
ncs.* export-ignore
|
||||
.travis.yml export-ignore
|
||||
ecs.php export-ignore
|
||||
phpstan.neon export-ignore
|
||||
tests/ export-ignore
|
||||
|
||||
|
8
.github/workflows/coding-style.yml
vendored
8
.github/workflows/coding-style.yml
vendored
@@ -7,10 +7,10 @@ jobs:
|
||||
name: Nette Code Checker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
php-version: 8.0
|
||||
coverage: none
|
||||
|
||||
- run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
name: Nette Coding Standard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
php-version: 8.0
|
||||
coverage: none
|
||||
|
||||
- run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress
|
||||
|
4
.github/workflows/static-analysis.yml
vendored
4
.github/workflows/static-analysis.yml
vendored
@@ -10,10 +10,10 @@ jobs:
|
||||
name: PHPStan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.2
|
||||
php-version: 8.0
|
||||
coverage: none
|
||||
|
||||
- run: composer install --no-progress --prefer-dist
|
||||
|
17
.github/workflows/tests.yml
vendored
17
.github/workflows/tests.yml
vendored
@@ -3,7 +3,7 @@ name: Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
php-extensions: mbstring, intl, mysqli, pgsql, sqlsrv-5.12.0, pdo_sqlsrv-5.12.0
|
||||
php-extensions: mbstring, intl, mysqli, pgsql, sqlsrv-5.10.0beta2, pdo_sqlsrv-5.10.0beta2
|
||||
php-tools: "composer:v2, pecl"
|
||||
|
||||
jobs:
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.2', '8.3', '8.4', '8.5']
|
||||
php: ['8.0', '8.1', '8.2']
|
||||
|
||||
fail-fast: false
|
||||
|
||||
@@ -43,6 +43,7 @@ jobs:
|
||||
--health-retries=5
|
||||
-e MYSQL_ROOT_PASSWORD=root
|
||||
-e MYSQL_DATABASE=dibi_test
|
||||
--entrypoint sh mysql:8 -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password"
|
||||
|
||||
postgres96:
|
||||
image: postgres:9.6
|
||||
@@ -82,13 +83,13 @@ jobs:
|
||||
- 1433:1433
|
||||
options: >-
|
||||
--name=mssql
|
||||
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1' -N -C"
|
||||
--health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
@@ -100,19 +101,19 @@ jobs:
|
||||
run: cp ./tests/databases.github.ini ./tests/databases.ini
|
||||
|
||||
- name: Create MS SQL Database
|
||||
run: docker exec -i mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE dibi_test' -N -C
|
||||
run: docker exec -i mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE dibi_test'
|
||||
|
||||
- run: composer install --no-progress --prefer-dist
|
||||
- run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src
|
||||
- if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: output-${{ matrix.php }}
|
||||
name: output
|
||||
path: tests/**/output
|
||||
|
||||
|
||||
- name: Save Code Coverage
|
||||
if: ${{ matrix.php == '8.2' }}
|
||||
if: ${{ matrix.php == '8.0' }}
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
|
@@ -11,32 +11,27 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "8.2 - 8.5"
|
||||
"php": ">=8.0 <8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"tracy/tracy": "^2.9",
|
||||
"nette/tester": "^2.5",
|
||||
"nette/di": "^3.1",
|
||||
"phpstan/phpstan-nette": "^2.0@stable",
|
||||
"jetbrains/phpstorm-attributes": "^1.0"
|
||||
"tracy/tracy": "^2.8",
|
||||
"nette/tester": "^2.4",
|
||||
"nette/di": "^3.0",
|
||||
"phpstan/phpstan": "^0.12"
|
||||
},
|
||||
"replace": {
|
||||
"dg/dibi": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": ["src/"],
|
||||
"psr-4": {
|
||||
"Dibi\\": "src/Dibi"
|
||||
}
|
||||
"classmap": ["src/"]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"scripts": {
|
||||
"phpstan": "phpstan analyse",
|
||||
"tester": "tester tests -s"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "6.0-dev"
|
||||
"dev-master": "5.0-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -27,9 +27,9 @@ $dibi = new Dibi\Connection([
|
||||
// using manual hints
|
||||
$res = $dibi->query('SELECT * FROM [customers]');
|
||||
|
||||
$res->setType('customer_id', Type::Integer)
|
||||
->setType('added', Type::DateTime)
|
||||
->setFormat(Type::DateTime, 'Y-m-d H:i:s');
|
||||
$res->setType('customer_id', Type::INTEGER)
|
||||
->setType('added', Type::DATETIME)
|
||||
->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
|
||||
|
||||
|
||||
Tracy\Dumper::dump($res->fetch());
|
||||
|
@@ -45,7 +45,7 @@ function substFallBack($expr)
|
||||
|
||||
|
||||
// define callback
|
||||
$dibi->getSubstitutes()->setCallback(substFallBack(...));
|
||||
$dibi->getSubstitutes()->setCallback('substFallBack');
|
||||
|
||||
// define substitutes as constants
|
||||
define('SUBST_ACCOUNT', 'eshop_');
|
||||
|
@@ -34,7 +34,7 @@ Install Dibi via Composer:
|
||||
composer require dibi/dibi
|
||||
```
|
||||
|
||||
The Dibi 6.0 requires PHP version 8.2 and supports PHP up to 8.5.
|
||||
The Dibi 5.0 requires PHP version 8.0 and supports PHP up to 8.2.
|
||||
|
||||
|
||||
Usage
|
||||
@@ -341,7 +341,7 @@ $database->query('INSERT INTO users', [
|
||||
There are three methods for dealing with transactions:
|
||||
|
||||
```php
|
||||
$database->begin();
|
||||
$database->beginTransaction();
|
||||
|
||||
$database->commit();
|
||||
|
||||
@@ -608,7 +608,7 @@ $database->query("UPDATE [:blog:items] SET [text]='Hello World'");
|
||||
Dibi automatically detects the types of query columns and converts fields them to native PHP types. We can also specify the type manually. You can find the possible types in the `Dibi\Type` class.
|
||||
|
||||
```php
|
||||
$result->setType('id', Dibi\Type::Integer); // id will be integer
|
||||
$result->setType('id', Dibi\Type::INTEGER); // id will be integer
|
||||
$row = $result->fetch();
|
||||
|
||||
is_int($row->id) // true
|
||||
@@ -639,7 +639,7 @@ In the configuration file, we will register the DI extensions and add the `dibi`
|
||||
|
||||
```neon
|
||||
extensions:
|
||||
dibi: Dibi\Bridges\Nette\DibiExtension3
|
||||
dibi: Dibi\Bridges\Nette\DibiExtension22
|
||||
|
||||
dibi:
|
||||
host: localhost
|
||||
|
@@ -19,10 +19,15 @@ use Tracy;
|
||||
*/
|
||||
class DibiExtension22 extends Nette\DI\CompilerExtension
|
||||
{
|
||||
public function __construct(
|
||||
private ?bool $debugMode = null,
|
||||
private ?bool $cliMode = null,
|
||||
) {
|
||||
private ?bool $debugMode;
|
||||
|
||||
private ?bool $cliMode;
|
||||
|
||||
|
||||
public function __construct(?bool $debugMode = null, ?bool $cliMode = null)
|
||||
{
|
||||
$this->debugMode = $debugMode;
|
||||
$this->cliMode = $cliMode;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Bridges\Nette;
|
||||
|
||||
use Dibi;
|
||||
use Nette;
|
||||
use Nette\Schema\Expect;
|
||||
use Tracy;
|
||||
use function is_array;
|
||||
|
||||
|
||||
/**
|
||||
* Dibi extension for Nette Framework 3. Creates 'connection' & 'panel' services.
|
||||
*/
|
||||
class DibiExtension3 extends Nette\DI\CompilerExtension
|
||||
{
|
||||
public function __construct(
|
||||
private ?bool $debugMode = null,
|
||||
private ?bool $cliMode = null,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public function getConfigSchema(): Nette\Schema\Schema
|
||||
{
|
||||
return Expect::structure([
|
||||
'autowired' => Expect::bool(true),
|
||||
'flags' => Expect::anyOf(Expect::arrayOf('string'), Expect::type('dynamic')),
|
||||
'profiler' => Expect::bool(),
|
||||
'explain' => Expect::bool(true),
|
||||
'filter' => Expect::bool(true),
|
||||
'driver' => Expect::string()->dynamic(),
|
||||
'name' => Expect::string()->dynamic(),
|
||||
'lazy' => Expect::bool(false)->dynamic(),
|
||||
'onConnect' => Expect::array()->dynamic(),
|
||||
'substitutes' => Expect::arrayOf('string')->dynamic(),
|
||||
'result' => Expect::structure([
|
||||
'normalize' => Expect::bool(true),
|
||||
'formatDateTime' => Expect::string(),
|
||||
'formatTimeInterval' => Expect::string(),
|
||||
'formatJson' => Expect::string(),
|
||||
])->castTo('array'),
|
||||
])->otherItems(Expect::type('mixed'))
|
||||
->castTo('array');
|
||||
}
|
||||
|
||||
|
||||
public function loadConfiguration()
|
||||
{
|
||||
$container = $this->getContainerBuilder();
|
||||
$config = $this->getConfig();
|
||||
$this->debugMode ??= $container->parameters['debugMode'];
|
||||
$this->cliMode ??= $container->parameters['consoleMode'];
|
||||
|
||||
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode && !$this->cliMode);
|
||||
unset($config['profiler']);
|
||||
|
||||
if (is_array($config['flags'])) {
|
||||
$flags = 0;
|
||||
foreach ((array) $config['flags'] as $flag) {
|
||||
$flags |= constant($flag);
|
||||
}
|
||||
$config['flags'] = $flags;
|
||||
}
|
||||
|
||||
$connection = $container->addDefinition($this->prefix('connection'))
|
||||
->setCreator(Dibi\Connection::class, [$config])
|
||||
->setAutowired($config['autowired']);
|
||||
|
||||
if (class_exists(Tracy\Debugger::class)) {
|
||||
$connection->addSetup(
|
||||
[new Nette\DI\Definitions\Statement('Tracy\Debugger::getBlueScreen'), 'addPanel'],
|
||||
[[Dibi\Bridges\Tracy\Panel::class, 'renderException']],
|
||||
);
|
||||
}
|
||||
|
||||
if ($useProfiler) {
|
||||
$panel = $container->addDefinition($this->prefix('panel'))
|
||||
->setCreator(Dibi\Bridges\Tracy\Panel::class, [
|
||||
$config['explain'],
|
||||
$config['filter'] ? Dibi\Event::QUERY : Dibi\Event::ALL,
|
||||
]);
|
||||
$connection->addSetup([$panel, 'register'], [$connection]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
# This will create service named 'dibi.connection'.
|
||||
# Requires Nette Framework 3 or later
|
||||
# Requires Nette Framework 2.2 or later
|
||||
|
||||
extensions:
|
||||
dibi: Dibi\Bridges\Nette\DibiExtension3
|
||||
dibi: Dibi\Bridges\Nette\DibiExtension22
|
||||
|
||||
dibi:
|
||||
host: localhost
|
||||
|
@@ -13,7 +13,6 @@ use Dibi;
|
||||
use Dibi\Event;
|
||||
use Dibi\Helpers;
|
||||
use Tracy;
|
||||
use function count, is_string, strlen;
|
||||
|
||||
|
||||
/**
|
||||
@@ -21,23 +20,30 @@ use function count, is_string, strlen;
|
||||
*/
|
||||
class Panel implements Tracy\IBarPanel
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** maximum SQL length */
|
||||
public static int $maxLength = 1000;
|
||||
|
||||
public bool|string $explain;
|
||||
|
||||
public int $filter;
|
||||
|
||||
private array $events = [];
|
||||
|
||||
|
||||
public function __construct(
|
||||
public bool|string $explain = true,
|
||||
public int $filter = Event::QUERY,
|
||||
) {
|
||||
public function __construct(bool $explain = true, ?int $filter = null)
|
||||
{
|
||||
$this->filter = $filter ?: Event::QUERY;
|
||||
$this->explain = $explain;
|
||||
}
|
||||
|
||||
|
||||
public function register(Dibi\Connection $connection): void
|
||||
{
|
||||
Tracy\Debugger::getBar()->addPanel($this);
|
||||
Tracy\Debugger::getBlueScreen()->addPanel(self::renderException(...));
|
||||
$connection->onEvent[] = $this->logEvent(...);
|
||||
Tracy\Debugger::getBlueScreen()->addPanel([self::class, 'renderException']);
|
||||
$connection->onEvent[] = [$this, 'logEvent'];
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +68,7 @@ class Panel implements Tracy\IBarPanel
|
||||
if ($e instanceof Dibi\Exception && $e->getSql()) {
|
||||
return [
|
||||
'tab' => 'SQL',
|
||||
'panel' => Helpers::dump($e->getSql(), return: true),
|
||||
'panel' => Helpers::dump($e->getSql(), true),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -118,7 +124,7 @@ class Panel implements Tracy\IBarPanel
|
||||
? $this->explain
|
||||
: ($connection->getConfig('driver') === 'oracle' ? 'EXPLAIN PLAN FOR' : 'EXPLAIN');
|
||||
try {
|
||||
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), return: true);
|
||||
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), true);
|
||||
} catch (Dibi\Exception $e) {
|
||||
}
|
||||
|
||||
@@ -132,7 +138,7 @@ class Panel implements Tracy\IBarPanel
|
||||
$s .= "<br /><a href='#tracy-debug-DibiProfiler-row-$counter' class='tracy-toggle tracy-collapsed' rel='#tracy-debug-DibiProfiler-row-$counter'>explain</a>";
|
||||
}
|
||||
|
||||
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, return: true);
|
||||
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, true);
|
||||
if ($explain) {
|
||||
$s .= "<div id='tracy-debug-DibiProfiler-row-$counter' class='tracy-collapsed'>{$explain}</div>";
|
||||
}
|
||||
@@ -151,8 +157,8 @@ class Panel implements Tracy\IBarPanel
|
||||
#tracy-debug .tracy-DibiProfiler-source { color: #999 !important }
|
||||
#tracy-debug tracy-DibiProfiler tr table { margin: 8px 0; max-height: 150px; overflow:auto } </style>
|
||||
<h1>Queries:' . "\u{a0}" . count($this->events)
|
||||
. ($totalTime === null ? '' : ", time:\u{a0}" . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms")
|
||||
. ($singleConnection === null ? '' : ', ' . htmlspecialchars($this->getConnectionName($singleConnection))) . '</h1>
|
||||
. ($totalTime === null ? '' : ", time:\u{a0}" . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms") . ', '
|
||||
. htmlspecialchars($this->getConnectionName($singleConnection)) . '</h1>
|
||||
<div class="tracy-inner tracy-DibiProfiler">
|
||||
<table class="tracy-sortable">
|
||||
<tr><th>Time ms</th><th>SQL Statement</th><th>Rows</th>' . (!$singleConnection ? '<th>Connection</th>' : '') . '</tr>
|
||||
@@ -165,7 +171,7 @@ class Panel implements Tracy\IBarPanel
|
||||
private function getConnectionName(Dibi\Connection $connection): string
|
||||
{
|
||||
$driver = $connection->getConfig('driver');
|
||||
return get_debug_type($driver)
|
||||
return (is_object($driver) ? $driver::class : $driver)
|
||||
. ($connection->getConfig('name') ? '/' . $connection->getConfig('name') : '')
|
||||
. ($connection->getConfig('host') ? "\u{202f}@\u{202f}" . $connection->getConfig('host') : '');
|
||||
}
|
||||
|
@@ -9,10 +9,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use JetBrains\PhpStorm\Language;
|
||||
use Traversable;
|
||||
use function array_key_exists, is_array, sprintf;
|
||||
use const PHP_SAPI;
|
||||
|
||||
|
||||
/**
|
||||
@@ -21,34 +18,28 @@ use const PHP_SAPI;
|
||||
* @property-read int $affectedRows
|
||||
* @property-read int $insertId
|
||||
*/
|
||||
class Connection
|
||||
class Connection implements IConnection
|
||||
{
|
||||
private const Drivers = [
|
||||
'firebird' => Drivers\Ibase\Connection::class,
|
||||
'mysqli' => Drivers\MySQLi\Connection::class,
|
||||
'odbc' => Drivers\ODBC\Connection::class,
|
||||
'oracle' => Drivers\OCI8\Connection::class,
|
||||
'pdo' => Drivers\PDO\Connection::class,
|
||||
'postgre' => Drivers\PgSQL\Connection::class,
|
||||
'sqlite3' => Drivers\SQLite3\Connection::class,
|
||||
'sqlite' => Drivers\SQLite3\Connection::class,
|
||||
'sqlsrv' => Drivers\SQLSrv\Connection::class,
|
||||
];
|
||||
use Strict;
|
||||
|
||||
/** function (Event $event); Occurs after query is executed */
|
||||
public ?array $onEvent = [];
|
||||
|
||||
/** Current connection configuration */
|
||||
private array $config;
|
||||
|
||||
/** @var string[] resultset formats */
|
||||
private array $formats;
|
||||
private ?Drivers\Connection $driver = null;
|
||||
private Drivers\Engine $engine;
|
||||
|
||||
private ?Driver $driver = null;
|
||||
|
||||
private ?Translator $translator = null;
|
||||
|
||||
/** @var array<string, callable(object): Expression | null> */
|
||||
/** @var array<string, callable(object): Expression> */
|
||||
private array $translators = [];
|
||||
private bool $sortTranslators = false;
|
||||
|
||||
private HashMap $substitutes;
|
||||
|
||||
private int $transactionDepth = 0;
|
||||
|
||||
|
||||
@@ -89,10 +80,10 @@ class Connection
|
||||
$this->config = $config;
|
||||
|
||||
$this->formats = [
|
||||
Type::Date => $this->config['result']['formatDate'],
|
||||
Type::DateTime => $this->config['result']['formatDateTime'],
|
||||
Type::DATE => $this->config['result']['formatDate'],
|
||||
Type::DATETIME => $this->config['result']['formatDateTime'],
|
||||
Type::JSON => $this->config['result']['formatJson'] ?? 'array',
|
||||
Type::TimeInterval => $this->config['result']['formatTimeInterval'] ?? null,
|
||||
Type::TIME_INTERVAL => $this->config['result']['formatTimeInterval'] ?? null,
|
||||
];
|
||||
|
||||
// profiler
|
||||
@@ -135,16 +126,17 @@ class Connection
|
||||
*/
|
||||
final public function connect(): void
|
||||
{
|
||||
if ($this->config['driver'] instanceof Drivers\Connection) {
|
||||
if ($this->config['driver'] instanceof Driver) {
|
||||
$this->driver = $this->config['driver'];
|
||||
$this->translator = new Translator($this);
|
||||
return;
|
||||
|
||||
} elseif (is_subclass_of($this->config['driver'], Drivers\Connection::class)) {
|
||||
} elseif (is_subclass_of($this->config['driver'], Driver::class)) {
|
||||
$class = $this->config['driver'];
|
||||
|
||||
} else {
|
||||
$class = self::Drivers[strtolower($this->config['driver'])] ?? throw new Exception("Unknown driver '{$this->config['driver']}'.");
|
||||
$class = preg_replace(['#\W#', '#sql#'], ['_', 'Sql'], ucfirst(strtolower($this->config['driver'])));
|
||||
$class = "Dibi\\Drivers\\{$class}Driver";
|
||||
if (!class_exists($class)) {
|
||||
throw new Exception("Unable to create instance of Dibi driver '$class'.");
|
||||
}
|
||||
@@ -210,7 +202,7 @@ class Connection
|
||||
/**
|
||||
* Returns the driver and connects to a database in lazy mode.
|
||||
*/
|
||||
final public function getDriver(): Drivers\Connection
|
||||
final public function getDriver(): Driver
|
||||
{
|
||||
if (!$this->driver) {
|
||||
$this->connect();
|
||||
@@ -224,7 +216,7 @@ class Connection
|
||||
* Generates (translates) and executes SQL query.
|
||||
* @throws Exception
|
||||
*/
|
||||
final public function query(#[Language('GenericSQL')] mixed ...$args): Result
|
||||
final public function query(mixed ...$args): Result
|
||||
{
|
||||
return $this->nativeQuery($this->translate(...$args));
|
||||
}
|
||||
@@ -234,7 +226,7 @@ class Connection
|
||||
* Generates SQL query.
|
||||
* @throws Exception
|
||||
*/
|
||||
final public function translate(#[Language('GenericSQL')] mixed ...$args): string
|
||||
final public function translate(mixed ...$args): string
|
||||
{
|
||||
if (!$this->driver) {
|
||||
$this->connect();
|
||||
@@ -247,7 +239,7 @@ class Connection
|
||||
/**
|
||||
* Generates and prints SQL query.
|
||||
*/
|
||||
final public function test(#[Language('GenericSQL')] mixed ...$args): bool
|
||||
final public function test(mixed ...$args): bool
|
||||
{
|
||||
try {
|
||||
Helpers::dump($this->translate(...$args));
|
||||
@@ -269,7 +261,7 @@ class Connection
|
||||
* Generates (translates) and returns SQL query as DataSource.
|
||||
* @throws Exception
|
||||
*/
|
||||
final public function dataSource(#[Language('GenericSQL')] mixed ...$args): DataSource
|
||||
final public function dataSource(mixed ...$args): DataSource
|
||||
{
|
||||
return new DataSource($this->translate(...$args), $this);
|
||||
}
|
||||
@@ -279,7 +271,7 @@ class Connection
|
||||
* Executes the SQL query.
|
||||
* @throws Exception
|
||||
*/
|
||||
final public function nativeQuery(#[Language('SQL')] string $sql): Result
|
||||
final public function nativeQuery(string $sql): Result
|
||||
{
|
||||
if (!$this->driver) {
|
||||
$this->connect();
|
||||
@@ -298,7 +290,7 @@ class Connection
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$res = $this->createResultSet($res ?: new Drivers\Dummy\Result(max(0, $this->driver->getAffectedRows())));
|
||||
$res = $this->createResultSet($res ?: new Drivers\NoDataResult(max(0, $this->driver->getAffectedRows())));
|
||||
if ($event) {
|
||||
$this->onEvent($event->done($res));
|
||||
}
|
||||
@@ -462,7 +454,7 @@ class Connection
|
||||
/**
|
||||
* Result set factory.
|
||||
*/
|
||||
public function createResultSet(Drivers\Result $resultDriver): Result
|
||||
public function createResultSet(ResultDriver $resultDriver): Result
|
||||
{
|
||||
return (new Result($resultDriver, $this->config['result']['normalize'] ?? true))
|
||||
->setFormats($this->formats);
|
||||
@@ -536,68 +528,19 @@ class Connection
|
||||
/********************* value objects translation ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* @param callable(object): Expression $translator
|
||||
*/
|
||||
public function setObjectTranslator(callable $translator): void
|
||||
/** @param callable(object): Expression $translator */
|
||||
public function addObjectTranslator(string $class, callable $translator): self
|
||||
{
|
||||
if (!$translator instanceof \Closure) {
|
||||
$translator = \Closure::fromCallable($translator);
|
||||
}
|
||||
|
||||
$param = (new \ReflectionFunction($translator))->getParameters()[0] ?? null;
|
||||
$type = $param?->getType();
|
||||
$types = match (true) {
|
||||
$type instanceof \ReflectionNamedType => [$type],
|
||||
$type instanceof \ReflectionUnionType => $type->getTypes(),
|
||||
default => throw new Exception('Object translator must have exactly one parameter with class typehint.'),
|
||||
};
|
||||
|
||||
foreach ($types as $type) {
|
||||
if ($type->isBuiltin() || $type->allowsNull()) {
|
||||
throw new Exception("Object translator must have exactly one parameter with non-nullable class typehint, got '$type'.");
|
||||
}
|
||||
$this->translators[$type->getName()] = $translator;
|
||||
}
|
||||
$this->sortTranslators = true;
|
||||
$this->translators[$class] = $translator;
|
||||
uksort($this->translators, fn($a, $b) => class_exists($a, false) && is_subclass_of($a, $b) ? -1 : 1);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function translateObject(object $object): ?Expression
|
||||
/** @return array<string, callable(object): Expression> */
|
||||
public function getObjectTranslators(): array
|
||||
{
|
||||
if ($this->sortTranslators) {
|
||||
$this->translators = array_filter($this->translators);
|
||||
uksort($this->translators, fn($a, $b) => is_subclass_of($a, $b) ? -1 : 1);
|
||||
$this->sortTranslators = false;
|
||||
}
|
||||
|
||||
if (!array_key_exists($object::class, $this->translators)) {
|
||||
$translator = null;
|
||||
foreach ($this->translators as $class => $t) {
|
||||
if ($object instanceof $class) {
|
||||
$translator = $t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->translators[$object::class] = $translator;
|
||||
}
|
||||
|
||||
$translator = $this->translators[$object::class];
|
||||
if ($translator === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $translator($object);
|
||||
if (!$result instanceof Expression) {
|
||||
throw new Exception(sprintf(
|
||||
"Object translator for class '%s' returned '%s' but %s expected.",
|
||||
$object::class,
|
||||
get_debug_type($result),
|
||||
Expression::class,
|
||||
));
|
||||
}
|
||||
|
||||
return $result;
|
||||
return $this->translators;
|
||||
}
|
||||
|
||||
|
||||
@@ -608,7 +551,7 @@ class Connection
|
||||
* Executes SQL query and fetch result - shortcut for query() & fetch().
|
||||
* @throws Exception
|
||||
*/
|
||||
public function fetch(#[Language('GenericSQL')] mixed ...$args): ?Row
|
||||
public function fetch(mixed ...$args): ?Row
|
||||
{
|
||||
return $this->query($args)->fetch();
|
||||
}
|
||||
@@ -619,7 +562,7 @@ class Connection
|
||||
* @return Row[]|array[]
|
||||
* @throws Exception
|
||||
*/
|
||||
public function fetchAll(#[Language('GenericSQL')] mixed ...$args): array
|
||||
public function fetchAll(mixed ...$args): array
|
||||
{
|
||||
return $this->query($args)->fetchAll();
|
||||
}
|
||||
@@ -629,7 +572,7 @@ class Connection
|
||||
* Executes SQL query and fetch first column - shortcut for query() & fetchSingle().
|
||||
* @throws Exception
|
||||
*/
|
||||
public function fetchSingle(#[Language('GenericSQL')] mixed ...$args): mixed
|
||||
public function fetchSingle(mixed ...$args): mixed
|
||||
{
|
||||
return $this->query($args)->fetchSingle();
|
||||
}
|
||||
@@ -639,7 +582,7 @@ class Connection
|
||||
* Executes SQL query and fetch pairs - shortcut for query() & fetchPairs().
|
||||
* @throws Exception
|
||||
*/
|
||||
public function fetchPairs(#[Language('GenericSQL')] mixed ...$args): array
|
||||
public function fetchPairs(mixed ...$args): array
|
||||
{
|
||||
return $this->query($args)->fetchPairs();
|
||||
}
|
||||
@@ -671,15 +614,6 @@ class Connection
|
||||
}
|
||||
|
||||
|
||||
public function getDatabaseEngine(): Drivers\Engine
|
||||
{
|
||||
if (!$this->driver) { // TODO
|
||||
$this->connect();
|
||||
}
|
||||
return $this->engine ??= $this->driver->getReflector();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a information about the current database.
|
||||
*/
|
||||
@@ -689,14 +623,14 @@ class Connection
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
return new Reflection\Database($this->getDatabaseEngine(), $this->config['database'] ?? null);
|
||||
return new Reflection\Database($this->driver->getReflector(), $this->config['database'] ?? null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prevents unserialization.
|
||||
*/
|
||||
public function __unserialize($_)
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
|
||||
}
|
||||
@@ -705,7 +639,7 @@ class Connection
|
||||
/**
|
||||
* Prevents serialization.
|
||||
*/
|
||||
public function __serialize()
|
||||
public function __sleep()
|
||||
{
|
||||
throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
|
||||
}
|
||||
|
@@ -9,23 +9,32 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function func_get_args, is_array, strpbrk;
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of IDataSource.
|
||||
*/
|
||||
class DataSource implements IDataSource
|
||||
{
|
||||
private readonly Connection $connection;
|
||||
private readonly string $sql;
|
||||
use Strict;
|
||||
|
||||
private Connection $connection;
|
||||
|
||||
private string $sql;
|
||||
|
||||
private ?Result $result = null;
|
||||
|
||||
private ?int $count = null;
|
||||
|
||||
private ?int $totalCount = null;
|
||||
|
||||
private array $cols = [];
|
||||
|
||||
private array $sorting = [];
|
||||
|
||||
private array $conds = [];
|
||||
|
||||
private ?int $offset = null;
|
||||
|
||||
private ?int $limit = null;
|
||||
|
||||
|
||||
@@ -35,7 +44,7 @@ class DataSource implements IDataSource
|
||||
public function __construct(string $sql, Connection $connection)
|
||||
{
|
||||
$this->sql = strpbrk($sql, " \t\r\n") === false
|
||||
? $connection->getDatabaseEngine()->escapeIdentifier($sql) // table name
|
||||
? $connection->getDriver()->escapeIdentifier($sql) // table name
|
||||
: '(' . $sql . ') t'; // SQL command
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ namespace Dibi;
|
||||
*/
|
||||
class DateTime extends \DateTimeImmutable
|
||||
{
|
||||
use Strict;
|
||||
|
||||
public function __construct(string|int $time = 'now', ?\DateTimeZone $timezone = null)
|
||||
{
|
||||
$timezone = $timezone ?: new \DateTimeZone(date_default_timezone_get());
|
||||
|
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi\DriverException;
|
||||
use Dibi\Exception;
|
||||
|
||||
|
||||
/**
|
||||
* Database connection driver.
|
||||
*/
|
||||
interface Connection
|
||||
{
|
||||
/**
|
||||
* Disconnects from a database.
|
||||
* @throws Exception
|
||||
*/
|
||||
function disconnect(): void;
|
||||
|
||||
/**
|
||||
* Internal: Executes the SQL query.
|
||||
* @throws DriverException
|
||||
*/
|
||||
function query(string $sql): ?Result;
|
||||
|
||||
/**
|
||||
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
|
||||
*/
|
||||
function getAffectedRows(): ?int;
|
||||
|
||||
/**
|
||||
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
|
||||
*/
|
||||
function getInsertId(?string $sequence): ?int;
|
||||
|
||||
/**
|
||||
* Begins a transaction (if supported).
|
||||
* @throws DriverException
|
||||
*/
|
||||
function begin(?string $savepoint = null): void;
|
||||
|
||||
/**
|
||||
* Commits statements in a transaction.
|
||||
* @throws DriverException
|
||||
*/
|
||||
function commit(?string $savepoint = null): void;
|
||||
|
||||
/**
|
||||
* Rollback changes in a transaction.
|
||||
* @throws DriverException
|
||||
*/
|
||||
function rollback(?string $savepoint = null): void;
|
||||
|
||||
/**
|
||||
* Returns the connection resource.
|
||||
*/
|
||||
function getResource(): mixed;
|
||||
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
function getReflector(): Engine;
|
||||
|
||||
/**
|
||||
* Encodes data for use in a SQL statement.
|
||||
*/
|
||||
function escapeText(string $value): string;
|
||||
|
||||
function escapeBinary(string $value): string;
|
||||
}
|
@@ -7,23 +7,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Dummy;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
|
||||
|
||||
/**
|
||||
* The dummy driver for testing purposes.
|
||||
*/
|
||||
class Connection implements Drivers\Connection, Drivers\Result, Drivers\Engine
|
||||
class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -65,7 +66,7 @@ class Connection implements Drivers\Connection, Drivers\Result, Drivers\Engine
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return $this;
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
|
||||
/**
|
||||
* Engine-specific behaviors.
|
||||
*/
|
||||
interface Engine
|
||||
{
|
||||
function escapeIdentifier(string $value): string;
|
||||
|
||||
function escapeBool(bool $value): string;
|
||||
|
||||
function escapeDate(\DateTimeInterface $value): string;
|
||||
|
||||
function escapeDateTime(\DateTimeInterface $value): string;
|
||||
|
||||
function escapeDateInterval(\DateInterval $value): string;
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
function escapeLike(string $value, int $pos): string;
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
function applyLimit(string &$sql, ?int $limit, ?int $offset): void;
|
||||
|
||||
/**
|
||||
* Returns list of tables.
|
||||
* @return array of {name [, (bool) view ]}
|
||||
*/
|
||||
function getTables(): array;
|
||||
|
||||
/**
|
||||
* Returns metadata for all columns in a table.
|
||||
* @return array of {name, nativetype [, table, fullname, (int) size, (bool) nullable, (mixed) default, (bool) autoincrement, (array) vendor ]}
|
||||
*/
|
||||
function getColumns(string $table): array;
|
||||
|
||||
/**
|
||||
* Returns metadata for all indexes in a table.
|
||||
* @return array of {name, (array of names) columns [, (bool) unique, (bool) primary ]}
|
||||
*/
|
||||
function getIndexes(string $table): array;
|
||||
|
||||
/**
|
||||
* Returns metadata for all foreign keys in a table.
|
||||
*/
|
||||
function getForeignKeys(string $table): array;
|
||||
}
|
@@ -1,153 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Engines;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers\Connection;
|
||||
use Dibi\Drivers\Engine;
|
||||
|
||||
|
||||
/**
|
||||
* The reflector for Oracle database.
|
||||
*/
|
||||
class OracleEngine implements Engine
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
// @see http://download.oracle.com/docs/cd/B10500_01/server.920/a96540/sql_elements9a.htm
|
||||
return '"' . str_replace('"', '""', $value) . '"';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $this->nativeDate // TODO
|
||||
? "to_date('" . $value->format('Y-m-d') . "', 'YYYY-mm-dd')"
|
||||
: $value->format('U');
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $this->nativeDate // TODO
|
||||
? "to_date('" . $value->format('Y-m-d G:i:s') . "', 'YYYY-mm-dd hh24:mi:ss')"
|
||||
: $value->format('U');
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_");
|
||||
$value = str_replace("'", "''", $value);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($offset) {
|
||||
// see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html
|
||||
$sql = 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (' . $sql . ') t '
|
||||
. ($limit !== null ? 'WHERE ROWNUM <= ' . ($offset + $limit) : '')
|
||||
. ') WHERE "__rnum" > ' . $offset;
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns list of tables.
|
||||
*/
|
||||
public function getTables(): array
|
||||
{
|
||||
$res = $this->driver->query('SELECT * FROM cat');
|
||||
$tables = [];
|
||||
while ($row = $res->fetch(false)) {
|
||||
if ($row[1] === 'TABLE' || $row[1] === 'VIEW') {
|
||||
$tables[] = [
|
||||
'name' => $row[0],
|
||||
'view' => $row[1] === 'VIEW',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns metadata for all columns in a table.
|
||||
*/
|
||||
public function getColumns(string $table): array
|
||||
{
|
||||
$res = $this->driver->query('SELECT * FROM "ALL_TAB_COLUMNS" WHERE "TABLE_NAME" = ' . $this->driver->escapeText($table));
|
||||
$columns = [];
|
||||
while ($row = $res->fetch(true)) {
|
||||
$columns[] = [
|
||||
'table' => $row['TABLE_NAME'],
|
||||
'name' => $row['COLUMN_NAME'],
|
||||
'nativetype' => $row['DATA_TYPE'],
|
||||
'size' => $row['DATA_LENGTH'] ?? null,
|
||||
'nullable' => $row['NULLABLE'] === 'Y',
|
||||
'default' => $row['DATA_DEFAULT'],
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns metadata for all indexes in a table.
|
||||
*/
|
||||
public function getIndexes(string $table): array
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns metadata for all foreign keys in a table.
|
||||
*/
|
||||
public function getForeignKeys(string $table): array
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
}
|
@@ -7,12 +7,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Ibase;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use Dibi\Helpers;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -26,18 +24,18 @@ use function is_resource;
|
||||
* - buffers (int) => buffers is the number of database buffers to allocate for the server-side cache. If 0 or omitted, server chooses its own default.
|
||||
* - resource (resource) => existing connection resource
|
||||
*/
|
||||
class Connection implements Drivers\Connection
|
||||
class FirebirdDriver implements Dibi\Driver
|
||||
{
|
||||
public const ErrorExceptionThrown = -836;
|
||||
use Dibi\Strict;
|
||||
|
||||
#[\Deprecated('use FirebirdDriver::ErrorExceptionThrown')]
|
||||
public const ERROR_EXCEPTION_THROWN = self::ErrorExceptionThrown;
|
||||
public const ERROR_EXCEPTION_THROWN = -836;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
/** @var ?resource */
|
||||
private $transaction;
|
||||
|
||||
private bool $inTransaction = false;
|
||||
|
||||
|
||||
@@ -87,7 +85,7 @@ class Connection implements Drivers\Connection
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException|Dibi\Exception
|
||||
*/
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
$resource = $this->inTransaction
|
||||
? $this->transaction
|
||||
@@ -201,9 +199,9 @@ class Connection implements Drivers\Connection
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return new Drivers\Engines\FirebirdEngine($this);
|
||||
return new FirebirdReflector($this);
|
||||
}
|
||||
|
||||
|
||||
@@ -211,9 +209,9 @@ class Connection implements Drivers\Connection
|
||||
* Result set driver factory.
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function createResultDriver($resource): Result
|
||||
public function createResultDriver($resource): FirebirdResult
|
||||
{
|
||||
return new Result($resource);
|
||||
return new FirebirdResult($resource);
|
||||
}
|
||||
|
||||
|
||||
@@ -233,4 +231,56 @@ class Connection implements Drivers\Connection
|
||||
{
|
||||
return "'" . str_replace("'", "''", $value) . "'";
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
return '"' . str_replace('"', '""', $value) . '"';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return "'" . substr($value->format('Y-m-d H:i:s.u'), 0, -2) . "'";
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = addcslashes($this->escapeText($value), '%_\\');
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit > 0 || $offset > 0) {
|
||||
// http://www.firebirdsql.org/refdocs/langrefupd20-select.html
|
||||
$sql = 'SELECT ' . ($limit > 0 ? 'FIRST ' . $limit : '') . ($offset > 0 ? ' SKIP ' . $offset : '') . ' * FROM (' . $sql . ')';
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,73 +7,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Engines;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers\Connection;
|
||||
use Dibi\Drivers\Engine;
|
||||
|
||||
|
||||
/**
|
||||
* The reflector for Firebird/InterBase database.
|
||||
*/
|
||||
class FirebirdEngine implements Engine
|
||||
class FirebirdReflector implements Dibi\Reflector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $driver,
|
||||
) {
|
||||
}
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Driver $driver;
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
return '"' . str_replace('"', '""', $value) . '"';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return "'" . substr($value->format('Y-m-d H:i:s.u'), 0, -2) . "'";
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = addcslashes($this->driver->escapeText($value), '%_\\');
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit > 0 || $offset > 0) {
|
||||
// http://www.firebirdsql.org/refdocs/langrefupd20-select.html
|
||||
$sql = 'SELECT ' . ($limit > 0 ? 'FIRST ' . $limit : '') . ($offset > 0 ? ' SKIP ' . $offset : '') . ' * FROM (' . $sql . ')';
|
||||
}
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
|
@@ -7,23 +7,42 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Ibase;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use Dibi\Helpers;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
* The driver for Firebird/InterBase result set.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class FirebirdResult implements Dibi\ResultDriver
|
||||
{
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private $resultSet,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
private bool $autoFree = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +66,7 @@ class Result implements Drivers\Result
|
||||
: @ibase_fetch_row($this->resultSet, IBASE_TEXT); // intentionally @
|
||||
|
||||
if (ibase_errcode()) {
|
||||
if (ibase_errcode() === Connection::ERROR_EXCEPTION_THROWN) {
|
||||
if (ibase_errcode() === FirebirdDriver::ERROR_EXCEPTION_THROWN) {
|
||||
preg_match('/exception (\d+) (\w+) (.*)/is', ibase_errmsg(), $match);
|
||||
throw new Dibi\ProcedureException($match[3], $match[1], $match[2]);
|
||||
|
||||
@@ -85,6 +104,7 @@ class Result implements Drivers\Result
|
||||
*/
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
}
|
||||
|
@@ -7,82 +7,25 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Engines;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers\Connection;
|
||||
use Dibi\Drivers\Engine;
|
||||
|
||||
|
||||
/**
|
||||
* The reflector for MySQL databases.
|
||||
* @internal
|
||||
*/
|
||||
class MySQLEngine implements Engine
|
||||
class MySqlReflector implements Dibi\Reflector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $driver,
|
||||
) {
|
||||
}
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Driver $driver;
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
return '`' . str_replace('`', '``', $value) . '`';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d H:i:s.u'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
if ($value->y || $value->m || $value->d) {
|
||||
throw new Dibi\NotSupportedException('Only time interval is supported.');
|
||||
}
|
||||
|
||||
return $value->format("'%r%H:%I:%S.%f'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null || $offset) {
|
||||
// see http://dev.mysql.com/doc/refman/5.0/en/select.html
|
||||
$sql .= ' LIMIT ' . ($limit ?? '18446744073709551615')
|
||||
. ($offset ? ' OFFSET ' . $offset : '');
|
||||
}
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +52,7 @@ class MySQLEngine implements Engine
|
||||
*/
|
||||
public function getColumns(string $table): array
|
||||
{
|
||||
$res = $this->driver->query("SHOW FULL COLUMNS FROM {$this->escapeIdentifier($table)}");
|
||||
$res = $this->driver->query("SHOW FULL COLUMNS FROM {$this->driver->escapeIdentifier($table)}");
|
||||
$columns = [];
|
||||
while ($row = $res->fetch(true)) {
|
||||
$type = explode('(', $row['Type']);
|
||||
@@ -134,7 +77,7 @@ class MySQLEngine implements Engine
|
||||
*/
|
||||
public function getIndexes(string $table): array
|
||||
{
|
||||
$res = $this->driver->query("SHOW INDEX FROM {$this->escapeIdentifier($table)}");
|
||||
$res = $this->driver->query("SHOW INDEX FROM {$this->driver->escapeIdentifier($table)}");
|
||||
$indexes = [];
|
||||
while ($row = $res->fetch(true)) {
|
||||
$indexes[$row['Key_name']]['name'] = $row['Key_name'];
|
@@ -7,12 +7,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\MySQLi;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use function in_array;
|
||||
use const MYSQLI_REPORT_OFF, MYSQLI_STORE_RESULT, MYSQLI_USE_RESULT, PREG_SET_ORDER;
|
||||
|
||||
|
||||
/**
|
||||
@@ -33,22 +30,19 @@ use const MYSQLI_REPORT_OFF, MYSQLI_STORE_RESULT, MYSQLI_USE_RESULT, PREG_SET_OR
|
||||
* - sqlmode => see http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html
|
||||
* - resource (mysqli) => existing connection resource
|
||||
*/
|
||||
class Connection implements Drivers\Connection
|
||||
class MySqliDriver implements Dibi\Driver
|
||||
{
|
||||
public const ErrorAccessDenied = 1045;
|
||||
public const ErrorDuplicateEntry = 1062;
|
||||
public const ErrorDataTruncated = 1265;
|
||||
use Dibi\Strict;
|
||||
|
||||
#[\Deprecated('use MySqliDriver::ErrorAccessDenied')]
|
||||
public const ERROR_ACCESS_DENIED = self::ErrorAccessDenied;
|
||||
public const ERROR_ACCESS_DENIED = 1045;
|
||||
|
||||
#[\Deprecated('use MySqliDriver::ErrorDuplicateEntry')]
|
||||
public const ERROR_DUPLICATE_ENTRY = self::ErrorDuplicateEntry;
|
||||
public const ERROR_DUPLICATE_ENTRY = 1062;
|
||||
|
||||
#[\Deprecated('use MySqliDriver::ErrorDataTruncated')]
|
||||
public const ERROR_DATA_TRUNCATED = self::ErrorDataTruncated;
|
||||
public const ERROR_DATA_TRUNCATED = 1265;
|
||||
|
||||
private \mysqli $connection;
|
||||
|
||||
/** Is buffered (seekable and countable)? */
|
||||
private bool $buffered = false;
|
||||
|
||||
|
||||
@@ -149,7 +143,7 @@ class Connection implements Drivers\Connection
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
$res = @$this->connection->query($sql, $this->buffered ? MYSQLI_STORE_RESULT : MYSQLI_USE_RESULT); // intentionally @
|
||||
|
||||
@@ -266,18 +260,18 @@ class Connection implements Drivers\Connection
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return new Drivers\Engines\MySQLEngine($this);
|
||||
return new MySqlReflector($this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Result set driver factory.
|
||||
*/
|
||||
public function createResultDriver(\mysqli_result $result): Result
|
||||
public function createResultDriver(\mysqli_result $result): MySqliResult
|
||||
{
|
||||
return new Result($result, $this->buffered);
|
||||
return new MySqliResult($result, $this->buffered);
|
||||
}
|
||||
|
||||
|
||||
@@ -297,4 +291,64 @@ class Connection implements Drivers\Connection
|
||||
{
|
||||
return "_binary'" . $this->connection->escape_string($value) . "'";
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
return '`' . str_replace('`', '``', $value) . '`';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d H:i:s.u'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
if ($value->y || $value->m || $value->d) {
|
||||
throw new Dibi\NotSupportedException('Only time interval is supported.');
|
||||
}
|
||||
|
||||
return $value->format("'%r%H:%I:%S.%f'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null || $offset) {
|
||||
// see http://dev.mysql.com/doc/refman/5.0/en/select.html
|
||||
$sql .= ' LIMIT ' . ($limit ?? '18446744073709551615')
|
||||
. ($offset ? ' OFFSET ' . $offset : '');
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,22 +7,41 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\MySQLi;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use const MYSQLI_TYPE_LONG, MYSQLI_TYPE_SHORT, MYSQLI_TYPE_TIME, MYSQLI_TYPE_TINY;
|
||||
|
||||
|
||||
/**
|
||||
* The driver for MySQL result set.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class MySqliResult implements Dibi\ResultDriver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly \mysqli_result $resultSet,
|
||||
private readonly bool $buffered,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
private \mysqli_result $resultSet;
|
||||
|
||||
private bool $autoFree = true;
|
||||
|
||||
/** Is buffered (seekable and countable)? */
|
||||
private bool $buffered;
|
||||
|
||||
|
||||
public function __construct(\mysqli_result $resultSet, bool $buffered)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
$this->buffered = $buffered;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
@$this->free();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +120,7 @@ class Result implements Drivers\Result
|
||||
'table' => $row['orgtable'],
|
||||
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
|
||||
'nativetype' => $types[$row['type']] ?? $row['type'],
|
||||
'type' => $row['type'] === MYSQLI_TYPE_TIME ? Dibi\Type::TimeInterval : null,
|
||||
'type' => $row['type'] === MYSQLI_TYPE_TIME ? Dibi\Type::TIME_INTERVAL : null,
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
@@ -115,6 +134,7 @@ class Result implements Drivers\Result
|
||||
*/
|
||||
public function getResultResource(): \mysqli_result
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return $this->resultSet;
|
||||
}
|
||||
|
@@ -7,19 +7,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Dummy;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi\Drivers;
|
||||
use Dibi;
|
||||
|
||||
|
||||
/**
|
||||
* The driver for no result set.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class NoDataResult implements Dibi\ResultDriver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly int $rows,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
private int $rows;
|
||||
|
||||
|
||||
public function __construct(int $rows)
|
||||
{
|
||||
$this->rows = $rows;
|
||||
}
|
||||
|
||||
|
@@ -7,11 +7,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\ODBC;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -25,11 +23,15 @@ use function is_resource;
|
||||
* - resource (resource) => existing connection resource
|
||||
* - microseconds (bool) => use microseconds in datetime format?
|
||||
*/
|
||||
class Connection implements Drivers\Connection
|
||||
class OdbcDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
private ?int $affectedRows;
|
||||
|
||||
private bool $microseconds = true;
|
||||
|
||||
|
||||
@@ -78,7 +80,7 @@ class Connection implements Drivers\Connection
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
$this->affectedRows = null;
|
||||
$res = @odbc_exec($this->connection, $sql); // intentionally @
|
||||
@@ -121,7 +123,7 @@ class Connection implements Drivers\Connection
|
||||
*/
|
||||
public function begin(?string $savepoint = null): void
|
||||
{
|
||||
if (!odbc_autocommit($this->connection)) {
|
||||
if (!odbc_autocommit($this->connection, false)) {
|
||||
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
|
||||
}
|
||||
}
|
||||
@@ -177,9 +179,9 @@ class Connection implements Drivers\Connection
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return new Drivers\Engines\ODBCEngine($this);
|
||||
return new OdbcReflector($this);
|
||||
}
|
||||
|
||||
|
||||
@@ -187,9 +189,9 @@ class Connection implements Drivers\Connection
|
||||
* Result set driver factory.
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function createResultDriver($resource): Result
|
||||
public function createResultDriver($resource): OdbcResult
|
||||
{
|
||||
return new Result($resource);
|
||||
return new OdbcResult($resource);
|
||||
}
|
||||
|
||||
|
||||
@@ -209,4 +211,61 @@ class Connection implements Drivers\Connection
|
||||
{
|
||||
return "'" . str_replace("'", "''", $value) . "'";
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format('#m/d/Y#');
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format($this->microseconds ? '#m/d/Y H:i:s.u#' : '#m/d/Y H:i:s#');
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($offset) {
|
||||
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
|
||||
|
||||
} elseif ($limit < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = 'SELECT TOP ' . $limit . ' * FROM (' . $sql . ') t';
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,78 +7,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Engines;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers\Connection;
|
||||
use Dibi\Drivers\Engine;
|
||||
|
||||
|
||||
/**
|
||||
* The reflector for ODBC connections.
|
||||
*/
|
||||
class ODBCEngine implements Engine
|
||||
class OdbcReflector implements Dibi\Reflector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $driver,
|
||||
) {
|
||||
}
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Driver $driver;
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format('#m/d/Y#');
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format($this->microseconds ? '#m/d/Y H:i:s.u#' : '#m/d/Y H:i:s#'); // TODO
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($offset) {
|
||||
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
|
||||
|
||||
} elseif ($limit < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = 'SELECT TOP ' . $limit . ' * FROM (' . $sql . ') t';
|
||||
}
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
|
@@ -7,25 +7,43 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\ODBC;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
* The driver interacting with result set via ODBC connections.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class OdbcResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
private bool $autoFree = true;
|
||||
|
||||
private int $row = 0;
|
||||
|
||||
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private $resultSet,
|
||||
) {
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +127,7 @@ class Result implements Drivers\Result
|
||||
*/
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
}
|
||||
|
@@ -7,11 +7,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\OCI8;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use function in_array, is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -27,12 +25,18 @@ use function in_array, is_resource;
|
||||
* - resource (resource) => existing connection resource
|
||||
* - persistent => Creates persistent connections with oci_pconnect instead of oci_new_connect
|
||||
*/
|
||||
class Connection implements Drivers\Connection
|
||||
class OracleDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
private bool $autocommit = true;
|
||||
|
||||
/** use native datetime format */
|
||||
private bool $nativeDate;
|
||||
|
||||
private ?int $affectedRows;
|
||||
|
||||
|
||||
@@ -78,7 +82,7 @@ class Connection implements Drivers\Connection
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
$this->affectedRows = null;
|
||||
$res = oci_parse($this->connection, $sql);
|
||||
@@ -191,9 +195,9 @@ class Connection implements Drivers\Connection
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return new Drivers\Engines\OracleEngine($this);
|
||||
return new OracleReflector($this);
|
||||
}
|
||||
|
||||
|
||||
@@ -201,9 +205,9 @@ class Connection implements Drivers\Connection
|
||||
* Result set driver factory.
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function createResultDriver($resource): Result
|
||||
public function createResultDriver($resource): OracleResult
|
||||
{
|
||||
return new Result($resource);
|
||||
return new OracleResult($resource);
|
||||
}
|
||||
|
||||
|
||||
@@ -223,4 +227,70 @@ class Connection implements Drivers\Connection
|
||||
{
|
||||
return "'" . str_replace("'", "''", $value) . "'"; // TODO: not tested
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
// @see http://download.oracle.com/docs/cd/B10500_01/server.920/a96540/sql_elements9a.htm
|
||||
return '"' . str_replace('"', '""', $value) . '"';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $this->nativeDate
|
||||
? "to_date('" . $value->format('Y-m-d') . "', 'YYYY-mm-dd')"
|
||||
: $value->format('U');
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $this->nativeDate
|
||||
? "to_date('" . $value->format('Y-m-d G:i:s') . "', 'YYYY-mm-dd hh24:mi:ss')"
|
||||
: $value->format('U');
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_");
|
||||
$value = str_replace("'", "''", $value);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($offset) {
|
||||
// see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html
|
||||
$sql = 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (' . $sql . ') t '
|
||||
. ($limit !== null ? 'WHERE ROWNUM <= ' . ($offset + $limit) : '')
|
||||
. ') WHERE "__rnum" > ' . $offset;
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit;
|
||||
}
|
||||
}
|
||||
}
|
90
src/Dibi/Drivers/OracleReflector.php
Normal file
90
src/Dibi/Drivers/OracleReflector.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
|
||||
|
||||
/**
|
||||
* The reflector for Oracle database.
|
||||
*/
|
||||
class OracleReflector implements Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Driver $driver;
|
||||
|
||||
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns list of tables.
|
||||
*/
|
||||
public function getTables(): array
|
||||
{
|
||||
$res = $this->driver->query('SELECT * FROM cat');
|
||||
$tables = [];
|
||||
while ($row = $res->fetch(false)) {
|
||||
if ($row[1] === 'TABLE' || $row[1] === 'VIEW') {
|
||||
$tables[] = [
|
||||
'name' => $row[0],
|
||||
'view' => $row[1] === 'VIEW',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns metadata for all columns in a table.
|
||||
*/
|
||||
public function getColumns(string $table): array
|
||||
{
|
||||
$res = $this->driver->query('SELECT * FROM "ALL_TAB_COLUMNS" WHERE "TABLE_NAME" = ' . $this->driver->escapeText($table));
|
||||
$columns = [];
|
||||
while ($row = $res->fetch(true)) {
|
||||
$columns[] = [
|
||||
'table' => $row['TABLE_NAME'],
|
||||
'name' => $row['COLUMN_NAME'],
|
||||
'nativetype' => $row['DATA_TYPE'],
|
||||
'size' => $row['DATA_LENGTH'] ?? null,
|
||||
'nullable' => $row['NULLABLE'] === 'Y',
|
||||
'default' => $row['DATA_DEFAULT'],
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns metadata for all indexes in a table.
|
||||
*/
|
||||
public function getIndexes(string $table): array
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns metadata for all foreign keys in a table.
|
||||
*/
|
||||
public function getForeignKeys(string $table): array
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
}
|
@@ -7,22 +7,41 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\OCI8;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
* The driver for Oracle result set.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class OracleResult implements Dibi\ResultDriver
|
||||
{
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private $resultSet,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
private bool $autoFree = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +95,7 @@ class Result implements Drivers\Result
|
||||
'name' => oci_field_name($this->resultSet, $i),
|
||||
'table' => null,
|
||||
'fullname' => oci_field_name($this->resultSet, $i),
|
||||
'type' => $type === 'LONG' ? Dibi\Type::Text : null,
|
||||
'type' => $type === 'LONG' ? Dibi\Type::TEXT : null,
|
||||
'nativetype' => $type === 'NUMBER' && oci_field_scale($this->resultSet, $i) === 0 ? 'INTEGER' : $type,
|
||||
];
|
||||
}
|
||||
@@ -91,6 +110,7 @@ class Result implements Drivers\Result
|
||||
*/
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
}
|
||||
|
@@ -1,223 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\PDO;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use Dibi\Drivers\Engines;
|
||||
use Dibi\Helpers;
|
||||
use PDO;
|
||||
use function sprintf;
|
||||
|
||||
|
||||
/**
|
||||
* The driver for PDO.
|
||||
*
|
||||
* Driver options:
|
||||
* - dsn => driver specific DSN
|
||||
* - username (or user)
|
||||
* - password (or pass)
|
||||
* - options (array) => driver specific options {@see PDO::__construct}
|
||||
* - resource (PDO) => existing connection
|
||||
*/
|
||||
class Connection implements Drivers\Connection
|
||||
{
|
||||
private ?PDO $connection;
|
||||
private ?int $affectedRows;
|
||||
private string $driverName;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
public function __construct(array $config)
|
||||
{
|
||||
if (!extension_loaded('pdo')) {
|
||||
throw new Dibi\NotSupportedException("PHP extension 'pdo' is not loaded.");
|
||||
}
|
||||
|
||||
$foo = &$config['dsn'];
|
||||
$foo = &$config['options'];
|
||||
Helpers::alias($config, 'resource', 'pdo');
|
||||
|
||||
if ($config['resource'] instanceof PDO) {
|
||||
$this->connection = $config['resource'];
|
||||
unset($config['resource'], $config['pdo']);
|
||||
|
||||
if ($this->connection->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_SILENT) {
|
||||
throw new Dibi\DriverException('PDO connection in exception or warning error mode is not supported.');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$this->connection = new PDO($config['dsn'], $config['username'], $config['password'], $config['options']);
|
||||
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getMessage() === 'could not find driver') {
|
||||
throw new Dibi\NotSupportedException('PHP extension for PDO is not loaded.');
|
||||
}
|
||||
|
||||
throw new Dibi\DriverException($e->getMessage(), $e->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
$this->driverName = $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnects from a database.
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function query(string $sql): ?Result
|
||||
{
|
||||
$res = $this->connection->query($sql);
|
||||
if ($res) {
|
||||
$this->affectedRows = $res->rowCount();
|
||||
return $res->columnCount() ? $this->createResultDriver($res) : null;
|
||||
}
|
||||
|
||||
$this->affectedRows = null;
|
||||
|
||||
[$sqlState, $code, $message] = $this->connection->errorInfo();
|
||||
$code ??= 0;
|
||||
$message = "SQLSTATE[$sqlState]: $message";
|
||||
throw match ($this->driverName) {
|
||||
'mysql' => Drivers\MySQLi\Connection::createException($message, $code, $sql),
|
||||
'oci' => Drivers\OCI8\Connection::createException($message, $code, $sql),
|
||||
'pgsql' => Drivers\PgSQL\Connection::createException($message, $sqlState, $sql),
|
||||
'sqlite' => Drivers\SQLite3\Connection::createException($message, $code, $sql),
|
||||
default => new Dibi\DriverException($message, $code, $sql),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
|
||||
*/
|
||||
public function getAffectedRows(): ?int
|
||||
{
|
||||
return $this->affectedRows;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
|
||||
*/
|
||||
public function getInsertId(?string $sequence): ?int
|
||||
{
|
||||
return Helpers::intVal($this->connection->lastInsertId($sequence));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Begins a transaction (if supported).
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function begin(?string $savepoint = null): void
|
||||
{
|
||||
if (!$this->connection->beginTransaction()) {
|
||||
$err = $this->connection->errorInfo();
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Commits statements in a transaction.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function commit(?string $savepoint = null): void
|
||||
{
|
||||
if (!$this->connection->commit()) {
|
||||
$err = $this->connection->errorInfo();
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rollback changes in a transaction.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function rollback(?string $savepoint = null): void
|
||||
{
|
||||
if (!$this->connection->rollBack()) {
|
||||
$err = $this->connection->errorInfo();
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the connection resource.
|
||||
*/
|
||||
public function getResource(): ?PDO
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
{
|
||||
return match ($this->driverName) {
|
||||
'mysql' => new Engines\MySQLEngine($this),
|
||||
'oci' => new Engines\OracleEngine($this),
|
||||
'pgsql' => new Engines\PostgreSQLEngine($this),
|
||||
'sqlite' => new Engines\SQLiteEngine($this),
|
||||
'mssql', 'dblib', 'sqlsrv' => new Engines\SQLServerEngine($this),
|
||||
default => throw new Dibi\NotSupportedException,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Result set driver factory.
|
||||
*/
|
||||
public function createResultDriver(\PDOStatement $result): Result
|
||||
{
|
||||
return new Result($result, $this->driverName);
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Encodes data for use in a SQL statement.
|
||||
*/
|
||||
public function escapeText(string $value): string
|
||||
{
|
||||
return match ($this->driverName) {
|
||||
'odbc' => "'" . str_replace("'", "''", $value) . "'",
|
||||
'sqlsrv' => "N'" . str_replace("'", "''", $value) . "'",
|
||||
default => $this->connection->quote($value, PDO::PARAM_STR),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public function escapeBinary(string $value): string
|
||||
{
|
||||
return match ($this->driverName) {
|
||||
'odbc' => "'" . str_replace("'", "''", $value) . "'",
|
||||
'sqlsrv' => '0x' . bin2hex($value),
|
||||
default => $this->connection->quote($value, PDO::PARAM_LOB),
|
||||
};
|
||||
}
|
||||
}
|
427
src/Dibi/Drivers/PdoDriver.php
Normal file
427
src/Dibi/Drivers/PdoDriver.php
Normal file
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use PDO;
|
||||
|
||||
|
||||
/**
|
||||
* The driver for PDO.
|
||||
*
|
||||
* Driver options:
|
||||
* - dsn => driver specific DSN
|
||||
* - username (or user)
|
||||
* - password (or pass)
|
||||
* - options (array) => driver specific options {@see PDO::__construct}
|
||||
* - resource (PDO) => existing connection
|
||||
* - version
|
||||
*/
|
||||
class PdoDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
private ?PDO $connection;
|
||||
|
||||
private ?int $affectedRows;
|
||||
|
||||
private string $driverName;
|
||||
|
||||
private string $serverVersion = '';
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
public function __construct(array $config)
|
||||
{
|
||||
if (!extension_loaded('pdo')) {
|
||||
throw new Dibi\NotSupportedException("PHP extension 'pdo' is not loaded.");
|
||||
}
|
||||
|
||||
$foo = &$config['dsn'];
|
||||
$foo = &$config['options'];
|
||||
Helpers::alias($config, 'resource', 'pdo');
|
||||
|
||||
if ($config['resource'] instanceof PDO) {
|
||||
$this->connection = $config['resource'];
|
||||
unset($config['resource'], $config['pdo']);
|
||||
|
||||
if ($this->connection->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_SILENT) {
|
||||
throw new Dibi\DriverException('PDO connection in exception or warning error mode is not supported.');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$this->connection = new PDO($config['dsn'], $config['username'], $config['password'], $config['options']);
|
||||
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getMessage() === 'could not find driver') {
|
||||
throw new Dibi\NotSupportedException('PHP extension for PDO is not loaded.');
|
||||
}
|
||||
|
||||
throw new Dibi\DriverException($e->getMessage(), $e->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
$this->driverName = $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$this->serverVersion = (string) ($config['version'] ?? @$this->connection->getAttribute(PDO::ATTR_SERVER_VERSION)); // @ - may be not supported
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnects from a database.
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
$res = $this->connection->query($sql);
|
||||
if ($res) {
|
||||
$this->affectedRows = $res->rowCount();
|
||||
return $res->columnCount() ? $this->createResultDriver($res) : null;
|
||||
}
|
||||
|
||||
$this->affectedRows = null;
|
||||
|
||||
[$sqlState, $code, $message] = $this->connection->errorInfo();
|
||||
$message = "SQLSTATE[$sqlState]: $message";
|
||||
switch ($this->driverName) {
|
||||
case 'mysql':
|
||||
throw MySqliDriver::createException($message, $code, $sql);
|
||||
|
||||
case 'oci':
|
||||
throw OracleDriver::createException($message, $code, $sql);
|
||||
|
||||
case 'pgsql':
|
||||
throw PostgreDriver::createException($message, $sqlState, $sql);
|
||||
|
||||
case 'sqlite':
|
||||
throw SqliteDriver::createException($message, $code, $sql);
|
||||
|
||||
default:
|
||||
throw new Dibi\DriverException($message, $code, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
|
||||
*/
|
||||
public function getAffectedRows(): ?int
|
||||
{
|
||||
return $this->affectedRows;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
|
||||
*/
|
||||
public function getInsertId(?string $sequence): ?int
|
||||
{
|
||||
return Helpers::intVal($this->connection->lastInsertId($sequence));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Begins a transaction (if supported).
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function begin(?string $savepoint = null): void
|
||||
{
|
||||
if (!$this->connection->beginTransaction()) {
|
||||
$err = $this->connection->errorInfo();
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Commits statements in a transaction.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function commit(?string $savepoint = null): void
|
||||
{
|
||||
if (!$this->connection->commit()) {
|
||||
$err = $this->connection->errorInfo();
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rollback changes in a transaction.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function rollback(?string $savepoint = null): void
|
||||
{
|
||||
if (!$this->connection->rollBack()) {
|
||||
$err = $this->connection->errorInfo();
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the connection resource.
|
||||
*/
|
||||
public function getResource(): ?PDO
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
switch ($this->driverName) {
|
||||
case 'mysql':
|
||||
return new MySqlReflector($this);
|
||||
|
||||
case 'oci':
|
||||
return new OracleReflector($this);
|
||||
|
||||
case 'pgsql':
|
||||
return new PostgreReflector($this, $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION));
|
||||
|
||||
case 'sqlite':
|
||||
return new SqliteReflector($this);
|
||||
|
||||
case 'mssql':
|
||||
case 'dblib':
|
||||
case 'sqlsrv':
|
||||
return new SqlsrvReflector($this);
|
||||
|
||||
default:
|
||||
throw new Dibi\NotSupportedException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Result set driver factory.
|
||||
*/
|
||||
public function createResultDriver(\PDOStatement $result): PdoResult
|
||||
{
|
||||
return new PdoResult($result, $this->driverName);
|
||||
}
|
||||
|
||||
|
||||
/********************* SQL ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Encodes data for use in a SQL statement.
|
||||
*/
|
||||
public function escapeText(string $value): string
|
||||
{
|
||||
return $this->driverName === 'odbc'
|
||||
? "'" . str_replace("'", "''", $value) . "'"
|
||||
: $this->connection->quote($value, PDO::PARAM_STR);
|
||||
}
|
||||
|
||||
|
||||
public function escapeBinary(string $value): string
|
||||
{
|
||||
return $this->driverName === 'odbc'
|
||||
? "'" . str_replace("'", "''", $value) . "'"
|
||||
: $this->connection->quote($value, PDO::PARAM_LOB);
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
switch ($this->driverName) {
|
||||
case 'mysql':
|
||||
return '`' . str_replace('`', '``', $value) . '`';
|
||||
|
||||
case 'oci':
|
||||
case 'pgsql':
|
||||
return '"' . str_replace('"', '""', $value) . '"';
|
||||
|
||||
case 'sqlite':
|
||||
return '[' . strtr($value, '[]', ' ') . ']';
|
||||
|
||||
case 'odbc':
|
||||
case 'mssql':
|
||||
return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']';
|
||||
|
||||
case 'dblib':
|
||||
case 'sqlsrv':
|
||||
return '[' . str_replace(']', ']]', $value) . ']';
|
||||
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
if ($this->driverName === 'pgsql') {
|
||||
return $value ? 'TRUE' : 'FALSE';
|
||||
} else {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format($this->driverName === 'odbc' ? '#m/d/Y#' : "'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
switch ($this->driverName) {
|
||||
case 'odbc':
|
||||
return $value->format('#m/d/Y H:i:s.u#');
|
||||
case 'mssql':
|
||||
case 'dblib':
|
||||
case 'sqlsrv':
|
||||
return 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')';
|
||||
default:
|
||||
return $value->format("'Y-m-d H:i:s.u'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
switch ($this->driverName) {
|
||||
case 'mysql':
|
||||
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
|
||||
case 'oci':
|
||||
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_");
|
||||
$value = str_replace("'", "''", $value);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
|
||||
case 'pgsql':
|
||||
$bs = substr($this->connection->quote('\\', PDO::PARAM_STR), 1, -1); // standard_conforming_strings = on/off
|
||||
$value = substr($this->connection->quote($value, PDO::PARAM_STR), 1, -1);
|
||||
$value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
|
||||
case 'sqlite':
|
||||
$value = addcslashes(substr($this->connection->quote($value, PDO::PARAM_STR), 1, -1), '%_\\');
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
|
||||
|
||||
case 'odbc':
|
||||
case 'mssql':
|
||||
case 'dblib':
|
||||
case 'sqlsrv':
|
||||
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
|
||||
default:
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
}
|
||||
|
||||
switch ($this->driverName) {
|
||||
case 'mysql':
|
||||
if ($limit !== null || $offset) {
|
||||
// see http://dev.mysql.com/doc/refman/5.0/en/select.html
|
||||
$sql .= ' LIMIT ' . ($limit ?? '18446744073709551615')
|
||||
. ($offset ? ' OFFSET ' . $offset : '');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'pgsql':
|
||||
if ($limit !== null) {
|
||||
$sql .= ' LIMIT ' . $limit;
|
||||
}
|
||||
|
||||
if ($offset) {
|
||||
$sql .= ' OFFSET ' . $offset;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'sqlite':
|
||||
if ($limit !== null || $offset) {
|
||||
$sql .= ' LIMIT ' . ($limit ?? '-1')
|
||||
. ($offset ? ' OFFSET ' . $offset : '');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'oci':
|
||||
if ($offset) {
|
||||
// see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html
|
||||
$sql = 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (' . $sql . ') t '
|
||||
. ($limit !== null ? 'WHERE ROWNUM <= ' . ($offset + $limit) : '')
|
||||
. ') WHERE "__rnum" > ' . $offset;
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'mssql':
|
||||
case 'sqlsrv':
|
||||
case 'dblib':
|
||||
if (version_compare($this->serverVersion, '11.0') >= 0) { // 11 == SQL Server 2012
|
||||
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
|
||||
if ($limit !== null) {
|
||||
$sql = sprintf('%s OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', rtrim($sql), $offset, $limit);
|
||||
} elseif ($offset) {
|
||||
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// break omitted
|
||||
case 'odbc':
|
||||
if ($offset) {
|
||||
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = 'SELECT TOP ' . $limit . ' * FROM (' . $sql . ') t';
|
||||
break;
|
||||
}
|
||||
// break omitted
|
||||
default:
|
||||
throw new Dibi\NotSupportedException('PDO or driver does not support applying limit or offset.');
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,10 +7,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\PDO;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use Dibi\Helpers;
|
||||
use PDO;
|
||||
|
||||
@@ -18,12 +17,19 @@ use PDO;
|
||||
/**
|
||||
* The driver for PDO result set.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class PdoResult implements Dibi\ResultDriver
|
||||
{
|
||||
public function __construct(
|
||||
private ?\PDOStatement $resultSet,
|
||||
private readonly string $driverName,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
private ?\PDOStatement $resultSet;
|
||||
|
||||
private string $driverName;
|
||||
|
||||
|
||||
public function __construct(\PDOStatement $resultSet, string $driverName)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
$this->driverName = $driverName;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +93,7 @@ class Result implements Drivers\Result
|
||||
'name' => $row['name'],
|
||||
'table' => $row['table'],
|
||||
'nativetype' => $row['native_type'],
|
||||
'type' => $row['native_type'] === 'TIME' && $this->driverName === 'mysql' ? Dibi\Type::TimeInterval : null,
|
||||
'type' => $row['native_type'] === 'TIME' && $this->driverName === 'mysql' ? Dibi\Type::TIME_INTERVAL : null,
|
||||
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
|
||||
'vendor' => $row,
|
||||
];
|
@@ -7,13 +7,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\PgSQL;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use Dibi\Helpers;
|
||||
use PgSql;
|
||||
use function in_array, is_array, is_resource, strlen;
|
||||
|
||||
|
||||
/**
|
||||
@@ -25,12 +23,16 @@ use function in_array, is_array, is_resource, strlen;
|
||||
* - schema => the schema search path
|
||||
* - charset => character encoding to set (default is utf8)
|
||||
* - persistent (bool) => try to find a persistent link?
|
||||
* - resource (PgSql\Connection) => existing connection resource
|
||||
* - resource (resource) => existing connection resource
|
||||
* - connect_type (int) => see pg_connect()
|
||||
*/
|
||||
class Connection implements Drivers\Connection
|
||||
class PostgreDriver implements Dibi\Driver
|
||||
{
|
||||
private PgSql\Connection $connection;
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource|PgSql\Connection */
|
||||
private $connection;
|
||||
|
||||
private ?int $affectedRows;
|
||||
|
||||
|
||||
@@ -73,7 +75,7 @@ class Connection implements Drivers\Connection
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
if (!$this->connection instanceof PgSql\Connection) {
|
||||
if (!is_resource($this->connection) && !$this->connection instanceof PgSql\Connection) {
|
||||
throw new Dibi\DriverException($error ?: 'Connecting error.');
|
||||
}
|
||||
|
||||
@@ -111,7 +113,7 @@ class Connection implements Drivers\Connection
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
$this->affectedRows = null;
|
||||
$res = @pg_query($this->connection, $sql); // intentionally @
|
||||
@@ -119,7 +121,7 @@ class Connection implements Drivers\Connection
|
||||
if ($res === false) {
|
||||
throw static::createException(pg_last_error($this->connection), null, $sql);
|
||||
|
||||
} elseif ($res instanceof PgSql\Result) {
|
||||
} elseif (is_resource($res) || $res instanceof PgSql\Result) {
|
||||
$this->affectedRows = Helpers::false2Null(pg_affected_rows($res));
|
||||
if (pg_num_fields($res)) {
|
||||
return $this->createResultDriver($res);
|
||||
@@ -223,28 +225,32 @@ class Connection implements Drivers\Connection
|
||||
|
||||
/**
|
||||
* Returns the connection resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResource(): PgSql\Connection
|
||||
public function getResource(): mixed
|
||||
{
|
||||
return $this->connection;
|
||||
return is_resource($this->connection) || $this->connection instanceof PgSql\Connection
|
||||
? $this->connection
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return new Drivers\Engines\PostgreSQLEngine($this);
|
||||
return new PostgreReflector($this, pg_parameter_status($this->connection, 'server_version'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Result set driver factory.
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function createResultDriver(PgSql\Result $resource): Result
|
||||
public function createResultDriver($resource): PostgreResult
|
||||
{
|
||||
return new Result($resource);
|
||||
return new PostgreResult($resource);
|
||||
}
|
||||
|
||||
|
||||
@@ -272,4 +278,66 @@ class Connection implements Drivers\Connection
|
||||
|
||||
return "'" . pg_escape_bytea($this->connection, $value) . "'";
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
// @see http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
||||
return '"' . str_replace('"', '""', $value) . '"';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d H:i:s.u'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$bs = pg_escape_string($this->connection, '\\'); // standard_conforming_strings = on/off
|
||||
$value = pg_escape_string($this->connection, $value);
|
||||
$value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
}
|
||||
|
||||
if ($limit !== null) {
|
||||
$sql .= ' LIMIT ' . $limit;
|
||||
}
|
||||
|
||||
if ($offset) {
|
||||
$sql .= ' OFFSET ' . $offset;
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,83 +7,27 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Engines;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers\Connection;
|
||||
use Dibi\Drivers\Engine;
|
||||
|
||||
|
||||
/**
|
||||
* The reflector for PostgreSQL database.
|
||||
*/
|
||||
class PostgreSQLEngine implements Engine
|
||||
class PostgreReflector implements Dibi\Reflector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $driver,
|
||||
) {
|
||||
}
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Driver $driver;
|
||||
|
||||
private string $version;
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
public function __construct(Dibi\Driver $driver, string $version)
|
||||
{
|
||||
// @see http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
||||
return '"' . str_replace('"', '""', $value) . '"';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d H:i:s.u'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$bs = $this->driver->escapeText('\\'); // standard_conforming_strings = on/off
|
||||
$value = $this->driver->escapeText($value);
|
||||
$value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
}
|
||||
|
||||
if ($limit !== null) {
|
||||
$sql .= ' LIMIT ' . $limit;
|
||||
}
|
||||
|
||||
if ($offset) {
|
||||
$sql .= ' OFFSET ' . $offset;
|
||||
}
|
||||
$this->driver = $driver;
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,15 +46,18 @@ class PostgreSQLEngine implements Engine
|
||||
FROM
|
||||
information_schema.tables
|
||||
WHERE
|
||||
table_schema = ANY (current_schemas(false))
|
||||
table_schema = ANY (current_schemas(false))";
|
||||
|
||||
UNION ALL
|
||||
SELECT
|
||||
matviewname, 1
|
||||
FROM
|
||||
pg_matviews
|
||||
WHERE
|
||||
schemaname = ANY (current_schemas(false))";
|
||||
if ($this->version >= 9.3) {
|
||||
$query .= '
|
||||
UNION ALL
|
||||
SELECT
|
||||
matviewname, 1
|
||||
FROM
|
||||
pg_matviews
|
||||
WHERE
|
||||
schemaname = ANY (current_schemas(false))';
|
||||
}
|
||||
|
||||
$res = $this->driver->query($query);
|
||||
$tables = [];
|
||||
@@ -127,7 +74,14 @@ class PostgreSQLEngine implements Engine
|
||||
*/
|
||||
public function getColumns(string $table): array
|
||||
{
|
||||
$_table = $this->driver->escapeText($this->escapeIdentifier($table));
|
||||
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
|
||||
$res = $this->driver->query("
|
||||
SELECT indkey
|
||||
FROM pg_class
|
||||
LEFT JOIN pg_index on pg_class.oid = pg_index.indrelid AND pg_index.indisprimary
|
||||
WHERE pg_class.oid = $_table::regclass
|
||||
");
|
||||
$primary = (int) $res->fetch(true)['indkey'];
|
||||
|
||||
$res = $this->driver->query("
|
||||
SELECT *
|
||||
@@ -147,8 +101,7 @@ class PostgreSQLEngine implements Engine
|
||||
a.atttypmod-4 AS character_maximum_length,
|
||||
NOT a.attnotnull AS is_nullable,
|
||||
a.attnum AS ordinal_position,
|
||||
pg_get_expr(adef.adbin, adef.adrelid) AS column_default,
|
||||
CASE WHEN a.attidentity IN ('a', 'd') THEN 'YES' ELSE 'NO' END AS is_identity
|
||||
pg_get_expr(adef.adbin, adef.adrelid) AS column_default
|
||||
FROM
|
||||
pg_attribute a
|
||||
JOIN pg_type ON a.atttypid = pg_type.oid
|
||||
@@ -173,7 +126,7 @@ class PostgreSQLEngine implements Engine
|
||||
'size' => $size > 0 ? $size : null,
|
||||
'nullable' => $row['is_nullable'] === 'YES' || $row['is_nullable'] === 't' || $row['is_nullable'] === true,
|
||||
'default' => $row['column_default'],
|
||||
'autoincrement' => $row['is_identity'] === 'YES' || str_starts_with($row['column_default'] ?? '', 'nextval('),
|
||||
'autoincrement' => (int) $row['ordinal_position'] === $primary && str_starts_with($row['column_default'] ?? '', 'nextval'),
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
@@ -187,7 +140,7 @@ class PostgreSQLEngine implements Engine
|
||||
*/
|
||||
public function getIndexes(string $table): array
|
||||
{
|
||||
$_table = $this->driver->escapeText($this->escapeIdentifier($table));
|
||||
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
|
||||
$res = $this->driver->query("
|
||||
SELECT
|
||||
a.attnum AS ordinal_position,
|
||||
@@ -237,7 +190,7 @@ class PostgreSQLEngine implements Engine
|
||||
*/
|
||||
public function getForeignKeys(string $table): array
|
||||
{
|
||||
$_table = $this->driver->escapeText($this->escapeIdentifier($table));
|
||||
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
|
||||
|
||||
$res = $this->driver->query("
|
||||
SELECT
|
@@ -7,9 +7,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\PgSQL;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi\Drivers;
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use PgSql;
|
||||
|
||||
@@ -17,11 +17,33 @@ use PgSql;
|
||||
/**
|
||||
* The driver for PostgreSQL result set.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class PostgreResult implements Dibi\ResultDriver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PgSql\Result $resultSet,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource|PgSql\Result */
|
||||
private $resultSet;
|
||||
|
||||
private bool $autoFree = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource|PgSql\Result $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,10 +109,14 @@ class Result implements Drivers\Result
|
||||
|
||||
/**
|
||||
* Returns the result set resource.
|
||||
* @return resource|PgSql\Result|null
|
||||
*/
|
||||
public function getResultResource(): PgSql\Result
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
return $this->resultSet;
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) || $this->resultSet instanceof PgSql\Result
|
||||
? $this->resultSet
|
||||
: null;
|
||||
}
|
||||
|
||||
|
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi\Exception;
|
||||
|
||||
|
||||
/**
|
||||
* Database result driver.
|
||||
*/
|
||||
interface Result
|
||||
{
|
||||
/**
|
||||
* Returns the number of rows in a result set.
|
||||
*/
|
||||
function getRowCount(): int;
|
||||
|
||||
/**
|
||||
* Moves cursor position without fetching row.
|
||||
* @throws Exception
|
||||
*/
|
||||
function seek(int $row): bool;
|
||||
|
||||
/**
|
||||
* Fetches the row at current position and moves the internal cursor to the next position.
|
||||
* @param bool $type true for associative array, false for numeric
|
||||
* @internal
|
||||
*/
|
||||
function fetch(bool $type): ?array;
|
||||
|
||||
/**
|
||||
* Frees the resources allocated for this result set.
|
||||
*/
|
||||
function free(): void;
|
||||
|
||||
/**
|
||||
* Returns metadata for all columns in a result set.
|
||||
* @return array of {name, nativetype [, table, fullname, (int) size, (bool) nullable, (mixed) default, (bool) autoincrement, (array) vendor ]}
|
||||
*/
|
||||
function getResultColumns(): array;
|
||||
|
||||
/**
|
||||
* Returns the result set resource.
|
||||
*/
|
||||
function getResultResource(): mixed;
|
||||
|
||||
/**
|
||||
* Decodes data from result set.
|
||||
*/
|
||||
function unescapeBinary(string $value): string;
|
||||
}
|
18
src/Dibi/Drivers/Sqlite3Driver.php
Normal file
18
src/Dibi/Drivers/Sqlite3Driver.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
|
||||
/**
|
||||
* Alias for SqliteDriver driver.
|
||||
*/
|
||||
class Sqlite3Driver extends SqliteDriver
|
||||
{
|
||||
}
|
18
src/Dibi/Drivers/Sqlite3Result.php
Normal file
18
src/Dibi/Drivers/Sqlite3Result.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
|
||||
/**
|
||||
* Alias for SqliteResult driver.
|
||||
*/
|
||||
class Sqlite3Result extends SqliteResult
|
||||
{
|
||||
}
|
@@ -7,10 +7,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\SQLite3;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use Dibi\Helpers;
|
||||
use SQLite3;
|
||||
|
||||
@@ -24,10 +23,14 @@ use SQLite3;
|
||||
* - formatDateTime => how to format datetime in SQL (@see date)
|
||||
* - resource (SQLite3) => existing connection resource
|
||||
*/
|
||||
class Connection implements Drivers\Connection
|
||||
class SqliteDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
private SQLite3 $connection;
|
||||
|
||||
private string $fmtDate;
|
||||
|
||||
private string $fmtDateTime;
|
||||
|
||||
|
||||
@@ -57,7 +60,10 @@ class Connection implements Drivers\Connection
|
||||
}
|
||||
|
||||
// enable foreign keys support (defaultly disabled; if disabled then foreign key constraints are not enforced)
|
||||
$this->query('PRAGMA foreign_keys = ON');
|
||||
$version = SQLite3::version();
|
||||
if ($version['versionNumber'] >= '3006019') {
|
||||
$this->query('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +80,7 @@ class Connection implements Drivers\Connection
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
$res = @$this->connection->query($sql); // intentionally @
|
||||
if ($code = $this->connection->lastErrorCode()) {
|
||||
@@ -175,18 +181,18 @@ class Connection implements Drivers\Connection
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return new Drivers\Engines\SQLiteEngine($this);
|
||||
return new SqliteReflector($this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Result set driver factory.
|
||||
*/
|
||||
public function createResultDriver(\SQLite3Result $result): Result
|
||||
public function createResultDriver(\SQLite3Result $result): SqliteResult
|
||||
{
|
||||
return new Result($result);
|
||||
return new SqliteResult($result);
|
||||
}
|
||||
|
||||
|
||||
@@ -208,6 +214,61 @@ class Connection implements Drivers\Connection
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
return '[' . strtr($value, '[]', ' ') . ']';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format($this->fmtDate);
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format($this->fmtDateTime);
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = addcslashes($this->connection->escapeString($value), '%_\\');
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null || $offset) {
|
||||
$sql .= ' LIMIT ' . ($limit ?? '-1')
|
||||
. ($offset ? ' OFFSET ' . $offset : '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/********************* user defined functions ****************d*g**/
|
||||
|
||||
|
@@ -7,76 +7,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Engines;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers\Connection;
|
||||
use Dibi\Drivers\Engine;
|
||||
|
||||
|
||||
/**
|
||||
* The reflector for SQLite database.
|
||||
*/
|
||||
class SQLiteEngine implements Engine
|
||||
class SqliteReflector implements Dibi\Reflector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $driver,
|
||||
) {
|
||||
}
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Driver $driver;
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
return '[' . strtr($value, '[]', ' ') . ']';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format($this->fmtDate); // TODO
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format($this->fmtDateTime); // TODO
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = addcslashes($this->driver->escapeText($value), '%_\\');
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null || $offset) {
|
||||
$sql .= ' LIMIT ' . ($limit ?? '-1')
|
||||
. ($offset ? ' OFFSET ' . $offset : '');
|
||||
}
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +53,7 @@ class SQLiteEngine implements Engine
|
||||
*/
|
||||
public function getColumns(string $table): array
|
||||
{
|
||||
$res = $this->driver->query("PRAGMA table_info({$this->escapeIdentifier($table)})");
|
||||
$res = $this->driver->query("PRAGMA table_info({$this->driver->escapeIdentifier($table)})");
|
||||
$columns = [];
|
||||
while ($row = $res->fetch(true)) {
|
||||
$column = $row['name'];
|
||||
@@ -132,7 +80,7 @@ class SQLiteEngine implements Engine
|
||||
*/
|
||||
public function getIndexes(string $table): array
|
||||
{
|
||||
$res = $this->driver->query("PRAGMA index_list({$this->escapeIdentifier($table)})");
|
||||
$res = $this->driver->query("PRAGMA index_list({$this->driver->escapeIdentifier($table)})");
|
||||
$indexes = [];
|
||||
while ($row = $res->fetch(true)) {
|
||||
$indexes[$row['name']]['name'] = $row['name'];
|
||||
@@ -140,7 +88,7 @@ class SQLiteEngine implements Engine
|
||||
}
|
||||
|
||||
foreach ($indexes as $index => $values) {
|
||||
$res = $this->driver->query("PRAGMA index_info({$this->escapeIdentifier($index)})");
|
||||
$res = $this->driver->query("PRAGMA index_info({$this->driver->escapeIdentifier($index)})");
|
||||
while ($row = $res->fetch(true)) {
|
||||
$indexes[$index]['columns'][$row['seqno']] = $row['name'];
|
||||
}
|
||||
@@ -183,7 +131,7 @@ class SQLiteEngine implements Engine
|
||||
*/
|
||||
public function getForeignKeys(string $table): array
|
||||
{
|
||||
$res = $this->driver->query("PRAGMA foreign_key_list({$this->escapeIdentifier($table)})");
|
||||
$res = $this->driver->query("PRAGMA foreign_key_list({$this->driver->escapeIdentifier($table)})");
|
||||
$keys = [];
|
||||
while ($row = $res->fetch(true)) {
|
||||
$keys[$row['id']]['name'] = $row['id']; // foreign key name
|
@@ -7,22 +7,38 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\SQLite3;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use Dibi\Helpers;
|
||||
use const SQLITE3_ASSOC, SQLITE3_BLOB, SQLITE3_FLOAT, SQLITE3_INTEGER, SQLITE3_NULL, SQLITE3_NUM, SQLITE3_TEXT;
|
||||
|
||||
|
||||
/**
|
||||
* The driver for SQLite result set.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class SqliteResult implements Dibi\ResultDriver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly \SQLite3Result $resultSet,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
private \SQLite3Result $resultSet;
|
||||
|
||||
private bool $autoFree = true;
|
||||
|
||||
|
||||
public function __construct(\SQLite3Result $resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
@$this->free();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +107,7 @@ class Result implements Drivers\Result
|
||||
*/
|
||||
public function getResultResource(): \SQLite3Result
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return $this->resultSet;
|
||||
}
|
||||
|
@@ -7,12 +7,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\SQLSrv;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use Dibi\Helpers;
|
||||
use function is_resource, sprintf;
|
||||
|
||||
|
||||
/**
|
||||
@@ -27,12 +25,17 @@ use function is_resource, sprintf;
|
||||
* - charset => character encoding to set (default is UTF-8)
|
||||
* - resource (resource) => existing connection resource
|
||||
*/
|
||||
class Connection implements Drivers\Connection
|
||||
class SqlsrvDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
private ?int $affectedRows;
|
||||
|
||||
private string $version = '';
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
public function __construct(array $config)
|
||||
@@ -69,6 +72,8 @@ class Connection implements Drivers\Connection
|
||||
|
||||
sqlsrv_configure('WarningsReturnAsErrors', 1);
|
||||
}
|
||||
|
||||
$this->version = sqlsrv_server_info($this->connection)['SQLServerVersion'];
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +90,7 @@ class Connection implements Drivers\Connection
|
||||
* Executes the SQL query.
|
||||
* @throws Dibi\DriverException
|
||||
*/
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
$this->affectedRows = null;
|
||||
$res = sqlsrv_query($this->connection, $sql);
|
||||
@@ -172,9 +177,9 @@ class Connection implements Drivers\Connection
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
public function getReflector(): Drivers\Engine
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return new Drivers\Engines\SQLServerEngine($this);
|
||||
return new SqlsrvReflector($this);
|
||||
}
|
||||
|
||||
|
||||
@@ -182,9 +187,9 @@ class Connection implements Drivers\Connection
|
||||
* Result set driver factory.
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function createResultDriver($resource): Result
|
||||
public function createResultDriver($resource): SqlsrvResult
|
||||
{
|
||||
return new Result($resource);
|
||||
return new SqlsrvResult($resource);
|
||||
}
|
||||
|
||||
|
||||
@@ -204,4 +209,70 @@ class Connection implements Drivers\Connection
|
||||
{
|
||||
return '0x' . bin2hex($value);
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
// @see https://msdn.microsoft.com/en-us/library/ms176027.aspx
|
||||
return '[' . str_replace(']', ']]', $value) . ']';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif (version_compare($this->version, '11', '<')) { // 11 == SQL Server 2012
|
||||
if ($offset) {
|
||||
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
$sql = sprintf('SELECT TOP (%d) * FROM (%s) t', $limit, $sql);
|
||||
}
|
||||
} elseif ($limit !== null) {
|
||||
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
|
||||
$sql = sprintf('%s OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', rtrim($sql), $offset, $limit);
|
||||
} elseif ($offset) {
|
||||
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
|
||||
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,80 +7,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\Engines;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers\Connection;
|
||||
use Dibi\Drivers\Engine;
|
||||
|
||||
|
||||
/**
|
||||
* The reflector for Microsoft SQL Server and SQL Azure databases.
|
||||
*/
|
||||
class SQLServerEngine implements Engine
|
||||
class SqlsrvReflector implements Dibi\Reflector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $driver,
|
||||
) {
|
||||
}
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Driver $driver;
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
// @see https://msdn.microsoft.com/en-us/library/ms176027.aspx
|
||||
return '[' . str_replace(']', ']]', $value) . ']';
|
||||
}
|
||||
|
||||
|
||||
public function escapeBool(bool $value): string
|
||||
{
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDate(\DateTimeInterface $value): string
|
||||
{
|
||||
return $value->format("'Y-m-d'");
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
return 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')';
|
||||
}
|
||||
|
||||
|
||||
public function escapeDateInterval(\DateInterval $value): string
|
||||
{
|
||||
throw new Dibi\NotImplementedException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
public function escapeLike(string $value, int $pos): string
|
||||
{
|
||||
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
|
||||
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
|
||||
{
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif ($limit !== null) {
|
||||
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
|
||||
$sql = sprintf('%s OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', rtrim($sql), $offset, $limit);
|
||||
} elseif ($offset) {
|
||||
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
|
||||
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
|
||||
}
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
|
@@ -7,22 +7,41 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Drivers\SQLSrv;
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Drivers;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
* The driver for Microsoft SQL Server and SQL Azure result set.
|
||||
*/
|
||||
class Result implements Drivers\Result
|
||||
class SqlsrvResult implements Dibi\ResultDriver
|
||||
{
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private $resultSet,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
private bool $autoFree = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +106,7 @@ class Result implements Drivers\Result
|
||||
*/
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
}
|
||||
|
@@ -9,15 +9,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function count, dirname, microtime, preg_match, str_starts_with, strtoupper, trim;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
|
||||
/**
|
||||
* Profiler & logger event.
|
||||
*/
|
||||
class Event
|
||||
{
|
||||
use Strict;
|
||||
|
||||
/** event type */
|
||||
public const
|
||||
CONNECT = 1,
|
||||
@@ -32,12 +31,18 @@ class Event
|
||||
TRANSACTION = 448, // BEGIN | COMMIT | ROLLBACK
|
||||
ALL = 1023;
|
||||
|
||||
public readonly Connection $connection;
|
||||
public Connection $connection;
|
||||
|
||||
public int $type;
|
||||
public readonly string $sql;
|
||||
public readonly Result|DriverException|null $result;
|
||||
|
||||
public string $sql;
|
||||
|
||||
public Result|DriverException|null $result;
|
||||
|
||||
public float $time;
|
||||
|
||||
public ?int $count = null;
|
||||
|
||||
public ?array $source = null;
|
||||
|
||||
|
||||
|
@@ -15,7 +15,9 @@ namespace Dibi;
|
||||
*/
|
||||
class Expression
|
||||
{
|
||||
private readonly array $values;
|
||||
use Strict;
|
||||
|
||||
private array $values;
|
||||
|
||||
|
||||
public function __construct(...$values)
|
||||
|
@@ -9,8 +9,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_key_exists, count, func_get_args, is_array, is_string;
|
||||
|
||||
|
||||
/**
|
||||
* SQL builder via fluent interfaces.
|
||||
@@ -29,8 +27,6 @@ use function array_key_exists, count, func_get_args, is_array, is_string;
|
||||
* @method Fluent innerJoin(...$table)
|
||||
* @method Fluent rightJoin(...$table)
|
||||
* @method Fluent outerJoin(...$table)
|
||||
* @method Fluent union(Fluent $fluent)
|
||||
* @method Fluent unionAll(Fluent $fluent)
|
||||
* @method Fluent as(...$field)
|
||||
* @method Fluent on(...$cond)
|
||||
* @method Fluent and(...$cond)
|
||||
@@ -47,13 +43,9 @@ use function array_key_exists, count, func_get_args, is_array, is_string;
|
||||
*/
|
||||
class Fluent implements IDataSource
|
||||
{
|
||||
public const
|
||||
AffectedRows = 'a',
|
||||
Identifier = 'n',
|
||||
Remove = false;
|
||||
use Strict;
|
||||
|
||||
#[\Deprecated('use Fluent::Remove')]
|
||||
public const REMOVE = self::Remove;
|
||||
public const REMOVE = false;
|
||||
|
||||
public static array $masks = [
|
||||
'SELECT' => ['SELECT', 'DISTINCT', 'FROM', 'WHERE', 'GROUP BY',
|
||||
@@ -99,11 +91,16 @@ class Fluent implements IDataSource
|
||||
'RIGHT JOIN' => 'FROM',
|
||||
];
|
||||
|
||||
private readonly Connection $connection;
|
||||
private Connection $connection;
|
||||
|
||||
private array $setups = [];
|
||||
|
||||
private ?string $command = null;
|
||||
|
||||
private array $clauses = [];
|
||||
|
||||
private array $flags = [];
|
||||
|
||||
private $cursor;
|
||||
|
||||
/** normalized clauses */
|
||||
@@ -115,7 +112,7 @@ class Fluent implements IDataSource
|
||||
$this->connection = $connection;
|
||||
|
||||
if (!isset(self::$normalizer)) {
|
||||
self::$normalizer = new HashMap(self::_formatClause(...));
|
||||
self::$normalizer = new HashMap([self::class, '_formatClause']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +145,7 @@ class Fluent implements IDataSource
|
||||
$this->cursor = &$this->clauses[$clause];
|
||||
|
||||
// TODO: really delete?
|
||||
if ($args === [self::Remove]) {
|
||||
if ($args === [self::REMOVE]) {
|
||||
$this->cursor = null;
|
||||
return $this;
|
||||
}
|
||||
@@ -164,7 +161,7 @@ class Fluent implements IDataSource
|
||||
}
|
||||
} else {
|
||||
// append to currect flow
|
||||
if ($args === [self::Remove]) {
|
||||
if ($args === [self::REMOVE]) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -287,17 +284,19 @@ class Fluent implements IDataSource
|
||||
/**
|
||||
* Generates and executes SQL query.
|
||||
* Returns result set or number of affected rows
|
||||
* @return ($return is self::Identifier|self::AffectedRows ? int : Result)
|
||||
* @throws Exception
|
||||
*/
|
||||
public function execute(?string $return = null): Result|int|null
|
||||
{
|
||||
$res = $this->query($this->_export());
|
||||
return match ($return) {
|
||||
self::Identifier => $this->connection->getInsertId(),
|
||||
self::AffectedRows => $this->connection->getAffectedRows(),
|
||||
default => $res,
|
||||
};
|
||||
switch ($return) {
|
||||
case \dibi::IDENTIFIER:
|
||||
return $this->connection->getInsertId();
|
||||
case \dibi::AFFECTED_ROWS:
|
||||
return $this->connection->getAffectedRows();
|
||||
default:
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -46,7 +46,7 @@ abstract class HashMapBase
|
||||
*/
|
||||
final class HashMap extends HashMapBase
|
||||
{
|
||||
public function __set(string $nm, mixed $val): void
|
||||
public function __set(string $nm, $val)
|
||||
{
|
||||
if ($nm === '') {
|
||||
$nm = "\xFF";
|
||||
@@ -56,7 +56,7 @@ final class HashMap extends HashMapBase
|
||||
}
|
||||
|
||||
|
||||
public function __get(string $nm): mixed
|
||||
public function __get(string $nm)
|
||||
{
|
||||
if ($nm === '') {
|
||||
$nm = "\xFF";
|
||||
|
@@ -9,12 +9,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_map, array_unique, explode, fclose, fgets, fopen, fstat, getenv, htmlspecialchars, is_float, is_int, is_string, levenshtein, max, mb_strlen, ob_end_flush, ob_get_clean, ob_start, preg_match, preg_replace, preg_replace_callback, rtrim, set_time_limit, str_ends_with, str_repeat, str_starts_with, strlen, strtoupper, substr, trim, wordwrap;
|
||||
use const PHP_SAPI;
|
||||
|
||||
|
||||
class Helpers
|
||||
{
|
||||
use Strict;
|
||||
|
||||
private static HashMap $types;
|
||||
|
||||
|
||||
@@ -159,15 +158,15 @@ class Helpers
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function escape(Drivers\Connection $driver, $value, string $type): string
|
||||
public static function escape(Driver $driver, $value, string $type): string
|
||||
{
|
||||
$types = [
|
||||
Type::Text => 'text',
|
||||
Type::Binary => 'binary',
|
||||
Type::Bool => 'bool',
|
||||
Type::Date => 'date',
|
||||
Type::DateTime => 'datetime',
|
||||
Fluent::Identifier => 'identifier',
|
||||
Type::TEXT => 'text',
|
||||
Type::BINARY => 'binary',
|
||||
Type::BOOL => 'bool',
|
||||
Type::DATE => 'date',
|
||||
Type::DATETIME => 'datetime',
|
||||
\dibi::IDENTIFIER => 'identifier',
|
||||
];
|
||||
if (isset($types[$type])) {
|
||||
return $driver->{'escape' . $types[$type]}($value);
|
||||
@@ -184,16 +183,16 @@ class Helpers
|
||||
public static function detectType(string $type): ?string
|
||||
{
|
||||
$patterns = [
|
||||
'^_' => Type::Text, // PostgreSQL arrays
|
||||
'RANGE$' => Type::Text, // PostgreSQL range types
|
||||
'BYTEA|BLOB|BIN' => Type::Binary,
|
||||
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::Text,
|
||||
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::Integer,
|
||||
'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::Float,
|
||||
'^TIME$' => Type::Time,
|
||||
'TIME' => Type::DateTime, // DATETIME, TIMESTAMP
|
||||
'DATE' => Type::Date,
|
||||
'BOOL' => Type::Bool,
|
||||
'^_' => Type::TEXT, // PostgreSQL arrays
|
||||
'RANGE$' => Type::TEXT, // PostgreSQL range types
|
||||
'BYTEA|BLOB|BIN' => Type::BINARY,
|
||||
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::TEXT,
|
||||
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::INTEGER,
|
||||
'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::FLOAT,
|
||||
'^TIME$' => Type::TIME,
|
||||
'TIME' => Type::DATETIME, // DATETIME, TIMESTAMP
|
||||
'DATE' => Type::DATE,
|
||||
'BOOL' => Type::BOOL,
|
||||
'JSON' => Type::JSON,
|
||||
];
|
||||
|
||||
@@ -211,7 +210,7 @@ class Helpers
|
||||
public static function getTypeCache(): HashMap
|
||||
{
|
||||
if (!isset(self::$types)) {
|
||||
self::$types = new HashMap(self::detectType(...));
|
||||
self::$types = new HashMap([self::class, 'detectType']);
|
||||
}
|
||||
|
||||
return self::$types;
|
||||
|
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
|
||||
/**
|
||||
* Provides an interface between a dataset and data-aware components.
|
||||
*/
|
||||
interface IDataSource extends \Countable, \IteratorAggregate
|
||||
{
|
||||
//function \IteratorAggregate::getIterator();
|
||||
//function \Countable::count();
|
||||
}
|
@@ -15,7 +15,9 @@ namespace Dibi;
|
||||
*/
|
||||
class Literal
|
||||
{
|
||||
private readonly string $value;
|
||||
use Strict;
|
||||
|
||||
private string $value;
|
||||
|
||||
|
||||
public function __construct($value)
|
||||
|
@@ -10,8 +10,6 @@ declare(strict_types=1);
|
||||
namespace Dibi\Loggers;
|
||||
|
||||
use Dibi;
|
||||
use function sprintf;
|
||||
use const FILE_APPEND, LOCK_EX;
|
||||
|
||||
|
||||
/**
|
||||
@@ -19,11 +17,21 @@ use const FILE_APPEND, LOCK_EX;
|
||||
*/
|
||||
class FileLogger
|
||||
{
|
||||
public function __construct(
|
||||
public string $file,
|
||||
public int $filter = Dibi\Event::QUERY,
|
||||
private bool $errorsOnly = false,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
/** Name of the file where SQL errors should be logged */
|
||||
public string $file;
|
||||
|
||||
public int $filter;
|
||||
|
||||
private bool $errorsOnly;
|
||||
|
||||
|
||||
public function __construct(string $file, ?int $filter = null, bool $errorsOnly = false)
|
||||
{
|
||||
$this->file = $file;
|
||||
$this->filter = $filter ?: Dibi\Event::QUERY;
|
||||
$this->errorsOnly = $errorsOnly;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +74,7 @@ class FileLogger
|
||||
{
|
||||
$driver = $event->connection->getConfig('driver');
|
||||
$message .=
|
||||
"\n-- driver: " . get_debug_type($driver) . '/' . $event->connection->getConfig('name')
|
||||
"\n-- driver: " . (is_object($driver) ? $driver::class : $driver) . '/' . $event->connection->getConfig('name')
|
||||
. "\n-- " . date('Y-m-d H:i:s')
|
||||
. "\n\n";
|
||||
file_put_contents($this->file, $message, FILE_APPEND | LOCK_EX);
|
||||
|
@@ -27,10 +27,19 @@ use Dibi;
|
||||
*/
|
||||
class Column
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?Dibi\Drivers\Engine $reflector,
|
||||
private array $info,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
/** when created by Result */
|
||||
private ?Dibi\Reflector $reflector;
|
||||
|
||||
/** @var array (name, nativetype, [table], [fullname], [size], [nullable], [default], [autoincrement], [vendor]) */
|
||||
private array $info;
|
||||
|
||||
|
||||
public function __construct(?Dibi\Reflector $reflector, array $info)
|
||||
{
|
||||
$this->reflector = $reflector;
|
||||
$this->info = $info;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
use function array_values, strtolower;
|
||||
|
||||
|
||||
/**
|
||||
@@ -22,14 +21,20 @@ use function array_values, strtolower;
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Reflector $reflector;
|
||||
|
||||
private ?string $name;
|
||||
|
||||
/** @var Table[] */
|
||||
private array $tables;
|
||||
|
||||
|
||||
public function __construct(
|
||||
private readonly Dibi\Drivers\Engine $reflector,
|
||||
private ?string $name = null,
|
||||
) {
|
||||
public function __construct(Dibi\Reflector $reflector, ?string $name = null)
|
||||
{
|
||||
$this->reflector = $reflector;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
|
||||
|
||||
/**
|
||||
@@ -19,10 +20,18 @@ namespace Dibi\Reflection;
|
||||
*/
|
||||
class ForeignKey
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly array $references,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
private string $name;
|
||||
|
||||
/** @var array of [local, foreign, onDelete, onUpdate] */
|
||||
private array $references;
|
||||
|
||||
|
||||
public function __construct(string $name, array $references)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->references = $references;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
|
||||
|
||||
/**
|
||||
@@ -21,9 +22,15 @@ namespace Dibi\Reflection;
|
||||
*/
|
||||
class Index
|
||||
{
|
||||
public function __construct(
|
||||
private readonly array $info,
|
||||
) {
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var array (name, columns, [unique], [primary]) */
|
||||
private array $info;
|
||||
|
||||
|
||||
public function __construct(array $info)
|
||||
{
|
||||
$this->info = $info;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
use function array_values, strtolower;
|
||||
|
||||
|
||||
/**
|
||||
@@ -21,6 +20,10 @@ use function array_values, strtolower;
|
||||
*/
|
||||
class Result
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\ResultDriver $driver;
|
||||
|
||||
/** @var Column[]|null */
|
||||
private ?array $columns;
|
||||
|
||||
@@ -28,9 +31,9 @@ class Result
|
||||
private ?array $names;
|
||||
|
||||
|
||||
public function __construct(
|
||||
private readonly Dibi\Drivers\Result $driver,
|
||||
) {
|
||||
public function __construct(Dibi\ResultDriver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +82,7 @@ class Result
|
||||
{
|
||||
if (!isset($this->columns)) {
|
||||
$this->columns = [];
|
||||
$reflector = $this->driver instanceof Dibi\Drivers\Engine
|
||||
$reflector = $this->driver instanceof Dibi\Reflector
|
||||
? $this->driver
|
||||
: null;
|
||||
foreach ($this->driver->getResultColumns() as $info) {
|
||||
|
@@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
use function array_values, strtolower;
|
||||
|
||||
|
||||
/**
|
||||
@@ -26,8 +25,12 @@ use function array_values, strtolower;
|
||||
*/
|
||||
class Table
|
||||
{
|
||||
private readonly Dibi\Drivers\Engine $reflector;
|
||||
use Dibi\Strict;
|
||||
|
||||
private Dibi\Reflector $reflector;
|
||||
|
||||
private string $name;
|
||||
|
||||
private bool $view;
|
||||
|
||||
/** @var Column[] */
|
||||
@@ -38,10 +41,11 @@ class Table
|
||||
|
||||
/** @var Index[] */
|
||||
private array $indexes;
|
||||
|
||||
private ?Index $primaryKey;
|
||||
|
||||
|
||||
public function __construct(Dibi\Drivers\Engine $reflector, array $info)
|
||||
public function __construct(Dibi\Reflector $reflector, array $info)
|
||||
{
|
||||
$this->reflector = $reflector;
|
||||
$this->name = $info['name'];
|
||||
|
@@ -9,9 +9,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_keys, array_pop, count, explode, is_float, is_string, json_decode, ltrim, preg_match, preg_split, property_exists, reset, rtrim, str_contains, str_replace, str_starts_with, strpos;
|
||||
use const PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY;
|
||||
|
||||
|
||||
/**
|
||||
* Query result.
|
||||
@@ -20,10 +17,13 @@ use const PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY;
|
||||
*/
|
||||
class Result implements IDataSource
|
||||
{
|
||||
private ?Drivers\Result $driver;
|
||||
use Strict;
|
||||
|
||||
private ?ResultDriver $driver;
|
||||
|
||||
/** Translate table */
|
||||
private array $types = [];
|
||||
|
||||
private ?Reflection\Result $meta;
|
||||
|
||||
/** Already fetched? Used for allowance for first seek(0) */
|
||||
@@ -34,10 +34,11 @@ class Result implements IDataSource
|
||||
|
||||
/** @var callable|null returned object factory */
|
||||
private $rowFactory;
|
||||
|
||||
private array $formats = [];
|
||||
|
||||
|
||||
public function __construct(Drivers\Result $driver, bool $normalize = true)
|
||||
public function __construct(ResultDriver $driver, bool $normalize = true)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
if ($normalize) {
|
||||
@@ -62,7 +63,7 @@ class Result implements IDataSource
|
||||
* Safe access to property $driver.
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
final public function getResultDriver(): Drivers\Result
|
||||
final public function getResultDriver(): ResultDriver
|
||||
{
|
||||
if ($this->driver === null) {
|
||||
throw new \RuntimeException('Result-set was released from memory.');
|
||||
@@ -460,22 +461,16 @@ class Result implements IDataSource
|
||||
if ($type === null || $format === 'native') {
|
||||
$row[$key] = $value;
|
||||
|
||||
} elseif ($type === Type::Text) {
|
||||
} elseif ($type === Type::TEXT) {
|
||||
$row[$key] = (string) $value;
|
||||
|
||||
} elseif ($type === Type::Integer) {
|
||||
} elseif ($type === Type::INTEGER) {
|
||||
$row[$key] = is_float($tmp = $value * 1)
|
||||
? (is_string($value) ? $value : (int) $value)
|
||||
: $tmp;
|
||||
|
||||
} elseif ($type === Type::Float) {
|
||||
if (!is_string($value)) {
|
||||
$row[$key] = (float) $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
$negative = ($value[0] ?? null) === '-';
|
||||
$value = ltrim($value, '0-');
|
||||
} elseif ($type === Type::FLOAT) {
|
||||
$value = ltrim((string) $value, '0');
|
||||
$p = strpos($value, '.');
|
||||
$e = strpos($value, 'e');
|
||||
if ($p !== false && $e === false) {
|
||||
@@ -488,31 +483,27 @@ class Result implements IDataSource
|
||||
$value = '0' . $value;
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = '-' . $value;
|
||||
}
|
||||
|
||||
$row[$key] = $value === str_replace(',', '.', (string) ($float = (float) $value))
|
||||
? $float
|
||||
: $value;
|
||||
|
||||
} elseif ($type === Type::Bool) {
|
||||
} elseif ($type === Type::BOOL) {
|
||||
$row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F';
|
||||
|
||||
} elseif ($type === Type::DateTime || $type === Type::Date || $type === Type::Time) {
|
||||
} elseif ($type === Type::DATETIME || $type === Type::DATE || $type === Type::TIME) {
|
||||
if ($value && !str_starts_with((string) $value, '0000-00')) { // '', null, false, '0000-00-00', ...
|
||||
$value = new DateTime($value);
|
||||
$row[$key] = $format ? $value->format($format) : $value;
|
||||
} else {
|
||||
$row[$key] = null;
|
||||
}
|
||||
} elseif ($type === Type::TimeInterval) {
|
||||
} elseif ($type === Type::TIME_INTERVAL) {
|
||||
preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)\z#', $value, $m);
|
||||
$value = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
|
||||
$value->invert = (int) (bool) $m[1];
|
||||
$row[$key] = $format ? $value->format($format) : $value;
|
||||
|
||||
} elseif ($type === Type::Binary) {
|
||||
} elseif ($type === Type::BINARY) {
|
||||
$row[$key] = is_string($value)
|
||||
? $this->getResultDriver()->unescapeBinary($value)
|
||||
: $value;
|
||||
|
@@ -15,13 +15,18 @@ namespace Dibi;
|
||||
*/
|
||||
class ResultIterator implements \Iterator, \Countable
|
||||
{
|
||||
use Strict;
|
||||
|
||||
private Result $result;
|
||||
|
||||
private mixed $row;
|
||||
|
||||
private int $pointer = 0;
|
||||
|
||||
|
||||
public function __construct(
|
||||
private readonly Result $result,
|
||||
) {
|
||||
public function __construct(Result $result)
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +44,6 @@ class ResultIterator implements \Iterator, \Countable
|
||||
/**
|
||||
* Returns the key of the current element.
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function key(): mixed
|
||||
{
|
||||
return $this->pointer;
|
||||
@@ -49,7 +53,6 @@ class ResultIterator implements \Iterator, \Countable
|
||||
/**
|
||||
* Returns the current element.
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function current(): mixed
|
||||
{
|
||||
return $this->row;
|
||||
|
@@ -9,8 +9,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_keys, count, str_starts_with;
|
||||
|
||||
|
||||
/**
|
||||
* Result set single row.
|
||||
@@ -50,11 +48,10 @@ class Row implements \ArrayAccess, \IteratorAggregate, \Countable
|
||||
}
|
||||
|
||||
|
||||
public function __get(string $key): mixed
|
||||
public function __get(string $key)
|
||||
{
|
||||
$hint = Helpers::getSuggestion(array_keys((array) $this), $key);
|
||||
trigger_error("Attempt to read missing column '$key'" . ($hint ? ", did you mean '$hint'?" : '.'), E_USER_NOTICE);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
112
src/Dibi/Strict.php
Normal file
112
src/Dibi/Strict.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use ReflectionProperty;
|
||||
|
||||
|
||||
/**
|
||||
* Better OOP experience.
|
||||
*/
|
||||
trait Strict
|
||||
{
|
||||
/** @var array [method => [type => callback]] */
|
||||
private static array $extMethods;
|
||||
|
||||
|
||||
/**
|
||||
* Call to undefined method.
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function __call(string $name, array $args)
|
||||
{
|
||||
$class = method_exists($this, $name) ? 'parent' : static::class;
|
||||
$items = (new ReflectionClass($this))->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||
$items = array_map(fn($item) => $item->getName(), $items);
|
||||
$hint = ($t = Helpers::getSuggestion($items, $name))
|
||||
? ", did you mean $t()?"
|
||||
: '.';
|
||||
throw new \LogicException("Call to undefined method $class::$name()$hint");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call to undefined static method.
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public static function __callStatic(string $name, array $args)
|
||||
{
|
||||
$rc = new ReflectionClass(static::class);
|
||||
$items = array_filter($rc->getMethods(\ReflectionMethod::IS_STATIC), fn($m) => $m->isPublic());
|
||||
$items = array_map(fn($item) => $item->getName(), $items);
|
||||
$hint = ($t = Helpers::getSuggestion($items, $name))
|
||||
? ", did you mean $t()?"
|
||||
: '.';
|
||||
throw new \LogicException("Call to undefined static method {$rc->getName()}::$name()$hint");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Access to undeclared property.
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function &__get(string $name)
|
||||
{
|
||||
if ((method_exists($this, $m = 'get' . $name) || method_exists($this, $m = 'is' . $name))
|
||||
&& (new ReflectionMethod($this, $m))->isPublic()
|
||||
) { // back compatiblity
|
||||
$ret = $this->$m();
|
||||
return $ret;
|
||||
}
|
||||
|
||||
$rc = new ReflectionClass($this);
|
||||
$items = array_filter($rc->getProperties(ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic());
|
||||
$items = array_map(fn($item) => $item->getName(), $items);
|
||||
$hint = ($t = Helpers::getSuggestion($items, $name))
|
||||
? ", did you mean $$t?"
|
||||
: '.';
|
||||
throw new \LogicException("Attempt to read undeclared property {$rc->getName()}::$$name$hint");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Access to undeclared property.
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function __set(string $name, $value)
|
||||
{
|
||||
$rc = new ReflectionClass($this);
|
||||
$items = array_filter($rc->getProperties(ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic());
|
||||
$items = array_map(fn($item) => $item->getName(), $items);
|
||||
$hint = ($t = Helpers::getSuggestion($items, $name))
|
||||
? ", did you mean $$t?"
|
||||
: '.';
|
||||
throw new \LogicException("Attempt to write to undeclared property {$rc->getName()}::$$name$hint");
|
||||
}
|
||||
|
||||
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Access to undeclared property.
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function __unset(string $name)
|
||||
{
|
||||
$class = static::class;
|
||||
throw new \LogicException("Attempt to unset undeclared property $class::$$name.");
|
||||
}
|
||||
}
|
@@ -9,27 +9,35 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_filter, array_keys, array_splice, array_values, count, explode, get_debug_type, gettype, implode, is_array, is_bool, is_float, is_int, is_numeric, is_object, is_scalar, is_string, iterator_to_array, key, ltrim, number_format, preg_last_error, preg_match, preg_replace_callback, reset, rtrim, str_contains, str_replace, strcspn, strlen, strncasecmp, strtoupper, substr, trim;
|
||||
|
||||
|
||||
/**
|
||||
* SQL translator.
|
||||
*/
|
||||
final class Translator
|
||||
{
|
||||
private readonly Connection $connection;
|
||||
private readonly Drivers\Connection $driver;
|
||||
private readonly Drivers\Engine $engine;
|
||||
use Strict;
|
||||
|
||||
private Connection $connection;
|
||||
|
||||
private Driver $driver;
|
||||
|
||||
private int $cursor = 0;
|
||||
|
||||
private array $args;
|
||||
|
||||
/** @var string[] */
|
||||
private array $errors;
|
||||
|
||||
private bool $comment = false;
|
||||
|
||||
private int $ifLevel = 0;
|
||||
|
||||
private int $ifLevelStart = 0;
|
||||
|
||||
private ?int $limit = null;
|
||||
|
||||
private ?int $offset = null;
|
||||
|
||||
private HashMap $identifiers;
|
||||
|
||||
|
||||
@@ -37,8 +45,7 @@ final class Translator
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->driver = $connection->getDriver();
|
||||
$this->engine = $connection->getDatabaseEngine();
|
||||
$this->identifiers = new HashMap($this->delimite(...));
|
||||
$this->identifiers = new HashMap([$this, 'delimite']);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +99,7 @@ final class Translator
|
||||
(\?) ## 11) placeholder
|
||||
)/xs
|
||||
XX,
|
||||
$this->cb(...),
|
||||
[$this, 'cb'],
|
||||
substr($arg, $toSkip),
|
||||
);
|
||||
if (preg_last_error()) {
|
||||
@@ -146,7 +153,7 @@ final class Translator
|
||||
|
||||
// apply limit
|
||||
if ($this->limit !== null || $this->offset !== null) {
|
||||
$this->engine->applyLimit($sql, $this->limit, $this->offset);
|
||||
$this->driver->applyLimit($sql, $this->limit, $this->offset);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
@@ -211,7 +218,7 @@ final class Translator
|
||||
case 'n': // key, key, ... identifier names
|
||||
foreach ($value as $k => $v) {
|
||||
if (is_string($k)) {
|
||||
$vx[] = $this->identifiers->$k . (empty($v) ? '' : ' AS ' . $this->engine->escapeIdentifier($v));
|
||||
$vx[] = $this->identifiers->$k . (empty($v) ? '' : ' AS ' . $this->driver->escapeIdentifier($v));
|
||||
} else {
|
||||
$pair = explode('%', $v, 2); // split into identifier & modifier
|
||||
$vx[] = $this->identifiers->{$pair[0]};
|
||||
@@ -223,7 +230,7 @@ final class Translator
|
||||
|
||||
case 'a': // key=val, key=val, ...
|
||||
foreach ($value as $k => $v) {
|
||||
$pair = explode('%', (string) $k, 2); // split into identifier & modifier
|
||||
$pair = explode('%', $k, 2); // split into identifier & modifier
|
||||
$vx[] = $this->identifiers->{$pair[0]} . '='
|
||||
. $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
|
||||
}
|
||||
@@ -261,7 +268,7 @@ final class Translator
|
||||
$proto = array_keys($v);
|
||||
}
|
||||
} else {
|
||||
return $this->errors[] = '**Unexpected type ' . get_debug_type($v) . '**';
|
||||
return $this->errors[] = '**Unexpected type ' . (is_object($v) ? $v::class : gettype($v)) . '**';
|
||||
}
|
||||
|
||||
$pair = explode('%', $k, 2); // split into identifier & modifier
|
||||
@@ -311,9 +318,13 @@ final class Translator
|
||||
&& $modifier === null
|
||||
&& !$value instanceof Literal
|
||||
&& !$value instanceof Expression
|
||||
&& $result = $this->connection->translateObject($value)
|
||||
) {
|
||||
return $this->connection->translate(...$result->getValues());
|
||||
foreach ($this->connection->getObjectTranslators() as $class => $translator) {
|
||||
if ($value instanceof $class) {
|
||||
$value = $translator($value);
|
||||
return $this->connection->translate(...$value->getValues());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// object-to-scalar procession
|
||||
@@ -339,7 +350,7 @@ final class Translator
|
||||
) {
|
||||
// continue
|
||||
} else {
|
||||
$type = get_debug_type($value);
|
||||
$type = is_object($value) ? $value::class : gettype($value);
|
||||
return $this->errors[] = "**Invalid combination of type $type and modifier %$modifier**";
|
||||
}
|
||||
}
|
||||
@@ -348,7 +359,7 @@ final class Translator
|
||||
case 's': // string
|
||||
return $value === null
|
||||
? 'NULL'
|
||||
: $this->engine->escapeText((string) $value);
|
||||
: $this->driver->escapeText((string) $value);
|
||||
|
||||
case 'bin':// binary
|
||||
return $value === null
|
||||
@@ -358,7 +369,7 @@ final class Translator
|
||||
case 'b': // boolean
|
||||
return $value === null
|
||||
? 'NULL'
|
||||
: $this->engine->escapeBool((bool) $value);
|
||||
: $this->driver->escapeBool((bool) $value);
|
||||
|
||||
case 'sN': // string or null
|
||||
case 'sn':
|
||||
@@ -408,15 +419,15 @@ final class Translator
|
||||
}
|
||||
|
||||
return $modifier === 'd'
|
||||
? $this->engine->escapeDate($value)
|
||||
: $this->engine->escapeDateTime($value);
|
||||
? $this->driver->escapeDate($value)
|
||||
: $this->driver->escapeDateTime($value);
|
||||
|
||||
case 'by':
|
||||
case 'n': // composed identifier name
|
||||
return $this->identifiers->$value;
|
||||
|
||||
case 'N': // identifier name
|
||||
return $this->engine->escapeIdentifier($value);
|
||||
return $this->driver->escapeIdentifier($value);
|
||||
|
||||
case 'ex':
|
||||
case 'sql': // preserve as dibi-SQL (TODO: leave only %ex)
|
||||
@@ -427,18 +438,19 @@ final class Translator
|
||||
$value = substr($value, 0, $toSkip)
|
||||
. preg_replace_callback(
|
||||
<<<'XX'
|
||||
/
|
||||
(?=[`['":])
|
||||
(?:
|
||||
`(.+?)`|
|
||||
\[(.+?)]|
|
||||
(')((?:''|[^'])*)'|
|
||||
(")((?:""|[^"])*)"|
|
||||
(['"])|
|
||||
:(\S*?:)([a-zA-Z0-9._]?)
|
||||
)/sx
|
||||
XX,
|
||||
$this->cb(...),
|
||||
/
|
||||
(?=[`['":])
|
||||
(?:
|
||||
`(.+?)`|
|
||||
\[(.+?)\]|
|
||||
(')((?:''|[^'])*)'|
|
||||
(")((?:""|[^"])*)"|
|
||||
('|")|
|
||||
:(\S*?:)([a-zA-Z0-9._]?)
|
||||
)/sx
|
||||
XX
|
||||
,
|
||||
[$this, 'cb'],
|
||||
substr($value, $toSkip),
|
||||
);
|
||||
if (preg_last_error()) {
|
||||
@@ -452,16 +464,16 @@ final class Translator
|
||||
return (string) $value;
|
||||
|
||||
case 'like~': // LIKE string%
|
||||
return $this->engine->escapeLike($value, 2);
|
||||
return $this->driver->escapeLike($value, 2);
|
||||
|
||||
case '~like': // LIKE %string
|
||||
return $this->engine->escapeLike($value, 1);
|
||||
return $this->driver->escapeLike($value, 1);
|
||||
|
||||
case '~like~': // LIKE %string%
|
||||
return $this->engine->escapeLike($value, 3);
|
||||
return $this->driver->escapeLike($value, 3);
|
||||
|
||||
case 'like': // LIKE string
|
||||
return $this->engine->escapeLike($value, 0);
|
||||
return $this->driver->escapeLike($value, 0);
|
||||
|
||||
case 'and':
|
||||
case 'or':
|
||||
@@ -487,16 +499,16 @@ final class Translator
|
||||
return rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
|
||||
|
||||
} elseif (is_bool($value)) {
|
||||
return $this->engine->escapeBool($value);
|
||||
return $this->driver->escapeBool($value);
|
||||
|
||||
} elseif ($value === null) {
|
||||
return 'NULL';
|
||||
|
||||
} elseif ($value instanceof \DateTimeInterface) {
|
||||
return $this->engine->escapeDateTime($value);
|
||||
return $this->driver->escapeDateTime($value);
|
||||
|
||||
} elseif ($value instanceof \DateInterval) {
|
||||
return $this->engine->escapeDateInterval($value);
|
||||
return $this->driver->escapeDateInterval($value);
|
||||
|
||||
} elseif ($value instanceof Literal) {
|
||||
return (string) $value;
|
||||
@@ -505,7 +517,7 @@ final class Translator
|
||||
return $this->connection->translate(...$value->getValues());
|
||||
|
||||
} else {
|
||||
$type = get_debug_type($value);
|
||||
$type = is_object($value) ? $value::class : gettype($value);
|
||||
return $this->errors[] = "**Unexpected $type**";
|
||||
}
|
||||
}
|
||||
@@ -655,7 +667,7 @@ final class Translator
|
||||
$parts = explode('.', $value);
|
||||
foreach ($parts as &$v) {
|
||||
if ($v !== '*') {
|
||||
$v = $this->engine->escapeIdentifier($v);
|
||||
$v = $this->driver->escapeIdentifier($v);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,43 +16,16 @@ namespace Dibi;
|
||||
class Type
|
||||
{
|
||||
public const
|
||||
Text = 's', // as 'string'
|
||||
Binary = 'bin',
|
||||
TEXT = 's', // as 'string'
|
||||
BINARY = 'bin',
|
||||
JSON = 'json',
|
||||
Bool = 'b',
|
||||
Integer = 'i',
|
||||
Float = 'f',
|
||||
Date = 'd',
|
||||
DateTime = 'dt',
|
||||
Time = 't',
|
||||
TimeInterval = 'ti';
|
||||
|
||||
#[\Deprecated('use Type::Text')]
|
||||
public const TEXT = self::Text;
|
||||
|
||||
#[\Deprecated('use Type::Binary')]
|
||||
public const BINARY = self::Binary;
|
||||
|
||||
#[\Deprecated('use Type::Bool')]
|
||||
public const BOOL = self::Bool;
|
||||
|
||||
#[\Deprecated('use Type::Integer')]
|
||||
public const INTEGER = self::Integer;
|
||||
|
||||
#[\Deprecated('use Type::Float')]
|
||||
public const FLOAT = self::Float;
|
||||
|
||||
#[\Deprecated('use Type::Date')]
|
||||
public const DATE = self::Date;
|
||||
|
||||
#[\Deprecated('use Type::DateTime')]
|
||||
public const DATETIME = self::DateTime;
|
||||
|
||||
#[\Deprecated('use Type::Time')]
|
||||
public const TIME = self::Time;
|
||||
|
||||
#[\Deprecated('use Type::TimeInterval')]
|
||||
public const TIME_INTERVAL = self::TimeInterval;
|
||||
BOOL = 'b',
|
||||
INTEGER = 'i',
|
||||
FLOAT = 'f',
|
||||
DATE = 'd',
|
||||
DATETIME = 'dt',
|
||||
TIME = 't',
|
||||
TIME_INTERVAL = 'ti';
|
||||
|
||||
|
||||
final public function __construct()
|
||||
|
@@ -37,13 +37,14 @@ declare(strict_types=1);
|
||||
*/
|
||||
class dibi
|
||||
{
|
||||
public const Version = '6.0-dev';
|
||||
use Dibi\Strict;
|
||||
|
||||
public const VERSION = self::Version;
|
||||
public const
|
||||
AFFECTED_ROWS = 'a',
|
||||
IDENTIFIER = 'n';
|
||||
|
||||
public const AFFECTED_ROWS = Dibi\Fluent::AffectedRows;
|
||||
|
||||
public const IDENTIFIER = Dibi\Fluent::Identifier;
|
||||
/** version */
|
||||
public const VERSION = '5.0-dev';
|
||||
|
||||
/** sorting order */
|
||||
public const
|
||||
|
@@ -11,7 +11,7 @@ namespace Dibi;
|
||||
|
||||
|
||||
/**
|
||||
* A database operation failed.
|
||||
* Dibi common exception.
|
||||
*/
|
||||
class Exception extends \Exception
|
||||
{
|
||||
@@ -44,7 +44,7 @@ class Exception extends \Exception
|
||||
|
||||
|
||||
/**
|
||||
* The database server reported an error.
|
||||
* database server exception.
|
||||
*/
|
||||
class DriverException extends Exception
|
||||
{
|
||||
@@ -52,7 +52,7 @@ class DriverException extends Exception
|
||||
|
||||
|
||||
/**
|
||||
* Regular expression pattern or execution failed.
|
||||
* PCRE exception.
|
||||
*/
|
||||
class PcreException extends Exception
|
||||
{
|
||||
@@ -63,24 +63,18 @@ class PcreException extends Exception
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The requested feature is not implemented.
|
||||
*/
|
||||
class NotImplementedException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The requested operation is not supported.
|
||||
*/
|
||||
class NotSupportedException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A database stored procedure failed.
|
||||
* Database procedure exception.
|
||||
*/
|
||||
class ProcedureException extends Exception
|
||||
{
|
||||
@@ -108,7 +102,7 @@ class ProcedureException extends Exception
|
||||
|
||||
|
||||
/**
|
||||
* A database constraint was violated.
|
||||
* Base class for all constraint violation related exceptions.
|
||||
*/
|
||||
class ConstraintViolationException extends DriverException
|
||||
{
|
||||
@@ -116,7 +110,7 @@ class ConstraintViolationException extends DriverException
|
||||
|
||||
|
||||
/**
|
||||
* The foreign key constraint check failed.
|
||||
* Exception for a foreign key constraint violation.
|
||||
*/
|
||||
class ForeignKeyConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
@@ -124,7 +118,7 @@ class ForeignKeyConstraintViolationException extends ConstraintViolationExceptio
|
||||
|
||||
|
||||
/**
|
||||
* The NOT NULL constraint check failed.
|
||||
* Exception for a NOT NULL constraint violation.
|
||||
*/
|
||||
class NotNullConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
@@ -132,7 +126,7 @@ class NotNullConstraintViolationException extends ConstraintViolationException
|
||||
|
||||
|
||||
/**
|
||||
* The unique constraint check failed.
|
||||
* Exception for a unique constraint violation.
|
||||
*/
|
||||
class UniqueConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
|
240
src/Dibi/interfaces.php
Normal file
240
src/Dibi/interfaces.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
|
||||
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
|
||||
/**
|
||||
* Provides an interface between a dataset and data-aware components.
|
||||
*/
|
||||
interface IDataSource extends \Countable, \IteratorAggregate
|
||||
{
|
||||
//function \IteratorAggregate::getIterator();
|
||||
//function \Countable::count();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Driver interface.
|
||||
*/
|
||||
interface Driver
|
||||
{
|
||||
/**
|
||||
* Disconnects from a database.
|
||||
* @throws Exception
|
||||
*/
|
||||
function disconnect(): void;
|
||||
|
||||
/**
|
||||
* Internal: Executes the SQL query.
|
||||
* @throws DriverException
|
||||
*/
|
||||
function query(string $sql): ?ResultDriver;
|
||||
|
||||
/**
|
||||
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
|
||||
*/
|
||||
function getAffectedRows(): ?int;
|
||||
|
||||
/**
|
||||
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
|
||||
*/
|
||||
function getInsertId(?string $sequence): ?int;
|
||||
|
||||
/**
|
||||
* Begins a transaction (if supported).
|
||||
* @throws DriverException
|
||||
*/
|
||||
function begin(?string $savepoint = null): void;
|
||||
|
||||
/**
|
||||
* Commits statements in a transaction.
|
||||
* @throws DriverException
|
||||
*/
|
||||
function commit(?string $savepoint = null): void;
|
||||
|
||||
/**
|
||||
* Rollback changes in a transaction.
|
||||
* @throws DriverException
|
||||
*/
|
||||
function rollback(?string $savepoint = null): void;
|
||||
|
||||
/**
|
||||
* Returns the connection resource.
|
||||
*/
|
||||
function getResource(): mixed;
|
||||
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
*/
|
||||
function getReflector(): Reflector;
|
||||
|
||||
/**
|
||||
* Encodes data for use in a SQL statement.
|
||||
*/
|
||||
function escapeText(string $value): string;
|
||||
|
||||
function escapeBinary(string $value): string;
|
||||
|
||||
function escapeIdentifier(string $value): string;
|
||||
|
||||
function escapeBool(bool $value): string;
|
||||
|
||||
function escapeDate(\DateTimeInterface $value): string;
|
||||
|
||||
function escapeDateTime(\DateTimeInterface $value): string;
|
||||
|
||||
function escapeDateInterval(\DateInterval $value): string;
|
||||
|
||||
/**
|
||||
* Encodes string for use in a LIKE statement.
|
||||
*/
|
||||
function escapeLike(string $value, int $pos): string;
|
||||
|
||||
/**
|
||||
* Injects LIMIT/OFFSET to the SQL query.
|
||||
*/
|
||||
function applyLimit(string &$sql, ?int $limit, ?int $offset): void;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Result set driver interface.
|
||||
*/
|
||||
interface ResultDriver
|
||||
{
|
||||
/**
|
||||
* Returns the number of rows in a result set.
|
||||
*/
|
||||
function getRowCount(): int;
|
||||
|
||||
/**
|
||||
* Moves cursor position without fetching row.
|
||||
* @throws Exception
|
||||
*/
|
||||
function seek(int $row): bool;
|
||||
|
||||
/**
|
||||
* Fetches the row at current position and moves the internal cursor to the next position.
|
||||
* @param bool $type true for associative array, false for numeric
|
||||
* @internal
|
||||
*/
|
||||
function fetch(bool $type): ?array;
|
||||
|
||||
/**
|
||||
* Frees the resources allocated for this result set.
|
||||
*/
|
||||
function free(): void;
|
||||
|
||||
/**
|
||||
* Returns metadata for all columns in a result set.
|
||||
* @return array of {name, nativetype [, table, fullname, (int) size, (bool) nullable, (mixed) default, (bool) autoincrement, (array) vendor ]}
|
||||
*/
|
||||
function getResultColumns(): array;
|
||||
|
||||
/**
|
||||
* Returns the result set resource.
|
||||
*/
|
||||
function getResultResource(): mixed;
|
||||
|
||||
/**
|
||||
* Decodes data from result set.
|
||||
*/
|
||||
function unescapeBinary(string $value): string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reflection driver.
|
||||
*/
|
||||
interface Reflector
|
||||
{
|
||||
/**
|
||||
* Returns list of tables.
|
||||
* @return array of {name [, (bool) view ]}
|
||||
*/
|
||||
function getTables(): array;
|
||||
|
||||
/**
|
||||
* Returns metadata for all columns in a table.
|
||||
* @return array of {name, nativetype [, table, fullname, (int) size, (bool) nullable, (mixed) default, (bool) autoincrement, (array) vendor ]}
|
||||
*/
|
||||
function getColumns(string $table): array;
|
||||
|
||||
/**
|
||||
* Returns metadata for all indexes in a table.
|
||||
* @return array of {name, (array of names) columns [, (bool) unique, (bool) primary ]}
|
||||
*/
|
||||
function getIndexes(string $table): array;
|
||||
|
||||
/**
|
||||
* Returns metadata for all foreign keys in a table.
|
||||
*/
|
||||
function getForeignKeys(string $table): array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dibi connection.
|
||||
*/
|
||||
interface IConnection
|
||||
{
|
||||
/**
|
||||
* Connects to a database.
|
||||
*/
|
||||
function connect(): void;
|
||||
|
||||
/**
|
||||
* Disconnects from a database.
|
||||
*/
|
||||
function disconnect(): void;
|
||||
|
||||
/**
|
||||
* Returns true when connection was established.
|
||||
*/
|
||||
function isConnected(): bool;
|
||||
|
||||
/**
|
||||
* Returns the driver and connects to a database in lazy mode.
|
||||
*/
|
||||
function getDriver(): Driver;
|
||||
|
||||
/**
|
||||
* Generates (translates) and executes SQL query.
|
||||
* @throws Exception
|
||||
*/
|
||||
function query(...$args): Result;
|
||||
|
||||
/**
|
||||
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
|
||||
* @throws Exception
|
||||
*/
|
||||
function getAffectedRows(): int;
|
||||
|
||||
/**
|
||||
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
|
||||
* @throws Exception
|
||||
*/
|
||||
function getInsertId(?string $sequence = null): int;
|
||||
|
||||
/**
|
||||
* Begins a transaction (if supported).
|
||||
*/
|
||||
function begin(?string $savepoint = null): void;
|
||||
|
||||
/**
|
||||
* Commits statements in a transaction.
|
||||
*/
|
||||
function commit(?string $savepoint = null): void;
|
||||
|
||||
/**
|
||||
* Rollback changes in a transaction.
|
||||
*/
|
||||
function rollback(?string $savepoint = null): void;
|
||||
}
|
@@ -12,7 +12,7 @@ use Tester\Assert;
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
|
||||
test('immediate connection and disconnection state', function () use ($config) {
|
||||
test('', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::true($conn->isConnected());
|
||||
|
||||
@@ -21,7 +21,7 @@ test('immediate connection and disconnection state', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('lazy connection initiated on first query', function () use ($config) {
|
||||
test('lazy', function () use ($config) {
|
||||
$conn = new Connection($config + ['lazy' => true]);
|
||||
Assert::false($conn->isConnected());
|
||||
|
||||
@@ -30,17 +30,17 @@ test('lazy connection initiated on first query', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('config retrieval and driver instance access', function () use ($config) {
|
||||
test('', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::true($conn->isConnected());
|
||||
|
||||
Assert::null($conn->getConfig('lazy'));
|
||||
Assert::same($config['driver'], $conn->getConfig('driver'));
|
||||
Assert::type(Dibi\Drivers\Connection::class, $conn->getDriver());
|
||||
Assert::type(Dibi\Driver::class, $conn->getDriver());
|
||||
});
|
||||
|
||||
|
||||
test('idempotent disconnect calls', function () use ($config) {
|
||||
test('', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::true($conn->isConnected());
|
||||
|
||||
@@ -52,7 +52,7 @@ test('idempotent disconnect calls', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('reconnect after disconnection', function () use ($config) {
|
||||
test('', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::equal('hello', $conn->query('SELECT %s', 'hello')->fetchSingle());
|
||||
|
||||
@@ -63,7 +63,7 @@ test('reconnect after disconnection', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('destructor disconnects active connection', function () use ($config) {
|
||||
test('', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::true($conn->isConnected());
|
||||
|
||||
@@ -72,30 +72,25 @@ test('destructor disconnects active connection', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('invalid onConnect option triggers exceptions', function () use ($config) {
|
||||
Assert::exception(
|
||||
fn() => new Connection($config + ['onConnect' => '']),
|
||||
InvalidArgumentException::class,
|
||||
"Configuration option 'onConnect' must be array.",
|
||||
);
|
||||
test('', function () use ($config) {
|
||||
Assert::exception(function () use ($config) {
|
||||
new Connection($config + ['onConnect' => '']);
|
||||
}, InvalidArgumentException::class, "Configuration option 'onConnect' must be array.");
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => new Connection($config + ['onConnect' => ['STOP']]),
|
||||
Dibi\DriverException::class,
|
||||
);
|
||||
$e = Assert::exception(function () use ($config) {
|
||||
new Connection($config + ['onConnect' => ['STOP']]);
|
||||
}, Dibi\DriverException::class);
|
||||
Assert::same('STOP', $e->getSql());
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => new Connection($config + ['onConnect' => [['STOP %i', 123]]]),
|
||||
Dibi\DriverException::class,
|
||||
);
|
||||
$e = Assert::exception(function () use ($config) {
|
||||
new Connection($config + ['onConnect' => [['STOP %i', 123]]]);
|
||||
}, Dibi\DriverException::class);
|
||||
Assert::same('STOP 123', $e->getSql());
|
||||
|
||||
// lazy
|
||||
$conn = new Connection($config + ['lazy' => true, 'onConnect' => ['STOP']]);
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('SELECT 1'),
|
||||
Dibi\DriverException::class,
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('SELECT 1');
|
||||
}, Dibi\DriverException::class);
|
||||
Assert::same('STOP', $e->getSql());
|
||||
});
|
||||
|
@@ -24,16 +24,14 @@ class Time extends DateTimeImmutable
|
||||
|
||||
|
||||
test('Without object translator', function () use ($conn) {
|
||||
Assert::exception(
|
||||
fn() => $conn->translate('?', new Email),
|
||||
Dibi\Exception::class,
|
||||
'SQL translate error: Unexpected Email',
|
||||
);
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->translate('?', new Email);
|
||||
}, Dibi\Exception::class, 'SQL translate error: Unexpected Email');
|
||||
});
|
||||
|
||||
|
||||
test('Basics', function () use ($conn) {
|
||||
$conn->setObjectTranslator(fn(Email $email) => new Dibi\Expression('?', $email->address));
|
||||
$conn->addObjectTranslator(Email::class, fn($object) => new Dibi\Expression('?', $object->address));
|
||||
Assert::same(
|
||||
reformat([
|
||||
'sqlsrv' => "N'address@example.com'",
|
||||
@@ -49,13 +47,13 @@ test('DateTime', function () use ($conn) {
|
||||
|
||||
// Without object translator, DateTime child is translated by driver
|
||||
Assert::same(
|
||||
$conn->getDatabaseEngine()->escapeDateTime($stamp),
|
||||
$conn->getDriver()->escapeDateTime($stamp),
|
||||
$conn->translate('?', $stamp),
|
||||
);
|
||||
|
||||
|
||||
// With object translator
|
||||
$conn->setObjectTranslator(fn(Time $time) => new Dibi\Expression('OwnTime(?)', $time->format('H:i:s')));
|
||||
$conn->addObjectTranslator(Time::class, fn($object) => new Dibi\Expression('OwnTime(?)', $object->format('H:i:s')));
|
||||
Assert::same(
|
||||
reformat([
|
||||
'sqlsrv' => "OwnTime(N'12:13:14')",
|
||||
@@ -67,15 +65,15 @@ test('DateTime', function () use ($conn) {
|
||||
|
||||
// With modifier, it is still translated by driver
|
||||
Assert::same(
|
||||
$conn->getDatabaseEngine()->escapeDateTime($stamp),
|
||||
$conn->getDriver()->escapeDateTime($stamp),
|
||||
$conn->translate('%dt', $stamp),
|
||||
);
|
||||
Assert::same(
|
||||
$conn->getDatabaseEngine()->escapeDateTime($stamp),
|
||||
$conn->getDriver()->escapeDateTime($stamp),
|
||||
$conn->translate('%t', $stamp),
|
||||
);
|
||||
Assert::same(
|
||||
$conn->getDatabaseEngine()->escapeDate($stamp),
|
||||
$conn->getDriver()->escapeDate($stamp),
|
||||
$conn->translate('%d', $stamp),
|
||||
);
|
||||
|
||||
@@ -83,12 +81,12 @@ test('DateTime', function () use ($conn) {
|
||||
// DateTimeImmutable as a Time parent is not affected and still translated by driver
|
||||
$dt = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');
|
||||
Assert::same(
|
||||
$conn->getDatabaseEngine()->escapeDateTime($dt),
|
||||
$conn->getDriver()->escapeDateTime($dt),
|
||||
$conn->translate('?', $dt),
|
||||
);
|
||||
|
||||
// But DateTime translation can be overloaded
|
||||
$conn->setObjectTranslator(fn(DateTimeInterface $dt) => new Dibi\Expression('OwnDateTime'));
|
||||
$conn->addObjectTranslator(DateTimeInterface::class, fn() => new Dibi\Expression('OwnDateTime'));
|
||||
Assert::same(
|
||||
'OwnDateTime',
|
||||
$conn->translate('?', $dt),
|
||||
@@ -97,9 +95,9 @@ test('DateTime', function () use ($conn) {
|
||||
|
||||
|
||||
test('Complex structures', function () use ($conn) {
|
||||
$conn->setObjectTranslator(fn(Email $email) => new Dibi\Expression('?', $email->address));
|
||||
$conn->setObjectTranslator(fn(Time $time) => new Dibi\Expression('OwnTime(?)', $time->format('H:i:s')));
|
||||
$conn->setObjectTranslator(fn(DateTimeInterface $dt) => new Dibi\Expression('OwnDateTime'));
|
||||
$conn->addObjectTranslator(Email::class, fn($object) => new Dibi\Expression('?', $object->address));
|
||||
$conn->addObjectTranslator(Time::class, fn($object) => new Dibi\Expression('OwnTime(?)', $object->format('H:i:s')));
|
||||
$conn->addObjectTranslator(Time::class, fn($object) => new Dibi\Expression('OwnTime(?)', $object->format('H:i:s')));
|
||||
|
||||
$time = Time::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');
|
||||
Assert::same(
|
||||
@@ -119,37 +117,3 @@ test('Complex structures', function () use ($conn) {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test('Invalid translator', function () use ($conn) {
|
||||
Assert::exception(
|
||||
fn() => $conn->setObjectTranslator(fn($email) => 'foo'),
|
||||
Dibi\Exception::class,
|
||||
'Object translator must have exactly one parameter with class typehint.',
|
||||
);
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->setObjectTranslator(fn(string $email) => 'foo'),
|
||||
Dibi\Exception::class,
|
||||
"Object translator must have exactly one parameter with non-nullable class typehint, got 'string'.",
|
||||
);
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->setObjectTranslator(fn(Email|bool $email) => 'foo'),
|
||||
Dibi\Exception::class,
|
||||
"Object translator must have exactly one parameter with non-nullable class typehint, got 'bool'.",
|
||||
);
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->setObjectTranslator(fn(Email|null $email) => 'foo'),
|
||||
Dibi\Exception::class,
|
||||
"Object translator must have exactly one parameter with non-nullable class typehint, got '?Email'.",
|
||||
);
|
||||
|
||||
$conn->setObjectTranslator(fn(Email $email) => 'foo');
|
||||
Assert::exception(
|
||||
fn() => $conn->translate('?', new Email),
|
||||
Dibi\Exception::class,
|
||||
"Object translator for class 'Email' returned 'string' but Dibi\\Expression expected.",
|
||||
);
|
||||
});
|
||||
|
@@ -15,22 +15,18 @@ $conn = new Dibi\Connection($config);
|
||||
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
|
||||
|
||||
|
||||
/*
|
||||
Assert::exception(
|
||||
fn() => $conn->rollback(),
|
||||
Dibi\Exception::class,
|
||||
);
|
||||
/*Assert::exception(function () use ($conn) {
|
||||
$conn->rollback();
|
||||
}, Dibi\Exception::class);
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->commit(),
|
||||
Dibi\Exception::class,
|
||||
);
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->commit();
|
||||
}, Dibi\Exception::class);
|
||||
|
||||
$conn->begin();
|
||||
Assert::exception(
|
||||
fn() => $conn->begin(),
|
||||
Dibi\Exception::class,
|
||||
);
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->begin();
|
||||
}, Dibi\Exception::class);
|
||||
*/
|
||||
|
||||
|
||||
@@ -57,16 +53,14 @@ test('begin() & commit()', function () use ($conn) {
|
||||
|
||||
|
||||
test('transaction() fail', function () use ($conn) {
|
||||
Assert::exception(
|
||||
fn() => $conn->transaction(function (Dibi\Connection $connection) {
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->transaction(function (Dibi\Connection $connection) {
|
||||
$connection->query('INSERT INTO [products]', [
|
||||
'title' => 'Test product',
|
||||
]);
|
||||
throw new Exception('my exception');
|
||||
}),
|
||||
Throwable::class,
|
||||
'my exception',
|
||||
);
|
||||
});
|
||||
}, Throwable::class, 'my exception');
|
||||
Assert::same(4, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
|
||||
});
|
||||
|
||||
@@ -82,8 +76,8 @@ test('transaction() success', function () use ($conn) {
|
||||
|
||||
|
||||
test('nested transaction() call fail', function () use ($conn) {
|
||||
Assert::exception(
|
||||
fn() => $conn->transaction(function (Dibi\Connection $connection) {
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->transaction(function (Dibi\Connection $connection) {
|
||||
$connection->query('INSERT INTO [products]', [
|
||||
'title' => 'Test product',
|
||||
]);
|
||||
@@ -94,10 +88,8 @@ test('nested transaction() call fail', function () use ($conn) {
|
||||
]);
|
||||
throw new Exception('my exception');
|
||||
});
|
||||
}),
|
||||
Throwable::class,
|
||||
'my exception',
|
||||
);
|
||||
});
|
||||
}, Throwable::class, 'my exception');
|
||||
Assert::same(5, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
|
||||
});
|
||||
|
||||
@@ -119,27 +111,21 @@ test('nested transaction() call success', function () use ($conn) {
|
||||
|
||||
|
||||
test('begin(), commit() & rollback() calls are forbidden in transaction()', function () use ($conn) {
|
||||
Assert::exception(
|
||||
fn() => $conn->transaction(function (Dibi\Connection $connection) {
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->transaction(function (Dibi\Connection $connection) {
|
||||
$connection->begin();
|
||||
}),
|
||||
LogicException::class,
|
||||
Dibi\Connection::class . '::begin() call is forbidden inside a transaction() callback',
|
||||
);
|
||||
});
|
||||
}, LogicException::class, Dibi\Connection::class . '::begin() call is forbidden inside a transaction() callback');
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->transaction(function (Dibi\Connection $connection) {
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->transaction(function (Dibi\Connection $connection) {
|
||||
$connection->commit();
|
||||
}),
|
||||
LogicException::class,
|
||||
Dibi\Connection::class . '::commit() call is forbidden inside a transaction() callback',
|
||||
);
|
||||
});
|
||||
}, LogicException::class, Dibi\Connection::class . '::commit() call is forbidden inside a transaction() callback');
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->transaction(function (Dibi\Connection $connection) {
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->transaction(function (Dibi\Connection $connection) {
|
||||
$connection->rollback();
|
||||
}),
|
||||
LogicException::class,
|
||||
Dibi\Connection::class . '::rollback() call is forbidden inside a transaction() callback',
|
||||
);
|
||||
});
|
||||
}, LogicException::class, Dibi\Connection::class . '::rollback() call is forbidden inside a transaction() callback');
|
||||
});
|
||||
|
@@ -2,14 +2,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Dibi\Drivers\SQLSrv\Connection;
|
||||
use Dibi\Drivers\SQLSrv\Result;
|
||||
use Tester\Assert;
|
||||
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
|
||||
class MockDriver extends Connection
|
||||
class MockDriver extends Dibi\Drivers\SqlsrvDriver
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@@ -21,14 +19,14 @@ class MockDriver extends Connection
|
||||
}
|
||||
|
||||
|
||||
public function query(string $sql): ?Result
|
||||
public function query(string $sql): ?Dibi\ResultDriver
|
||||
{
|
||||
return new MockResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MockResult extends Result
|
||||
class MockResult extends Dibi\Drivers\SqlsrvResult
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@@ -59,28 +57,28 @@ $fluent = $conn->select('*')
|
||||
->orderBy('customer_id');
|
||||
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
(string) $fluent,
|
||||
);
|
||||
|
||||
|
||||
$fluent->fetch();
|
||||
Assert::same(
|
||||
'SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY',
|
||||
'SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t',
|
||||
dibi::$sql,
|
||||
);
|
||||
$fluent->fetchSingle();
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql,
|
||||
);
|
||||
$fluent->fetchAll(0, 3);
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY'),
|
||||
reformat('SELECT TOP (3) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql,
|
||||
);
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
(string) $fluent,
|
||||
);
|
||||
|
||||
@@ -88,16 +86,16 @@ Assert::same(
|
||||
$fluent->limit(0);
|
||||
$fluent->fetch();
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'),
|
||||
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql,
|
||||
);
|
||||
$fluent->fetchSingle();
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'),
|
||||
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql,
|
||||
);
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'),
|
||||
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
(string) $fluent,
|
||||
);
|
||||
|
||||
@@ -106,12 +104,12 @@ $fluent->removeClause('limit');
|
||||
$fluent->removeClause('offset');
|
||||
$fluent->fetch();
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql,
|
||||
);
|
||||
$fluent->fetchSingle();
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql,
|
||||
);
|
||||
Assert::same(
|
||||
|
@@ -74,7 +74,7 @@ Assert::same(
|
||||
(string) $fluent,
|
||||
);
|
||||
|
||||
$fluent->orderBy(Dibi\Fluent::Remove);
|
||||
$fluent->orderBy(Dibi\Fluent::REMOVE);
|
||||
|
||||
Assert::same(
|
||||
reformat('SELECT * , [a] , [b] AS [bAlias] , [c], [d], [e] , [d] FROM [anotherTable] AS [anotherAlias] INNER JOIN [table3] ON table.col = table3.col WHERE col > 10 OR col < 5 AND active = 1 AND [col] IN (1, 2, 3)'),
|
||||
@@ -147,11 +147,9 @@ if ($config['system'] === 'mysql') {
|
||||
->limit(' 1; DROP TABLE users')
|
||||
->offset(' 1; DROP TABLE users');
|
||||
|
||||
Assert::exception(
|
||||
fn() => (string) $fluent,
|
||||
Dibi\Exception::class,
|
||||
"Expected number, ' 1; DROP TABLE users' given.",
|
||||
);
|
||||
Assert::exception(function () use ($fluent) {
|
||||
(string) $fluent;
|
||||
}, Dibi\Exception::class, "Expected number, ' 1; DROP TABLE users' given.");
|
||||
}
|
||||
|
||||
|
||||
|
@@ -12,32 +12,22 @@ Assert::same(0, Helpers::intVal(0));
|
||||
Assert::same(0, Helpers::intVal('0'));
|
||||
Assert::same(-10, Helpers::intVal('-10'));
|
||||
|
||||
Assert::exception(
|
||||
fn() => Helpers::intVal('12345678901234567890123456879'),
|
||||
Dibi\Exception::class,
|
||||
'Number 12345678901234567890123456879 is greater than integer.',
|
||||
);
|
||||
Assert::exception(function () {
|
||||
Helpers::intVal('12345678901234567890123456879');
|
||||
}, Dibi\Exception::class, 'Number 12345678901234567890123456879 is greater than integer.');
|
||||
|
||||
Assert::exception(
|
||||
fn() => Helpers::intVal('-12345678901234567890123456879'),
|
||||
Dibi\Exception::class,
|
||||
'Number -12345678901234567890123456879 is greater than integer.',
|
||||
);
|
||||
Assert::exception(function () {
|
||||
Helpers::intVal('-12345678901234567890123456879');
|
||||
}, Dibi\Exception::class, 'Number -12345678901234567890123456879 is greater than integer.');
|
||||
|
||||
Assert::exception(
|
||||
fn() => Helpers::intVal(''),
|
||||
Dibi\Exception::class,
|
||||
"Expected number, '' given.",
|
||||
);
|
||||
Assert::exception(function () {
|
||||
Helpers::intVal('');
|
||||
}, Dibi\Exception::class, "Expected number, '' given.");
|
||||
|
||||
Assert::exception(
|
||||
fn() => Helpers::intVal('not number'),
|
||||
Dibi\Exception::class,
|
||||
"Expected number, 'not number' given.",
|
||||
);
|
||||
Assert::exception(function () {
|
||||
Helpers::intVal('not number');
|
||||
}, Dibi\Exception::class, "Expected number, 'not number' given.");
|
||||
|
||||
Assert::exception(
|
||||
fn() => Helpers::intVal(null),
|
||||
Dibi\Exception::class,
|
||||
"Expected number, '' given.",
|
||||
);
|
||||
Assert::exception(function () {
|
||||
Helpers::intVal(null);
|
||||
}, Dibi\Exception::class, "Expected number, '' given.");
|
||||
|
@@ -14,27 +14,22 @@ function buildPdoDriver(?int $errorMode)
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
|
||||
}
|
||||
|
||||
new Dibi\Drivers\PDO\Connection(['resource' => $pdo]);
|
||||
new Dibi\Drivers\PdoDriver(['resource' => $pdo]);
|
||||
}
|
||||
|
||||
|
||||
// PDO error mode: exception
|
||||
Assert::exception(
|
||||
fn() => buildPdoDriver(PDO::ERRMODE_EXCEPTION),
|
||||
Dibi\DriverException::class,
|
||||
'PDO connection in exception or warning error mode is not supported.',
|
||||
);
|
||||
Assert::exception(function () {
|
||||
buildPdoDriver(PDO::ERRMODE_EXCEPTION);
|
||||
}, Dibi\DriverException::class, 'PDO connection in exception or warning error mode is not supported.');
|
||||
|
||||
|
||||
// PDO error mode: warning
|
||||
Assert::exception(
|
||||
fn() => buildPdoDriver(PDO::ERRMODE_WARNING),
|
||||
Dibi\DriverException::class,
|
||||
'PDO connection in exception or warning error mode is not supported.',
|
||||
);
|
||||
Assert::exception(function () {
|
||||
buildPdoDriver(PDO::ERRMODE_WARNING);
|
||||
}, Dibi\DriverException::class, 'PDO connection in exception or warning error mode is not supported.');
|
||||
|
||||
|
||||
test(
|
||||
'PDO error mode: explicitly set silent',
|
||||
fn() => buildPdoDriver(PDO::ERRMODE_SILENT),
|
||||
);
|
||||
test('PDO error mode: explicitly set silent', function () {
|
||||
buildPdoDriver(PDO::ERRMODE_SILENT);
|
||||
});
|
||||
|
@@ -18,9 +18,9 @@ $tests = function ($conn) {
|
||||
Assert::false($conn->query("SELECT 'AAxBB' LIKE %~like~", 'A%B')->fetchSingle());
|
||||
Assert::true($conn->query("SELECT 'AA%BB' LIKE %~like~", 'A%B')->fetchSingle());
|
||||
|
||||
Assert::same('AA\BB', $conn->query("SELECT 'AA\\BB'")->fetchSingle());
|
||||
Assert::false($conn->query("SELECT 'AAxBB' LIKE %~like~", 'A\B')->fetchSingle());
|
||||
Assert::true($conn->query("SELECT 'AA\\BB' LIKE %~like~", 'A\B')->fetchSingle());
|
||||
Assert::same('AA\\BB', $conn->query("SELECT 'AA\\BB'")->fetchSingle());
|
||||
Assert::false($conn->query("SELECT 'AAxBB' LIKE %~like~", 'A\\B')->fetchSingle());
|
||||
Assert::true($conn->query("SELECT 'AA\\BB' LIKE %~like~", 'A\\B')->fetchSingle());
|
||||
};
|
||||
|
||||
$conn = new Dibi\Connection($config);
|
||||
|
@@ -25,10 +25,10 @@ class MockResult extends Dibi\Result
|
||||
}
|
||||
|
||||
|
||||
test('native text conversion preserves boolean values', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::Text);
|
||||
$result->setFormat(Type::Text, 'native');
|
||||
$result->setType('col', Type::TEXT);
|
||||
$result->setFormat(Type::TEXT, 'native');
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => true], $result->test(['col' => true]));
|
||||
@@ -36,9 +36,9 @@ test('native text conversion preserves boolean values', function () {
|
||||
});
|
||||
|
||||
|
||||
test('boolean conversion from diverse representations', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::Bool);
|
||||
$result->setType('col', Type::BOOL);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => true], $result->test(['col' => true]));
|
||||
@@ -58,9 +58,9 @@ test('boolean conversion from diverse representations', function () {
|
||||
});
|
||||
|
||||
|
||||
test('text conversion of booleans and numerics', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::Text);
|
||||
$result->setType('col', Type::TEXT);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => '1'], $result->test(['col' => true]));
|
||||
@@ -74,9 +74,9 @@ test('text conversion of booleans and numerics', function () {
|
||||
});
|
||||
|
||||
|
||||
test('float conversion with various numeric formats', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::Float);
|
||||
$result->setType('col', Type::FLOAT);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => 1.0], $result->test(['col' => true]));
|
||||
@@ -117,37 +117,6 @@ test('float conversion with various numeric formats', function () {
|
||||
Assert::same(['col' => '1.1e+10'], $result->test(['col' => '001.1e+10']));
|
||||
Assert::notSame(['col' => '1.1e+1'], $result->test(['col' => '1.1e+10']));
|
||||
|
||||
// negative
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-.0']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-.1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0.0']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-0.1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0.000']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-0.100']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1.0']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-1.1']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1.000']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-1.100']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-001.000']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-001.100']));
|
||||
Assert::same(['col' => -10.0], $result->test(['col' => '-10']));
|
||||
Assert::same(['col' => -11.0], $result->test(['col' => '-11']));
|
||||
Assert::same(['col' => -10.0], $result->test(['col' => '-0010']));
|
||||
Assert::same(['col' => -11.0], $result->test(['col' => '-0011']));
|
||||
Assert::same(['col' => '-0.00000000000000000001'], $result->test(['col' => '-0.00000000000000000001']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-012345678901234567890']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890.000']));
|
||||
Assert::same(['col' => '-12345678901234567890.1'], $result->test(['col' => '-012345678901234567890.100']));
|
||||
|
||||
Assert::same(['col' => '-1.1e+10'], $result->test(['col' => '-1.1e+10']));
|
||||
Assert::same(['col' => '-1.1e-10'], $result->test(['col' => '-1.1e-10']));
|
||||
Assert::same(['col' => '-1.1e+10'], $result->test(['col' => '-001.1e+10']));
|
||||
Assert::notSame(['col' => '-1.1e+1'], $result->test(['col' => '-1.1e+10']));
|
||||
|
||||
setlocale(LC_ALL, 'de_DE@euro', 'de_DE', 'deu_deu');
|
||||
Assert::same(['col' => 0.0], $result->test(['col' => '']));
|
||||
Assert::same(['col' => 0.0], $result->test(['col' => '0']));
|
||||
@@ -178,54 +147,25 @@ test('float conversion with various numeric formats', function () {
|
||||
Assert::same(['col' => 0.0], $result->test(['col' => 0.0]));
|
||||
Assert::same(['col' => 1.0], $result->test(['col' => 1]));
|
||||
Assert::same(['col' => 1.0], $result->test(['col' => 1.0]));
|
||||
|
||||
// Same but negative
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-.0']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-.1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0.0']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-0.1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0.000']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-0.100']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1.0']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-1.1']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1.000']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-1.100']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-001.000']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-001.100']));
|
||||
Assert::same(['col' => -10.0], $result->test(['col' => '-10']));
|
||||
Assert::same(['col' => -11.0], $result->test(['col' => '-11']));
|
||||
Assert::same(['col' => -10.0], $result->test(['col' => '-0010']));
|
||||
Assert::same(['col' => -11.0], $result->test(['col' => '-0011']));
|
||||
Assert::same(['col' => '-0.00000000000000000001'], $result->test(['col' => '-0.00000000000000000001']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-012345678901234567890']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890.000']));
|
||||
Assert::same(['col' => '-12345678901234567890.1'], $result->test(['col' => '-012345678901234567890.100']));
|
||||
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => -0]));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => -0.0]));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => -1]));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => -1.0]));
|
||||
|
||||
setlocale(LC_NUMERIC, 'C');
|
||||
});
|
||||
|
||||
|
||||
test('strict integer conversion with error on empty string', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::Integer);
|
||||
$result->setType('col', Type::INTEGER);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => 1], $result->test(['col' => true]));
|
||||
Assert::same(['col' => 0], $result->test(['col' => false]));
|
||||
|
||||
Assert::exception(
|
||||
fn() => Assert::same(['col' => 0], $result->test(['col' => ''])),
|
||||
TypeError::class,
|
||||
);
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
Assert::same(['col' => 0], @$result->test(['col' => ''])); // triggers warning since PHP 7.1
|
||||
} else {
|
||||
Assert::exception(function () use ($result) {
|
||||
Assert::same(['col' => 0], $result->test(['col' => '']));
|
||||
}, TypeError::class);
|
||||
}
|
||||
|
||||
Assert::same(['col' => 0], $result->test(['col' => '0']));
|
||||
Assert::same(['col' => 1], $result->test(['col' => '1']));
|
||||
@@ -244,15 +184,14 @@ test('strict integer conversion with error on empty string', function () {
|
||||
});
|
||||
|
||||
|
||||
test('dateTime conversion with object instantiation', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::DateTime);
|
||||
$result->setType('col', Type::DATETIME);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::exception(
|
||||
fn() => $result->test(['col' => true]),
|
||||
TypeError::class,
|
||||
);
|
||||
Assert::exception(function () use ($result) {
|
||||
$result->test(['col' => true]);
|
||||
}, TypeError::class);
|
||||
Assert::same(['col' => null], $result->test(['col' => false]));
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => '']));
|
||||
@@ -263,16 +202,15 @@ test('dateTime conversion with object instantiation', function () {
|
||||
});
|
||||
|
||||
|
||||
test('dateTime conversion using custom format', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::DateTime);
|
||||
$result->setFormat(Type::DateTime, 'Y-m-d H:i:s');
|
||||
$result->setType('col', Type::DATETIME);
|
||||
$result->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::exception(
|
||||
fn() => $result->test(['col' => true]),
|
||||
TypeError::class,
|
||||
);
|
||||
Assert::exception(function () use ($result) {
|
||||
$result->test(['col' => true]);
|
||||
}, TypeError::class);
|
||||
Assert::same(['col' => null], $result->test(['col' => false]));
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => '']));
|
||||
@@ -283,15 +221,14 @@ test('dateTime conversion using custom format', function () {
|
||||
});
|
||||
|
||||
|
||||
test('date conversion to DateTime instance', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::Date);
|
||||
$result->setType('col', Type::DATE);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::exception(
|
||||
fn() => $result->test(['col' => true]),
|
||||
TypeError::class,
|
||||
);
|
||||
Assert::exception(function () use ($result) {
|
||||
$result->test(['col' => true]);
|
||||
}, TypeError::class);
|
||||
Assert::same(['col' => null], $result->test(['col' => false]));
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => '']));
|
||||
@@ -300,15 +237,14 @@ test('date conversion to DateTime instance', function () {
|
||||
});
|
||||
|
||||
|
||||
test('time conversion to DateTime instance', function () {
|
||||
test('', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::Time);
|
||||
$result->setType('col', Type::TIME);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::exception(
|
||||
fn() => $result->test(['col' => true]),
|
||||
TypeError::class,
|
||||
);
|
||||
Assert::exception(function () use ($result) {
|
||||
$result->test(['col' => true]);
|
||||
}, TypeError::class);
|
||||
Assert::same(['col' => null], $result->test(['col' => false]));
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => '']));
|
||||
|
@@ -12,7 +12,7 @@ $conn->loadFile(__DIR__ . "/data/$config[system].sql");
|
||||
$res = $conn->query('SELECT * FROM [customers]');
|
||||
|
||||
// auto-converts this column to integer
|
||||
$res->setType('customer_id', Dibi\Type::DateTime);
|
||||
$res->setType('customer_id', Dibi\Type::DATETIME);
|
||||
|
||||
Assert::equal(new Dibi\Row([
|
||||
'customer_id' => new Dibi\DateTime('1970-01-01 01:00:01'),
|
||||
|
@@ -24,17 +24,13 @@ Assert::true(isset($row['title']));
|
||||
|
||||
|
||||
// missing
|
||||
Assert::error(
|
||||
fn() => $x = $row->missing,
|
||||
E_USER_NOTICE,
|
||||
"Attempt to read missing column 'missing'.",
|
||||
);
|
||||
Assert::error(function () use ($row) {
|
||||
$x = $row->missing;
|
||||
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
|
||||
|
||||
Assert::error(
|
||||
fn() => $x = $row['missing'],
|
||||
E_USER_NOTICE,
|
||||
"Attempt to read missing column 'missing'.",
|
||||
);
|
||||
Assert::error(function () use ($row) {
|
||||
$x = $row['missing'];
|
||||
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
|
||||
|
||||
Assert::false(isset($row->missing));
|
||||
Assert::false(isset($row['missing']));
|
||||
@@ -45,17 +41,13 @@ Assert::same(123, $row['missing'] ?? 123);
|
||||
|
||||
|
||||
// suggestions
|
||||
Assert::error(
|
||||
fn() => $x = $row->tilte,
|
||||
E_USER_NOTICE,
|
||||
"Attempt to read missing column 'tilte', did you mean 'title'?",
|
||||
);
|
||||
Assert::error(function () use ($row) {
|
||||
$x = $row->tilte;
|
||||
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
|
||||
|
||||
Assert::error(
|
||||
fn() => $x = $row['tilte'],
|
||||
E_USER_NOTICE,
|
||||
"Attempt to read missing column 'tilte', did you mean 'title'?",
|
||||
);
|
||||
Assert::error(function () use ($row) {
|
||||
$x = $row['tilte'];
|
||||
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
|
||||
|
||||
|
||||
// to array
|
||||
|
@@ -11,59 +11,82 @@ use Tester\Assert;
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
$tests = function ($conn) {
|
||||
// Limit and offset
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY',
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, 10),
|
||||
);
|
||||
$resource = $conn->getDriver()->getResource();
|
||||
$version = is_resource($resource)
|
||||
? sqlsrv_server_info($resource)['SQLServerVersion']
|
||||
: $resource->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
|
||||
// Limit only
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY',
|
||||
$conn->translate('SELECT 1 %lmt', 10),
|
||||
);
|
||||
// MsSQL2012+
|
||||
if (version_compare($version, '11.0') >= 0) {
|
||||
// Limit and offset
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY',
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, 10),
|
||||
);
|
||||
|
||||
// Offset only
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 10 ROWS',
|
||||
$conn->translate('SELECT 1 %ofs', 10),
|
||||
);
|
||||
// Limit only
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY',
|
||||
$conn->translate('SELECT 1 %lmt', 10),
|
||||
);
|
||||
|
||||
// Offset invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs', -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
// Offset only
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 10 ROWS',
|
||||
$conn->translate('SELECT 1 %ofs', 10),
|
||||
);
|
||||
|
||||
// Limit invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %lmt', -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
// Offset invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs', -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
|
||||
// Limit invalid, offset valid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
// Limit invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %lmt', -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
|
||||
// Limit valid, offset invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs %lmt', -10, 10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
// Limit invalid, offset valid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
|
||||
// Limit valid, offset invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs %lmt', -10, 10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
} else {
|
||||
Assert::same(
|
||||
'SELECT TOP (1) * FROM (SELECT 1) t',
|
||||
$conn->translate('SELECT 1 %lmt', 1),
|
||||
);
|
||||
|
||||
Assert::same(
|
||||
'SELECT 1',
|
||||
$conn->translate('SELECT 1 %lmt', -10),
|
||||
);
|
||||
|
||||
Assert::exception(
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, 10),
|
||||
Dibi\NotSupportedException::class,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$conn = new Dibi\Connection($config);
|
||||
|
151
tests/dibi/Strict.phpt
Normal file
151
tests/dibi/Strict.phpt
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tester\Assert;
|
||||
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
|
||||
class TestClass
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
public $public;
|
||||
|
||||
public static $publicStatic;
|
||||
|
||||
protected $protected;
|
||||
|
||||
|
||||
public function publicMethod()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public static function publicMethodStatic()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
protected function protectedMethod()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
protected static function protectedMethodS()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function getBar()
|
||||
{
|
||||
return 123;
|
||||
}
|
||||
|
||||
|
||||
public function isFoo()
|
||||
{
|
||||
return 456;
|
||||
}
|
||||
}
|
||||
|
||||
class TestChild extends TestClass
|
||||
{
|
||||
public function callParent()
|
||||
{
|
||||
parent::callParent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// calling
|
||||
Assert::exception(function () {
|
||||
$obj = new TestClass;
|
||||
$obj->undeclared();
|
||||
}, LogicException::class, 'Call to undefined method TestClass::undeclared().');
|
||||
|
||||
Assert::exception(function () {
|
||||
TestClass::undeclared();
|
||||
}, LogicException::class, 'Call to undefined static method TestClass::undeclared().');
|
||||
|
||||
Assert::exception(function () {
|
||||
$obj = new TestChild;
|
||||
$obj->callParent();
|
||||
}, LogicException::class, 'Call to undefined method parent::callParent().');
|
||||
|
||||
Assert::exception(function () {
|
||||
$obj = new TestClass;
|
||||
$obj->publicMethodX();
|
||||
}, LogicException::class, 'Call to undefined method TestClass::publicMethodX(), did you mean publicMethod()?');
|
||||
|
||||
Assert::exception(function () { // suggest static method
|
||||
$obj = new TestClass;
|
||||
$obj->publicMethodStaticX();
|
||||
}, LogicException::class, 'Call to undefined method TestClass::publicMethodStaticX(), did you mean publicMethodStatic()?');
|
||||
|
||||
Assert::exception(function () { // suggest only public method
|
||||
$obj = new TestClass;
|
||||
$obj->protectedMethodX();
|
||||
}, LogicException::class, 'Call to undefined method TestClass::protectedMethodX().');
|
||||
|
||||
|
||||
// writing
|
||||
Assert::exception(function () {
|
||||
$obj = new TestClass;
|
||||
$obj->undeclared = 'value';
|
||||
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$undeclared.');
|
||||
|
||||
Assert::exception(function () {
|
||||
$obj = new TestClass;
|
||||
$obj->publicX = 'value';
|
||||
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$publicX, did you mean $public?');
|
||||
|
||||
Assert::exception(function () { // suggest only non-static property
|
||||
$obj = new TestClass;
|
||||
$obj->publicStaticX = 'value';
|
||||
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$publicStaticX.');
|
||||
|
||||
Assert::exception(function () { // suggest only public property
|
||||
$obj = new TestClass;
|
||||
$obj->protectedX = 'value';
|
||||
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$protectedX.');
|
||||
|
||||
|
||||
// property getter
|
||||
$obj = new TestClass;
|
||||
Assert::false(isset($obj->bar));
|
||||
Assert::same(123, $obj->bar);
|
||||
Assert::false(isset($obj->foo));
|
||||
Assert::same(456, $obj->foo);
|
||||
|
||||
|
||||
// reading
|
||||
Assert::exception(function () {
|
||||
$obj = new TestClass;
|
||||
$val = $obj->undeclared;
|
||||
}, LogicException::class, 'Attempt to read undeclared property TestClass::$undeclared.');
|
||||
|
||||
Assert::exception(function () {
|
||||
$obj = new TestClass;
|
||||
$val = $obj->publicX;
|
||||
}, LogicException::class, 'Attempt to read undeclared property TestClass::$publicX, did you mean $public?');
|
||||
|
||||
Assert::exception(function () { // suggest only non-static property
|
||||
$obj = new TestClass;
|
||||
$val = $obj->publicStaticX;
|
||||
}, LogicException::class, 'Attempt to read undeclared property TestClass::$publicStaticX.');
|
||||
|
||||
Assert::exception(function () { // suggest only public property
|
||||
$obj = new TestClass;
|
||||
$val = $obj->protectedX;
|
||||
}, LogicException::class, 'Attempt to read undeclared property TestClass::$protectedX.');
|
||||
|
||||
|
||||
// unset/isset
|
||||
Assert::exception(function () {
|
||||
$obj = new TestClass;
|
||||
unset($obj->undeclared);
|
||||
}, LogicException::class, 'Attempt to unset undeclared property TestClass::$undeclared.');
|
||||
|
||||
Assert::false(isset($obj->undeclared));
|
@@ -13,16 +13,13 @@ switch ($config['system']) {
|
||||
case 'mysql':
|
||||
Assert::equal('10:20:30.0', $translator->formatValue(new DateInterval('PT10H20M30S'), null));
|
||||
Assert::equal('-1:00:00.0', $translator->formatValue(DateInterval::createFromDateString('-1 hour'), null));
|
||||
Assert::exception(
|
||||
fn() => $translator->formatValue(new DateInterval('P2Y4DT6H8M'), null),
|
||||
Dibi\NotSupportedException::class,
|
||||
'Only time interval is supported.',
|
||||
);
|
||||
Assert::exception(function () use ($translator) {
|
||||
$translator->formatValue(new DateInterval('P2Y4DT6H8M'), null);
|
||||
}, Dibi\NotSupportedException::class, 'Only time interval is supported.');
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert::exception(
|
||||
fn() => $translator->formatValue(new DateInterval('PT10H20M30S'), null),
|
||||
Dibi\Exception::class,
|
||||
);
|
||||
Assert::exception(function () use ($translator) {
|
||||
$translator->formatValue(new DateInterval('PT10H20M30S'), null);
|
||||
}, Dibi\Exception::class);
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpVersion 8.1
|
||||
* @dataProvider ../databases.ini
|
||||
*/
|
||||
|
||||
@@ -32,7 +33,7 @@ enum PureEnum
|
||||
|
||||
Assert::equal('1', $translator->formatValue(EnumInt::One, null));
|
||||
|
||||
Assert::equal(match ($config['system']) {
|
||||
Assert::equal(match ($config['driver']) {
|
||||
'sqlsrv' => "N'one'",
|
||||
default => "'one'",
|
||||
}, $translator->formatValue(EnumString::One, null));
|
||||
|
@@ -32,7 +32,7 @@ Assert::truthy($conn->fetchSingle('SELECT ? LIKE %like~', "a'a", "a'"));
|
||||
Assert::falsey($conn->fetchSingle('SELECT ? LIKE %like~', "b'", "%'"));
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %like~', "%'", "%'"));
|
||||
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %like~', 'a\a', 'a\\'));
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %like~', 'a\\a', 'a\\'));
|
||||
Assert::falsey($conn->fetchSingle('SELECT ? LIKE %like~', 'b\\', '%\\'));
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %like~', '%\\', '%\\'));
|
||||
|
||||
@@ -60,9 +60,9 @@ Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like', "a'a", "'a"));
|
||||
Assert::falsey($conn->fetchSingle('SELECT ? LIKE %~like', "'b", "'%"));
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like', "'%", "'%"));
|
||||
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like', 'a\a', '\a'));
|
||||
Assert::falsey($conn->fetchSingle('SELECT ? LIKE %~like', '\b', '\%'));
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like', '\%', '\%'));
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like', 'a\\a', '\\a'));
|
||||
Assert::falsey($conn->fetchSingle('SELECT ? LIKE %~like', '\\b', '\\%'));
|
||||
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like', '\\%', '\\%'));
|
||||
|
||||
|
||||
// contains
|
||||
|
@@ -91,11 +91,9 @@ Assert::same(
|
||||
);
|
||||
|
||||
// invalid input
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->translate('SELECT %s', (object) [123], ', %m', 123),
|
||||
Dibi\Exception::class,
|
||||
'SQL translate error: Invalid combination of type stdClass and modifier %s',
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->translate('SELECT %s', (object) [123], ', %m', 123);
|
||||
}, Dibi\Exception::class, 'SQL translate error: Invalid combination of type stdClass and modifier %s');
|
||||
Assert::same('SELECT **Invalid combination of type stdClass and modifier %s** , **Unknown or unexpected modifier %m**', $e->getSql());
|
||||
|
||||
Assert::same(
|
||||
@@ -178,10 +176,9 @@ Assert::same(
|
||||
);
|
||||
|
||||
if ($config['system'] === 'odbc') {
|
||||
Assert::exception(
|
||||
fn() => $conn->translate('SELECT * FROM [products] %lmt %ofs', 2, 1),
|
||||
Dibi\Exception::class,
|
||||
);
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->translate('SELECT * FROM [products] %lmt %ofs', 2, 1);
|
||||
}, Dibi\Exception::class);
|
||||
} else {
|
||||
// with limit = 2, offset = 1
|
||||
Assert::same(
|
||||
@@ -229,11 +226,9 @@ Assert::same(
|
||||
]),
|
||||
);
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->translate('SELECT %s', new DateTime('1212-09-26')),
|
||||
Dibi\Exception::class,
|
||||
'SQL translate error: Invalid combination of type Dibi\DateTime and modifier %s',
|
||||
);
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->translate('SELECT %s', new DateTime('1212-09-26'));
|
||||
}, Dibi\Exception::class, 'SQL translate error: Invalid combination of type Dibi\DateTime and modifier %s');
|
||||
|
||||
|
||||
|
||||
@@ -273,11 +268,9 @@ if ($config['system'] === 'postgre') {
|
||||
}
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->translate("SELECT '"),
|
||||
Dibi\Exception::class,
|
||||
'SQL translate error: Alone quote',
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->translate("SELECT '");
|
||||
}, Dibi\Exception::class, 'SQL translate error: Alone quote');
|
||||
Assert::same('SELECT **Alone quote**', $e->getSql());
|
||||
|
||||
Assert::match(
|
||||
@@ -654,17 +647,13 @@ Assert::same(
|
||||
$conn->translate('INSERT INTO [test.*]'),
|
||||
);
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->translate('INSERT INTO %i', 'ahoj'),
|
||||
Dibi\Exception::class,
|
||||
"Expected number, 'ahoj' given.",
|
||||
);
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->translate('INSERT INTO %i', 'ahoj');
|
||||
}, Dibi\Exception::class, "Expected number, 'ahoj' given.");
|
||||
|
||||
Assert::exception(
|
||||
fn() => $conn->translate('INSERT INTO %f', 'ahoj'),
|
||||
Dibi\Exception::class,
|
||||
"Expected number, 'ahoj' given.",
|
||||
);
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->translate('INSERT INTO %f', 'ahoj');
|
||||
}, Dibi\Exception::class, "Expected number, 'ahoj' given.");
|
||||
|
||||
|
||||
Assert::same(
|
||||
|
@@ -20,7 +20,7 @@ date_default_timezone_set('Europe/Prague');
|
||||
try {
|
||||
$config = Tester\Environment::loadData();
|
||||
} catch (Throwable $e) {
|
||||
$config = parse_ini_file(__DIR__ . '/../databases.ini', process_sections: true);
|
||||
$config = parse_ini_file(__DIR__ . '/../databases.ini', true);
|
||||
$config = reset($config);
|
||||
}
|
||||
|
||||
|
@@ -15,54 +15,41 @@ $conn = new Dibi\Connection($config);
|
||||
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => new Dibi\Connection([
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
new Dibi\Connection([
|
||||
'driver' => 'mysqli',
|
||||
'host' => 'localhost',
|
||||
'username' => 'unknown',
|
||||
'password' => 'unknown',
|
||||
]),
|
||||
Dibi\DriverException::class,
|
||||
);
|
||||
]);
|
||||
}, Dibi\DriverException::class);
|
||||
|
||||
Assert::null($e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('SELECT'),
|
||||
Dibi\DriverException::class,
|
||||
'%a% error in your SQL syntax;%a%',
|
||||
1064,
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('SELECT');
|
||||
}, Dibi\DriverException::class, '%a% error in your SQL syntax;%a%', 1064);
|
||||
|
||||
Assert::same('SELECT', $e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")'),
|
||||
Dibi\UniqueConstraintViolationException::class,
|
||||
"%a?%Duplicate entry '1' for key '%a?%PRIMARY'",
|
||||
1062,
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")');
|
||||
}, Dibi\UniqueConstraintViolationException::class, "%a?%Duplicate entry '1' for key '%a?%PRIMARY'", 1062);
|
||||
|
||||
Assert::same("INSERT INTO products (product_id, title) VALUES (1, 'New')", $e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('INSERT INTO products (title) VALUES (NULL)'),
|
||||
Dibi\NotNullConstraintViolationException::class,
|
||||
"%a?%Column 'title' cannot be null",
|
||||
1048,
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('INSERT INTO products (title) VALUES (NULL)');
|
||||
}, Dibi\NotNullConstraintViolationException::class, "%a?%Column 'title' cannot be null", 1048);
|
||||
|
||||
Assert::same('INSERT INTO products (title) VALUES (NULL)', $e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)'),
|
||||
Dibi\ForeignKeyConstraintViolationException::class,
|
||||
'%a% a foreign key constraint fails %a%',
|
||||
1452,
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)');
|
||||
}, Dibi\ForeignKeyConstraintViolationException::class, '%a% a foreign key constraint fails %a%', 1452);
|
||||
|
||||
Assert::same('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)', $e->getSql());
|
||||
|
@@ -15,40 +15,29 @@ $conn = new Dibi\Connection($config);
|
||||
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('SELECT INTO'),
|
||||
Dibi\DriverException::class,
|
||||
'%a?%syntax error %A%',
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('SELECT INTO');
|
||||
}, Dibi\DriverException::class, '%a?%syntax error %A%');
|
||||
|
||||
Assert::same('SELECT INTO', $e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")'),
|
||||
Dibi\UniqueConstraintViolationException::class,
|
||||
'%a% violates unique constraint %A%',
|
||||
'23505',
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")');
|
||||
}, Dibi\UniqueConstraintViolationException::class, '%a% violates unique constraint %A%', '23505');
|
||||
|
||||
Assert::same("INSERT INTO products (product_id, title) VALUES (1, 'New')", $e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('INSERT INTO products (title) VALUES (NULL)'),
|
||||
Dibi\NotNullConstraintViolationException::class,
|
||||
'%a?%null value in column "title"%a%violates not-null constraint%A?%',
|
||||
'23502',
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('INSERT INTO products (title) VALUES (NULL)');
|
||||
}, Dibi\NotNullConstraintViolationException::class, '%a?%null value in column "title"%a%violates not-null constraint%A?%', '23502');
|
||||
|
||||
Assert::same('INSERT INTO products (title) VALUES (NULL)', $e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)'),
|
||||
Dibi\ForeignKeyConstraintViolationException::class,
|
||||
'%a% violates foreign key constraint %A%',
|
||||
'23503',
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)');
|
||||
}, Dibi\ForeignKeyConstraintViolationException::class, '%a% violates foreign key constraint %A%', '23503');
|
||||
|
||||
Assert::same('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)', $e->getSql());
|
||||
|
@@ -15,32 +15,23 @@ $conn = new Dibi\Connection($config);
|
||||
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('SELECT'),
|
||||
Dibi\DriverException::class,
|
||||
'%a%',
|
||||
1,
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('SELECT');
|
||||
}, Dibi\DriverException::class, '%a%', 1);
|
||||
|
||||
Assert::same('SELECT', $e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")'),
|
||||
Dibi\UniqueConstraintViolationException::class,
|
||||
null,
|
||||
19,
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")');
|
||||
}, Dibi\UniqueConstraintViolationException::class, null, 19);
|
||||
|
||||
Assert::same("INSERT INTO products (product_id, title) VALUES (1, 'New')", $e->getSql());
|
||||
|
||||
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('INSERT INTO products (title) VALUES (NULL)'),
|
||||
Dibi\NotNullConstraintViolationException::class,
|
||||
null,
|
||||
19,
|
||||
);
|
||||
$e = Assert::exception(function () use ($conn) {
|
||||
$conn->query('INSERT INTO products (title) VALUES (NULL)');
|
||||
}, Dibi\NotNullConstraintViolationException::class, null, 19);
|
||||
|
||||
Assert::same('INSERT INTO products (title) VALUES (NULL)', $e->getSql());
|
||||
|
||||
|
Reference in New Issue
Block a user