MDL-38766 wiki2.0: fix redirects
[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             mssql_data_seek($result, $limitfrom);
717         }
719         return $this->create_recordset($result);
720     }
722     protected function create_recordset($result) {
723         return new mssql_native_moodle_recordset($result);
724     }
726     /**
727      * Get a number of records as an array of objects using a SQL statement.
728      *
729      * Return value is like:
730      * @see function get_records.
731      *
732      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
733      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
734      *   returned array.
735      * @param array $params array of sql parameters
736      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
737      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
738      * @return array of objects, or empty array if no records were found
739      * @throws dml_exception A DML specific exception is thrown for any errors.
740      */
741     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
743         $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
745         $results = array();
747         foreach ($rs as $row) {
748             $id = reset($row);
749             if (isset($results[$id])) {
750                 $colname = key($row);
751                 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);
752             }
753             $results[$id] = $row;
754         }
755         $rs->close();
757         return $results;
758     }
760     /**
761      * Selects records and return values (first field) as an array using a SQL statement.
762      *
763      * @param string $sql The SQL query
764      * @param array $params array of sql parameters
765      * @return array of values
766      * @throws dml_exception A DML specific exception is thrown for any errors.
767      */
768     public function get_fieldset_sql($sql, array $params=null) {
770         $rs = $this->get_recordset_sql($sql, $params);
772         $results = array();
774         foreach ($rs as $row) {
775             $results[] = reset($row);
776         }
777         $rs->close();
779         return $results;
780     }
782     /**
783      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
784      * @param string $table name
785      * @param mixed $params data record as object or array
786      * @param bool $returnit return it of inserted record
787      * @param bool $bulk true means repeated inserts expected
788      * @param bool $customsequence true if 'id' included in $params, disables $returnid
789      * @return bool|int true or new id
790      * @throws dml_exception A DML specific exception is thrown for any errors.
791      */
792     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
793         if (!is_array($params)) {
794             $params = (array)$params;
795         }
797         $returning = "";
798         $isidentity = false;
800         if ($customsequence) {
801             if (!isset($params['id'])) {
802                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
803             }
804             $returnid = false;
806             $columns = $this->get_columns($table);
807             if (isset($columns['id']) and $columns['id']->auto_increment) {
808                 $isidentity = true;
809             }
811             // Disable IDENTITY column before inserting record with id, only if the
812             // column is identity, from meta information.
813             if ($isidentity) {
814                 $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
815                 list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
816                 $this->query_start($sql, null, SQL_QUERY_AUX);
817                 $result = mssql_query($sql, $this->mssql);
818                 $this->query_end($result);
819                 $this->free_result($result);
820             }
822         } else {
823             unset($params['id']);
824             if ($returnid) {
825                 $returning = "OUTPUT inserted.id";
826             }
827         }
829         if (empty($params)) {
830             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
831         }
833         $fields = implode(',', array_keys($params));
834         $qms    = array_fill(0, count($params), '?');
835         $qms    = implode(',', $qms);
837         $sql = "INSERT INTO {" . $table . "} ($fields) $returning VALUES ($qms)";
839         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
840         $rawsql = $this->emulate_bound_params($sql, $params);
842         $this->query_start($sql, $params, SQL_QUERY_INSERT);
843         $result = mssql_query($rawsql, $this->mssql);
844         // Expected results are:
845         //     - true: insert ok and there isn't returned information.
846         //     - false: insert failed and there isn't returned information.
847         //     - resource: insert executed, need to look for returned (output)
848         //           values to know if the insert was ok or no. Posible values
849         //           are false = failed, integer = insert ok, id returned.
850         $end = false;
851         if (is_bool($result)) {
852             $end = $result;
853         } else if (is_resource($result)) {
854             $end = mssql_result($result, 0, 0); // Fetch 1st column from 1st row.
855         }
856         $this->query_end($end); // End the query with the calculated $end.
858         if ($returning !== "") {
859             $params['id'] = $end;
860         }
861         $this->free_result($result);
863         if ($customsequence) {
864             // Enable IDENTITY column after inserting record with id, only if the
865             // column is identity, from meta information.
866             if ($isidentity) {
867                 $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
868                 list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
869                 $this->query_start($sql, null, SQL_QUERY_AUX);
870                 $result = mssql_query($sql, $this->mssql);
871                 $this->query_end($result);
872                 $this->free_result($result);
873             }
874         }
876         if (!$returnid) {
877             return true;
878         }
880         return (int)$params['id'];
881     }
883     /**
884      * Insert a record into a table and return the "id" field if required.
885      *
886      * Some conversions and safety checks are carried out. Lobs are supported.
887      * If the return ID isn't required, then this just reports success as true/false.
888      * $data is an object containing needed data
889      * @param string $table The database table to be inserted into
890      * @param object $data A data object with values for one or more fields in the record
891      * @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.
892      * @return bool|int true or new id
893      * @throws dml_exception A DML specific exception is thrown for any errors.
894      */
895     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
896         $dataobject = (array)$dataobject;
898         $columns = $this->get_columns($table);
899         $cleaned = array();
901         foreach ($dataobject as $field => $value) {
902             if ($field === 'id') {
903                 continue;
904             }
905             if (!isset($columns[$field])) {
906                 continue;
907             }
908             $column = $columns[$field];
909             $cleaned[$field] = $this->normalise_value($column, $value);
910         }
912         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
913     }
915     /**
916      * Import a record into a table, id field is required.
917      * Safety checks are NOT carried out. Lobs are supported.
918      *
919      * @param string $table name of database table to be inserted into
920      * @param object $dataobject A data object with values for one or more fields in the record
921      * @return bool true
922      * @throws dml_exception A DML specific exception is thrown for any errors.
923      */
924     public function import_record($table, $dataobject) {
925         $dataobject = (array)$dataobject;
927         $columns = $this->get_columns($table);
928         $cleaned = array();
930         foreach ($dataobject as $field => $value) {
931             if (!isset($columns[$field])) {
932                 continue;
933             }
934             $column = $columns[$field];
935             $cleaned[$field] = $this->normalise_value($column, $value);
936         }
938         $this->insert_record_raw($table, $cleaned, false, false, true);
940         return true;
941     }
943     /**
944      * Update record in database, as fast as possible, no safety checks, lobs not supported.
945      * @param string $table name
946      * @param mixed $params data record as object or array
947      * @param bool true means repeated updates expected
948      * @return bool true
949      * @throws dml_exception A DML specific exception is thrown for any errors.
950      */
951     public function update_record_raw($table, $params, $bulk=false) {
952         $params = (array)$params;
954         if (!isset($params['id'])) {
955             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
956         }
957         $id = $params['id'];
958         unset($params['id']);
960         if (empty($params)) {
961             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
962         }
964         $sets = array();
965         foreach ($params as $field=>$value) {
966             $sets[] = "$field = ?";
967         }
969         $params[] = $id; // last ? in WHERE condition
971         $sets = implode(',', $sets);
972         $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?";
974         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
975         $rawsql = $this->emulate_bound_params($sql, $params);
977         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
978         $result = mssql_query($rawsql, $this->mssql);
979         $this->query_end($result);
981         $this->free_result($result);
982         return true;
983     }
985     /**
986      * Update a record in a table
987      *
988      * $dataobject is an object containing needed data
989      * Relies on $dataobject having a variable "id" to
990      * specify the record to update
991      *
992      * @param string $table The database table to be checked against.
993      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
994      * @param bool true means repeated updates expected
995      * @return bool true
996      * @throws dml_exception A DML specific exception is thrown for any errors.
997      */
998     public function update_record($table, $dataobject, $bulk=false) {
999         $dataobject = (array)$dataobject;
1001         $columns = $this->get_columns($table);
1002         $cleaned = array();
1004         foreach ($dataobject as $field => $value) {
1005             if (!isset($columns[$field])) {
1006                 continue;
1007             }
1008             $column = $columns[$field];
1009             $cleaned[$field] = $this->normalise_value($column, $value);
1010         }
1012         return $this->update_record_raw($table, $cleaned, $bulk);
1013     }
1015     /**
1016      * Set a single field in every table record which match a particular WHERE clause.
1017      *
1018      * @param string $table The database table to be checked against.
1019      * @param string $newfield the field to set.
1020      * @param string $newvalue the value to set the field to.
1021      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1022      * @param array $params array of sql parameters
1023      * @return bool true
1024      * @throws dml_exception A DML specific exception is thrown for any errors.
1025      */
1026     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1028         if ($select) {
1029             $select = "WHERE $select";
1030         }
1031         if (is_null($params)) {
1032             $params = array();
1033         }
1035         // convert params to ? types
1036         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1038         // Get column metadata
1039         $columns = $this->get_columns($table);
1040         $column = $columns[$newfield];
1042         $newvalue = $this->normalise_value($column, $newvalue);
1044         if (is_null($newvalue)) {
1045             $newfield = "$newfield = NULL";
1046         } else {
1047             $newfield = "$newfield = ?";
1048             array_unshift($params, $newvalue);
1049         }
1050         $sql = "UPDATE {" . $table . "} SET $newfield $select";
1052         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1053         $rawsql = $this->emulate_bound_params($sql, $params);
1055         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1056         $result = mssql_query($rawsql, $this->mssql);
1057         $this->query_end($result);
1059         $this->free_result($result);
1061         return true;
1062     }
1064     /**
1065      * Delete one or more records from a table which match a particular WHERE clause.
1066      *
1067      * @param string $table The database table to be checked against.
1068      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1069      * @param array $params array of sql parameters
1070      * @return bool true
1071      * @throws dml_exception A DML specific exception is thrown for any errors.
1072      */
1073     public function delete_records_select($table, $select, array $params=null) {
1075         if ($select) {
1076             $select = "WHERE $select";
1077         }
1079         $sql = "DELETE FROM {" . $table . "} $select";
1081         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1082         $rawsql = $this->emulate_bound_params($sql, $params);
1084         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1085         $result = mssql_query($rawsql, $this->mssql);
1086         $this->query_end($result);
1088         $this->free_result($result);
1090         return true;
1091     }
1093     public function sql_cast_char2int($fieldname, $text=false) {
1094         if (!$text) {
1095             return ' CAST(' . $fieldname . ' AS INT) ';
1096         } else {
1097             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1098         }
1099     }
1101     public function sql_cast_char2real($fieldname, $text=false) {
1102         if (!$text) {
1103             return ' CAST(' . $fieldname . ' AS REAL) ';
1104         } else {
1105             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) ';
1106         }
1107     }
1109     public function sql_ceil($fieldname) {
1110         return ' CEILING(' . $fieldname . ')';
1111     }
1114     protected function get_collation() {
1115         if (isset($this->collation)) {
1116             return $this->collation;
1117         }
1118         if (!empty($this->dboptions['dbcollation'])) {
1119             // perf speedup
1120             $this->collation = $this->dboptions['dbcollation'];
1121             return $this->collation;
1122         }
1124         // make some default
1125         $this->collation = 'Latin1_General_CI_AI';
1127         $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation";
1128         $this->query_start($sql, null, SQL_QUERY_AUX);
1129         $result = mssql_query($sql, $this->mssql);
1130         $this->query_end($result);
1132         if ($result) {
1133             if ($rawcolumn = mssql_fetch_assoc($result)) {
1134                 $this->collation = reset($rawcolumn);
1135             }
1136             $this->free_result($result);
1137         }
1139         return $this->collation;
1140     }
1142     /**
1143      * Returns 'LIKE' part of a query.
1144      *
1145      * @param string $fieldname usually name of the table column
1146      * @param string $param usually bound query parameter (?, :named)
1147      * @param bool $casesensitive use case sensitive search
1148      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1149      * @param bool $notlike true means "NOT LIKE"
1150      * @param string $escapechar escape char for '%' and '_'
1151      * @return string SQL code fragment
1152      */
1153     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1154         if (strpos($param, '%') !== false) {
1155             debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1156         }
1158         $collation = $this->get_collation();
1160         if ($casesensitive) {
1161             $collation = str_replace('_CI', '_CS', $collation);
1162         } else {
1163             $collation = str_replace('_CS', '_CI', $collation);
1164         }
1165         if ($accentsensitive) {
1166             $collation = str_replace('_AI', '_AS', $collation);
1167         } else {
1168             $collation = str_replace('_AS', '_AI', $collation);
1169         }
1171         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1173         return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'";
1174     }
1176     public function sql_concat() {
1177         $arr = func_get_args();
1178         foreach ($arr as $key => $ele) {
1179             $arr[$key] = ' CAST(' . $ele . ' AS VARCHAR(255)) ';
1180         }
1181         $s = implode(' + ', $arr);
1182         if ($s === '') {
1183             return " '' ";
1184         }
1185         return " $s ";
1186     }
1188     public function sql_concat_join($separator="' '", $elements=array()) {
1189         for ($n=count($elements)-1; $n > 0 ; $n--) {
1190             array_splice($elements, $n, 0, $separator);
1191         }
1192         $s = implode(' + ', $elements);
1193         if ($s === '') {
1194             return " '' ";
1195         }
1196         return " $s ";
1197     }
1199    public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1200         if ($textfield) {
1201             return ' (' . $this->sql_compare_text($fieldname) . " = '') ";
1202         } else {
1203             return " ($fieldname = '') ";
1204         }
1205     }
1207    /**
1208      * Returns the SQL text to be used to calculate the length in characters of one expression.
1209      * @param string fieldname or expression to calculate its length in characters.
1210      * @return string the piece of SQL code to be used in the statement.
1211      */
1212     public function sql_length($fieldname) {
1213         return ' LEN(' . $fieldname . ')';
1214     }
1216     public function sql_order_by_text($fieldname, $numchars=32) {
1217         return ' CONVERT(varchar, ' . $fieldname . ', ' . $numchars . ')';
1218     }
1220    /**
1221      * Returns the SQL for returning searching one string for the location of another.
1222      */
1223     public function sql_position($needle, $haystack) {
1224         return "CHARINDEX(($needle), ($haystack))";
1225     }
1227     /**
1228      * Returns the proper substr() SQL text used to extract substrings from DB
1229      * NOTE: this was originally returning only function name
1230      *
1231      * @param string $expr some string field, no aggregates
1232      * @param mixed $start integer or expression evaluating to int
1233      * @param mixed $length optional integer or expression evaluating to int
1234      * @return string sql fragment
1235      */
1236     public function sql_substr($expr, $start, $length=false) {
1237         if (count(func_get_args()) < 2) {
1238             throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa
1239 s only returning name of SQL substring function, it now requires all parameters.');
1240         }
1241         if ($length === false) {
1242             return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))";
1243         } else {
1244             return "SUBSTRING($expr, $start, $length)";
1245         }
1246     }
1248     public function session_lock_supported() {
1249         return true;
1250     }
1252     /**
1253      * Obtain session lock
1254      * @param int $rowid id of the row with session record
1255      * @param int $timeout max allowed time to wait for the lock in seconds
1256      * @return bool success
1257      */
1258     public function get_session_lock($rowid, $timeout) {
1259         if (!$this->session_lock_supported()) {
1260             return;
1261         }
1262         parent::get_session_lock($rowid, $timeout);
1264         $timeoutmilli = $timeout * 1000;
1266         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1267         // There is one bug in PHP/freetds (both reproducible with mssql_query()
1268         // and its mssql_init()/mssql_bind()/mssql_execute() alternative) for
1269         // stored procedures, causing scalar results of the execution
1270         // to be cast to boolean (true/fals). Here there is one
1271         // workaround that forces the return of one recordset resource.
1272         // $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session',  $timeoutmilli";
1273         $sql = "BEGIN
1274                     DECLARE @result INT
1275                     EXECUTE @result = sp_getapplock @Resource='$fullname',
1276                                                     @LockMode='Exclusive',
1277                                                     @LockOwner='Session',
1278                                                     @LockTimeout='$timeoutmilli'
1279                     SELECT @result
1280                 END";
1281         $this->query_start($sql, null, SQL_QUERY_AUX);
1282         $result = mssql_query($sql, $this->mssql);
1283         $this->query_end($result);
1285         if ($result) {
1286             $row = mssql_fetch_row($result);
1287             if ($row[0] < 0) {
1288                 throw new dml_sessionwait_exception();
1289             }
1290         }
1292         $this->free_result($result);
1293     }
1295     public function release_session_lock($rowid) {
1296         if (!$this->session_lock_supported()) {
1297             return;
1298         }
1299         parent::release_session_lock($rowid);
1301         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1302         $sql = "sp_releaseapplock '$fullname', 'Session'";
1303         $this->query_start($sql, null, SQL_QUERY_AUX);
1304         $result = mssql_query($sql, $this->mssql);
1305         $this->query_end($result);
1307         $this->free_result($result);
1308     }
1310     /**
1311      * Driver specific start of real database transaction,
1312      * this can not be used directly in code.
1313      * @return void
1314      */
1315     protected function begin_transaction() {
1316         // requires database to run with READ_COMMITTED_SNAPSHOT ON
1317         $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation
1318         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1319         $result = mssql_query($sql, $this->mssql);
1320         $this->query_end($result);
1322         $this->free_result($result);
1323     }
1325     /**
1326      * Driver specific commit of real database transaction,
1327      * this can not be used directly in code.
1328      * @return void
1329      */
1330     protected function commit_transaction() {
1331         $sql = "COMMIT TRANSACTION";
1332         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1333         $result = mssql_query($sql, $this->mssql);
1334         $this->query_end($result);
1336         $this->free_result($result);
1337     }
1339     /**
1340      * Driver specific abort of real database transaction,
1341      * this can not be used directly in code.
1342      * @return void
1343      */
1344     protected function rollback_transaction() {
1345         $sql = "ROLLBACK TRANSACTION";
1346         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1347         $result = mssql_query($sql, $this->mssql);
1348         $this->query_end($result);
1350         $this->free_result($result);
1351     }