File indexing completed on 2025-01-19 05:21:01
0001 <?php 0002 /** 0003 * Zend Framework 0004 * 0005 * LICENSE 0006 * 0007 * This source file is subject to the new BSD license that is bundled 0008 * with this package in the file LICENSE.txt. 0009 * It is also available through the world-wide-web at this URL: 0010 * http://framework.zend.com/license/new-bsd 0011 * If you did not receive a copy of the license and are unable to 0012 * obtain it through the world-wide-web, please send an email 0013 * to license@zend.com so we can send you a copy immediately. 0014 * 0015 * @category Zend 0016 * @package Zend_Db 0017 * @subpackage Adapter 0018 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0019 * @license http://framework.zend.com/license/new-bsd New BSD License 0020 * @version $Id$ 0021 */ 0022 0023 0024 /** 0025 * @see Zend_Db_Adapter_Pdo_Abstract 0026 */ 0027 // require_once 'Zend/Db/Adapter/Pdo/Abstract.php'; 0028 0029 0030 /** 0031 * Class for connecting to Microsoft SQL Server databases and performing common operations. 0032 * 0033 * @category Zend 0034 * @package Zend_Db 0035 * @subpackage Adapter 0036 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0037 * @license http://framework.zend.com/license/new-bsd New BSD License 0038 */ 0039 class Zend_Db_Adapter_Pdo_Mssql extends Zend_Db_Adapter_Pdo_Abstract 0040 { 0041 /** 0042 * PDO type. 0043 * 0044 * @var string 0045 */ 0046 protected $_pdoType = 'mssql'; 0047 0048 /** 0049 * Keys are UPPERCASE SQL datatypes or the constants 0050 * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE. 0051 * 0052 * Values are: 0053 * 0 = 32-bit integer 0054 * 1 = 64-bit integer 0055 * 2 = float or decimal 0056 * 0057 * @var array Associative array of datatypes to values 0, 1, or 2. 0058 */ 0059 protected $_numericDataTypes = array( 0060 Zend_Db::INT_TYPE => Zend_Db::INT_TYPE, 0061 Zend_Db::BIGINT_TYPE => Zend_Db::BIGINT_TYPE, 0062 Zend_Db::FLOAT_TYPE => Zend_Db::FLOAT_TYPE, 0063 'INT' => Zend_Db::INT_TYPE, 0064 'SMALLINT' => Zend_Db::INT_TYPE, 0065 'TINYINT' => Zend_Db::INT_TYPE, 0066 'BIGINT' => Zend_Db::BIGINT_TYPE, 0067 'DECIMAL' => Zend_Db::FLOAT_TYPE, 0068 'FLOAT' => Zend_Db::FLOAT_TYPE, 0069 'MONEY' => Zend_Db::FLOAT_TYPE, 0070 'NUMERIC' => Zend_Db::FLOAT_TYPE, 0071 'REAL' => Zend_Db::FLOAT_TYPE, 0072 'SMALLMONEY' => Zend_Db::FLOAT_TYPE 0073 ); 0074 0075 /** 0076 * Creates a PDO DSN for the adapter from $this->_config settings. 0077 * 0078 * @return string 0079 */ 0080 protected function _dsn() 0081 { 0082 // baseline of DSN parts 0083 $dsn = $this->_config; 0084 0085 // don't pass the username and password in the DSN 0086 unset($dsn['username']); 0087 unset($dsn['password']); 0088 unset($dsn['options']); 0089 unset($dsn['persistent']); 0090 unset($dsn['driver_options']); 0091 0092 if (isset($dsn['port'])) { 0093 $seperator = ':'; 0094 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 0095 $seperator = ','; 0096 } 0097 $dsn['host'] .= $seperator . $dsn['port']; 0098 unset($dsn['port']); 0099 } 0100 0101 // this driver supports multiple DSN prefixes 0102 // @see http://www.php.net/manual/en/ref.pdo-dblib.connection.php 0103 if (isset($dsn['pdoType'])) { 0104 switch (strtolower($dsn['pdoType'])) { 0105 case 'freetds': 0106 case 'sybase': 0107 $this->_pdoType = 'sybase'; 0108 break; 0109 case 'mssql': 0110 $this->_pdoType = 'mssql'; 0111 break; 0112 case 'dblib': 0113 default: 0114 $this->_pdoType = 'dblib'; 0115 break; 0116 } 0117 unset($dsn['pdoType']); 0118 } 0119 0120 // use all remaining parts in the DSN 0121 foreach ($dsn as $key => $val) { 0122 $dsn[$key] = "$key=$val"; 0123 } 0124 0125 $dsn = $this->_pdoType . ':' . implode(';', $dsn); 0126 return $dsn; 0127 } 0128 0129 /** 0130 * @return void 0131 */ 0132 protected function _connect() 0133 { 0134 if ($this->_connection) { 0135 return; 0136 } 0137 parent::_connect(); 0138 $this->_connection->exec('SET QUOTED_IDENTIFIER ON'); 0139 } 0140 0141 /** 0142 * Begin a transaction. 0143 * 0144 * It is necessary to override the abstract PDO transaction functions here, as 0145 * the PDO driver for MSSQL does not support transactions. 0146 */ 0147 protected function _beginTransaction() 0148 { 0149 $this->_connect(); 0150 $this->_connection->exec('BEGIN TRANSACTION'); 0151 return true; 0152 } 0153 0154 /** 0155 * Commit a transaction. 0156 * 0157 * It is necessary to override the abstract PDO transaction functions here, as 0158 * the PDO driver for MSSQL does not support transactions. 0159 */ 0160 protected function _commit() 0161 { 0162 $this->_connect(); 0163 $this->_connection->exec('COMMIT TRANSACTION'); 0164 return true; 0165 } 0166 0167 /** 0168 * Roll-back a transaction. 0169 * 0170 * It is necessary to override the abstract PDO transaction functions here, as 0171 * the PDO driver for MSSQL does not support transactions. 0172 */ 0173 protected function _rollBack() { 0174 $this->_connect(); 0175 $this->_connection->exec('ROLLBACK TRANSACTION'); 0176 return true; 0177 } 0178 0179 /** 0180 * Returns a list of the tables in the database. 0181 * 0182 * @return array 0183 */ 0184 public function listTables() 0185 { 0186 $sql = "SELECT name FROM sysobjects WHERE type = 'U' ORDER BY name"; 0187 return $this->fetchCol($sql); 0188 } 0189 0190 /** 0191 * Returns the column descriptions for a table. 0192 * 0193 * The return value is an associative array keyed by the column name, 0194 * as returned by the RDBMS. 0195 * 0196 * The value of each array element is an associative array 0197 * with the following keys: 0198 * 0199 * SCHEMA_NAME => string; name of database or schema 0200 * TABLE_NAME => string; 0201 * COLUMN_NAME => string; column name 0202 * COLUMN_POSITION => number; ordinal position of column in table 0203 * DATA_TYPE => string; SQL datatype name of column 0204 * DEFAULT => string; default expression of column, null if none 0205 * NULLABLE => boolean; true if column can have nulls 0206 * LENGTH => number; length of CHAR/VARCHAR 0207 * SCALE => number; scale of NUMERIC/DECIMAL 0208 * PRECISION => number; precision of NUMERIC/DECIMAL 0209 * UNSIGNED => boolean; unsigned property of an integer type 0210 * PRIMARY => boolean; true if column is part of the primary key 0211 * PRIMARY_POSITION => integer; position of column in primary key 0212 * PRIMARY_AUTO => integer; position of auto-generated column in primary key 0213 * 0214 * @todo Discover column primary key position. 0215 * @todo Discover integer unsigned property. 0216 * 0217 * @param string $tableName 0218 * @param string $schemaName OPTIONAL 0219 * @return array 0220 */ 0221 public function describeTable($tableName, $schemaName = null) 0222 { 0223 if ($schemaName != null) { 0224 if (strpos($schemaName, '.') !== false) { 0225 $result = explode('.', $schemaName); 0226 $schemaName = $result[1]; 0227 } 0228 } 0229 /** 0230 * Discover metadata information about this table. 0231 */ 0232 $sql = "exec sp_columns @table_name = " . $this->quoteIdentifier($tableName, true); 0233 if ($schemaName != null) { 0234 $sql .= ", @table_owner = " . $this->quoteIdentifier($schemaName, true); 0235 } 0236 0237 $stmt = $this->query($sql); 0238 $result = $stmt->fetchAll(Zend_Db::FETCH_NUM); 0239 0240 $table_name = 2; 0241 $column_name = 3; 0242 $type_name = 5; 0243 $precision = 6; 0244 $length = 7; 0245 $scale = 8; 0246 $nullable = 10; 0247 $column_def = 12; 0248 $column_position = 16; 0249 0250 /** 0251 * Discover primary key column(s) for this table. 0252 */ 0253 $sql = "exec sp_pkeys @table_name = " . $this->quoteIdentifier($tableName, true); 0254 if ($schemaName != null) { 0255 $sql .= ", @table_owner = " . $this->quoteIdentifier($schemaName, true); 0256 } 0257 0258 $stmt = $this->query($sql); 0259 $primaryKeysResult = $stmt->fetchAll(Zend_Db::FETCH_NUM); 0260 $primaryKeyColumn = array(); 0261 $pkey_column_name = 3; 0262 $pkey_key_seq = 4; 0263 foreach ($primaryKeysResult as $pkeysRow) { 0264 $primaryKeyColumn[$pkeysRow[$pkey_column_name]] = $pkeysRow[$pkey_key_seq]; 0265 } 0266 0267 $desc = array(); 0268 $p = 1; 0269 foreach ($result as $key => $row) { 0270 $identity = false; 0271 $words = explode(' ', $row[$type_name], 2); 0272 if (isset($words[0])) { 0273 $type = $words[0]; 0274 if (isset($words[1])) { 0275 $identity = (bool) preg_match('/identity/', $words[1]); 0276 } 0277 } 0278 0279 $isPrimary = array_key_exists($row[$column_name], $primaryKeyColumn); 0280 if ($isPrimary) { 0281 $primaryPosition = $primaryKeyColumn[$row[$column_name]]; 0282 } else { 0283 $primaryPosition = null; 0284 } 0285 0286 $desc[$this->foldCase($row[$column_name])] = array( 0287 'SCHEMA_NAME' => null, // @todo 0288 'TABLE_NAME' => $this->foldCase($row[$table_name]), 0289 'COLUMN_NAME' => $this->foldCase($row[$column_name]), 0290 'COLUMN_POSITION' => (int) $row[$column_position], 0291 'DATA_TYPE' => $type, 0292 'DEFAULT' => $row[$column_def], 0293 'NULLABLE' => (bool) $row[$nullable], 0294 'LENGTH' => $row[$length], 0295 'SCALE' => $row[$scale], 0296 'PRECISION' => $row[$precision], 0297 'UNSIGNED' => null, // @todo 0298 'PRIMARY' => $isPrimary, 0299 'PRIMARY_POSITION' => $primaryPosition, 0300 'IDENTITY' => $identity 0301 ); 0302 } 0303 return $desc; 0304 } 0305 0306 /** 0307 * Adds an adapter-specific LIMIT clause to the SELECT statement. 0308 * 0309 * @link http://lists.bestpractical.com/pipermail/rt-devel/2005-June/007339.html 0310 * 0311 * @param string $sql 0312 * @param integer $count 0313 * @param integer $offset OPTIONAL 0314 * @throws Zend_Db_Adapter_Exception 0315 * @return string 0316 */ 0317 public function limit($sql, $count, $offset = 0) 0318 { 0319 $count = intval($count); 0320 if ($count <= 0) { 0321 /** @see Zend_Db_Adapter_Exception */ 0322 // require_once 'Zend/Db/Adapter/Exception.php'; 0323 throw new Zend_Db_Adapter_Exception("LIMIT argument count=$count is not valid"); 0324 } 0325 0326 $offset = intval($offset); 0327 if ($offset < 0) { 0328 /** @see Zend_Db_Adapter_Exception */ 0329 // require_once 'Zend/Db/Adapter/Exception.php'; 0330 throw new Zend_Db_Adapter_Exception("LIMIT argument offset=$offset is not valid"); 0331 } 0332 0333 $sql = preg_replace( 0334 '/^SELECT\s+(DISTINCT\s)?/i', 0335 'SELECT $1TOP ' . ($count+$offset) . ' ', 0336 $sql 0337 ); 0338 0339 if ($offset > 0) { 0340 $orderby = stristr($sql, 'ORDER BY'); 0341 0342 if ($orderby !== false) { 0343 $orderParts = explode(',', substr($orderby, 8)); 0344 $pregReplaceCount = null; 0345 $orderbyInverseParts = array(); 0346 foreach ($orderParts as $orderPart) { 0347 $orderPart = rtrim($orderPart); 0348 $inv = preg_replace('/\s+desc$/i', ' ASC', $orderPart, 1, $pregReplaceCount); 0349 if ($pregReplaceCount) { 0350 $orderbyInverseParts[] = $inv; 0351 continue; 0352 } 0353 $inv = preg_replace('/\s+asc$/i', ' DESC', $orderPart, 1, $pregReplaceCount); 0354 if ($pregReplaceCount) { 0355 $orderbyInverseParts[] = $inv; 0356 continue; 0357 } else { 0358 $orderbyInverseParts[] = $orderPart . ' DESC'; 0359 } 0360 } 0361 0362 $orderbyInverse = 'ORDER BY ' . implode(', ', $orderbyInverseParts); 0363 } 0364 0365 0366 0367 0368 $sql = 'SELECT * FROM (SELECT TOP ' . $count . ' * FROM (' . $sql . ') AS inner_tbl'; 0369 if ($orderby !== false) { 0370 $sql .= ' ' . $orderbyInverse . ' '; 0371 } 0372 $sql .= ') AS outer_tbl'; 0373 if ($orderby !== false) { 0374 $sql .= ' ' . $orderby; 0375 } 0376 } 0377 0378 return $sql; 0379 } 0380 0381 /** 0382 * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column. 0383 * 0384 * As a convention, on RDBMS brands that support sequences 0385 * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence 0386 * from the arguments and returns the last id generated by that sequence. 0387 * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method 0388 * returns the last value generated for such a column, and the table name 0389 * argument is disregarded. 0390 * 0391 * Microsoft SQL Server does not support sequences, so the arguments to 0392 * this method are ignored. 0393 * 0394 * @param string $tableName OPTIONAL Name of table. 0395 * @param string $primaryKey OPTIONAL Name of primary key column. 0396 * @return string 0397 * @throws Zend_Db_Adapter_Exception 0398 */ 0399 public function lastInsertId($tableName = null, $primaryKey = null) 0400 { 0401 $sql = 'SELECT SCOPE_IDENTITY()'; 0402 return (int)$this->fetchOne($sql); 0403 } 0404 0405 /** 0406 * Retrieve server version in PHP style 0407 * Pdo_Mssql doesn't support getAttribute(PDO::ATTR_SERVER_VERSION) 0408 * @return string 0409 */ 0410 public function getServerVersion() 0411 { 0412 try { 0413 $stmt = $this->query("SELECT CAST(SERVERPROPERTY('productversion') AS VARCHAR)"); 0414 $result = $stmt->fetchAll(Zend_Db::FETCH_NUM); 0415 if (count($result)) { 0416 return $result[0][0]; 0417 } 0418 return null; 0419 } catch (PDOException $e) { 0420 return null; 0421 } 0422 } 0423 0424 /** 0425 * Quote a raw string. 0426 * 0427 * @param string $value Raw string 0428 * @return string Quoted string 0429 */ 0430 protected function _quote($value) 0431 { 0432 if (!is_int($value) && !is_float($value)) { 0433 // Fix for null-byte injection 0434 $value = addcslashes($value, "\000\032"); 0435 } 0436 return parent::_quote($value); 0437 } 0438 }