2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Native mysqli class representing moodle database interface.
21 * @copyright 2008 Petr Skoda (http://skodak.org)
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 require_once(__DIR__.'/moodle_database.php');
28 require_once(__DIR__.'/mysqli_native_moodle_recordset.php');
29 require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
32 * Native mysqli class representing moodle database interface.
35 * @copyright 2008 Petr Skoda (http://skodak.org)
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 class mysqli_native_moodle_database extends moodle_database {
40 /** @var mysqli $mysqli */
41 protected $mysqli = null;
42 /** @var bool is compressed row format supported cache */
43 protected $compressedrowformatsupported = null;
45 private $transactions_supported = null;
48 * Attempt to create the database
49 * @param string $dbhost
50 * @param string $dbuser
51 * @param string $dbpass
52 * @param string $dbname
53 * @return bool success
54 * @throws dml_exception A DML specific exception is thrown for any errors.
56 public function create_database($dbhost, $dbuser, $dbpass, $dbname, array $dboptions=null) {
57 $driverstatus = $this->driver_installed();
59 if ($driverstatus !== true) {
60 throw new dml_exception('dbdriverproblem', $driverstatus);
63 if (!empty($dboptions['dbsocket'])
64 and (strpos($dboptions['dbsocket'], '/') !== false or strpos($dboptions['dbsocket'], '\\') !== false)) {
65 $dbsocket = $dboptions['dbsocket'];
67 $dbsocket = ini_get('mysqli.default_socket');
69 if (empty($dboptions['dbport'])) {
70 $dbport = (int)ini_get('mysqli.default_port');
72 $dbport = (int)$dboptions['dbport'];
74 // verify ini.get does not return nonsense
79 $conn = new mysqli($dbhost, $dbuser, $dbpass, '', $dbport, $dbsocket); // Connect without db
80 $dberr = ob_get_contents();
82 $errorno = @$conn->connect_errno;
85 throw new dml_connection_exception($dberr);
88 // Normally a check would be done before setting utf8mb4, but the database can be created
89 // before the enviroment checks are done. We'll proceed with creating the database and then do checks next.
91 if (isset($dboptions['dbcollation']) and (strpos($dboptions['dbcollation'], 'utf8_') === 0
92 || strpos($dboptions['dbcollation'], 'utf8mb4_') === 0)) {
93 $collation = $dboptions['dbcollation'];
94 $collationinfo = explode('_', $dboptions['dbcollation']);
95 $charset = reset($collationinfo);
97 $collation = 'utf8mb4_unicode_ci';
100 $result = $conn->query("CREATE DATABASE $dbname DEFAULT CHARACTER SET $charset DEFAULT COLLATE ".$collation);
105 throw new dml_exception('cannotcreatedb');
112 * Detects if all needed PHP stuff installed.
113 * Note: can be used before connect()
114 * @return mixed true if ok, string if something
116 public function driver_installed() {
117 if (!extension_loaded('mysqli')) {
118 return get_string('mysqliextensionisnotpresentinphp', 'install');
124 * Returns database family type - describes SQL dialect
125 * Note: can be used before connect()
126 * @return string db family name (mysql, postgres, mssql, oracle, etc.)
128 public function get_dbfamily() {
133 * Returns more specific database driver type
134 * Note: can be used before connect()
135 * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
137 protected function get_dbtype() {
142 * Returns general database library name
143 * Note: can be used before connect()
144 * @return string db type pdo, native
146 protected function get_dblibrary() {
151 * Returns the current MySQL db engine.
153 * This is an ugly workaround for MySQL default engine problems,
154 * Moodle is designed to work best on ACID compliant databases
155 * with full transaction support. Do not use MyISAM.
157 * @return string or null MySQL engine name
159 public function get_dbengine() {
160 if (isset($this->dboptions['dbengine'])) {
161 return $this->dboptions['dbengine'];
164 if ($this->external) {
170 // Look for current engine of our config table (the first table that gets created),
171 // so that we create all tables with the same engine.
172 $sql = "SELECT engine
173 FROM INFORMATION_SCHEMA.TABLES
174 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config'";
175 $this->query_start($sql, NULL, SQL_QUERY_AUX);
176 $result = $this->mysqli->query($sql);
177 $this->query_end($result);
178 if ($rec = $result->fetch_assoc()) {
179 $engine = $rec['engine'];
184 // Cache the result to improve performance.
185 $this->dboptions['dbengine'] = $engine;
189 // Get the default database engine.
190 $sql = "SELECT @@default_storage_engine engine";
191 $this->query_start($sql, NULL, SQL_QUERY_AUX);
192 $result = $this->mysqli->query($sql);
193 $this->query_end($result);
194 if ($rec = $result->fetch_assoc()) {
195 $engine = $rec['engine'];
199 if ($engine === 'MyISAM') {
200 // we really do not want MyISAM for Moodle, InnoDB or XtraDB is a reasonable defaults if supported
201 $sql = "SHOW STORAGE ENGINES";
202 $this->query_start($sql, NULL, SQL_QUERY_AUX);
203 $result = $this->mysqli->query($sql);
204 $this->query_end($result);
206 while ($res = $result->fetch_assoc()) {
207 if ($res['Support'] === 'YES' or $res['Support'] === 'DEFAULT') {
208 $engines[$res['Engine']] = true;
212 if (isset($engines['InnoDB'])) {
215 if (isset($engines['XtraDB'])) {
220 // Cache the result to improve performance.
221 $this->dboptions['dbengine'] = $engine;
226 * Returns the current MySQL db collation.
228 * This is an ugly workaround for MySQL default collation problems.
230 * @return string or null MySQL collation name
232 public function get_dbcollation() {
233 if (isset($this->dboptions['dbcollation'])) {
234 return $this->dboptions['dbcollation'];
236 if ($this->external) {
242 // Look for current collation of our config table (the first table that gets created),
243 // so that we create all tables with the same collation.
244 $sql = "SELECT collation_name
245 FROM INFORMATION_SCHEMA.COLUMNS
246 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config' AND column_name = 'value'";
247 $this->query_start($sql, NULL, SQL_QUERY_AUX);
248 $result = $this->mysqli->query($sql);
249 $this->query_end($result);
250 if ($rec = $result->fetch_assoc()) {
251 $collation = $rec['collation_name'];
257 // Get the default database collation, but only if using UTF-8.
258 $sql = "SELECT @@collation_database";
259 $this->query_start($sql, NULL, SQL_QUERY_AUX);
260 $result = $this->mysqli->query($sql);
261 $this->query_end($result);
262 if ($rec = $result->fetch_assoc()) {
263 if (strpos($rec['@@collation_database'], 'utf8_') === 0 || strpos($rec['@@collation_database'], 'utf8mb4_') === 0) {
264 $collation = $rec['@@collation_database'];
271 // We want only utf8 compatible collations.
273 $sql = "SHOW COLLATION WHERE Collation LIKE 'utf8mb4\_%' AND Charset = 'utf8mb4'";
274 $this->query_start($sql, NULL, SQL_QUERY_AUX);
275 $result = $this->mysqli->query($sql);
276 $this->query_end($result);
277 while ($res = $result->fetch_assoc()) {
278 $collation = $res['Collation'];
279 if (strtoupper($res['Default']) === 'YES') {
280 $collation = $res['Collation'];
287 // Cache the result to improve performance.
288 $this->dboptions['dbcollation'] = $collation;
293 * Get the row format from the database schema.
295 * @param string $table
296 * @return string row_format name or null if not known or table does not exist.
298 public function get_row_format($table = null) {
301 $table = $this->mysqli->real_escape_string($table);
302 $sql = "SELECT row_format
303 FROM INFORMATION_SCHEMA.TABLES
304 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}$table'";
306 $sql = "SHOW VARIABLES LIKE 'innodb_file_format'";
308 $this->query_start($sql, NULL, SQL_QUERY_AUX);
309 $result = $this->mysqli->query($sql);
310 $this->query_end($result);
311 if ($rec = $result->fetch_assoc()) {
313 $rowformat = $rec['row_format'];
315 $rowformat = $rec['Value'];
324 * Is this database compatible with compressed row format?
325 * This feature is necessary for support of large number of text
326 * columns in InnoDB/XtraDB database.
328 * @param bool $cached use cached result
329 * @return bool true if table can be created or changed to compressed row format.
331 public function is_compressed_row_format_supported($cached = true) {
332 if ($cached and isset($this->compressedrowformatsupported)) {
333 return($this->compressedrowformatsupported);
336 $engine = strtolower($this->get_dbengine());
337 $info = $this->get_server_info();
339 if (version_compare($info['version'], '5.5.0') < 0) {
340 // MySQL 5.1 is not supported here because we cannot read the file format.
341 $this->compressedrowformatsupported = false;
343 } else if ($engine !== 'innodb' and $engine !== 'xtradb') {
344 // Other engines are not supported, most probably not compatible.
345 $this->compressedrowformatsupported = false;
347 } else if (!$this->is_file_per_table_enabled()) {
348 $this->compressedrowformatsupported = false;
350 } else if ($this->get_row_format() !== 'Barracuda') {
351 $this->compressedrowformatsupported = false;
354 // All the tests passed, we can safely use ROW_FORMAT=Compressed in sql statements.
355 $this->compressedrowformatsupported = true;
358 return $this->compressedrowformatsupported;
362 * Check the database to see if innodb_file_per_table is on.
364 * @return bool True if on otherwise false.
366 public function is_file_per_table_enabled() {
367 if ($filepertable = $this->get_record_sql("SHOW VARIABLES LIKE 'innodb_file_per_table'")) {
368 if ($filepertable->value == 'ON') {
376 * Check the database to see if innodb_large_prefix is on.
378 * @return bool True if on otherwise false.
380 public function is_large_prefix_enabled() {
381 if ($largeprefix = $this->get_record_sql("SHOW VARIABLES LIKE 'innodb_large_prefix'")) {
382 if ($largeprefix->value == 'ON') {
390 * Determine if the row format should be set to compressed, dynamic, or default.
392 * Terrible kludge. If we're using utf8mb4 AND we're using InnoDB, we need to specify row format to
393 * be either dynamic or compressed (default is compact) in order to allow for bigger indexes (MySQL
394 * errors #1709 and #1071).
396 * @param string $engine The database engine being used. Will be looked up if not supplied.
397 * @param string $collation The database collation to use. Will look up the current collation if not supplied.
398 * @return string An sql fragment to add to sql statements.
400 public function get_row_format_sql($engine = null, $collation = null) {
402 if (!isset($engine)) {
403 $engine = $this->get_dbengine();
405 $engine = strtolower($engine);
407 if (!isset($collation)) {
408 $collation = $this->get_dbcollation();
412 if (($engine === 'innodb' || $engine === 'xtradb') && strpos($collation, 'utf8mb4_') === 0) {
413 if ($this->is_compressed_row_format_supported()) {
414 $rowformat = "ROW_FORMAT=Compressed";
416 $rowformat = "ROW_FORMAT=Dynamic";
423 * Returns localised database type name
424 * Note: can be used before connect()
427 public function get_name() {
428 return get_string('nativemysqli', 'install');
432 * Returns localised database configuration help.
433 * Note: can be used before connect()
436 public function get_configuration_help() {
437 return get_string('nativemysqlihelp', 'install');
441 * Diagnose database and tables, this function is used
442 * to verify database and driver settings, db engine types, etc.
444 * @return string null means everything ok, string means problem found.
446 public function diagnose() {
447 $sloppymyisamfound = false;
448 $prefix = str_replace('_', '\\_', $this->prefix);
449 $sql = "SELECT COUNT('x')
450 FROM INFORMATION_SCHEMA.TABLES
451 WHERE table_schema = DATABASE()
452 AND table_name LIKE BINARY '$prefix%'
453 AND Engine = 'MyISAM'";
454 $this->query_start($sql, null, SQL_QUERY_AUX);
455 $result = $this->mysqli->query($sql);
456 $this->query_end($result);
458 if ($arr = $result->fetch_assoc()) {
459 $count = reset($arr);
461 $sloppymyisamfound = true;
467 if ($sloppymyisamfound) {
468 return get_string('myisamproblem', 'error');
476 * Must be called before other methods.
477 * @param string $dbhost The database host.
478 * @param string $dbuser The database username.
479 * @param string $dbpass The database username's password.
480 * @param string $dbname The name of the database being connected to.e
481 * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
482 * @param array $dboptions driver specific options
483 * @return bool success
485 public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
486 $driverstatus = $this->driver_installed();
488 if ($driverstatus !== true) {
489 throw new dml_exception('dbdriverproblem', $driverstatus);
492 $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
494 // dbsocket is used ONLY if host is NULL or 'localhost',
495 // you can not disable it because it is always tried if dbhost is 'localhost'
496 if (!empty($this->dboptions['dbsocket'])
497 and (strpos($this->dboptions['dbsocket'], '/') !== false or strpos($this->dboptions['dbsocket'], '\\') !== false)) {
498 $dbsocket = $this->dboptions['dbsocket'];
500 $dbsocket = ini_get('mysqli.default_socket');
502 if (empty($this->dboptions['dbport'])) {
503 $dbport = (int)ini_get('mysqli.default_port');
505 $dbport = (int)$this->dboptions['dbport'];
507 // verify ini.get does not return nonsense
508 if (empty($dbport)) {
511 if ($dbhost and !empty($this->dboptions['dbpersist'])) {
512 $dbhost = "p:$dbhost";
514 $this->mysqli = @new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
516 if ($this->mysqli->connect_errno !== 0) {
517 $dberr = $this->mysqli->connect_error;
518 $this->mysqli = null;
519 throw new dml_connection_exception($dberr);
522 // Disable logging until we are fully setup.
523 $this->query_log_prevent();
525 if (isset($dboptions['dbcollation'])) {
526 $collationinfo = explode('_', $dboptions['dbcollation']);
527 $this->dboptions['dbcollation'] = $dboptions['dbcollation'];
529 $collationinfo = explode('_', $this->get_dbcollation());
531 $charset = reset($collationinfo);
533 $this->query_start("--set_charset()", null, SQL_QUERY_AUX);
534 $this->mysqli->set_charset($charset);
535 $this->query_end(true);
537 // If available, enforce strict mode for the session. That guaranties
538 // standard behaviour under some situations, avoiding some MySQL nasty
539 // habits like truncating data or performing some transparent cast losses.
540 // With strict mode enforced, Moodle DB layer will be consistently throwing
541 // the corresponding exceptions as expected.
542 $si = $this->get_server_info();
543 if (version_compare($si['version'], '5.0.2', '>=')) {
544 $sql = "SET SESSION sql_mode = 'STRICT_ALL_TABLES'";
545 $this->query_start($sql, null, SQL_QUERY_AUX);
546 $result = $this->mysqli->query($sql);
547 $this->query_end($result);
550 // We can enable logging now.
551 $this->query_log_allow();
553 // Connection stabilised and configured, going to instantiate the temptables controller
554 $this->temptables = new mysqli_native_moodle_temptables($this);
560 * Close database connection and release all resources
561 * and memory (especially circular memory references).
562 * Do NOT use connect() again, create a new instance if needed.
564 public function dispose() {
565 parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
567 $this->mysqli->close();
568 $this->mysqli = null;
573 * Returns database server info array
574 * @return array Array containing 'description' and 'version' info
576 public function get_server_info() {
577 return array('description'=>$this->mysqli->server_info, 'version'=>$this->mysqli->server_info);
581 * Returns supported query parameter types
582 * @return int bitmask of accepted SQL_PARAMS_*
584 protected function allowed_param_types() {
585 return SQL_PARAMS_QM;
589 * Returns last error reported by database engine.
590 * @return string error message
592 public function get_last_error() {
593 return $this->mysqli->error;
597 * Return tables in database WITHOUT current prefix
598 * @param bool $usecache if true, returns list of cached tables.
599 * @return array of table names in lowercase and without prefix
601 public function get_tables($usecache=true) {
602 if ($usecache and $this->tables !== null) {
603 return $this->tables;
605 $this->tables = array();
606 $prefix = str_replace('_', '\\_', $this->prefix);
607 $sql = "SHOW TABLES LIKE '$prefix%'";
608 $this->query_start($sql, null, SQL_QUERY_AUX);
609 $result = $this->mysqli->query($sql);
610 $this->query_end($result);
611 $len = strlen($this->prefix);
613 while ($arr = $result->fetch_assoc()) {
614 $tablename = reset($arr);
615 $tablename = substr($tablename, $len);
616 $this->tables[$tablename] = $tablename;
621 // Add the currently available temptables
622 $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
623 return $this->tables;
627 * Return table indexes - everything lowercased.
628 * @param string $table The table we want to get indexes from.
629 * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
631 public function get_indexes($table) {
633 $sql = "SHOW INDEXES FROM {$this->prefix}$table";
634 $this->query_start($sql, null, SQL_QUERY_AUX);
635 $result = $this->mysqli->query($sql);
637 $this->query_end($result);
638 } catch (dml_read_exception $e) {
639 return $indexes; // table does not exist - no indexes...
642 while ($res = $result->fetch_object()) {
643 if ($res->Key_name === 'PRIMARY') {
646 if (!isset($indexes[$res->Key_name])) {
647 $indexes[$res->Key_name] = array('unique'=>empty($res->Non_unique), 'columns'=>array());
649 $indexes[$res->Key_name]['columns'][$res->Seq_in_index-1] = $res->Column_name;
657 * Returns detailed information about columns in table. This information is cached internally.
658 * @param string $table name
659 * @param bool $usecache
660 * @return database_column_info[] array of database_column_info objects indexed with column names
662 public function get_columns($table, $usecache=true) {
664 if ($this->temptables->is_temptable($table)) {
665 if ($data = $this->get_temp_tables_cache()->get($table)) {
669 if ($data = $this->get_metacache()->get($table)) {
675 $structure = array();
677 $sql = "SELECT column_name, data_type, character_maximum_length, numeric_precision,
678 numeric_scale, is_nullable, column_type, column_default, column_key, extra
679 FROM information_schema.columns
680 WHERE table_name = '" . $this->prefix.$table . "'
681 AND table_schema = '" . $this->dbname . "'
682 ORDER BY ordinal_position";
683 $this->query_start($sql, null, SQL_QUERY_AUX);
684 $result = $this->mysqli->query($sql);
685 $this->query_end(true); // Don't want to throw anything here ever. MDL-30147
687 if ($result === false) {
691 if ($result->num_rows > 0) {
692 // standard table exists
693 while ($rawcolumn = $result->fetch_assoc()) {
694 $info = (object)$this->get_column_info((object)$rawcolumn);
695 $structure[$info->name] = new database_column_info($info);
700 // temporary tables are not in information schema, let's try it the old way
702 $sql = "SHOW COLUMNS FROM {$this->prefix}$table";
703 $this->query_start($sql, null, SQL_QUERY_AUX);
704 $result = $this->mysqli->query($sql);
705 $this->query_end(true);
706 if ($result === false) {
709 while ($rawcolumn = $result->fetch_assoc()) {
710 $rawcolumn = (object)array_change_key_case($rawcolumn, CASE_LOWER);
711 $rawcolumn->column_name = $rawcolumn->field; unset($rawcolumn->field);
712 $rawcolumn->column_type = $rawcolumn->type; unset($rawcolumn->type);
713 $rawcolumn->character_maximum_length = null;
714 $rawcolumn->numeric_precision = null;
715 $rawcolumn->numeric_scale = null;
716 $rawcolumn->is_nullable = $rawcolumn->null; unset($rawcolumn->null);
717 $rawcolumn->column_default = $rawcolumn->default; unset($rawcolumn->default);
718 $rawcolumn->column_key = $rawcolumn->key; unset($rawcolumn->key);
720 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
721 $rawcolumn->data_type = $matches[1];
722 $rawcolumn->character_maximum_length = $matches[2];
724 } else if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
725 $rawcolumn->data_type = $matches[1];
726 $rawcolumn->numeric_precision = $matches[2];
727 $rawcolumn->max_length = $rawcolumn->numeric_precision;
729 $type = strtoupper($matches[1]);
730 if ($type === 'BIGINT') {
732 } else if ($type === 'INT' or $type === 'INTEGER') {
734 } else if ($type === 'MEDIUMINT') {
736 } else if ($type === 'SMALLINT') {
738 } else if ($type === 'TINYINT') {
741 // This should not happen.
744 if ($maxlength < $rawcolumn->max_length) {
745 $rawcolumn->max_length = $maxlength;
748 } else if (preg_match('/(decimal)\((\d+),(\d+)\)/i', $rawcolumn->column_type, $matches)) {
749 $rawcolumn->data_type = $matches[1];
750 $rawcolumn->numeric_precision = $matches[2];
751 $rawcolumn->numeric_scale = $matches[3];
753 } else if (preg_match('/(double|float)(\((\d+),(\d+)\))?/i', $rawcolumn->column_type, $matches)) {
754 $rawcolumn->data_type = $matches[1];
755 $rawcolumn->numeric_precision = isset($matches[3]) ? $matches[3] : null;
756 $rawcolumn->numeric_scale = isset($matches[4]) ? $matches[4] : null;
758 } else if (preg_match('/([a-z]*text)/i', $rawcolumn->column_type, $matches)) {
759 $rawcolumn->data_type = $matches[1];
760 $rawcolumn->character_maximum_length = -1; // unknown
762 } else if (preg_match('/([a-z]*blob)/i', $rawcolumn->column_type, $matches)) {
763 $rawcolumn->data_type = $matches[1];
766 $rawcolumn->data_type = $rawcolumn->column_type;
769 $info = $this->get_column_info($rawcolumn);
770 $structure[$info->name] = new database_column_info($info);
776 if ($this->temptables->is_temptable($table)) {
777 $this->get_temp_tables_cache()->set($table, $structure);
779 $this->get_metacache()->set($table, $structure);
787 * Indicates whether column information retrieved from `information_schema.columns` has default values quoted or not.
788 * @return boolean True when default values are quoted (breaking change); otherwise, false.
790 protected function has_breaking_change_quoted_defaults() {
795 * Returns moodle column info for raw column from information schema.
796 * @param stdClass $rawcolumn
797 * @return stdClass standardised colum info
799 private function get_column_info(stdClass $rawcolumn) {
800 $rawcolumn = (object)$rawcolumn;
801 $info = new stdClass();
802 $info->name = $rawcolumn->column_name;
803 $info->type = $rawcolumn->data_type;
804 $info->meta_type = $this->mysqltype2moodletype($rawcolumn->data_type);
805 if ($this->has_breaking_change_quoted_defaults()) {
806 $info->default_value = trim($rawcolumn->column_default, "'");
807 if ($info->default_value === 'NULL') {
808 $info->default_value = null;
811 $info->default_value = $rawcolumn->column_default;
813 $info->has_default = !is_null($info->default_value);
814 $info->not_null = ($rawcolumn->is_nullable === 'NO');
815 $info->primary_key = ($rawcolumn->column_key === 'PRI');
816 $info->binary = false;
817 $info->unsigned = null;
818 $info->auto_increment = false;
819 $info->unique = null;
822 if ($info->meta_type === 'C') {
823 $info->max_length = $rawcolumn->character_maximum_length;
825 } else if ($info->meta_type === 'I') {
826 if ($info->primary_key) {
827 $info->meta_type = 'R';
828 $info->unique = true;
830 // Return number of decimals, not bytes here.
831 $info->max_length = $rawcolumn->numeric_precision;
832 if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
833 $type = strtoupper($matches[1]);
834 if ($type === 'BIGINT') {
836 } else if ($type === 'INT' or $type === 'INTEGER') {
838 } else if ($type === 'MEDIUMINT') {
840 } else if ($type === 'SMALLINT') {
842 } else if ($type === 'TINYINT') {
845 // This should not happen.
848 // It is possible that display precision is different from storage type length,
849 // always use the smaller value to make sure our data fits.
850 if ($maxlength < $info->max_length) {
851 $info->max_length = $maxlength;
854 $info->unsigned = (stripos($rawcolumn->column_type, 'unsigned') !== false);
855 $info->auto_increment= (strpos($rawcolumn->extra, 'auto_increment') !== false);
857 } else if ($info->meta_type === 'N') {
858 $info->max_length = $rawcolumn->numeric_precision;
859 $info->scale = $rawcolumn->numeric_scale;
860 $info->unsigned = (stripos($rawcolumn->column_type, 'unsigned') !== false);
862 } else if ($info->meta_type === 'X') {
863 if ("$rawcolumn->character_maximum_length" === '4294967295') { // watch out for PHP max int limits!
864 // means maximum moodle size for text column, in other drivers it may also mean unknown size
865 $info->max_length = -1;
867 $info->max_length = $rawcolumn->character_maximum_length;
869 $info->primary_key = false;
871 } else if ($info->meta_type === 'B') {
872 $info->max_length = -1;
873 $info->primary_key = false;
874 $info->binary = true;
881 * Normalise column type.
882 * @param string $mysql_type
883 * @return string one character
884 * @throws dml_exception
886 private function mysqltype2moodletype($mysql_type) {
889 switch(strtoupper($mysql_type)) {
942 throw new dml_exception('invalidmysqlnativetype', $mysql_type);
948 * Normalise values based in RDBMS dependencies (booleans, LOBs...)
950 * @param database_column_info $column column metadata corresponding with the value we are going to normalise
951 * @param mixed $value value we are going to normalise
952 * @return mixed the normalised value
954 protected function normalise_value($column, $value) {
955 $this->detect_objects($value);
957 if (is_bool($value)) { // Always, convert boolean to int
958 $value = (int)$value;
960 } else if ($value === '') {
961 if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
962 $value = 0; // prevent '' problems in numeric fields
964 // Any float value being stored in varchar or text field is converted to string to avoid
965 // any implicit conversion by MySQL
966 } else if (is_float($value) and ($column->meta_type == 'C' or $column->meta_type == 'X')) {
973 * Is this database compatible with utf8?
976 public function setup_is_unicodedb() {
977 // All new tables are created with this collation, we just have to make sure it is utf8 compatible,
978 // if config table already exists it has this collation too.
979 $collation = $this->get_dbcollation();
981 $collationinfo = explode('_', $collation);
982 $charset = reset($collationinfo);
984 $sql = "SHOW COLLATION WHERE Collation ='$collation' AND Charset = '$charset'";
985 $this->query_start($sql, NULL, SQL_QUERY_AUX);
986 $result = $this->mysqli->query($sql);
987 $this->query_end($result);
988 if ($result->fetch_assoc()) {
999 * Do NOT use in code, to be used by database_manager only!
1000 * @param string|array $sql query
1001 * @param array|null $tablenames an array of xmldb table names affected by this request.
1003 * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
1005 public function change_database_structure($sql, $tablenames = null) {
1006 $this->get_manager(); // Includes DDL exceptions classes ;-)
1007 if (is_array($sql)) {
1008 $sql = implode("\n;\n", $sql);
1012 $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
1013 $result = $this->mysqli->multi_query($sql);
1014 if ($result === false) {
1015 $this->query_end(false);
1017 while ($this->mysqli->more_results()) {
1018 $result = $this->mysqli->next_result();
1019 if ($result === false) {
1020 $this->query_end(false);
1023 $this->query_end(true);
1024 } catch (ddl_change_structure_exception $e) {
1025 while (@$this->mysqli->more_results()) {
1026 @$this->mysqli->next_result();
1028 $this->reset_caches($tablenames);
1032 $this->reset_caches($tablenames);
1037 * Very ugly hack which emulates bound parameters in queries
1038 * because prepared statements do not use query cache.
1040 protected function emulate_bound_params($sql, array $params=null) {
1041 if (empty($params)) {
1044 // ok, we have verified sql statement with ? and correct number of params
1045 $parts = array_reverse(explode('?', $sql));
1046 $return = array_pop($parts);
1047 foreach ($params as $param) {
1048 if (is_bool($param)) {
1049 $return .= (int)$param;
1050 } else if (is_null($param)) {
1052 } else if (is_number($param)) {
1053 $return .= "'".$param."'"; // we have to always use strings because mysql is using weird automatic int casting
1054 } else if (is_float($param)) {
1057 $param = $this->mysqli->real_escape_string($param);
1058 $return .= "'$param'";
1060 $return .= array_pop($parts);
1066 * Execute general sql query. Should be used only when no other method suitable.
1067 * Do NOT use this to make changes in db structure, use database_manager methods instead!
1068 * @param string $sql query
1069 * @param array $params query parameters
1071 * @throws dml_exception A DML specific exception is thrown for any errors.
1073 public function execute($sql, array $params=null) {
1074 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1076 if (strpos($sql, ';') !== false) {
1077 throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
1080 $rawsql = $this->emulate_bound_params($sql, $params);
1082 $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1083 $result = $this->mysqli->query($rawsql);
1084 $this->query_end($result);
1086 if ($result === true) {
1096 * Get a number of records as a moodle_recordset using a SQL statement.
1098 * Since this method is a little less readable, use of it should be restricted to
1099 * code where it's possible there might be large datasets being returned. For known
1100 * small datasets use get_records_sql - it leads to simpler code.
1102 * The return type is like:
1103 * @see function get_recordset.
1105 * @param string $sql the SQL select query to execute.
1106 * @param array $params array of sql parameters
1107 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1108 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1109 * @return moodle_recordset instance
1110 * @throws dml_exception A DML specific exception is thrown for any errors.
1112 public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1114 list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
1116 if ($limitfrom or $limitnum) {
1117 if ($limitnum < 1) {
1118 $limitnum = "18446744073709551615";
1120 $sql .= " LIMIT $limitfrom, $limitnum";
1123 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1124 $rawsql = $this->emulate_bound_params($sql, $params);
1126 $this->query_start($sql, $params, SQL_QUERY_SELECT);
1127 // no MYSQLI_USE_RESULT here, it would block write ops on affected tables
1128 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
1129 $this->query_end($result);
1131 return $this->create_recordset($result);
1135 * Get all records from a table.
1137 * This method works around potential memory problems and may improve performance,
1138 * this method may block access to table until the recordset is closed.
1140 * @param string $table Name of database table.
1141 * @return moodle_recordset A moodle_recordset instance {@link function get_recordset}.
1142 * @throws dml_exception A DML specific exception is thrown for any errors.
1144 public function export_table_recordset($table) {
1145 $sql = $this->fix_table_names("SELECT * FROM {{$table}}");
1147 $this->query_start($sql, array(), SQL_QUERY_SELECT);
1148 // MYSQLI_STORE_RESULT may eat all memory for large tables, unfortunately MYSQLI_USE_RESULT blocks other queries.
1149 $result = $this->mysqli->query($sql, MYSQLI_USE_RESULT);
1150 $this->query_end($result);
1152 return $this->create_recordset($result);
1155 protected function create_recordset($result) {
1156 return new mysqli_native_moodle_recordset($result);
1160 * Get a number of records as an array of objects using a SQL statement.
1162 * Return value is like:
1163 * @see function get_records.
1165 * @param string $sql the SQL select query to execute. The first column of this SELECT statement
1166 * must be a unique value (usually the 'id' field), as it will be used as the key of the
1168 * @param array $params array of sql parameters
1169 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1170 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1171 * @return array of objects, or empty array if no records were found
1172 * @throws dml_exception A DML specific exception is thrown for any errors.
1174 public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1176 list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
1178 if ($limitfrom or $limitnum) {
1179 if ($limitnum < 1) {
1180 $limitnum = "18446744073709551615";
1182 $sql .= " LIMIT $limitfrom, $limitnum";
1185 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1186 $rawsql = $this->emulate_bound_params($sql, $params);
1188 $this->query_start($sql, $params, SQL_QUERY_SELECT);
1189 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
1190 $this->query_end($result);
1194 while($row = $result->fetch_assoc()) {
1195 $row = array_change_key_case($row, CASE_LOWER);
1197 if (isset($return[$id])) {
1198 $colname = key($row);
1199 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);
1201 $return[$id] = (object)$row;
1209 * Selects records and return values (first field) as an array using a SQL statement.
1211 * @param string $sql The SQL query
1212 * @param array $params array of sql parameters
1213 * @return array of values
1214 * @throws dml_exception A DML specific exception is thrown for any errors.
1216 public function get_fieldset_sql($sql, array $params=null) {
1217 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1218 $rawsql = $this->emulate_bound_params($sql, $params);
1220 $this->query_start($sql, $params, SQL_QUERY_SELECT);
1221 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
1222 $this->query_end($result);
1226 while($row = $result->fetch_assoc()) {
1227 $return[] = reset($row);
1235 * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
1236 * @param string $table name
1237 * @param mixed $params data record as object or array
1238 * @param bool $returnit return it of inserted record
1239 * @param bool $bulk true means repeated inserts expected
1240 * @param bool $customsequence true if 'id' included in $params, disables $returnid
1241 * @return bool|int true or new id
1242 * @throws dml_exception A DML specific exception is thrown for any errors.
1244 public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
1245 if (!is_array($params)) {
1246 $params = (array)$params;
1249 if ($customsequence) {
1250 if (!isset($params['id'])) {
1251 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
1255 unset($params['id']);
1258 if (empty($params)) {
1259 throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
1262 $fields = implode(',', array_keys($params));
1263 $qms = array_fill(0, count($params), '?');
1264 $qms = implode(',', $qms);
1266 $sql = "INSERT INTO {$this->prefix}$table ($fields) VALUES($qms)";
1268 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1269 $rawsql = $this->emulate_bound_params($sql, $params);
1271 $this->query_start($sql, $params, SQL_QUERY_INSERT);
1272 $result = $this->mysqli->query($rawsql);
1273 $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db
1274 $this->query_end($result);
1276 if (!$customsequence and !$id) {
1277 throw new dml_write_exception('unknown error fetching inserted id');
1288 * Insert a record into a table and return the "id" field if required.
1290 * Some conversions and safety checks are carried out. Lobs are supported.
1291 * If the return ID isn't required, then this just reports success as true/false.
1292 * $data is an object containing needed data
1293 * @param string $table The database table to be inserted into
1294 * @param object $data A data object with values for one or more fields in the record
1295 * @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.
1296 * @return bool|int true or new id
1297 * @throws dml_exception A DML specific exception is thrown for any errors.
1299 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1300 $dataobject = (array)$dataobject;
1302 $columns = $this->get_columns($table);
1303 if (empty($columns)) {
1304 throw new dml_exception('ddltablenotexist', $table);
1309 foreach ($dataobject as $field=>$value) {
1310 if ($field === 'id') {
1313 if (!isset($columns[$field])) {
1316 $column = $columns[$field];
1317 $cleaned[$field] = $this->normalise_value($column, $value);
1320 return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
1324 * Insert multiple records into database as fast as possible.
1326 * Order of inserts is maintained, but the operation is not atomic,
1327 * use transactions if necessary.
1329 * This method is intended for inserting of large number of small objects,
1330 * do not use for huge objects with text or binary fields.
1334 * @param string $table The database table to be inserted into
1335 * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
1336 * @return void does not return new record ids
1338 * @throws coding_exception if data objects have different structure
1339 * @throws dml_exception A DML specific exception is thrown for any errors.
1341 public function insert_records($table, $dataobjects) {
1342 if (!is_array($dataobjects) and !$dataobjects instanceof Traversable) {
1343 throw new coding_exception('insert_records() passed non-traversable object');
1346 // MySQL has a relatively small query length limit by default,
1347 // make sure 'max_allowed_packet' in my.cnf is high enough
1348 // if you change the following default...
1349 static $chunksize = null;
1350 if ($chunksize === null) {
1351 if (!empty($this->dboptions['bulkinsertsize'])) {
1352 $chunksize = (int)$this->dboptions['bulkinsertsize'];
1355 if (PHP_INT_SIZE === 4) {
1356 // Bad luck for Windows, we cannot do any maths with large numbers.
1359 $sql = "SHOW VARIABLES LIKE 'max_allowed_packet'";
1360 $this->query_start($sql, null, SQL_QUERY_AUX);
1361 $result = $this->mysqli->query($sql);
1362 $this->query_end($result);
1364 if ($rec = $result->fetch_assoc()) {
1365 $size = $rec['Value'];
1368 // Hopefully 200kb per object are enough.
1369 $chunksize = (int)($size / 200000);
1370 if ($chunksize > 50) {
1377 $columns = $this->get_columns($table, true);
1381 foreach ($dataobjects as $dataobject) {
1382 if (!is_array($dataobject) and !is_object($dataobject)) {
1383 throw new coding_exception('insert_records() passed invalid record object');
1385 $dataobject = (array)$dataobject;
1386 if ($fields === null) {
1387 $fields = array_keys($dataobject);
1388 $columns = array_intersect_key($columns, $dataobject);
1389 unset($columns['id']);
1390 } else if ($fields !== array_keys($dataobject)) {
1391 throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
1395 $chunk[] = $dataobject;
1397 if ($count === $chunksize) {
1398 $this->insert_chunk($table, $chunk, $columns);
1405 $this->insert_chunk($table, $chunk, $columns);
1410 * Insert records in chunks.
1412 * Note: can be used only from insert_records().
1414 * @param string $table
1415 * @param array $chunk
1416 * @param database_column_info[] $columns
1418 protected function insert_chunk($table, array $chunk, array $columns) {
1419 $fieldssql = '('.implode(',', array_keys($columns)).')';
1421 $valuessql = '('.implode(',', array_fill(0, count($columns), '?')).')';
1422 $valuessql = implode(',', array_fill(0, count($chunk), $valuessql));
1425 foreach ($chunk as $dataobject) {
1426 foreach ($columns as $field => $column) {
1427 $params[] = $this->normalise_value($column, $dataobject[$field]);
1431 $sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
1433 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1434 $rawsql = $this->emulate_bound_params($sql, $params);
1436 $this->query_start($sql, $params, SQL_QUERY_INSERT);
1437 $result = $this->mysqli->query($rawsql);
1438 $this->query_end($result);
1442 * Import a record into a table, id field is required.
1443 * Safety checks are NOT carried out. Lobs are supported.
1445 * @param string $table name of database table to be inserted into
1446 * @param object $dataobject A data object with values for one or more fields in the record
1448 * @throws dml_exception A DML specific exception is thrown for any errors.
1450 public function import_record($table, $dataobject) {
1451 $dataobject = (array)$dataobject;
1453 $columns = $this->get_columns($table);
1456 foreach ($dataobject as $field=>$value) {
1457 if (!isset($columns[$field])) {
1460 $cleaned[$field] = $value;
1463 return $this->insert_record_raw($table, $cleaned, false, true, true);
1467 * Update record in database, as fast as possible, no safety checks, lobs not supported.
1468 * @param string $table name
1469 * @param mixed $params data record as object or array
1470 * @param bool true means repeated updates expected
1472 * @throws dml_exception A DML specific exception is thrown for any errors.
1474 public function update_record_raw($table, $params, $bulk=false) {
1475 $params = (array)$params;
1477 if (!isset($params['id'])) {
1478 throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1480 $id = $params['id'];
1481 unset($params['id']);
1483 if (empty($params)) {
1484 throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1488 foreach ($params as $field=>$value) {
1489 $sets[] = "$field = ?";
1492 $params[] = $id; // last ? in WHERE condition
1494 $sets = implode(',', $sets);
1495 $sql = "UPDATE {$this->prefix}$table SET $sets WHERE id=?";
1497 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1498 $rawsql = $this->emulate_bound_params($sql, $params);
1500 $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1501 $result = $this->mysqli->query($rawsql);
1502 $this->query_end($result);
1508 * Update a record in a table
1510 * $dataobject is an object containing needed data
1511 * Relies on $dataobject having a variable "id" to
1512 * specify the record to update
1514 * @param string $table The database table to be checked against.
1515 * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1516 * @param bool true means repeated updates expected
1518 * @throws dml_exception A DML specific exception is thrown for any errors.
1520 public function update_record($table, $dataobject, $bulk=false) {
1521 $dataobject = (array)$dataobject;
1523 $columns = $this->get_columns($table);
1526 foreach ($dataobject as $field=>$value) {
1527 if (!isset($columns[$field])) {
1530 $column = $columns[$field];
1531 $cleaned[$field] = $this->normalise_value($column, $value);
1534 return $this->update_record_raw($table, $cleaned, $bulk);
1538 * Set a single field in every table record which match a particular WHERE clause.
1540 * @param string $table The database table to be checked against.
1541 * @param string $newfield the field to set.
1542 * @param string $newvalue the value to set the field to.
1543 * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1544 * @param array $params array of sql parameters
1546 * @throws dml_exception A DML specific exception is thrown for any errors.
1548 public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1550 $select = "WHERE $select";
1552 if (is_null($params)) {
1555 list($select, $params, $type) = $this->fix_sql_params($select, $params);
1557 // Get column metadata
1558 $columns = $this->get_columns($table);
1559 $column = $columns[$newfield];
1561 $normalised_value = $this->normalise_value($column, $newvalue);
1563 if (is_null($normalised_value)) {
1564 $newfield = "$newfield = NULL";
1566 $newfield = "$newfield = ?";
1567 array_unshift($params, $normalised_value);
1569 $sql = "UPDATE {$this->prefix}$table SET $newfield $select";
1570 $rawsql = $this->emulate_bound_params($sql, $params);
1572 $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1573 $result = $this->mysqli->query($rawsql);
1574 $this->query_end($result);
1580 * Delete one or more records from a table which match a particular WHERE clause.
1582 * @param string $table The database table to be checked against.
1583 * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1584 * @param array $params array of sql parameters
1586 * @throws dml_exception A DML specific exception is thrown for any errors.
1588 public function delete_records_select($table, $select, array $params=null) {
1590 $select = "WHERE $select";
1592 $sql = "DELETE FROM {$this->prefix}$table $select";
1594 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1595 $rawsql = $this->emulate_bound_params($sql, $params);
1597 $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1598 $result = $this->mysqli->query($rawsql);
1599 $this->query_end($result);
1604 public function sql_cast_char2int($fieldname, $text=false) {
1605 return ' CAST(' . $fieldname . ' AS SIGNED) ';
1608 public function sql_cast_char2real($fieldname, $text=false) {
1609 // Set to 65 (max mysql 5.5 precision) with 7 as scale
1610 // because we must ensure at least 6 decimal positions
1611 // per casting given that postgres is casting to that scale (::real::).
1612 // Can be raised easily but that must be done in all DBs and tests.
1613 return ' CAST(' . $fieldname . ' AS DECIMAL(65,7)) ';
1616 public function sql_equal($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notequal = false) {
1617 $equalop = $notequal ? '<>' : '=';
1619 $collationinfo = explode('_', $this->get_dbcollation());
1620 $bincollate = reset($collationinfo) . '_bin';
1622 if ($casesensitive) {
1623 // Current MySQL versions do not support case sensitive and accent insensitive.
1624 return "$fieldname COLLATE $bincollate $equalop $param";
1625 } else if ($accentsensitive) {
1626 // Case insensitive and accent sensitive, we can force a binary comparison once all texts are using the same case.
1627 return "LOWER($fieldname) COLLATE $bincollate $equalop LOWER($param)";
1629 // Case insensitive and accent insensitive. All collations are that way, but utf8_bin.
1631 if ($this->get_dbcollation() == 'utf8_bin') {
1632 $collation = 'COLLATE utf8_unicode_ci';
1633 } else if ($this->get_dbcollation() == 'utf8mb4_bin') {
1634 $collation = 'COLLATE utf8mb4_unicode_ci';
1636 return "$fieldname $collation $equalop $param";
1641 * Returns 'LIKE' part of a query.
1643 * Note that mysql does not support $casesensitive = true and $accentsensitive = false.
1644 * More information in http://bugs.mysql.com/bug.php?id=19567.
1646 * @param string $fieldname usually name of the table column
1647 * @param string $param usually bound query parameter (?, :named)
1648 * @param bool $casesensitive use case sensitive search
1649 * @param bool $accensensitive use accent sensitive search (ignored if $casesensitive is true)
1650 * @param bool $notlike true means "NOT LIKE"
1651 * @param string $escapechar escape char for '%' and '_'
1652 * @return string SQL code fragment
1654 public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1655 if (strpos($param, '%') !== false) {
1656 debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1658 $escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\'
1660 $collationinfo = explode('_', $this->get_dbcollation());
1661 $bincollate = reset($collationinfo) . '_bin';
1663 $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1665 if ($casesensitive) {
1666 // Current MySQL versions do not support case sensitive and accent insensitive.
1667 return "$fieldname $LIKE $param COLLATE $bincollate ESCAPE '$escapechar'";
1669 } else if ($accentsensitive) {
1670 // Case insensitive and accent sensitive, we can force a binary comparison once all texts are using the same case.
1671 return "LOWER($fieldname) $LIKE LOWER($param) COLLATE $bincollate ESCAPE '$escapechar'";
1674 // Case insensitive and accent insensitive.
1676 if ($this->get_dbcollation() == 'utf8_bin') {
1677 // Force a case insensitive comparison if using utf8_bin.
1678 $collation = 'COLLATE utf8_unicode_ci';
1679 } else if ($this->get_dbcollation() == 'utf8mb4_bin') {
1680 // Force a case insensitive comparison if using utf8mb4_bin.
1681 $collation = 'COLLATE utf8mb4_unicode_ci';
1684 return "$fieldname $LIKE $param $collation ESCAPE '$escapechar'";
1689 * Returns the proper SQL to do CONCAT between the elements passed
1690 * Can take many parameters
1692 * @param string $str,... 1 or more fields/strings to concat
1694 * @return string The concat sql
1696 public function sql_concat() {
1697 $arr = func_get_args();
1698 $s = implode(', ', $arr);
1702 return "CONCAT($s)";
1706 * Returns the proper SQL to do CONCAT between the elements passed
1707 * with a given separator
1709 * @param string $separator The string to use as the separator
1710 * @param array $elements An array of items to concatenate
1711 * @return string The concat SQL
1713 public function sql_concat_join($separator="' '", $elements=array()) {
1714 $s = implode(', ', $elements);
1719 return "CONCAT_WS($separator, $s)";
1723 * Returns the SQL text to be used to calculate the length in characters of one expression.
1724 * @param string fieldname or expression to calculate its length in characters.
1725 * @return string the piece of SQL code to be used in the statement.
1727 public function sql_length($fieldname) {
1728 return ' CHAR_LENGTH(' . $fieldname . ')';
1732 * Does this driver support regex syntax when searching
1734 public function sql_regex_supported() {
1739 * Return regex positive or negative match sql
1740 * @param bool $positivematch
1741 * @return string or empty if not supported
1743 public function sql_regex($positivematch=true) {
1744 return $positivematch ? 'REGEXP' : 'NOT REGEXP';
1748 * Returns the SQL to be used in order to an UNSIGNED INTEGER column to SIGNED.
1750 * @deprecated since 2.3
1751 * @param string $fieldname The name of the field to be cast
1752 * @return string The piece of SQL code to be used in your statement.
1754 public function sql_cast_2signed($fieldname) {
1755 return ' CAST(' . $fieldname . ' AS SIGNED) ';
1759 * Returns the SQL that allows to find intersection of two or more queries
1763 * @param array $selects array of SQL select queries, each of them only returns fields with the names from $fields
1764 * @param string $fields comma-separated list of fields
1765 * @return string SQL query that will return only values that are present in each of selects
1767 public function sql_intersect($selects, $fields) {
1768 if (count($selects) <= 1) {
1769 return parent::sql_intersect($selects, $fields);
1771 $fields = preg_replace('/\s/', '', $fields);
1772 static $aliascnt = 0;
1773 $falias = 'intsctal'.($aliascnt++);
1774 $rv = "SELECT $falias.".
1775 preg_replace('/,/', ','.$falias.'.', $fields).
1776 " FROM ($selects[0]) $falias";
1777 for ($i = 1; $i < count($selects); $i++) {
1778 $alias = 'intsctal'.($aliascnt++);
1779 $rv .= " JOIN (".$selects[$i].") $alias ON ".
1782 create_function('$a', 'return "'.$falias.'.$a = '.$alias.'.$a";'),
1783 preg_split('/,/', $fields))
1790 * Does this driver support tool_replace?
1792 * @since Moodle 2.6.1
1795 public function replace_all_text_supported() {
1799 public function session_lock_supported() {
1804 * Obtain session lock
1805 * @param int $rowid id of the row with session record
1806 * @param int $timeout max allowed time to wait for the lock in seconds
1809 public function get_session_lock($rowid, $timeout) {
1810 parent::get_session_lock($rowid, $timeout);
1812 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1813 $sql = "SELECT GET_LOCK('$fullname', $timeout)";
1814 $this->query_start($sql, null, SQL_QUERY_AUX);
1815 $result = $this->mysqli->query($sql);
1816 $this->query_end($result);
1819 $arr = $result->fetch_assoc();
1822 if (reset($arr) == 1) {
1825 throw new dml_sessionwait_exception();
1830 public function release_session_lock($rowid) {
1831 if (!$this->used_for_db_sessions) {
1835 parent::release_session_lock($rowid);
1836 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1837 $sql = "SELECT RELEASE_LOCK('$fullname')";
1838 $this->query_start($sql, null, SQL_QUERY_AUX);
1839 $result = $this->mysqli->query($sql);
1840 $this->query_end($result);
1848 * Are transactions supported?
1849 * It is not responsible to run productions servers
1850 * on databases without transaction support ;-)
1852 * MyISAM does not support support transactions.
1854 * You can override this via the dbtransactions option.
1858 protected function transactions_supported() {
1859 if (!is_null($this->transactions_supported)) {
1860 return $this->transactions_supported;
1863 // this is all just guessing, might be better to just specify it in config.php
1864 if (isset($this->dboptions['dbtransactions'])) {
1865 $this->transactions_supported = $this->dboptions['dbtransactions'];
1866 return $this->transactions_supported;
1869 $this->transactions_supported = false;
1871 $engine = $this->get_dbengine();
1873 // Only will accept transactions if using compatible storage engine (more engines can be added easily BDB, Falcon...)
1874 if (in_array($engine, array('InnoDB', 'INNOBASE', 'BDB', 'XtraDB', 'Aria', 'Falcon'))) {
1875 $this->transactions_supported = true;
1878 return $this->transactions_supported;
1882 * Driver specific start of real database transaction,
1883 * this can not be used directly in code.
1886 protected function begin_transaction() {
1887 if (!$this->transactions_supported()) {
1891 $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
1892 $this->query_start($sql, NULL, SQL_QUERY_AUX);
1893 $result = $this->mysqli->query($sql);
1894 $this->query_end($result);
1896 $sql = "START TRANSACTION";
1897 $this->query_start($sql, NULL, SQL_QUERY_AUX);
1898 $result = $this->mysqli->query($sql);
1899 $this->query_end($result);
1903 * Driver specific commit of real database transaction,
1904 * this can not be used directly in code.
1907 protected function commit_transaction() {
1908 if (!$this->transactions_supported()) {
1913 $this->query_start($sql, NULL, SQL_QUERY_AUX);
1914 $result = $this->mysqli->query($sql);
1915 $this->query_end($result);
1919 * Driver specific abort of real database transaction,
1920 * this can not be used directly in code.
1923 protected function rollback_transaction() {
1924 if (!$this->transactions_supported()) {
1929 $this->query_start($sql, NULL, SQL_QUERY_AUX);
1930 $result = $this->mysqli->query($sql);
1931 $this->query_end($result);
1937 * Converts a table to either 'Compressed' or 'Dynamic' row format.
1939 * @param string $tablename Name of the table to convert to the new row format.
1941 public function convert_table_row_format($tablename) {
1942 $currentrowformat = $this->get_row_format($tablename);
1943 if ($currentrowformat == 'Compact' || $currentrowformat == 'Redundant') {
1944 $rowformat = ($this->is_compressed_row_format_supported(false)) ? "ROW_FORMAT=Compressed" : "ROW_FORMAT=Dynamic";
1945 $prefix = $this->get_prefix();
1946 $this->change_database_structure("ALTER TABLE {$prefix}$tablename $rowformat");