PageRenderTime 38ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/dml/sqlsrv_native_moodle_database.php

https://bitbucket.org/kudutest1/moodlegit
PHP | 1347 lines | 833 code | 170 blank | 344 comment | 118 complexity | 39230246d83cdf68d952a45ad2899293 MD5 | raw file
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 2 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Native sqlsrv class representing moodle database interface.
  18. *
  19. * @package core_dml
  20. * @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later
  22. */
  23. defined('MOODLE_INTERNAL') || die();
  24. require_once(__DIR__.'/moodle_database.php');
  25. require_once(__DIR__.'/sqlsrv_native_moodle_recordset.php');
  26. require_once(__DIR__.'/sqlsrv_native_moodle_temptables.php');
  27. /**
  28. * Native sqlsrv class representing moodle database interface.
  29. *
  30. * @package core_dml
  31. * @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  32. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later
  33. */
  34. class sqlsrv_native_moodle_database extends moodle_database {
  35. protected $sqlsrv = null;
  36. protected $last_error_reporting; // To handle SQL*Server-Native driver default verbosity
  37. protected $temptables; // Control existing temptables (sqlsrv_moodle_temptables object)
  38. protected $collation; // current DB collation cache
  39. /** @var array list of open recordsets */
  40. protected $recordsets = array();
  41. /**
  42. * Constructor - instantiates the database, specifying if it's external (connect to other systems) or no (Moodle DB)
  43. * note this has effect to decide if prefix checks must be performed or no
  44. * @param bool true means external database used
  45. */
  46. public function __construct($external=false) {
  47. parent::__construct($external);
  48. }
  49. /**
  50. * Detects if all needed PHP stuff installed.
  51. * Note: can be used before connect()
  52. * @return mixed true if ok, string if something
  53. */
  54. public function driver_installed() {
  55. // use 'function_exists()' rather than 'extension_loaded()' because
  56. // the name used by 'extension_loaded()' is case specific! The extension
  57. // therefore *could be* mixed case and hence not found.
  58. if (!function_exists('sqlsrv_num_rows')) {
  59. if (stripos(PHP_OS, 'win') === 0) {
  60. return get_string('nativesqlsrvnodriver', 'install');
  61. } else {
  62. return get_string('nativesqlsrvnonwindows', 'install');
  63. }
  64. }
  65. return true;
  66. }
  67. /**
  68. * Returns database family type - describes SQL dialect
  69. * Note: can be used before connect()
  70. * @return string db family name (mysql, postgres, mssql, sqlsrv, oracle, etc.)
  71. */
  72. public function get_dbfamily() {
  73. return 'mssql';
  74. }
  75. /**
  76. * Returns more specific database driver type
  77. * Note: can be used before connect()
  78. * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
  79. */
  80. protected function get_dbtype() {
  81. return 'sqlsrv';
  82. }
  83. /**
  84. * Returns general database library name
  85. * Note: can be used before connect()
  86. * @return string db type pdo, native
  87. */
  88. protected function get_dblibrary() {
  89. return 'native';
  90. }
  91. /**
  92. * Returns localised database type name
  93. * Note: can be used before connect()
  94. * @return string
  95. */
  96. public function get_name() {
  97. return get_string('nativesqlsrv', 'install');
  98. }
  99. /**
  100. * Returns localised database configuration help.
  101. * Note: can be used before connect()
  102. * @return string
  103. */
  104. public function get_configuration_help() {
  105. return get_string('nativesqlsrvhelp', 'install');
  106. }
  107. /**
  108. * Returns localised database description
  109. * Note: can be used before connect()
  110. * @return string
  111. */
  112. public function get_configuration_hints() {
  113. $str = get_string('databasesettingssub_sqlsrv', 'install');
  114. $str .= "<p style='text-align:right'><a href=\"javascript:void(0)\" ";
  115. $str .= "onclick=\"return window.open('http://docs.moodle.org/en/Using_the_Microsoft_SQL_Server_Driver_for_PHP')\"";
  116. $str .= ">";
  117. $str .= '<img src="pix/docs.gif'.'" alt="Docs" class="iconhelp" />';
  118. $str .= get_string('moodledocslink', 'install').'</a></p>';
  119. return $str;
  120. }
  121. /**
  122. * Connect to db
  123. * Must be called before most other methods. (you can call methods that return connection configuration parameters)
  124. * @param string $dbhost The database host.
  125. * @param string $dbuser The database username.
  126. * @param string $dbpass The database username's password.
  127. * @param string $dbname The name of the database being connected to.
  128. * @param mixed $prefix string|bool The moodle db table name's prefix. false is used for external databases where prefix not used
  129. * @param array $dboptions driver specific options
  130. * @return bool true
  131. * @throws dml_connection_exception if error
  132. */
  133. public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
  134. $driverstatus = $this->driver_installed();
  135. if ($driverstatus !== true) {
  136. throw new dml_exception('dbdriverproblem', $driverstatus);
  137. }
  138. /*
  139. * Log all Errors.
  140. */
  141. sqlsrv_configure("WarningsReturnAsErrors", FALSE);
  142. sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF);
  143. sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR);
  144. $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
  145. $this->sqlsrv = sqlsrv_connect($this->dbhost, array
  146. (
  147. 'UID' => $this->dbuser,
  148. 'PWD' => $this->dbpass,
  149. 'Database' => $this->dbname,
  150. 'CharacterSet' => 'UTF-8',
  151. 'MultipleActiveResultSets' => true,
  152. 'ConnectionPooling' => !empty($this->dboptions['dbpersist']),
  153. 'ReturnDatesAsStrings' => true,
  154. ));
  155. if ($this->sqlsrv === false) {
  156. $this->sqlsrv = null;
  157. $dberr = $this->get_last_error();
  158. throw new dml_connection_exception($dberr);
  159. }
  160. // Allow quoted identifiers
  161. $sql = "SET QUOTED_IDENTIFIER ON";
  162. $this->query_start($sql, null, SQL_QUERY_AUX);
  163. $result = sqlsrv_query($this->sqlsrv, $sql);
  164. $this->query_end($result);
  165. $this->free_result($result);
  166. // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL
  167. // instead of equal(=) and distinct(<>) symbols
  168. $sql = "SET ANSI_NULLS ON";
  169. $this->query_start($sql, null, SQL_QUERY_AUX);
  170. $result = sqlsrv_query($this->sqlsrv, $sql);
  171. $this->query_end($result);
  172. $this->free_result($result);
  173. // Force ANSI warnings so arithmetic/string overflows will be
  174. // returning error instead of transparently truncating data
  175. $sql = "SET ANSI_WARNINGS ON";
  176. $this->query_start($sql, null, SQL_QUERY_AUX);
  177. $result = sqlsrv_query($this->sqlsrv, $sql);
  178. $this->query_end($result);
  179. // Concatenating null with anything MUST return NULL
  180. $sql = "SET CONCAT_NULL_YIELDS_NULL ON";
  181. $this->query_start($sql, null, SQL_QUERY_AUX);
  182. $result = sqlsrv_query($this->sqlsrv, $sql);
  183. $this->query_end($result);
  184. $this->free_result($result);
  185. // Set transactions isolation level to READ_COMMITTED
  186. // prevents dirty reads when using transactions +
  187. // is the default isolation level of sqlsrv
  188. $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
  189. $this->query_start($sql, NULL, SQL_QUERY_AUX);
  190. $result = sqlsrv_query($this->sqlsrv, $sql);
  191. $this->query_end($result);
  192. $this->free_result($result);
  193. // Connection established and configured, going to instantiate the temptables controller
  194. $this->temptables = new sqlsrv_native_moodle_temptables($this);
  195. return true;
  196. }
  197. /**
  198. * Close database connection and release all resources
  199. * and memory (especially circular memory references).
  200. * Do NOT use connect() again, create a new instance if needed.
  201. */
  202. public function dispose() {
  203. parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
  204. if ($this->sqlsrv) {
  205. sqlsrv_close($this->sqlsrv);
  206. $this->sqlsrv = null;
  207. }
  208. }
  209. /**
  210. * Called before each db query.
  211. * @param string $sql
  212. * @param array $params array of parameters
  213. * @param int $type type of query
  214. * @param mixed $extrainfo driver specific extra information
  215. * @return void
  216. */
  217. protected function query_start($sql, array $params = null, $type, $extrainfo = null) {
  218. parent::query_start($sql, $params, $type, $extrainfo);
  219. }
  220. /**
  221. * Called immediately after each db query.
  222. * @param mixed db specific result
  223. * @return void
  224. */
  225. protected function query_end($result) {
  226. parent::query_end($result);
  227. }
  228. /**
  229. * Returns database server info array
  230. * @return array Array containing 'description', 'version' and 'database' (current db) info
  231. */
  232. public function get_server_info() {
  233. static $info;
  234. if (!$info) {
  235. $server_info = sqlsrv_server_info($this->sqlsrv);
  236. if ($server_info) {
  237. $info['description'] = $server_info['SQLServerName'];
  238. $info['version'] = $server_info['SQLServerVersion'];
  239. $info['database'] = $server_info['CurrentDatabase'];
  240. }
  241. }
  242. return $info;
  243. }
  244. /**
  245. * Override: Converts short table name {tablename} to real table name
  246. * supporting temp tables (#) if detected
  247. *
  248. * @param string sql
  249. * @return string sql
  250. */
  251. protected function fix_table_names($sql) {
  252. if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/i', $sql, $matches)) {
  253. foreach ($matches[0] as $key => $match) {
  254. $name = $matches[1][$key];
  255. if ($this->temptables->is_temptable($name)) {
  256. $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
  257. } else {
  258. $sql = str_replace($match, $this->prefix.$name, $sql);
  259. }
  260. }
  261. }
  262. return $sql;
  263. }
  264. /**
  265. * Returns supported query parameter types
  266. * @return int bitmask
  267. */
  268. protected function allowed_param_types() {
  269. return SQL_PARAMS_QM; // sqlsrv 1.1 can bind
  270. }
  271. /**
  272. * Returns last error reported by database engine.
  273. * @return string error message
  274. */
  275. public function get_last_error() {
  276. $retErrors = sqlsrv_errors(SQLSRV_ERR_ALL);
  277. $errorMessage = 'No errors found';
  278. if ($retErrors != null) {
  279. $errorMessage = '';
  280. foreach ($retErrors as $arrError) {
  281. $errorMessage .= "SQLState: ".$arrError['SQLSTATE']."<br>\n";
  282. $errorMessage .= "Error Code: ".$arrError['code']."<br>\n";
  283. $errorMessage .= "Message: ".$arrError['message']."<br>\n";
  284. }
  285. }
  286. return $errorMessage;
  287. }
  288. /**
  289. * Prepare the query binding and do the actual query.
  290. *
  291. * @param string $sql The sql statement
  292. * @param array $params array of params for binding. If NULL, they are ignored.
  293. * @param int $sql_query_type - Type of operation
  294. * @param bool $free_result - Default true, transaction query will be freed.
  295. * @param bool $scrollable - Default false, to use for quickly seeking to target records
  296. * @return resource|bool result
  297. */
  298. private function do_query($sql, $params, $sql_query_type, $free_result = true, $scrollable = false) {
  299. list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
  300. /*
  301. * Bound variables *are* supported. Until I can get it to work, emulate the bindings
  302. * The challenge/problem/bug is that although they work, doing a SELECT SCOPE_IDENTITY()
  303. * doesn't return a value (no result set)
  304. *
  305. * -- somebody from MS
  306. */
  307. $sql = $this->emulate_bound_params($sql, $params);
  308. $this->query_start($sql, $params, $sql_query_type);
  309. if (!$scrollable) { // Only supporting next row
  310. $result = sqlsrv_query($this->sqlsrv, $sql);
  311. } else { // Supporting absolute/relative rows
  312. $result = sqlsrv_query($this->sqlsrv, $sql, array(), array('Scrollable' => SQLSRV_CURSOR_STATIC));
  313. }
  314. if ($result === false) {
  315. // TODO do something with error or just use if DEV or DEBUG?
  316. $dberr = $this->get_last_error();
  317. }
  318. $this->query_end($result);
  319. if ($free_result) {
  320. $this->free_result($result);
  321. return true;
  322. }
  323. return $result;
  324. }
  325. /**
  326. * Return tables in database WITHOUT current prefix.
  327. * @param bool $usecache if true, returns list of cached tables.
  328. * @return array of table names in lowercase and without prefix
  329. */
  330. public function get_tables($usecache = true) {
  331. if ($usecache and count($this->tables) > 0) {
  332. return $this->tables;
  333. }
  334. $this->tables = array ();
  335. $prefix = str_replace('_', '\\_', $this->prefix);
  336. $sql = "SELECT table_name
  337. FROM INFORMATION_SCHEMA.TABLES
  338. WHERE table_name LIKE '$prefix%' ESCAPE '\\' AND table_type = 'BASE TABLE'";
  339. $this->query_start($sql, null, SQL_QUERY_AUX);
  340. $result = sqlsrv_query($this->sqlsrv, $sql);
  341. $this->query_end($result);
  342. if ($result) {
  343. while ($row = sqlsrv_fetch_array($result)) {
  344. $tablename = reset($row);
  345. if ($this->prefix !== '') {
  346. if (strpos($tablename, $this->prefix) !== 0) {
  347. continue;
  348. }
  349. $tablename = substr($tablename, strlen($this->prefix));
  350. }
  351. $this->tables[$tablename] = $tablename;
  352. }
  353. $this->free_result($result);
  354. }
  355. // Add the currently available temptables
  356. $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
  357. return $this->tables;
  358. }
  359. /**
  360. * Return table indexes - everything lowercased.
  361. * @param string $table The table we want to get indexes from.
  362. * @return array of arrays
  363. */
  364. public function get_indexes($table) {
  365. $indexes = array ();
  366. $tablename = $this->prefix.$table;
  367. // Indexes aren't covered by information_schema metatables, so we need to
  368. // go to sys ones. Skipping primary key indexes on purpose.
  369. $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name
  370. FROM sys.indexes i
  371. JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
  372. JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
  373. JOIN sys.tables t ON i.object_id = t.object_id
  374. WHERE t.name = '$tablename' AND i.is_primary_key = 0
  375. ORDER BY i.name, i.index_id, ic.index_column_id";
  376. $this->query_start($sql, null, SQL_QUERY_AUX);
  377. $result = sqlsrv_query($this->sqlsrv, $sql);
  378. $this->query_end($result);
  379. if ($result) {
  380. $lastindex = '';
  381. $unique = false;
  382. $columns = array ();
  383. while ($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) {
  384. if ($lastindex and $lastindex != $row['index_name'])
  385. { // Save lastindex to $indexes and reset info
  386. $indexes[$lastindex] = array
  387. (
  388. 'unique' => $unique,
  389. 'columns' => $columns
  390. );
  391. $unique = false;
  392. $columns = array ();
  393. }
  394. $lastindex = $row['index_name'];
  395. $unique = empty($row['is_unique']) ? false : true;
  396. $columns[] = $row['column_name'];
  397. }
  398. if ($lastindex) { // Add the last one if exists
  399. $indexes[$lastindex] = array
  400. (
  401. 'unique' => $unique,
  402. 'columns' => $columns
  403. );
  404. }
  405. $this->free_result($result);
  406. }
  407. return $indexes;
  408. }
  409. /**
  410. * Returns detailed information about columns in table. This information is cached internally.
  411. * @param string $table name
  412. * @param bool $usecache
  413. * @return array array of database_column_info objects indexed with column names
  414. */
  415. public function get_columns($table, $usecache = true) {
  416. if ($usecache and isset($this->columns[$table])) {
  417. return $this->columns[$table];
  418. }
  419. $this->columns[$table] = array ();
  420. if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema
  421. $sql = "SELECT column_name AS name,
  422. data_type AS type,
  423. numeric_precision AS max_length,
  424. character_maximum_length AS char_max_length,
  425. numeric_scale AS scale,
  426. is_nullable AS is_nullable,
  427. columnproperty(object_id(quotename(table_schema) + '.' + quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
  428. column_default AS default_value
  429. FROM INFORMATION_SCHEMA.COLUMNS
  430. WHERE table_name = '{".$table."}'
  431. ORDER BY ordinal_position";
  432. } else { // temp table, get metadata from tempdb schema
  433. $sql = "SELECT column_name AS name,
  434. data_type AS type,
  435. numeric_precision AS max_length,
  436. character_maximum_length AS char_max_length,
  437. numeric_scale AS scale,
  438. is_nullable AS is_nullable,
  439. columnproperty(object_id(quotename(table_schema) + '.' + quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
  440. column_default AS default_value
  441. FROM tempdb.INFORMATION_SCHEMA.COLUMNS ".
  442. // check this statement
  443. // JOIN tempdb..sysobjects ON name = table_name
  444. // WHERE id = object_id('tempdb..{".$table."}')
  445. "WHERE table_name LIKE '{".$table."}__________%'
  446. ORDER BY ordinal_position";
  447. }
  448. list($sql, $params, $type) = $this->fix_sql_params($sql, null);
  449. $this->query_start($sql, null, SQL_QUERY_AUX);
  450. $result = sqlsrv_query($this->sqlsrv, $sql);
  451. $this->query_end($result);
  452. if (!$result) {
  453. return array ();
  454. }
  455. while ($rawcolumn = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) {
  456. $rawcolumn = (object)$rawcolumn;
  457. $info = new stdClass();
  458. $info->name = $rawcolumn->name;
  459. $info->type = $rawcolumn->type;
  460. $info->meta_type = $this->sqlsrvtype2moodletype($info->type);
  461. // Prepare auto_increment info
  462. $info->auto_increment = $rawcolumn->auto_increment ? true : false;
  463. // Define type for auto_increment columns
  464. $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type;
  465. // id columns being auto_incremnt are PK by definition
  466. $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
  467. // Put correct length for character and LOB types
  468. $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
  469. $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
  470. // Scale
  471. $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false;
  472. // Prepare not_null info
  473. $info->not_null = $rawcolumn->is_nullable == 'NO' ? true : false;
  474. // Process defaults
  475. $info->has_default = !empty($rawcolumn->default_value);
  476. if ($rawcolumn->default_value === NULL) {
  477. $info->default_value = NULL;
  478. } else {
  479. $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value);
  480. }
  481. // Process binary
  482. $info->binary = $info->meta_type == 'B' ? true : false;
  483. $this->columns[$table][$info->name] = new database_column_info($info);
  484. }
  485. $this->free_result($result);
  486. return $this->columns[$table];
  487. }
  488. /**
  489. * Normalise values based in RDBMS dependencies (booleans, LOBs...)
  490. *
  491. * @param database_column_info $column column metadata corresponding with the value we are going to normalise
  492. * @param mixed $value value we are going to normalise
  493. * @return mixed the normalised value
  494. */
  495. protected function normalise_value($column, $value) {
  496. $this->detect_objects($value);
  497. if (is_bool($value)) { // Always, convert boolean to int
  498. $value = (int)$value;
  499. } // And continue processing because text columns with numeric info need special handling below
  500. if ($column->meta_type == 'B')
  501. { // BLOBs need to be properly "packed", but can be inserted directly if so.
  502. if (!is_null($value)) { // If value not null, unpack it to unquoted hexadecimal byte-string format
  503. $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it
  504. } // easily and "bind" the param ok.
  505. } else if ($column->meta_type == 'X') { // sqlsrv doesn't cast from int to text, so if text column
  506. if (is_numeric($value)) { // and is numeric value then cast to string
  507. $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how
  508. } // to "bind" the param ok, avoiding reverse conversion to number
  509. } else if ($value === '') {
  510. if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
  511. $value = 0; // prevent '' problems in numeric fields
  512. }
  513. }
  514. return $value;
  515. }
  516. /**
  517. * Selectively call sqlsrv_free_stmt(), avoiding some warnings without using the horrible @
  518. *
  519. * @param sqlsrv_resource $resource resource to be freed if possible
  520. * @return bool
  521. */
  522. private function free_result($resource) {
  523. if (!is_bool($resource)) { // true/false resources cannot be freed
  524. return sqlsrv_free_stmt($resource);
  525. }
  526. }
  527. /**
  528. * Provides mapping between sqlsrv native data types and moodle_database - database_column_info - ones)
  529. *
  530. * @param string $sqlsrv_type native sqlsrv data type
  531. * @return string 1-char database_column_info data type
  532. */
  533. private function sqlsrvtype2moodletype($sqlsrv_type) {
  534. $type = null;
  535. switch (strtoupper($sqlsrv_type)) {
  536. case 'BIT':
  537. $type = 'L';
  538. break;
  539. case 'INT':
  540. case 'SMALLINT':
  541. case 'INTEGER':
  542. case 'BIGINT':
  543. $type = 'I';
  544. break;
  545. case 'DECIMAL':
  546. case 'REAL':
  547. case 'FLOAT':
  548. $type = 'N';
  549. break;
  550. case 'VARCHAR':
  551. case 'NVARCHAR':
  552. $type = 'C';
  553. break;
  554. case 'TEXT':
  555. case 'NTEXT':
  556. case 'VARCHAR(MAX)':
  557. case 'NVARCHAR(MAX)':
  558. $type = 'X';
  559. break;
  560. case 'IMAGE':
  561. case 'VARBINARY(MAX)':
  562. $type = 'B';
  563. break;
  564. case 'DATETIME':
  565. $type = 'D';
  566. break;
  567. }
  568. if (!$type) {
  569. throw new dml_exception('invalidsqlsrvnativetype', $sqlsrv_type);
  570. }
  571. return $type;
  572. }
  573. /**
  574. * Do NOT use in code, to be used by database_manager only!
  575. * @param string $sql query
  576. * @return bool true
  577. * @throws dml_exception A DML specific exception is thrown for any errors.
  578. */
  579. public function change_database_structure($sql) {
  580. $this->reset_caches();
  581. $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
  582. $result = sqlsrv_query($this->sqlsrv, $sql);
  583. $this->query_end($result);
  584. return true;
  585. }
  586. /**
  587. * Prepare the array of params for native binding
  588. */
  589. protected function build_native_bound_params(array $params = null) {
  590. return null;
  591. }
  592. /**
  593. * Workaround for SQL*Server Native driver similar to MSSQL driver for
  594. * consistent behavior.
  595. */
  596. protected function emulate_bound_params($sql, array $params = null) {
  597. if (empty($params)) {
  598. return $sql;
  599. }
  600. // ok, we have verified sql statement with ? and correct number of params
  601. $parts = explode('?', $sql);
  602. $return = array_shift($parts);
  603. foreach ($params as $param) {
  604. if (is_bool($param)) {
  605. $return .= (int)$param;
  606. } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially
  607. $return .= '0x'.$param['hex'];
  608. } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not*
  609. $return .= "N'{$param['numstr']}'"; // be converted back to number params, but bound as strings
  610. } else if (is_null($param)) {
  611. $return .= 'NULL';
  612. } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646
  613. $return .= "'$param'"; // this is a hack for MDL-23997, we intentionally use string because it is compatible with both nvarchar and int types
  614. } else if (is_float($param)) {
  615. $return .= $param;
  616. } else {
  617. $param = str_replace("'", "''", $param);
  618. $return .= "N'$param'";
  619. }
  620. $return .= array_shift($parts);
  621. }
  622. return $return;
  623. }
  624. /**
  625. * Execute general sql query. Should be used only when no other method suitable.
  626. * Do NOT use this to make changes in db structure, use database_manager methods instead!
  627. * @param string $sql query
  628. * @param array $params query parameters
  629. * @return bool true
  630. * @throws dml_exception A DML specific exception is thrown for any errors.
  631. */
  632. public function execute($sql, array $params = null) {
  633. if (strpos($sql, ';') !== false) {
  634. throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
  635. }
  636. $this->do_query($sql, $params, SQL_QUERY_UPDATE);
  637. return true;
  638. }
  639. /**
  640. * Get a number of records as a moodle_recordset using a SQL statement.
  641. *
  642. * Since this method is a little less readable, use of it should be restricted to
  643. * code where it's possible there might be large datasets being returned. For known
  644. * small datasets use get_records_sql - it leads to simpler code.
  645. *
  646. * The return type is like:
  647. * @see function get_recordset.
  648. *
  649. * @param string $sql the SQL select query to execute.
  650. * @param array $params array of sql parameters
  651. * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
  652. * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
  653. * @return moodle_recordset instance
  654. * @throws dml_exception A DML specific exception is thrown for any errors.
  655. */
  656. public function get_recordset_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
  657. $limitfrom = (int)$limitfrom;
  658. $limitnum = (int)$limitnum;
  659. $limitfrom = max(0, $limitfrom);
  660. $limitnum = max(0, $limitnum);
  661. if ($limitfrom or $limitnum) {
  662. if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later)
  663. $fetch = $limitfrom + $limitnum;
  664. if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow
  665. $fetch = PHP_INT_MAX;
  666. }
  667. $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
  668. "\\1SELECT\\2 TOP $fetch", $sql);
  669. }
  670. }
  671. $result = $this->do_query($sql, $params, SQL_QUERY_SELECT, false, (bool)$limitfrom);
  672. if ($limitfrom) { // Skip $limitfrom records
  673. sqlsrv_fetch($result, SQLSRV_SCROLL_ABSOLUTE, $limitfrom - 1);
  674. }
  675. return $this->create_recordset($result);
  676. }
  677. /**
  678. * Create a record set and initialize with first row
  679. *
  680. * @param mixed $result
  681. * @return sqlsrv_native_moodle_recordset
  682. */
  683. protected function create_recordset($result) {
  684. $rs = new sqlsrv_native_moodle_recordset($result, $this);
  685. $this->recordsets[] = $rs;
  686. return $rs;
  687. }
  688. /**
  689. * Do not use outside of recordset class.
  690. * @internal
  691. * @param sqlsrv_native_moodle_recordset $rs
  692. */
  693. public function recordset_closed(sqlsrv_native_moodle_recordset $rs) {
  694. if ($key = array_search($rs, $this->recordsets, true)) {
  695. unset($this->recordsets[$key]);
  696. }
  697. }
  698. /**
  699. * Get a number of records as an array of objects using a SQL statement.
  700. *
  701. * Return value is like:
  702. * @see function get_records.
  703. *
  704. * @param string $sql the SQL select query to execute. The first column of this SELECT statement
  705. * must be a unique value (usually the 'id' field), as it will be used as the key of the
  706. * returned array.
  707. * @param array $params array of sql parameters
  708. * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
  709. * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
  710. * @return array of objects, or empty array if no records were found
  711. * @throws dml_exception A DML specific exception is thrown for any errors.
  712. */
  713. public function get_records_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
  714. $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
  715. $results = array();
  716. foreach ($rs as $row) {
  717. $id = reset($row);
  718. if (isset($results[$id])) {
  719. $colname = key($row);
  720. debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate value '$id' found in column '$colname'.", DEBUG_DEVELOPER);
  721. }
  722. $results[$id] = (object)$row;
  723. }
  724. $rs->close();
  725. return $results;
  726. }
  727. /**
  728. * Selects records and return values (first field) as an array using a SQL statement.
  729. *
  730. * @param string $sql The SQL query
  731. * @param array $params array of sql parameters
  732. * @return array of values
  733. * @throws dml_exception A DML specific exception is thrown for any errors.
  734. */
  735. public function get_fieldset_sql($sql, array $params = null) {
  736. $rs = $this->get_recordset_sql($sql, $params);
  737. $results = array ();
  738. foreach ($rs as $row) {
  739. $results[] = reset($row);
  740. }
  741. $rs->close();
  742. return $results;
  743. }
  744. /**
  745. * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
  746. * @param string $table name
  747. * @param mixed $params data record as object or array
  748. * @param bool $returnit return it of inserted record
  749. * @param bool $bulk true means repeated inserts expected
  750. * @param bool $customsequence true if 'id' included in $params, disables $returnid
  751. * @return bool|int true or new id
  752. * @throws dml_exception A DML specific exception is thrown for any errors.
  753. */
  754. public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
  755. if (!is_array($params)) {
  756. $params = (array)$params;
  757. }
  758. $isidentity = false;
  759. if ($customsequence) {
  760. if (!isset($params['id'])) {
  761. throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
  762. }
  763. $returnid = false;
  764. $columns = $this->get_columns($table);
  765. if (isset($columns['id']) and $columns['id']->auto_increment) {
  766. $isidentity = true;
  767. }
  768. // Disable IDENTITY column before inserting record with id, only if the
  769. // column is identity, from meta information.
  770. if ($isidentity) {
  771. $sql = 'SET IDENTITY_INSERT {'.$table.'} ON'; // Yes, it' ON!!
  772. $this->do_query($sql, null, SQL_QUERY_AUX);
  773. }
  774. } else {
  775. unset($params['id']);
  776. }
  777. if (empty($params)) {
  778. throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
  779. }
  780. $fields = implode(',', array_keys($params));
  781. $qms = array_fill(0, count($params), '?');
  782. $qms = implode(',', $qms);
  783. $sql = "INSERT INTO {" . $table . "} ($fields) VALUES($qms)";
  784. $query_id = $this->do_query($sql, $params, SQL_QUERY_INSERT);
  785. if ($customsequence) {
  786. // Enable IDENTITY column after inserting record with id, only if the
  787. // column is identity, from meta information.
  788. if ($isidentity) {
  789. $sql = 'SET IDENTITY_INSERT {'.$table.'} OFF'; // Yes, it' OFF!!
  790. $this->do_query($sql, null, SQL_QUERY_AUX);
  791. }
  792. }
  793. if ($returnid) {
  794. $id = $this->sqlsrv_fetch_id();
  795. return $id;
  796. } else {
  797. return true;
  798. }
  799. }
  800. /**
  801. * Get the ID of the current action
  802. *
  803. * @return mixed ID
  804. */
  805. private function sqlsrv_fetch_id() {
  806. $query_id = sqlsrv_query($this->sqlsrv, 'SELECT SCOPE_IDENTITY()');
  807. if ($query_id === false) {
  808. $dberr = $this->get_last_error();
  809. return false;
  810. }
  811. $row = $this->sqlsrv_fetchrow($query_id);
  812. return (int)$row[0];
  813. }
  814. /**
  815. * Fetch a single row into an numbered array
  816. *
  817. * @param mixed $query_id
  818. */
  819. private function sqlsrv_fetchrow($query_id) {
  820. $row = sqlsrv_fetch_array($query_id, SQLSRV_FETCH_NUMERIC);
  821. if ($row === false) {
  822. $dberr = $this->get_last_error();
  823. return false;
  824. }
  825. foreach ($row as $key => $value) {
  826. $row[$key] = ($value === ' ' || $value === NULL) ? '' : $value;
  827. }
  828. return $row;
  829. }
  830. /**
  831. * Insert a record into a table and return the "id" field if required.
  832. *
  833. * Some conversions and safety checks are carried out. Lobs are supported.
  834. * If the return ID isn't required, then this just reports success as true/false.
  835. * $data is an object containing needed data
  836. * @param string $table The database table to be inserted into
  837. * @param object $data A data object with values for one or more fields in the record
  838. * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
  839. * @return bool|int true or new id
  840. * @throws dml_exception A DML specific exception is thrown for any errors.
  841. */
  842. public function insert_record($table, $dataobject, $returnid = true, $bulk = false) {
  843. $dataobject = (array)$dataobject;
  844. $columns = $this->get_columns($table);
  845. $cleaned = array ();
  846. foreach ($dataobject as $field => $value) {
  847. if ($field === 'id') {
  848. continue;
  849. }
  850. if (!isset($columns[$field])) {
  851. continue;
  852. }
  853. $column = $columns[$field];
  854. $cleaned[$field] = $this->normalise_value($column, $value);
  855. }
  856. return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
  857. }
  858. /**
  859. * Import a record into a table, id field is required.
  860. * Safety checks are NOT carried out. Lobs are supported.
  861. *
  862. * @param string $table name of database table to be inserted into
  863. * @param object $dataobject A data object with values for one or more fields in the record
  864. * @return bool true
  865. * @throws dml_exception A DML specific exception is thrown for any errors.
  866. */
  867. public function import_record($table, $dataobject) {
  868. if (!is_object($dataobject)) {
  869. $dataobject = (object)$dataobject;
  870. }
  871. $columns = $this->get_columns($table);
  872. $cleaned = array ();
  873. foreach ($dataobject as $field => $value) {
  874. if (!isset($columns[$field])) {
  875. continue;
  876. }
  877. $column = $columns[$field];
  878. $cleaned[$field] = $this->normalise_value($column, $value);
  879. }
  880. $this->insert_record_raw($table, $cleaned, false, false, true);
  881. return true;
  882. }
  883. /**
  884. * Update record in database, as fast as possible, no safety checks, lobs not supported.
  885. * @param string $table name
  886. * @param mixed $params data record as object or array
  887. * @param bool true means repeated updates expected
  888. * @return bool true
  889. * @throws dml_exception A DML specific exception is thrown for any errors.
  890. */
  891. public function update_record_raw($table, $params, $bulk = false) {
  892. $params = (array)$params;
  893. if (!isset($params['id'])) {
  894. throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
  895. }
  896. $id = $params['id'];
  897. unset($params['id']);
  898. if (empty($params)) {
  899. throw new coding_exception('moodle_database::update_record_raw() no fields found.');
  900. }
  901. $sets = array ();
  902. foreach ($params as $field => $value) {
  903. $sets[] = "$field = ?";
  904. }
  905. $params[] = $id; // last ? in WHERE condition
  906. $sets = implode(',', $sets);
  907. $sql = "UPDATE {".$table."} SET $sets WHERE id = ?";
  908. $this->do_query($sql, $params, SQL_QUERY_UPDATE);
  909. return true;
  910. }
  911. /**
  912. * Update a record in a table
  913. *
  914. * $dataobject is an object containing needed data
  915. * Relies on $dataobject having a variable "id" to
  916. * specify the record to update
  917. *
  918. * @param string $table The database table to be checked against.
  919. * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
  920. * @param bool true means repeated updates expected
  921. * @return bool true
  922. * @throws dml_exception A DML specific exception is thrown for any errors.
  923. */
  924. public function update_record($table, $dataobject, $bulk = false) {
  925. $dataobject = (array)$dataobject;
  926. $columns = $this->get_columns($table);
  927. $cleaned = array ();
  928. foreach ($dataobject as $field => $value) {
  929. if (!isset($columns[$field])) {
  930. continue;
  931. }
  932. $column = $columns[$field];
  933. $cleaned[$field] = $this->normalise_value($column, $value);
  934. }
  935. return $this->update_record_raw($table, $cleaned, $bulk);
  936. }
  937. /**
  938. * Set a single field in every table record which match a particular WHERE clause.
  939. *
  940. * @param string $table The database table to be checked against.
  941. * @param string $newfield the field to set.
  942. * @param string $newvalue the value to set the field to.
  943. * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
  944. * @param array $params array of sql parameters
  945. * @return bool true
  946. * @throws dml_exception A DML specific exception is thrown for any errors.
  947. */
  948. public function set_field_select($table, $newfield, $newvalue, $select, array $params = null) {
  949. if ($select) {
  950. $select = "WHERE $select";
  951. }
  952. if (is_null($params)) {
  953. $params = array ();
  954. }
  955. // convert params to ? types
  956. list($select, $params, $type) = $this->fix_sql_params($select, $params);
  957. // Get column metadata
  958. $columns = $this->get_columns($table);
  959. $column = $columns[$newfield];
  960. $newvalue = $this->normalise_value($column, $newvalue);
  961. if (is_null($newvalue)) {
  962. $newfield = "$newfield = NULL";
  963. } else {
  964. $newfield = "$newfield = ?";
  965. array_unshift($params, $newvalue);
  966. }
  967. $sql = "UPDATE {".$table."} SET $newfield $select";
  968. $this->do_query($sql, $params, SQL_QUERY_UPDATE);
  969. return true;
  970. }
  971. /**
  972. * Delete one or more records from a table which match a particular WHERE clause.
  973. *
  974. * @param string $table The database table to be checked against.
  975. * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
  976. * @param array $params array of sql parameters
  977. * @return bool true
  978. * @throws dml_exception A DML specific exception is thrown for any errors.
  979. */
  980. public function delete_records_select($table, $select, array $params = null) {
  981. if ($select) {
  982. $select = "WHERE $select";
  983. }
  984. $sql = "DELETE FROM {".$table."} $select";
  985. // we use SQL_QUERY_UPDATE because we do not know what is in general SQL, delete constant would not be accurate
  986. $this->do_query($sql, $params, SQL_QUERY_UPDATE);
  987. return true;
  988. }
  989. public function sql_cast_char2int($fieldname, $text = false) {
  990. if (!$text) {
  991. return ' CAST(' . $fieldname . ' AS INT) ';
  992. } else {
  993. return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
  994. }
  995. }
  996. public function sql_cast_char2real($fieldname, $text=false) {
  997. if (!$text) {
  998. return ' CAST(' . $fieldname . ' AS REAL) ';
  999. } else {
  1000. return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) ';
  1001. }
  1002. }
  1003. public function sql_ceil($fieldname) {
  1004. return ' CEILING('.$fieldname.')';
  1005. }
  1006. protected function get_collation() {
  1007. if (isset($this->collation)) {
  1008. return $this->collation;
  1009. }
  1010. if (!empty($this->dboptions['dbcollation'])) {
  1011. // perf speedup
  1012. $this->collation = $this->dboptions['dbcollation'];
  1013. return $this->collation;
  1014. }
  1015. // make some default
  1016. $this->collation = 'Latin1_General_CI_AI';
  1017. $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation";
  1018. $this->query_start($sql, null, SQL_QUERY_AUX);
  1019. $result = sqlsrv_query($this->sqlsrv, $sql);
  1020. $this->query_end($result);
  1021. if ($result) {
  1022. if ($rawcolumn = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) {
  1023. $this->collation = reset($rawcolumn);
  1024. }
  1025. $this->free_result($result);
  1026. }
  1027. return $this->collation;
  1028. }
  1029. /**
  1030. * Returns 'LIKE' part of a query.
  1031. *
  1032. * @param string $fieldname usually name of the table column
  1033. * @param string $param usually bound query parameter (?, :named)
  1034. * @param bool $casesensitive use case sensitive search
  1035. * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
  1036. * @param bool $notlike true means "NOT LIKE"
  1037. * @param string $escapechar escape char for '%' and '_'
  1038. * @return string SQL code fragment
  1039. */
  1040. public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
  1041. if (strpos($param, '%') !== false) {
  1042. debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
  1043. }
  1044. $collation = $this->get_collation();
  1045. $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
  1046. if ($casesensitive) {
  1047. $collation = str_replace('_CI', '_CS', $collation);
  1048. } else {
  1049. $collation = str_replace('_CS', '_CI', $collation);
  1050. }
  1051. if ($accentsensitive) {
  1052. $collation = str_replace('_AI', '_AS', $collation);
  1053. } else {
  1054. $collation = str_replace('_AS', '_AI', $collation);
  1055. }
  1056. return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'";
  1057. }
  1058. public function sql_concat() {
  1059. $arr = func_get_args();
  1060. foreach ($arr as $key => $ele) {
  1061. $arr[$key] = ' CAST('.$ele.' AS NVARCHAR(255)) ';
  1062. }
  1063. $s = implode(' + ', $arr);
  1064. if ($s === '') {
  1065. return " '' ";
  1066. }
  1067. return " $s ";
  1068. }
  1069. public function sql_concat_join($separator = "' '", $elements = array ()) {
  1070. for ($n = count($elements) - 1; $n > 0; $n--) {
  1071. array_splice($elements, $n, 0, $separator);
  1072. }
  1073. $s = implode(' + ', $elements);
  1074. if ($s === '') {
  1075. return " '' ";
  1076. }
  1077. return " $s ";
  1078. }
  1079. public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
  1080. if ($textfield) {
  1081. return ' ('.$this->sql_compare_text($fieldname)." = '') ";
  1082. } else {
  1083. return " ($fieldname = '') ";
  1084. }
  1085. }
  1086. /**
  1087. * Returns the SQL text to be used to calculate the length in characters of one expression.
  1088. * @param string fieldname or expression to calculate its length in characters.
  1089. * @return string the piece of SQL code to be used in the statement.
  1090. */
  1091. public function sql_length($fieldname) {
  1092. return ' LEN('.$fieldname.')';
  1093. }
  1094. public function sql_order_by_text($fieldname, $numchars = 32) {
  1095. return ' CONVERT(varchar, '.$fieldname.', '.$numchars.')';
  1096. }
  1097. /**
  1098. * Returns the SQL for returning searching one string for the location of another.
  1099. */
  1100. public function sql_position($needle, $haystack) {
  1101. return "CHARINDEX(($needle), ($haystack))";
  1102. }
  1103. /**
  1104. * Returns the proper substr() SQL text used to extract substrings from DB
  1105. * NOTE: this was originally returning only function name
  1106. *
  1107. * @param string $expr some string field, no aggregates
  1108. * @param mixed $start integer or expression evaluating to int
  1109. * @param mixed $length optional integer or expression evaluating to int
  1110. * @return string sql fragment
  1111. */
  1112. public function sql_substr($expr, $start, $length = false) {
  1113. if (count(func_get_args()) < 2) {
  1114. throw new coding_exception('moodle_database::sql_substr() requires at least two parameters',
  1115. 'Originally this function was only returning name of SQL substring function, it now requires all parameters.');
  1116. }
  1117. if ($length === false) {
  1118. return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))";
  1119. } else {
  1120. return "SUBSTRING($expr, $start, $length)";
  1121. }
  1122. }
  1123. public function session_lock_supported() {
  1124. return true;
  1125. }
  1126. /**
  1127. * Obtain session lock
  1128. * @param int $rowid id of the row with session record
  1129. * @param int $timeout max allowed time to wait for the lock in seconds
  1130. * @return void
  1131. */
  1132. public function get_session_lock($rowid, $timeout) {
  1133. if (!$this->session_lock_supported()) {
  1134. return;
  1135. }
  1136. parent::get_session_lock($rowid, $timeout);
  1137. $timeoutmilli = $timeout * 1000;
  1138. $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
  1139. // While this may work using proper {call sp_...} calls + binding +
  1140. // executing + consuming recordsets, the solution used for the mssql
  1141. // driver is working perfectly, so 100% mimic-ing that code.
  1142. // $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session', $timeoutmilli";
  1143. $sql = "BEGIN
  1144. DECLARE @result INT
  1145. EXECUTE @result = sp_getapplock @Resource='$fullname',
  1146. @LockMode='Exclusive',
  1147. @LockOwner='Session',
  1148. @LockTimeout='$timeoutmilli'
  1149. SELECT @result
  1150. END";
  1151. $this->que