cf55544eb9f9ca811c4846470f952628a2d09d8f
[moodle.git] / lib / dml / mssql_native_moodle_database.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Native mssql class representing moodle database interface.
19  *
20  * @package    core_dml
21  * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once(__DIR__.'/moodle_database.php');
28 require_once(__DIR__.'/mssql_native_moodle_recordset.php');
29 require_once(__DIR__.'/mssql_native_moodle_temptables.php');
31 /**
32  * Native mssql class representing moodle database interface.
33  *
34  * @package    core_dml
35  * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class mssql_native_moodle_database extends moodle_database {
40     protected $mssql     = null;
41     protected $last_error_reporting; // To handle mssql driver default verbosity
42     protected $collation;  // current DB collation cache
43     /**
44      * Does the used db version support ANSI way of limiting (2012 and higher)
45      * @var bool
46      */
47     protected $supportsoffsetfetch;
49     /**
50      * Detects if all needed PHP stuff installed.
51      * Note: can be used before connect()
52      * @return mixed true if ok, string if something
53      */
54     public function driver_installed() {
55         if (!function_exists('mssql_connect')) {
56             return get_string('mssqlextensionisnotpresentinphp', 'install');
57         }
58         return true;
59     }
61     /**
62      * Returns database family type - describes SQL dialect
63      * Note: can be used before connect()
64      * @return string db family name (mysql, postgres, mssql, oracle, etc.)
65      */
66     public function get_dbfamily() {
67         return 'mssql';
68     }
70     /**
71      * Returns more specific database driver type
72      * Note: can be used before connect()
73      * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
74      */
75     protected function get_dbtype() {
76         return 'mssql';
77     }
79     /**
80      * Returns general database library name
81      * Note: can be used before connect()
82      * @return string db type pdo, native
83      */
84     protected function get_dblibrary() {
85         return 'native';
86     }
88     /**
89      * Returns localised database type name
90      * Note: can be used before connect()
91      * @return string
92      */
93     public function get_name() {
94         return get_string('nativemssql', 'install');
95     }
97     /**
98      * Returns localised database configuration help.
99      * Note: can be used before connect()
100      * @return string
101      */
102     public function get_configuration_help() {
103         return get_string('nativemssqlhelp', 'install');
104     }
106     /**
107      * Diagnose database and tables, this function is used
108      * to verify database and driver settings, db engine types, etc.
109      *
110      * @return string null means everything ok, string means problem found.
111      */
112     public function diagnose() {
113         // Verify the database is running with READ_COMMITTED_SNAPSHOT enabled.
114         // (that's required to get snapshots/row versioning on READ_COMMITED mode).
115         $correctrcsmode = false;
116         $sql = "SELECT is_read_committed_snapshot_on
117                   FROM sys.databases
118                  WHERE name = '{$this->dbname}'";
119         $this->query_start($sql, null, SQL_QUERY_AUX);
120         $result = mssql_query($sql, $this->mssql);
121         $this->query_end($result);
122         if ($result) {
123             if ($row = mssql_fetch_assoc($result)) {
124                 $correctrcsmode = (bool)reset($row);
125             }
126         }
127         $this->free_result($result);
129         if (!$correctrcsmode) {
130             return get_string('mssqlrcsmodemissing', 'error');
131         }
133         // Arrived here, all right.
134         return null;
135     }
137     /**
138      * Connect to db
139      * Must be called before other methods.
140      * @param string $dbhost The database host.
141      * @param string $dbuser The database username.
142      * @param string $dbpass The database username's password.
143      * @param string $dbname The name of the database being connected to.
144      * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
145      * @param array $dboptions driver specific options
146      * @return bool true
147      * @throws dml_connection_exception if error
148      */
149     public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
150         if ($prefix == '' and !$this->external) {
151             //Enforce prefixes for everybody but mysql
152             throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
153         }
155         $driverstatus = $this->driver_installed();
157         if ($driverstatus !== true) {
158             throw new dml_exception('dbdriverproblem', $driverstatus);
159         }
161         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
163         $dbhost = $this->dbhost;
164         // Zero shouldn't be used as a port number so doing a check with empty() should be fine.
165         if (!empty($dboptions['dbport'])) {
166             if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) {
167                 $dbhost .= ','.$dboptions['dbport'];
168             } else {
169                 $dbhost .= ':'.$dboptions['dbport'];
170             }
171         }
172         ob_start();
173         if (!empty($this->dboptions['dbpersist'])) { // persistent connection
174             $this->mssql = mssql_pconnect($dbhost, $this->dbuser, $this->dbpass, true);
175         } else {
176             $this->mssql = mssql_connect($dbhost, $this->dbuser, $this->dbpass, true);
177         }
178         $dberr = ob_get_contents();
179         ob_end_clean();
181         if ($this->mssql === false) {
182             $this->mssql = null;
183             throw new dml_connection_exception($dberr);
184         }
186         // Disable logging until we are fully setup.
187         $this->query_log_prevent();
189         // already connected, select database and set some env. variables
190         $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX);
191         $result = mssql_select_db($this->dbname, $this->mssql);
192         $this->query_end($result);
194         // No need to set charset. It's UTF8, with transparent conversions
195         // back and forth performed both by FreeTDS or ODBTP
197         // Allow quoted identifiers
198         $sql = "SET QUOTED_IDENTIFIER ON";
199         $this->query_start($sql, null, SQL_QUERY_AUX);
200         $result = mssql_query($sql, $this->mssql);
201         $this->query_end($result);
203         $this->free_result($result);
205         // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL
206         // instead of equal(=) and distinct(<>) symbols
207         $sql = "SET ANSI_NULLS ON";
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         // Force ANSI warnings so arithmetic/string overflows will be
215         // returning error instead of transparently truncating data
216         $sql = "SET ANSI_WARNINGS ON";
217         $this->query_start($sql, null, SQL_QUERY_AUX);
218         $result = mssql_query($sql, $this->mssql);
219         $this->query_end($result);
221         // Concatenating null with anything MUST return NULL
222         $sql = "SET CONCAT_NULL_YIELDS_NULL  ON";
223         $this->query_start($sql, null, SQL_QUERY_AUX);
224         $result = mssql_query($sql, $this->mssql);
225         $this->query_end($result);
227         $this->free_result($result);
229         // Set transactions isolation level to READ_COMMITTED
230         // prevents dirty reads when using transactions +
231         // is the default isolation level of MSSQL
232         // Requires database to run with READ_COMMITTED_SNAPSHOT ON
233         $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
234         $this->query_start($sql, NULL, SQL_QUERY_AUX);
235         $result = mssql_query($sql, $this->mssql);
236         $this->query_end($result);
238         $this->free_result($result);
240         $serverinfo = $this->get_server_info();
241         // Fetch/offset is supported staring from SQL Server 2012.
242         $this->supportsoffsetfetch = $serverinfo['version'] > '11';
244         // We can enable logging now.
245         $this->query_log_allow();
247         // Connection stabilised and configured, going to instantiate the temptables controller
248         $this->temptables = new mssql_native_moodle_temptables($this);
250         return true;
251     }
253     /**
254      * Close database connection and release all resources
255      * and memory (especially circular memory references).
256      * Do NOT use connect() again, create a new instance if needed.
257      */
258     public function dispose() {
259         parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
260         if ($this->mssql) {
261             mssql_close($this->mssql);
262             $this->mssql = null;
263         }
264     }
266     /**
267      * Called before each db query.
268      * @param string $sql
269      * @param array array of parameters
270      * @param int $type type of query
271      * @param mixed $extrainfo driver specific extra information
272      * @return void
273      */
274     protected function query_start($sql, array $params=null, $type, $extrainfo=null) {
275         parent::query_start($sql, $params, $type, $extrainfo);
276         // mssql driver tends to send debug to output, we do not need that ;-)
277         $this->last_error_reporting = error_reporting(0);
278     }
280     /**
281      * Called immediately after each db query.
282      * @param mixed db specific result
283      * @return void
284      */
285     protected function query_end($result) {
286         // reset original debug level
287         error_reporting($this->last_error_reporting);
288         parent::query_end($result);
289     }
291     /**
292      * Returns database server info array
293      * @return array Array containing 'description' and 'version' info
294      */
295     public function get_server_info() {
296         static $info;
297         if (!$info) {
298             $info = array();
299             $sql = 'sp_server_info 2';
300             $this->query_start($sql, null, SQL_QUERY_AUX);
301             $result = mssql_query($sql, $this->mssql);
302             $this->query_end($result);
303             $row = mssql_fetch_row($result);
304             $info['description'] = $row[2];
305             $this->free_result($result);
307             $sql = 'sp_server_info 500';
308             $this->query_start($sql, null, SQL_QUERY_AUX);
309             $result = mssql_query($sql, $this->mssql);
310             $this->query_end($result);
311             $row = mssql_fetch_row($result);
312             $info['version'] = $row[2];
313             $this->free_result($result);
314         }
315         return $info;
316     }
318     /**
319      * Converts short table name {tablename} to real table name
320      * supporting temp tables (#) if detected
321      *
322      * @param string sql
323      * @return string sql
324      */
325     protected function fix_table_names($sql) {
326         if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
327             foreach($matches[0] as $key=>$match) {
328                 $name = $matches[1][$key];
329                 if ($this->temptables->is_temptable($name)) {
330                     $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
331                 } else {
332                     $sql = str_replace($match, $this->prefix.$name, $sql);
333                 }
334             }
335         }
336         return $sql;
337     }
339     /**
340      * Returns supported query parameter types
341      * @return int bitmask of accepted SQL_PARAMS_*
342      */
343     protected function allowed_param_types() {
344         return SQL_PARAMS_QM; // Not really, but emulated, see emulate_bound_params()
345     }
347     /**
348      * Returns last error reported by database engine.
349      * @return string error message
350      */
351     public function get_last_error() {
352         return mssql_get_last_message();
353     }
355     /**
356      * Return tables in database WITHOUT current prefix
357      * @param bool $usecache if true, returns list of cached tables.
358      * @return array of table names in lowercase and without prefix
359      */
360     public function get_tables($usecache=true) {
361         if ($usecache and $this->tables !== null) {
362             return $this->tables;
363         }
364         $this->tables = array();
365         $sql = "SELECT table_name
366                   FROM INFORMATION_SCHEMA.TABLES
367                  WHERE table_name LIKE '$this->prefix%'
368                    AND table_type = 'BASE TABLE'";
369         $this->query_start($sql, null, SQL_QUERY_AUX);
370         $result = mssql_query($sql, $this->mssql);
371         $this->query_end($result);
373         if ($result) {
374             while ($row = mssql_fetch_row($result)) {
375                 $tablename = reset($row);
376                 if ($this->prefix !== false && $this->prefix !== '') {
377                     if (strpos($tablename, $this->prefix) !== 0) {
378                         continue;
379                     }
380                     $tablename = substr($tablename, strlen($this->prefix));
381                 }
382                 $this->tables[$tablename] = $tablename;
383             }
384             $this->free_result($result);
385         }
387         // Add the currently available temptables
388         $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
389         return $this->tables;
390     }
392     /**
393      * Return table indexes - everything lowercased.
394      * @param string $table The table we want to get indexes from.
395      * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
396      */
397     public function get_indexes($table) {
398         $indexes = array();
399         $tablename = $this->prefix.$table;
401         // Indexes aren't covered by information_schema metatables, so we need to
402         // go to sys ones. Skipping primary key indexes on purpose.
403         $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name
404                   FROM sys.indexes i
405                   JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
406                   JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
407                   JOIN sys.tables t ON i.object_id = t.object_id
408                  WHERE t.name = '$tablename'
409                    AND i.is_primary_key = 0
410               ORDER BY i.name, i.index_id, ic.index_column_id";
412         $this->query_start($sql, null, SQL_QUERY_AUX);
413         $result = mssql_query($sql, $this->mssql);
414         $this->query_end($result);
416         if ($result) {
417             $lastindex = '';
418             $unique = false;
419             $columns = array();
420             while ($row = mssql_fetch_assoc($result)) {
421                 if ($lastindex and $lastindex != $row['index_name']) { // Save lastindex to $indexes and reset info
422                     $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
423                     $unique = false;
424                     $columns = array();
425                 }
426                 $lastindex = $row['index_name'];
427                 $unique = empty($row['is_unique']) ? false : true;
428                 $columns[] = $row['column_name'];
429             }
430             if ($lastindex ) { // Add the last one if exists
431                 $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
432             }
433             $this->free_result($result);
434         }
435         return $indexes;
436     }
438     /**
439      * Returns datailed information about columns in table. This information is cached internally.
440      * @param string $table name
441      * @param bool $usecache
442      * @return array array of database_column_info objects indexed with column names
443      */
444     public function get_columns($table, $usecache=true) {
446         if ($usecache) {
447             if ($this->temptables->is_temptable($table)) {
448                 if ($data = $this->get_temp_tables_cache()->get($table)) {
449                     return $data;
450                 }
451             } else {
452                 if ($data = $this->get_metacache()->get($table)) {
453                     return $data;
454                 }
455             }
456         }
458         $structure = array();
460         if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema
461             $sql = "SELECT column_name AS name,
462                            data_type AS type,
463                            numeric_precision AS max_length,
464                            character_maximum_length AS char_max_length,
465                            numeric_scale AS scale,
466                            is_nullable AS is_nullable,
467                            columnproperty(object_id(quotename(table_schema) + '.' +
468                                quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
469                            column_default AS default_value
470                       FROM INFORMATION_SCHEMA.COLUMNS
471                      WHERE table_name = '{" . $table . "}'
472                   ORDER BY ordinal_position";
473         } else { // temp table, get metadata from tempdb schema
474             $sql = "SELECT column_name AS name,
475                            data_type AS type,
476                            numeric_precision AS max_length,
477                            character_maximum_length AS char_max_length,
478                            numeric_scale AS scale,
479                            is_nullable AS is_nullable,
480                            columnproperty(object_id(quotename(table_schema) + '.' +
481                                quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
482                            column_default AS default_value
483                       FROM tempdb.INFORMATION_SCHEMA.COLUMNS
484                       JOIN tempdb..sysobjects ON name = table_name
485                      WHERE id = object_id('tempdb..{" . $table . "}')
486                   ORDER BY ordinal_position";
487         }
489         list($sql, $params, $type) = $this->fix_sql_params($sql, null);
491         $this->query_start($sql, null, SQL_QUERY_AUX);
492         $result = mssql_query($sql, $this->mssql);
493         $this->query_end($result);
495         if (!$result) {
496             return array();
497         }
499         while ($rawcolumn = mssql_fetch_assoc($result)) {
501             $rawcolumn = (object)$rawcolumn;
503             $info = new stdClass();
504             $info->name = $rawcolumn->name;
505             $info->type = $rawcolumn->type;
506             $info->meta_type = $this->mssqltype2moodletype($info->type);
508             // Prepare auto_increment info
509             $info->auto_increment = $rawcolumn->auto_increment ? true : false;
511             // Define type for auto_increment columns
512             $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type;
514             // id columns being auto_incremnt are PK by definition
515             $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
517             if ($info->meta_type === 'C' and $rawcolumn->char_max_length == -1) {
518                 // This is NVARCHAR(MAX), not a normal NVARCHAR.
519                 $info->max_length = -1;
520                 $info->meta_type = 'X';
521             } else {
522                 // Put correct length for character and LOB types
523                 $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
524                 $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
525             }
527             // Scale
528             $info->scale = $rawcolumn->scale;
530             // Prepare not_null info
531             $info->not_null = $rawcolumn->is_nullable == 'NO'  ? true : false;
533             // Process defaults
534             $info->has_default = !empty($rawcolumn->default_value);
535             if ($rawcolumn->default_value === NULL) {
536                 $info->default_value = NULL;
537             } else {
538                 $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value);
539             }
541             // Process binary
542             $info->binary = $info->meta_type == 'B' ? true : false;
544             $structure[$info->name] = new database_column_info($info);
545         }
546         $this->free_result($result);
548         if ($usecache) {
549             if ($this->temptables->is_temptable($table)) {
550                 $this->get_temp_tables_cache()->set($table, $structure);
551             } else {
552                 $this->get_metacache()->set($table, $structure);
553             }
554         }
556         return $structure;
557     }
559     /**
560      * Normalise values based on varying RDBMS's dependencies (booleans, LOBs...)
561      *
562      * @param database_column_info $column column metadata corresponding with the value we are going to normalise
563      * @param mixed $value value we are going to normalise
564      * @return mixed the normalised value
565      */
566     protected function normalise_value($column, $value) {
567         $this->detect_objects($value);
569         if (is_bool($value)) { // Always, convert boolean to int
570             $value = (int)$value;
571         } // And continue processing because text columns with numeric info need special handling below
573         if ($column->meta_type == 'B') {   // BLOBs need to be properly "packed", but can be inserted directly if so.
574             if (!is_null($value)) {               // If value not null, unpack it to unquoted hexadecimal byte-string format
575                 $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it
576             }                                     // easily and "bind" the param ok.
578         } else if ($column->meta_type == 'X') {             // MSSQL doesn't cast from int to text, so if text column
579             if (is_numeric($value)) {                       // and is numeric value then cast to string
580                 $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how
581             }                                               // to "bind" the param ok, avoiding reverse conversion to number
583         } else if ($value === '') {
584             if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
585                 $value = 0; // prevent '' problems in numeric fields
586             }
587         }
588         return $value;
589     }
591     /**
592      * Selectively call mssql_free_result(), avoiding some warnings without using the horrible @
593      *
594      * @param mssql_resource $resource resource to be freed if possible
595      */
596     private function free_result($resource) {
597         if (!is_bool($resource)) { // true/false resources cannot be freed
598             mssql_free_result($resource);
599         }
600     }
602     /**
603      * Provides mapping between mssql native data types and moodle_database - database_column_info - ones)
604      *
605      * @param string $mssql_type native mssql data type
606      * @return string 1-char database_column_info data type
607      */
608     private function mssqltype2moodletype($mssql_type) {
609         $type = null;
610         switch (strtoupper($mssql_type)) {
611             case 'BIT':
612                 $type = 'L';
613                 break;
614             case 'INT':
615             case 'SMALLINT':
616             case 'INTEGER':
617             case 'BIGINT':
618                 $type = 'I';
619                 break;
620             case 'DECIMAL':
621             case 'REAL':
622             case 'FLOAT':
623                 $type = 'N';
624                 break;
625             case 'VARCHAR':
626             case 'NVARCHAR':
627                 $type = 'C';
628                 break;
629             case 'TEXT':
630             case 'NTEXT':
631             case 'VARCHAR(MAX)':
632             case 'NVARCHAR(MAX)':
633                 $type = 'X';
634                 break;
635             case 'IMAGE':
636             case 'VARBINARY':
637             case 'VARBINARY(MAX)':
638                 $type = 'B';
639                 break;
640             case 'DATETIME':
641                 $type = 'D';
642                 break;
643         }
644         if (!$type) {
645             throw new dml_exception('invalidmssqlnativetype', $mssql_type);
646         }
647         return $type;
648     }
650     /**
651      * Do NOT use in code, to be used by database_manager only!
652      * @param string|array $sql query
653      * @param array|null $tablenames an array of xmldb table names affected by this request.
654      * @return bool true
655      * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
656      */
657     public function change_database_structure($sql, $tablenames = null) {
658         $this->get_manager(); // Includes DDL exceptions classes ;-)
659         $sqls = (array)$sql;
661         try {
662             foreach ($sqls as $sql) {
663                 $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
664                 $result = mssql_query($sql, $this->mssql);
665                 $this->query_end($result);
666             }
667         } catch (ddl_change_structure_exception $e) {
668             $this->reset_caches($tablenames);
669             throw $e;
670         }
672         $this->reset_caches($tablenames);
673         return true;
674     }
676     /**
677      * Very ugly hack which emulates bound parameters in queries
678      * because the mssql driver doesn't support placeholders natively at all
679      */
680     protected function emulate_bound_params($sql, array $params=null) {
681         if (empty($params)) {
682             return $sql;
683         }
684         // ok, we have verified sql statement with ? and correct number of params
685         $parts = array_reverse(explode('?', $sql));
686         $return = array_pop($parts);
687         foreach ($params as $param) {
688             if (is_bool($param)) {
689                 $return .= (int)$param;
691             } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially
692                 $return .= '0x' . $param['hex'];
694             } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not*
695                 $return .= "N'{$param['numstr']}'";                   // be converted back to number params, but bound as strings
697             } else if (is_null($param)) {
698                 $return .= 'NULL';
700             } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646
701                 $return .= "'".$param."'"; //fix for MDL-24863 to prevent auto-cast to int.
703             } else if (is_float($param)) {
704                 $return .= $param;
706             } else {
707                 $param = str_replace("'", "''", $param);
708                 $param = str_replace("\0", "", $param);
709                 $return .= "N'$param'";
710             }
712             $return .= array_pop($parts);
713         }
714         return $return;
715     }
717     /**
718      * Execute general sql query. Should be used only when no other method suitable.
719      * Do NOT use this to make changes in db structure, use database_manager methods instead!
720      * @param string $sql query
721      * @param array $params query parameters
722      * @return bool true
723      * @throws dml_exception A DML specific exception is thrown for any errors.
724      */
725     public function execute($sql, array $params=null) {
727         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
728         $rawsql = $this->emulate_bound_params($sql, $params);
730         if (strpos($sql, ';') !== false) {
731             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
732         }
734         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
735         $result = mssql_query($rawsql, $this->mssql);
736         $this->query_end($result);
737         $this->free_result($result);
739         return true;
740     }
742     /**
743      * Get a number of records as a moodle_recordset using a SQL statement.
744      *
745      * Since this method is a little less readable, use of it should be restricted to
746      * code where it's possible there might be large datasets being returned.  For known
747      * small datasets use get_records_sql - it leads to simpler code.
748      *
749      * The return type is like:
750      * @see function get_recordset.
751      *
752      * @param string $sql the SQL select query to execute.
753      * @param array $params array of sql parameters
754      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
755      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
756      * @return moodle_recordset instance
757      * @throws dml_exception A DML specific exception is thrown for any errors.
758      */
759     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
761         list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
763         if ($limitfrom or $limitnum) {
764             if (!$this->supportsoffsetfetch) {
765                 if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later).
766                     $fetch = $limitfrom + $limitnum;
767                     if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow.
768                         $fetch = PHP_INT_MAX;
769                     }
770                     $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
771                                         "\\1SELECT\\2 TOP $fetch", $sql);
772                 }
773             } else {
774                 $sql = (substr($sql, -1) === ';') ? substr($sql, 0, -1) : $sql;
775                 // We need order by to use FETCH/OFFSET.
776                 // Ordering by first column shouldn't break anything if there was no order in the first place.
777                 if (!strpos(strtoupper($sql), "ORDER BY")) {
778                     $sql .= " ORDER BY 1";
779                 }
781                 $sql .= " OFFSET ".$limitfrom." ROWS ";
783                 if ($limitnum > 0) {
784                     $sql .= " FETCH NEXT ".$limitnum." ROWS ONLY";
785                 }
786             }
787         }
789         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
790         $rawsql = $this->emulate_bound_params($sql, $params);
792         $this->query_start($sql, $params, SQL_QUERY_SELECT);
793         $result = mssql_query($rawsql, $this->mssql);
794         $this->query_end($result);
796         if ($limitfrom && !$this->supportsoffsetfetch) { // Skip $limitfrom records.
797             if (!@mssql_data_seek($result, $limitfrom)) {
798                 // Nothing, most probably seek past the end.
799                 mssql_free_result($result);
800                 $result = null;
801             }
802         }
804         return $this->create_recordset($result);
805     }
807     protected function create_recordset($result) {
808         return new mssql_native_moodle_recordset($result);
809     }
811     /**
812      * Get a number of records as an array of objects using a SQL statement.
813      *
814      * Return value is like:
815      * @see function get_records.
816      *
817      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
818      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
819      *   returned array.
820      * @param array $params array of sql parameters
821      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
822      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
823      * @return array of objects, or empty array if no records were found
824      * @throws dml_exception A DML specific exception is thrown for any errors.
825      */
826     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
828         $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
830         $results = array();
832         foreach ($rs as $row) {
833             $id = reset($row);
834             if (isset($results[$id])) {
835                 $colname = key($row);
836                 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);
837             }
838             $results[$id] = $row;
839         }
840         $rs->close();
842         return $results;
843     }
845     /**
846      * Selects records and return values (first field) as an array using a SQL statement.
847      *
848      * @param string $sql The SQL query
849      * @param array $params array of sql parameters
850      * @return array of values
851      * @throws dml_exception A DML specific exception is thrown for any errors.
852      */
853     public function get_fieldset_sql($sql, array $params=null) {
855         $rs = $this->get_recordset_sql($sql, $params);
857         $results = array();
859         foreach ($rs as $row) {
860             $results[] = reset($row);
861         }
862         $rs->close();
864         return $results;
865     }
867     /**
868      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
869      * @param string $table name
870      * @param mixed $params data record as object or array
871      * @param bool $returnit return it of inserted record
872      * @param bool $bulk true means repeated inserts expected
873      * @param bool $customsequence true if 'id' included in $params, disables $returnid
874      * @return bool|int true or new id
875      * @throws dml_exception A DML specific exception is thrown for any errors.
876      */
877     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
878         if (!is_array($params)) {
879             $params = (array)$params;
880         }
882         $returning = "";
883         $isidentity = false;
885         if ($customsequence) {
886             if (!isset($params['id'])) {
887                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
888             }
889             $returnid = false;
891             $columns = $this->get_columns($table);
892             if (isset($columns['id']) and $columns['id']->auto_increment) {
893                 $isidentity = true;
894             }
896             // Disable IDENTITY column before inserting record with id, only if the
897             // column is identity, from meta information.
898             if ($isidentity) {
899                 $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
900                 list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
901                 $this->query_start($sql, null, SQL_QUERY_AUX);
902                 $result = mssql_query($sql, $this->mssql);
903                 $this->query_end($result);
904                 $this->free_result($result);
905             }
907         } else {
908             unset($params['id']);
909             if ($returnid) {
910                 $returning = "OUTPUT inserted.id";
911             }
912         }
914         if (empty($params)) {
915             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
916         }
918         $fields = implode(',', array_keys($params));
919         $qms    = array_fill(0, count($params), '?');
920         $qms    = implode(',', $qms);
922         $sql = "INSERT INTO {" . $table . "} ($fields) $returning VALUES ($qms)";
924         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
925         $rawsql = $this->emulate_bound_params($sql, $params);
927         $this->query_start($sql, $params, SQL_QUERY_INSERT);
928         $result = mssql_query($rawsql, $this->mssql);
929         // Expected results are:
930         //     - true: insert ok and there isn't returned information.
931         //     - false: insert failed and there isn't returned information.
932         //     - resource: insert executed, need to look for returned (output)
933         //           values to know if the insert was ok or no. Posible values
934         //           are false = failed, integer = insert ok, id returned.
935         $end = false;
936         if (is_bool($result)) {
937             $end = $result;
938         } else if (is_resource($result)) {
939             $end = mssql_result($result, 0, 0); // Fetch 1st column from 1st row.
940         }
941         $this->query_end($end); // End the query with the calculated $end.
943         if ($returning !== "") {
944             $params['id'] = $end;
945         }
946         $this->free_result($result);
948         if ($customsequence) {
949             // Enable IDENTITY column after inserting record with id, only if the
950             // column is identity, from meta information.
951             if ($isidentity) {
952                 $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
953                 list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
954                 $this->query_start($sql, null, SQL_QUERY_AUX);
955                 $result = mssql_query($sql, $this->mssql);
956                 $this->query_end($result);
957                 $this->free_result($result);
958             }
959         }
961         if (!$returnid) {
962             return true;
963         }
965         return (int)$params['id'];
966     }
968     /**
969      * Insert a record into a table and return the "id" field if required.
970      *
971      * Some conversions and safety checks are carried out. Lobs are supported.
972      * If the return ID isn't required, then this just reports success as true/false.
973      * $data is an object containing needed data
974      * @param string $table The database table to be inserted into
975      * @param object $data A data object with values for one or more fields in the record
976      * @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.
977      * @return bool|int true or new id
978      * @throws dml_exception A DML specific exception is thrown for any errors.
979      */
980     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
981         $dataobject = (array)$dataobject;
983         $columns = $this->get_columns($table);
984         if (empty($columns)) {
985             throw new dml_exception('ddltablenotexist', $table);
986         }
987         $cleaned = array();
989         foreach ($dataobject as $field => $value) {
990             if ($field === 'id') {
991                 continue;
992             }
993             if (!isset($columns[$field])) {
994                 continue;
995             }
996             $column = $columns[$field];
997             $cleaned[$field] = $this->normalise_value($column, $value);
998         }
1000         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
1001     }
1003     /**
1004      * Import a record into a table, id field is required.
1005      * Safety checks are NOT carried out. Lobs are supported.
1006      *
1007      * @param string $table name of database table to be inserted into
1008      * @param object $dataobject A data object with values for one or more fields in the record
1009      * @return bool true
1010      * @throws dml_exception A DML specific exception is thrown for any errors.
1011      */
1012     public function import_record($table, $dataobject) {
1013         $dataobject = (array)$dataobject;
1015         $columns = $this->get_columns($table);
1016         $cleaned = array();
1018         foreach ($dataobject as $field => $value) {
1019             if (!isset($columns[$field])) {
1020                 continue;
1021             }
1022             $column = $columns[$field];
1023             $cleaned[$field] = $this->normalise_value($column, $value);
1024         }
1026         $this->insert_record_raw($table, $cleaned, false, false, true);
1028         return true;
1029     }
1031     /**
1032      * Update record in database, as fast as possible, no safety checks, lobs not supported.
1033      * @param string $table name
1034      * @param mixed $params data record as object or array
1035      * @param bool true means repeated updates expected
1036      * @return bool true
1037      * @throws dml_exception A DML specific exception is thrown for any errors.
1038      */
1039     public function update_record_raw($table, $params, $bulk=false) {
1040         $params = (array)$params;
1042         if (!isset($params['id'])) {
1043             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1044         }
1045         $id = $params['id'];
1046         unset($params['id']);
1048         if (empty($params)) {
1049             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1050         }
1052         $sets = array();
1053         foreach ($params as $field=>$value) {
1054             $sets[] = "$field = ?";
1055         }
1057         $params[] = $id; // last ? in WHERE condition
1059         $sets = implode(',', $sets);
1060         $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?";
1062         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1063         $rawsql = $this->emulate_bound_params($sql, $params);
1065         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1066         $result = mssql_query($rawsql, $this->mssql);
1067         $this->query_end($result);
1069         $this->free_result($result);
1070         return true;
1071     }
1073     /**
1074      * Update a record in a table
1075      *
1076      * $dataobject is an object containing needed data
1077      * Relies on $dataobject having a variable "id" to
1078      * specify the record to update
1079      *
1080      * @param string $table The database table to be checked against.
1081      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1082      * @param bool true means repeated updates expected
1083      * @return bool true
1084      * @throws dml_exception A DML specific exception is thrown for any errors.
1085      */
1086     public function update_record($table, $dataobject, $bulk=false) {
1087         $dataobject = (array)$dataobject;
1089         $columns = $this->get_columns($table);
1090         $cleaned = array();
1092         foreach ($dataobject as $field => $value) {
1093             if (!isset($columns[$field])) {
1094                 continue;
1095             }
1096             $column = $columns[$field];
1097             $cleaned[$field] = $this->normalise_value($column, $value);
1098         }
1100         return $this->update_record_raw($table, $cleaned, $bulk);
1101     }
1103     /**
1104      * Set a single field in every table record which match a particular WHERE clause.
1105      *
1106      * @param string $table The database table to be checked against.
1107      * @param string $newfield the field to set.
1108      * @param string $newvalue the value to set the field to.
1109      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1110      * @param array $params array of sql parameters
1111      * @return bool true
1112      * @throws dml_exception A DML specific exception is thrown for any errors.
1113      */
1114     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1116         if ($select) {
1117             $select = "WHERE $select";
1118         }
1119         if (is_null($params)) {
1120             $params = array();
1121         }
1123         // convert params to ? types
1124         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1126         // Get column metadata
1127         $columns = $this->get_columns($table);
1128         $column = $columns[$newfield];
1130         $newvalue = $this->normalise_value($column, $newvalue);
1132         if (is_null($newvalue)) {
1133             $newfield = "$newfield = NULL";
1134         } else {
1135             $newfield = "$newfield = ?";
1136             array_unshift($params, $newvalue);
1137         }
1138         $sql = "UPDATE {" . $table . "} SET $newfield $select";
1140         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1141         $rawsql = $this->emulate_bound_params($sql, $params);
1143         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1144         $result = mssql_query($rawsql, $this->mssql);
1145         $this->query_end($result);
1147         $this->free_result($result);
1149         return true;
1150     }
1152     /**
1153      * Delete one or more records from a table which match a particular WHERE clause.
1154      *
1155      * @param string $table The database table to be checked against.
1156      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1157      * @param array $params array of sql parameters
1158      * @return bool true
1159      * @throws dml_exception A DML specific exception is thrown for any errors.
1160      */
1161     public function delete_records_select($table, $select, array $params=null) {
1163         if ($select) {
1164             $select = "WHERE $select";
1165         }
1167         $sql = "DELETE FROM {" . $table . "} $select";
1169         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1170         $rawsql = $this->emulate_bound_params($sql, $params);
1172         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1173         $result = mssql_query($rawsql, $this->mssql);
1174         $this->query_end($result);
1176         $this->free_result($result);
1178         return true;
1179     }
1181     public function sql_cast_char2int($fieldname, $text=false) {
1182         if (!$text) {
1183             return ' CAST(' . $fieldname . ' AS INT) ';
1184         } else {
1185             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1186         }
1187     }
1189     public function sql_cast_char2real($fieldname, $text=false) {
1190         if (!$text) {
1191             return ' CAST(' . $fieldname . ' AS REAL) ';
1192         } else {
1193             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) ';
1194         }
1195     }
1197     public function sql_ceil($fieldname) {
1198         return ' CEILING(' . $fieldname . ')';
1199     }
1202     protected function get_collation() {
1203         if (isset($this->collation)) {
1204             return $this->collation;
1205         }
1206         if (!empty($this->dboptions['dbcollation'])) {
1207             // perf speedup
1208             $this->collation = $this->dboptions['dbcollation'];
1209             return $this->collation;
1210         }
1212         // make some default
1213         $this->collation = 'Latin1_General_CI_AI';
1215         $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation";
1216         $this->query_start($sql, null, SQL_QUERY_AUX);
1217         $result = mssql_query($sql, $this->mssql);
1218         $this->query_end($result);
1220         if ($result) {
1221             if ($rawcolumn = mssql_fetch_assoc($result)) {
1222                 $this->collation = reset($rawcolumn);
1223             }
1224             $this->free_result($result);
1225         }
1227         return $this->collation;
1228     }
1230     public function sql_equal($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notequal = false) {
1231         $equalop = $notequal ? '<>' : '=';
1232         $collation = $this->get_collation();
1234         if ($casesensitive) {
1235             $collation = str_replace('_CI', '_CS', $collation);
1236         } else {
1237             $collation = str_replace('_CS', '_CI', $collation);
1238         }
1239         if ($accentsensitive) {
1240             $collation = str_replace('_AI', '_AS', $collation);
1241         } else {
1242             $collation = str_replace('_AS', '_AI', $collation);
1243         }
1245         return "$fieldname COLLATE $collation $equalop $param";
1246     }
1248     /**
1249      * Returns 'LIKE' part of a query.
1250      *
1251      * @param string $fieldname usually name of the table column
1252      * @param string $param usually bound query parameter (?, :named)
1253      * @param bool $casesensitive use case sensitive search
1254      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1255      * @param bool $notlike true means "NOT LIKE"
1256      * @param string $escapechar escape char for '%' and '_'
1257      * @return string SQL code fragment
1258      */
1259     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1260         if (strpos($param, '%') !== false) {
1261             debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1262         }
1264         $collation = $this->get_collation();
1266         if ($casesensitive) {
1267             $collation = str_replace('_CI', '_CS', $collation);
1268         } else {
1269             $collation = str_replace('_CS', '_CI', $collation);
1270         }
1271         if ($accentsensitive) {
1272             $collation = str_replace('_AI', '_AS', $collation);
1273         } else {
1274             $collation = str_replace('_AS', '_AI', $collation);
1275         }
1277         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1279         return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'";
1280     }
1282     public function sql_concat() {
1283         $arr = func_get_args();
1284         foreach ($arr as $key => $ele) {
1285             $arr[$key] = ' CAST(' . $ele . ' AS NVARCHAR(255)) ';
1286         }
1287         $s = implode(' + ', $arr);
1288         if ($s === '') {
1289             return " '' ";
1290         }
1291         return " $s ";
1292     }
1294     public function sql_concat_join($separator="' '", $elements=array()) {
1295         for ($n=count($elements)-1; $n > 0 ; $n--) {
1296             array_splice($elements, $n, 0, $separator);
1297         }
1298         return call_user_func_array(array($this, 'sql_concat'), $elements);
1299     }
1301    public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1302         if ($textfield) {
1303             return ' (' . $this->sql_compare_text($fieldname) . " = '') ";
1304         } else {
1305             return " ($fieldname = '') ";
1306         }
1307     }
1309    /**
1310      * Returns the SQL text to be used to calculate the length in characters of one expression.
1311      * @param string fieldname or expression to calculate its length in characters.
1312      * @return string the piece of SQL code to be used in the statement.
1313      */
1314     public function sql_length($fieldname) {
1315         return ' LEN(' . $fieldname . ')';
1316     }
1318     public function sql_order_by_text($fieldname, $numchars=32) {
1319         return " CONVERT(varchar({$numchars}), {$fieldname})";
1320     }
1322    /**
1323      * Returns the SQL for returning searching one string for the location of another.
1324      */
1325     public function sql_position($needle, $haystack) {
1326         return "CHARINDEX(($needle), ($haystack))";
1327     }
1329     /**
1330      * Returns the proper substr() SQL text used to extract substrings from DB
1331      * NOTE: this was originally returning only function name
1332      *
1333      * @param string $expr some string field, no aggregates
1334      * @param mixed $start integer or expression evaluating to int
1335      * @param mixed $length optional integer or expression evaluating to int
1336      * @return string sql fragment
1337      */
1338     public function sql_substr($expr, $start, $length=false) {
1339         if (count(func_get_args()) < 2) {
1340             throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa
1341 s only returning name of SQL substring function, it now requires all parameters.');
1342         }
1343         if ($length === false) {
1344             return "SUBSTRING($expr, " . $this->sql_cast_char2int($start) . ", 2^31-1)";
1345         } else {
1346             return "SUBSTRING($expr, " . $this->sql_cast_char2int($start) . ", " . $this->sql_cast_char2int($length) . ")";
1347         }
1348     }
1350     /**
1351      * Does this driver support tool_replace?
1352      *
1353      * @since Moodle 2.6.1
1354      * @return bool
1355      */
1356     public function replace_all_text_supported() {
1357         return true;
1358     }
1360     public function session_lock_supported() {
1361         return true;
1362     }
1364     /**
1365      * Obtain session lock
1366      * @param int $rowid id of the row with session record
1367      * @param int $timeout max allowed time to wait for the lock in seconds
1368      * @return bool success
1369      */
1370     public function get_session_lock($rowid, $timeout) {
1371         if (!$this->session_lock_supported()) {
1372             return;
1373         }
1374         parent::get_session_lock($rowid, $timeout);
1376         $timeoutmilli = $timeout * 1000;
1378         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1379         // There is one bug in PHP/freetds (both reproducible with mssql_query()
1380         // and its mssql_init()/mssql_bind()/mssql_execute() alternative) for
1381         // stored procedures, causing scalar results of the execution
1382         // to be cast to boolean (true/fals). Here there is one
1383         // workaround that forces the return of one recordset resource.
1384         // $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session',  $timeoutmilli";
1385         $sql = "BEGIN
1386                     DECLARE @result INT
1387                     EXECUTE @result = sp_getapplock @Resource='$fullname',
1388                                                     @LockMode='Exclusive',
1389                                                     @LockOwner='Session',
1390                                                     @LockTimeout='$timeoutmilli'
1391                     SELECT @result
1392                 END";
1393         $this->query_start($sql, null, SQL_QUERY_AUX);
1394         $result = mssql_query($sql, $this->mssql);
1395         $this->query_end($result);
1397         if ($result) {
1398             $row = mssql_fetch_row($result);
1399             if ($row[0] < 0) {
1400                 throw new dml_sessionwait_exception();
1401             }
1402         }
1404         $this->free_result($result);
1405     }
1407     public function release_session_lock($rowid) {
1408         if (!$this->session_lock_supported()) {
1409             return;
1410         }
1411         if (!$this->used_for_db_sessions) {
1412             return;
1413         }
1415         parent::release_session_lock($rowid);
1417         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1418         $sql = "sp_releaseapplock '$fullname', 'Session'";
1419         $this->query_start($sql, null, SQL_QUERY_AUX);
1420         $result = mssql_query($sql, $this->mssql);
1421         $this->query_end($result);
1423         $this->free_result($result);
1424     }
1426     /**
1427      * Driver specific start of real database transaction,
1428      * this can not be used directly in code.
1429      * @return void
1430      */
1431     protected function begin_transaction() {
1432         // requires database to run with READ_COMMITTED_SNAPSHOT ON
1433         $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation
1434         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1435         $result = mssql_query($sql, $this->mssql);
1436         $this->query_end($result);
1438         $this->free_result($result);
1439     }
1441     /**
1442      * Driver specific commit of real database transaction,
1443      * this can not be used directly in code.
1444      * @return void
1445      */
1446     protected function commit_transaction() {
1447         $sql = "COMMIT TRANSACTION";
1448         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1449         $result = mssql_query($sql, $this->mssql);
1450         $this->query_end($result);
1452         $this->free_result($result);
1453     }
1455     /**
1456      * Driver specific abort of real database transaction,
1457      * this can not be used directly in code.
1458      * @return void
1459      */
1460     protected function rollback_transaction() {
1461         $sql = "ROLLBACK TRANSACTION";
1462         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1463         $result = mssql_query($sql, $this->mssql);
1464         $this->query_end($result);
1466         $this->free_result($result);
1467     }