weekly release 2.3dev
[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         if (is_bool($value)) { // Always, convert boolean to int
655             $value = (int)$value;
657         } else if ($value === '') {
658             if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
659                 $value = 0; // prevent '' problems in numeric fields
660             }
661         // Any float value being stored in varchar or text field is converted to string to avoid
662         // any implicit conversion by MySQL
663         } else if (is_float($value) and ($column->meta_type == 'C' or $column->meta_type == 'X')) {
664             $value = "$value";
665         }
666         return $value;
667     }
669     /**
670      * Is db in unicode mode?
671      * @return bool
672      */
673     public function setup_is_unicodedb() {
674         $sql = "SHOW LOCAL VARIABLES LIKE 'character_set_database'";
675         $this->query_start($sql, null, SQL_QUERY_AUX);
676         $result = $this->mysqli->query($sql);
677         $this->query_end($result);
679         $return = false;
680         if ($result) {
681             while($row = $result->fetch_assoc()) {
682                 if (isset($row['Value'])) {
683                     $return = (strtoupper($row['Value']) === 'UTF8' or strtoupper($row['Value']) === 'UTF-8');
684                 }
685                 break;
686             }
687             $result->close();
688         }
690         if (!$return) {
691             return false;
692         }
694         $sql = "SHOW LOCAL VARIABLES LIKE 'collation_database'";
695         $this->query_start($sql, null, SQL_QUERY_AUX);
696         $result = $this->mysqli->query($sql);
697         $this->query_end($result);
699         $return = false;
700         if ($result) {
701             while($row = $result->fetch_assoc()) {
702                 if (isset($row['Value'])) {
703                     $return = (strpos($row['Value'], 'latin1') !== 0);
704                 }
705                 break;
706             }
707             $result->close();
708         }
710         return $return;
711     }
713     /**
714      * Do NOT use in code, to be used by database_manager only!
715      * @param string $sql query
716      * @return bool true
717      * @throws dml_exception A DML specific exception is thrown for any errors.
718      */
719     public function change_database_structure($sql) {
720         $this->reset_caches();
722         $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
723         $result = $this->mysqli->query($sql);
724         $this->query_end($result);
726         return true;
727     }
729     /**
730      * Very ugly hack which emulates bound parameters in queries
731      * because prepared statements do not use query cache.
732      */
733     protected function emulate_bound_params($sql, array $params=null) {
734         if (empty($params)) {
735             return $sql;
736         }
737         /// ok, we have verified sql statement with ? and correct number of params
738         $parts = explode('?', $sql);
739         $return = array_shift($parts);
740         foreach ($params as $param) {
741             if (is_bool($param)) {
742                 $return .= (int)$param;
743             } else if (is_null($param)) {
744                 $return .= 'NULL';
745             } else if (is_number($param)) {
746                 $return .= "'".$param."'"; // we have to always use strings because mysql is using weird automatic int casting
747             } else if (is_float($param)) {
748                 $return .= $param;
749             } else {
750                 $param = $this->mysqli->real_escape_string($param);
751                 $return .= "'$param'";
752             }
753             $return .= array_shift($parts);
754         }
755         return $return;
756     }
758     /**
759      * Execute general sql query. Should be used only when no other method suitable.
760      * Do NOT use this to make changes in db structure, use database_manager methods instead!
761      * @param string $sql query
762      * @param array $params query parameters
763      * @return bool true
764      * @throws dml_exception A DML specific exception is thrown for any errors.
765      */
766     public function execute($sql, array $params=null) {
767         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
769         if (strpos($sql, ';') !== false) {
770             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
771         }
773         $rawsql = $this->emulate_bound_params($sql, $params);
775         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
776         $result = $this->mysqli->query($rawsql);
777         $this->query_end($result);
779         if ($result === true) {
780             return true;
782         } else {
783             $result->close();
784             return true;
785         }
786     }
788     /**
789      * Get a number of records as a moodle_recordset using a SQL statement.
790      *
791      * Since this method is a little less readable, use of it should be restricted to
792      * code where it's possible there might be large datasets being returned.  For known
793      * small datasets use get_records_sql - it leads to simpler code.
794      *
795      * The return type is like:
796      * @see function get_recordset.
797      *
798      * @param string $sql the SQL select query to execute.
799      * @param array $params array of sql parameters
800      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
801      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
802      * @return moodle_recordset instance
803      * @throws dml_exception A DML specific exception is thrown for any errors.
804      */
805     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
806         $limitfrom = (int)$limitfrom;
807         $limitnum  = (int)$limitnum;
808         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
809         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
811         if ($limitfrom or $limitnum) {
812             if ($limitnum < 1) {
813                 $limitnum = "18446744073709551615";
814             }
815             $sql .= " LIMIT $limitfrom, $limitnum";
816         }
818         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
819         $rawsql = $this->emulate_bound_params($sql, $params);
821         $this->query_start($sql, $params, SQL_QUERY_SELECT);
822         // no MYSQLI_USE_RESULT here, it would block write ops on affected tables
823         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
824         $this->query_end($result);
826         return $this->create_recordset($result);
827     }
829     protected function create_recordset($result) {
830         return new mysqli_native_moodle_recordset($result);
831     }
833     /**
834      * Get a number of records as an array of objects using a SQL statement.
835      *
836      * Return value is like:
837      * @see function get_records.
838      *
839      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
840      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
841      *   returned array.
842      * @param array $params array of sql parameters
843      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
844      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
845      * @return array of objects, or empty array if no records were found
846      * @throws dml_exception A DML specific exception is thrown for any errors.
847      */
848     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
849         $limitfrom = (int)$limitfrom;
850         $limitnum  = (int)$limitnum;
851         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
852         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
854         if ($limitfrom or $limitnum) {
855             if ($limitnum < 1) {
856                 $limitnum = "18446744073709551615";
857             }
858             $sql .= " LIMIT $limitfrom, $limitnum";
859         }
861         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
862         $rawsql = $this->emulate_bound_params($sql, $params);
864         $this->query_start($sql, $params, SQL_QUERY_SELECT);
865         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
866         $this->query_end($result);
868         $return = array();
870         while($row = $result->fetch_assoc()) {
871             $row = array_change_key_case($row, CASE_LOWER);
872             $id  = reset($row);
873             if (isset($return[$id])) {
874                 $colname = key($row);
875                 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);
876             }
877             $return[$id] = (object)$row;
878         }
879         $result->close();
881         return $return;
882     }
884     /**
885      * Selects records and return values (first field) as an array using a SQL statement.
886      *
887      * @param string $sql The SQL query
888      * @param array $params array of sql parameters
889      * @return array of values
890      * @throws dml_exception A DML specific exception is thrown for any errors.
891      */
892     public function get_fieldset_sql($sql, array $params=null) {
893         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
894         $rawsql = $this->emulate_bound_params($sql, $params);
896         $this->query_start($sql, $params, SQL_QUERY_SELECT);
897         $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
898         $this->query_end($result);
900         $return = array();
902         while($row = $result->fetch_assoc()) {
903             $return[] = reset($row);
904         }
905         $result->close();
907         return $return;
908     }
910     /**
911      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
912      * @param string $table name
913      * @param mixed $params data record as object or array
914      * @param bool $returnit return it of inserted record
915      * @param bool $bulk true means repeated inserts expected
916      * @param bool $customsequence true if 'id' included in $params, disables $returnid
917      * @return bool|int true or new id
918      * @throws dml_exception A DML specific exception is thrown for any errors.
919      */
920     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
921         if (!is_array($params)) {
922             $params = (array)$params;
923         }
925         if ($customsequence) {
926             if (!isset($params['id'])) {
927                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
928             }
929             $returnid = false;
930         } else {
931             unset($params['id']);
932         }
934         if (empty($params)) {
935             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
936         }
938         $fields = implode(',', array_keys($params));
939         $qms    = array_fill(0, count($params), '?');
940         $qms    = implode(',', $qms);
942         $sql = "INSERT INTO {$this->prefix}$table ($fields) VALUES($qms)";
944         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
945         $rawsql = $this->emulate_bound_params($sql, $params);
947         $this->query_start($sql, $params, SQL_QUERY_INSERT);
948         $result = $this->mysqli->query($rawsql);
949         $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db
950         $this->query_end($result);
952         if (!$id) {
953             throw new dml_write_exception('unknown error fetching inserted id');
954         }
956         if (!$returnid) {
957             return true;
958         } else {
959             return (int)$id;
960         }
961     }
963     /**
964      * Insert a record into a table and return the "id" field if required.
965      *
966      * Some conversions and safety checks are carried out. Lobs are supported.
967      * If the return ID isn't required, then this just reports success as true/false.
968      * $data is an object containing needed data
969      * @param string $table The database table to be inserted into
970      * @param object $data A data object with values for one or more fields in the record
971      * @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.
972      * @return bool|int true or new id
973      * @throws dml_exception A DML specific exception is thrown for any errors.
974      */
975     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
976         $dataobject = (array)$dataobject;
978         $columns = $this->get_columns($table);
979         $cleaned = array();
981         foreach ($dataobject as $field=>$value) {
982             if ($field === 'id') {
983                 continue;
984             }
985             if (!isset($columns[$field])) {
986                 continue;
987             }
988             $column = $columns[$field];
989             $cleaned[$field] = $this->normalise_value($column, $value);
990         }
992         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
993     }
995     /**
996      * Import a record into a table, id field is required.
997      * Safety checks are NOT carried out. Lobs are supported.
998      *
999      * @param string $table name of database table to be inserted into
1000      * @param object $dataobject A data object with values for one or more fields in the record
1001      * @return bool true
1002      * @throws dml_exception A DML specific exception is thrown for any errors.
1003      */
1004     public function import_record($table, $dataobject) {
1005         $dataobject = (array)$dataobject;
1007         $columns = $this->get_columns($table);
1008         $cleaned = array();
1010         foreach ($dataobject as $field=>$value) {
1011             if (!isset($columns[$field])) {
1012                 continue;
1013             }
1014             $cleaned[$field] = $value;
1015         }
1017         return $this->insert_record_raw($table, $cleaned, false, true, true);
1018     }
1020     /**
1021      * Update record in database, as fast as possible, no safety checks, lobs not supported.
1022      * @param string $table name
1023      * @param mixed $params data record as object or array
1024      * @param bool true means repeated updates expected
1025      * @return bool true
1026      * @throws dml_exception A DML specific exception is thrown for any errors.
1027      */
1028     public function update_record_raw($table, $params, $bulk=false) {
1029         $params = (array)$params;
1031         if (!isset($params['id'])) {
1032             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1033         }
1034         $id = $params['id'];
1035         unset($params['id']);
1037         if (empty($params)) {
1038             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1039         }
1041         $sets = array();
1042         foreach ($params as $field=>$value) {
1043             $sets[] = "$field = ?";
1044         }
1046         $params[] = $id; // last ? in WHERE condition
1048         $sets = implode(',', $sets);
1049         $sql = "UPDATE {$this->prefix}$table SET $sets WHERE id=?";
1051         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1052         $rawsql = $this->emulate_bound_params($sql, $params);
1054         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1055         $result = $this->mysqli->query($rawsql);
1056         $this->query_end($result);
1058         return true;
1059     }
1061     /**
1062      * Update a record in a table
1063      *
1064      * $dataobject is an object containing needed data
1065      * Relies on $dataobject having a variable "id" to
1066      * specify the record to update
1067      *
1068      * @param string $table The database table to be checked against.
1069      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1070      * @param bool true means repeated updates expected
1071      * @return bool true
1072      * @throws dml_exception A DML specific exception is thrown for any errors.
1073      */
1074     public function update_record($table, $dataobject, $bulk=false) {
1075         $dataobject = (array)$dataobject;
1077         $columns = $this->get_columns($table);
1078         $cleaned = array();
1080         foreach ($dataobject as $field=>$value) {
1081             if (!isset($columns[$field])) {
1082                 continue;
1083             }
1084             $column = $columns[$field];
1085             $cleaned[$field] = $this->normalise_value($column, $value);
1086         }
1088         return $this->update_record_raw($table, $cleaned, $bulk);
1089     }
1091     /**
1092      * Set a single field in every table record which match a particular WHERE clause.
1093      *
1094      * @param string $table The database table to be checked against.
1095      * @param string $newfield the field to set.
1096      * @param string $newvalue the value to set the field to.
1097      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1098      * @param array $params array of sql parameters
1099      * @return bool true
1100      * @throws dml_exception A DML specific exception is thrown for any errors.
1101      */
1102     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1103         if ($select) {
1104             $select = "WHERE $select";
1105         }
1106         if (is_null($params)) {
1107             $params = array();
1108         }
1109         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1111         // Get column metadata
1112         $columns = $this->get_columns($table);
1113         $column = $columns[$newfield];
1115         $normalised_value = $this->normalise_value($column, $newvalue);
1117         if (is_null($normalised_value)) {
1118             $newfield = "$newfield = NULL";
1119         } else {
1120             $newfield = "$newfield = ?";
1121             array_unshift($params, $normalised_value);
1122         }
1123         $sql = "UPDATE {$this->prefix}$table SET $newfield $select";
1124         $rawsql = $this->emulate_bound_params($sql, $params);
1126         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1127         $result = $this->mysqli->query($rawsql);
1128         $this->query_end($result);
1130         return true;
1131     }
1133     /**
1134      * Delete one or more records from a table which match a particular WHERE clause.
1135      *
1136      * @param string $table The database table to be checked against.
1137      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1138      * @param array $params array of sql parameters
1139      * @return bool true
1140      * @throws dml_exception A DML specific exception is thrown for any errors.
1141      */
1142     public function delete_records_select($table, $select, array $params=null) {
1143         if ($select) {
1144             $select = "WHERE $select";
1145         }
1146         $sql = "DELETE FROM {$this->prefix}$table $select";
1148         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1149         $rawsql = $this->emulate_bound_params($sql, $params);
1151         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1152         $result = $this->mysqli->query($rawsql);
1153         $this->query_end($result);
1155         return true;
1156     }
1158     public function sql_cast_char2int($fieldname, $text=false) {
1159         return ' CAST(' . $fieldname . ' AS SIGNED) ';
1160     }
1162     public function sql_cast_char2real($fieldname, $text=false) {
1163         return ' CAST(' . $fieldname . ' AS DECIMAL) ';
1164     }
1166     /**
1167      * Returns 'LIKE' part of a query.
1168      *
1169      * @param string $fieldname usually name of the table column
1170      * @param string $param usually bound query parameter (?, :named)
1171      * @param bool $casesensitive use case sensitive search
1172      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1173      * @param bool $notlike true means "NOT LIKE"
1174      * @param string $escapechar escape char for '%' and '_'
1175      * @return string SQL code fragment
1176      */
1177     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1178         if (strpos($param, '%') !== false) {
1179             debugging('Potential SQL injection detected, sql_ilike() expects bound parameters (? or :named)');
1180         }
1181         $escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\'
1183         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1184         if ($casesensitive) {
1185             return "$fieldname $LIKE $param COLLATE utf8_bin ESCAPE '$escapechar'";
1186         } else {
1187             if ($accentsensitive) {
1188                 return "LOWER($fieldname) $LIKE LOWER($param) COLLATE utf8_bin ESCAPE '$escapechar'";
1189             } else {
1190                 return "$fieldname $LIKE $param ESCAPE '$escapechar'";
1191             }
1192         }
1193     }
1195     /**
1196      * Returns the proper SQL to do CONCAT between the elements passed
1197      * Can take many parameters
1198      *
1199      * @param string $str,... 1 or more fields/strings to concat
1200      *
1201      * @return string The concat sql
1202      */
1203     public function sql_concat() {
1204         $arr = func_get_args();
1205         $s = implode(', ', $arr);
1206         if ($s === '') {
1207             return "''";
1208         }
1209         return "CONCAT($s)";
1210     }
1212     /**
1213      * Returns the proper SQL to do CONCAT between the elements passed
1214      * with a given separator
1215      *
1216      * @param string $separator The string to use as the separator
1217      * @param array $elements An array of items to concatenate
1218      * @return string The concat SQL
1219      */
1220     public function sql_concat_join($separator="' '", $elements=array()) {
1221         $s = implode(', ', $elements);
1223         if ($s === '') {
1224             return "''";
1225         }
1226         return "CONCAT_WS($separator, $s)";
1227     }
1229     /**
1230      * Returns the SQL text to be used to calculate the length in characters of one expression.
1231      * @param string fieldname or expression to calculate its length in characters.
1232      * @return string the piece of SQL code to be used in the statement.
1233      */
1234     public function sql_length($fieldname) {
1235         return ' CHAR_LENGTH(' . $fieldname . ')';
1236     }
1238     /**
1239      * Does this driver support regex syntax when searching
1240      */
1241     public function sql_regex_supported() {
1242         return true;
1243     }
1245     /**
1246      * Return regex positive or negative match sql
1247      * @param bool $positivematch
1248      * @return string or empty if not supported
1249      */
1250     public function sql_regex($positivematch=true) {
1251         return $positivematch ? 'REGEXP' : 'NOT REGEXP';
1252     }
1254     /**
1255      * Returns the SQL to be used in order to an UNSIGNED INTEGER column to SIGNED.
1256      *
1257      * @deprecated since 2.3
1258      * @param string $fieldname The name of the field to be cast
1259      * @return string The piece of SQL code to be used in your statement.
1260      */
1261     public function sql_cast_2signed($fieldname) {
1262         return ' CAST(' . $fieldname . ' AS SIGNED) ';
1263     }
1265 /// session locking
1266     public function session_lock_supported() {
1267         return true;
1268     }
1270     /**
1271      * Obtain session lock
1272      * @param int $rowid id of the row with session record
1273      * @param int $timeout max allowed time to wait for the lock in seconds
1274      * @return bool success
1275      */
1276     public function get_session_lock($rowid, $timeout) {
1277         parent::get_session_lock($rowid, $timeout);
1279         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1280         $sql = "SELECT GET_LOCK('$fullname', $timeout)";
1281         $this->query_start($sql, null, SQL_QUERY_AUX);
1282         $result = $this->mysqli->query($sql);
1283         $this->query_end($result);
1285         if ($result) {
1286             $arr = $result->fetch_assoc();
1287             $result->close();
1289             if (reset($arr) == 1) {
1290                 return;
1291             } else {
1292                 throw new dml_sessionwait_exception();
1293             }
1294         }
1295     }
1297     public function release_session_lock($rowid) {
1298         parent::release_session_lock($rowid);
1299         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1300         $sql = "SELECT RELEASE_LOCK('$fullname')";
1301         $this->query_start($sql, null, SQL_QUERY_AUX);
1302         $result = $this->mysqli->query($sql);
1303         $this->query_end($result);
1305         if ($result) {
1306             $result->close();
1307         }
1308     }
1310 /// transactions
1311     /**
1312      * Are transactions supported?
1313      * It is not responsible to run productions servers
1314      * on databases without transaction support ;-)
1315      *
1316      * MyISAM does not support support transactions.
1317      *
1318      * You can override this via the dbtransactions option.
1319      *
1320      * @return bool
1321      */
1322     protected function transactions_supported() {
1323         if (!is_null($this->transactions_supported)) {
1324             return $this->transactions_supported;
1325         }
1327         // this is all just guessing, might be better to just specify it in config.php
1328         if (isset($this->dboptions['dbtransactions'])) {
1329             $this->transactions_supported = $this->dboptions['dbtransactions'];
1330             return $this->transactions_supported;
1331         }
1333         $this->transactions_supported = false;
1335         $engine = $this->get_dbengine();
1337         // Only will accept transactions if using compatible storage engine (more engines can be added easily BDB, Falcon...)
1338         if (in_array($engine, array('InnoDB', 'INNOBASE', 'BDB', 'XtraDB', 'Aria', 'Falcon'))) {
1339             $this->transactions_supported = true;
1340         }
1342         return $this->transactions_supported;
1343     }
1345     /**
1346      * Driver specific start of real database transaction,
1347      * this can not be used directly in code.
1348      * @return void
1349      */
1350     protected function begin_transaction() {
1351         if (!$this->transactions_supported()) {
1352             return;
1353         }
1355         $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
1356         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1357         $result = $this->mysqli->query($sql);
1358         $this->query_end($result);
1360         $sql = "START TRANSACTION";
1361         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1362         $result = $this->mysqli->query($sql);
1363         $this->query_end($result);
1364     }
1366     /**
1367      * Driver specific commit of real database transaction,
1368      * this can not be used directly in code.
1369      * @return void
1370      */
1371     protected function commit_transaction() {
1372         if (!$this->transactions_supported()) {
1373             return;
1374         }
1376         $sql = "COMMIT";
1377         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1378         $result = $this->mysqli->query($sql);
1379         $this->query_end($result);
1380     }
1382     /**
1383      * Driver specific abort of real database transaction,
1384      * this can not be used directly in code.
1385      * @return void
1386      */
1387     protected function rollback_transaction() {
1388         if (!$this->transactions_supported()) {
1389             return;
1390         }
1392         $sql = "ROLLBACK";
1393         $this->query_start($sql, NULL, SQL_QUERY_AUX);
1394         $result = $this->mysqli->query($sql);
1395         $this->query_end($result);
1397         return true;
1398     }