Merge branch 'MDL-38766/MOODLE_23_STABLE' of github.com:kemitix/moodle into MOODLE_23...
[moodle.git] / lib / dml / mssql_native_moodle_database.php
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 3 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/>.
17 /**
18  * Native mssql class representing moodle database interface.
19  *
20  * @package    core_dml
21  * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once(__DIR__.'/moodle_database.php');
28 require_once(__DIR__.'/mssql_native_moodle_recordset.php');
29 require_once(__DIR__.'/mssql_native_moodle_temptables.php');
31 /**
32  * Native mssql class representing moodle database interface.
33  *
34  * @package    core_dml
35  * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class mssql_native_moodle_database extends moodle_database {
40     protected $mssql     = null;
41     protected $last_error_reporting; // To handle mssql driver default verbosity
42     protected $collation;  // current DB collation cache
44     /**
45      * Detects if all needed PHP stuff installed.
46      * Note: can be used before connect()
47      * @return mixed true if ok, string if something
48      */
49     public function driver_installed() {
50         if (!function_exists('mssql_connect')) {
51             return get_string('mssqlextensionisnotpresentinphp', 'install');
52         }
53         return true;
54     }
56     /**
57      * Returns database family type - describes SQL dialect
58      * Note: can be used before connect()
59      * @return string db family name (mysql, postgres, mssql, oracle, etc.)
60      */
61     public function get_dbfamily() {
62         return 'mssql';
63     }
65     /**
66      * Returns more specific database driver type
67      * Note: can be used before connect()
68      * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
69      */
70     protected function get_dbtype() {
71         return 'mssql';
72     }
74     /**
75      * Returns general database library name
76      * Note: can be used before connect()
77      * @return string db type pdo, native
78      */
79     protected function get_dblibrary() {
80         return 'native';
81     }
83     /**
84      * Returns localised database type name
85      * Note: can be used before connect()
86      * @return string
87      */
88     public function get_name() {
89         return get_string('nativemssql', 'install');
90     }
92     /**
93      * Returns localised database configuration help.
94      * Note: can be used before connect()
95      * @return string
96      */
97     public function get_configuration_help() {
98         return get_string('nativemssqlhelp', 'install');
99     }
101     /**
102      * Returns localised database description
103      * Note: can be used before connect()
104      * @return string
105      */
106     public function get_configuration_hints() {
107         $str = get_string('databasesettingssub_mssql', 'install');
108         $str .= "<p style='text-align:right'><a href=\"javascript:void(0)\" ";
109         $str .= "onclick=\"return window.open('http://docs.moodle.org/en/Installing_MSSQL_for_PHP')\"";
110         $str .= ">";
111         $str .= '<img src="pix/docs.gif' . '" alt="Docs" class="iconhelp" />';
112         $str .= get_string('moodledocslink', 'install') . '</a></p>';
113         return $str;
114     }
116     /**
117      * Connect to db
118      * Must be called before other methods.
119      * @param string $dbhost The database host.
120      * @param string $dbuser The database username.
121      * @param string $dbpass The database username's password.
122      * @param string $dbname The name of the database being connected to.
123      * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
124      * @param array $dboptions driver specific options
125      * @return bool true
126      * @throws dml_connection_exception if error
127      */
128     public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
129         if ($prefix == '' and !$this->external) {
130             //Enforce prefixes for everybody but mysql
131             throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
132         }
134         $driverstatus = $this->driver_installed();
136         if ($driverstatus !== true) {
137             throw new dml_exception('dbdriverproblem', $driverstatus);
138         }
140         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
142         $dbhost = $this->dbhost;
143         if (isset($dboptions['dbport'])) {
144             if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) {
145                 $dbhost .= ','.$dboptions['dbport'];
146             } else {
147                 $dbhost .= ':'.$dboptions['dbport'];
148             }
149         }
150         ob_start();
151         if (!empty($this->dboptions['dbpersist'])) { // persistent connection
152             $this->mssql = mssql_pconnect($dbhost, $this->dbuser, $this->dbpass, true);
153         } else {
154             $this->mssql = mssql_connect($dbhost, $this->dbuser, $this->dbpass, true);
155         }
156         $dberr = ob_get_contents();
157         ob_end_clean();
159         if ($this->mssql === false) {
160             $this->mssql = null;
161             throw new dml_connection_exception($dberr);
162         }
164         // already connected, select database and set some env. variables
165         $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX);
166         $result = mssql_select_db($this->dbname, $this->mssql);
167         $this->query_end($result);
169         // No need to set charset. It's UTF8, with transparent conversions
170         // back and forth performed both by FreeTDS or ODBTP
172         // Allow quoted identifiers
173         $sql = "SET QUOTED_IDENTIFIER ON";
174         $this->query_start($sql, null, SQL_QUERY_AUX);
175         $result = mssql_query($sql, $this->mssql);
176         $this->query_end($result);
178         $this->free_result($result);
180         // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL
181         // instead of equal(=) and distinct(<>) symbols
182         $sql = "SET ANSI_NULLS ON";
183         $this->query_start($sql, null, SQL_QUERY_AUX);
184         $result = mssql_query($sql, $this->mssql);
185         $this->query_end($result);
187         $this->free_result($result);
189         // Force ANSI warnings so arithmetic/string overflows will be
190         // returning error instead of transparently truncating data
191         $sql = "SET ANSI_WARNINGS ON";
192         $this->query_start($sql, null, SQL_QUERY_AUX);
193         $result = mssql_query($sql, $this->mssql);
194         $this->query_end($result);
196         // Concatenating null with anything MUST return NULL
197         $sql = "SET CONCAT_NULL_YIELDS_NULL  ON";
198         $this->query_start($sql, null, SQL_QUERY_AUX);
199         $result = mssql_query($sql, $this->mssql);
200         $this->query_end($result);
202         $this->free_result($result);
204         // Set transactions isolation level to READ_COMMITTED
205         // prevents dirty reads when using transactions +
206         // is the default isolation level of MSSQL
207         // Requires database to run with READ_COMMITTED_SNAPSHOT ON
208         $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
209         $this->query_start($sql, NULL, SQL_QUERY_AUX);
210         $result = mssql_query($sql, $this->mssql);
211         $this->query_end($result);
213         $this->free_result($result);
215         // Connection stabilised and configured, going to instantiate the temptables controller
216         $this->temptables = new mssql_native_moodle_temptables($this);
218         return true;
219     }
221     /**
222      * Close database connection and release all resources
223      * and memory (especially circular memory references).
224      * Do NOT use connect() again, create a new instance if needed.
225      */
226     public function dispose() {
227         parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
228         if ($this->mssql) {
229             mssql_close($this->mssql);
230             $this->mssql = null;
231         }
232     }
234     /**
235      * Called before each db query.
236      * @param string $sql
237      * @param array array of parameters
238      * @param int $type type of query
239      * @param mixed $extrainfo driver specific extra information
240      * @return void
241      */
242     protected function query_start($sql, array $params=null, $type, $extrainfo=null) {
243         parent::query_start($sql, $params, $type, $extrainfo);
244         // mssql driver tends to send debug to output, we do not need that ;-)
245         $this->last_error_reporting = error_reporting(0);
246     }
248     /**
249      * Called immediately after each db query.
250      * @param mixed db specific result
251      * @return void
252      */
253     protected function query_end($result) {
254         // reset original debug level
255         error_reporting($this->last_error_reporting);
256         parent::query_end($result);
257     }
259     /**
260      * Returns database server info array
261      * @return array Array containing 'description' and 'version' info
262      */
263     public function get_server_info() {
264         static $info;
265         if (!$info) {
266             $info = array();
267             $sql = 'sp_server_info 2';
268             $this->query_start($sql, null, SQL_QUERY_AUX);
269             $result = mssql_query($sql, $this->mssql);
270             $this->query_end($result);
271             $row = mssql_fetch_row($result);
272             $info['description'] = $row[2];
273             $this->free_result($result);
275             $sql = 'sp_server_info 500';
276             $this->query_start($sql, null, SQL_QUERY_AUX);
277             $result = mssql_query($sql, $this->mssql);
278             $this->query_end($result);
279             $row = mssql_fetch_row($result);
280             $info['version'] = $row[2];
281             $this->free_result($result);
282         }
283         return $info;
284     }
286     /**
287      * Converts short table name {tablename} to real table name
288      * supporting temp tables (#) if detected
289      *
290      * @param string sql
291      * @return string sql
292      */
293     protected function fix_table_names($sql) {
294         if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
295             foreach($matches[0] as $key=>$match) {
296                 $name = $matches[1][$key];
297                 if ($this->temptables->is_temptable($name)) {
298                     $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
299                 } else {
300                     $sql = str_replace($match, $this->prefix.$name, $sql);
301                 }
302             }
303         }
304         return $sql;
305     }
307     /**
308      * Returns supported query parameter types
309      * @return int bitmask of accepted SQL_PARAMS_*
310      */
311     protected function allowed_param_types() {
312         return SQL_PARAMS_QM; // Not really, but emulated, see emulate_bound_params()
313     }
315     /**
316      * Returns last error reported by database engine.
317      * @return string error message
318      */
319     public function get_last_error() {
320         return mssql_get_last_message();
321     }
323     /**
324      * Return tables in database WITHOUT current prefix
325      * @param bool $usecache if true, returns list of cached tables.
326      * @return array of table names in lowercase and without prefix
327      */
328     public function get_tables($usecache=true) {
329         if ($usecache and $this->tables !== null) {
330             return $this->tables;
331         }
332         $this->tables = array();
333         $sql = "SELECT table_name
334                   FROM INFORMATION_SCHEMA.TABLES
335                  WHERE table_name LIKE '$this->prefix%'
336                    AND table_type = 'BASE TABLE'";
337         $this->query_start($sql, null, SQL_QUERY_AUX);
338         $result = mssql_query($sql, $this->mssql);
339         $this->query_end($result);
341         if ($result) {
342             while ($row = mssql_fetch_row($result)) {
343                 $tablename = reset($row);
344                 if ($this->prefix !== '') {
345                     if (strpos($tablename, $this->prefix) !== 0) {
346                         continue;
347                     }
348                     $tablename = substr($tablename, strlen($this->prefix));
349                 }
350                 $this->tables[$tablename] = $tablename;
351             }
352             $this->free_result($result);
353         }
355         // Add the currently available temptables
356         $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
357         return $this->tables;
358     }
360     /**
361      * Return table indexes - everything lowercased.
362      * @param string $table The table we want to get indexes from.
363      * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
364      */
365     public function get_indexes($table) {
366         $indexes = array();
367         $tablename = $this->prefix.$table;
369         // Indexes aren't covered by information_schema metatables, so we need to
370         // go to sys ones. Skipping primary key indexes on purpose.
371         $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name
372                   FROM sys.indexes i
373                   JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
374                   JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
375                   JOIN sys.tables t ON i.object_id = t.object_id
376                  WHERE t.name = '$tablename'
377                    AND i.is_primary_key = 0
378               ORDER BY i.name, i.index_id, ic.index_column_id";
380         $this->query_start($sql, null, SQL_QUERY_AUX);
381         $result = mssql_query($sql, $this->mssql);
382         $this->query_end($result);
384         if ($result) {
385             $lastindex = '';
386             $unique = false;
387             $columns = array();
388             while ($row = mssql_fetch_assoc($result)) {
389                 if ($lastindex and $lastindex != $row['index_name']) { // Save lastindex to $indexes and reset info
390                     $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
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('unique' => $unique, 'columns' => $columns);
400             }
401             $this->free_result($result);
402         }
403         return $indexes;
404     }
406     /**
407      * Returns datailed information about columns in table. This information is cached internally.
408      * @param string $table name
409      * @param bool $usecache
410      * @return array array of database_column_info objects indexed with column names
411      */
412     public function get_columns($table, $usecache=true) {
413         if ($usecache and isset($this->columns[$table])) {
414             return $this->columns[$table];
415         }
417         $this->columns[$table] = array();
419         if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema
420             $sql = "SELECT column_name AS name,
421                            data_type AS type,
422                            numeric_precision AS max_length,
423                            character_maximum_length AS char_max_length,
424                            numeric_scale AS scale,
425                            is_nullable AS is_nullable,
426                            columnproperty(object_id(quotename(table_schema) + '.' +
427                                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) + '.' +
440                                quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
441                            column_default AS default_value
442                       FROM tempdb.INFORMATION_SCHEMA.COLUMNS
443                       JOIN tempdb..sysobjects ON name = table_name
444                      WHERE id = object_id('tempdb..{" . $table . "}')
445                   ORDER BY ordinal_position";
446         }
448         list($sql, $params, $type) = $this->fix_sql_params($sql, null);
450         $this->query_start($sql, null, SQL_QUERY_AUX);
451         $result = mssql_query($sql, $this->mssql);
452         $this->query_end($result);
454         if (!$result) {
455             return array();
456         }
458         while ($rawcolumn = mssql_fetch_assoc($result)) {
460             $rawcolumn = (object)$rawcolumn;
462             $info = new stdClass();
463             $info->name = $rawcolumn->name;
464             $info->type = $rawcolumn->type;
465             $info->meta_type = $this->mssqltype2moodletype($info->type);
467             // Prepare auto_increment info
468             $info->auto_increment = $rawcolumn->auto_increment ? true : false;
470             // Define type for auto_increment columns
471             $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type;
473             // id columns being auto_incremnt are PK by definition
474             $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
476             // Put correct length for character and LOB types
477             $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
478             $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
480             // Scale
481             $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false;
483             // Prepare not_null info
484             $info->not_null = $rawcolumn->is_nullable == 'NO'  ? true : false;
486             // Process defaults
487             $info->has_default = !empty($rawcolumn->default_value);
488             if ($rawcolumn->default_value === NULL) {
489                 $info->default_value = NULL;
490             } else {
491                 $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value);
492             }
494             // Process binary
495             $info->binary = $info->meta_type == 'B' ? true : false;
497             $this->columns[$table][$info->name] = new database_column_info($info);
498         }
499         $this->free_result($result);
501         return $this->columns[$table];
502     }
504     /**
505      * Normalise values based on varying RDBMS's dependencies (booleans, LOBs...)
506      *
507      * @param database_column_info $column column metadata corresponding with the value we are going to normalise
508      * @param mixed $value value we are going to normalise
509      * @return mixed the normalised value
510      */
511     protected function normalise_value($column, $value) {
512         $this->detect_objects($value);
514         if (is_bool($value)) { // Always, convert boolean to int
515             $value = (int)$value;
516         } // And continue processing because text columns with numeric info need special handling below
518         if ($column->meta_type == 'B') {   // BLOBs need to be properly "packed", but can be inserted directly if so.
519             if (!is_null($value)) {               // If value not null, unpack it to unquoted hexadecimal byte-string format
520                 $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it
521             }                                     // easily and "bind" the param ok.
523         } else if ($column->meta_type == 'X') {             // MSSQL doesn't cast from int to text, so if text column
524             if (is_numeric($value)) {                       // and is numeric value then cast to string
525                 $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how
526             }                                               // to "bind" the param ok, avoiding reverse conversion to number
528         } else if ($value === '') {
529             if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
530                 $value = 0; // prevent '' problems in numeric fields
531             }
532         }
533         return $value;
534     }
536     /**
537      * Selectively call mssql_free_result(), avoiding some warnings without using the horrible @
538      *
539      * @param mssql_resource $resource resource to be freed if possible
540      */
541     private function free_result($resource) {
542         if (!is_bool($resource)) { // true/false resources cannot be freed
543             mssql_free_result($resource);
544         }
545     }
547     /**
548      * Provides mapping between mssql native data types and moodle_database - database_column_info - ones)
549      *
550      * @param string $mssql_type native mssql data type
551      * @return string 1-char database_column_info data type
552      */
553     private function mssqltype2moodletype($mssql_type) {
554         $type = null;
555         switch (strtoupper($mssql_type)) {
556             case 'BIT':
557                 $type = 'L';
558                 break;
559             case 'INT':
560             case 'SMALLINT':
561             case 'INTEGER':
562             case 'BIGINT':
563                 $type = 'I';
564                 break;
565             case 'DECIMAL':
566             case 'REAL':
567             case 'FLOAT':
568                 $type = 'N';
569                 break;
570             case 'VARCHAR':
571             case 'NVARCHAR':
572                 $type = 'C';
573                 break;
574             case 'TEXT':
575             case 'NTEXT':
576             case 'VARCHAR(MAX)':
577             case 'NVARCHAR(MAX)':
578                 $type = 'X';
579                 break;
580             case 'IMAGE':
581             case 'VARBINARY(MAX)':
582                 $type = 'B';
583                 break;
584             case 'DATETIME':
585                 $type = 'D';
586                 break;
587         }
588         if (!$type) {
589             throw new dml_exception('invalidmssqlnativetype', $mssql_type);
590         }
591         return $type;
592     }
594     /**
595      * Do NOT use in code, to be used by database_manager only!
596      * @param string $sql query
597      * @return bool true
598      * @throws dml_exception A DML specific exception is thrown for any errors.
599      */
600     public function change_database_structure($sql) {
601         $this->reset_caches();
603         $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
604         $result = mssql_query($sql, $this->mssql);
605         $this->query_end($result);
607         return true;
608     }
610     /**
611      * Very ugly hack which emulates bound parameters in queries
612      * because the mssql driver doesn't support placeholders natively at all
613      */
614     protected function emulate_bound_params($sql, array $params=null) {
615         if (empty($params)) {
616             return $sql;
617         }
618         // ok, we have verified sql statement with ? and correct number of params
619         $parts = explode('?', $sql);
620         $return = array_shift($parts);
621         foreach ($params as $param) {
622             if (is_bool($param)) {
623                 $return .= (int)$param;
625             } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially
626                 $return .= '0x' . $param['hex'];
628             } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not*
629                 $return .= "N'{$param['numstr']}'";                   // be converted back to number params, but bound as strings
631             } else if (is_null($param)) {
632                 $return .= 'NULL';
634             } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646
635                 $return .= "'".$param."'"; //fix for MDL-24863 to prevent auto-cast to int.
637             } else if (is_float($param)) {
638                 $return .= $param;
640             } else {
641                 $param = str_replace("'", "''", $param);
642                 $return .= "N'$param'";
643             }
645             $return .= array_shift($parts);
646         }
647         return $return;
648     }
650     /**
651      * Execute general sql query. Should be used only when no other method suitable.
652      * Do NOT use this to make changes in db structure, use database_manager methods instead!
653      * @param string $sql query
654      * @param array $params query parameters
655      * @return bool true
656      * @throws dml_exception A DML specific exception is thrown for any errors.
657      */
658     public function execute($sql, array $params=null) {
660         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
661         $rawsql = $this->emulate_bound_params($sql, $params);
663         if (strpos($sql, ';') !== false) {
664             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
665         }
667         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
668         $result = mssql_query($rawsql, $this->mssql);
669         $this->query_end($result);
670         $this->free_result($result);
672         return true;
673     }
675     /**
676      * Get a number of records as a moodle_recordset using a SQL statement.
677      *
678      * Since this method is a little less readable, use of it should be restricted to
679      * code where it's possible there might be large datasets being returned.  For known
680      * small datasets use get_records_sql - it leads to simpler code.
681      *
682      * The return type is like:
683      * @see function get_recordset.
684      *
685      * @param string $sql the SQL select query to execute.
686      * @param array $params array of sql parameters
687      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
688      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
689      * @return moodle_recordset instance
690      * @throws dml_exception A DML specific exception is thrown for any errors.
691      */
692     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
693         $limitfrom = (int)$limitfrom;
694         $limitnum  = (int)$limitnum;
695         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
696         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
697         if ($limitfrom or $limitnum) {
698             if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later)
699                 $fetch = $limitfrom + $limitnum;
700                 if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow
701                     $fetch = PHP_INT_MAX;
702                 }
703                 $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
704                                     "\\1SELECT\\2 TOP $fetch", $sql);
705             }
706         }
708         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
709         $rawsql = $this->emulate_bound_params($sql, $params);
711         $this->query_start($sql, $params, SQL_QUERY_SELECT);
712         $result = mssql_query($rawsql, $this->mssql);
713         $this->query_end($result);
715         if ($limitfrom) { // Skip $limitfrom records
716             if (!@mssql_data_seek($result, $limitfrom)) {
717                 // Nothing, most probably seek past the end.
718                 mssql_free_result($result);
719                 $result = null;
720             }
721         }
723         return $this->create_recordset($result);
724     }
726     protected function create_recordset($result) {
727         return new mssql_native_moodle_recordset($result);
728     }
730     /**
731      * Get a number of records as an array of objects using a SQL statement.
732      *
733      * Return value is like:
734      * @see function get_records.
735      *
736      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
737      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
738      *   returned array.
739      * @param array $params array of sql parameters
740      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
741      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
742      * @return array of objects, or empty array if no records were found
743      * @throws dml_exception A DML specific exception is thrown for any errors.
744      */
745     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
747         $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
749         $results = array();
751         foreach ($rs as $row) {
752             $id = reset($row);
753             if (isset($results[$id])) {
754                 $colname = key($row);
755                 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);
756             }
757             $results[$id] = $row;
758         }
759         $rs->close();
761         return $results;
762     }
764     /**
765      * Selects records and return values (first field) as an array using a SQL statement.
766      *
767      * @param string $sql The SQL query
768      * @param array $params array of sql parameters
769      * @return array of values
770      * @throws dml_exception A DML specific exception is thrown for any errors.
771      */
772     public function get_fieldset_sql($sql, array $params=null) {
774         $rs = $this->get_recordset_sql($sql, $params);
776         $results = array();
778         foreach ($rs as $row) {
779             $results[] = reset($row);
780         }
781         $rs->close();
783         return $results;
784     }
786     /**
787      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
788      * @param string $table name
789      * @param mixed $params data record as object or array
790      * @param bool $returnit return it of inserted record
791      * @param bool $bulk true means repeated inserts expected
792      * @param bool $customsequence true if 'id' included in $params, disables $returnid
793      * @return bool|int true or new id
794      * @throws dml_exception A DML specific exception is thrown for any errors.
795      */
796     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
797         if (!is_array($params)) {
798             $params = (array)$params;
799         }
801         $returning = "";
802         $isidentity = false;
804         if ($customsequence) {
805             if (!isset($params['id'])) {
806                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
807             }
808             $returnid = false;
810             $columns = $this->get_columns($table);
811             if (isset($columns['id']) and $columns['id']->auto_increment) {
812                 $isidentity = true;
813             }
815             // Disable IDENTITY column before inserting record with id, only if the
816             // column is identity, from meta information.
817             if ($isidentity) {
818                 $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
819                 list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
820                 $this->query_start($sql, null, SQL_QUERY_AUX);
821                 $result = mssql_query($sql, $this->mssql);
822                 $this->query_end($result);
823                 $this->free_result($result);
824             }
826         } else {
827             unset($params['id']);
828             if ($returnid) {
829                 $returning = "OUTPUT inserted.id";
830             }
831         }
833         if (empty($params)) {
834             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
835         }
837         $fields = implode(',', array_keys($params));
838         $qms    = array_fill(0, count($params), '?');
839         $qms    = implode(',', $qms);
841         $sql = "INSERT INTO {" . $table . "} ($fields) $returning VALUES ($qms)";
843         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
844         $rawsql = $this->emulate_bound_params($sql, $params);
846         $this->query_start($sql, $params, SQL_QUERY_INSERT);
847         $result = mssql_query($rawsql, $this->mssql);
848         // Expected results are:
849         //     - true: insert ok and there isn't returned information.
850         //     - false: insert failed and there isn't returned information.
851         //     - resource: insert executed, need to look for returned (output)
852         //           values to know if the insert was ok or no. Posible values
853         //           are false = failed, integer = insert ok, id returned.
854         $end = false;
855         if (is_bool($result)) {
856             $end = $result;
857         } else if (is_resource($result)) {
858             $end = mssql_result($result, 0, 0); // Fetch 1st column from 1st row.
859         }
860         $this->query_end($end); // End the query with the calculated $end.
862         if ($returning !== "") {
863             $params['id'] = $end;
864         }
865         $this->free_result($result);
867         if ($customsequence) {
868             // Enable IDENTITY column after inserting record with id, only if the
869             // column is identity, from meta information.
870             if ($isidentity) {
871                 $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
872                 list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
873                 $this->query_start($sql, null, SQL_QUERY_AUX);
874                 $result = mssql_query($sql, $this->mssql);
875                 $this->query_end($result);
876                 $this->free_result($result);
877             }
878         }
880         if (!$returnid) {
881             return true;
882         }
884         return (int)$params['id'];
885     }
887     /**
888      * Insert a record into a table and return the "id" field if required.
889      *
890      * Some conversions and safety checks are carried out. Lobs are supported.
891      * If the return ID isn't required, then this just reports success as true/false.
892      * $data is an object containing needed data
893      * @param string $table The database table to be inserted into
894      * @param object $data A data object with values for one or more fields in the record
895      * @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.
896      * @return bool|int true or new id
897      * @throws dml_exception A DML specific exception is thrown for any errors.
898      */
899     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
900         $dataobject = (array)$dataobject;
902         $columns = $this->get_columns($table);
903         $cleaned = array();
905         foreach ($dataobject as $field => $value) {
906             if ($field === 'id') {
907                 continue;
908             }
909             if (!isset($columns[$field])) {
910                 continue;
911             }
912             $column = $columns[$field];
913             $cleaned[$field] = $this->normalise_value($column, $value);
914         }
916         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
917     }
919     /**
920      * Import a record into a table, id field is required.
921      * Safety checks are NOT carried out. Lobs are supported.
922      *
923      * @param string $table name of database table to be inserted into
924      * @param object $dataobject A data object with values for one or more fields in the record
925      * @return bool true
926      * @throws dml_exception A DML specific exception is thrown for any errors.
927      */
928     public function import_record($table, $dataobject) {
929         $dataobject = (array)$dataobject;
931         $columns = $this->get_columns($table);
932         $cleaned = array();
934         foreach ($dataobject as $field => $value) {
935             if (!isset($columns[$field])) {
936                 continue;
937             }
938             $column = $columns[$field];
939             $cleaned[$field] = $this->normalise_value($column, $value);
940         }
942         $this->insert_record_raw($table, $cleaned, false, false, true);
944         return true;
945     }
947     /**
948      * Update record in database, as fast as possible, no safety checks, lobs not supported.
949      * @param string $table name
950      * @param mixed $params data record as object or array
951      * @param bool true means repeated updates expected
952      * @return bool true
953      * @throws dml_exception A DML specific exception is thrown for any errors.
954      */
955     public function update_record_raw($table, $params, $bulk=false) {
956         $params = (array)$params;
958         if (!isset($params['id'])) {
959             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
960         }
961         $id = $params['id'];
962         unset($params['id']);
964         if (empty($params)) {
965             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
966         }
968         $sets = array();
969         foreach ($params as $field=>$value) {
970             $sets[] = "$field = ?";
971         }
973         $params[] = $id; // last ? in WHERE condition
975         $sets = implode(',', $sets);
976         $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?";
978         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
979         $rawsql = $this->emulate_bound_params($sql, $params);
981         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
982         $result = mssql_query($rawsql, $this->mssql);
983         $this->query_end($result);
985         $this->free_result($result);
986         return true;
987     }
989     /**
990      * Update a record in a table
991      *
992      * $dataobject is an object containing needed data
993      * Relies on $dataobject having a variable "id" to
994      * specify the record to update
995      *
996      * @param string $table The database table to be checked against.
997      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
998      * @param bool true means repeated updates expected
999      * @return bool true
1000      * @throws dml_exception A DML specific exception is thrown for any errors.
1001      */
1002     public function update_record($table, $dataobject, $bulk=false) {
1003         $dataobject = (array)$dataobject;
1005         $columns = $this->get_columns($table);
1006         $cleaned = array();
1008         foreach ($dataobject as $field => $value) {
1009             if (!isset($columns[$field])) {
1010                 continue;
1011             }
1012             $column = $columns[$field];
1013             $cleaned[$field] = $this->normalise_value($column, $value);
1014         }
1016         return $this->update_record_raw($table, $cleaned, $bulk);
1017     }
1019     /**
1020      * Set a single field in every table record which match a particular WHERE clause.
1021      *
1022      * @param string $table The database table to be checked against.
1023      * @param string $newfield the field to set.
1024      * @param string $newvalue the value to set the field to.
1025      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1026      * @param array $params array of sql parameters
1027      * @return bool true
1028      * @throws dml_exception A DML specific exception is thrown for any errors.
1029      */
1030     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1032         if ($select) {
1033             $select = "WHERE $select";
1034         }
1035         if (is_null($params)) {
1036             $params = array();
1037         }
1039         // convert params to ? types
1040         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1042         // Get column metadata
1043         $columns = $this->get_columns($table);
1044         $column = $columns[$newfield];
1046         $newvalue = $this->normalise_value($column, $newvalue);
1048         if (is_null($newvalue)) {
1049             $newfield = "$newfield = NULL";
1050         } else {
1051             $newfield = "$newfield = ?";
1052             array_unshift($params, $newvalue);
1053         }
1054         $sql = "UPDATE {" . $table . "} SET $newfield $select";
1056         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1057         $rawsql = $this->emulate_bound_params($sql, $params);
1059         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1060         $result = mssql_query($rawsql, $this->mssql);
1061         $this->query_end($result);
1063         $this->free_result($result);
1065         return true;
1066     }
1068     /**
1069      * Delete one or more records from a table which match a particular WHERE clause.
1070      *
1071      * @param string $table The database table to be checked against.
1072      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1073      * @param array $params array of sql parameters
1074      * @return bool true
1075      * @throws dml_exception A DML specific exception is thrown for any errors.
1076      */
1077     public function delete_records_select($table, $select, array $params=null) {
1079         if ($select) {
1080             $select = "WHERE $select";
1081         }
1083         $sql = "DELETE FROM {" . $table . "} $select";
1085         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1086         $rawsql = $this->emulate_bound_params($sql, $params);
1088         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1089         $result = mssql_query($rawsql, $this->mssql);
1090         $this->query_end($result);
1092         $this->free_result($result);
1094         return true;
1095     }
1097     public function sql_cast_char2int($fieldname, $text=false) {
1098         if (!$text) {
1099             return ' CAST(' . $fieldname . ' AS INT) ';
1100         } else {
1101             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1102         }
1103     }
1105     public function sql_cast_char2real($fieldname, $text=false) {
1106         if (!$text) {
1107             return ' CAST(' . $fieldname . ' AS REAL) ';
1108         } else {
1109             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) ';
1110         }
1111     }
1113     public function sql_ceil($fieldname) {
1114         return ' CEILING(' . $fieldname . ')';
1115     }
1118     protected function get_collation() {
1119         if (isset($this->collation)) {
1120             return $this->collation;
1121         }
1122         if (!empty($this->dboptions['dbcollation'])) {
1123             // perf speedup
1124             $this->collation = $this->dboptions['dbcollation'];
1125             return $this->collation;
1126         }
1128         // make some default
1129         $this->collation = 'Latin1_General_CI_AI';
1131         $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation";
1132         $this->query_start($sql, null, SQL_QUERY_AUX);
1133         $result = mssql_query($sql, $this->mssql);
1134         $this->query_end($result);
1136         if ($result) {
1137             if ($rawcolumn = mssql_fetch_assoc($result)) {
1138                 $this->collation = reset($rawcolumn);
1139             }
1140             $this->free_result($result);
1141         }
1143         return $this->collation;
1144     }
1146     /**
1147      * Returns 'LIKE' part of a query.
1148      *
1149      * @param string $fieldname usually name of the table column
1150      * @param string $param usually bound query parameter (?, :named)
1151      * @param bool $casesensitive use case sensitive search
1152      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1153      * @param bool $notlike true means "NOT LIKE"
1154      * @param string $escapechar escape char for '%' and '_'
1155      * @return string SQL code fragment
1156      */
1157     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1158         if (strpos($param, '%') !== false) {
1159             debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1160         }
1162         $collation = $this->get_collation();
1164         if ($casesensitive) {
1165             $collation = str_replace('_CI', '_CS', $collation);
1166         } else {
1167             $collation = str_replace('_CS', '_CI', $collation);
1168         }
1169         if ($accentsensitive) {
1170             $collation = str_replace('_AI', '_AS', $collation);
1171         } else {
1172             $collation = str_replace('_AS', '_AI', $collation);
1173         }
1175         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1177         return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'";
1178     }
1180     public function sql_concat() {
1181         $arr = func_get_args();
1182         foreach ($arr as $key => $ele) {
1183             $arr[$key] = ' CAST(' . $ele . ' AS NVARCHAR(255)) ';
1184         }
1185         $s = implode(' + ', $arr);
1186         if ($s === '') {
1187             return " '' ";
1188         }
1189         return " $s ";
1190     }
1192     public function sql_concat_join($separator="' '", $elements=array()) {
1193         for ($n=count($elements)-1; $n > 0 ; $n--) {
1194             array_splice($elements, $n, 0, $separator);
1195         }
1196         $s = implode(' + ', $elements);
1197         if ($s === '') {
1198             return " '' ";
1199         }
1200         return " $s ";
1201     }
1203    public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1204         if ($textfield) {
1205             return ' (' . $this->sql_compare_text($fieldname) . " = '') ";
1206         } else {
1207             return " ($fieldname = '') ";
1208         }
1209     }
1211    /**
1212      * Returns the SQL text to be used to calculate the length in characters of one expression.
1213      * @param string fieldname or expression to calculate its length in characters.
1214      * @return string the piece of SQL code to be used in the statement.
1215      */
1216     public function sql_length($fieldname) {
1217         return ' LEN(' . $fieldname . ')';
1218     }
1220     public function sql_order_by_text($fieldname, $numchars=32) {
1221         return ' CONVERT(varchar, ' . $fieldname . ', ' . $numchars . ')';
1222     }
1224    /**
1225      * Returns the SQL for returning searching one string for the location of another.
1226      */
1227     public function sql_position($needle, $haystack) {
1228         return "CHARINDEX(($needle), ($haystack))";
1229     }
1231     /**
1232      * Returns the proper substr() SQL text used to extract substrings from DB
1233      * NOTE: this was originally returning only function name
1234      *
1235      * @param string $expr some string field, no aggregates
1236      * @param mixed $start integer or expression evaluating to int
1237      * @param mixed $length optional integer or expression evaluating to int
1238      * @return string sql fragment
1239      */
1240     public function sql_substr($expr, $start, $length=false) {
1241         if (count(func_get_args()) < 2) {
1242             throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa
1243 s only returning name of SQL substring function, it now requires all parameters.');
1244         }
1245         if ($length === false) {
1246             return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))";
1247         } else {
1248             return "SUBSTRING($expr, $start, $length)";
1249         }
1250     }
1252     public function session_lock_supported() {
1253         return true;
1254     }
1256     /**
1257      * Obtain session lock
1258      * @param int $rowid id of the row with session record
1259      * @param int $timeout max allowed time to wait for the lock in seconds
1260      * @return bool success
1261      */
1262     public function get_session_lock($rowid, $timeout) {
1263         if (!$this->session_lock_supported()) {
1264             return;
1265         }
1266         parent::get_session_lock($rowid, $timeout);
1268         $timeoutmilli = $timeout * 1000;
1270         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1271         // There is one bug in PHP/freetds (both reproducible with mssql_query()
1272         // and its mssql_init()/mssql_bind()/mssql_execute() alternative) for
1273         // stored procedures, causing scalar results of the execution
1274         // to be cast to boolean (true/fals). Here there is one
1275         // workaround that forces the return of one recordset resource.
1276         // $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session',  $timeoutmilli";
1277         $sql = "BEGIN
1278                     DECLARE @result INT
1279                     EXECUTE @result = sp_getapplock @Resource='$fullname',
1280                                                     @LockMode='Exclusive',
1281                                                     @LockOwner='Session',
1282                                                     @LockTimeout='$timeoutmilli'
1283                     SELECT @result
1284                 END";
1285         $this->query_start($sql, null, SQL_QUERY_AUX);
1286         $result = mssql_query($sql, $this->mssql);
1287         $this->query_end($result);
1289         if ($result) {
1290             $row = mssql_fetch_row($result);
1291             if ($row[0] < 0) {
1292                 throw new dml_sessionwait_exception();
1293             }
1294         }
1296         $this->free_result($result);
1297     }
1299     public function release_session_lock($rowid) {
1300         if (!$this->session_lock_supported()) {
1301             return;
1302         }
1303         parent::release_session_lock($rowid);
1305         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1306         $sql = "sp_releaseapplock '$fullname', 'Session'";
1307         $this->query_start($sql, null, SQL_QUERY_AUX);
1308         $result = mssql_query($sql, $this->mssql);
1309         $this->query_end($result);
1311         $this->free_result($result);
1312     }
1314     /**
1315      * Driver specific start of real database transaction,
1316      * this can not be used directly in code.
1317      * @return void
1318      */
1319     protected function begin_transaction() {
1320         // requires database to run with READ_COMMITTED_SNAPSHOT ON
1321         $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation
1322         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1323         $result = mssql_query($sql, $this->mssql);
1324         $this->query_end($result);
1326         $this->free_result($result);
1327     }
1329     /**
1330      * Driver specific commit of real database transaction,
1331      * this can not be used directly in code.
1332      * @return void
1333      */
1334     protected function commit_transaction() {
1335         $sql = "COMMIT TRANSACTION";
1336         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1337         $result = mssql_query($sql, $this->mssql);
1338         $this->query_end($result);
1340         $this->free_result($result);
1341     }
1343     /**
1344      * Driver specific abort of real database transaction,
1345      * this can not be used directly in code.
1346      * @return void
1347      */
1348     protected function rollback_transaction() {
1349         $sql = "ROLLBACK TRANSACTION";
1350         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1351         $result = mssql_query($sql, $this->mssql);
1352         $this->query_end($result);
1354         $this->free_result($result);
1355     }