From 8a7dbcba861087b54215c9c9326909ac826ec814 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 24 Jan 2016 22:47:44 +0100 Subject: [PATCH] Revert "removed MsSqlDriver (is not available with PHP 5.3 or later; replaced with SqlsrvDriver)" This reverts commit ac1ab26e7aefb3a2e4fa809c71935db9b375e7ab. --- examples/connecting-to-databases.php | 16 ++ src/Dibi/Drivers/MsSqlDriver.php | 386 +++++++++++++++++++++++++++ src/Dibi/Drivers/MsSqlReflector.php | 216 +++++++++++++++ src/Dibi/Drivers/PdoDriver.php | 3 + src/loader.php | 4 + tests/databases.sample.ini | 14 + tests/php-win.ini | 2 + 7 files changed, 641 insertions(+) create mode 100644 src/Dibi/Drivers/MsSqlDriver.php create mode 100644 src/Dibi/Drivers/MsSqlReflector.php diff --git a/examples/connecting-to-databases.php b/examples/connecting-to-databases.php index 88bf7bee..945809e3 100644 --- a/examples/connecting-to-databases.php +++ b/examples/connecting-to-databases.php @@ -112,6 +112,22 @@ try { echo "

\n"; +// connects to MS SQL +echo '

Connecting to MS SQL: '; +try { + dibi::connect([ + 'driver' => 'mssql', + 'host' => 'localhost', + 'username' => 'root', + 'password' => 'xxx', + ]); + echo 'OK'; +} catch (Dibi\Exception $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +echo "

\n"; + + // connects to SQLSRV echo '

Connecting to Microsoft SQL Server: '; try { diff --git a/src/Dibi/Drivers/MsSqlDriver.php b/src/Dibi/Drivers/MsSqlDriver.php new file mode 100644 index 00000000..daf2b3f9 --- /dev/null +++ b/src/Dibi/Drivers/MsSqlDriver.php @@ -0,0 +1,386 @@ + the MS SQL server host name. It can also include a port number (hostname:port) + * - username (or user) + * - password (or pass) + * - database => the database name to select + * - persistent (bool) => try to find a persistent link? + * - resource (resource) => existing connection resource + * - lazy, profiler, result, substitutes, ... => see Dibi\Connection options + */ +class MsSqlDriver implements Dibi\Driver, Dibi\ResultDriver +{ + use Dibi\Strict; + + /** @var resource Connection resource */ + private $connection; + + /** @var resource Resultset resource */ + private $resultSet; + + /** @var bool */ + private $autoFree = TRUE; + + + /** + * @throws Dibi\NotSupportedException + */ + public function __construct() + { + if (!extension_loaded('mssql')) { + throw new Dibi\NotSupportedException("PHP extension 'mssql' is not loaded."); + } + } + + + /** + * Connects to a database. + * @return void + * @throws Dibi\Exception + */ + public function connect(array & $config) + { + if (isset($config['resource'])) { + $this->connection = $config['resource']; + } elseif (empty($config['persistent'])) { + $this->connection = @mssql_connect($config['host'], $config['username'], $config['password'], TRUE); // intentionally @ + } else { + $this->connection = @mssql_pconnect($config['host'], $config['username'], $config['password']); // intentionally @ + } + + if (!is_resource($this->connection)) { + throw new Dibi\DriverException("Can't connect to DB."); + } + + if (isset($config['database']) && !@mssql_select_db($this->escapeIdentifier($config['database']), $this->connection)) { // intentionally @ + throw new Dibi\DriverException("Can't select DB '$config[database]'."); + } + } + + + /** + * Disconnects from a database. + * @return void + */ + public function disconnect() + { + mssql_close($this->connection); + } + + + /** + * Executes the SQL query. + * @param string SQL statement. + * @return Dibi\ResultDriver|NULL + * @throws Dibi\DriverException + */ + public function query($sql) + { + $res = @mssql_query($sql, $this->connection); // intentionally @ + + if ($res === FALSE) { + throw new Dibi\DriverException(mssql_get_last_message(), 0, $sql); + + } elseif (is_resource($res)) { + return $this->createResultDriver($res); + } + } + + + /** + * Gets the number of affected rows by the last INSERT, UPDATE or DELETE query. + * @return int|FALSE number of rows or FALSE on error + */ + public function getAffectedRows() + { + return mssql_rows_affected($this->connection); + } + + + /** + * Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query. + * @return int|FALSE int on success or FALSE on failure + */ + public function getInsertId($sequence) + { + $res = mssql_query('SELECT @@IDENTITY', $this->connection); + if (is_resource($res)) { + $row = mssql_fetch_row($res); + return $row[0]; + } + return FALSE; + } + + + /** + * Begins a transaction (if supported). + * @param string optional savepoint name + * @return void + * @throws Dibi\DriverException + */ + public function begin($savepoint = NULL) + { + $this->query('BEGIN TRANSACTION'); + } + + + /** + * Commits statements in a transaction. + * @param string optional savepoint name + * @return void + * @throws Dibi\DriverException + */ + public function commit($savepoint = NULL) + { + $this->query('COMMIT'); + } + + + /** + * Rollback changes in a transaction. + * @param string optional savepoint name + * @return void + * @throws Dibi\DriverException + */ + public function rollback($savepoint = NULL) + { + $this->query('ROLLBACK'); + } + + + /** + * Returns the connection resource. + * @return mixed + */ + public function getResource() + { + return is_resource($this->connection) ? $this->connection : NULL; + } + + + /** + * Returns the connection reflector. + * @return Dibi\Reflector + */ + public function getReflector() + { + return new MsSqlReflector($this); + } + + + /** + * Result set driver factory. + * @param resource + * @return Dibi\ResultDriver + */ + public function createResultDriver($resource) + { + $res = clone $this; + $res->resultSet = $resource; + return $res; + } + + + /********************* SQL ****************d*g**/ + + + /** + * Encodes data for use in a SQL statement. + * @param mixed value + * @return string encoded value + */ + public function escapeText($value) + { + return "'" . str_replace("'", "''", $value) . "'"; + } + + + public function escapeBinary($value) + { + return "'" . str_replace("'", "''", $value) . "'"; + } + + + public function escapeIdentifier($value) + { + // @see https://msdn.microsoft.com/en-us/library/ms176027.aspx + return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']'; + } + + + public function escapeBool($value) + { + return $value ? 1 : 0; + } + + + public function escapeDate($value) + { + if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) { + $value = new Dibi\DateTime($value); + } + return $value->format("'Y-m-d'"); + } + + + public function escapeDateTime($value) + { + if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) { + $value = new Dibi\DateTime($value); + } + return $value->format("'Y-m-d H:i:s'"); + } + + + /** + * Encodes string for use in a LIKE statement. + * @param string + * @param int + * @return string + */ + public function escapeLike($value, $pos) + { + $value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']); + return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'"); + } + + + /** + * Decodes data from result set. + * @param string + * @return string + */ + public function unescapeBinary($value) + { + return $value; + } + + + /** @deprecated */ + public function escape($value, $type) + { + trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED); + return Dibi\Helpers::escape($this, $value, $type); + } + + + /** + * Injects LIMIT/OFFSET to the SQL query. + * @return void + */ + public function applyLimit(& $sql, $limit, $offset) + { + 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 ' . (int) $limit . ' * FROM (' . $sql . ') t'; + } + } + + + /********************* result set ****************d*g**/ + + + /** + * Automatically frees the resources allocated for this result set. + * @return void + */ + public function __destruct() + { + $this->autoFree && $this->getResultResource() && $this->free(); + } + + + /** + * Returns the number of rows in a result set. + * @return int + */ + public function getRowCount() + { + return mssql_num_rows($this->resultSet); + } + + + /** + * Fetches the row at current position and moves the internal cursor to the next position. + * @param bool TRUE for associative array, FALSE for numeric + * @return array array on success, nonarray if no next record + */ + public function fetch($assoc) + { + return mssql_fetch_array($this->resultSet, $assoc ? MSSQL_ASSOC : MSSQL_NUM); + } + + + /** + * Moves cursor position without fetching row. + * @param int the 0-based cursor pos to seek to + * @return boolean TRUE on success, FALSE if unable to seek to specified record + */ + public function seek($row) + { + return mssql_data_seek($this->resultSet, $row); + } + + + /** + * Frees the resources allocated for this result set. + * @return void + */ + public function free() + { + mssql_free_result($this->resultSet); + $this->resultSet = NULL; + } + + + /** + * Returns metadata for all columns in a result set. + * @return array + */ + public function getResultColumns() + { + $count = mssql_num_fields($this->resultSet); + $columns = []; + for ($i = 0; $i < $count; $i++) { + $row = (array) mssql_fetch_field($this->resultSet, $i); + $columns[] = [ + 'name' => $row['name'], + 'fullname' => $row['column_source'] ? $row['column_source'] . '.' . $row['name'] : $row['name'], + 'table' => $row['column_source'], + 'nativetype' => $row['type'], + ]; + } + return $columns; + } + + + /** + * Returns the result set resource. + * @return mixed + */ + public function getResultResource() + { + $this->autoFree = FALSE; + return is_resource($this->resultSet) ? $this->resultSet : NULL; + } + +} diff --git a/src/Dibi/Drivers/MsSqlReflector.php b/src/Dibi/Drivers/MsSqlReflector.php new file mode 100644 index 00000000..52cc9fd4 --- /dev/null +++ b/src/Dibi/Drivers/MsSqlReflector.php @@ -0,0 +1,216 @@ +driver = $driver; + } + + + /** + * Returns list of tables. + * @return array + */ + public function getTables() + { + $res = $this->driver->query(' + SELECT TABLE_NAME, TABLE_TYPE + FROM INFORMATION_SCHEMA.TABLES + '); + $tables = []; + while ($row = $res->fetch(FALSE)) { + $tables[] = [ + 'name' => $row[0], + 'view' => isset($row[1]) && $row[1] === 'VIEW', + ]; + } + return $tables; + } + + + /** + * Returns count of rows in a table + * @param string + * @return int + */ + public function getTableCount($table, $fallback = TRUE) + { + if (empty($table)) { + return FALSE; + } + $result = $this->driver->query(" + SELECT MAX(rowcnt) + FROM sys.sysindexes + WHERE id=OBJECT_ID({$this->driver->escapeIdentifier($table)}) + "); + $row = $result->fetch(FALSE); + + if (!is_array($row) || count($row) < 1) { + if ($fallback) { + $row = $this->driver->query("SELECT COUNT(*) FROM {$this->driver->escapeIdentifier($table)}")->fetch(FALSE); + $count = intval($row[0]); + } else { + $count = FALSE; + } + } else { + $count = intval($row[0]); + } + + return $count; + } + + + /** + * Returns metadata for all columns in a table. + * @param string + * @return array + */ + public function getColumns($table) + { + $res = $this->driver->query(" + SELECT * FROM + INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = {$this->driver->escapeText($table)} + ORDER BY TABLE_NAME, ORDINAL_POSITION + "); + $columns = []; + while ($row = $res->fetch(TRUE)) { + $size = FALSE; + $type = strtoupper($row['DATA_TYPE']); + + $size_cols = [ + 'DATETIME' => 'DATETIME_PRECISION', + 'DECIMAL' => 'NUMERIC_PRECISION', + 'CHAR' => 'CHARACTER_MAXIMUM_LENGTH', + 'NCHAR' => 'CHARACTER_OCTET_LENGTH', + 'NVARCHAR' => 'CHARACTER_OCTET_LENGTH', + 'VARCHAR' => 'CHARACTER_OCTET_LENGTH', + ]; + + if (isset($size_cols[$type])) { + if ($size_cols[$type]) { + $size = $row[$size_cols[$type]]; + } + } + + $columns[] = [ + 'name' => $row['COLUMN_NAME'], + 'table' => $table, + 'nativetype' => $type, + 'size' => $size, + 'unsigned' => NULL, + 'nullable' => $row['IS_NULLABLE'] === 'YES', + 'default' => $row['COLUMN_DEFAULT'], + 'autoincrement' => FALSE, + 'vendor' => $row, + ]; + } + + return $columns; + } + + + /** + * Returns metadata for all indexes in a table. + * @param string + * @return array + */ + public function getIndexes($table) + { + $res = $this->driver->query( + "SELECT ind.name index_name, ind.index_id, ic.index_column_id, + col.name column_name, ind.is_unique, ind.is_primary_key + FROM sys.indexes ind + INNER JOIN sys.index_columns ic ON + (ind.object_id = ic.object_id AND ind.index_id = ic.index_id) + INNER JOIN sys.columns col ON + (ic.object_id = col.object_id and ic.column_id = col.column_id) + INNER JOIN sys.tables t ON + (ind.object_id = t.object_id) + WHERE t.name = {$this->driver->escapeText($table)} + AND t.is_ms_shipped = 0 + ORDER BY + t.name, ind.name, ind.index_id, ic.index_column_id + "); + + $indexes = []; + while ($row = $res->fetch(TRUE)) { + $index_name = $row['index_name']; + + if (!isset($indexes[$index_name])) { + $indexes[$index_name] = []; + $indexes[$index_name]['name'] = $index_name; + $indexes[$index_name]['unique'] = (bool) $row['is_unique']; + $indexes[$index_name]['primary'] = (bool) $row['is_primary_key']; + $indexes[$index_name]['columns'] = []; + } + $indexes[$index_name]['columns'][] = $row['column_name']; + } + + return array_values($indexes); + } + + + /** + * Returns metadata for all foreign keys in a table. + * @param string + * @return array + */ + public function getForeignKeys($table) + { + $res = $this->driver->query(" + SELECT f.name AS foreign_key, + OBJECT_NAME(f.parent_object_id) AS table_name, + COL_NAME(fc.parent_object_id, + fc.parent_column_id) AS column_name, + OBJECT_NAME (f.referenced_object_id) AS reference_table_name, + COL_NAME(fc.referenced_object_id, + fc.referenced_column_id) AS reference_column_name, + fc.* + FROM sys.foreign_keys AS f + INNER JOIN sys.foreign_key_columns AS fc + ON f.OBJECT_ID = fc.constraint_object_id + WHERE OBJECT_NAME(f.parent_object_id) = {$this->driver->escapeText($table)} + "); + + $keys = []; + while ($row = $res->fetch(TRUE)) { + $key_name = $row['foreign_key']; + + if (!isset($keys[$key_name])) { + $keys[$key_name]['name'] = $row['foreign_key']; // foreign key name + $keys[$key_name]['local'] = [$row['column_name']]; // local columns + $keys[$key_name]['table'] = $row['reference_table_name']; // referenced table + $keys[$key_name]['foreign'] = [$row['reference_column_name']]; // referenced columns + $keys[$key_name]['onDelete'] = FALSE; + $keys[$key_name]['onUpdate'] = FALSE; + } else { + $keys[$key_name]['local'][] = $row['column_name']; // local columns + $keys[$key_name]['foreign'][] = $row['reference_column_name']; // referenced columns + } + } + return array_values($keys); + } + +} diff --git a/src/Dibi/Drivers/PdoDriver.php b/src/Dibi/Drivers/PdoDriver.php index 5be28a7b..4f6ca0bc 100644 --- a/src/Dibi/Drivers/PdoDriver.php +++ b/src/Dibi/Drivers/PdoDriver.php @@ -291,6 +291,7 @@ class PdoDriver implements Dibi\Driver, Dibi\ResultDriver return '[' . strtr($value, '[]', ' ') . ']'; case 'odbc': + case 'mssql': return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']'; case 'dblib': @@ -360,6 +361,7 @@ class PdoDriver implements Dibi\Driver, Dibi\ResultDriver return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'"; case 'odbc': + case 'mssql': case 'dblib': case 'sqlsrv': $value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']); @@ -437,6 +439,7 @@ class PdoDriver implements Dibi\Driver, Dibi\ResultDriver } break; + case 'mssql': case 'sqlsrv': case 'dblib': if (version_compare($this->serverVersion, '11.0') >= 0) { // 11 == SQL Server 2012 diff --git a/src/loader.php b/src/loader.php index 97f2d752..71aa55df 100644 --- a/src/loader.php +++ b/src/loader.php @@ -26,6 +26,8 @@ spl_autoload_register(function ($class) { 'Dibi\Drivers\FirebirdDriver' => 'Drivers/FirebirdDriver.php', 'Dibi\Drivers\SqlsrvDriver' => 'Drivers/SqlsrvDriver.php', 'Dibi\Drivers\SqlsrvReflector' => 'Drivers/SqlsrvReflector.php', + 'Dibi\Drivers\MsSqlDriver' => 'Drivers/MsSqlDriver.php', + 'Dibi\Drivers\MsSqlReflector' => 'Drivers/MsSqlReflector.php', 'Dibi\Drivers\MySqlDriver' => 'Drivers/MySqlDriver.php', 'Dibi\Drivers\MySqliDriver' => 'Drivers/MySqliDriver.php', 'Dibi\Drivers\MySqlReflector' => 'Drivers/MySqlReflector.php', @@ -84,6 +86,8 @@ spl_autoload_register(function ($class) { 'DibiLiteral' => 'Dibi\Literal', 'DibiMsSql2005Driver' => 'Dibi\Drivers\SqlsrvDriver', 'DibiMsSql2005Reflector' => 'Dibi\Drivers\SqlsrvReflector', + 'DibiMsSqlDriver' => 'Dibi\Drivers\MsSqlDriver', + 'DibiMsSqlReflector' => 'Dibi\Drivers\MsSqlReflector', 'DibiMySqlDriver' => 'Dibi\Drivers\MySqlDriver', 'DibiMySqliDriver' => 'Dibi\Drivers\MySqliDriver', 'DibiMySqlReflector' => 'Dibi\Drivers\MySqlReflector', diff --git a/tests/databases.sample.ini b/tests/databases.sample.ini index eac7d0c1..1c6e12ea 100644 --- a/tests/databases.sample.ini +++ b/tests/databases.sample.ini @@ -57,6 +57,20 @@ username = password = system = odbc +[mssql] +driver = mssql +host = 127.0.0.1 +username = dibi +password = +system = mssql + +[mssql pdo] +driver = pdo +host = mssql:host=127.0.0.1;dbname=dibi_test +username = dibi +password = +system = mssql + [sqlsrv] driver = sqlsrv host = (local) diff --git a/tests/php-win.ini b/tests/php-win.ini index b1951739..28f95133 100644 --- a/tests/php-win.ini +++ b/tests/php-win.ini @@ -1,10 +1,12 @@ [PHP] extension_dir = "./ext" +;extension=php_mssql.dll extension=php_mysql.dll extension=php_mysqli.dll ;extension=php_oci8.dll ;extension=php_oci8_11g.dll ;extension=php_pdo_firebird.dll +;extension=php_pdo_mssql.dll extension=php_pdo_mysql.dll ;extension=php_pdo_oci.dll extension=php_pdo_odbc.dll