You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

543 lines
14 KiB

7 years ago
  1. <?php
  2. /**
  3. * CodeIgniter
  4. *
  5. * An open source application development framework for PHP
  6. *
  7. * This content is released under the MIT License (MIT)
  8. *
  9. * Copyright (c) 2014 - 2017, British Columbia Institute of Technology
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining a copy
  12. * of this software and associated documentation files (the "Software"), to deal
  13. * in the Software without restriction, including without limitation the rights
  14. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. * copies of the Software, and to permit persons to whom the Software is
  16. * furnished to do so, subject to the following conditions:
  17. *
  18. * The above copyright notice and this permission notice shall be included in
  19. * all copies or substantial portions of the Software.
  20. *
  21. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. * THE SOFTWARE.
  28. *
  29. * @package CodeIgniter
  30. * @author EllisLab Dev Team
  31. * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
  32. * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/)
  33. * @license http://opensource.org/licenses/MIT MIT License
  34. * @link https://codeigniter.com
  35. * @since Version 2.0.3
  36. * @filesource
  37. */
  38. defined('BASEPATH') OR exit('No direct script access allowed');
  39. /**
  40. * SQLSRV Database Adapter Class
  41. *
  42. * Note: _DB is an extender class that the app controller
  43. * creates dynamically based on whether the query builder
  44. * class is being used or not.
  45. *
  46. * @package CodeIgniter
  47. * @subpackage Drivers
  48. * @category Database
  49. * @author EllisLab Dev Team
  50. * @link https://codeigniter.com/user_guide/database/
  51. */
  52. class CI_DB_sqlsrv_driver extends CI_DB {
  53. /**
  54. * Database driver
  55. *
  56. * @var string
  57. */
  58. public $dbdriver = 'sqlsrv';
  59. /**
  60. * Scrollable flag
  61. *
  62. * Determines what cursor type to use when executing queries.
  63. *
  64. * FALSE or SQLSRV_CURSOR_FORWARD would increase performance,
  65. * but would disable num_rows() (and possibly insert_id())
  66. *
  67. * @var mixed
  68. */
  69. public $scrollable;
  70. // --------------------------------------------------------------------
  71. /**
  72. * ORDER BY random keyword
  73. *
  74. * @var array
  75. */
  76. protected $_random_keyword = array('NEWID()', 'RAND(%d)');
  77. /**
  78. * Quoted identifier flag
  79. *
  80. * Whether to use SQL-92 standard quoted identifier
  81. * (double quotes) or brackets for identifier escaping.
  82. *
  83. * @var bool
  84. */
  85. protected $_quoted_identifier = TRUE;
  86. // --------------------------------------------------------------------
  87. /**
  88. * Class constructor
  89. *
  90. * @param array $params
  91. * @return void
  92. */
  93. public function __construct($params)
  94. {
  95. parent::__construct($params);
  96. // This is only supported as of SQLSRV 3.0
  97. if ($this->scrollable === NULL)
  98. {
  99. $this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED')
  100. ? SQLSRV_CURSOR_CLIENT_BUFFERED
  101. : FALSE;
  102. }
  103. }
  104. // --------------------------------------------------------------------
  105. /**
  106. * Database connection
  107. *
  108. * @param bool $pooling
  109. * @return resource
  110. */
  111. public function db_connect($pooling = FALSE)
  112. {
  113. $charset = in_array(strtolower($this->char_set), array('utf-8', 'utf8'), TRUE)
  114. ? 'UTF-8' : SQLSRV_ENC_CHAR;
  115. $connection = array(
  116. 'UID' => empty($this->username) ? '' : $this->username,
  117. 'PWD' => empty($this->password) ? '' : $this->password,
  118. 'Database' => $this->database,
  119. 'ConnectionPooling' => ($pooling === TRUE) ? 1 : 0,
  120. 'CharacterSet' => $charset,
  121. 'Encrypt' => ($this->encrypt === TRUE) ? 1 : 0,
  122. 'ReturnDatesAsStrings' => 1
  123. );
  124. // If the username and password are both empty, assume this is a
  125. // 'Windows Authentication Mode' connection.
  126. if (empty($connection['UID']) && empty($connection['PWD']))
  127. {
  128. unset($connection['UID'], $connection['PWD']);
  129. }
  130. if (FALSE !== ($this->conn_id = sqlsrv_connect($this->hostname, $connection)))
  131. {
  132. // Determine how identifiers are escaped
  133. $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
  134. $query = $query->row_array();
  135. $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi'];
  136. $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']');
  137. }
  138. return $this->conn_id;
  139. }
  140. // --------------------------------------------------------------------
  141. /**
  142. * Select the database
  143. *
  144. * @param string $database
  145. * @return bool
  146. */
  147. public function db_select($database = '')
  148. {
  149. if ($database === '')
  150. {
  151. $database = $this->database;
  152. }
  153. if ($this->_execute('USE '.$this->escape_identifiers($database)))
  154. {
  155. $this->database = $database;
  156. $this->data_cache = array();
  157. return TRUE;
  158. }
  159. return FALSE;
  160. }
  161. // --------------------------------------------------------------------
  162. /**
  163. * Execute the query
  164. *
  165. * @param string $sql an SQL query
  166. * @return resource
  167. */
  168. protected function _execute($sql)
  169. {
  170. return ($this->scrollable === FALSE OR $this->is_write_type($sql))
  171. ? sqlsrv_query($this->conn_id, $sql)
  172. : sqlsrv_query($this->conn_id, $sql, NULL, array('Scrollable' => $this->scrollable));
  173. }
  174. // --------------------------------------------------------------------
  175. /**
  176. * Begin Transaction
  177. *
  178. * @return bool
  179. */
  180. protected function _trans_begin()
  181. {
  182. return sqlsrv_begin_transaction($this->conn_id);
  183. }
  184. // --------------------------------------------------------------------
  185. /**
  186. * Commit Transaction
  187. *
  188. * @return bool
  189. */
  190. protected function _trans_commit()
  191. {
  192. return sqlsrv_commit($this->conn_id);
  193. }
  194. // --------------------------------------------------------------------
  195. /**
  196. * Rollback Transaction
  197. *
  198. * @return bool
  199. */
  200. protected function _trans_rollback()
  201. {
  202. return sqlsrv_rollback($this->conn_id);
  203. }
  204. // --------------------------------------------------------------------
  205. /**
  206. * Affected Rows
  207. *
  208. * @return int
  209. */
  210. public function affected_rows()
  211. {
  212. return sqlsrv_rows_affected($this->result_id);
  213. }
  214. // --------------------------------------------------------------------
  215. /**
  216. * Insert ID
  217. *
  218. * Returns the last id created in the Identity column.
  219. *
  220. * @return string
  221. */
  222. public function insert_id()
  223. {
  224. return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->row()->insert_id;
  225. }
  226. // --------------------------------------------------------------------
  227. /**
  228. * Database version number
  229. *
  230. * @return string
  231. */
  232. public function version()
  233. {
  234. if (isset($this->data_cache['version']))
  235. {
  236. return $this->data_cache['version'];
  237. }
  238. if ( ! $this->conn_id OR ($info = sqlsrv_server_info($this->conn_id)) === FALSE)
  239. {
  240. return FALSE;
  241. }
  242. return $this->data_cache['version'] = $info['SQLServerVersion'];
  243. }
  244. // --------------------------------------------------------------------
  245. /**
  246. * List table query
  247. *
  248. * Generates a platform-specific query string so that the table names can be fetched
  249. *
  250. * @param bool
  251. * @return string $prefix_limit
  252. */
  253. protected function _list_tables($prefix_limit = FALSE)
  254. {
  255. $sql = 'SELECT '.$this->escape_identifiers('name')
  256. .' FROM '.$this->escape_identifiers('sysobjects')
  257. .' WHERE '.$this->escape_identifiers('type')." = 'U'";
  258. if ($prefix_limit === TRUE && $this->dbprefix !== '')
  259. {
  260. $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' "
  261. .sprintf($this->_escape_like_str, $this->_escape_like_chr);
  262. }
  263. return $sql.' ORDER BY '.$this->escape_identifiers('name');
  264. }
  265. // --------------------------------------------------------------------
  266. /**
  267. * List column query
  268. *
  269. * Generates a platform-specific query string so that the column names can be fetched
  270. *
  271. * @param string $table
  272. * @return string
  273. */
  274. protected function _list_columns($table = '')
  275. {
  276. return 'SELECT COLUMN_NAME
  277. FROM INFORMATION_SCHEMA.Columns
  278. WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
  279. }
  280. // --------------------------------------------------------------------
  281. /**
  282. * Returns an object with field data
  283. *
  284. * @param string $table
  285. * @return array
  286. */
  287. public function field_data($table)
  288. {
  289. $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT
  290. FROM INFORMATION_SCHEMA.Columns
  291. WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
  292. if (($query = $this->query($sql)) === FALSE)
  293. {
  294. return FALSE;
  295. }
  296. $query = $query->result_object();
  297. $retval = array();
  298. for ($i = 0, $c = count($query); $i < $c; $i++)
  299. {
  300. $retval[$i] = new stdClass();
  301. $retval[$i]->name = $query[$i]->COLUMN_NAME;
  302. $retval[$i]->type = $query[$i]->DATA_TYPE;
  303. $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION;
  304. $retval[$i]->default = $query[$i]->COLUMN_DEFAULT;
  305. }
  306. return $retval;
  307. }
  308. // --------------------------------------------------------------------
  309. /**
  310. * Error
  311. *
  312. * Returns an array containing code and message of the last
  313. * database error that has occurred.
  314. *
  315. * @return array
  316. */
  317. public function error()
  318. {
  319. $error = array('code' => '00000', 'message' => '');
  320. $sqlsrv_errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
  321. if ( ! is_array($sqlsrv_errors))
  322. {
  323. return $error;
  324. }
  325. $sqlsrv_error = array_shift($sqlsrv_errors);
  326. if (isset($sqlsrv_error['SQLSTATE']))
  327. {
  328. $error['code'] = isset($sqlsrv_error['code']) ? $sqlsrv_error['SQLSTATE'].'/'.$sqlsrv_error['code'] : $sqlsrv_error['SQLSTATE'];
  329. }
  330. elseif (isset($sqlsrv_error['code']))
  331. {
  332. $error['code'] = $sqlsrv_error['code'];
  333. }
  334. if (isset($sqlsrv_error['message']))
  335. {
  336. $error['message'] = $sqlsrv_error['message'];
  337. }
  338. return $error;
  339. }
  340. // --------------------------------------------------------------------
  341. /**
  342. * Update statement
  343. *
  344. * Generates a platform-specific update string from the supplied data
  345. *
  346. * @param string $table
  347. * @param array $values
  348. * @return string
  349. */
  350. protected function _update($table, $values)
  351. {
  352. $this->qb_limit = FALSE;
  353. $this->qb_orderby = array();
  354. return parent::_update($table, $values);
  355. }
  356. // --------------------------------------------------------------------
  357. /**
  358. * Truncate statement
  359. *
  360. * Generates a platform-specific truncate string from the supplied data
  361. *
  362. * If the database does not support the TRUNCATE statement,
  363. * then this method maps to 'DELETE FROM table'
  364. *
  365. * @param string $table
  366. * @return string
  367. */
  368. protected function _truncate($table)
  369. {
  370. return 'TRUNCATE TABLE '.$table;
  371. }
  372. // --------------------------------------------------------------------
  373. /**
  374. * Delete statement
  375. *
  376. * Generates a platform-specific delete string from the supplied data
  377. *
  378. * @param string $table
  379. * @return string
  380. */
  381. protected function _delete($table)
  382. {
  383. if ($this->qb_limit)
  384. {
  385. return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete';
  386. }
  387. return parent::_delete($table);
  388. }
  389. // --------------------------------------------------------------------
  390. /**
  391. * LIMIT
  392. *
  393. * Generates a platform-specific LIMIT clause
  394. *
  395. * @param string $sql SQL Query
  396. * @return string
  397. */
  398. protected function _limit($sql)
  399. {
  400. // As of SQL Server 2012 (11.0.*) OFFSET is supported
  401. if (version_compare($this->version(), '11', '>='))
  402. {
  403. // SQL Server OFFSET-FETCH can be used only with the ORDER BY clause
  404. empty($this->qb_orderby) && $sql .= ' ORDER BY 1';
  405. return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY';
  406. }
  407. $limit = $this->qb_offset + $this->qb_limit;
  408. // An ORDER BY clause is required for ROW_NUMBER() to work
  409. if ($this->qb_offset && ! empty($this->qb_orderby))
  410. {
  411. $orderby = $this->_compile_order_by();
  412. // We have to strip the ORDER BY clause
  413. $sql = trim(substr($sql, 0, strrpos($sql, $orderby)));
  414. // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results
  415. if (count($this->qb_select) === 0)
  416. {
  417. $select = '*'; // Inevitable
  418. }
  419. else
  420. {
  421. // Use only field names and their aliases, everything else is out of our scope.
  422. $select = array();
  423. $field_regexp = ($this->_quoted_identifier)
  424. ? '("[^\"]+")' : '(\[[^\]]+\])';
  425. for ($i = 0, $c = count($this->qb_select); $i < $c; $i++)
  426. {
  427. $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m)
  428. ? $m[1] : $this->qb_select[$i];
  429. }
  430. $select = implode(', ', $select);
  431. }
  432. return 'SELECT '.$select." FROM (\n\n"
  433. .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql)
  434. ."\n\n) ".$this->escape_identifiers('CI_subquery')
  435. ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit;
  436. }
  437. return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql);
  438. }
  439. // --------------------------------------------------------------------
  440. /**
  441. * Insert batch statement
  442. *
  443. * Generates a platform-specific insert string from the supplied data.
  444. *
  445. * @param string $table Table name
  446. * @param array $keys INSERT keys
  447. * @param array $values INSERT values
  448. * @return string|bool
  449. */
  450. protected function _insert_batch($table, $keys, $values)
  451. {
  452. // Multiple-value inserts are only supported as of SQL Server 2008
  453. if (version_compare($this->version(), '10', '>='))
  454. {
  455. return parent::_insert_batch($table, $keys, $values);
  456. }
  457. return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE;
  458. }
  459. // --------------------------------------------------------------------
  460. /**
  461. * Close DB Connection
  462. *
  463. * @return void
  464. */
  465. protected function _close()
  466. {
  467. sqlsrv_close($this->conn_id);
  468. }
  469. }