MDL-40266 improve emulate_bound_params() for mysqli
[moodle.git] / lib / dml / mysqli_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 mysqli class representing moodle database interface.
19  *
20  * @package    core_dml
21  * @copyright  2008 Petr Skoda (http://skodak.org)
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__.'/mysqli_native_moodle_recordset.php');
29 require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
31 /**
32  * Native mysqli class representing moodle database interface.
33  *
34  * @package    core_dml
35  * @copyright  2008 Petr Skoda (http://skodak.org)
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class mysqli_native_moodle_database extends moodle_database {
40     protected $mysqli = null;
42     private $transactions_supported = null;
44     /**
45      * Attempt to create the database
46      * @param string $dbhost
47      * @param string $dbuser
48      * @param string $dbpass
49      * @param string $dbname
50      * @return bool success
51      * @throws dml_exception A DML specific exception is thrown for any errors.
52      */
53     public function create_database($dbhost, $dbuser, $dbpass, $dbname, array $dboptions=null) {
54         $driverstatus = $this->driver_installed();
56         if ($driverstatus !== true) {
57             throw new dml_exception('dbdriverproblem', $driverstatus);
58         }
60         if (!empty($dboptions['dbsocket'])
61                 and (strpos($dboptions['dbsocket'], '/') !== false or strpos($dboptions['dbsocket'], '\\') !== false)) {
62             $dbsocket = $dboptions['dbsocket'];
63         } else {
64             $dbsocket = ini_get('mysqli.default_socket');
65         }
66         if (empty($dboptions['dbport'])) {
67             $dbport = (int)ini_get('mysqli.default_port');
68         } else {
69             $dbport = (int)$dboptions['dbport'];
70         }
71         // verify ini.get does not return nonsense
72         if (empty($dbport)) {
73             $dbport = 3306;
74         }
75         ob_start();
76         $conn = new mysqli($dbhost, $dbuser, $dbpass, '', $dbport, $dbsocket); // Connect without db
77         $dberr = ob_get_contents();
78         ob_end_clean();
79         $errorno = @$conn->connect_errno;
81         if ($errorno !== 0) {
82             throw new dml_connection_exception($dberr);
83         }
85         if (isset($dboptions['dbcollation']) and strpos($dboptions['dbcollation'], 'utf8_') === 0) {
86             $collation = $dboptions['dbcollation'];
87         } else {
88             $collation = 'utf8_unicode_ci';
89         }
91         $result = $conn->query("CREATE DATABASE $dbname DEFAULT CHARACTER SET utf8 DEFAULT COLLATE ".$collation);
93         $conn->close();
95         if (!$result) {
96             throw new dml_exception('cannotcreatedb');
97         }
99         return true;
100     }
102     /**
103      * Detects if all needed PHP stuff installed.
104      * Note: can be used before connect()
105      * @return mixed true if ok, string if something
106      */
107     public function driver_installed() {
108         if (!extension_loaded('mysqli')) {
109             return get_string('mysqliextensionisnotpresentinphp', 'install');
110         }
111         return true;
112     }
114     /**
115      * Returns database family type - describes SQL dialect
116      * Note: can be used before connect()
117      * @return string db family name (mysql, postgres, mssql, oracle, etc.)
118      */
119     public function get_dbfamily() {
120         return 'mysql';
121     }
123     /**
124      * Returns more specific database driver type
125      * Note: can be used before connect()
126      * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
127      */
128     protected function get_dbtype() {
129         return 'mysqli';
130     }
132     /**
133      * Returns general database library name
134      * Note: can be used before connect()
135      * @return string db type pdo, native
136      */
137     protected function get_dblibrary() {
138         return 'native';
139     }
141     /**
142      * Returns the current MySQL db engine.
143      *
144      * This is an ugly workaround for MySQL default engine problems,
145      * Moodle is designed to work best on ACID compliant databases
146      * with full transaction support. Do not use MyISAM.
147      *
148      * @return string or null MySQL engine name
149      */
150     public function get_dbengine() {
151         if (isset($this->dboptions['dbengine'])) {
152             return $this->dboptions['dbengine'];
153         }
155         if ($this->external) {
156             return null;
157         }
159         $engine = null;
161         // Look for current engine of our config table (the first table that gets created),
162         // so that we create all tables with the same engine.
163         $sql = "SELECT engine
164                   FROM INFORMATION_SCHEMA.TABLES
165                  WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config'";
166         $this->query_start($sql, NULL, SQL_QUERY_AUX);
167         $result = $this->mysqli->query($sql);
168         $this->query_end($result);
169         if ($rec = $result->fetch_assoc()) {
170             $engine = $rec['engine'];
171         }
172         $result->close();
174         if ($engine) {
175             // Cache the result to improve performance.
176             $this->dboptions['dbengine'] = $engine;
177             return $engine;
178         }
180         // get the default database engine
181         $sql = "SELECT @@storage_engine";
182         $this->query_start($sql, NULL, SQL_QUERY_AUX);
183         $result = $this->mysqli->query($sql);
184         $this->query_end($result);
185         if ($rec = $result->fetch_assoc()) {
186             $engine = $rec['@@storage_engine'];
187         }
188         $result->close();
190         if ($engine === 'MyISAM') {
191             // we really do not want MyISAM for Moodle, InnoDB or XtraDB is a reasonable defaults if supported
192             $sql = "SHOW STORAGE ENGINES";
193             $this->query_start($sql, NULL, SQL_QUERY_AUX);
194             $result = $this->mysqli->query($sql);
195             $this->query_end($result);
196             $engines = array();
197             while ($res = $result->fetch_assoc()) {
198                 if ($res['Support'] === 'YES' or $res['Support'] === 'DEFAULT') {
199                     $engines[$res['Engine']] = true;
200                 }
201             }
202             $result->close();
203             if (isset($engines['InnoDB'])) {
204                 $engine = 'InnoDB';
205             }
206             if (isset($engines['XtraDB'])) {
207                 $engine = 'XtraDB';
208             }
209         }
211         // Cache the result to improve performance.
212         $this->dboptions['dbengine'] = $engine;
213         return $engine;
214     }
216     /**
217      * Returns the current MySQL db collation.
218      *
219      * This is an ugly workaround for MySQL default collation problems.
220      *
221      * @return string or null MySQL collation name
222      */
223     public function get_dbcollation() {
224         if (isset($this->dboptions['dbcollation'])) {
225             return $this->dboptions['dbcollation'];
226         }
227         if ($this->external) {
228             return null;
229         }
231         $collation = null;
233         // Look for current collation of our config table (the first table that gets created),
234         // so that we create all tables with the same collation.
235         $sql = "SELECT collation_name
236                   FROM INFORMATION_SCHEMA.COLUMNS
237                  WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config' AND column_name = 'value'";
238         $this->query_start($sql, NULL, SQL_QUERY_AUX);
239         $result = $this->mysqli->query($sql);
240         $this->query_end($result);
241         if ($rec = $result->fetch_assoc()) {
242             $collation = $rec['collation_name'];
243         }
244         $result->close();
246         if (!$collation) {
247             // Get the default database collation, but only if using UTF-8.
248             $sql = "SELECT @@collation_database";
249             $this->query_start($sql, NULL, SQL_QUERY_AUX);
250             $result = $this->mysqli->query($sql);
251             $this->query_end($result);
252             if ($rec = $result->fetch_assoc()) {
253                 if (strpos($rec['@@collation_database'], 'utf8_') === 0) {
254                     $collation = $rec['@@collation_database'];
255                 }
256             }
257             $result->close();
258         }
260         if (!$collation) {
261             // We want only utf8 compatible collations.
262             $collation = null;
263             $sql = "SHOW COLLATION WHERE Collation LIKE 'utf8\_%' AND Charset = 'utf8'";
264             $this->query_start($sql, NULL, SQL_QUERY_AUX);
265             $result = $this->mysqli->query($sql);
266             $this->query_end($result);
267             while ($res = $result->fetch_assoc()) {
268                 $collation = $res['Collation'];
269                 if (strtoupper($res['Default']) === 'YES') {
270                     $collation = $res['Collation'];
271                     break;
272                 }
273             }
274             $result->close();
275         }
277         // Cache the result to improve performance.
278         $this->dboptions['dbcollation'] = $collation;
279         return $collation;
280     }
282     /**
283      * Returns localised database type name
284      * Note: can be used before connect()
285      * @return string
286      */
287     public function get_name() {
288         return get_string('nativemysqli', 'install');
289     }
291     /**
292      * Returns localised database configuration help.
293      * Note: can be used before connect()
294      * @return string
295      */
296     public function get_configuration_help() {
297         return get_string('nativemysqlihelp', 'install');
298     }
300     /**
301      * Returns localised database description
302      * Note: can be used before connect()
303      * @return string
304      */
305     public function get_configuration_hints() {
306         return get_string('databasesettingssub_mysqli', 'install');
307     }
309     /**
310      * Diagnose database and tables, this function is used
311      * to verify database and driver settings, db engine types, etc.
312      *
313      * @return string null means everything ok, string means problem found.
314      */
315     public function diagnose() {
316         $sloppymyisamfound = false;
317         $prefix = str_replace('_', '\\_', $this->prefix);
318         $sql = "SELECT COUNT('x')
319                   FROM INFORMATION_SCHEMA.TABLES
320                  WHERE table_schema = DATABASE()
321                        AND table_name LIKE BINARY '$prefix%'
322                        AND Engine = 'MyISAM'";
323         $this->query_start($sql, null, SQL_QUERY_AUX);
324         $result = $this->mysqli->query($sql);
325         $this->query_end($result);
326         if ($result) {
327             if ($arr = $result->fetch_assoc()) {
328                 $count = reset($arr);
329                 if ($count) {
330                     $sloppymyisamfound = true;
331                 }
332             }
333             $result->close();
334         }
336         if ($sloppymyisamfound) {
337             return get_string('myisamproblem', 'error');
338         } else {
339             return null;
340         }
341     }
343     /**
344      * Connect to db
345      * Must be called before other methods.
346      * @param string $dbhost The database host.
347      * @param string $dbuser The database username.
348      * @param string $dbpass The database username's password.
349      * @param string $dbname The name of the database being connected to.e
350      * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
351      * @param array $dboptions driver specific options
352      * @return bool success
353      */
354     public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
355         $driverstatus = $this->driver_installed();
357         if ($driverstatus !== true) {
358             throw new dml_exception('dbdriverproblem', $driverstatus);
359         }
361         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
363         // dbsocket is used ONLY if host is NULL or 'localhost',
364         // you can not disable it because it is always tried if dbhost is 'localhost'
365         if (!empty($this->dboptions['dbsocket'])
366                 and (strpos($this->dboptions['dbsocket'], '/') !== false or strpos($this->dboptions['dbsocket'], '\\') !== false)) {
367             $dbsocket = $this->dboptions['dbsocket'];
368         } else {
369             $dbsocket = ini_get('mysqli.default_socket');
370         }
371         if (empty($this->dboptions['dbport'])) {
372             $dbport = (int)ini_get('mysqli.default_port');
373         } else {
374             $dbport = (int)$this->dboptions['dbport'];
375         }
376         // verify ini.get does not return nonsense
377         if (empty($dbport)) {
378             $dbport = 3306;
379         }
380         if ($dbhost and !empty($this->dboptions['dbpersist'])) {
381             $dbhost = "p:$dbhost";
382         }
383         ob_start();
384         $this->mysqli = new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
385         $dberr = ob_get_contents();
386         ob_end_clean();
387         $errorno = @$this->mysqli->connect_errno;
389         if ($errorno !== 0) {
390             $this->mysqli = null;
391             throw new dml_connection_exception($dberr);
392         }
394         $this->query_start("--set_charset()", null, SQL_QUERY_AUX);
395         $this->mysqli->set_charset('utf8');
396         $this->query_end(true);
398         // If available, enforce strict mode for the session. That guaranties
399         // standard behaviour under some situations, avoiding some MySQL nasty
400         // habits like truncating data or performing some transparent cast losses.
401         // With strict mode enforced, Moodle DB layer will be consistently throwing
402         // the corresponding exceptions as expected.
403         $si = $this->get_server_info();
404         if (version_compare($si['version'], '5.0.2', '>=')) {
405             $sql = "SET SESSION sql_mode = 'STRICT_ALL_TABLES'";
406             $this->query_start($sql, null, SQL_QUERY_AUX);
407             $result = $this->mysqli->query($sql);
408             $this->query_end($result);
409         }
411         // Connection stabilised and configured, going to instantiate the temptables controller
412         $this->temptables = new mysqli_native_moodle_temptables($this);
414         return true;
415     }
417     /**
418      * Close database connection and release all resources
419      * and memory (especially circular memory references).
420      * Do NOT use connect() again, create a new instance if needed.
421      */
422     public function dispose() {
423         parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
424         if ($this->mysqli) {
425             $this->mysqli->close();
426             $this->mysqli = null;
427         }
428     }
430     /**
431      * Returns database server info array
432      * @return array Array containing 'description' and 'version' info
433      */
434     public function get_server_info() {
435         return array('description'=>$this->mysqli->server_info, 'version'=>$this->mysqli->server_info);
436     }
438     /**
439      * Returns supported query parameter types
440      * @return int bitmask of accepted SQL_PARAMS_*
441      */
442     protected function allowed_param_types() {
443         return SQL_PARAMS_QM;
444     }
446     /**
447      * Returns last error reported by database engine.
448      * @return string error message
449      */
450     public function get_last_error() {
451         return $this->mysqli->error;
452     }
454     /**
455      * Return tables in database WITHOUT current prefix
456      * @param bool $usecache if true, returns list of cached tables.
457      * @return array of table names in lowercase and without prefix
458      */
459     public function get_tables($usecache=true) {
460         if ($usecache and $this->tables !== null) {
461             return $this->tables;
462         }
463         $this->tables = array();
464         $prefix = str_replace('_', '\\_', $this->prefix);
465         $sql = "SHOW TABLES LIKE '$prefix%'";
466         $this->query_start($sql, null, SQL_QUERY_AUX);
467         $result = $this->mysqli->query($sql);
468         $this->query_end($result);
469         $len = strlen($this->prefix);
470         if ($result) {
471             while ($arr = $result->fetch_assoc()) {
472                 $tablename = reset($arr);
473                 $tablename = substr($tablename, $len);
474                 $this->tables[$tablename] = $tablename;
475             }
476             $result->close();
477         }
479         // Add the currently available temptables
480         $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
481         return $this->tables;
482     }
484     /**
485      * Return table indexes - everything lowercased.
486      * @param string $table The table we want to get indexes from.
487      * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
488      */
489     public function get_indexes($table) {
490         $indexes = array();
491         $sql = "SHOW INDEXES FROM {$this->prefix}$table";
492         $this->query_start($sql, null, SQL_QUERY_AUX);
493         $result = $this->mysqli->query($sql);
494         try {
495             $this->query_end($result);
496         } catch (dml_read_exception $e) {
497             return $indexes; // table does not exist - no indexes...
498         }
499         if ($result) {
500             while ($res = $result->fetch_object()) {
501                 if ($res->Key_name === 'PRIMARY') {
502                     continue;
503                 }
504                 if (!isset($indexes[$res->Key_name])) {
505                     $indexes[$res->Key_name] = array('unique'=>empty($res->Non_unique), 'columns'=>array());
506                 }
507                 $indexes[$res->Key_name]['columns'][$res->Seq_in_index-1] = $res->Column_name;
508             }
509             $result->close();
510         }
511         return $indexes;
512     }
514     /**
515      * Returns detailed information about columns in table. This information is cached internally.
516      * @param string $table name
517      * @param bool $usecache
518      * @return array array of database_column_info objects indexed with column names
519      */
520     public function get_columns($table, $usecache=true) {
522         if ($usecache) {
523             $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
524             $cache = cache::make('core', 'databasemeta', $properties);
525             if ($data = $cache->get($table)) {
526                 return $data;
527             }
528         }
530         $structure = array();
532         $sql = "SELECT column_name, data_type, character_maximum_length, numeric_precision,
533                        numeric_scale, is_nullable, column_type, column_default, column_key, extra
534                   FROM information_schema.columns
535                  WHERE table_name = '" . $this->prefix.$table . "'
536                        AND table_schema = '" . $this->dbname . "'
537               ORDER BY ordinal_position";
538         $this->query_start($sql, null, SQL_QUERY_AUX);
539         $result = $this->mysqli->query($sql);
540         $this->query_end(true); // Don't want to throw anything here ever. MDL-30147
542         if ($result === false) {
543             return array();
544         }
546         if ($result->num_rows > 0) {
547             // standard table exists
548             while ($rawcolumn = $result->fetch_assoc()) {
549                 $info = (object)$this->get_column_info((object)$rawcolumn);
550                 $structure[$info->name] = new database_column_info($info);
551             }
552             $result->close();
554         } else {
555             // temporary tables are not in information schema, let's try it the old way
556             $result->close();
557             $sql = "SHOW COLUMNS FROM {$this->prefix}$table";
558             $this->query_start($sql, null, SQL_QUERY_AUX);
559             $result = $this->mysqli->query($sql);
560             $this->query_end(true);
561             if ($result === false) {
562                 return array();
563             }
564             while ($rawcolumn = $result->fetch_assoc()) {
565                 $rawcolumn = (object)array_change_key_case($rawcolumn, CASE_LOWER);
566                 $rawcolumn->column_name              = $rawcolumn->field; unset($rawcolumn->field);
567                 $rawcolumn->column_type              = $rawcolumn->type; unset($rawcolumn->type);
568                 $rawcolumn->character_maximum_length = null;
569                 $rawcolumn->numeric_precision        = null;
570                 $rawcolumn->numeric_scale            = null;
571                 $rawcolumn->is_nullable              = $rawcolumn->null; unset($rawcolumn->null);
572                 $rawcolumn->column_default           = $rawcolumn->default; unset($rawcolumn->default);
573                 $rawcolumn->column_key               = $rawcolumn->key; unset($rawcolumn->default);
575                 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
576                     $rawcolumn->data_type = $matches[1];
577                     $rawcolumn->character_maximum_length = $matches[2];
579                 } else if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
580                     $rawcolumn->data_type = $matches[1];
581                     $rawcolumn->numeric_precision = $matches[2];
582                     $rawcolumn->max_length = $rawcolumn->numeric_precision;
584                     $type = strtoupper($matches[1]);
585                     if ($type === 'BIGINT') {
586                         $maxlength = 18;
587                     } else if ($type === 'INT' or $type === 'INTEGER') {
588                         $maxlength = 9;
589                     } else if ($type === 'MEDIUMINT') {
590                         $maxlength = 6;
591                     } else if ($type === 'SMALLINT') {
592                         $maxlength = 4;
593                     } else if ($type === 'TINYINT') {
594                         $maxlength = 2;
595                     } else {
596                         // This should not happen.
597                         $maxlength = 0;
598                     }
599                     if ($maxlength < $rawcolumn->max_length) {
600                         $rawcolumn->max_length = $maxlength;
601                     }
603                 } else if (preg_match('/(decimal)\((\d+),(\d+)\)/i', $rawcolumn->column_type, $matches)) {
604                     $rawcolumn->data_type = $matches[1];
605                     $rawcolumn->numeric_precision = $matches[2];
606                     $rawcolumn->numeric_scale = $matches[3];
608                 } else if (preg_match('/(double|float)(\((\d+),(\d+)\))?/i', $rawcolumn->column_type, $matches)) {
609                     $rawcolumn->data_type = $matches[1];
610                     $rawcolumn->numeric_precision = isset($matches[3]) ? $matches[3] : null;
611                     $rawcolumn->numeric_scale = isset($matches[4]) ? $matches[4] : null;
613                 } else if (preg_match('/([a-z]*text)/i', $rawcolumn->column_type, $matches)) {
614                     $rawcolumn->data_type = $matches[1];
615                     $rawcolumn->character_maximum_length = -1; // unknown
617                 } else if (preg_match('/([a-z]*blob)/i', $rawcolumn->column_type, $matches)) {
618                     $rawcolumn->data_type = $matches[1];
620                 } else {
621                     $rawcolumn->data_type = $rawcolumn->column_type;
622                 }
624                 $info = $this->get_column_info($rawcolumn);
625                 $structure[$info->name] = new database_column_info($info);
626             }
627             $result->close();
628         }
630         if ($usecache) {
631             $result = $cache->set($table, $structure);
632         }
634         return $structure;
635     }
637     /**
638      * Returns moodle column info for raw column from information schema.
639      * @param stdClass $rawcolumn
640      * @return stdClass standardised colum info
641      */
642     private function get_column_info(stdClass $rawcolumn) {
643         $rawcolumn = (object)$rawcolumn;
644         $info = new stdClass();
645         $info->name           = $rawcolumn->column_name;
646         $info->type           = $rawcolumn->data_type;
647         $info->meta_type      = $this->mysqltype2moodletype($rawcolumn->data_type);
648         $info->default_value  = $rawcolumn->column_default;
649         $info->has_default    = !is_null($rawcolumn->column_default);
650         $info->not_null       = ($rawcolumn->is_nullable === 'NO');
651         $info->primary_key    = ($rawcolumn->column_key === 'PRI');
652         $info->binary         = false;
653         $info->unsigned       = null;
654         $info->auto_increment = false;
655         $info->unique         = null;
656         $info->scale          = null;
658         if ($info->meta_type === 'C') {
659             $info->max_length = $rawcolumn->character_maximum_length;
661         } else if ($info->meta_type === 'I') {
662             if ($info->primary_key) {
663                 $info->meta_type = 'R';
664                 $info->unique    = true;
665             }
666             // Return number of decimals, not bytes here.
667             $info->max_length    = $rawcolumn->numeric_precision;
668             if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
669                 $type = strtoupper($matches[1]);
670                 if ($type === 'BIGINT') {
671                     $maxlength = 18;
672                 } else if ($type === 'INT' or $type === 'INTEGER') {
673                     $maxlength = 9;
674                 } else if ($type === 'MEDIUMINT') {
675                     $maxlength = 6;
676                 } else if ($type === 'SMALLINT') {
677                     $maxlength = 4;
678                 } else if ($type === 'TINYINT') {
679                     $maxlength = 2;
680                 } else {
681                     // This should not happen.
682                     $maxlength = 0;
683                 }
684                 // It is possible that display precision is different from storage type length,
685                 // always use the smaller value to make sure our data fits.
686                 if ($maxlength < $info->max_length) {
687                     $info->max_length = $maxlength;
688                 }
689             }
690             $info->unsigned      = (stripos($rawcolumn->column_type, 'unsigned') !== false);
691             $info->auto_increment= (strpos($rawcolumn->extra, 'auto_increment') !== false);
693         } else if ($info->meta_type === 'N') {
694             $info->max_length    = $rawcolumn->numeric_precision;
695             $info->scale         = $rawcolumn->numeric_scale;
696             $info->unsigned      = (stripos($rawcolumn->column_type, 'unsigned') !== false);
698         } else if ($info->meta_type === 'X') {
699             if ("$rawcolumn->character_maximum_length" === '4294967295') { // watch out for PHP max int limits!
700                 // means maximum moodle size for text column, in other drivers it may also mean unknown size
701                 $info->max_length = -1;
702             } else {
703                 $info->max_length = $rawcolumn->character_maximum_length;
704             }
705             $info->primary_key   = false;
707         } else if ($info->meta_type === 'B') {
708             $info->max_length    = -1;
709             $info->primary_key   = false;
710             $info->binary        = true;
711         }
713         return $info;
714     }
716     /**
717      * Normalise column type.
718      * @param string $mysql_type
719      * @return string one character
720      * @throws dml_exception
721      */
722     private function mysqltype2moodletype($mysql_type) {
723         $type = null;
725         switch(strtoupper($mysql_type)) {
726             case 'BIT':
727                 $type = 'L';
728                 break;
730             case 'TINYINT':
731             case 'SMALLINT':
732             case 'MEDIUMINT':
733             case 'INT':
734             case 'INTEGER':
735             case 'BIGINT':
736                 $type = 'I';
737                 break;
739             case 'FLOAT':
740             case 'DOUBLE':
741             case 'DECIMAL':
742                 $type = 'N';
743                 break;
745             case 'CHAR':
746             case 'ENUM':
747             case 'SET':
748             case 'VARCHAR':
749                 $type = 'C';
750                 break;
752             case 'TINYTEXT':
753             case 'TEXT':
754             case 'MEDIUMTEXT':
755             case 'LONGTEXT':
756                 $type = 'X';
757                 break;
759             case 'BINARY':
760             case 'VARBINARY':
761             case 'BLOB':
762             case 'TINYBLOB':
763             case 'MEDIUMBLOB':
764             case 'LONGBLOB':
765                 $type = 'B';
766                 break;
768             case 'DATE':
769             case 'TIME':
770             case 'DATETIME':
771             case 'TIMESTAMP':
772             case 'YEAR':
773                 $type = 'D';
774                 break;
775         }
777         if (!$type) {
778             throw new dml_exception('invalidmysqlnativetype', $mysql_type);
779         }
780         return $type;
781     }
783     /**
784      * Normalise values based in RDBMS dependencies (booleans, LOBs...)
785      *
786      * @param database_column_info $column column metadata corresponding with the value we are going to normalise
787      * @param mixed $value value we are going to normalise
788      * @return mixed the normalised value
789      */
790     protected function normalise_value($column, $value) {
791         $this->detect_objects($value);
793         if (is_bool($value)) { // Always, convert boolean to int
794             $value = (int)$value;
796         } else if ($value === '') {
797             if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
798                 $value = 0; // prevent '' problems in numeric fields
799             }
800         // Any float value being stored in varchar or text field is converted to string to avoid
801         // any implicit conversion by MySQL
802         } else if (is_float($value) and ($column->meta_type == 'C' or $column->meta_type == 'X')) {
803             $value = "$value";
804         }
805         return $value;
806     }
808     /**
809      * Is this database compatible with utf8?
810      * @return bool
811      */
812     public function setup_is_unicodedb() {
813         // All new tables are created with this collation, we just have to make sure it is utf8 compatible,
814         // if config table already exists it has this collation too.
815         $collation = $this->get_dbcollation();
817         $sql = "SHOW COLLATION WHERE Collation ='$collation' AND Charset = 'utf8'";
818         $this->query_start($sql, NULL, SQL_QUERY_AUX);
819         $result = $this->mysqli->query($sql);
820         $this->query_end($result);
821         if ($result->fetch_assoc()) {
822             $return = true;
823         } else {
824             $return = false;
825         }
826         $result->close();
828         return $return;
829     }
831     /**
832      * Do NOT use in code, to be used by database_manager only!
833      * @param string $sql query
834      * @return bool true
835      * @throws dml_exception A DML specific exception is thrown for any errors.
836      */
837     public function change_database_structure($sql) {
838         $this->reset_caches();
840         $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
841         $result = $this->mysqli->query($sql);
842         $this->query_end($result);
844         return true;
845     }
847     /**
848      * Very ugly hack which emulates bound parameters in queries
849      * because prepared statements do not use query cache.
850      */
851     protected function emulate_bound_params($sql, array $params=null) {
852         if (empty($params)) {
853             return $sql;
854         }
855         // ok, we have verified sql statement with ? and correct number of params
856         $parts = array_reverse(explode('?', $sql));
857         $return = array_pop($parts);
858         foreach ($params as $param) {
859             if (is_bool($param)) {
860                 $return .= (int)$param;
861             } else if (is_null($param)) {
862                 $return .= 'NULL';
863             } else if (is_number($param)) {
864                 $return .= "'".$param."'"; // we have to always use strings because mysql is using weird automatic int casting
865             } else if (is_float($param)) {
866                 $return .= $param;
867             } else {
868                 $param = $this->mysqli->real_escape_string($param);
869                 $return .= "'$param'";
870             }
871             $return .= array_pop($parts);
872         }
873         return $return;
874     }
876     /**
877      * Execute general sql query. Should be used only when no other method suitable.
878      * Do NOT use this to make changes in db structure, use database_manager methods instead!
879      * @param string $sql query
880      * @param array $params query parameters
881      * @return bool true
882      * @throws dml_exception A DML specific exception is thrown for any errors.
883      */
884     public function execute($sql, array $params=null) {
885         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
887         if (strpos($sql, ';') !== false) {
888             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
889         }
891         $rawsql = $this->emulate_bound_params($sql, $params);
893         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
894         $result = $this->mysqli->query($rawsql);
895         $this->query_end($result);
897         if ($result === true) {
898             return true;
900         } else {
901             $result->close();
902             return true;
903         }
904     }
906     /**
907      * Get a number of records as a moodle_recordset using a SQL statement.
908      *
909      * Since this method is a little less readable, use of it should be restricted to
910      * code where it's possible there might be large datasets being returned.  For known
911      * small datasets use get_records_sql - it leads to simpler code.
912      *
913      * The return type is like:
914      * @see function get_recordset.
915      *
916      * @param string $sql the SQL select query to execute.
917      * @param array $params array of sql parameters
918      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
919      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
920      * @return moodle_recordset instance
921      * @throws dml_exception A DML specific exception is thrown for any errors.
922      */
923     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
924         $limitfrom = (int)$limitfrom;
925         $limitnum  = (int)$limitnum;
926         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
927         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
929         if ($limitfrom or $limitnum) {
930             if ($limitnum < 1) {
931                 $limitnum = "18446744073709551615";
932             }
933             $sql .= " LIMIT $limitfrom, $limitnum";
934         }
936         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
937         $rawsql = $this->emulate_bound_params($sql, $params);
939         $this->query_start($sql, $params, SQL_QUERY_SELECT);
940         // no MYSQLI_USE_RESULT here, it would block write ops on affected tables
941         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
942         $this->query_end($result);
944         return $this->create_recordset($result);
945     }
947     /**
948      * Get all records from a table.
949      *
950      * This method works around potential memory problems and may improve performance,
951      * this method may block access to table until the recordset is closed.
952      *
953      * @param string $table Name of database table.
954      * @return moodle_recordset A moodle_recordset instance {@link function get_recordset}.
955      * @throws dml_exception A DML specific exception is thrown for any errors.
956      */
957     public function export_table_recordset($table) {
958         $sql = $this->fix_table_names("SELECT * FROM {{$table}}");
960         $this->query_start($sql, array(), SQL_QUERY_SELECT);
961         // MYSQLI_STORE_RESULT may eat all memory for large tables, unfortunately MYSQLI_USE_RESULT blocks other queries.
962         $result = $this->mysqli->query($sql, MYSQLI_USE_RESULT);
963         $this->query_end($result);
965         return $this->create_recordset($result);
966     }
968     protected function create_recordset($result) {
969         return new mysqli_native_moodle_recordset($result);
970     }
972     /**
973      * Get a number of records as an array of objects using a SQL statement.
974      *
975      * Return value is like:
976      * @see function get_records.
977      *
978      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
979      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
980      *   returned array.
981      * @param array $params array of sql parameters
982      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
983      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
984      * @return array of objects, or empty array if no records were found
985      * @throws dml_exception A DML specific exception is thrown for any errors.
986      */
987     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
988         $limitfrom = (int)$limitfrom;
989         $limitnum  = (int)$limitnum;
990         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
991         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
993         if ($limitfrom or $limitnum) {
994             if ($limitnum < 1) {
995                 $limitnum = "18446744073709551615";
996             }
997             $sql .= " LIMIT $limitfrom, $limitnum";
998         }
1000         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1001         $rawsql = $this->emulate_bound_params($sql, $params);
1003         $this->query_start($sql, $params, SQL_QUERY_SELECT);
1004         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
1005         $this->query_end($result);
1007         $return = array();
1009         while($row = $result->fetch_assoc()) {
1010             $row = array_change_key_case($row, CASE_LOWER);
1011             $id  = reset($row);
1012             if (isset($return[$id])) {
1013                 $colname = key($row);
1014                 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);
1015             }
1016             $return[$id] = (object)$row;
1017         }
1018         $result->close();
1020         return $return;
1021     }
1023     /**
1024      * Selects records and return values (first field) as an array using a SQL statement.
1025      *
1026      * @param string $sql The SQL query
1027      * @param array $params array of sql parameters
1028      * @return array of values
1029      * @throws dml_exception A DML specific exception is thrown for any errors.
1030      */
1031     public function get_fieldset_sql($sql, array $params=null) {
1032         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1033         $rawsql = $this->emulate_bound_params($sql, $params);
1035         $this->query_start($sql, $params, SQL_QUERY_SELECT);
1036         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
1037         $this->query_end($result);
1039         $return = array();
1041         while($row = $result->fetch_assoc()) {
1042             $return[] = reset($row);
1043         }
1044         $result->close();
1046         return $return;
1047     }
1049     /**
1050      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
1051      * @param string $table name
1052      * @param mixed $params data record as object or array
1053      * @param bool $returnit return it of inserted record
1054      * @param bool $bulk true means repeated inserts expected
1055      * @param bool $customsequence true if 'id' included in $params, disables $returnid
1056      * @return bool|int true or new id
1057      * @throws dml_exception A DML specific exception is thrown for any errors.
1058      */
1059     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
1060         if (!is_array($params)) {
1061             $params = (array)$params;
1062         }
1064         if ($customsequence) {
1065             if (!isset($params['id'])) {
1066                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
1067             }
1068             $returnid = false;
1069         } else {
1070             unset($params['id']);
1071         }
1073         if (empty($params)) {
1074             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
1075         }
1077         $fields = implode(',', array_keys($params));
1078         $qms    = array_fill(0, count($params), '?');
1079         $qms    = implode(',', $qms);
1081         $sql = "INSERT INTO {$this->prefix}$table ($fields) VALUES($qms)";
1083         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1084         $rawsql = $this->emulate_bound_params($sql, $params);
1086         $this->query_start($sql, $params, SQL_QUERY_INSERT);
1087         $result = $this->mysqli->query($rawsql);
1088         $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db
1089         $this->query_end($result);
1091         if (!$customsequence and !$id) {
1092             throw new dml_write_exception('unknown error fetching inserted id');
1093         }
1095         if (!$returnid) {
1096             return true;
1097         } else {
1098             return (int)$id;
1099         }
1100     }
1102     /**
1103      * Insert a record into a table and return the "id" field if required.
1104      *
1105      * Some conversions and safety checks are carried out. Lobs are supported.
1106      * If the return ID isn't required, then this just reports success as true/false.
1107      * $data is an object containing needed data
1108      * @param string $table The database table to be inserted into
1109      * @param object $data A data object with values for one or more fields in the record
1110      * @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.
1111      * @return bool|int true or new id
1112      * @throws dml_exception A DML specific exception is thrown for any errors.
1113      */
1114     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1115         $dataobject = (array)$dataobject;
1117         $columns = $this->get_columns($table);
1118         $cleaned = array();
1120         foreach ($dataobject as $field=>$value) {
1121             if ($field === 'id') {
1122                 continue;
1123             }
1124             if (!isset($columns[$field])) {
1125                 continue;
1126             }
1127             $column = $columns[$field];
1128             $cleaned[$field] = $this->normalise_value($column, $value);
1129         }
1131         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
1132     }
1134     /**
1135      * Import a record into a table, id field is required.
1136      * Safety checks are NOT carried out. Lobs are supported.
1137      *
1138      * @param string $table name of database table to be inserted into
1139      * @param object $dataobject A data object with values for one or more fields in the record
1140      * @return bool true
1141      * @throws dml_exception A DML specific exception is thrown for any errors.
1142      */
1143     public function import_record($table, $dataobject) {
1144         $dataobject = (array)$dataobject;
1146         $columns = $this->get_columns($table);
1147         $cleaned = array();
1149         foreach ($dataobject as $field=>$value) {
1150             if (!isset($columns[$field])) {
1151                 continue;
1152             }
1153             $cleaned[$field] = $value;
1154         }
1156         return $this->insert_record_raw($table, $cleaned, false, true, true);
1157     }
1159     /**
1160      * Update record in database, as fast as possible, no safety checks, lobs not supported.
1161      * @param string $table name
1162      * @param mixed $params data record as object or array
1163      * @param bool true means repeated updates expected
1164      * @return bool true
1165      * @throws dml_exception A DML specific exception is thrown for any errors.
1166      */
1167     public function update_record_raw($table, $params, $bulk=false) {
1168         $params = (array)$params;
1170         if (!isset($params['id'])) {
1171             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1172         }
1173         $id = $params['id'];
1174         unset($params['id']);
1176         if (empty($params)) {
1177             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1178         }
1180         $sets = array();
1181         foreach ($params as $field=>$value) {
1182             $sets[] = "$field = ?";
1183         }
1185         $params[] = $id; // last ? in WHERE condition
1187         $sets = implode(',', $sets);
1188         $sql = "UPDATE {$this->prefix}$table SET $sets WHERE id=?";
1190         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1191         $rawsql = $this->emulate_bound_params($sql, $params);
1193         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1194         $result = $this->mysqli->query($rawsql);
1195         $this->query_end($result);
1197         return true;
1198     }
1200     /**
1201      * Update a record in a table
1202      *
1203      * $dataobject is an object containing needed data
1204      * Relies on $dataobject having a variable "id" to
1205      * specify the record to update
1206      *
1207      * @param string $table The database table to be checked against.
1208      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1209      * @param bool true means repeated updates expected
1210      * @return bool true
1211      * @throws dml_exception A DML specific exception is thrown for any errors.
1212      */
1213     public function update_record($table, $dataobject, $bulk=false) {
1214         $dataobject = (array)$dataobject;
1216         $columns = $this->get_columns($table);
1217         $cleaned = array();
1219         foreach ($dataobject as $field=>$value) {
1220             if (!isset($columns[$field])) {
1221                 continue;
1222             }
1223             $column = $columns[$field];
1224             $cleaned[$field] = $this->normalise_value($column, $value);
1225         }
1227         return $this->update_record_raw($table, $cleaned, $bulk);
1228     }
1230     /**
1231      * Set a single field in every table record which match a particular WHERE clause.
1232      *
1233      * @param string $table The database table to be checked against.
1234      * @param string $newfield the field to set.
1235      * @param string $newvalue the value to set the field to.
1236      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1237      * @param array $params array of sql parameters
1238      * @return bool true
1239      * @throws dml_exception A DML specific exception is thrown for any errors.
1240      */
1241     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1242         if ($select) {
1243             $select = "WHERE $select";
1244         }
1245         if (is_null($params)) {
1246             $params = array();
1247         }
1248         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1250         // Get column metadata
1251         $columns = $this->get_columns($table);
1252         $column = $columns[$newfield];
1254         $normalised_value = $this->normalise_value($column, $newvalue);
1256         if (is_null($normalised_value)) {
1257             $newfield = "$newfield = NULL";
1258         } else {
1259             $newfield = "$newfield = ?";
1260             array_unshift($params, $normalised_value);
1261         }
1262         $sql = "UPDATE {$this->prefix}$table SET $newfield $select";
1263         $rawsql = $this->emulate_bound_params($sql, $params);
1265         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1266         $result = $this->mysqli->query($rawsql);
1267         $this->query_end($result);
1269         return true;
1270     }
1272     /**
1273      * Delete one or more records from a table which match a particular WHERE clause.
1274      *
1275      * @param string $table The database table to be checked against.
1276      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1277      * @param array $params array of sql parameters
1278      * @return bool true
1279      * @throws dml_exception A DML specific exception is thrown for any errors.
1280      */
1281     public function delete_records_select($table, $select, array $params=null) {
1282         if ($select) {
1283             $select = "WHERE $select";
1284         }
1285         $sql = "DELETE FROM {$this->prefix}$table $select";
1287         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1288         $rawsql = $this->emulate_bound_params($sql, $params);
1290         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1291         $result = $this->mysqli->query($rawsql);
1292         $this->query_end($result);
1294         return true;
1295     }
1297     public function sql_cast_char2int($fieldname, $text=false) {
1298         return ' CAST(' . $fieldname . ' AS SIGNED) ';
1299     }
1301     public function sql_cast_char2real($fieldname, $text=false) {
1302         return ' CAST(' . $fieldname . ' AS DECIMAL) ';
1303     }
1305     /**
1306      * Returns 'LIKE' part of a query.
1307      *
1308      * @param string $fieldname usually name of the table column
1309      * @param string $param usually bound query parameter (?, :named)
1310      * @param bool $casesensitive use case sensitive search
1311      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1312      * @param bool $notlike true means "NOT LIKE"
1313      * @param string $escapechar escape char for '%' and '_'
1314      * @return string SQL code fragment
1315      */
1316     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1317         if (strpos($param, '%') !== false) {
1318             debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1319         }
1320         $escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\'
1322         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1323         if ($casesensitive) {
1324             return "$fieldname $LIKE $param COLLATE utf8_bin ESCAPE '$escapechar'";
1325         } else {
1326             if ($accentsensitive) {
1327                 return "LOWER($fieldname) $LIKE LOWER($param) COLLATE utf8_bin ESCAPE '$escapechar'";
1328             } else {
1329                 return "$fieldname $LIKE $param ESCAPE '$escapechar'";
1330             }
1331         }
1332     }
1334     /**
1335      * Returns the proper SQL to do CONCAT between the elements passed
1336      * Can take many parameters
1337      *
1338      * @param string $str,... 1 or more fields/strings to concat
1339      *
1340      * @return string The concat sql
1341      */
1342     public function sql_concat() {
1343         $arr = func_get_args();
1344         $s = implode(', ', $arr);
1345         if ($s === '') {
1346             return "''";
1347         }
1348         return "CONCAT($s)";
1349     }
1351     /**
1352      * Returns the proper SQL to do CONCAT between the elements passed
1353      * with a given separator
1354      *
1355      * @param string $separator The string to use as the separator
1356      * @param array $elements An array of items to concatenate
1357      * @return string The concat SQL
1358      */
1359     public function sql_concat_join($separator="' '", $elements=array()) {
1360         $s = implode(', ', $elements);
1362         if ($s === '') {
1363             return "''";
1364         }
1365         return "CONCAT_WS($separator, $s)";
1366     }
1368     /**
1369      * Returns the SQL text to be used to calculate the length in characters of one expression.
1370      * @param string fieldname or expression to calculate its length in characters.
1371      * @return string the piece of SQL code to be used in the statement.
1372      */
1373     public function sql_length($fieldname) {
1374         return ' CHAR_LENGTH(' . $fieldname . ')';
1375     }
1377     /**
1378      * Does this driver support regex syntax when searching
1379      */
1380     public function sql_regex_supported() {
1381         return true;
1382     }
1384     /**
1385      * Return regex positive or negative match sql
1386      * @param bool $positivematch
1387      * @return string or empty if not supported
1388      */
1389     public function sql_regex($positivematch=true) {
1390         return $positivematch ? 'REGEXP' : 'NOT REGEXP';
1391     }
1393     /**
1394      * Returns the SQL to be used in order to an UNSIGNED INTEGER column to SIGNED.
1395      *
1396      * @deprecated since 2.3
1397      * @param string $fieldname The name of the field to be cast
1398      * @return string The piece of SQL code to be used in your statement.
1399      */
1400     public function sql_cast_2signed($fieldname) {
1401         return ' CAST(' . $fieldname . ' AS SIGNED) ';
1402     }
1404     public function session_lock_supported() {
1405         return true;
1406     }
1408     /**
1409      * Obtain session lock
1410      * @param int $rowid id of the row with session record
1411      * @param int $timeout max allowed time to wait for the lock in seconds
1412      * @return void
1413      */
1414     public function get_session_lock($rowid, $timeout) {
1415         parent::get_session_lock($rowid, $timeout);
1417         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1418         $sql = "SELECT GET_LOCK('$fullname', $timeout)";
1419         $this->query_start($sql, null, SQL_QUERY_AUX);
1420         $result = $this->mysqli->query($sql);
1421         $this->query_end($result);
1423         if ($result) {
1424             $arr = $result->fetch_assoc();
1425             $result->close();
1427             if (reset($arr) == 1) {
1428                 return;
1429             } else {
1430                 throw new dml_sessionwait_exception();
1431             }
1432         }
1433     }
1435     public function release_session_lock($rowid) {
1436         if (!$this->used_for_db_sessions) {
1437             return;
1438         }
1440         parent::release_session_lock($rowid);
1441         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1442         $sql = "SELECT RELEASE_LOCK('$fullname')";
1443         $this->query_start($sql, null, SQL_QUERY_AUX);
1444         $result = $this->mysqli->query($sql);
1445         $this->query_end($result);
1447         if ($result) {
1448             $result->close();
1449         }
1450     }
1452     /**
1453      * Are transactions supported?
1454      * It is not responsible to run productions servers
1455      * on databases without transaction support ;-)
1456      *
1457      * MyISAM does not support support transactions.
1458      *
1459      * You can override this via the dbtransactions option.
1460      *
1461      * @return bool
1462      */
1463     protected function transactions_supported() {
1464         if (!is_null($this->transactions_supported)) {
1465             return $this->transactions_supported;
1466         }
1468         // this is all just guessing, might be better to just specify it in config.php
1469         if (isset($this->dboptions['dbtransactions'])) {
1470             $this->transactions_supported = $this->dboptions['dbtransactions'];
1471             return $this->transactions_supported;
1472         }
1474         $this->transactions_supported = false;
1476         $engine = $this->get_dbengine();
1478         // Only will accept transactions if using compatible storage engine (more engines can be added easily BDB, Falcon...)
1479         if (in_array($engine, array('InnoDB', 'INNOBASE', 'BDB', 'XtraDB', 'Aria', 'Falcon'))) {
1480             $this->transactions_supported = true;
1481         }
1483         return $this->transactions_supported;
1484     }
1486     /**
1487      * Driver specific start of real database transaction,
1488      * this can not be used directly in code.
1489      * @return void
1490      */
1491     protected function begin_transaction() {
1492         if (!$this->transactions_supported()) {
1493             return;
1494         }
1496         $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
1497         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1498         $result = $this->mysqli->query($sql);
1499         $this->query_end($result);
1501         $sql = "START TRANSACTION";
1502         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1503         $result = $this->mysqli->query($sql);
1504         $this->query_end($result);
1505     }
1507     /**
1508      * Driver specific commit of real database transaction,
1509      * this can not be used directly in code.
1510      * @return void
1511      */
1512     protected function commit_transaction() {
1513         if (!$this->transactions_supported()) {
1514             return;
1515         }
1517         $sql = "COMMIT";
1518         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1519         $result = $this->mysqli->query($sql);
1520         $this->query_end($result);
1521     }
1523     /**
1524      * Driver specific abort of real database transaction,
1525      * this can not be used directly in code.
1526      * @return void
1527      */
1528     protected function rollback_transaction() {
1529         if (!$this->transactions_supported()) {
1530             return;
1531         }
1533         $sql = "ROLLBACK";
1534         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1535         $result = $this->mysqli->query($sql);
1536         $this->query_end($result);
1538         return true;
1539     }