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