bf289900b7385a1feb6e5795e2dbc072477dbd25
[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         $return = strtok($sql, '?');
619         foreach ($params as $param) {
620             if (is_bool($param)) {
621                 $return .= (int)$param;
623             } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially
624                 $return .= '0x' . $param['hex'];
626             } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not*
627                 $return .= "N'{$param['numstr']}'";                   // be converted back to number params, but bound as strings
629             } else if (is_null($param)) {
630                 $return .= 'NULL';
632             } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646
633                 $return .= "'".$param."'"; //fix for MDL-24863 to prevent auto-cast to int.
635             } else if (is_float($param)) {
636                 $return .= $param;
638             } else {
639                 $param = str_replace("'", "''", $param);
640                 $return .= "N'$param'";
641             }
643             $return .= strtok('?');
644         }
645         return $return;
646     }
648     /**
649      * Execute general sql query. Should be used only when no other method suitable.
650      * Do NOT use this to make changes in db structure, use database_manager::execute_sql() instead!
651      * @param string $sql query
652      * @param array $params query parameters
653      * @return bool true
654      * @throws dml_exception if error
655      */
656     public function execute($sql, array $params=null) {
658         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
659         $rawsql = $this->emulate_bound_params($sql, $params);
661         if (strpos($sql, ';') !== false) {
662             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
663         }
665         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
666         $result = mssql_query($rawsql, $this->mssql);
667         $this->query_end($result);
668         $this->free_result($result);
670         return true;
671     }
673     /**
674      * Get a number of records as a moodle_recordset using a SQL statement.
675      *
676      * Since this method is a little less readable, use of it should be restricted to
677      * code where it's possible there might be large datasets being returned.  For known
678      * small datasets use get_records_sql - it leads to simpler code.
679      *
680      * The return type is as for @see function get_recordset.
681      *
682      * @param string $sql the SQL select query to execute.
683      * @param array $params array of sql parameters
684      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
685      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
686      * @return moodle_recordset instance
687      * @throws dml_exception if error
688      */
689     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
690         $limitfrom = (int)$limitfrom;
691         $limitnum  = (int)$limitnum;
692         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
693         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
694         if ($limitfrom or $limitnum) {
695             if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later)
696                 $fetch = $limitfrom + $limitnum;
697                 if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow
698                     $fetch = PHP_INT_MAX;
699                 }
700                 $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
701                                     "\\1SELECT\\2 TOP $fetch", $sql);
702             }
703         }
705         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
706         $rawsql = $this->emulate_bound_params($sql, $params);
708         $this->query_start($sql, $params, SQL_QUERY_SELECT);
709         $result = mssql_query($rawsql, $this->mssql);
710         $this->query_end($result);
712         if ($limitfrom) { // Skip $limitfrom records
713             mssql_data_seek($result, $limitfrom);
714         }
716         return $this->create_recordset($result);
717     }
719     protected function create_recordset($result) {
720         return new mssql_native_moodle_recordset($result);
721     }
723     /**
724      * Get a number of records as an array of objects using a SQL statement.
725      *
726      * Return value as for @see function get_records.
727      *
728      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
729      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
730      *   returned array.
731      * @param array $params array of sql parameters
732      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
733      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
734      * @return array of objects, or empty array if no records were found
735      * @throws dml_exception if error
736      */
737     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
739         $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
741         $results = array();
743         foreach ($rs as $row) {
744             $id = reset($row);
745             if (isset($results[$id])) {
746                 $colname = key($row);
747                 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);
748             }
749             $results[$id] = $row;
750         }
751         $rs->close();
753         return $results;
754     }
756     /**
757      * Selects records and return values (first field) as an array using a SQL statement.
758      *
759      * @param string $sql The SQL query
760      * @param array $params array of sql parameters
761      * @return array of values
762      * @throws dml_exception if error
763      */
764     public function get_fieldset_sql($sql, array $params=null) {
766         $rs = $this->get_recordset_sql($sql, $params);
768         $results = array();
770         foreach ($rs as $row) {
771             $results[] = reset($row);
772         }
773         $rs->close();
775         return $results;
776     }
778     /**
779      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
780      * @param string $table name
781      * @param mixed $params data record as object or array
782      * @param bool $returnit return it of inserted record
783      * @param bool $bulk true means repeated inserts expected
784      * @param bool $customsequence true if 'id' included in $params, disables $returnid
785      * @return bool|int true or new id
786      * @throws dml_exception if error
787      */
788     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
789         if (!is_array($params)) {
790             $params = (array)$params;
791         }
793         $returning = "";
795         if ($customsequence) {
796             if (!isset($params['id'])) {
797                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
798             }
799             $returnid = false;
801             // Disable IDENTITY column before inserting record with id
802             $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
803             list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
804             $this->query_start($sql, null, SQL_QUERY_AUX);
805             $result = mssql_query($sql, $this->mssql);
806             $this->query_end($result);
807             $this->free_result($result);
809         } else {
810             unset($params['id']);
811             if ($returnid) {
812                 $returning = "; SELECT SCOPE_IDENTITY()";
813             }
814         }
816         if (empty($params)) {
817             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
818         }
820         $fields = implode(',', array_keys($params));
821         $qms    = array_fill(0, count($params), '?');
822         $qms    = implode(',', $qms);
824         $sql = "INSERT INTO {" . $table . "} ($fields) VALUES($qms) $returning";
826         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
827         $rawsql = $this->emulate_bound_params($sql, $params);
829         $this->query_start($sql, $params, SQL_QUERY_INSERT);
830         $result = mssql_query($rawsql, $this->mssql);
831         $this->query_end($result);
833         if ($returning !== "") {
834             $row = mssql_fetch_assoc($result);
835             $params['id'] = reset($row);
836         }
837         $this->free_result($result);
839         if ($customsequence) {
840             // Enable IDENTITY column after inserting record with id
841             $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
842             list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
843             $this->query_start($sql, null, SQL_QUERY_AUX);
844             $result = mssql_query($sql, $this->mssql);
845             $this->query_end($result);
846             $this->free_result($result);
847         }
849         if (!$returnid) {
850             return true;
851         }
853         return (int)$params['id'];
854     }
856     /**
857      * Insert a record into a table and return the "id" field if required.
858      *
859      * Some conversions and safety checks are carried out. Lobs are supported.
860      * If the return ID isn't required, then this just reports success as true/false.
861      * $data is an object containing needed data
862      * @param string $table The database table to be inserted into
863      * @param object $data A data object with values for one or more fields in the record
864      * @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.
865      * @return bool|int true or new id
866      * @throws dml_exception if error
867      */
868     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
869         $dataobject = (array)$dataobject;
871         $columns = $this->get_columns($table);
872         $cleaned = array();
874         foreach ($dataobject as $field => $value) {
875             if ($field === 'id') {
876                 continue;
877             }
878             if (!isset($columns[$field])) {
879                 continue;
880             }
881             $column = $columns[$field];
882             $cleaned[$field] = $this->normalise_value($column, $value);
883         }
885         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
886     }
888     /**
889      * Import a record into a table, id field is required.
890      * Safety checks are NOT carried out. Lobs are supported.
891      *
892      * @param string $table name of database table to be inserted into
893      * @param object $dataobject A data object with values for one or more fields in the record
894      * @return bool true
895      * @throws dml_exception if error
896      */
897     public function import_record($table, $dataobject) {
898         $dataobject = (array)$dataobject;
900         $columns = $this->get_columns($table);
901         $cleaned = array();
903         foreach ($dataobject as $field => $value) {
904             if (!isset($columns[$field])) {
905                 continue;
906             }
907             $column = $columns[$field];
908             $cleaned[$field] = $this->normalise_value($column, $value);
909         }
911         $this->insert_record_raw($table, $cleaned, false, false, true);
913         return true;
914     }
916     /**
917      * Update record in database, as fast as possible, no safety checks, lobs not supported.
918      * @param string $table name
919      * @param mixed $params data record as object or array
920      * @param bool true means repeated updates expected
921      * @return bool true
922      * @throws dml_exception if error
923      */
924     public function update_record_raw($table, $params, $bulk=false) {
925         $params = (array)$params;
927         if (!isset($params['id'])) {
928             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
929         }
930         $id = $params['id'];
931         unset($params['id']);
933         if (empty($params)) {
934             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
935         }
937         $sets = array();
938         foreach ($params as $field=>$value) {
939             $sets[] = "$field = ?";
940         }
942         $params[] = $id; // last ? in WHERE condition
944         $sets = implode(',', $sets);
945         $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?";
947         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
948         $rawsql = $this->emulate_bound_params($sql, $params);
950         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
951         $result = mssql_query($rawsql, $this->mssql);
952         $this->query_end($result);
954         $this->free_result($result);
955         return true;
956     }
958     /**
959      * Update a record in a table
960      *
961      * $dataobject is an object containing needed data
962      * Relies on $dataobject having a variable "id" to
963      * specify the record to update
964      *
965      * @param string $table The database table to be checked against.
966      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
967      * @param bool true means repeated updates expected
968      * @return bool true
969      * @throws dml_exception if error
970      */
971     public function update_record($table, $dataobject, $bulk=false) {
972         $dataobject = (array)$dataobject;
974         $columns = $this->get_columns($table);
975         $cleaned = array();
977         foreach ($dataobject as $field => $value) {
978             if (!isset($columns[$field])) {
979                 continue;
980             }
981             $column = $columns[$field];
982             $cleaned[$field] = $this->normalise_value($column, $value);
983         }
985         return $this->update_record_raw($table, $cleaned, $bulk);
986     }
988     /**
989      * Set a single field in every table record which match a particular WHERE clause.
990      *
991      * @param string $table The database table to be checked against.
992      * @param string $newfield the field to set.
993      * @param string $newvalue the value to set the field to.
994      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
995      * @param array $params array of sql parameters
996      * @return bool true
997      * @throws dml_exception if error
998      */
999     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1001         if ($select) {
1002             $select = "WHERE $select";
1003         }
1004         if (is_null($params)) {
1005             $params = array();
1006         }
1008         // convert params to ? types
1009         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1011     /// Get column metadata
1012         $columns = $this->get_columns($table);
1013         $column = $columns[$newfield];
1015         $newvalue = $this->normalise_value($column, $newvalue);
1017         if (is_null($newvalue)) {
1018             $newfield = "$newfield = NULL";
1019         } else {
1020             $newfield = "$newfield = ?";
1021             array_unshift($params, $newvalue);
1022         }
1023         $sql = "UPDATE {" . $table . "} SET $newfield $select";
1025         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1026         $rawsql = $this->emulate_bound_params($sql, $params);
1028         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1029         $result = mssql_query($rawsql, $this->mssql);
1030         $this->query_end($result);
1032         $this->free_result($result);
1034         return true;
1035     }
1037     /**
1038      * Delete one or more records from a table which match a particular WHERE clause.
1039      *
1040      * @param string $table The database table to be checked against.
1041      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1042      * @param array $params array of sql parameters
1043      * @return bool true
1044      * @throws dml_exception if error
1045      */
1046     public function delete_records_select($table, $select, array $params=null) {
1048         if ($select) {
1049             $select = "WHERE $select";
1050         }
1052         $sql = "DELETE FROM {" . $table . "} $select";
1054         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1055         $rawsql = $this->emulate_bound_params($sql, $params);
1057         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1058         $result = mssql_query($rawsql, $this->mssql);
1059         $this->query_end($result);
1061         $this->free_result($result);
1063         return true;
1064     }
1066 /// SQL helper functions
1068     public function sql_cast_char2int($fieldname, $text=false) {
1069         if (!$text) {
1070             return ' CAST(' . $fieldname . ' AS INT) ';
1071         } else {
1072             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1073         }
1074     }
1076     public function sql_cast_char2real($fieldname, $text=false) {
1077         if (!$text) {
1078             return ' CAST(' . $fieldname . ' AS REAL) ';
1079         } else {
1080             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) ';
1081         }
1082     }
1084     public function sql_ceil($fieldname) {
1085         return ' CEILING(' . $fieldname . ')';
1086     }
1089     protected function get_collation() {
1090         if (isset($this->collation)) {
1091             return $this->collation;
1092         }
1093         if (!empty($this->dboptions['dbcollation'])) {
1094             // perf speedup
1095             $this->collation = $this->dboptions['dbcollation'];
1096             return $this->collation;
1097         }
1099         // make some default
1100         $this->collation = 'Latin1_General_CI_AI';
1102         $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation";
1103         $this->query_start($sql, null, SQL_QUERY_AUX);
1104         $result = mssql_query($sql, $this->mssql);
1105         $this->query_end($result);
1107         if ($result) {
1108             if ($rawcolumn = mssql_fetch_assoc($result)) {
1109                 $this->collation = reset($rawcolumn);
1110             }
1111             $this->free_result($result);
1112         }
1114         return $this->collation;
1115     }
1117     /**
1118      * Returns 'LIKE' part of a query.
1119      *
1120      * @param string $fieldname usually name of the table column
1121      * @param string $param usually bound query parameter (?, :named)
1122      * @param bool $casesensitive use case sensitive search
1123      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1124      * @param bool $notlike true means "NOT LIKE"
1125      * @param string $escapechar escape char for '%' and '_'
1126      * @return string SQL code fragment
1127      */
1128     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1129         if (strpos($param, '%') !== false) {
1130             debugging('Potential SQL injection detected, sql_ilike() expects bound parameters (? or :named)');
1131         }
1133         $collation = $this->get_collation();
1135         if ($casesensitive) {
1136             $collation = str_replace('_CI', '_CS', $collation);
1137         } else {
1138             $collation = str_replace('_CS', '_CI', $collation);
1139         }
1140         if ($accentsensitive) {
1141             $collation = str_replace('_AI', '_AS', $collation);
1142         } else {
1143             $collation = str_replace('_AS', '_AI', $collation);
1144         }
1146         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1148         return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'";
1149     }
1151     public function sql_concat() {
1152         $arr = func_get_args();
1153         foreach ($arr as $key => $ele) {
1154             $arr[$key] = ' CAST(' . $ele . ' AS VARCHAR(255)) ';
1155         }
1156         $s = implode(' + ', $arr);
1157         if ($s === '') {
1158             return " '' ";
1159         }
1160         return " $s ";
1161     }
1163     public function sql_concat_join($separator="' '", $elements=array()) {
1164         for ($n=count($elements)-1; $n > 0 ; $n--) {
1165             array_splice($elements, $n, 0, $separator);
1166         }
1167         $s = implode(' + ', $elements);
1168         if ($s === '') {
1169             return " '' ";
1170         }
1171         return " $s ";
1172     }
1174    public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1175         if ($textfield) {
1176             return ' (' . $this->sql_compare_text($fieldname) . " = '') ";
1177         } else {
1178             return " ($fieldname = '') ";
1179         }
1180     }
1182    /**
1183      * Returns the SQL text to be used to calculate the length in characters of one expression.
1184      * @param string fieldname or expression to calculate its length in characters.
1185      * @return string the piece of SQL code to be used in the statement.
1186      */
1187     public function sql_length($fieldname) {
1188         return ' LEN(' . $fieldname . ')';
1189     }
1191     public function sql_order_by_text($fieldname, $numchars=32) {
1192         return ' CONVERT(varchar, ' . $fieldname . ', ' . $numchars . ')';
1193     }
1195    /**
1196      * Returns the SQL for returning searching one string for the location of another.
1197      */
1198     public function sql_position($needle, $haystack) {
1199         return "CHARINDEX(($needle), ($haystack))";
1200     }
1202     /**
1203      * Returns the proper substr() SQL text used to extract substrings from DB
1204      * NOTE: this was originally returning only function name
1205      *
1206      * @param string $expr some string field, no aggregates
1207      * @param mixed $start integer or expression evaluating to int
1208      * @param mixed $length optional integer or expression evaluating to int
1209      * @return string sql fragment
1210      */
1211     public function sql_substr($expr, $start, $length=false) {
1212         if (count(func_get_args()) < 2) {
1213             throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa
1214 s only returning name of SQL substring function, it now requires all parameters.');
1215         }
1216         if ($length === false) {
1217             return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))";
1218         } else {
1219             return "SUBSTRING($expr, $start, $length)";
1220         }
1221     }
1223 /// session locking
1225     public function session_lock_supported() {
1226         return true;
1227     }
1229     public function get_session_lock($rowid) {
1230         if (!$this->session_lock_supported()) {
1231             return;
1232         }
1233         parent::get_session_lock($rowid);
1235         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1236         $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session',  120000";
1237         $this->query_start($sql, null, SQL_QUERY_AUX);
1238         $result = mssql_query($sql, $this->mssql);
1239         $this->query_end($result);
1241         $this->free_result($result);
1242     }
1244     public function release_session_lock($rowid) {
1245         if (!$this->session_lock_supported()) {
1246             return;
1247         }
1248         parent::release_session_lock($rowid);
1250         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1251         $sql = "sp_releaseapplock '$fullname', 'Session'";
1252         $this->query_start($sql, null, SQL_QUERY_AUX);
1253         $result = mssql_query($sql, $this->mssql);
1254         $this->query_end($result);
1256         $this->free_result($result);
1257     }
1259 /// transactions
1261     /**
1262      * Driver specific start of real database transaction,
1263      * this can not be used directly in code.
1264      * @return void
1265      */
1266     protected function begin_transaction() {
1267         // requires database to run with READ_COMMITTED_SNAPSHOT ON
1268         $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation
1269         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1270         $result = mssql_query($sql, $this->mssql);
1271         $this->query_end($result);
1273         $this->free_result($result);
1274     }
1276     /**
1277      * Driver specific commit of real database transaction,
1278      * this can not be used directly in code.
1279      * @return void
1280      */
1281     protected function commit_transaction() {
1282         $sql = "COMMIT TRANSACTION";
1283         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1284         $result = mssql_query($sql, $this->mssql);
1285         $this->query_end($result);
1287         $this->free_result($result);
1288     }
1290     /**
1291      * Driver specific abort of real database transaction,
1292      * this can not be used directly in code.
1293      * @return void
1294      */
1295     protected function rollback_transaction() {
1296         $sql = "ROLLBACK TRANSACTION";
1297         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1298         $result = mssql_query($sql, $this->mssql);
1299         $this->query_end($result);
1301         $this->free_result($result);
1302     }