diff --git a/src/Dibi/Connection.php b/src/Dibi/Connection.php index 1f49e943..97f45558 100644 --- a/src/Dibi/Connection.php +++ b/src/Dibi/Connection.php @@ -42,6 +42,7 @@ class Connection /** @var string[] resultset formats */ private array $formats; private ?Drivers\Connection $driver = null; + private Drivers\Engine $engine; private ?Translator $translator = null; /** @var array */ @@ -670,6 +671,15 @@ class Connection } + public function getDatabaseEngine(): Drivers\Engine + { + if (!$this->driver) { // TODO + $this->connect(); + } + return $this->engine ??= $this->driver->getReflector(); + } + + /** * Gets a information about the current database. */ @@ -679,7 +689,7 @@ class Connection $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 04ffc29c..df10617c 100644 --- a/src/Dibi/DataSource.php +++ b/src/Dibi/DataSource.php @@ -35,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 index 9ff28e70..1f98ec15 100644 --- a/src/Dibi/Drivers/Connection.php +++ b/src/Dibi/Drivers/Connection.php @@ -74,24 +74,4 @@ interface Connection function escapeText(string $value): string; function escapeBinary(string $value): string; - - function escapeIdentifier(string $value): string; - - function escapeBool(bool $value): string; - - function escapeDate(\DateTimeInterface $value): string; - - function escapeDateTime(\DateTimeInterface $value): string; - - function escapeDateInterval(\DateInterval $value): string; - - /** - * Encodes string for use in a LIKE statement. - */ - function escapeLike(string $value, int $pos): string; - - /** - * Injects LIMIT/OFFSET to the SQL query. - */ - function applyLimit(string &$sql, ?int $limit, ?int $offset): void; } diff --git a/src/Dibi/Drivers/Engine.php b/src/Dibi/Drivers/Engine.php index f1822d05..d744b0cd 100644 --- a/src/Dibi/Drivers/Engine.php +++ b/src/Dibi/Drivers/Engine.php @@ -15,6 +15,26 @@ namespace Dibi\Drivers; */ interface Engine { + function escapeIdentifier(string $value): string; + + function escapeBool(bool $value): string; + + function escapeDate(\DateTimeInterface $value): string; + + function escapeDateTime(\DateTimeInterface $value): string; + + function escapeDateInterval(\DateInterval $value): string; + + /** + * Encodes string for use in a LIKE statement. + */ + function escapeLike(string $value, int $pos): string; + + /** + * Injects LIMIT/OFFSET to the SQL query. + */ + function applyLimit(string &$sql, ?int $limit, ?int $offset): void; + /** * Returns list of tables. * @return array of {name [, (bool) view ]} diff --git a/src/Dibi/Drivers/Engines/FirebirdEngine.php b/src/Dibi/Drivers/Engines/FirebirdEngine.php index 1818d8f7..dac87ca5 100644 --- a/src/Dibi/Drivers/Engines/FirebirdEngine.php +++ b/src/Dibi/Drivers/Engines/FirebirdEngine.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace Dibi\Drivers\Engines; +use Dibi; use Dibi\Drivers\Connection; use Dibi\Drivers\Engine; @@ -24,6 +25,58 @@ class FirebirdEngine implements Engine } + 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->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 . ')'; + } + } + + /** * Returns list of tables. */ diff --git a/src/Dibi/Drivers/Engines/MySQLEngine.php b/src/Dibi/Drivers/Engines/MySQLEngine.php index f3680634..4a6d15df 100644 --- a/src/Dibi/Drivers/Engines/MySQLEngine.php +++ b/src/Dibi/Drivers/Engines/MySQLEngine.php @@ -26,6 +26,66 @@ class MySQLEngine implements Engine } + 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 : ''); + } + } + + /** * Returns list of tables. */ @@ -49,7 +109,7 @@ class MySQLEngine implements Engine */ 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']); @@ -74,7 +134,7 @@ class MySQLEngine implements Engine */ 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/Engines/ODBCEngine.php b/src/Dibi/Drivers/Engines/ODBCEngine.php index 4831bf0e..2f1523d5 100644 --- a/src/Dibi/Drivers/Engines/ODBCEngine.php +++ b/src/Dibi/Drivers/Engines/ODBCEngine.php @@ -25,6 +25,63 @@ class ODBCEngine implements Engine } + 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 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'; + } + } + + /** * Returns list of tables. */ diff --git a/src/Dibi/Drivers/Engines/OracleEngine.php b/src/Dibi/Drivers/Engines/OracleEngine.php index b20b29a1..deb4ddd0 100644 --- a/src/Dibi/Drivers/Engines/OracleEngine.php +++ b/src/Dibi/Drivers/Engines/OracleEngine.php @@ -25,6 +25,72 @@ class OracleEngine implements Engine } + public function escapeIdentifier(string $value): string + { + // @see http://download.oracle.com/docs/cd/B10500_01/server.920/a96540/sql_elements9a.htm + return '"' . str_replace('"', '""', $value) . '"'; + } + + + public function escapeBool(bool $value): string + { + return $value ? '1' : '0'; + } + + + public function escapeDate(\DateTimeInterface $value): string + { + return $this->nativeDate // TODO + ? "to_date('" . $value->format('Y-m-d') . "', 'YYYY-mm-dd')" + : $value->format('U'); + } + + + public function escapeDateTime(\DateTimeInterface $value): string + { + return $this->nativeDate // TODO + ? "to_date('" . $value->format('Y-m-d G:i:s') . "', 'YYYY-mm-dd hh24:mi:ss')" + : $value->format('U'); + } + + + public function escapeDateInterval(\DateInterval $value): string + { + throw new Dibi\NotImplementedException; + } + + + /** + * Encodes string for use in a LIKE statement. + */ + public function escapeLike(string $value, int $pos): string + { + $value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_"); + $value = str_replace("'", "''", $value); + return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'"); + } + + + /** + * Injects LIMIT/OFFSET to the SQL query. + */ + public function applyLimit(string &$sql, ?int $limit, ?int $offset): void + { + if ($limit < 0 || $offset < 0) { + throw new Dibi\NotSupportedException('Negative offset or limit.'); + + } elseif ($offset) { + // see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html + $sql = 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (' . $sql . ') t ' + . ($limit !== null ? 'WHERE ROWNUM <= ' . ($offset + $limit) : '') + . ') WHERE "__rnum" > ' . $offset; + + } elseif ($limit !== null) { + $sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit; + } + } + + /** * Returns list of tables. */ diff --git a/src/Dibi/Drivers/Engines/PostgreSQLEngine.php b/src/Dibi/Drivers/Engines/PostgreSQLEngine.php index 04c2927b..5accda2c 100644 --- a/src/Dibi/Drivers/Engines/PostgreSQLEngine.php +++ b/src/Dibi/Drivers/Engines/PostgreSQLEngine.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace Dibi\Drivers\Engines; +use Dibi; use Dibi\Drivers\Connection; use Dibi\Drivers\Engine; @@ -24,6 +25,68 @@ class PostgreSQLEngine implements Engine } + 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 = $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; + } + } + + /** * Returns list of tables. */ @@ -64,7 +127,7 @@ class PostgreSQLEngine implements Engine */ 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 * @@ -124,7 +187,7 @@ class PostgreSQLEngine implements Engine */ 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, @@ -174,7 +237,7 @@ class PostgreSQLEngine implements Engine */ 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/Engines/SQLServerEngine.php b/src/Dibi/Drivers/Engines/SQLServerEngine.php index bee8c746..5994f17f 100644 --- a/src/Dibi/Drivers/Engines/SQLServerEngine.php +++ b/src/Dibi/Drivers/Engines/SQLServerEngine.php @@ -25,6 +25,65 @@ class SQLServerEngine implements Engine } + 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 ($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); + } + } + + /** * Returns list of tables. */ diff --git a/src/Dibi/Drivers/Engines/SQLiteEngine.php b/src/Dibi/Drivers/Engines/SQLiteEngine.php index 7ab2328e..f9418709 100644 --- a/src/Dibi/Drivers/Engines/SQLiteEngine.php +++ b/src/Dibi/Drivers/Engines/SQLiteEngine.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace Dibi\Drivers\Engines; +use Dibi; use Dibi\Drivers\Connection; use Dibi\Drivers\Engine; @@ -24,6 +25,61 @@ class SQLiteEngine implements Engine } + 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 escapeDateTime(\DateTimeInterface $value): string + { + return $value->format($this->fmtDateTime); // TODO + } + + + public function escapeDateInterval(\DateInterval $value): string + { + throw new Dibi\NotImplementedException; + } + + + /** + * Encodes string for use in a LIKE statement. + */ + public function escapeLike(string $value, int $pos): string + { + $value = addcslashes($this->driver->escapeText($value), '%_\\'); + return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'"; + } + + + /** + * Injects LIMIT/OFFSET to the SQL query. + */ + public function applyLimit(string &$sql, ?int $limit, ?int $offset): void + { + if ($limit < 0 || $offset < 0) { + throw new Dibi\NotSupportedException('Negative offset or limit.'); + + } elseif ($limit !== null || $offset) { + $sql .= ' LIMIT ' . ($limit ?? '-1') + . ($offset ? ' OFFSET ' . $offset : ''); + } + } + + /** * Returns list of tables. */ @@ -49,7 +105,7 @@ class SQLiteEngine implements Engine */ 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']; @@ -76,7 +132,7 @@ class SQLiteEngine implements Engine */ 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']; @@ -84,7 +140,7 @@ class SQLiteEngine implements Engine } 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']; } @@ -127,7 +183,7 @@ class SQLiteEngine implements Engine */ 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/Firebird/Connection.php b/src/Dibi/Drivers/Firebird/Connection.php index daa8c3df..2ff5f744 100644 --- a/src/Dibi/Drivers/Firebird/Connection.php +++ b/src/Dibi/Drivers/Firebird/Connection.php @@ -233,56 +233,4 @@ class Connection implements Drivers\Connection { return "'" . str_replace("'", "''", $value) . "'"; } - - - public function escapeIdentifier(string $value): string - { - return '"' . str_replace('"', '""', $value) . '"'; - } - - - public function escapeBool(bool $value): string - { - return $value ? '1' : '0'; - } - - - public function escapeDate(\DateTimeInterface $value): string - { - return $value->format("'Y-m-d'"); - } - - - public function escapeDateTime(\DateTimeInterface $value): string - { - return "'" . substr($value->format('Y-m-d H:i:s.u'), 0, -2) . "'"; - } - - - public function escapeDateInterval(\DateInterval $value): string - { - throw new Dibi\NotImplementedException; - } - - - /** - * Encodes string for use in a LIKE statement. - */ - public function escapeLike(string $value, int $pos): string - { - $value = addcslashes($this->escapeText($value), '%_\\'); - return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'"; - } - - - /** - * Injects LIMIT/OFFSET to the SQL query. - */ - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($limit > 0 || $offset > 0) { - // http://www.firebirdsql.org/refdocs/langrefupd20-select.html - $sql = 'SELECT ' . ($limit > 0 ? 'FIRST ' . $limit : '') . ($offset > 0 ? ' SKIP ' . $offset : '') . ' * FROM (' . $sql . ')'; - } - } } diff --git a/src/Dibi/Drivers/MySQLi/Connection.php b/src/Dibi/Drivers/MySQLi/Connection.php index 3c5cad39..8beb469f 100644 --- a/src/Dibi/Drivers/MySQLi/Connection.php +++ b/src/Dibi/Drivers/MySQLi/Connection.php @@ -297,64 +297,4 @@ class Connection implements Drivers\Connection { return "_binary'" . $this->connection->escape_string($value) . "'"; } - - - public function escapeIdentifier(string $value): string - { - return '`' . str_replace('`', '``', $value) . '`'; - } - - - public function escapeBool(bool $value): string - { - return $value ? '1' : '0'; - } - - - public function escapeDate(\DateTimeInterface $value): string - { - return $value->format("'Y-m-d'"); - } - - - public function escapeDateTime(\DateTimeInterface $value): string - { - return $value->format("'Y-m-d H:i:s.u'"); - } - - - public function escapeDateInterval(\DateInterval $value): string - { - if ($value->y || $value->m || $value->d) { - throw new Dibi\NotSupportedException('Only time interval is supported.'); - } - - return $value->format("'%r%H:%I:%S.%f'"); - } - - - /** - * Encodes string for use in a LIKE statement. - */ - public function escapeLike(string $value, int $pos): string - { - $value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_"); - return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'"); - } - - - /** - * Injects LIMIT/OFFSET to the SQL query. - */ - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($limit < 0 || $offset < 0) { - throw new Dibi\NotSupportedException('Negative offset or limit.'); - - } elseif ($limit !== null || $offset) { - // see http://dev.mysql.com/doc/refman/5.0/en/select.html - $sql .= ' LIMIT ' . ($limit ?? '18446744073709551615') - . ($offset ? ' OFFSET ' . $offset : ''); - } - } } diff --git a/src/Dibi/Drivers/OCI8/Connection.php b/src/Dibi/Drivers/OCI8/Connection.php index 149e1e33..9838cfdf 100644 --- a/src/Dibi/Drivers/OCI8/Connection.php +++ b/src/Dibi/Drivers/OCI8/Connection.php @@ -223,70 +223,4 @@ class Connection implements Drivers\Connection { return "'" . str_replace("'", "''", $value) . "'"; // TODO: not tested } - - - public function escapeIdentifier(string $value): string - { - // @see http://download.oracle.com/docs/cd/B10500_01/server.920/a96540/sql_elements9a.htm - return '"' . str_replace('"', '""', $value) . '"'; - } - - - public function escapeBool(bool $value): string - { - return $value ? '1' : '0'; - } - - - public function escapeDate(\DateTimeInterface $value): string - { - return $this->nativeDate - ? "to_date('" . $value->format('Y-m-d') . "', 'YYYY-mm-dd')" - : $value->format('U'); - } - - - public function escapeDateTime(\DateTimeInterface $value): string - { - return $this->nativeDate - ? "to_date('" . $value->format('Y-m-d G:i:s') . "', 'YYYY-mm-dd hh24:mi:ss')" - : $value->format('U'); - } - - - public function escapeDateInterval(\DateInterval $value): string - { - throw new Dibi\NotImplementedException; - } - - - /** - * Encodes string for use in a LIKE statement. - */ - public function escapeLike(string $value, int $pos): string - { - $value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_"); - $value = str_replace("'", "''", $value); - return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'"); - } - - - /** - * Injects LIMIT/OFFSET to the SQL query. - */ - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($limit < 0 || $offset < 0) { - throw new Dibi\NotSupportedException('Negative offset or limit.'); - - } elseif ($offset) { - // see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html - $sql = 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (' . $sql . ') t ' - . ($limit !== null ? 'WHERE ROWNUM <= ' . ($offset + $limit) : '') - . ') WHERE "__rnum" > ' . $offset; - - } elseif ($limit !== null) { - $sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit; - } - } } diff --git a/src/Dibi/Drivers/ODBC/Connection.php b/src/Dibi/Drivers/ODBC/Connection.php index 3920f7a8..3fb632b2 100644 --- a/src/Dibi/Drivers/ODBC/Connection.php +++ b/src/Dibi/Drivers/ODBC/Connection.php @@ -209,61 +209,4 @@ class Connection implements Drivers\Connection { return "'" . str_replace("'", "''", $value) . "'"; } - - - public function escapeIdentifier(string $value): string - { - return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']'; - } - - - public function escapeBool(bool $value): string - { - return $value ? '1' : '0'; - } - - - public function escapeDate(\DateTimeInterface $value): string - { - return $value->format('#m/d/Y#'); - } - - - public function escapeDateTime(\DateTimeInterface $value): string - { - return $value->format($this->microseconds ? '#m/d/Y H:i:s.u#' : '#m/d/Y H:i:s#'); - } - - - public function escapeDateInterval(\DateInterval $value): string - { - throw new Dibi\NotImplementedException; - } - - - /** - * Encodes string for use in a LIKE statement. - */ - public function escapeLike(string $value, int $pos): string - { - $value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']); - return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'"); - } - - - /** - * Injects LIMIT/OFFSET to the SQL query. - */ - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($offset) { - throw new Dibi\NotSupportedException('Offset is not supported by this database.'); - - } elseif ($limit < 0) { - throw new Dibi\NotSupportedException('Negative offset or limit.'); - - } elseif ($limit !== null) { - $sql = 'SELECT TOP ' . $limit . ' * FROM (' . $sql . ') t'; - } - } } diff --git a/src/Dibi/Drivers/PDO/Connection.php b/src/Dibi/Drivers/PDO/Connection.php index e089b148..cae79b31 100644 --- a/src/Dibi/Drivers/PDO/Connection.php +++ b/src/Dibi/Drivers/PDO/Connection.php @@ -220,165 +220,4 @@ class Connection implements Drivers\Connection 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': - // 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; - - 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/PgSQL/Connection.php b/src/Dibi/Drivers/PgSQL/Connection.php index 3350f034..399867e9 100644 --- a/src/Dibi/Drivers/PgSQL/Connection.php +++ b/src/Dibi/Drivers/PgSQL/Connection.php @@ -272,66 +272,4 @@ class Connection implements Drivers\Connection return "'" . pg_escape_bytea($this->connection, $value) . "'"; } - - - public function escapeIdentifier(string $value): string - { - // @see http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - return '"' . str_replace('"', '""', $value) . '"'; - } - - - public function escapeBool(bool $value): string - { - return $value ? 'TRUE' : 'FALSE'; - } - - - public function escapeDate(\DateTimeInterface $value): string - { - return $value->format("'Y-m-d'"); - } - - - public function escapeDateTime(\DateTimeInterface $value): string - { - return $value->format("'Y-m-d H:i:s.u'"); - } - - - public function escapeDateInterval(\DateInterval $value): string - { - throw new Dibi\NotImplementedException; - } - - - /** - * Encodes string for use in a LIKE statement. - */ - public function escapeLike(string $value, int $pos): string - { - $bs = pg_escape_string($this->connection, '\\'); // standard_conforming_strings = on/off - $value = pg_escape_string($this->connection, $value); - $value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']); - return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'"); - } - - - /** - * Injects LIMIT/OFFSET to the SQL query. - */ - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($limit < 0 || $offset < 0) { - throw new Dibi\NotSupportedException('Negative offset or limit.'); - } - - if ($limit !== null) { - $sql .= ' LIMIT ' . $limit; - } - - if ($offset) { - $sql .= ' OFFSET ' . $offset; - } - } } diff --git a/src/Dibi/Drivers/SQLSrv/Connection.php b/src/Dibi/Drivers/SQLSrv/Connection.php index f7b38bcd..71dfcb8d 100644 --- a/src/Dibi/Drivers/SQLSrv/Connection.php +++ b/src/Dibi/Drivers/SQLSrv/Connection.php @@ -204,63 +204,4 @@ class Connection implements Drivers\Connection { return '0x' . bin2hex($value); } - - - public function escapeIdentifier(string $value): string - { - // @see https://msdn.microsoft.com/en-us/library/ms176027.aspx - return '[' . str_replace(']', ']]', $value) . ']'; - } - - - public function escapeBool(bool $value): string - { - return $value ? '1' : '0'; - } - - - public function escapeDate(\DateTimeInterface $value): string - { - return $value->format("'Y-m-d'"); - } - - - public function escapeDateTime(\DateTimeInterface $value): string - { - return 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')'; - } - - - public function escapeDateInterval(\DateInterval $value): string - { - throw new Dibi\NotImplementedException; - } - - - /** - * Encodes string for use in a LIKE statement. - */ - public function escapeLike(string $value, int $pos): string - { - $value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']); - return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'"); - } - - - /** - * Injects LIMIT/OFFSET to the SQL query. - */ - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($limit < 0 || $offset < 0) { - throw new Dibi\NotSupportedException('Negative offset or limit.'); - - } elseif ($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/SQLite3/Connection.php b/src/Dibi/Drivers/SQLite3/Connection.php index 391ad206..86d3be85 100644 --- a/src/Dibi/Drivers/SQLite3/Connection.php +++ b/src/Dibi/Drivers/SQLite3/Connection.php @@ -208,61 +208,6 @@ class Connection implements Drivers\Connection } - public function escapeIdentifier(string $value): string - { - return '[' . strtr($value, '[]', ' ') . ']'; - } - - - public function escapeBool(bool $value): string - { - return $value ? '1' : '0'; - } - - - public function escapeDate(\DateTimeInterface $value): string - { - return $value->format($this->fmtDate); - } - - - public function escapeDateTime(\DateTimeInterface $value): string - { - return $value->format($this->fmtDateTime); - } - - - public function escapeDateInterval(\DateInterval $value): string - { - throw new Dibi\NotImplementedException; - } - - - /** - * Encodes string for use in a LIKE statement. - */ - public function escapeLike(string $value, int $pos): string - { - $value = addcslashes($this->connection->escapeString($value), '%_\\'); - return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'"; - } - - - /** - * Injects LIMIT/OFFSET to the SQL query. - */ - public function applyLimit(string &$sql, ?int $limit, ?int $offset): void - { - if ($limit < 0 || $offset < 0) { - throw new Dibi\NotSupportedException('Negative offset or limit.'); - - } elseif ($limit !== null || $offset) { - $sql .= ' LIMIT ' . ($limit ?? '-1') - . ($offset ? ' OFFSET ' . $offset : ''); - } - } - - /********************* user defined functions ****************d*g**/ diff --git a/src/Dibi/Translator.php b/src/Dibi/Translator.php index 58b9bb4c..22a9e952 100644 --- a/src/Dibi/Translator.php +++ b/src/Dibi/Translator.php @@ -17,8 +17,9 @@ use function array_filter, array_keys, array_splice, array_values, count, explod */ final class Translator { - private Connection $connection; + private readonly Connection $connection; private readonly Drivers\Connection $driver; + private readonly Drivers\Engine $engine; private int $cursor = 0; private array $args; @@ -36,6 +37,7 @@ final class Translator { $this->connection = $connection; $this->driver = $connection->getDriver(); + $this->engine = $connection->getDatabaseEngine(); $this->identifiers = new HashMap($this->delimite(...)); } @@ -144,7 +146,7 @@ final class Translator // 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; @@ -209,7 +211,7 @@ final class Translator case 'n': // key, key, ... identifier names foreach ($value as $k => $v) { if (is_string($k)) { - $vx[] = $this->identifiers->$k . (empty($v) ? '' : ' AS ' . $this->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]}; @@ -346,7 +348,7 @@ final class Translator case 's': // string return $value === null ? 'NULL' - : $this->driver->escapeText((string) $value); + : $this->engine->escapeText((string) $value); case 'bin':// binary return $value === null @@ -356,7 +358,7 @@ final class Translator case 'b': // boolean return $value === null ? 'NULL' - : $this->driver->escapeBool((bool) $value); + : $this->engine->escapeBool((bool) $value); case 'sN': // string or null case 'sn': @@ -406,15 +408,15 @@ final class Translator } 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) @@ -450,16 +452,16 @@ final class Translator 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': @@ -485,16 +487,16 @@ final class Translator 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; @@ -653,7 +655,7 @@ final class Translator $parts = explode('.', $value); foreach ($parts as &$v) { if ($v !== '*') { - $v = $this->driver->escapeIdentifier($v); + $v = $this->engine->escapeIdentifier($v); } } diff --git a/tests/dibi/Connection.objectTranslator.phpt b/tests/dibi/Connection.objectTranslator.phpt index 32938af2..58e3be45 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), );