MDL-29295 do not use strtok in dml and ddl layers
[moodle.git] / lib / dml / mssql_native_moodle_database.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
19 /**
20  * Native mssql class representing moodle database interface.
21  *
22  * @package    core
23  * @subpackage dml
24  * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
25  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
28 defined('MOODLE_INTERNAL') || die();
30 require_once($CFG->libdir.'/dml/moodle_database.php');
31 require_once($CFG->libdir.'/dml/mssql_native_moodle_recordset.php');
32 require_once($CFG->libdir.'/dml/mssql_native_moodle_temptables.php');
34 /**
35  * Native mssql class representing moodle database interface.
36  */
37 class mssql_native_moodle_database extends moodle_database {
39     protected $mssql     = null;
40     protected $last_error_reporting; // To handle mssql driver default verbosity
41     protected $collation;  // current DB collation cache
43     /**
44      * Detects if all needed PHP stuff installed.
45      * Note: can be used before connect()
46      * @return mixed true if ok, string if something
47      */
48     public function driver_installed() {
49         if (!function_exists('mssql_connect')) {
50             return get_string('mssqlextensionisnotpresentinphp', 'install');
51         }
52         return true;
53     }
55     /**
56      * Returns database family type - describes SQL dialect
57      * Note: can be used before connect()
58      * @return string db family name (mysql, postgres, mssql, oracle, etc.)
59      */
60     public function get_dbfamily() {
61         return 'mssql';
62     }
64     /**
65      * Returns more specific database driver type
66      * Note: can be used before connect()
67      * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
68      */
69     protected function get_dbtype() {
70         return 'mssql';
71     }
73     /**
74      * Returns general database library name
75      * Note: can be used before connect()
76      * @return string db type pdo, native
77      */
78     protected function get_dblibrary() {
79         return 'native';
80     }
82     /**
83      * Returns localised database type name
84      * Note: can be used before connect()
85      * @return string
86      */
87     public function get_name() {
88         return get_string('nativemssql', 'install');
89     }
91     /**
92      * Returns localised database configuration help.
93      * Note: can be used before connect()
94      * @return string
95      */
96     public function get_configuration_help() {
97         return get_string('nativemssqlhelp', 'install');
98     }
100     /**
101      * Returns localised database description
102      * Note: can be used before connect()
103      * @return string
104      */
105     public function get_configuration_hints() {
106         $str = get_string('databasesettingssub_mssql', 'install');
107         $str .= "<p style='text-align:right'><a href=\"javascript:void(0)\" ";
108         $str .= "onclick=\"return window.open('http://docs.moodle.org/en/Installing_MSSQL_for_PHP')\"";
109         $str .= ">";
110         $str .= '<img src="pix/docs.gif' . '" alt="Docs" class="iconhelp" />';
111         $str .= get_string('moodledocslink', 'install') . '</a></p>';
112         return $str;
113     }
115     /**
116      * Connect to db
117      * Must be called before other methods.
118      * @param string $dbhost
119      * @param string $dbuser
120      * @param string $dbpass
121      * @param string $dbname
122      * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
123      * @param array $dboptions driver specific options
124      * @return bool true
125      * @throws dml_connection_exception if error
126      */
127     public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
128         if ($prefix == '' and !$this->external) {
129             //Enforce prefixes for everybody but mysql
130             throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
131         }
133         $driverstatus = $this->driver_installed();
135         if ($driverstatus !== true) {
136             throw new dml_exception('dbdriverproblem', $driverstatus);
137         }
139         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
141         $dbhost = $this->dbhost;
142         if (isset($dboptions['dbport'])) {
143             if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) {
144                 $dbhost .= ','.$dboptions['dbport'];
145             } else {
146                 $dbhost .= ':'.$dboptions['dbport'];
147             }
148         }
149         ob_start();
150         if (!empty($this->dboptions['dbpersist'])) { // persistent connection
151             $this->mssql = mssql_pconnect($dbhost, $this->dbuser, $this->dbpass, true);
152         } else {
153             $this->mssql = mssql_connect($dbhost, $this->dbuser, $this->dbpass, true);
154         }
155         $dberr = ob_get_contents();
156         ob_end_clean();
158         if ($this->mssql === false) {
159             $this->mssql = null;
160             throw new dml_connection_exception($dberr);
161         }
163         // already connected, select database and set some env. variables
164         $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX);
165         $result = mssql_select_db($this->dbname, $this->mssql);
166         $this->query_end($result);
168         // No need to set charset. It's UTF8, with transparent conversions
169         // back and forth performed both by FreeTDS or ODBTP
171         // Allow quoted identifiers
172         $sql = "SET QUOTED_IDENTIFIER ON";
173         $this->query_start($sql, null, SQL_QUERY_AUX);
174         $result = mssql_query($sql, $this->mssql);
175         $this->query_end($result);
177         $this->free_result($result);
179         // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL
180         // instead of equal(=) and distinct(<>) symbols
181         $sql = "SET ANSI_NULLS ON";
182         $this->query_start($sql, null, SQL_QUERY_AUX);
183         $result = mssql_query($sql, $this->mssql);
184         $this->query_end($result);
186         $this->free_result($result);
188         // Force ANSI warnings so arithmetic/string overflows will be
189         // returning error instead of transparently truncating data
190         $sql = "SET ANSI_WARNINGS ON";
191         $this->query_start($sql, null, SQL_QUERY_AUX);
192         $result = mssql_query($sql, $this->mssql);
193         $this->query_end($result);
195         // Concatenating null with anything MUST return NULL
196         $sql = "SET CONCAT_NULL_YIELDS_NULL  ON";
197         $this->query_start($sql, null, SQL_QUERY_AUX);
198         $result = mssql_query($sql, $this->mssql);
199         $this->query_end($result);
201         $this->free_result($result);
203         // Set transactions isolation level to READ_COMMITTED
204         // prevents dirty reads when using transactions +
205         // is the default isolation level of MSSQL
206         // Requires database to run with READ_COMMITTED_SNAPSHOT ON
207         $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
208         $this->query_start($sql, NULL, SQL_QUERY_AUX);
209         $result = mssql_query($sql, $this->mssql);
210         $this->query_end($result);
212         $this->free_result($result);
214         // Connection stabilised and configured, going to instantiate the temptables controller
215         $this->temptables = new mssql_native_moodle_temptables($this);
217         return true;
218     }
220     /**
221      * Close database connection and release all resources
222      * and memory (especially circular memory references).
223      * Do NOT use connect() again, create a new instance if needed.
224      */
225     public function dispose() {
226         parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
227         if ($this->mssql) {
228             mssql_close($this->mssql);
229             $this->mssql = null;
230         }
231     }
233     /**
234      * Called before each db query.
235      * @param string $sql
236      * @param array array of parameters
237      * @param int $type type of query
238      * @param mixed $extrainfo driver specific extra information
239      * @return void
240      */
241     protected function query_start($sql, array $params=null, $type, $extrainfo=null) {
242         parent::query_start($sql, $params, $type, $extrainfo);
243         // mssql driver tends to send debug to output, we do not need that ;-)
244         $this->last_error_reporting = error_reporting(0);
245     }
247     /**
248      * Called immediately after each db query.
249      * @param mixed db specific result
250      * @return void
251      */
252     protected function query_end($result) {
253         // reset original debug level
254         error_reporting($this->last_error_reporting);
255         parent::query_end($result);
256     }
258     /**
259      * Returns database server info array
260      * @return array
261      */
262     public function get_server_info() {
263         static $info;
264         if (!$info) {
265             $info = array();
266             $sql = 'sp_server_info 2';
267             $this->query_start($sql, null, SQL_QUERY_AUX);
268             $result = mssql_query($sql, $this->mssql);
269             $this->query_end($result);
270             $row = mssql_fetch_row($result);
271             $info['description'] = $row[2];
272             $this->free_result($result);
274             $sql = 'sp_server_info 500';
275             $this->query_start($sql, null, SQL_QUERY_AUX);
276             $result = mssql_query($sql, $this->mssql);
277             $this->query_end($result);
278             $row = mssql_fetch_row($result);
279             $info['version'] = $row[2];
280             $this->free_result($result);
281         }
282         return $info;
283     }
285     protected function is_min_version($version) {
286         $server = $this->get_server_info();
287         $server = $server['version'];
288         return version_compare($server, $version, '>=');
289     }
291     /**
292      * Converts short table name {tablename} to real table name
293      * supporting temp tables (#) if detected
294      *
295      * @param string sql
296      * @return string sql
297      */
298     protected function fix_table_names($sql) {
299         if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
300             foreach($matches[0] as $key=>$match) {
301                 $name = $matches[1][$key];
302                 if ($this->temptables->is_temptable($name)) {
303                     $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
304                 } else {
305                     $sql = str_replace($match, $this->prefix.$name, $sql);
306                 }
307             }
308         }
309         return $sql;
310     }
312     /**
313      * Returns supported query parameter types
314      * @return int bitmask
315      */
316     protected function allowed_param_types() {
317         return SQL_PARAMS_QM; // Not really, but emulated, see emulate_bound_params()
318     }
320     /**
321      * Returns last error reported by database engine.
322      * @return string error message
323      */
324     public function get_last_error() {
325         return mssql_get_last_message();
326     }
328     /**
329      * Return tables in database WITHOUT current prefix
330      * @return array of table names in lowercase and without prefix
331      */
332     public function get_tables($usecache=true) {
333         if ($usecache and $this->tables !== null) {
334             return $this->tables;
335         }
336         $this->tables = array();
337         $sql = "SELECT table_name
338                   FROM information_schema.tables
339                  WHERE table_name LIKE '$this->prefix%'
340                    AND table_type = 'BASE TABLE'";
341         $this->query_start($sql, null, SQL_QUERY_AUX);
342         $result = mssql_query($sql, $this->mssql);
343         $this->query_end($result);
345         if ($result) {
346             while ($row = mssql_fetch_row($result)) {
347                 $tablename = reset($row);
348                 if (strpos($tablename, $this->prefix) !== 0) {
349                     continue;
350                 }
351                 $tablename = substr($tablename, strlen($this->prefix));
352                 $this->tables[$tablename] = $tablename;
353             }
354             $this->free_result($result);
355         }
357         // Add the currently available temptables
358         $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
359         return $this->tables;
360     }
362     /**
363      * Return table indexes - everything lowercased
364      * @return array of arrays
365      */
366     public function get_indexes($table) {
367         $indexes = array();
368         $tablename = $this->prefix.$table;
370         // Indexes aren't covered by information_schema metatables, so we need to
371         // go to sys ones. Skipping primary key indexes on purpose.
372         $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name
373                   FROM sys.indexes i
374                   JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
375                   JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
376                   JOIN sys.tables t ON i.object_id = t.object_id
377                  WHERE t.name = '$tablename'
378                    AND i.is_primary_key = 0
379               ORDER BY i.name, i.index_id, ic.index_column_id";
381         $this->query_start($sql, null, SQL_QUERY_AUX);
382         $result = mssql_query($sql, $this->mssql);
383         $this->query_end($result);
385         if ($result) {
386             $lastindex = '';
387             $unique = false;
388             $columns = array();
389             while ($row = mssql_fetch_assoc($result)) {
390                 if ($lastindex and $lastindex != $row['index_name']) { // Save lastindex to $indexes and reset info
391                     $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
392                     $unique = false;
393                     $columns = array();
394                 }
395                 $lastindex = $row['index_name'];
396                 $unique = empty($row['is_unique']) ? false : true;
397                 $columns[] = $row['column_name'];
398             }
399             if ($lastindex ) { // Add the last one if exists
400                 $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
401             }
402             $this->free_result($result);
403         }
404         return $indexes;
405     }
407     /**
408      * Returns datailed information about columns in table. This information is cached internally.
409      * @param string $table name
410      * @param bool $usecache
411      * @return array array of database_column_info objects indexed with column names
412      */
413     public function get_columns($table, $usecache=true) {
414         if ($usecache and isset($this->columns[$table])) {
415             return $this->columns[$table];
416         }
418         $this->columns[$table] = array();
420         if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema
421             $sql = "SELECT column_name AS name,
422                            data_type AS type,
423                            numeric_precision AS max_length,
424                            character_maximum_length AS char_max_length,
425                            numeric_scale AS scale,
426                            is_nullable AS is_nullable,
427                            columnproperty(object_id(quotename(table_schema) + '.' +
428                                quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
429                            column_default AS default_value
430                       FROM information_schema.columns
431                      WHERE table_name = '{" . $table . "}'
432                   ORDER BY ordinal_position";
433         } else { // temp table, get metadata from tempdb schema
434             $sql = "SELECT column_name AS name,
435                            data_type AS type,
436                            numeric_precision AS max_length,
437                            character_maximum_length AS char_max_length,
438                            numeric_scale AS scale,
439                            is_nullable AS is_nullable,
440                            columnproperty(object_id(quotename(table_schema) + '.' +
441                                quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
442                            column_default AS default_value
443                       FROM tempdb.information_schema.columns
444                       JOIN tempdb..sysobjects ON name = table_name
445                      WHERE id = object_id('tempdb..{" . $table . "}')
446                   ORDER BY ordinal_position";
447         }
449         list($sql, $params, $type) = $this->fix_sql_params($sql, null);
451         $this->query_start($sql, null, SQL_QUERY_AUX);
452         $result = mssql_query($sql, $this->mssql);
453         $this->query_end($result);
455         if (!$result) {
456             return array();
457         }
459         while ($rawcolumn = mssql_fetch_assoc($result)) {
461             $rawcolumn = (object)$rawcolumn;
463             $info = new stdClass();
464             $info->name = $rawcolumn->name;
465             $info->type = $rawcolumn->type;
466             $info->meta_type = $this->mssqltype2moodletype($info->type);
468             // Prepare auto_increment info
469             $info->auto_increment = $rawcolumn->auto_increment ? true : false;
471             // Define type for auto_increment columns
472             $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type;
474             // id columns being auto_incremnt are PK by definition
475             $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
477             // Put correct length for character and LOB types
478             $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
479             $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
481             // Scale
482             $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false;
484             // Prepare not_null info
485             $info->not_null = $rawcolumn->is_nullable == 'NO'  ? true : false;
487             // Process defaults
488             $info->has_default = !empty($rawcolumn->default_value);
489             if ($rawcolumn->default_value === NULL) {
490                 $info->default_value = NULL;
491             } else {
492                 $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value);
493             }
495             // Process binary
496             $info->binary = $info->meta_type == 'B' ? true : false;
498             $this->columns[$table][$info->name] = new database_column_info($info);
499         }
500         $this->free_result($result);
502         return $this->columns[$table];
503     }
505     /**
506      * Normalise values based in RDBMS dependencies (booleans, LOBs...)
507      *
508      * @param database_column_info $column column metadata corresponding with the value we are going to normalise
509      * @param mixed $value value we are going to normalise
510      * @return mixed the normalised value
511      */
512     protected function normalise_value($column, $value) {
513         if (is_bool($value)) { /// Always, convert boolean to int
514             $value = (int)$value;
515         } // And continue processing because text columns with numeric info need special handling below
517         if ($column->meta_type == 'B') {   // BLOBs need to be properly "packed", but can be inserted directly if so.
518             if (!is_null($value)) {               // If value not null, unpack it to unquoted hexadecimal byte-string format
519                 $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it
520             }                                     // easily and "bind" the param ok.
522         } else if ($column->meta_type == 'X') {             // MSSQL doesn't cast from int to text, so if text column
523             if (is_numeric($value)) {                       // and is numeric value then cast to string
524                 $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how
525             }                                               // to "bind" the param ok, avoiding reverse conversion to number
527         } else if ($value === '') {
528             if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
529                 $value = 0; // prevent '' problems in numeric fields
530             }
531         }
532         return $value;
533     }
535     /**
536      * Selectively call mssql_free_result(), avoiding some warnings without using the horrible @
537      *
538      * @param mssql_resource $resource resource to be freed if possible
539      */
540     private function free_result($resource) {
541         if (!is_bool($resource)) { // true/false resources cannot be freed
542             mssql_free_result($resource);
543         }
544     }
546     /**
547      * Provides mapping between mssql native data types and moodle_database - database_column_info - ones)
548      *
549      * @param string $mssql_type native mssql data type
550      * @return string 1-char database_column_info data type
551      */
552     private function mssqltype2moodletype($mssql_type) {
553         $type = null;
554         switch (strtoupper($mssql_type)) {
555             case 'BIT':
556                 $type = 'L';
557                 break;
558             case 'INT':
559             case 'SMALLINT':
560             case 'INTEGER':
561             case 'BIGINT':
562                 $type = 'I';
563                 break;
564             case 'DECIMAL':
565             case 'REAL':
566             case 'FLOAT':
567                 $type = 'N';
568                 break;
569             case 'VARCHAR':
570             case 'NVARCHAR':
571                 $type = 'C';
572                 break;
573             case 'TEXT':
574             case 'NTEXT':
575             case 'VARCHAR(MAX)':
576             case 'NVARCHAR(MAX)':
577                 $type = 'X';
578                 break;
579             case 'IMAGE':
580             case 'VARBINARY(MAX)':
581                 $type = 'B';
582                 break;
583             case 'DATETIME':
584                 $type = 'D';
585                 break;
586         }
587         if (!$type) {
588             throw new dml_exception('invalidmssqlnativetype', $mssql_type);
589         }
590         return $type;
591     }
593     /**
594      * Do NOT use in code, to be used by database_manager only!
595      * @param string $sql query
596      * @return bool true
597      * @throws dml_exception if error
598      */
599     public function change_database_structure($sql) {
600         $this->reset_caches();
602         $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
603         $result = mssql_query($sql, $this->mssql);
604         $this->query_end($result);
606         return true;
607     }
609     /**
610      * Very ugly hack which emulates bound parameters in queries
611      * because the mssql driver doesn't support placeholders natively at all
612      */
613     protected function emulate_bound_params($sql, array $params=null) {
614         if (empty($params)) {
615             return $sql;
616         }
617         /// ok, we have verified sql statement with ? and correct number of params
618         $parts = explode('?', $sql);
619         $return = array_shift($parts);
620         foreach ($params as $param) {
621             if (is_bool($param)) {
622                 $return .= (int)$param;
624             } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially
625                 $return .= '0x' . $param['hex'];
627             } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not*
628                 $return .= "N'{$param['numstr']}'";                   // be converted back to number params, but bound as strings
630             } else if (is_null($param)) {
631                 $return .= 'NULL';
633             } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646
634                 $return .= "'".$param."'"; //fix for MDL-24863 to prevent auto-cast to int.
636             } else if (is_float($param)) {
637                 $return .= $param;
639             } else {
640                 $param = str_replace("'", "''", $param);
641                 $return .= "N'$param'";
642             }
644             $return .= array_shift($parts);
645         }
646         return $return;
647     }
649     /**
650      * Execute general sql query. Should be used only when no other method suitable.
651      * Do NOT use this to make changes in db structure, use database_manager::execute_sql() instead!
652      * @param string $sql query
653      * @param array $params query parameters
654      * @return bool true
655      * @throws dml_exception if error
656      */
657     public function execute($sql, array $params=null) {
659         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
660         $rawsql = $this->emulate_bound_params($sql, $params);
662         if (strpos($sql, ';') !== false) {
663             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
664         }
666         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
667         $result = mssql_query($rawsql, $this->mssql);
668         $this->query_end($result);
669         $this->free_result($result);
671         return true;
672     }
674     /**
675      * Get a number of records as a moodle_recordset using a SQL statement.
676      *
677      * Since this method is a little less readable, use of it should be restricted to
678      * code where it's possible there might be large datasets being returned.  For known
679      * small datasets use get_records_sql - it leads to simpler code.
680      *
681      * The return type is as for @see function get_recordset.
682      *
683      * @param string $sql the SQL select query to execute.
684      * @param array $params array of sql parameters
685      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
686      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
687      * @return moodle_recordset instance
688      * @throws dml_exception if error
689      */
690     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
691         $limitfrom = (int)$limitfrom;
692         $limitnum  = (int)$limitnum;
693         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
694         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
695         if ($limitfrom or $limitnum) {
696             if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later)
697                 $fetch = $limitfrom + $limitnum;
698                 if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow
699                     $fetch = PHP_INT_MAX;
700                 }
701                 $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
702                                     "\\1SELECT\\2 TOP $fetch", $sql);
703             }
704         }
706         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
707         $rawsql = $this->emulate_bound_params($sql, $params);
709         $this->query_start($sql, $params, SQL_QUERY_SELECT);
710         $result = mssql_query($rawsql, $this->mssql);
711         $this->query_end($result);
713         if ($limitfrom) { // Skip $limitfrom records
714             mssql_data_seek($result, $limitfrom);
715         }
717         return $this->create_recordset($result);
718     }
720     protected function create_recordset($result) {
721         return new mssql_native_moodle_recordset($result);
722     }
724     /**
725      * Get a number of records as an array of objects using a SQL statement.
726      *
727      * Return value as for @see function get_records.
728      *
729      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
730      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
731      *   returned array.
732      * @param array $params array of sql parameters
733      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
734      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
735      * @return array of objects, or empty array if no records were found
736      * @throws dml_exception if error
737      */
738     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
740         $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
742         $results = array();
744         foreach ($rs as $row) {
745             $id = reset($row);
746             if (isset($results[$id])) {
747                 $colname = key($row);
748                 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);
749             }
750             $results[$id] = $row;
751         }
752         $rs->close();
754         return $results;
755     }
757     /**
758      * Selects records and return values (first field) as an array using a SQL statement.
759      *
760      * @param string $sql The SQL query
761      * @param array $params array of sql parameters
762      * @return array of values
763      * @throws dml_exception if error
764      */
765     public function get_fieldset_sql($sql, array $params=null) {
767         $rs = $this->get_recordset_sql($sql, $params);
769         $results = array();
771         foreach ($rs as $row) {
772             $results[] = reset($row);
773         }
774         $rs->close();
776         return $results;
777     }
779     /**
780      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
781      * @param string $table name
782      * @param mixed $params data record as object or array
783      * @param bool $returnit return it of inserted record
784      * @param bool $bulk true means repeated inserts expected
785      * @param bool $customsequence true if 'id' included in $params, disables $returnid
786      * @return bool|int true or new id
787      * @throws dml_exception if error
788      */
789     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
790         if (!is_array($params)) {
791             $params = (array)$params;
792         }
794         $returning = "";
796         if ($customsequence) {
797             if (!isset($params['id'])) {
798                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
799             }
800             $returnid = false;
802             // Disable IDENTITY column before inserting record with id
803             $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
804             list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
805             $this->query_start($sql, null, SQL_QUERY_AUX);
806             $result = mssql_query($sql, $this->mssql);
807             $this->query_end($result);
808             $this->free_result($result);
810         } else {
811             unset($params['id']);
812             if ($returnid) {
813                 $returning = "; SELECT SCOPE_IDENTITY()";
814             }
815         }
817         if (empty($params)) {
818             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
819         }
821         $fields = implode(',', array_keys($params));
822         $qms    = array_fill(0, count($params), '?');
823         $qms    = implode(',', $qms);
825         $sql = "INSERT INTO {" . $table . "} ($fields) VALUES($qms) $returning";
827         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
828         $rawsql = $this->emulate_bound_params($sql, $params);
830         $this->query_start($sql, $params, SQL_QUERY_INSERT);
831         $result = mssql_query($rawsql, $this->mssql);
832         $this->query_end($result);
834         if ($returning !== "") {
835             $row = mssql_fetch_assoc($result);
836             $params['id'] = reset($row);
837         }
838         $this->free_result($result);
840         if ($customsequence) {
841             // Enable IDENTITY column after inserting record with id
842             $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
843             list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
844             $this->query_start($sql, null, SQL_QUERY_AUX);
845             $result = mssql_query($sql, $this->mssql);
846             $this->query_end($result);
847             $this->free_result($result);
848         }
850         if (!$returnid) {
851             return true;
852         }
854         return (int)$params['id'];
855     }
857     /**
858      * Insert a record into a table and return the "id" field if required.
859      *
860      * Some conversions and safety checks are carried out. Lobs are supported.
861      * If the return ID isn't required, then this just reports success as true/false.
862      * $data is an object containing needed data
863      * @param string $table The database table to be inserted into
864      * @param object $data A data object with values for one or more fields in the record
865      * @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.
866      * @return bool|int true or new id
867      * @throws dml_exception if error
868      */
869     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
870         $dataobject = (array)$dataobject;
872         $columns = $this->get_columns($table);
873         $cleaned = array();
875         foreach ($dataobject as $field => $value) {
876             if ($field === 'id') {
877                 continue;
878             }
879             if (!isset($columns[$field])) {
880                 continue;
881             }
882             $column = $columns[$field];
883             $cleaned[$field] = $this->normalise_value($column, $value);
884         }
886         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
887     }
889     /**
890      * Import a record into a table, id field is required.
891      * Safety checks are NOT carried out. Lobs are supported.
892      *
893      * @param string $table name of database table to be inserted into
894      * @param object $dataobject A data object with values for one or more fields in the record
895      * @return bool true
896      * @throws dml_exception if error
897      */
898     public function import_record($table, $dataobject) {
899         $dataobject = (array)$dataobject;
901         $columns = $this->get_columns($table);
902         $cleaned = array();
904         foreach ($dataobject as $field => $value) {
905             if (!isset($columns[$field])) {
906                 continue;
907             }
908             $column = $columns[$field];
909             $cleaned[$field] = $this->normalise_value($column, $value);
910         }
912         $this->insert_record_raw($table, $cleaned, false, false, true);
914         return true;
915     }
917     /**
918      * Update record in database, as fast as possible, no safety checks, lobs not supported.
919      * @param string $table name
920      * @param mixed $params data record as object or array
921      * @param bool true means repeated updates expected
922      * @return bool true
923      * @throws dml_exception if error
924      */
925     public function update_record_raw($table, $params, $bulk=false) {
926         $params = (array)$params;
928         if (!isset($params['id'])) {
929             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
930         }
931         $id = $params['id'];
932         unset($params['id']);
934         if (empty($params)) {
935             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
936         }
938         $sets = array();
939         foreach ($params as $field=>$value) {
940             $sets[] = "$field = ?";
941         }
943         $params[] = $id; // last ? in WHERE condition
945         $sets = implode(',', $sets);
946         $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?";
948         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
949         $rawsql = $this->emulate_bound_params($sql, $params);
951         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
952         $result = mssql_query($rawsql, $this->mssql);
953         $this->query_end($result);
955         $this->free_result($result);
956         return true;
957     }
959     /**
960      * Update a record in a table
961      *
962      * $dataobject is an object containing needed data
963      * Relies on $dataobject having a variable "id" to
964      * specify the record to update
965      *
966      * @param string $table The database table to be checked against.
967      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
968      * @param bool true means repeated updates expected
969      * @return bool true
970      * @throws dml_exception if error
971      */
972     public function update_record($table, $dataobject, $bulk=false) {
973         $dataobject = (array)$dataobject;
975         $columns = $this->get_columns($table);
976         $cleaned = array();
978         foreach ($dataobject as $field => $value) {
979             if (!isset($columns[$field])) {
980                 continue;
981             }
982             $column = $columns[$field];
983             $cleaned[$field] = $this->normalise_value($column, $value);
984         }
986         return $this->update_record_raw($table, $cleaned, $bulk);
987     }
989     /**
990      * Set a single field in every table record which match a particular WHERE clause.
991      *
992      * @param string $table The database table to be checked against.
993      * @param string $newfield the field to set.
994      * @param string $newvalue the value to set the field to.
995      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
996      * @param array $params array of sql parameters
997      * @return bool true
998      * @throws dml_exception if error
999      */
1000     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1002         if ($select) {
1003             $select = "WHERE $select";
1004         }
1005         if (is_null($params)) {
1006             $params = array();
1007         }
1009         // convert params to ? types
1010         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1012     /// Get column metadata
1013         $columns = $this->get_columns($table);
1014         $column = $columns[$newfield];
1016         $newvalue = $this->normalise_value($column, $newvalue);
1018         if (is_null($newvalue)) {
1019             $newfield = "$newfield = NULL";
1020         } else {
1021             $newfield = "$newfield = ?";
1022             array_unshift($params, $newvalue);
1023         }
1024         $sql = "UPDATE {" . $table . "} SET $newfield $select";
1026         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1027         $rawsql = $this->emulate_bound_params($sql, $params);
1029         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1030         $result = mssql_query($rawsql, $this->mssql);
1031         $this->query_end($result);
1033         $this->free_result($result);
1035         return true;
1036     }
1038     /**
1039      * Delete one or more records from a table which match a particular WHERE clause.
1040      *
1041      * @param string $table The database table to be checked against.
1042      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1043      * @param array $params array of sql parameters
1044      * @return bool true
1045      * @throws dml_exception if error
1046      */
1047     public function delete_records_select($table, $select, array $params=null) {
1049         if ($select) {
1050             $select = "WHERE $select";
1051         }
1053         $sql = "DELETE FROM {" . $table . "} $select";
1055         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1056         $rawsql = $this->emulate_bound_params($sql, $params);
1058         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1059         $result = mssql_query($rawsql, $this->mssql);
1060         $this->query_end($result);
1062         $this->free_result($result);
1064         return true;
1065     }
1067 /// SQL helper functions
1069     public function sql_cast_char2int($fieldname, $text=false) {
1070         if (!$text) {
1071             return ' CAST(' . $fieldname . ' AS INT) ';
1072         } else {
1073             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1074         }
1075     }
1077     public function sql_cast_char2real($fieldname, $text=false) {
1078         if (!$text) {
1079             return ' CAST(' . $fieldname . ' AS REAL) ';
1080         } else {
1081             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) ';
1082         }
1083     }
1085     public function sql_ceil($fieldname) {
1086         return ' CEILING(' . $fieldname . ')';
1087     }
1090     protected function get_collation() {
1091         if (isset($this->collation)) {
1092             return $this->collation;
1093         }
1094         if (!empty($this->dboptions['dbcollation'])) {
1095             // perf speedup
1096             $this->collation = $this->dboptions['dbcollation'];
1097             return $this->collation;
1098         }
1100         // make some default
1101         $this->collation = 'Latin1_General_CI_AI';
1103         $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation";
1104         $this->query_start($sql, null, SQL_QUERY_AUX);
1105         $result = mssql_query($sql, $this->mssql);
1106         $this->query_end($result);
1108         if ($result) {
1109             if ($rawcolumn = mssql_fetch_assoc($result)) {
1110                 $this->collation = reset($rawcolumn);
1111             }
1112             $this->free_result($result);
1113         }
1115         return $this->collation;
1116     }
1118     /**
1119      * Returns 'LIKE' part of a query.
1120      *
1121      * @param string $fieldname usually name of the table column
1122      * @param string $param usually bound query parameter (?, :named)
1123      * @param bool $casesensitive use case sensitive search
1124      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1125      * @param bool $notlike true means "NOT LIKE"
1126      * @param string $escapechar escape char for '%' and '_'
1127      * @return string SQL code fragment
1128      */
1129     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1130         if (strpos($param, '%') !== false) {
1131             debugging('Potential SQL injection detected, sql_ilike() expects bound parameters (? or :named)');
1132         }
1134         $collation = $this->get_collation();
1136         if ($casesensitive) {
1137             $collation = str_replace('_CI', '_CS', $collation);
1138         } else {
1139             $collation = str_replace('_CS', '_CI', $collation);
1140         }
1141         if ($accentsensitive) {
1142             $collation = str_replace('_AI', '_AS', $collation);
1143         } else {
1144             $collation = str_replace('_AS', '_AI', $collation);
1145         }
1147         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1149         return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'";
1150     }
1152     public function sql_concat() {
1153         $arr = func_get_args();
1154         foreach ($arr as $key => $ele) {
1155             $arr[$key] = ' CAST(' . $ele . ' AS VARCHAR(255)) ';
1156         }
1157         $s = implode(' + ', $arr);
1158         if ($s === '') {
1159             return " '' ";
1160         }
1161         return " $s ";
1162     }
1164     public function sql_concat_join($separator="' '", $elements=array()) {
1165         for ($n=count($elements)-1; $n > 0 ; $n--) {
1166             array_splice($elements, $n, 0, $separator);
1167         }
1168         $s = implode(' + ', $elements);
1169         if ($s === '') {
1170             return " '' ";
1171         }
1172         return " $s ";
1173     }
1175    public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1176         if ($textfield) {
1177             return ' (' . $this->sql_compare_text($fieldname) . " = '') ";
1178         } else {
1179             return " ($fieldname = '') ";
1180         }
1181     }
1183    /**
1184      * Returns the SQL text to be used to calculate the length in characters of one expression.
1185      * @param string fieldname or expression to calculate its length in characters.
1186      * @return string the piece of SQL code to be used in the statement.
1187      */
1188     public function sql_length($fieldname) {
1189         return ' LEN(' . $fieldname . ')';
1190     }
1192     public function sql_order_by_text($fieldname, $numchars=32) {
1193         return ' CONVERT(varchar, ' . $fieldname . ', ' . $numchars . ')';
1194     }
1196    /**
1197      * Returns the SQL for returning searching one string for the location of another.
1198      */
1199     public function sql_position($needle, $haystack) {
1200         return "CHARINDEX(($needle), ($haystack))";
1201     }
1203     /**
1204      * Returns the proper substr() SQL text used to extract substrings from DB
1205      * NOTE: this was originally returning only function name
1206      *
1207      * @param string $expr some string field, no aggregates
1208      * @param mixed $start integer or expression evaluating to int
1209      * @param mixed $length optional integer or expression evaluating to int
1210      * @return string sql fragment
1211      */
1212     public function sql_substr($expr, $start, $length=false) {
1213         if (count(func_get_args()) < 2) {
1214             throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa
1215 s only returning name of SQL substring function, it now requires all parameters.');
1216         }
1217         if ($length === false) {
1218             return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))";
1219         } else {
1220             return "SUBSTRING($expr, $start, $length)";
1221         }
1222     }
1224 /// session locking
1226     public function session_lock_supported() {
1227         return true;
1228     }
1230     public function get_session_lock($rowid) {
1231         if (!$this->session_lock_supported()) {
1232             return;
1233         }
1234         parent::get_session_lock($rowid);
1236         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1237         $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session',  120000";
1238         $this->query_start($sql, null, SQL_QUERY_AUX);
1239         $result = mssql_query($sql, $this->mssql);
1240         $this->query_end($result);
1242         $this->free_result($result);
1243     }
1245     public function release_session_lock($rowid) {
1246         if (!$this->session_lock_supported()) {
1247             return;
1248         }
1249         parent::release_session_lock($rowid);
1251         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1252         $sql = "sp_releaseapplock '$fullname', 'Session'";
1253         $this->query_start($sql, null, SQL_QUERY_AUX);
1254         $result = mssql_query($sql, $this->mssql);
1255         $this->query_end($result);
1257         $this->free_result($result);
1258     }
1260 /// transactions
1262     /**
1263      * Driver specific start of real database transaction,
1264      * this can not be used directly in code.
1265      * @return void
1266      */
1267     protected function begin_transaction() {
1268         // requires database to run with READ_COMMITTED_SNAPSHOT ON
1269         $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation
1270         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1271         $result = mssql_query($sql, $this->mssql);
1272         $this->query_end($result);
1274         $this->free_result($result);
1275     }
1277     /**
1278      * Driver specific commit of real database transaction,
1279      * this can not be used directly in code.
1280      * @return void
1281      */
1282     protected function commit_transaction() {
1283         $sql = "COMMIT TRANSACTION";
1284         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1285         $result = mssql_query($sql, $this->mssql);
1286         $this->query_end($result);
1288         $this->free_result($result);
1289     }
1291     /**
1292      * Driver specific abort of real database transaction,
1293      * this can not be used directly in code.
1294      * @return void
1295      */
1296     protected function rollback_transaction() {
1297         $sql = "ROLLBACK TRANSACTION";
1298         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1299         $result = mssql_query($sql, $this->mssql);
1300         $this->query_end($result);
1302         $this->free_result($result);
1303     }