diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 97e6c166d..9e412ba6e 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.3 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.3 coverage: none - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 5a39324eb..6f22b2fbf 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.2 coverage: none - run: composer install --no-progress --prefer-dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d31760993..5f67cc4f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.0', '8.1', '8.2', '8.3', '8.4'] + php: ['8.2', '8.3', '8.4'] fail-fast: false @@ -105,14 +105,14 @@ jobs: - 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@v3 + uses: actions/upload-artifact@v4 with: - name: output + name: output-${{ matrix.php }} path: tests/**/output - name: Save Code Coverage - if: ${{ matrix.php == '8.0' }} + if: ${{ matrix.php == '8.2' }} env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/composer.json b/composer.json index eb8aa73e7..d5e2f9c2e 100644 --- a/composer.json +++ b/composer.json @@ -11,29 +11,32 @@ } ], "require": { - "php": "8.0 - 8.4" + "php": "8.2 - 8.4" }, "require-dev": { "tracy/tracy": "^2.9", "nette/tester": "^2.5", "nette/di": "^3.1", - "phpstan/phpstan": "^1.0", + "phpstan/phpstan-nette": "^2.0", "jetbrains/phpstorm-attributes": "^1.0" }, "replace": { "dg/dibi": "*" }, "autoload": { - "classmap": ["src/"] + "classmap": ["src/"], + "psr-4": { + "Dibi\\": "src" + } }, - "minimum-stability": "dev", + "minimum-stability": "stable", "scripts": { "phpstan": "phpstan analyse", "tester": "tester tests -s" }, "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "6.0-dev" } } } diff --git a/examples/using-substitutions.php b/examples/using-substitutions.php index d10d85a1e..058ed47b4 100644 --- a/examples/using-substitutions.php +++ b/examples/using-substitutions.php @@ -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_'); diff --git a/readme.md b/readme.md index e75f27737..b225b2030 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ Install Dibi via Composer: composer require dibi/dibi ``` -The Dibi 5.0 requires PHP version 8.0 and supports PHP up to 8.4. +The Dibi 5.0 requires PHP version 8.2 and supports PHP up to 8.4. Usage diff --git a/src/Dibi/Bridges/Nette/DibiExtension22.php b/src/Dibi/Bridges/Nette/DibiExtension22.php index a01e1e41c..8f310b034 100644 --- a/src/Dibi/Bridges/Nette/DibiExtension22.php +++ b/src/Dibi/Bridges/Nette/DibiExtension22.php @@ -19,14 +19,10 @@ */ class DibiExtension22 extends Nette\DI\CompilerExtension { - private ?bool $debugMode; - private ?bool $cliMode; - - - public function __construct(?bool $debugMode = null, ?bool $cliMode = null) - { - $this->debugMode = $debugMode; - $this->cliMode = $cliMode; + public function __construct( + private ?bool $debugMode = null, + private ?bool $cliMode = null, + ) { } diff --git a/src/Dibi/Bridges/Nette/DibiExtension3.php b/src/Dibi/Bridges/Nette/DibiExtension3.php index b8b4cece0..e954ecf33 100644 --- a/src/Dibi/Bridges/Nette/DibiExtension3.php +++ b/src/Dibi/Bridges/Nette/DibiExtension3.php @@ -13,6 +13,7 @@ use Nette; use Nette\Schema\Expect; use Tracy; +use function is_array; /** @@ -20,14 +21,10 @@ */ class DibiExtension3 extends Nette\DI\CompilerExtension { - private ?bool $debugMode; - private ?bool $cliMode; - - - public function __construct(?bool $debugMode = null, ?bool $cliMode = null) - { - $this->debugMode = $debugMode; - $this->cliMode = $cliMode; + public function __construct( + private ?bool $debugMode = null, + private ?bool $cliMode = null, + ) { } diff --git a/src/Dibi/Bridges/Tracy/Panel.php b/src/Dibi/Bridges/Tracy/Panel.php index 97694b519..f53e50028 100644 --- a/src/Dibi/Bridges/Tracy/Panel.php +++ b/src/Dibi/Bridges/Tracy/Panel.php @@ -13,6 +13,7 @@ use Dibi\Event; use Dibi\Helpers; use Tracy; +use function count, is_string, strlen; /** @@ -21,23 +22,22 @@ class Panel implements Tracy\IBarPanel { public static int $maxLength = 1000; - public bool|string $explain; - public int $filter; + private array $events = []; - public function __construct(bool $explain = true, ?int $filter = null) - { - $this->filter = $filter ?: Event::QUERY; - $this->explain = $explain; + public function __construct( + public bool|string $explain = true, + public int $filter = Event::QUERY, + ) { } public function register(Dibi\Connection $connection): void { Tracy\Debugger::getBar()->addPanel($this); - Tracy\Debugger::getBlueScreen()->addPanel([self::class, 'renderException']); - $connection->onEvent[] = [$this, 'logEvent']; + Tracy\Debugger::getBlueScreen()->addPanel(self::renderException(...)); + $connection->onEvent[] = $this->logEvent(...); } diff --git a/src/Dibi/Connection.php b/src/Dibi/Connection.php index 916d0956f..97f455584 100644 --- a/src/Dibi/Connection.php +++ b/src/Dibi/Connection.php @@ -11,6 +11,8 @@ use JetBrains\PhpStorm\Language; use Traversable; +use function array_key_exists, is_array, sprintf; +use const PHP_SAPI; /** @@ -19,15 +21,28 @@ * @property-read int $affectedRows * @property-read int $insertId */ -class Connection implements IConnection +class Connection { + 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, + ]; + /** function (Event $event); Occurs after query is executed */ public ?array $onEvent = []; private array $config; /** @var string[] resultset formats */ private array $formats; - private ?Driver $driver = null; + private ?Drivers\Connection $driver = null; + private Drivers\Engine $engine; private ?Translator $translator = null; /** @var array */ @@ -120,17 +135,16 @@ public function __destruct() */ final public function connect(): void { - if ($this->config['driver'] instanceof Driver) { + if ($this->config['driver'] instanceof Drivers\Connection) { $this->driver = $this->config['driver']; $this->translator = new Translator($this); return; - } elseif (is_subclass_of($this->config['driver'], Driver::class)) { + } elseif (is_subclass_of($this->config['driver'], Drivers\Connection::class)) { $class = $this->config['driver']; } else { - $class = preg_replace(['#\W#', '#sql#'], ['_', 'Sql'], ucfirst(strtolower($this->config['driver']))); - $class = "Dibi\\Drivers\\{$class}Driver"; + $class = self::Drivers[strtolower($this->config['driver'])] ?? throw new Exception("Unknown driver '{$this->config['driver']}'."); if (!class_exists($class)) { throw new Exception("Unable to create instance of Dibi driver '$class'."); } @@ -196,7 +210,7 @@ final public function getConfig(?string $key = null, $default = null): mixed /** * Returns the driver and connects to a database in lazy mode. */ - final public function getDriver(): Driver + final public function getDriver(): Drivers\Connection { if (!$this->driver) { $this->connect(); @@ -284,7 +298,7 @@ final public function nativeQuery(#[Language('SQL')] string $sql): Result throw $e; } - $res = $this->createResultSet($res ?: new Drivers\NoDataResult(max(0, $this->driver->getAffectedRows()))); + $res = $this->createResultSet($res ?: new Drivers\Dummy\Result(max(0, $this->driver->getAffectedRows()))); if ($event) { $this->onEvent($event->done($res)); } @@ -448,7 +462,7 @@ public function transaction(callable $callback): mixed /** * Result set factory. */ - public function createResultSet(ResultDriver $resultDriver): Result + public function createResultSet(Drivers\Result $resultDriver): Result { return (new Result($resultDriver, $this->config['result']['normalize'] ?? true)) ->setFormats($this->formats); @@ -657,6 +671,15 @@ public function loadFile(string $file, ?callable $onProgress = null): int } + public function getDatabaseEngine(): Drivers\Engine + { + if (!$this->driver) { // TODO + $this->connect(); + } + return $this->engine ??= $this->driver->getReflector(); + } + + /** * Gets a information about the current database. */ @@ -666,7 +689,7 @@ public function getDatabaseInfo(): Reflection\Database $this->connect(); } - return new Reflection\Database($this->driver->getReflector(), $this->config['database'] ?? null); + return new Reflection\Database($this->getDatabaseEngine(), $this->config['database'] ?? null); } diff --git a/src/Dibi/DataSource.php b/src/Dibi/DataSource.php index 89aa37071..df10617c2 100644 --- a/src/Dibi/DataSource.php +++ b/src/Dibi/DataSource.php @@ -9,14 +9,16 @@ namespace Dibi; +use function func_get_args, is_array, strpbrk; + /** * Default implementation of IDataSource. */ class DataSource implements IDataSource { - private Connection $connection; - private string $sql; + private readonly Connection $connection; + private readonly string $sql; private ?Result $result = null; private ?int $count = null; private ?int $totalCount = null; @@ -33,7 +35,7 @@ class DataSource implements IDataSource public function __construct(string $sql, Connection $connection) { $this->sql = strpbrk($sql, " \t\r\n") === false - ? $connection->getDriver()->escapeIdentifier($sql) // table name + ? $connection->getDatabaseEngine()->escapeIdentifier($sql) // table name : '(' . $sql . ') t'; // SQL command $this->connection = $connection; } diff --git a/src/Dibi/Drivers/Connection.php b/src/Dibi/Drivers/Connection.php new file mode 100644 index 000000000..1f98ec158 --- /dev/null +++ b/src/Dibi/Drivers/Connection.php @@ -0,0 +1,77 @@ +rows = $rows; + public function __construct( + private readonly int $rows, + ) { } diff --git a/src/Dibi/Drivers/Engine.php b/src/Dibi/Drivers/Engine.php new file mode 100644 index 000000000..d744b0cde --- /dev/null +++ b/src/Dibi/Drivers/Engine.php @@ -0,0 +1,60 @@ +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; + } - public function __construct(Dibi\Driver $driver) + /** + * Encodes string for use in a LIKE statement. + */ + public function escapeLike(string $value, int $pos): string { - $this->driver = $driver; + $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 . ')'; + } } diff --git a/src/Dibi/Drivers/MySqlReflector.php b/src/Dibi/Drivers/Engines/MySQLEngine.php similarity index 65% rename from src/Dibi/Drivers/MySqlReflector.php rename to src/Dibi/Drivers/Engines/MySQLEngine.php index dbd55828a..4a6d15df0 100644 --- a/src/Dibi/Drivers/MySqlReflector.php +++ b/src/Dibi/Drivers/Engines/MySQLEngine.php @@ -7,23 +7,82 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\Engines; use Dibi; +use Dibi\Drivers\Connection; +use Dibi\Drivers\Engine; /** * The reflector for MySQL databases. * @internal */ -class MySqlReflector implements Dibi\Reflector +class MySQLEngine implements Engine { - private Dibi\Driver $driver; + public function __construct( + private readonly Connection $driver, + ) { + } + + + 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 __construct(Dibi\Driver $driver) + public function escapeDateInterval(\DateInterval $value): string { - $this->driver = $driver; + 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 : ''); + } } @@ -50,7 +109,7 @@ public function getTables(): array */ public function getColumns(string $table): array { - $res = $this->driver->query("SHOW FULL COLUMNS FROM {$this->driver->escapeIdentifier($table)}"); + $res = $this->driver->query("SHOW FULL COLUMNS FROM {$this->escapeIdentifier($table)}"); $columns = []; while ($row = $res->fetch(true)) { $type = explode('(', $row['Type']); @@ -75,7 +134,7 @@ public function getColumns(string $table): array */ public function getIndexes(string $table): array { - $res = $this->driver->query("SHOW INDEX FROM {$this->driver->escapeIdentifier($table)}"); + $res = $this->driver->query("SHOW INDEX FROM {$this->escapeIdentifier($table)}"); $indexes = []; while ($row = $res->fetch(true)) { $indexes[$row['Key_name']]['name'] = $row['Key_name']; diff --git a/src/Dibi/Drivers/OdbcReflector.php b/src/Dibi/Drivers/Engines/ODBCEngine.php similarity index 51% rename from src/Dibi/Drivers/OdbcReflector.php rename to src/Dibi/Drivers/Engines/ODBCEngine.php index 3285fe034..2f1523d57 100644 --- a/src/Dibi/Drivers/OdbcReflector.php +++ b/src/Dibi/Drivers/Engines/ODBCEngine.php @@ -7,22 +7,78 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\Engines; use Dibi; +use Dibi\Drivers\Connection; +use Dibi\Drivers\Engine; /** * The reflector for ODBC connections. */ -class OdbcReflector implements Dibi\Reflector +class ODBCEngine implements Engine { - private Dibi\Driver $driver; + public function __construct( + private readonly Connection $driver, + ) { + } + + + 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#'); // TODO + } - public function __construct(Dibi\Driver $driver) + public function escapeDateInterval(\DateInterval $value): string { - $this->driver = $driver; + 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'; + } } diff --git a/src/Dibi/Drivers/Engines/OracleEngine.php b/src/Dibi/Drivers/Engines/OracleEngine.php new file mode 100644 index 000000000..deb4ddd01 --- /dev/null +++ b/src/Dibi/Drivers/Engines/OracleEngine.php @@ -0,0 +1,153 @@ +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; + } +} diff --git a/src/Dibi/Drivers/PostgreReflector.php b/src/Dibi/Drivers/Engines/PostgreSQLEngine.php similarity index 74% rename from src/Dibi/Drivers/PostgreReflector.php rename to src/Dibi/Drivers/Engines/PostgreSQLEngine.php index 29a33c15f..5accda2c7 100644 --- a/src/Dibi/Drivers/PostgreReflector.php +++ b/src/Dibi/Drivers/Engines/PostgreSQLEngine.php @@ -7,24 +7,83 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\Engines; use Dibi; +use Dibi\Drivers\Connection; +use Dibi\Drivers\Engine; /** * The reflector for PostgreSQL database. */ -class PostgreReflector implements Dibi\Reflector +class PostgreSQLEngine implements Engine { - private Dibi\Driver $driver; - private string $version; + public function __construct( + private readonly Connection $driver, + ) { + } + + + 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 __construct(Dibi\Driver $driver, string $version) + public function escapeBool(bool $value): string { - $this->driver = $driver; - $this->version = $version; + 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; + } } @@ -43,18 +102,15 @@ public function getTables(): array FROM information_schema.tables WHERE - table_schema = ANY (current_schemas(false))"; + table_schema = ANY (current_schemas(false)) - if ($this->version >= 9.3) { - $query .= ' - UNION ALL - SELECT - matviewname, 1 - FROM - pg_matviews - WHERE - schemaname = ANY (current_schemas(false))'; - } + UNION ALL + SELECT + matviewname, 1 + FROM + pg_matviews + WHERE + schemaname = ANY (current_schemas(false))"; $res = $this->driver->query($query); $tables = []; @@ -71,7 +127,7 @@ public function getTables(): array */ public function getColumns(string $table): array { - $_table = $this->driver->escapeText($this->driver->escapeIdentifier($table)); + $_table = $this->driver->escapeText($this->escapeIdentifier($table)); $res = $this->driver->query(" SELECT * @@ -131,7 +187,7 @@ public function getColumns(string $table): array */ public function getIndexes(string $table): array { - $_table = $this->driver->escapeText($this->driver->escapeIdentifier($table)); + $_table = $this->driver->escapeText($this->escapeIdentifier($table)); $res = $this->driver->query(" SELECT a.attnum AS ordinal_position, @@ -181,7 +237,7 @@ public function getIndexes(string $table): array */ public function getForeignKeys(string $table): array { - $_table = $this->driver->escapeText($this->driver->escapeIdentifier($table)); + $_table = $this->driver->escapeText($this->escapeIdentifier($table)); $res = $this->driver->query(" SELECT diff --git a/src/Dibi/Drivers/SqlsrvReflector.php b/src/Dibi/Drivers/Engines/SQLServerEngine.php similarity index 65% rename from src/Dibi/Drivers/SqlsrvReflector.php rename to src/Dibi/Drivers/Engines/SQLServerEngine.php index 32fec929c..5994f17ff 100644 --- a/src/Dibi/Drivers/SqlsrvReflector.php +++ b/src/Dibi/Drivers/Engines/SQLServerEngine.php @@ -7,22 +7,80 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\Engines; use Dibi; +use Dibi\Drivers\Connection; +use Dibi\Drivers\Engine; /** * The reflector for Microsoft SQL Server and SQL Azure databases. */ -class SqlsrvReflector implements Dibi\Reflector +class SQLServerEngine implements Engine { - private Dibi\Driver $driver; + public function __construct( + private readonly Connection $driver, + ) { + } + + + 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 __construct(Dibi\Driver $driver) + public function escapeDateTime(\DateTimeInterface $value): string { - $this->driver = $driver; + 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); + } } diff --git a/src/Dibi/Drivers/SqliteReflector.php b/src/Dibi/Drivers/Engines/SQLiteEngine.php similarity index 64% rename from src/Dibi/Drivers/SqliteReflector.php rename to src/Dibi/Drivers/Engines/SQLiteEngine.php index a85018e40..f9418709c 100644 --- a/src/Dibi/Drivers/SqliteReflector.php +++ b/src/Dibi/Drivers/Engines/SQLiteEngine.php @@ -7,22 +7,76 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\Engines; use Dibi; +use Dibi\Drivers\Connection; +use Dibi\Drivers\Engine; /** * The reflector for SQLite database. */ -class SqliteReflector implements Dibi\Reflector +class SQLiteEngine implements Engine { - private Dibi\Driver $driver; + public function __construct( + private readonly Connection $driver, + ) { + } + + + 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); // TODO + } - public function __construct(Dibi\Driver $driver) + public function escapeDateTime(\DateTimeInterface $value): string { - $this->driver = $driver; + 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 : ''); + } } @@ -51,7 +105,7 @@ public function getTables(): array */ public function getColumns(string $table): array { - $res = $this->driver->query("PRAGMA table_info({$this->driver->escapeIdentifier($table)})"); + $res = $this->driver->query("PRAGMA table_info({$this->escapeIdentifier($table)})"); $columns = []; while ($row = $res->fetch(true)) { $column = $row['name']; @@ -78,7 +132,7 @@ public function getColumns(string $table): array */ public function getIndexes(string $table): array { - $res = $this->driver->query("PRAGMA index_list({$this->driver->escapeIdentifier($table)})"); + $res = $this->driver->query("PRAGMA index_list({$this->escapeIdentifier($table)})"); $indexes = []; while ($row = $res->fetch(true)) { $indexes[$row['name']]['name'] = $row['name']; @@ -86,7 +140,7 @@ public function getIndexes(string $table): array } foreach ($indexes as $index => $values) { - $res = $this->driver->query("PRAGMA index_info({$this->driver->escapeIdentifier($index)})"); + $res = $this->driver->query("PRAGMA index_info({$this->escapeIdentifier($index)})"); while ($row = $res->fetch(true)) { $indexes[$index]['columns'][$row['seqno']] = $row['name']; } @@ -129,7 +183,7 @@ public function getIndexes(string $table): array */ public function getForeignKeys(string $table): array { - $res = $this->driver->query("PRAGMA foreign_key_list({$this->driver->escapeIdentifier($table)})"); + $res = $this->driver->query("PRAGMA foreign_key_list({$this->escapeIdentifier($table)})"); $keys = []; while ($row = $res->fetch(true)) { $keys[$row['id']]['name'] = $row['id']; // foreign key name diff --git a/src/Dibi/Drivers/FirebirdDriver.php b/src/Dibi/Drivers/Firebird/Connection.php similarity index 77% rename from src/Dibi/Drivers/FirebirdDriver.php rename to src/Dibi/Drivers/Firebird/Connection.php index cef0b98b2..7a054e29c 100644 --- a/src/Dibi/Drivers/FirebirdDriver.php +++ b/src/Dibi/Drivers/Firebird/Connection.php @@ -7,10 +7,12 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\Ibase; use Dibi; +use Dibi\Drivers; use Dibi\Helpers; +use function is_resource; /** @@ -24,11 +26,11 @@ * - 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 FirebirdDriver implements Dibi\Driver +class Connection implements Drivers\Connection { public const ErrorExceptionThrown = -836; - /** @deprecated use FirebirdDriver::ErrorExceptionThrown */ + #[\Deprecated('use FirebirdDriver::ErrorExceptionThrown')] public const ERROR_EXCEPTION_THROWN = self::ErrorExceptionThrown; /** @var resource */ @@ -85,7 +87,7 @@ public function disconnect(): void * Executes the SQL query. * @throws Dibi\DriverException|Dibi\Exception */ - public function query(string $sql): ?Dibi\ResultDriver + public function query(string $sql): ?Result { $resource = $this->inTransaction ? $this->transaction @@ -199,9 +201,9 @@ public function getResource(): mixed /** * Returns the connection reflector. */ - public function getReflector(): Dibi\Reflector + public function getReflector(): Drivers\Engine { - return new FirebirdReflector($this); + return new Drivers\Engines\FirebirdEngine($this); } @@ -209,9 +211,9 @@ public function getReflector(): Dibi\Reflector * Result set driver factory. * @param resource $resource */ - public function createResultDriver($resource): FirebirdResult + public function createResultDriver($resource): Result { - return new FirebirdResult($resource); + return new Result($resource); } @@ -231,56 +233,4 @@ public function escapeBinary(string $value): string { 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 . ')'; - } - } } diff --git a/src/Dibi/Drivers/FirebirdResult.php b/src/Dibi/Drivers/Firebird/Result.php similarity index 88% rename from src/Dibi/Drivers/FirebirdResult.php rename to src/Dibi/Drivers/Firebird/Result.php index a4c0ba782..3c3d6b174 100644 --- a/src/Dibi/Drivers/FirebirdResult.php +++ b/src/Dibi/Drivers/Firebird/Result.php @@ -7,27 +7,23 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\Ibase; use Dibi; +use Dibi\Drivers; use Dibi\Helpers; +use function is_resource; /** * The driver for Firebird/InterBase result set. */ -class FirebirdResult implements Dibi\ResultDriver +class Result implements Drivers\Result { - /** @var resource */ - private $resultSet; - - - /** - * @param resource $resultSet - */ - public function __construct($resultSet) - { - $this->resultSet = $resultSet; + public function __construct( + /** @var resource */ + private $resultSet, + ) { } @@ -51,7 +47,7 @@ public function fetch(bool $assoc): ?array : @ibase_fetch_row($this->resultSet, IBASE_TEXT); // intentionally @ if (ibase_errcode()) { - if (ibase_errcode() === FirebirdDriver::ERROR_EXCEPTION_THROWN) { + if (ibase_errcode() === Connection::ERROR_EXCEPTION_THROWN) { preg_match('/exception (\d+) (\w+) (.*)/is', ibase_errmsg(), $match); throw new Dibi\ProcedureException($match[3], $match[1], $match[2]); diff --git a/src/Dibi/Drivers/MySqliDriver.php b/src/Dibi/Drivers/MySQLi/Connection.php similarity index 78% rename from src/Dibi/Drivers/MySqliDriver.php rename to src/Dibi/Drivers/MySQLi/Connection.php index 41e7c6062..26061089e 100644 --- a/src/Dibi/Drivers/MySqliDriver.php +++ b/src/Dibi/Drivers/MySQLi/Connection.php @@ -7,9 +7,12 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\MySQLi; use Dibi; +use Dibi\Drivers; +use function in_array; +use const MYSQLI_REPORT_OFF, MYSQLI_STORE_RESULT, MYSQLI_USE_RESULT, PREG_SET_ORDER; /** @@ -30,19 +33,19 @@ * - sqlmode => see http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html * - resource (mysqli) => existing connection resource */ -class MySqliDriver implements Dibi\Driver +class Connection implements Drivers\Connection { public const ErrorAccessDenied = 1045; public const ErrorDuplicateEntry = 1062; public const ErrorDataTruncated = 1265; - /** @deprecated use MySqliDriver::ErrorAccessDenied */ + #[\Deprecated('use MySqliDriver::ErrorAccessDenied')] public const ERROR_ACCESS_DENIED = self::ErrorAccessDenied; - /** @deprecated use MySqliDriver::ErrorDuplicateEntry */ + #[\Deprecated('use MySqliDriver::ErrorDuplicateEntry')] public const ERROR_DUPLICATE_ENTRY = self::ErrorDuplicateEntry; - /** @deprecated use MySqliDriver::ErrorDataTruncated */ + #[\Deprecated('use MySqliDriver::ErrorDataTruncated')] public const ERROR_DATA_TRUNCATED = self::ErrorDataTruncated; private \mysqli $connection; @@ -146,7 +149,7 @@ public function ping(): bool * Executes the SQL query. * @throws Dibi\DriverException */ - public function query(string $sql): ?Dibi\ResultDriver + public function query(string $sql): ?Result { $res = @$this->connection->query($sql, $this->buffered ? MYSQLI_STORE_RESULT : MYSQLI_USE_RESULT); // intentionally @ @@ -263,18 +266,18 @@ public function getResource(): ?\mysqli /** * Returns the connection reflector. */ - public function getReflector(): Dibi\Reflector + public function getReflector(): Drivers\Engine { - return new MySqlReflector($this); + return new Drivers\Engines\MySQLEngine($this); } /** * Result set driver factory. */ - public function createResultDriver(\mysqli_result $result): MySqliResult + public function createResultDriver(\mysqli_result $result): Result { - return new MySqliResult($result, $this->buffered); + return new Result($result, $this->buffered); } @@ -294,64 +297,4 @@ public function escapeBinary(string $value): string { 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 : ''); - } - } } diff --git a/src/Dibi/Drivers/MySqliResult.php b/src/Dibi/Drivers/MySQLi/Result.php similarity index 89% rename from src/Dibi/Drivers/MySqliResult.php rename to src/Dibi/Drivers/MySQLi/Result.php index 4469ef0c9..d9cb27f35 100644 --- a/src/Dibi/Drivers/MySqliResult.php +++ b/src/Dibi/Drivers/MySQLi/Result.php @@ -7,24 +7,22 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\MySQLi; 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 MySqliResult implements Dibi\ResultDriver +class Result implements Drivers\Result { - private \mysqli_result $resultSet; - private bool $buffered; - - - public function __construct(\mysqli_result $resultSet, bool $buffered) - { - $this->resultSet = $resultSet; - $this->buffered = $buffered; + public function __construct( + private readonly \mysqli_result $resultSet, + private readonly bool $buffered, + ) { } diff --git a/src/Dibi/Drivers/OracleDriver.php b/src/Dibi/Drivers/OCI8/Connection.php similarity index 71% rename from src/Dibi/Drivers/OracleDriver.php rename to src/Dibi/Drivers/OCI8/Connection.php index 46a757dbd..9838cfdfc 100644 --- a/src/Dibi/Drivers/OracleDriver.php +++ b/src/Dibi/Drivers/OCI8/Connection.php @@ -7,9 +7,11 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\OCI8; use Dibi; +use Dibi\Drivers; +use function in_array, is_resource; /** @@ -25,7 +27,7 @@ * - resource (resource) => existing connection resource * - persistent => Creates persistent connections with oci_pconnect instead of oci_new_connect */ -class OracleDriver implements Dibi\Driver +class Connection implements Drivers\Connection { /** @var resource */ private $connection; @@ -76,7 +78,7 @@ public function disconnect(): void * Executes the SQL query. * @throws Dibi\DriverException */ - public function query(string $sql): ?Dibi\ResultDriver + public function query(string $sql): ?Result { $this->affectedRows = null; $res = oci_parse($this->connection, $sql); @@ -189,9 +191,9 @@ public function getResource(): mixed /** * Returns the connection reflector. */ - public function getReflector(): Dibi\Reflector + public function getReflector(): Drivers\Engine { - return new OracleReflector($this); + return new Drivers\Engines\OracleEngine($this); } @@ -199,9 +201,9 @@ public function getReflector(): Dibi\Reflector * Result set driver factory. * @param resource $resource */ - public function createResultDriver($resource): OracleResult + public function createResultDriver($resource): Result { - return new OracleResult($resource); + return new Result($resource); } @@ -221,70 +223,4 @@ public function escapeBinary(string $value): string { 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; - } - } } diff --git a/src/Dibi/Drivers/OracleResult.php b/src/Dibi/Drivers/OCI8/Result.php similarity index 89% rename from src/Dibi/Drivers/OracleResult.php rename to src/Dibi/Drivers/OCI8/Result.php index b1590546a..155389552 100644 --- a/src/Dibi/Drivers/OracleResult.php +++ b/src/Dibi/Drivers/OCI8/Result.php @@ -7,26 +7,22 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\OCI8; use Dibi; +use Dibi\Drivers; +use function is_resource; /** * The driver for Oracle result set. */ -class OracleResult implements Dibi\ResultDriver +class Result implements Drivers\Result { - /** @var resource */ - private $resultSet; - - - /** - * @param resource $resultSet - */ - public function __construct($resultSet) - { - $this->resultSet = $resultSet; + public function __construct( + /** @var resource */ + private $resultSet, + ) { } diff --git a/src/Dibi/Drivers/OdbcDriver.php b/src/Dibi/Drivers/ODBC/Connection.php similarity index 73% rename from src/Dibi/Drivers/OdbcDriver.php rename to src/Dibi/Drivers/ODBC/Connection.php index a5471c35f..3fb632b2c 100644 --- a/src/Dibi/Drivers/OdbcDriver.php +++ b/src/Dibi/Drivers/ODBC/Connection.php @@ -7,9 +7,11 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\ODBC; use Dibi; +use Dibi\Drivers; +use function is_resource; /** @@ -23,7 +25,7 @@ * - resource (resource) => existing connection resource * - microseconds (bool) => use microseconds in datetime format? */ -class OdbcDriver implements Dibi\Driver +class Connection implements Drivers\Connection { /** @var resource */ private $connection; @@ -76,7 +78,7 @@ public function disconnect(): void * Executes the SQL query. * @throws Dibi\DriverException */ - public function query(string $sql): ?Dibi\ResultDriver + public function query(string $sql): ?Result { $this->affectedRows = null; $res = @odbc_exec($this->connection, $sql); // intentionally @ @@ -175,9 +177,9 @@ public function getResource(): mixed /** * Returns the connection reflector. */ - public function getReflector(): Dibi\Reflector + public function getReflector(): Drivers\Engine { - return new OdbcReflector($this); + return new Drivers\Engines\ODBCEngine($this); } @@ -185,9 +187,9 @@ public function getReflector(): Dibi\Reflector * Result set driver factory. * @param resource $resource */ - public function createResultDriver($resource): OdbcResult + public function createResultDriver($resource): Result { - return new OdbcResult($resource); + return new Result($resource); } @@ -207,61 +209,4 @@ public function escapeBinary(string $value): string { 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'; - } - } } diff --git a/src/Dibi/Drivers/OdbcResult.php b/src/Dibi/Drivers/ODBC/Result.php similarity index 90% rename from src/Dibi/Drivers/OdbcResult.php rename to src/Dibi/Drivers/ODBC/Result.php index c8d88e7db..ad2a8fb4c 100644 --- a/src/Dibi/Drivers/OdbcResult.php +++ b/src/Dibi/Drivers/ODBC/Result.php @@ -7,27 +7,25 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\ODBC; use Dibi; +use Dibi\Drivers; +use function is_resource; /** * The driver interacting with result set via ODBC connections. */ -class OdbcResult implements Dibi\ResultDriver +class Result implements Drivers\Result { - /** @var resource */ - private $resultSet; private int $row = 0; - /** - * @param resource $resultSet - */ - public function __construct($resultSet) - { - $this->resultSet = $resultSet; + public function __construct( + /** @var resource */ + private $resultSet, + ) { } diff --git a/src/Dibi/Drivers/OracleReflector.php b/src/Dibi/Drivers/OracleReflector.php deleted file mode 100644 index 74c3db69a..000000000 --- a/src/Dibi/Drivers/OracleReflector.php +++ /dev/null @@ -1,88 +0,0 @@ -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; - } -} diff --git a/src/Dibi/Drivers/PDO/Connection.php b/src/Dibi/Drivers/PDO/Connection.php new file mode 100644 index 000000000..cae79b317 --- /dev/null +++ b/src/Dibi/Drivers/PDO/Connection.php @@ -0,0 +1,223 @@ + 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), + }; + } +} diff --git a/src/Dibi/Drivers/PdoResult.php b/src/Dibi/Drivers/PDO/Result.php similarity index 88% rename from src/Dibi/Drivers/PdoResult.php rename to src/Dibi/Drivers/PDO/Result.php index d0e22371e..bf0d39606 100644 --- a/src/Dibi/Drivers/PdoResult.php +++ b/src/Dibi/Drivers/PDO/Result.php @@ -7,9 +7,10 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\PDO; use Dibi; +use Dibi\Drivers; use Dibi\Helpers; use PDO; @@ -17,16 +18,12 @@ /** * The driver for PDO result set. */ -class PdoResult implements Dibi\ResultDriver +class Result implements Drivers\Result { - private ?\PDOStatement $resultSet; - private string $driverName; - - - public function __construct(\PDOStatement $resultSet, string $driverName) - { - $this->resultSet = $resultSet; - $this->driverName = $driverName; + public function __construct( + private ?\PDOStatement $resultSet, + private readonly string $driverName, + ) { } diff --git a/src/Dibi/Drivers/PdoDriver.php b/src/Dibi/Drivers/PdoDriver.php deleted file mode 100644 index e0944d84e..000000000 --- a/src/Dibi/Drivers/PdoDriver.php +++ /dev/null @@ -1,386 +0,0 @@ - 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 -{ - 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(); - $code ??= 0; - $message = "SQLSTATE[$sqlState]: $message"; - throw match ($this->driverName) { - 'mysql' => MySqliDriver::createException($message, $code, $sql), - 'oci' => OracleDriver::createException($message, $code, $sql), - 'pgsql' => PostgreDriver::createException($message, $sqlState, $sql), - 'sqlite' => SqliteDriver::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(): Dibi\Reflector - { - return match ($this->driverName) { - 'mysql' => new MySqlReflector($this), - 'oci' => new OracleReflector($this), - 'pgsql' => new PostgreReflector($this, $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION)), - 'sqlite' => new SqliteReflector($this), - 'mssql', 'dblib', 'sqlsrv' => 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 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), - }; - } - - - public function escapeIdentifier(string $value): string - { - return match ($this->driverName) { - 'mysql' => '`' . str_replace('`', '``', $value) . '`', - 'oci', 'pgsql' => '"' . str_replace('"', '""', $value) . '"', - 'sqlite' => '[' . strtr($value, '[]', ' ') . ']', - 'odbc', 'mssql' => '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']', - 'dblib', 'sqlsrv' => '[' . str_replace(']', ']]', $value) . ']', - default => $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 - { - return match ($this->driverName) { - 'odbc' => $value->format('#m/d/Y H:i:s.u#'), - 'mssql', 'dblib', 'sqlsrv' => 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')', - default => $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.'); - } - } -} diff --git a/src/Dibi/Drivers/PostgreDriver.php b/src/Dibi/Drivers/PgSQL/Connection.php similarity index 72% rename from src/Dibi/Drivers/PostgreDriver.php rename to src/Dibi/Drivers/PgSQL/Connection.php index 776e5288e..399867e94 100644 --- a/src/Dibi/Drivers/PostgreDriver.php +++ b/src/Dibi/Drivers/PgSQL/Connection.php @@ -7,11 +7,13 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\PgSQL; use Dibi; +use Dibi\Drivers; use Dibi\Helpers; use PgSql; +use function in_array, is_array, is_resource, strlen; /** @@ -23,13 +25,12 @@ * - schema => the schema search path * - charset => character encoding to set (default is utf8) * - persistent (bool) => try to find a persistent link? - * - resource (resource) => existing connection resource + * - resource (PgSql\Connection) => existing connection resource * - connect_type (int) => see pg_connect() */ -class PostgreDriver implements Dibi\Driver +class Connection implements Drivers\Connection { - /** @var resource|PgSql\Connection */ - private $connection; + private PgSql\Connection $connection; private ?int $affectedRows; @@ -72,7 +73,7 @@ public function __construct(array $config) restore_error_handler(); } - if (!is_resource($this->connection) && !$this->connection instanceof PgSql\Connection) { + if (!$this->connection instanceof PgSql\Connection) { throw new Dibi\DriverException($error ?: 'Connecting error.'); } @@ -110,7 +111,7 @@ public function ping(): bool * Executes the SQL query. * @throws Dibi\DriverException */ - public function query(string $sql): ?Dibi\ResultDriver + public function query(string $sql): ?Result { $this->affectedRows = null; $res = @pg_query($this->connection, $sql); // intentionally @ @@ -118,7 +119,7 @@ public function query(string $sql): ?Dibi\ResultDriver if ($res === false) { throw static::createException(pg_last_error($this->connection), null, $sql); - } elseif (is_resource($res) || $res instanceof PgSql\Result) { + } elseif ($res instanceof PgSql\Result) { $this->affectedRows = Helpers::false2Null(pg_affected_rows($res)); if (pg_num_fields($res)) { return $this->createResultDriver($res); @@ -222,32 +223,28 @@ public function inTransaction(): bool /** * Returns the connection resource. - * @return resource|null */ - public function getResource(): mixed + public function getResource(): PgSql\Connection { - return is_resource($this->connection) || $this->connection instanceof PgSql\Connection - ? $this->connection - : null; + return $this->connection; } /** * Returns the connection reflector. */ - public function getReflector(): Dibi\Reflector + public function getReflector(): Drivers\Engine { - return new PostgreReflector($this, pg_parameter_status($this->connection, 'server_version')); + return new Drivers\Engines\PostgreSQLEngine($this); } /** * Result set driver factory. - * @param resource $resource */ - public function createResultDriver($resource): PostgreResult + public function createResultDriver(PgSql\Result $resource): Result { - return new PostgreResult($resource); + return new Result($resource); } @@ -275,66 +272,4 @@ public function escapeBinary(string $value): string 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; - } - } } diff --git a/src/Dibi/Drivers/PostgreResult.php b/src/Dibi/Drivers/PgSQL/Result.php similarity index 78% rename from src/Dibi/Drivers/PostgreResult.php rename to src/Dibi/Drivers/PgSQL/Result.php index bcbecd290..347b66fd4 100644 --- a/src/Dibi/Drivers/PostgreResult.php +++ b/src/Dibi/Drivers/PgSQL/Result.php @@ -7,28 +7,22 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\PgSQL; -use Dibi; +use Dibi\Drivers; use Dibi\Helpers; use PgSql; +use function is_resource; /** * The driver for PostgreSQL result set. */ -class PostgreResult implements Dibi\ResultDriver +class Result implements Drivers\Result { - /** @var resource|PgSql\Result */ - private $resultSet; - - - /** - * @param resource|PgSql\Result $resultSet - */ - public function __construct($resultSet) - { - $this->resultSet = $resultSet; + public function __construct( + private readonly PgSql\Result $resultSet, + ) { } @@ -94,13 +88,10 @@ public function getResultColumns(): array /** * Returns the result set resource. - * @return resource|PgSql\Result|null */ - public function getResultResource(): mixed + public function getResultResource(): PgSql\Result { - return is_resource($this->resultSet) || $this->resultSet instanceof PgSql\Result - ? $this->resultSet - : null; + return $this->resultSet; } diff --git a/src/Dibi/Drivers/Result.php b/src/Dibi/Drivers/Result.php new file mode 100644 index 000000000..f90658b34 --- /dev/null +++ b/src/Dibi/Drivers/Result.php @@ -0,0 +1,58 @@ + character encoding to set (default is UTF-8) * - resource (resource) => existing connection resource */ -class SqlsrvDriver implements Dibi\Driver +class Connection implements Drivers\Connection { /** @var resource */ private $connection; private ?int $affectedRows; - private string $version = ''; /** @throws Dibi\NotSupportedException */ @@ -68,8 +69,6 @@ public function __construct(array $config) sqlsrv_configure('WarningsReturnAsErrors', 1); } - - $this->version = sqlsrv_server_info($this->connection)['SQLServerVersion']; } @@ -86,7 +85,7 @@ public function disconnect(): void * Executes the SQL query. * @throws Dibi\DriverException */ - public function query(string $sql): ?Dibi\ResultDriver + public function query(string $sql): ?Result { $this->affectedRows = null; $res = sqlsrv_query($this->connection, $sql); @@ -173,9 +172,9 @@ public function getResource(): mixed /** * Returns the connection reflector. */ - public function getReflector(): Dibi\Reflector + public function getReflector(): Drivers\Engine { - return new SqlsrvReflector($this); + return new Drivers\Engines\SQLServerEngine($this); } @@ -183,9 +182,9 @@ public function getReflector(): Dibi\Reflector * Result set driver factory. * @param resource $resource */ - public function createResultDriver($resource): SqlsrvResult + public function createResultDriver($resource): Result { - return new SqlsrvResult($resource); + return new Result($resource); } @@ -205,70 +204,4 @@ public function escapeBinary(string $value): string { 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); - } - } } diff --git a/src/Dibi/Drivers/SqlsrvResult.php b/src/Dibi/Drivers/SQLSrv/Result.php similarity index 61% rename from src/Dibi/Drivers/SqlsrvResult.php rename to src/Dibi/Drivers/SQLSrv/Result.php index ecd6ed538..92c0f753f 100644 --- a/src/Dibi/Drivers/SqlsrvResult.php +++ b/src/Dibi/Drivers/SQLSrv/Result.php @@ -7,26 +7,58 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\SQLSrv; use Dibi; +use Dibi\Drivers; +use function is_resource; /** * The driver for Microsoft SQL Server and SQL Azure result set. */ -class SqlsrvResult implements Dibi\ResultDriver +class Result implements Drivers\Result { - /** @var resource */ - private $resultSet; - - - /** - * @param resource $resultSet - */ - public function __construct($resultSet) - { - $this->resultSet = $resultSet; + //return values of sqlsrv_field_metadata + //https://learn.microsoft.com/en-us/sql/connect/php/sqlsrv-field-metadata + private $sqlsrvDataTypes = array( + -5=>'SQL_BIGINT', + -2=>'SQL_BINARY', + -7=>'SQL_BIT', + 1=>'SQL_CHAR', + 91=>'SQL_TYPE_DATE', + 93=>'SQL_TYPE_TIMESTAMP', + 93=>'SQL_TYPE_TIMESTAMP', + -155=>'SQL_SS_TIMESTAMPOFFSET', + 3=>'SQL_DECIMAL', + 6=>'SQL_FLOAT', + -4=>'SQL_LONGVARBINARY', + 4=>'SQL_INTEGER', + 3=>'SQL_DECIMAL', + -8=>'SQL_WCHAR', + -10=>'SQL_WLONGVARCHAR', + 2=>'SQL_NUMERIC', + -9=>'SQL_WVARCHAR', + 7=>'SQL_REAL', + 93=>'SQL_TYPE_TIMESTAMP', + 5=>'SQL_SMALLINT', + 3=>'SQL_DECIMAL', + -150=>'SQL_SS_VARIANT', + -1=>'SQL_LONGVARCHAR', + -154=>'SQL_SS_TIME2', + -2=>'SQL_BINARY', + -6=>'SQL_TINYINT', + -151=>'SQL_SS_UDT', + -11=>'SQL_GUID', + -3=>'SQL_VARBINARY', + 12=>'SQL_VARCHAR', + -152=>'SQL_SS_XML', + ); + + public function __construct( + /** @var resource */ + private $resultSet, + ) { } @@ -77,7 +109,7 @@ public function getResultColumns(): array $columns[] = [ 'name' => $fieldMetadata['Name'], 'fullname' => $fieldMetadata['Name'], - 'nativetype' => $fieldMetadata['Type'], + 'nativetype' => $this->sqlsrvDataTypes[$fieldMetadata['Type']] ?? 'SQL_VARCHAR', ]; } diff --git a/src/Dibi/Drivers/SqliteDriver.php b/src/Dibi/Drivers/SQLite3/Connection.php similarity index 76% rename from src/Dibi/Drivers/SqliteDriver.php rename to src/Dibi/Drivers/SQLite3/Connection.php index c1b4b2273..86d3be857 100644 --- a/src/Dibi/Drivers/SqliteDriver.php +++ b/src/Dibi/Drivers/SQLite3/Connection.php @@ -7,9 +7,10 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\SQLite3; use Dibi; +use Dibi\Drivers; use Dibi\Helpers; use SQLite3; @@ -23,7 +24,7 @@ * - formatDateTime => how to format datetime in SQL (@see date) * - resource (SQLite3) => existing connection resource */ -class SqliteDriver implements Dibi\Driver +class Connection implements Drivers\Connection { private SQLite3 $connection; private string $fmtDate; @@ -56,10 +57,7 @@ public function __construct(array $config) } // enable foreign keys support (defaultly disabled; if disabled then foreign key constraints are not enforced) - $version = SQLite3::version(); - if ($version['versionNumber'] >= '3006019') { - $this->query('PRAGMA foreign_keys = ON'); - } + $this->query('PRAGMA foreign_keys = ON'); } @@ -76,7 +74,7 @@ public function disconnect(): void * Executes the SQL query. * @throws Dibi\DriverException */ - public function query(string $sql): ?Dibi\ResultDriver + public function query(string $sql): ?Result { $res = @$this->connection->query($sql); // intentionally @ if ($code = $this->connection->lastErrorCode()) { @@ -177,18 +175,18 @@ public function getResource(): ?SQLite3 /** * Returns the connection reflector. */ - public function getReflector(): Dibi\Reflector + public function getReflector(): Drivers\Engine { - return new SqliteReflector($this); + return new Drivers\Engines\SQLiteEngine($this); } /** * Result set driver factory. */ - public function createResultDriver(\SQLite3Result $result): SqliteResult + public function createResultDriver(\SQLite3Result $result): Result { - return new SqliteResult($result); + return new Result($result); } @@ -210,61 +208,6 @@ public function escapeBinary(string $value): string } - 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**/ diff --git a/src/Dibi/Drivers/SqliteResult.php b/src/Dibi/Drivers/SQLite3/Result.php similarity index 88% rename from src/Dibi/Drivers/SqliteResult.php rename to src/Dibi/Drivers/SQLite3/Result.php index 218808b39..b1c6e8c0b 100644 --- a/src/Dibi/Drivers/SqliteResult.php +++ b/src/Dibi/Drivers/SQLite3/Result.php @@ -7,23 +7,22 @@ declare(strict_types=1); -namespace Dibi\Drivers; +namespace Dibi\Drivers\SQLite3; 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 SqliteResult implements Dibi\ResultDriver +class Result implements Drivers\Result { - private \SQLite3Result $resultSet; - - - public function __construct(\SQLite3Result $resultSet) - { - $this->resultSet = $resultSet; + public function __construct( + private readonly \SQLite3Result $resultSet, + ) { } diff --git a/src/Dibi/Drivers/Sqlite3Driver.php b/src/Dibi/Drivers/Sqlite3Driver.php deleted file mode 100644 index f3dfa9ede..000000000 --- a/src/Dibi/Drivers/Sqlite3Driver.php +++ /dev/null @@ -1,18 +0,0 @@ - 'FROM', ]; - private Connection $connection; + private readonly Connection $connection; private array $setups = []; private ?string $command = null; private array $clauses = []; @@ -113,7 +115,7 @@ public function __construct(Connection $connection) $this->connection = $connection; if (!isset(self::$normalizer)) { - self::$normalizer = new HashMap([self::class, '_formatClause']); + self::$normalizer = new HashMap(self::_formatClause(...)); } } diff --git a/src/Dibi/Helpers.php b/src/Dibi/Helpers.php index 82bed671e..e143623e4 100644 --- a/src/Dibi/Helpers.php +++ b/src/Dibi/Helpers.php @@ -9,6 +9,9 @@ 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 { @@ -156,7 +159,7 @@ public static function getSuggestion(array $items, string $value): ?string /** @internal */ - public static function escape(Driver $driver, $value, string $type): string + public static function escape(Drivers\Connection $driver, $value, string $type): string { $types = [ Type::Text => 'text', @@ -208,7 +211,7 @@ public static function detectType(string $type): ?string public static function getTypeCache(): HashMap { if (!isset(self::$types)) { - self::$types = new HashMap([self::class, 'detectType']); + self::$types = new HashMap(self::detectType(...)); } return self::$types; diff --git a/src/Dibi/IDataSource.php b/src/Dibi/IDataSource.php new file mode 100644 index 000000000..0565fc456 --- /dev/null +++ b/src/Dibi/IDataSource.php @@ -0,0 +1,20 @@ +file = $file; - $this->filter = $filter ?: Dibi\Event::QUERY; - $this->errorsOnly = $errorsOnly; + public function __construct( + public string $file, + public int $filter = Dibi\Event::QUERY, + private bool $errorsOnly = false, + ) { } diff --git a/src/Dibi/Reflection/Column.php b/src/Dibi/Reflection/Column.php index 5415501ba..fc9d4dbb3 100644 --- a/src/Dibi/Reflection/Column.php +++ b/src/Dibi/Reflection/Column.php @@ -27,17 +27,10 @@ */ class Column { - /** 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; + public function __construct( + private readonly ?Dibi\Drivers\Engine $reflector, + private array $info, + ) { } diff --git a/src/Dibi/Reflection/Database.php b/src/Dibi/Reflection/Database.php index a6c42efdd..14df23481 100644 --- a/src/Dibi/Reflection/Database.php +++ b/src/Dibi/Reflection/Database.php @@ -10,6 +10,7 @@ namespace Dibi\Reflection; use Dibi; +use function array_values, strtolower; /** @@ -21,17 +22,14 @@ */ class Database { - private Dibi\Reflector $reflector; - private ?string $name; - /** @var Table[] */ private array $tables; - public function __construct(Dibi\Reflector $reflector, ?string $name = null) - { - $this->reflector = $reflector; - $this->name = $name; + public function __construct( + private readonly Dibi\Drivers\Engine $reflector, + private ?string $name = null, + ) { } diff --git a/src/Dibi/Reflection/ForeignKey.php b/src/Dibi/Reflection/ForeignKey.php index 492a66f32..c7f005b00 100644 --- a/src/Dibi/Reflection/ForeignKey.php +++ b/src/Dibi/Reflection/ForeignKey.php @@ -19,16 +19,10 @@ */ class ForeignKey { - 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; + public function __construct( + private readonly string $name, + private readonly array $references, + ) { } diff --git a/src/Dibi/Reflection/Index.php b/src/Dibi/Reflection/Index.php index 13fe1df78..71e426c49 100644 --- a/src/Dibi/Reflection/Index.php +++ b/src/Dibi/Reflection/Index.php @@ -21,13 +21,9 @@ */ class Index { - /** @var array (name, columns, [unique], [primary]) */ - private array $info; - - - public function __construct(array $info) - { - $this->info = $info; + public function __construct( + private readonly array $info, + ) { } diff --git a/src/Dibi/Reflection/Result.php b/src/Dibi/Reflection/Result.php index 3cd1dd936..be8785120 100644 --- a/src/Dibi/Reflection/Result.php +++ b/src/Dibi/Reflection/Result.php @@ -10,6 +10,7 @@ namespace Dibi\Reflection; use Dibi; +use function array_values, strtolower; /** @@ -20,8 +21,6 @@ */ class Result { - private Dibi\ResultDriver $driver; - /** @var Column[]|null */ private ?array $columns; @@ -29,9 +28,9 @@ class Result private ?array $names; - public function __construct(Dibi\ResultDriver $driver) - { - $this->driver = $driver; + public function __construct( + private readonly Dibi\Drivers\Result $driver, + ) { } @@ -80,7 +79,7 @@ protected function initColumns(): void { if (!isset($this->columns)) { $this->columns = []; - $reflector = $this->driver instanceof Dibi\Reflector + $reflector = $this->driver instanceof Dibi\Drivers\Engine ? $this->driver : null; foreach ($this->driver->getResultColumns() as $info) { diff --git a/src/Dibi/Reflection/Table.php b/src/Dibi/Reflection/Table.php index b03d4807c..986a59bfe 100644 --- a/src/Dibi/Reflection/Table.php +++ b/src/Dibi/Reflection/Table.php @@ -10,6 +10,7 @@ namespace Dibi\Reflection; use Dibi; +use function array_values, strtolower; /** @@ -25,7 +26,7 @@ */ class Table { - private Dibi\Reflector $reflector; + private readonly Dibi\Drivers\Engine $reflector; private string $name; private bool $view; @@ -40,7 +41,7 @@ class Table private ?Index $primaryKey; - public function __construct(Dibi\Reflector $reflector, array $info) + public function __construct(Dibi\Drivers\Engine $reflector, array $info) { $this->reflector = $reflector; $this->name = $info['name']; diff --git a/src/Dibi/Result.php b/src/Dibi/Result.php index 24f4513d2..b45faf918 100644 --- a/src/Dibi/Result.php +++ b/src/Dibi/Result.php @@ -9,6 +9,9 @@ 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. @@ -17,7 +20,7 @@ */ class Result implements IDataSource { - private ?ResultDriver $driver; + private ?Drivers\Result $driver; /** Translate table */ private array $types = []; @@ -34,7 +37,7 @@ class Result implements IDataSource private array $formats = []; - public function __construct(ResultDriver $driver, bool $normalize = true) + public function __construct(Drivers\Result $driver, bool $normalize = true) { $this->driver = $driver; if ($normalize) { @@ -59,7 +62,7 @@ final public function free(): void * Safe access to property $driver. * @throws \RuntimeException */ - final public function getResultDriver(): ResultDriver + final public function getResultDriver(): Drivers\Result { if ($this->driver === null) { throw new \RuntimeException('Result-set was released from memory.'); diff --git a/src/Dibi/ResultIterator.php b/src/Dibi/ResultIterator.php index 505489a90..6bac6b570 100644 --- a/src/Dibi/ResultIterator.php +++ b/src/Dibi/ResultIterator.php @@ -15,14 +15,13 @@ */ class ResultIterator implements \Iterator, \Countable { - private Result $result; private mixed $row; private int $pointer = 0; - public function __construct(Result $result) - { - $this->result = $result; + public function __construct( + private readonly Result $result, + ) { } diff --git a/src/Dibi/Row.php b/src/Dibi/Row.php index 369d9d1b9..6838e0d69 100644 --- a/src/Dibi/Row.php +++ b/src/Dibi/Row.php @@ -9,6 +9,8 @@ namespace Dibi; +use function array_keys, count, str_starts_with; + /** * Result set single row. diff --git a/src/Dibi/Translator.php b/src/Dibi/Translator.php index 35d31c71c..22a9e9520 100644 --- a/src/Dibi/Translator.php +++ b/src/Dibi/Translator.php @@ -9,14 +9,17 @@ 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 Connection $connection; - private Driver $driver; + private readonly Connection $connection; + private readonly Drivers\Connection $driver; + private readonly Drivers\Engine $engine; private int $cursor = 0; private array $args; @@ -34,7 +37,8 @@ public function __construct(Connection $connection) { $this->connection = $connection; $this->driver = $connection->getDriver(); - $this->identifiers = new HashMap([$this, 'delimite']); + $this->engine = $connection->getDatabaseEngine(); + $this->identifiers = new HashMap($this->delimite(...)); } @@ -88,7 +92,7 @@ public function translate(array $args): string (\?) ## 11) placeholder )/xs XX, - [$this, 'cb'], + $this->cb(...), substr($arg, $toSkip), ); if (preg_last_error()) { @@ -142,7 +146,7 @@ public function translate(array $args): string // apply limit if ($this->limit !== null || $this->offset !== null) { - $this->driver->applyLimit($sql, $this->limit, $this->offset); + $this->engine->applyLimit($sql, $this->limit, $this->offset); } return $sql; @@ -207,7 +211,7 @@ public function formatValue(mixed $value, ?string $modifier): string case 'n': // key, key, ... identifier names foreach ($value as $k => $v) { if (is_string($k)) { - $vx[] = $this->identifiers->$k . (empty($v) ? '' : ' AS ' . $this->driver->escapeIdentifier($v)); + $vx[] = $this->identifiers->$k . (empty($v) ? '' : ' AS ' . $this->engine->escapeIdentifier($v)); } else { $pair = explode('%', $v, 2); // split into identifier & modifier $vx[] = $this->identifiers->{$pair[0]}; @@ -344,7 +348,7 @@ public function formatValue(mixed $value, ?string $modifier): string case 's': // string return $value === null ? 'NULL' - : $this->driver->escapeText((string) $value); + : $this->engine->escapeText((string) $value); case 'bin':// binary return $value === null @@ -354,7 +358,7 @@ public function formatValue(mixed $value, ?string $modifier): string case 'b': // boolean return $value === null ? 'NULL' - : $this->driver->escapeBool((bool) $value); + : $this->engine->escapeBool((bool) $value); case 'sN': // string or null case 'sn': @@ -404,15 +408,15 @@ public function formatValue(mixed $value, ?string $modifier): string } return $modifier === 'd' - ? $this->driver->escapeDate($value) - : $this->driver->escapeDateTime($value); + ? $this->engine->escapeDate($value) + : $this->engine->escapeDateTime($value); case 'by': case 'n': // composed identifier name return $this->identifiers->$value; case 'N': // identifier name - return $this->driver->escapeIdentifier($value); + return $this->engine->escapeIdentifier($value); case 'ex': case 'sql': // preserve as dibi-SQL (TODO: leave only %ex) @@ -434,7 +438,7 @@ public function formatValue(mixed $value, ?string $modifier): string :(\S*?:)([a-zA-Z0-9._]?) )/sx XX, - [$this, 'cb'], + $this->cb(...), substr($value, $toSkip), ); if (preg_last_error()) { @@ -448,16 +452,16 @@ public function formatValue(mixed $value, ?string $modifier): string return (string) $value; case 'like~': // LIKE string% - return $this->driver->escapeLike($value, 2); + return $this->engine->escapeLike($value, 2); case '~like': // LIKE %string - return $this->driver->escapeLike($value, 1); + return $this->engine->escapeLike($value, 1); case '~like~': // LIKE %string% - return $this->driver->escapeLike($value, 3); + return $this->engine->escapeLike($value, 3); case 'like': // LIKE string - return $this->driver->escapeLike($value, 0); + return $this->engine->escapeLike($value, 0); case 'and': case 'or': @@ -483,16 +487,16 @@ public function formatValue(mixed $value, ?string $modifier): string return rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.'); } elseif (is_bool($value)) { - return $this->driver->escapeBool($value); + return $this->engine->escapeBool($value); } elseif ($value === null) { return 'NULL'; } elseif ($value instanceof \DateTimeInterface) { - return $this->driver->escapeDateTime($value); + return $this->engine->escapeDateTime($value); } elseif ($value instanceof \DateInterval) { - return $this->driver->escapeDateInterval($value); + return $this->engine->escapeDateInterval($value); } elseif ($value instanceof Literal) { return (string) $value; @@ -651,7 +655,7 @@ public function delimite(string $value): string $parts = explode('.', $value); foreach ($parts as &$v) { if ($v !== '*') { - $v = $this->driver->escapeIdentifier($v); + $v = $this->engine->escapeIdentifier($v); } } diff --git a/src/Dibi/Type.php b/src/Dibi/Type.php index 21693dd9d..01a1356d2 100644 --- a/src/Dibi/Type.php +++ b/src/Dibi/Type.php @@ -27,31 +27,31 @@ class Type Time = 't', TimeInterval = 'ti'; - /** @deprecated use Type::Text */ + #[\Deprecated('use Type::Text')] public const TEXT = self::Text; - /** @deprecated use Type::Binary */ + #[\Deprecated('use Type::Binary')] public const BINARY = self::Binary; - /** @deprecated use Type::Bool */ + #[\Deprecated('use Type::Bool')] public const BOOL = self::Bool; - /** @deprecated use Type::Integer */ + #[\Deprecated('use Type::Integer')] public const INTEGER = self::Integer; - /** @deprecated use Type::Float */ + #[\Deprecated('use Type::Float')] public const FLOAT = self::Float; - /** @deprecated use Type::Date */ + #[\Deprecated('use Type::Date')] public const DATE = self::Date; - /** @deprecated use Type::DateTime */ + #[\Deprecated('use Type::DateTime')] public const DATETIME = self::DateTime; - /** @deprecated use Type::Time */ + #[\Deprecated('use Type::Time')] public const TIME = self::Time; - /** @deprecated use Type::TimeInterval */ + #[\Deprecated('use Type::TimeInterval')] public const TIME_INTERVAL = self::TimeInterval; diff --git a/src/Dibi/dibi.php b/src/Dibi/dibi.php index 7ae4620f0..1774a0732 100644 --- a/src/Dibi/dibi.php +++ b/src/Dibi/dibi.php @@ -37,15 +37,12 @@ */ class dibi { - public const Version = '5.0.2'; + public const Version = '6.0-dev'; - /** @deprecated use dibi::Version */ public const VERSION = self::Version; - /** @deprecated use Dibi\Fluent::AffectedRows */ public const AFFECTED_ROWS = Dibi\Fluent::AffectedRows; - /** @deprecated use Dibi\Fluent::Identifier */ public const IDENTIFIER = Dibi\Fluent::Identifier; /** sorting order */ diff --git a/src/Dibi/exceptions.php b/src/Dibi/exceptions.php index 612ca7a1c..2eb849f56 100644 --- a/src/Dibi/exceptions.php +++ b/src/Dibi/exceptions.php @@ -11,7 +11,7 @@ /** - * Dibi common exception. + * A database operation failed. */ class Exception extends \Exception { @@ -44,7 +44,7 @@ public function __toString(): string /** - * database server exception. + * The database server reported an error. */ class DriverException extends Exception { @@ -52,7 +52,7 @@ class DriverException extends Exception /** - * PCRE exception. + * Regular expression pattern or execution failed. */ class PcreException extends Exception { @@ -63,18 +63,24 @@ public function __construct() } +/** + * The requested feature is not implemented. + */ class NotImplementedException extends Exception { } +/** + * The requested operation is not supported. + */ class NotSupportedException extends Exception { } /** - * Database procedure exception. + * A database stored procedure failed. */ class ProcedureException extends Exception { @@ -102,7 +108,7 @@ public function getSeverity(): string /** - * Base class for all constraint violation related exceptions. + * A database constraint was violated. */ class ConstraintViolationException extends DriverException { @@ -110,7 +116,7 @@ class ConstraintViolationException extends DriverException /** - * Exception for a foreign key constraint violation. + * The foreign key constraint check failed. */ class ForeignKeyConstraintViolationException extends ConstraintViolationException { @@ -118,7 +124,7 @@ class ForeignKeyConstraintViolationException extends ConstraintViolationExceptio /** - * Exception for a NOT NULL constraint violation. + * The NOT NULL constraint check failed. */ class NotNullConstraintViolationException extends ConstraintViolationException { @@ -126,7 +132,7 @@ class NotNullConstraintViolationException extends ConstraintViolationException /** - * Exception for a unique constraint violation. + * The unique constraint check failed. */ class UniqueConstraintViolationException extends ConstraintViolationException { diff --git a/src/Dibi/interfaces.php b/src/Dibi/interfaces.php deleted file mode 100644 index 3cf336854..000000000 --- a/src/Dibi/interfaces.php +++ /dev/null @@ -1,240 +0,0 @@ -isConnected()); @@ -21,7 +21,7 @@ test('', function () use ($config) { }); -test('lazy', function () use ($config) { +test('lazy connection initiated on first query', function () use ($config) { $conn = new Connection($config + ['lazy' => true]); Assert::false($conn->isConnected()); @@ -30,17 +30,17 @@ test('lazy', function () use ($config) { }); -test('', function () use ($config) { +test('config retrieval and driver instance access', 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\Driver::class, $conn->getDriver()); + Assert::type(Dibi\Drivers\Connection::class, $conn->getDriver()); }); -test('', function () use ($config) { +test('idempotent disconnect calls', function () use ($config) { $conn = new Connection($config); Assert::true($conn->isConnected()); @@ -52,7 +52,7 @@ test('', function () use ($config) { }); -test('', function () use ($config) { +test('reconnect after disconnection', function () use ($config) { $conn = new Connection($config); Assert::equal('hello', $conn->query('SELECT %s', 'hello')->fetchSingle()); @@ -63,7 +63,7 @@ test('', function () use ($config) { }); -test('', function () use ($config) { +test('destructor disconnects active connection', function () use ($config) { $conn = new Connection($config); Assert::true($conn->isConnected()); @@ -72,7 +72,7 @@ test('', function () use ($config) { }); -test('', function () use ($config) { +test('invalid onConnect option triggers exceptions', function () use ($config) { Assert::exception( fn() => new Connection($config + ['onConnect' => '']), InvalidArgumentException::class, diff --git a/tests/dibi/Connection.objectTranslator.phpt b/tests/dibi/Connection.objectTranslator.phpt index 32938af26..58e3be453 100644 --- a/tests/dibi/Connection.objectTranslator.phpt +++ b/tests/dibi/Connection.objectTranslator.phpt @@ -49,7 +49,7 @@ test('DateTime', function () use ($conn) { // Without object translator, DateTime child is translated by driver Assert::same( - $conn->getDriver()->escapeDateTime($stamp), + $conn->getDatabaseEngine()->escapeDateTime($stamp), $conn->translate('?', $stamp), ); @@ -67,15 +67,15 @@ test('DateTime', function () use ($conn) { // With modifier, it is still translated by driver Assert::same( - $conn->getDriver()->escapeDateTime($stamp), + $conn->getDatabaseEngine()->escapeDateTime($stamp), $conn->translate('%dt', $stamp), ); Assert::same( - $conn->getDriver()->escapeDateTime($stamp), + $conn->getDatabaseEngine()->escapeDateTime($stamp), $conn->translate('%t', $stamp), ); Assert::same( - $conn->getDriver()->escapeDate($stamp), + $conn->getDatabaseEngine()->escapeDate($stamp), $conn->translate('%d', $stamp), ); @@ -83,7 +83,7 @@ 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->getDriver()->escapeDateTime($dt), + $conn->getDatabaseEngine()->escapeDateTime($dt), $conn->translate('?', $dt), ); diff --git a/tests/dibi/Fluent.fetch.limit.mssql.phpt b/tests/dibi/Fluent.fetch.limit.mssql.phpt index 7df79eb10..cc8fdc362 100644 --- a/tests/dibi/Fluent.fetch.limit.mssql.phpt +++ b/tests/dibi/Fluent.fetch.limit.mssql.phpt @@ -2,12 +2,14 @@ declare(strict_types=1); +use Dibi\Drivers\SQLSrv\Connection; +use Dibi\Drivers\SQLSrv\Result; use Tester\Assert; require __DIR__ . '/bootstrap.php'; -class MockDriver extends Dibi\Drivers\SqlsrvDriver +class MockDriver extends Connection { public function __construct() { @@ -19,14 +21,14 @@ class MockDriver extends Dibi\Drivers\SqlsrvDriver } - public function query(string $sql): ?Dibi\ResultDriver + public function query(string $sql): ?Result { return new MockResult; } } -class MockResult extends Dibi\Drivers\SqlsrvResult +class MockResult extends Result { public function __construct() { @@ -57,28 +59,28 @@ $fluent = $conn->select('*') ->orderBy('customer_id'); Assert::same( - reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'), (string) $fluent, ); $fluent->fetch(); Assert::same( - 'SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t', + 'SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY', dibi::$sql, ); $fluent->fetchSingle(); Assert::same( - reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'), dibi::$sql, ); $fluent->fetchAll(0, 3); Assert::same( - reformat('SELECT TOP (3) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY'), dibi::$sql, ); Assert::same( - reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'), (string) $fluent, ); @@ -86,16 +88,16 @@ Assert::same( $fluent->limit(0); $fluent->fetch(); Assert::same( - reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'), dibi::$sql, ); $fluent->fetchSingle(); Assert::same( - reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'), dibi::$sql, ); Assert::same( - reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'), (string) $fluent, ); @@ -104,12 +106,12 @@ $fluent->removeClause('limit'); $fluent->removeClause('offset'); $fluent->fetch(); Assert::same( - reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'), dibi::$sql, ); $fluent->fetchSingle(); Assert::same( - reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'), + reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'), dibi::$sql, ); Assert::same( diff --git a/tests/dibi/PdoDriver.providedConnection.phpt b/tests/dibi/PdoDriver.providedConnection.phpt index 41b0d5137..7492294d3 100644 --- a/tests/dibi/PdoDriver.providedConnection.phpt +++ b/tests/dibi/PdoDriver.providedConnection.phpt @@ -14,7 +14,7 @@ function buildPdoDriver(?int $errorMode) $pdo->setAttribute(PDO::ATTR_ERRMODE, $errorMode); } - new Dibi\Drivers\PdoDriver(['resource' => $pdo]); + new Dibi\Drivers\PDO\Connection(['resource' => $pdo]); } @@ -36,5 +36,5 @@ Assert::exception( test( 'PDO error mode: explicitly set silent', - fn() => buildPdoDriver(PDO::ERRMODE_SILENT) + fn() => buildPdoDriver(PDO::ERRMODE_SILENT), ); diff --git a/tests/dibi/Postgre.like.phpt b/tests/dibi/Postgre.like.phpt index b84c5c28e..b6bed7993 100644 --- a/tests/dibi/Postgre.like.phpt +++ b/tests/dibi/Postgre.like.phpt @@ -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); diff --git a/tests/dibi/Result.normalize.phpt b/tests/dibi/Result.normalize.phpt index 7a08ce9a6..6fa385f6d 100644 --- a/tests/dibi/Result.normalize.phpt +++ b/tests/dibi/Result.normalize.phpt @@ -25,7 +25,7 @@ class MockResult extends Dibi\Result } -test('', function () { +test('native text conversion preserves boolean values', function () { $result = new MockResult; $result->setType('col', Type::Text); $result->setFormat(Type::Text, 'native'); @@ -36,7 +36,7 @@ test('', function () { }); -test('', function () { +test('boolean conversion from diverse representations', function () { $result = new MockResult; $result->setType('col', Type::Bool); @@ -58,7 +58,7 @@ test('', function () { }); -test('', function () { +test('text conversion of booleans and numerics', function () { $result = new MockResult; $result->setType('col', Type::Text); @@ -74,7 +74,7 @@ test('', function () { }); -test('', function () { +test('float conversion with various numeric formats', function () { $result = new MockResult; $result->setType('col', Type::Float); @@ -214,7 +214,7 @@ test('', function () { }); -test('', function () { +test('strict integer conversion with error on empty string', function () { $result = new MockResult; $result->setType('col', Type::Integer); @@ -222,14 +222,10 @@ test('', function () { Assert::same(['col' => 1], $result->test(['col' => true])); Assert::same(['col' => 0], $result->test(['col' => false])); - if (PHP_VERSION_ID < 80000) { - Assert::same(['col' => 0], @$result->test(['col' => ''])); // triggers warning since PHP 7.1 - } else { - Assert::exception( - fn() => Assert::same(['col' => 0], $result->test(['col' => ''])), - TypeError::class, - ); - } + Assert::exception( + fn() => 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'])); @@ -248,7 +244,7 @@ test('', function () { }); -test('', function () { +test('dateTime conversion with object instantiation', function () { $result = new MockResult; $result->setType('col', Type::DateTime); @@ -267,7 +263,7 @@ test('', function () { }); -test('', function () { +test('dateTime conversion using custom format', function () { $result = new MockResult; $result->setType('col', Type::DateTime); $result->setFormat(Type::DateTime, 'Y-m-d H:i:s'); @@ -287,7 +283,7 @@ test('', function () { }); -test('', function () { +test('date conversion to DateTime instance', function () { $result = new MockResult; $result->setType('col', Type::Date); @@ -304,7 +300,7 @@ test('', function () { }); -test('', function () { +test('time conversion to DateTime instance', function () { $result = new MockResult; $result->setType('col', Type::Time); diff --git a/tests/dibi/Sqlsrv.limits.phpt b/tests/dibi/Sqlsrv.limits.phpt index f70ea1971..cc0c8a1cd 100644 --- a/tests/dibi/Sqlsrv.limits.phpt +++ b/tests/dibi/Sqlsrv.limits.phpt @@ -11,82 +11,59 @@ use Tester\Assert; require __DIR__ . '/bootstrap.php'; $tests = function ($conn) { - $resource = $conn->getDriver()->getResource(); - $version = is_resource($resource) - ? sqlsrv_server_info($resource)['SQLServerVersion'] - : $resource->getAttribute(PDO::ATTR_SERVER_VERSION); + // Limit and offset + Assert::same( + 'SELECT 1 OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY', + $conn->translate('SELECT 1 %ofs %lmt', 10, 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), - ); + // Limit only + Assert::same( + 'SELECT 1 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', + $conn->translate('SELECT 1 %lmt', 10), + ); - // Limit only - Assert::same( - 'SELECT 1 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', - $conn->translate('SELECT 1 %lmt', 10), - ); + // Offset only + Assert::same( + 'SELECT 1 OFFSET 10 ROWS', + $conn->translate('SELECT 1 %ofs', 10), + ); - // Offset only - Assert::same( - 'SELECT 1 OFFSET 10 ROWS', - $conn->translate('SELECT 1 %ofs', 10), - ); + // Offset invalid + Assert::error( + function () use ($conn) { + $conn->translate('SELECT 1 %ofs', -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 + Assert::error( + function () use ($conn) { + $conn->translate('SELECT 1 %lmt', -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 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, 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( - fn() => $conn->translate('SELECT 1 %ofs %lmt', 10, 10), - Dibi\NotSupportedException::class, - ); - } + // Limit valid, offset invalid + Assert::error( + function () use ($conn) { + $conn->translate('SELECT 1 %ofs %lmt', -10, 10); + }, + Dibi\NotSupportedException::class, + 'Negative offset or limit.', + ); }; $conn = new Dibi\Connection($config); diff --git a/tests/dibi/Translator.like.phpt b/tests/dibi/Translator.like.phpt index ad7eebec0..7387dba48 100644 --- a/tests/dibi/Translator.like.phpt +++ b/tests/dibi/Translator.like.phpt @@ -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