MDL-34211 Use the $table_name argument to check against $metatables key values
[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         $result = $conn->query("CREATE DATABASE $dbname DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci");
87         $conn->close();
89         if (!$result) {
90             throw new dml_exception('cannotcreatedb');
91         }
93         return true;
94     }
96     /**
97      * Detects if all needed PHP stuff installed.
98      * Note: can be used before connect()
99      * @return mixed true if ok, string if something
100      */
101     public function driver_installed() {
102         if (!extension_loaded('mysqli')) {
103             return get_string('mysqliextensionisnotpresentinphp', 'install');
104         }
105         return true;
106     }
108     /**
109      * Returns database family type - describes SQL dialect
110      * Note: can be used before connect()
111      * @return string db family name (mysql, postgres, mssql, oracle, etc.)
112      */
113     public function get_dbfamily() {
114         return 'mysql';
115     }
117     /**
118      * Returns more specific database driver type
119      * Note: can be used before connect()
120      * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
121      */
122     protected function get_dbtype() {
123         return 'mysqli';
124     }
126     /**
127      * Returns general database library name
128      * Note: can be used before connect()
129      * @return string db type pdo, native
130      */
131     protected function get_dblibrary() {
132         return 'native';
133     }
135     /**
136      * Returns the current MySQL db engine.
137      *
138      * This is an ugly workaround for MySQL default engine problems,
139      * Moodle is designed to work best on ACID compliant databases
140      * with full transaction support. Do not use MyISAM.
141      *
142      * @return string or null MySQL engine name
143      */
144     public function get_dbengine() {
145         if (isset($this->dboptions['dbengine'])) {
146             return $this->dboptions['dbengine'];
147         }
149         $engine = null;
151         if (!$this->external) {
152             // look for current engine of our config table (the first table that gets created),
153             // so that we create all tables with the same engine
154             $sql = "SELECT engine FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config'";
155             $this->query_start($sql, NULL, SQL_QUERY_AUX);
156             $result = $this->mysqli->query($sql);
157             $this->query_end($result);
158             if ($rec = $result->fetch_assoc()) {
159                 $engine = $rec['engine'];
160             }
161             $result->close();
162         }
164         if ($engine) {
165             return $engine;
166         }
168         // get the default database engine
169         $sql = "SELECT @@storage_engine";
170         $this->query_start($sql, NULL, SQL_QUERY_AUX);
171         $result = $this->mysqli->query($sql);
172         $this->query_end($result);
173         if ($rec = $result->fetch_assoc()) {
174             $engine = $rec['@@storage_engine'];
175         }
176         $result->close();
178         if (!$this->external and $engine === 'MyISAM') {
179             // we really do not want MyISAM for Moodle, InnoDB or XtraDB is a reasonable defaults if supported
180             $sql = "SHOW STORAGE ENGINES";
181             $this->query_start($sql, NULL, SQL_QUERY_AUX);
182             $result = $this->mysqli->query($sql);
183             $this->query_end($result);
184             $engines = array();
185             while ($res = $result->fetch_assoc()) {
186                 if ($res['Support'] === 'YES' or $res['Support'] === 'DEFAULT') {
187                     $engines[$res['Engine']] = true;
188                 }
189             }
190             $result->close();
191             if (isset($engines['InnoDB'])) {
192                 $engine = 'InnoDB';
193             }
194             if (isset($engines['XtraDB'])) {
195                 $engine = 'XtraDB';
196             }
197         }
199         return $engine;
200     }
202     /**
203      * Returns localised database type name
204      * Note: can be used before connect()
205      * @return string
206      */
207     public function get_name() {
208         return get_string('nativemysqli', 'install');
209     }
211     /**
212      * Returns localised database configuration help.
213      * Note: can be used before connect()
214      * @return string
215      */
216     public function get_configuration_help() {
217         return get_string('nativemysqlihelp', 'install');
218     }
220     /**
221      * Returns localised database description
222      * Note: can be used before connect()
223      * @return string
224      */
225     public function get_configuration_hints() {
226         return get_string('databasesettingssub_mysqli', 'install');
227     }
229     /**
230      * Diagnose database and tables, this function is used
231      * to verify database and driver settings, db engine types, etc.
232      *
233      * @return string null means everything ok, string means problem found.
234      */
235     public function diagnose() {
236         $sloppymyisamfound = false;
237         $prefix = str_replace('_', '\\_', $this->prefix);
238         $sql = "SHOW TABLE STATUS WHERE Name LIKE BINARY '$prefix%'";
239         $this->query_start($sql, null, SQL_QUERY_AUX);
240         $result = $this->mysqli->query($sql);
241         $this->query_end($result);
242         if ($result) {
243             while ($arr = $result->fetch_assoc()) {
244                 if ($arr['Engine'] === 'MyISAM') {
245                     $sloppymyisamfound = true;
246                     break;
247                 }
248             }
249             $result->close();
250         }
252         if ($sloppymyisamfound) {
253             return get_string('myisamproblem', 'error');
254         } else {
255             return null;
256         }
257     }
259     /**
260      * Connect to db
261      * Must be called before other methods.
262      * @param string $dbhost The database host.
263      * @param string $dbuser The database username.
264      * @param string $dbpass The database username's password.
265      * @param string $dbname The name of the database being connected to.e
266      * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
267      * @param array $dboptions driver specific options
268      * @return bool success
269      */
270     public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
271         $driverstatus = $this->driver_installed();
273         if ($driverstatus !== true) {
274             throw new dml_exception('dbdriverproblem', $driverstatus);
275         }
277         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
279         // dbsocket is used ONLY if host is NULL or 'localhost',
280         // you can not disable it because it is always tried if dbhost is 'localhost'
281         if (!empty($this->dboptions['dbsocket'])
282                 and (strpos($this->dboptions['dbsocket'], '/') !== false or strpos($this->dboptions['dbsocket'], '\\') !== false)) {
283             $dbsocket = $this->dboptions['dbsocket'];
284         } else {
285             $dbsocket = ini_get('mysqli.default_socket');
286         }
287         if (empty($this->dboptions['dbport'])) {
288             $dbport = (int)ini_get('mysqli.default_port');
289         } else {
290             $dbport = (int)$this->dboptions['dbport'];
291         }
292         // verify ini.get does not return nonsense
293         if (empty($dbport)) {
294             $dbport = 3306;
295         }
296         ob_start();
297         $this->mysqli = new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
298         $dberr = ob_get_contents();
299         ob_end_clean();
300         $errorno = @$this->mysqli->connect_errno;
302         if ($errorno !== 0) {
303             throw new dml_connection_exception($dberr);
304         }
306         $this->query_start("--set_charset()", null, SQL_QUERY_AUX);
307         $this->mysqli->set_charset('utf8');
308         $this->query_end(true);
310         // If available, enforce strict mode for the session. That guaranties
311         // standard behaviour under some situations, avoiding some MySQL nasty
312         // habits like truncating data or performing some transparent cast losses.
313         // With strict mode enforced, Moodle DB layer will be consistently throwing
314         // the corresponding exceptions as expected.
315         $si = $this->get_server_info();
316         if (version_compare($si['version'], '5.0.2', '>=')) {
317             $sql = "SET SESSION sql_mode = 'STRICT_ALL_TABLES'";
318             $this->query_start($sql, null, SQL_QUERY_AUX);
319             $result = $this->mysqli->query($sql);
320             $this->query_end($result);
321         }
323         // Connection stabilised and configured, going to instantiate the temptables controller
324         $this->temptables = new mysqli_native_moodle_temptables($this);
326         return true;
327     }
329     /**
330      * Close database connection and release all resources
331      * and memory (especially circular memory references).
332      * Do NOT use connect() again, create a new instance if needed.
333      */
334     public function dispose() {
335         parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
336         if ($this->mysqli) {
337             $this->mysqli->close();
338             $this->mysqli = null;
339         }
340     }
342     /**
343      * Returns database server info array
344      * @return array Array containing 'description' and 'version' info
345      */
346     public function get_server_info() {
347         return array('description'=>$this->mysqli->server_info, 'version'=>$this->mysqli->server_info);
348     }
350     /**
351      * Returns supported query parameter types
352      * @return int bitmask of accepted SQL_PARAMS_*
353      */
354     protected function allowed_param_types() {
355         return SQL_PARAMS_QM;
356     }
358     /**
359      * Returns last error reported by database engine.
360      * @return string error message
361      */
362     public function get_last_error() {
363         return $this->mysqli->error;
364     }
366     /**
367      * Return tables in database WITHOUT current prefix
368      * @param bool $usecache if true, returns list of cached tables.
369      * @return array of table names in lowercase and without prefix
370      */
371     public function get_tables($usecache=true) {
372         if ($usecache and $this->tables !== null) {
373             return $this->tables;
374         }
375         $this->tables = array();
376         $prefix = str_replace('_', '\\_', $this->prefix);
377         $sql = "SHOW TABLES LIKE '$prefix%'";
378         $this->query_start($sql, null, SQL_QUERY_AUX);
379         $result = $this->mysqli->query($sql);
380         $this->query_end($result);
381         $len = strlen($this->prefix);
382         if ($result) {
383             while ($arr = $result->fetch_assoc()) {
384                 $tablename = reset($arr);
385                 $tablename = substr($tablename, $len);
386                 $this->tables[$tablename] = $tablename;
387             }
388             $result->close();
389         }
391         // Add the currently available temptables
392         $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
393         return $this->tables;
394     }
396     /**
397      * Return table indexes - everything lowercased.
398      * @param string $table The table we want to get indexes from.
399      * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
400      */
401     public function get_indexes($table) {
402         $indexes = array();
403         $sql = "SHOW INDEXES FROM {$this->prefix}$table";
404         $this->query_start($sql, null, SQL_QUERY_AUX);
405         $result = $this->mysqli->query($sql);
406         try {
407             $this->query_end($result);
408         } catch (dml_read_exception $e) {
409             return $indexes; // table does not exist - no indexes...
410         }
411         if ($result) {
412             while ($res = $result->fetch_object()) {
413                 if ($res->Key_name === 'PRIMARY') {
414                     continue;
415                 }
416                 if (!isset($indexes[$res->Key_name])) {
417                     $indexes[$res->Key_name] = array('unique'=>empty($res->Non_unique), 'columns'=>array());
418                 }
419                 $indexes[$res->Key_name]['columns'][$res->Seq_in_index-1] = $res->Column_name;
420             }
421             $result->close();
422         }
423         return $indexes;
424     }
426     /**
427      * Returns detailed information about columns in table. This information is cached internally.
428      * @param string $table name
429      * @param bool $usecache
430      * @return array array of database_column_info objects indexed with column names
431      */
432     public function get_columns($table, $usecache=true) {
433         if ($usecache and isset($this->columns[$table])) {
434             return $this->columns[$table];
435         }
437         $this->columns[$table] = array();
439         $sql = "SELECT column_name, data_type, character_maximum_length, numeric_precision,
440                        numeric_scale, is_nullable, column_type, column_default, column_key, extra
441                   FROM information_schema.columns
442                  WHERE table_name = '" . $this->prefix.$table . "'
443                        AND table_schema = '" . $this->dbname . "'
444               ORDER BY ordinal_position";
445         $this->query_start($sql, null, SQL_QUERY_AUX);
446         $result = $this->mysqli->query($sql);
447         $this->query_end(true); // Don't want to throw anything here ever. MDL-30147
449         if ($result === false) {
450             return array();
451         }
453         if ($result->num_rows > 0) {
454             // standard table exists
455             while ($rawcolumn = $result->fetch_assoc()) {
456                 $info = (object)$this->get_column_info((object)$rawcolumn);
457                 $this->columns[$table][$info->name] = new database_column_info($info);
458             }
459             $result->close();
461         } else {
462             // temporary tables are not in information schema, let's try it the old way
463             $result->close();
464             $sql = "SHOW COLUMNS FROM {$this->prefix}$table";
465             $this->query_start($sql, null, SQL_QUERY_AUX);
466             $result = $this->mysqli->query($sql);
467             $this->query_end(true);
468             if ($result === false) {
469                 return array();
470             }
471             while ($rawcolumn = $result->fetch_assoc()) {
472                 $rawcolumn = (object)array_change_key_case($rawcolumn, CASE_LOWER);
473                 $rawcolumn->column_name              = $rawcolumn->field; unset($rawcolumn->field);
474                 $rawcolumn->column_type              = $rawcolumn->type; unset($rawcolumn->type);
475                 $rawcolumn->character_maximum_length = null;
476                 $rawcolumn->numeric_precision        = null;
477                 $rawcolumn->numeric_scale            = null;
478                 $rawcolumn->is_nullable              = $rawcolumn->null; unset($rawcolumn->null);
479                 $rawcolumn->column_default           = $rawcolumn->default; unset($rawcolumn->default);
480                 $rawcolumn->column_key               = $rawcolumn->key; unset($rawcolumn->default);
482                 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
483                     $rawcolumn->data_type = $matches[1];
484                     $rawcolumn->character_maximum_length = $matches[2];
486                 } else if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
487                     $rawcolumn->data_type = $matches[1];
488                     $rawcolumn->character_maximum_length = $matches[2];
490                 } else if (preg_match('/(decimal)\((\d+),(\d+)\)/i', $rawcolumn->column_type, $matches)) {
491                     $rawcolumn->data_type = $matches[1];
492                     $rawcolumn->numeric_precision = $matches[2];
493                     $rawcolumn->numeric_scale = $matches[3];
495                 } else if (preg_match('/(double|float)(\((\d+),(\d+)\))?/i', $rawcolumn->column_type, $matches)) {
496                     $rawcolumn->data_type = $matches[1];
497                     $rawcolumn->numeric_precision = isset($matches[3]) ? $matches[3] : null;
498                     $rawcolumn->numeric_scale = isset($matches[4]) ? $matches[4] : null;
500                 } else if (preg_match('/([a-z]*text)/i', $rawcolumn->column_type, $matches)) {
501                     $rawcolumn->data_type = $matches[1];
502                     $rawcolumn->character_maximum_length = -1; // unknown
504                 } else if (preg_match('/([a-z]*blob)/i', $rawcolumn->column_type, $matches)) {
505                     $rawcolumn->data_type = $matches[1];
507                 } else {
508                     $rawcolumn->data_type = $rawcolumn->column_type;
509                 }
511                 $info = $this->get_column_info($rawcolumn);
512                 $this->columns[$table][$info->name] = new database_column_info($info);
513             }
514             $result->close();
515         }
517         return $this->columns[$table];
518     }
520     /**
521      * Returns moodle column info for raw column from information schema.
522      * @param stdClass $rawcolumn
523      * @return stdClass standardised colum info
524      */
525     private function get_column_info(stdClass $rawcolumn) {
526         $rawcolumn = (object)$rawcolumn;
527         $info = new stdClass();
528         $info->name           = $rawcolumn->column_name;
529         $info->type           = $rawcolumn->data_type;
530         $info->meta_type      = $this->mysqltype2moodletype($rawcolumn->data_type);
531         $info->default_value  = $rawcolumn->column_default;
532         $info->has_default    = !is_null($rawcolumn->column_default);
533         $info->not_null       = ($rawcolumn->is_nullable === 'NO');
534         $info->primary_key    = ($rawcolumn->column_key === 'PRI');
535         $info->binary         = false;
536         $info->unsigned       = null;
537         $info->auto_increment = false;
538         $info->unique         = null;
539         $info->scale          = null;
541         if ($info->meta_type === 'C') {
542             $info->max_length = $rawcolumn->character_maximum_length;
544         } else if ($info->meta_type === 'I') {
545             if ($info->primary_key) {
546                 $info->meta_type = 'R';
547                 $info->unique    = true;
548             }
549             $info->max_length    = $rawcolumn->numeric_precision;
550             $info->unsigned      = (stripos($rawcolumn->column_type, 'unsigned') !== false);
551             $info->auto_increment= (strpos($rawcolumn->extra, 'auto_increment') !== false);
553         } else if ($info->meta_type === 'N') {
554             $info->max_length    = $rawcolumn->numeric_precision;
555             $info->scale         = $rawcolumn->numeric_scale;
556             $info->unsigned      = (stripos($rawcolumn->column_type, 'unsigned') !== false);
558         } else if ($info->meta_type === 'X') {
559             if ("$rawcolumn->character_maximum_length" === '4294967295') { // watch out for PHP max int limits!
560                 // means maximum moodle size for text column, in other drivers it may also mean unknown size
561                 $info->max_length = -1;
562             } else {
563                 $info->max_length = $rawcolumn->character_maximum_length;
564             }
565             $info->primary_key   = false;
567         } else if ($info->meta_type === 'B') {
568             $info->max_length    = -1;
569             $info->primary_key   = false;
570             $info->binary        = true;
571         }
573         return $info;
574     }
576     /**
577      * Normalise column type.
578      * @param string $mysql_type
579      * @return string one character
580      * @throws dml_exception
581      */
582     private function mysqltype2moodletype($mysql_type) {
583         $type = null;
585         switch(strtoupper($mysql_type)) {
586             case 'BIT':
587                 $type = 'L';
588                 break;
590             case 'TINYINT':
591             case 'SMALLINT':
592             case 'MEDIUMINT':
593             case 'INT':
594             case 'BIGINT':
595                 $type = 'I';
596                 break;
598             case 'FLOAT':
599             case 'DOUBLE':
600             case 'DECIMAL':
601                 $type = 'N';
602                 break;
604             case 'CHAR':
605             case 'ENUM':
606             case 'SET':
607             case 'VARCHAR':
608                 $type = 'C';
609                 break;
611             case 'TINYTEXT':
612             case 'TEXT':
613             case 'MEDIUMTEXT':
614             case 'LONGTEXT':
615                 $type = 'X';
616                 break;
618             case 'BINARY':
619             case 'VARBINARY':
620             case 'BLOB':
621             case 'TINYBLOB':
622             case 'MEDIUMBLOB':
623             case 'LONGBLOB':
624                 $type = 'B';
625                 break;
627             case 'DATE':
628             case 'TIME':
629             case 'DATETIME':
630             case 'TIMESTAMP':
631             case 'YEAR':
632                 $type = 'D';
633                 break;
634         }
636         if (!$type) {
637             throw new dml_exception('invalidmysqlnativetype', $mysql_type);
638         }
639         return $type;
640     }
642     /**
643      * Normalise values based in RDBMS dependencies (booleans, LOBs...)
644      *
645      * @param database_column_info $column column metadata corresponding with the value we are going to normalise
646      * @param mixed $value value we are going to normalise
647      * @return mixed the normalised value
648      */
649     protected function normalise_value($column, $value) {
650         $this->detect_objects($value);
652         if (is_bool($value)) { // Always, convert boolean to int
653             $value = (int)$value;
655         } else if ($value === '') {
656             if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
657                 $value = 0; // prevent '' problems in numeric fields
658             }
659         // Any float value being stored in varchar or text field is converted to string to avoid
660         // any implicit conversion by MySQL
661         } else if (is_float($value) and ($column->meta_type == 'C' or $column->meta_type == 'X')) {
662             $value = "$value";
663         }
664         return $value;
665     }
667     /**
668      * Is db in unicode mode?
669      * @return bool
670      */
671     public function setup_is_unicodedb() {
672         $sql = "SHOW LOCAL VARIABLES LIKE 'character_set_database'";
673         $this->query_start($sql, null, SQL_QUERY_AUX);
674         $result = $this->mysqli->query($sql);
675         $this->query_end($result);
677         $return = false;
678         if ($result) {
679             while($row = $result->fetch_assoc()) {
680                 if (isset($row['Value'])) {
681                     $return = (strtoupper($row['Value']) === 'UTF8' or strtoupper($row['Value']) === 'UTF-8');
682                 }
683                 break;
684             }
685             $result->close();
686         }
688         if (!$return) {
689             return false;
690         }
692         $sql = "SHOW LOCAL VARIABLES LIKE 'collation_database'";
693         $this->query_start($sql, null, SQL_QUERY_AUX);
694         $result = $this->mysqli->query($sql);
695         $this->query_end($result);
697         $return = false;
698         if ($result) {
699             while($row = $result->fetch_assoc()) {
700                 if (isset($row['Value'])) {
701                     $return = (strpos($row['Value'], 'latin1') !== 0);
702                 }
703                 break;
704             }
705             $result->close();
706         }
708         return $return;
709     }
711     /**
712      * Do NOT use in code, to be used by database_manager only!
713      * @param string $sql query
714      * @return bool true
715      * @throws dml_exception A DML specific exception is thrown for any errors.
716      */
717     public function change_database_structure($sql) {
718         $this->reset_caches();
720         $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
721         $result = $this->mysqli->query($sql);
722         $this->query_end($result);
724         return true;
725     }
727     /**
728      * Very ugly hack which emulates bound parameters in queries
729      * because prepared statements do not use query cache.
730      */
731     protected function emulate_bound_params($sql, array $params=null) {
732         if (empty($params)) {
733             return $sql;
734         }
735         // ok, we have verified sql statement with ? and correct number of params
736         $parts = explode('?', $sql);
737         $return = array_shift($parts);
738         foreach ($params as $param) {
739             if (is_bool($param)) {
740                 $return .= (int)$param;
741             } else if (is_null($param)) {
742                 $return .= 'NULL';
743             } else if (is_number($param)) {
744                 $return .= "'".$param."'"; // we have to always use strings because mysql is using weird automatic int casting
745             } else if (is_float($param)) {
746                 $return .= $param;
747             } else {
748                 $param = $this->mysqli->real_escape_string($param);
749                 $return .= "'$param'";
750             }
751             $return .= array_shift($parts);
752         }
753         return $return;
754     }
756     /**
757      * Execute general sql query. Should be used only when no other method suitable.
758      * Do NOT use this to make changes in db structure, use database_manager methods instead!
759      * @param string $sql query
760      * @param array $params query parameters
761      * @return bool true
762      * @throws dml_exception A DML specific exception is thrown for any errors.
763      */
764     public function execute($sql, array $params=null) {
765         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
767         if (strpos($sql, ';') !== false) {
768             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
769         }
771         $rawsql = $this->emulate_bound_params($sql, $params);
773         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
774         $result = $this->mysqli->query($rawsql);
775         $this->query_end($result);
777         if ($result === true) {
778             return true;
780         } else {
781             $result->close();
782             return true;
783         }
784     }
786     /**
787      * Get a number of records as a moodle_recordset using a SQL statement.
788      *
789      * Since this method is a little less readable, use of it should be restricted to
790      * code where it's possible there might be large datasets being returned.  For known
791      * small datasets use get_records_sql - it leads to simpler code.
792      *
793      * The return type is like:
794      * @see function get_recordset.
795      *
796      * @param string $sql the SQL select query to execute.
797      * @param array $params array of sql parameters
798      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
799      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
800      * @return moodle_recordset instance
801      * @throws dml_exception A DML specific exception is thrown for any errors.
802      */
803     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
804         $limitfrom = (int)$limitfrom;
805         $limitnum  = (int)$limitnum;
806         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
807         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
809         if ($limitfrom or $limitnum) {
810             if ($limitnum < 1) {
811                 $limitnum = "18446744073709551615";
812             }
813             $sql .= " LIMIT $limitfrom, $limitnum";
814         }
816         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
817         $rawsql = $this->emulate_bound_params($sql, $params);
819         $this->query_start($sql, $params, SQL_QUERY_SELECT);
820         // no MYSQLI_USE_RESULT here, it would block write ops on affected tables
821         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
822         $this->query_end($result);
824         return $this->create_recordset($result);
825     }
827     protected function create_recordset($result) {
828         return new mysqli_native_moodle_recordset($result);
829     }
831     /**
832      * Get a number of records as an array of objects using a SQL statement.
833      *
834      * Return value is like:
835      * @see function get_records.
836      *
837      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
838      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
839      *   returned array.
840      * @param array $params array of sql parameters
841      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
842      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
843      * @return array of objects, or empty array if no records were found
844      * @throws dml_exception A DML specific exception is thrown for any errors.
845      */
846     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
847         $limitfrom = (int)$limitfrom;
848         $limitnum  = (int)$limitnum;
849         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
850         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
852         if ($limitfrom or $limitnum) {
853             if ($limitnum < 1) {
854                 $limitnum = "18446744073709551615";
855             }
856             $sql .= " LIMIT $limitfrom, $limitnum";
857         }
859         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
860         $rawsql = $this->emulate_bound_params($sql, $params);
862         $this->query_start($sql, $params, SQL_QUERY_SELECT);
863         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
864         $this->query_end($result);
866         $return = array();
868         while($row = $result->fetch_assoc()) {
869             $row = array_change_key_case($row, CASE_LOWER);
870             $id  = reset($row);
871             if (isset($return[$id])) {
872                 $colname = key($row);
873                 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);
874             }
875             $return[$id] = (object)$row;
876         }
877         $result->close();
879         return $return;
880     }
882     /**
883      * Selects records and return values (first field) as an array using a SQL statement.
884      *
885      * @param string $sql The SQL query
886      * @param array $params array of sql parameters
887      * @return array of values
888      * @throws dml_exception A DML specific exception is thrown for any errors.
889      */
890     public function get_fieldset_sql($sql, array $params=null) {
891         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
892         $rawsql = $this->emulate_bound_params($sql, $params);
894         $this->query_start($sql, $params, SQL_QUERY_SELECT);
895         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
896         $this->query_end($result);
898         $return = array();
900         while($row = $result->fetch_assoc()) {
901             $return[] = reset($row);
902         }
903         $result->close();
905         return $return;
906     }
908     /**
909      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
910      * @param string $table name
911      * @param mixed $params data record as object or array
912      * @param bool $returnit return it of inserted record
913      * @param bool $bulk true means repeated inserts expected
914      * @param bool $customsequence true if 'id' included in $params, disables $returnid
915      * @return bool|int true or new id
916      * @throws dml_exception A DML specific exception is thrown for any errors.
917      */
918     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
919         if (!is_array($params)) {
920             $params = (array)$params;
921         }
923         if ($customsequence) {
924             if (!isset($params['id'])) {
925                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
926             }
927             $returnid = false;
928         } else {
929             unset($params['id']);
930         }
932         if (empty($params)) {
933             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
934         }
936         $fields = implode(',', array_keys($params));
937         $qms    = array_fill(0, count($params), '?');
938         $qms    = implode(',', $qms);
940         $sql = "INSERT INTO {$this->prefix}$table ($fields) VALUES($qms)";
942         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
943         $rawsql = $this->emulate_bound_params($sql, $params);
945         $this->query_start($sql, $params, SQL_QUERY_INSERT);
946         $result = $this->mysqli->query($rawsql);
947         $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db
948         $this->query_end($result);
950         if (!$customsequence and !$id) {
951             throw new dml_write_exception('unknown error fetching inserted id');
952         }
954         if (!$returnid) {
955             return true;
956         } else {
957             return (int)$id;
958         }
959     }
961     /**
962      * Insert a record into a table and return the "id" field if required.
963      *
964      * Some conversions and safety checks are carried out. Lobs are supported.
965      * If the return ID isn't required, then this just reports success as true/false.
966      * $data is an object containing needed data
967      * @param string $table The database table to be inserted into
968      * @param object $data A data object with values for one or more fields in the record
969      * @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.
970      * @return bool|int true or new id
971      * @throws dml_exception A DML specific exception is thrown for any errors.
972      */
973     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
974         $dataobject = (array)$dataobject;
976         $columns = $this->get_columns($table);
977         $cleaned = array();
979         foreach ($dataobject as $field=>$value) {
980             if ($field === 'id') {
981                 continue;
982             }
983             if (!isset($columns[$field])) {
984                 continue;
985             }
986             $column = $columns[$field];
987             $cleaned[$field] = $this->normalise_value($column, $value);
988         }
990         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
991     }
993     /**
994      * Import a record into a table, id field is required.
995      * Safety checks are NOT carried out. Lobs are supported.
996      *
997      * @param string $table name of database table to be inserted into
998      * @param object $dataobject A data object with values for one or more fields in the record
999      * @return bool true
1000      * @throws dml_exception A DML specific exception is thrown for any errors.
1001      */
1002     public function import_record($table, $dataobject) {
1003         $dataobject = (array)$dataobject;
1005         $columns = $this->get_columns($table);
1006         $cleaned = array();
1008         foreach ($dataobject as $field=>$value) {
1009             if (!isset($columns[$field])) {
1010                 continue;
1011             }
1012             $cleaned[$field] = $value;
1013         }
1015         return $this->insert_record_raw($table, $cleaned, false, true, true);
1016     }
1018     /**
1019      * Update record in database, as fast as possible, no safety checks, lobs not supported.
1020      * @param string $table name
1021      * @param mixed $params data record as object or array
1022      * @param bool true means repeated updates expected
1023      * @return bool true
1024      * @throws dml_exception A DML specific exception is thrown for any errors.
1025      */
1026     public function update_record_raw($table, $params, $bulk=false) {
1027         $params = (array)$params;
1029         if (!isset($params['id'])) {
1030             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1031         }
1032         $id = $params['id'];
1033         unset($params['id']);
1035         if (empty($params)) {
1036             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1037         }
1039         $sets = array();
1040         foreach ($params as $field=>$value) {
1041             $sets[] = "$field = ?";
1042         }
1044         $params[] = $id; // last ? in WHERE condition
1046         $sets = implode(',', $sets);
1047         $sql = "UPDATE {$this->prefix}$table SET $sets WHERE id=?";
1049         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1050         $rawsql = $this->emulate_bound_params($sql, $params);
1052         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1053         $result = $this->mysqli->query($rawsql);
1054         $this->query_end($result);
1056         return true;
1057     }
1059     /**
1060      * Update a record in a table
1061      *
1062      * $dataobject is an object containing needed data
1063      * Relies on $dataobject having a variable "id" to
1064      * specify the record to update
1065      *
1066      * @param string $table The database table to be checked against.
1067      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1068      * @param bool true means repeated updates expected
1069      * @return bool true
1070      * @throws dml_exception A DML specific exception is thrown for any errors.
1071      */
1072     public function update_record($table, $dataobject, $bulk=false) {
1073         $dataobject = (array)$dataobject;
1075         $columns = $this->get_columns($table);
1076         $cleaned = array();
1078         foreach ($dataobject as $field=>$value) {
1079             if (!isset($columns[$field])) {
1080                 continue;
1081             }
1082             $column = $columns[$field];
1083             $cleaned[$field] = $this->normalise_value($column, $value);
1084         }
1086         return $this->update_record_raw($table, $cleaned, $bulk);
1087     }
1089     /**
1090      * Set a single field in every table record which match a particular WHERE clause.
1091      *
1092      * @param string $table The database table to be checked against.
1093      * @param string $newfield the field to set.
1094      * @param string $newvalue the value to set the field to.
1095      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1096      * @param array $params array of sql parameters
1097      * @return bool true
1098      * @throws dml_exception A DML specific exception is thrown for any errors.
1099      */
1100     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1101         if ($select) {
1102             $select = "WHERE $select";
1103         }
1104         if (is_null($params)) {
1105             $params = array();
1106         }
1107         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1109         // Get column metadata
1110         $columns = $this->get_columns($table);
1111         $column = $columns[$newfield];
1113         $normalised_value = $this->normalise_value($column, $newvalue);
1115         if (is_null($normalised_value)) {
1116             $newfield = "$newfield = NULL";
1117         } else {
1118             $newfield = "$newfield = ?";
1119             array_unshift($params, $normalised_value);
1120         }
1121         $sql = "UPDATE {$this->prefix}$table SET $newfield $select";
1122         $rawsql = $this->emulate_bound_params($sql, $params);
1124         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1125         $result = $this->mysqli->query($rawsql);
1126         $this->query_end($result);
1128         return true;
1129     }
1131     /**
1132      * Delete one or more records from a table which match a particular WHERE clause.
1133      *
1134      * @param string $table The database table to be checked against.
1135      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1136      * @param array $params array of sql parameters
1137      * @return bool true
1138      * @throws dml_exception A DML specific exception is thrown for any errors.
1139      */
1140     public function delete_records_select($table, $select, array $params=null) {
1141         if ($select) {
1142             $select = "WHERE $select";
1143         }
1144         $sql = "DELETE FROM {$this->prefix}$table $select";
1146         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1147         $rawsql = $this->emulate_bound_params($sql, $params);
1149         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1150         $result = $this->mysqli->query($rawsql);
1151         $this->query_end($result);
1153         return true;
1154     }
1156     public function sql_cast_char2int($fieldname, $text=false) {
1157         return ' CAST(' . $fieldname . ' AS SIGNED) ';
1158     }
1160     public function sql_cast_char2real($fieldname, $text=false) {
1161         return ' CAST(' . $fieldname . ' AS DECIMAL) ';
1162     }
1164     /**
1165      * Returns 'LIKE' part of a query.
1166      *
1167      * @param string $fieldname usually name of the table column
1168      * @param string $param usually bound query parameter (?, :named)
1169      * @param bool $casesensitive use case sensitive search
1170      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1171      * @param bool $notlike true means "NOT LIKE"
1172      * @param string $escapechar escape char for '%' and '_'
1173      * @return string SQL code fragment
1174      */
1175     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1176         if (strpos($param, '%') !== false) {
1177             debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1178         }
1179         $escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\'
1181         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1182         if ($casesensitive) {
1183             return "$fieldname $LIKE $param COLLATE utf8_bin ESCAPE '$escapechar'";
1184         } else {
1185             if ($accentsensitive) {
1186                 return "LOWER($fieldname) $LIKE LOWER($param) COLLATE utf8_bin ESCAPE '$escapechar'";
1187             } else {
1188                 return "$fieldname $LIKE $param ESCAPE '$escapechar'";
1189             }
1190         }
1191     }
1193     /**
1194      * Returns the proper SQL to do CONCAT between the elements passed
1195      * Can take many parameters
1196      *
1197      * @param string $str,... 1 or more fields/strings to concat
1198      *
1199      * @return string The concat sql
1200      */
1201     public function sql_concat() {
1202         $arr = func_get_args();
1203         $s = implode(', ', $arr);
1204         if ($s === '') {
1205             return "''";
1206         }
1207         return "CONCAT($s)";
1208     }
1210     /**
1211      * Returns the proper SQL to do CONCAT between the elements passed
1212      * with a given separator
1213      *
1214      * @param string $separator The string to use as the separator
1215      * @param array $elements An array of items to concatenate
1216      * @return string The concat SQL
1217      */
1218     public function sql_concat_join($separator="' '", $elements=array()) {
1219         $s = implode(', ', $elements);
1221         if ($s === '') {
1222             return "''";
1223         }
1224         return "CONCAT_WS($separator, $s)";
1225     }
1227     /**
1228      * Returns the SQL text to be used to calculate the length in characters of one expression.
1229      * @param string fieldname or expression to calculate its length in characters.
1230      * @return string the piece of SQL code to be used in the statement.
1231      */
1232     public function sql_length($fieldname) {
1233         return ' CHAR_LENGTH(' . $fieldname . ')';
1234     }
1236     /**
1237      * Does this driver support regex syntax when searching
1238      */
1239     public function sql_regex_supported() {
1240         return true;
1241     }
1243     /**
1244      * Return regex positive or negative match sql
1245      * @param bool $positivematch
1246      * @return string or empty if not supported
1247      */
1248     public function sql_regex($positivematch=true) {
1249         return $positivematch ? 'REGEXP' : 'NOT REGEXP';
1250     }
1252     /**
1253      * Returns the SQL to be used in order to an UNSIGNED INTEGER column to SIGNED.
1254      *
1255      * @deprecated since 2.3
1256      * @param string $fieldname The name of the field to be cast
1257      * @return string The piece of SQL code to be used in your statement.
1258      */
1259     public function sql_cast_2signed($fieldname) {
1260         return ' CAST(' . $fieldname . ' AS SIGNED) ';
1261     }
1263     public function session_lock_supported() {
1264         return true;
1265     }
1267     /**
1268      * Obtain session lock
1269      * @param int $rowid id of the row with session record
1270      * @param int $timeout max allowed time to wait for the lock in seconds
1271      * @return void
1272      */
1273     public function get_session_lock($rowid, $timeout) {
1274         parent::get_session_lock($rowid, $timeout);
1276         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1277         $sql = "SELECT GET_LOCK('$fullname', $timeout)";
1278         $this->query_start($sql, null, SQL_QUERY_AUX);
1279         $result = $this->mysqli->query($sql);
1280         $this->query_end($result);
1282         if ($result) {
1283             $arr = $result->fetch_assoc();
1284             $result->close();
1286             if (reset($arr) == 1) {
1287                 return;
1288             } else {
1289                 throw new dml_sessionwait_exception();
1290             }
1291         }
1292     }
1294     public function release_session_lock($rowid) {
1295         parent::release_session_lock($rowid);
1296         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1297         $sql = "SELECT RELEASE_LOCK('$fullname')";
1298         $this->query_start($sql, null, SQL_QUERY_AUX);
1299         $result = $this->mysqli->query($sql);
1300         $this->query_end($result);
1302         if ($result) {
1303             $result->close();
1304         }
1305     }
1307     /**
1308      * Are transactions supported?
1309      * It is not responsible to run productions servers
1310      * on databases without transaction support ;-)
1311      *
1312      * MyISAM does not support support transactions.
1313      *
1314      * You can override this via the dbtransactions option.
1315      *
1316      * @return bool
1317      */
1318     protected function transactions_supported() {
1319         if (!is_null($this->transactions_supported)) {
1320             return $this->transactions_supported;
1321         }
1323         // this is all just guessing, might be better to just specify it in config.php
1324         if (isset($this->dboptions['dbtransactions'])) {
1325             $this->transactions_supported = $this->dboptions['dbtransactions'];
1326             return $this->transactions_supported;
1327         }
1329         $this->transactions_supported = false;
1331         $engine = $this->get_dbengine();
1333         // Only will accept transactions if using compatible storage engine (more engines can be added easily BDB, Falcon...)
1334         if (in_array($engine, array('InnoDB', 'INNOBASE', 'BDB', 'XtraDB', 'Aria', 'Falcon'))) {
1335             $this->transactions_supported = true;
1336         }
1338         return $this->transactions_supported;
1339     }
1341     /**
1342      * Driver specific start of real database transaction,
1343      * this can not be used directly in code.
1344      * @return void
1345      */
1346     protected function begin_transaction() {
1347         if (!$this->transactions_supported()) {
1348             return;
1349         }
1351         $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
1352         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1353         $result = $this->mysqli->query($sql);
1354         $this->query_end($result);
1356         $sql = "START TRANSACTION";
1357         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1358         $result = $this->mysqli->query($sql);
1359         $this->query_end($result);
1360     }
1362     /**
1363      * Driver specific commit of real database transaction,
1364      * this can not be used directly in code.
1365      * @return void
1366      */
1367     protected function commit_transaction() {
1368         if (!$this->transactions_supported()) {
1369             return;
1370         }
1372         $sql = "COMMIT";
1373         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1374         $result = $this->mysqli->query($sql);
1375         $this->query_end($result);
1376     }
1378     /**
1379      * Driver specific abort of real database transaction,
1380      * this can not be used directly in code.
1381      * @return void
1382      */
1383     protected function rollback_transaction() {
1384         if (!$this->transactions_supported()) {
1385             return;
1386         }
1388         $sql = "ROLLBACK";
1389         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1390         $result = $this->mysqli->query($sql);
1391         $this->query_end($result);
1393         return true;
1394     }