Merge branch 'MDL-77433-master' of https://github.com/ferranrecio/moodle
[moodle.git] / lib / dml / mysqli_native_moodle_database.php
blobc42e4c57d7b2799b292d3bb5327b7e14e3299686
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Native mysqli class representing moodle database interface.
20 * @package core_dml
21 * @copyright 2008 Petr Skoda (http://skodak.org)
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 require_once(__DIR__.'/moodle_database.php');
28 require_once(__DIR__.'/moodle_read_slave_trait.php');
29 require_once(__DIR__.'/mysqli_native_moodle_recordset.php');
30 require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
32 /**
33 * Native mysqli class representing moodle database interface.
35 * @package core_dml
36 * @copyright 2008 Petr Skoda (http://skodak.org)
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 class mysqli_native_moodle_database extends moodle_database {
40 use moodle_read_slave_trait {
41 can_use_readonly as read_slave_can_use_readonly;
44 /** @var mysqli $mysqli */
45 protected $mysqli = null;
46 /** @var bool is compressed row format supported cache */
47 protected $compressedrowformatsupported = null;
48 /** @var string DB server actual version */
49 protected $serverversion = null;
51 private $transactions_supported = null;
53 /**
54 * Attempt to create the database
55 * @param string $dbhost
56 * @param string $dbuser
57 * @param string $dbpass
58 * @param string $dbname
59 * @return bool success
60 * @throws dml_exception A DML specific exception is thrown for any errors.
62 public function create_database($dbhost, $dbuser, $dbpass, $dbname, array $dboptions=null) {
63 $driverstatus = $this->driver_installed();
65 if ($driverstatus !== true) {
66 throw new dml_exception('dbdriverproblem', $driverstatus);
69 if (!empty($dboptions['dbsocket'])
70 and (strpos($dboptions['dbsocket'], '/') !== false or strpos($dboptions['dbsocket'], '\\') !== false)) {
71 $dbsocket = $dboptions['dbsocket'];
72 } else {
73 $dbsocket = ini_get('mysqli.default_socket');
75 if (empty($dboptions['dbport'])) {
76 $dbport = (int)ini_get('mysqli.default_port');
77 } else {
78 $dbport = (int)$dboptions['dbport'];
80 // verify ini.get does not return nonsense
81 if (empty($dbport)) {
82 $dbport = 3306;
84 ob_start();
85 $conn = new mysqli($dbhost, $dbuser, $dbpass, '', $dbport, $dbsocket); // Connect without db
86 $dberr = ob_get_contents();
87 ob_end_clean();
88 $errorno = @$conn->connect_errno;
90 if ($errorno !== 0) {
91 throw new dml_connection_exception($dberr);
94 // Normally a check would be done before setting utf8mb4, but the database can be created
95 // before the enviroment checks are done. We'll proceed with creating the database and then do checks next.
96 $charset = 'utf8mb4';
97 if (isset($dboptions['dbcollation']) and (strpos($dboptions['dbcollation'], 'utf8_') === 0
98 || strpos($dboptions['dbcollation'], 'utf8mb4_') === 0)) {
99 $collation = $dboptions['dbcollation'];
100 $collationinfo = explode('_', $dboptions['dbcollation']);
101 $charset = reset($collationinfo);
102 } else {
103 $collation = 'utf8mb4_unicode_ci';
106 $result = $conn->query("CREATE DATABASE $dbname DEFAULT CHARACTER SET $charset DEFAULT COLLATE ".$collation);
108 $conn->close();
110 if (!$result) {
111 throw new dml_exception('cannotcreatedb');
114 return true;
118 * Detects if all needed PHP stuff installed.
119 * Note: can be used before connect()
120 * @return mixed true if ok, string if something
122 public function driver_installed() {
123 if (!extension_loaded('mysqli')) {
124 return get_string('mysqliextensionisnotpresentinphp', 'install');
126 return true;
130 * Returns database family type - describes SQL dialect
131 * Note: can be used before connect()
132 * @return string db family name (mysql, postgres, mssql, oracle, etc.)
134 public function get_dbfamily() {
135 return 'mysql';
139 * Returns more specific database driver type
140 * Note: can be used before connect()
141 * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
143 protected function get_dbtype() {
144 return 'mysqli';
148 * Returns general database library name
149 * Note: can be used before connect()
150 * @return string db type pdo, native
152 protected function get_dblibrary() {
153 return 'native';
157 * Returns the current MySQL db engine.
159 * This is an ugly workaround for MySQL default engine problems,
160 * Moodle is designed to work best on ACID compliant databases
161 * with full transaction support. Do not use MyISAM.
163 * @return string or null MySQL engine name
165 public function get_dbengine() {
166 if (isset($this->dboptions['dbengine'])) {
167 return $this->dboptions['dbengine'];
170 if ($this->external) {
171 return null;
174 $engine = null;
176 // Look for current engine of our config table (the first table that gets created),
177 // so that we create all tables with the same engine.
178 $sql = "SELECT engine
179 FROM INFORMATION_SCHEMA.TABLES
180 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config'";
181 $this->query_start($sql, null, SQL_QUERY_AUX);
182 $result = $this->mysqli->query($sql);
183 $this->query_end($result);
184 if ($rec = $result->fetch_assoc()) {
185 // MySQL 8 BC: information_schema.* returns the fields in upper case.
186 $rec = array_change_key_case($rec, CASE_LOWER);
187 $engine = $rec['engine'];
189 $result->close();
191 if ($engine) {
192 // Cache the result to improve performance.
193 $this->dboptions['dbengine'] = $engine;
194 return $engine;
197 // Get the default database engine.
198 $sql = "SELECT @@default_storage_engine engine";
199 $this->query_start($sql, null, SQL_QUERY_AUX);
200 $result = $this->mysqli->query($sql);
201 $this->query_end($result);
202 if ($rec = $result->fetch_assoc()) {
203 $engine = $rec['engine'];
205 $result->close();
207 if ($engine === 'MyISAM') {
208 // we really do not want MyISAM for Moodle, InnoDB or XtraDB is a reasonable defaults if supported
209 $sql = "SHOW STORAGE ENGINES";
210 $this->query_start($sql, null, SQL_QUERY_AUX);
211 $result = $this->mysqli->query($sql);
212 $this->query_end($result);
213 $engines = array();
214 while ($res = $result->fetch_assoc()) {
215 if ($res['Support'] === 'YES' or $res['Support'] === 'DEFAULT') {
216 $engines[$res['Engine']] = true;
219 $result->close();
220 if (isset($engines['InnoDB'])) {
221 $engine = 'InnoDB';
223 if (isset($engines['XtraDB'])) {
224 $engine = 'XtraDB';
228 // Cache the result to improve performance.
229 $this->dboptions['dbengine'] = $engine;
230 return $engine;
234 * Returns the current MySQL db collation.
236 * This is an ugly workaround for MySQL default collation problems.
238 * @return string or null MySQL collation name
240 public function get_dbcollation() {
241 if (isset($this->dboptions['dbcollation'])) {
242 return $this->dboptions['dbcollation'];
247 * Set 'dbcollation' option
249 * @return string|null $dbcollation
251 private function detect_collation(): ?string {
252 if ($this->external) {
253 return null;
256 $collation = null;
258 // Look for current collation of our config table (the first table that gets created),
259 // so that we create all tables with the same collation.
260 $sql = "SELECT collation_name
261 FROM INFORMATION_SCHEMA.COLUMNS
262 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config' AND column_name = 'value'";
263 $result = $this->mysqli->query($sql);
264 if ($rec = $result->fetch_assoc()) {
265 // MySQL 8 BC: information_schema.* returns the fields in upper case.
266 $rec = array_change_key_case($rec, CASE_LOWER);
267 $collation = $rec['collation_name'];
269 $result->close();
272 if (!$collation) {
273 // Get the default database collation, but only if using UTF-8.
274 $sql = "SELECT @@collation_database";
275 $result = $this->mysqli->query($sql);
276 if ($rec = $result->fetch_assoc()) {
277 if (strpos($rec['@@collation_database'], 'utf8_') === 0 || strpos($rec['@@collation_database'], 'utf8mb4_') === 0) {
278 $collation = $rec['@@collation_database'];
281 $result->close();
284 if (!$collation) {
285 // We want only utf8 compatible collations.
286 $collation = null;
287 $sql = "SHOW COLLATION WHERE Collation LIKE 'utf8mb4\_%' AND Charset = 'utf8mb4'";
288 $result = $this->mysqli->query($sql);
289 while ($res = $result->fetch_assoc()) {
290 $collation = $res['Collation'];
291 if (strtoupper($res['Default']) === 'YES') {
292 $collation = $res['Collation'];
293 break;
296 $result->close();
299 // Cache the result to improve performance.
300 $this->dboptions['dbcollation'] = $collation;
301 return $collation;
305 * Tests if the Antelope file format is still supported or it has been removed.
306 * When removed, only Barracuda file format is supported, given the XtraDB/InnoDB engine.
308 * @return bool True if the Antelope file format has been removed; otherwise, false.
310 protected function is_antelope_file_format_no_more_supported() {
311 // Breaking change: Antelope file format support has been removed from both MySQL and MariaDB.
312 // The following InnoDB file format configuration parameters were deprecated and then removed:
313 // - innodb_file_format
314 // - innodb_file_format_check
315 // - innodb_file_format_max
316 // - innodb_large_prefix
317 // 1. MySQL: deprecated in 5.7.7 and removed 8.0.0+.
318 $ismysqlge8d0d0 = ($this->get_dbtype() == 'mysqli' || $this->get_dbtype() == 'auroramysql') &&
319 version_compare($this->get_server_info()['version'], '8.0.0', '>=');
320 // 2. MariaDB: deprecated in 10.2.0 and removed 10.3.1+.
321 $ismariadbge10d3d1 = ($this->get_dbtype() == 'mariadb') &&
322 version_compare($this->get_server_info()['version'], '10.3.1', '>=');
324 return $ismysqlge8d0d0 || $ismariadbge10d3d1;
328 * Get the row format from the database schema.
330 * @param string $table
331 * @return string row_format name or null if not known or table does not exist.
333 public function get_row_format($table = null) {
334 $rowformat = null;
335 if (isset($table)) {
336 $table = $this->mysqli->real_escape_string($table);
337 $sql = "SELECT row_format
338 FROM INFORMATION_SCHEMA.TABLES
339 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}$table'";
340 } else {
341 if ($this->is_antelope_file_format_no_more_supported()) {
342 // Breaking change: Antelope file format support has been removed, only Barracuda.
343 $dbengine = $this->get_dbengine();
344 $supporteddbengines = array('InnoDB', 'XtraDB');
345 if (in_array($dbengine, $supporteddbengines)) {
346 $rowformat = 'Barracuda';
349 return $rowformat;
352 $sql = "SHOW VARIABLES LIKE 'innodb_file_format'";
354 $this->query_start($sql, null, SQL_QUERY_AUX);
355 $result = $this->mysqli->query($sql);
356 $this->query_end($result);
357 if ($rec = $result->fetch_assoc()) {
358 // MySQL 8 BC: information_schema.* returns the fields in upper case.
359 $rec = array_change_key_case($rec, CASE_LOWER);
360 if (isset($table)) {
361 $rowformat = $rec['row_format'];
362 } else {
363 $rowformat = $rec['value'];
366 $result->close();
368 return $rowformat;
372 * Is this database compatible with compressed row format?
373 * This feature is necessary for support of large number of text
374 * columns in InnoDB/XtraDB database.
376 * @param bool $cached use cached result
377 * @return bool true if table can be created or changed to compressed row format.
379 public function is_compressed_row_format_supported($cached = true) {
380 if ($cached and isset($this->compressedrowformatsupported)) {
381 return($this->compressedrowformatsupported);
384 $engine = strtolower($this->get_dbengine());
385 $info = $this->get_server_info();
387 if (version_compare($info['version'], '5.5.0') < 0) {
388 // MySQL 5.1 is not supported here because we cannot read the file format.
389 $this->compressedrowformatsupported = false;
391 } else if ($engine !== 'innodb' and $engine !== 'xtradb') {
392 // Other engines are not supported, most probably not compatible.
393 $this->compressedrowformatsupported = false;
395 } else if (!$this->is_file_per_table_enabled()) {
396 $this->compressedrowformatsupported = false;
398 } else if ($this->get_row_format() !== 'Barracuda') {
399 $this->compressedrowformatsupported = false;
401 } else {
402 // All the tests passed, we can safely use ROW_FORMAT=Compressed in sql statements.
403 $this->compressedrowformatsupported = true;
406 return $this->compressedrowformatsupported;
410 * Check the database to see if innodb_file_per_table is on.
412 * @return bool True if on otherwise false.
414 public function is_file_per_table_enabled() {
415 if ($filepertable = $this->get_record_sql("SHOW VARIABLES LIKE 'innodb_file_per_table'")) {
416 if ($filepertable->value == 'ON') {
417 return true;
420 return false;
424 * Check the database to see if innodb_large_prefix is on.
426 * @return bool True if on otherwise false.
428 public function is_large_prefix_enabled() {
429 if ($this->is_antelope_file_format_no_more_supported()) {
430 // Breaking change: Antelope file format support has been removed, only Barracuda.
431 return true;
434 if ($largeprefix = $this->get_record_sql("SHOW VARIABLES LIKE 'innodb_large_prefix'")) {
435 if ($largeprefix->value == 'ON') {
436 return true;
439 return false;
443 * Determine if the row format should be set to compressed, dynamic, or default.
445 * Terrible kludge. If we're using utf8mb4 AND we're using InnoDB, we need to specify row format to
446 * be either dynamic or compressed (default is compact) in order to allow for bigger indexes (MySQL
447 * errors #1709 and #1071).
449 * @param string $engine The database engine being used. Will be looked up if not supplied.
450 * @param string $collation The database collation to use. Will look up the current collation if not supplied.
451 * @return string An sql fragment to add to sql statements.
453 public function get_row_format_sql($engine = null, $collation = null) {
455 if (!isset($engine)) {
456 $engine = $this->get_dbengine();
458 $engine = strtolower($engine);
460 if (!isset($collation)) {
461 $collation = $this->get_dbcollation();
464 $rowformat = '';
465 if (($engine === 'innodb' || $engine === 'xtradb') && strpos($collation, 'utf8mb4_') === 0) {
466 if ($this->is_compressed_row_format_supported()) {
467 $rowformat = "ROW_FORMAT=Compressed";
468 } else {
469 $rowformat = "ROW_FORMAT=Dynamic";
472 return $rowformat;
476 * Returns localised database type name
477 * Note: can be used before connect()
478 * @return string
480 public function get_name() {
481 return get_string('nativemysqli', 'install');
485 * Returns localised database configuration help.
486 * Note: can be used before connect()
487 * @return string
489 public function get_configuration_help() {
490 return get_string('nativemysqlihelp', 'install');
494 * Diagnose database and tables, this function is used
495 * to verify database and driver settings, db engine types, etc.
497 * @return string null means everything ok, string means problem found.
499 public function diagnose() {
500 $sloppymyisamfound = false;
501 $prefix = str_replace('_', '\\_', $this->prefix);
502 $sql = "SELECT COUNT('x')
503 FROM INFORMATION_SCHEMA.TABLES
504 WHERE table_schema = DATABASE()
505 AND table_name LIKE BINARY '$prefix%'
506 AND Engine = 'MyISAM'";
507 $this->query_start($sql, null, SQL_QUERY_AUX);
508 $result = $this->mysqli->query($sql);
509 $this->query_end($result);
510 if ($result) {
511 if ($arr = $result->fetch_assoc()) {
512 $count = reset($arr);
513 if ($count) {
514 $sloppymyisamfound = true;
517 $result->close();
520 if ($sloppymyisamfound) {
521 return get_string('myisamproblem', 'error');
522 } else {
523 return null;
528 * Connect to db
529 * @param string $dbhost The database host.
530 * @param string $dbuser The database username.
531 * @param string $dbpass The database username's password.
532 * @param string $dbname The name of the database being connected to.e
533 * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
534 * @param array $dboptions driver specific options
535 * @return bool success
537 public function raw_connect(string $dbhost, string $dbuser, string $dbpass, string $dbname, $prefix, array $dboptions=null): bool {
538 $driverstatus = $this->driver_installed();
540 if ($driverstatus !== true) {
541 throw new dml_exception('dbdriverproblem', $driverstatus);
544 $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
546 // The dbsocket option is used ONLY if host is null or 'localhost'.
547 // You can not disable it because it is always tried if dbhost is 'localhost'.
548 if (!empty($this->dboptions['dbsocket'])
549 and (strpos($this->dboptions['dbsocket'], '/') !== false or strpos($this->dboptions['dbsocket'], '\\') !== false)) {
550 $dbsocket = $this->dboptions['dbsocket'];
551 } else {
552 $dbsocket = ini_get('mysqli.default_socket');
554 if (empty($this->dboptions['dbport'])) {
555 $dbport = (int)ini_get('mysqli.default_port');
556 } else {
557 $dbport = (int)$this->dboptions['dbport'];
559 // verify ini.get does not return nonsense
560 if (empty($dbport)) {
561 $dbport = 3306;
563 if ($dbhost and !empty($this->dboptions['dbpersist'])) {
564 $dbhost = "p:$dbhost";
567 // We want to keep exceptions out from the native driver.
568 // TODO: See MDL-75761 for future improvements.
569 mysqli_report(MYSQLI_REPORT_OFF); // Disable reporting (default before PHP 8.1).
571 $this->mysqli = mysqli_init();
572 if (!empty($this->dboptions['connecttimeout'])) {
573 $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->dboptions['connecttimeout']);
576 $conn = null;
577 $dberr = null;
578 try {
579 // real_connect() is doing things we don't expext.
580 $conn = @$this->mysqli->real_connect($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
581 } catch (\Exception $e) {
582 $dberr = "$e";
584 if (!$conn) {
585 $dberr = $dberr ?: $this->mysqli->connect_error;
586 $this->mysqli = null;
587 throw new dml_connection_exception($dberr);
590 // Disable logging until we are fully setup.
591 $this->query_log_prevent();
593 if (isset($dboptions['dbcollation'])) {
594 $collation = $this->dboptions['dbcollation'] = $dboptions['dbcollation'];
595 } else {
596 $collation = $this->detect_collation();
598 $collationinfo = explode('_', $collation);
599 $charset = reset($collationinfo);
601 $this->mysqli->set_charset($charset);
603 // If available, enforce strict mode for the session. That guaranties
604 // standard behaviour under some situations, avoiding some MySQL nasty
605 // habits like truncating data or performing some transparent cast losses.
606 // With strict mode enforced, Moodle DB layer will be consistently throwing
607 // the corresponding exceptions as expected.
608 $si = $this->get_server_info();
609 if (version_compare($si['version'], '5.0.2', '>=')) {
610 $sql = "SET SESSION sql_mode = 'STRICT_ALL_TABLES'";
611 $result = $this->mysqli->query($sql);
614 // We can enable logging now.
615 $this->query_log_allow();
617 // Connection stabilised and configured, going to instantiate the temptables controller
618 $this->temptables = new mysqli_native_moodle_temptables($this);
620 return true;
624 * Close database connection and release all resources
625 * and memory (especially circular memory references).
626 * Do NOT use connect() again, create a new instance if needed.
628 public function dispose() {
629 parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
630 if ($this->mysqli) {
631 $this->mysqli->close();
632 $this->mysqli = null;
637 * Gets db handle currently used with queries
638 * @return resource
640 protected function get_db_handle() {
641 return $this->mysqli;
645 * Sets db handle to be used with subsequent queries
646 * @param resource $dbh
647 * @return void
649 protected function set_db_handle($dbh): void {
650 $this->mysqli = $dbh;
654 * Check if The query qualifies for readonly connection execution
655 * Logging queries are exempt, those are write operations that circumvent
656 * standard query_start/query_end paths.
657 * @param int $type type of query
658 * @param string $sql
659 * @return bool
661 protected function can_use_readonly(int $type, string $sql): bool {
662 // ... *_LOCK queries always go to master.
663 if (preg_match('/\b(GET|RELEASE)_LOCK/i', $sql)) {
664 return false;
667 return $this->read_slave_can_use_readonly($type, $sql);
671 * Returns the version of the MySQL server, as reported by the PHP client connection.
673 * Wrap $this->mysqli->server_info to improve testing strategy.
675 * @return string A string representing the version of the MySQL server that the MySQLi extension is connected to.
677 protected function get_mysqli_server_info(): string {
678 return $this->mysqli->server_info;
682 * Returns the version of the MySQL server, as reported by 'SELECT VERSION()' query.
684 * @return string A string that indicates the MySQL server version.
685 * @throws dml_read_exception If the execution of 'SELECT VERSION()' query will fail.
687 protected function get_version_from_db(): string {
688 $version = null;
689 // Query the DB server for the server version.
690 $sql = "SELECT VERSION() version;";
691 try {
692 $result = $this->mysqli->query($sql);
693 if ($result) {
694 if ($row = $result->fetch_assoc()) {
695 $version = $row['version'];
697 $result->close();
698 unset($row);
700 } catch (\Throwable $e) { // Exceptions in case of MYSQLI_REPORT_STRICT.
701 // It looks like we've an issue out of the expected boolean 'false' result above.
702 throw new dml_read_exception($e->getMessage(), $sql);
704 if (empty($version)) {
705 // Exception dml_read_exception usually reports raw mysqli errors i.e. not localised by Moodle.
706 throw new dml_read_exception("Unable to read the DB server version.", $sql);
709 return $version;
713 * Returns whether $CFG->dboptions['versionfromdb'] has been set to boolean `true`.
715 * @return bool True if $CFG->dboptions['versionfromdb'] has been set to boolean `true`. Otherwise, `false`.
717 protected function should_db_version_be_read_from_db(): bool {
718 if (!empty($this->dboptions['versionfromdb'])) {
719 return true;
722 return false;
726 * Returns database server info array.
727 * @return array Array containing 'description' and 'version' info.
728 * @throws dml_read_exception If the execution of 'SELECT VERSION()' query will fail.
730 public function get_server_info() {
731 $version = $this->serverversion;
732 if (empty($version)) {
733 $version = $this->get_mysqli_server_info();
734 // The version returned by the PHP client could not be the actual DB server version.
735 // For example in MariaDB, it was prefixed by the RPL_VERSION_HACK, "5.5.5-" (MDEV-4088), starting from 10.x,
736 // when not using an authentication plug-in.
737 // Strip the RPL_VERSION_HACK prefix off - it will be "always" there in MariaDB until MDEV-28910 will be implemented.
738 $version = str_replace('5.5.5-', '', $version);
740 // Should we use the VERSION function to get the actual DB version instead of the PHP client version above?
741 if ($this->should_db_version_be_read_from_db()) {
742 // Try to query the actual version of the target database server: indeed some cloud providers, e.g. Azure,
743 // put a gateway in front of the actual instance which reports its own version to the PHP client
744 // and it doesn't represent the actual version of the DB server the PHP client is connected to.
745 // Refs:
746 // - https://learn.microsoft.com/en-us/azure/mariadb/concepts-supported-versions
747 // - https://learn.microsoft.com/en-us/azure/mysql/single-server/concepts-connect-to-a-gateway-node .
748 // Reset the version returned by the PHP client with the actual DB version reported by 'VERSION' function.
749 $version = $this->get_version_from_db();
752 // The version here starts with the following naming scheme: 'X.Y.Z[-<suffix>]'.
753 // Example: in MariaDB at least one suffix is "always" there, hardcoded in 'mysql_version.h.in':
754 // #define MYSQL_SERVER_VERSION "@VERSION@-MariaDB"
755 // MariaDB and MySQL server version could have extra suffixes too, set by the compilation environment,
756 // e.g. '-debug', '-embedded', '-log' or any other vendor specific suffix (e.g. build information).
757 // Strip out any suffix.
758 $parts = explode('-', $version, 2);
759 // Finally, keep just major, minor and patch versions (X.Y.Z) from the reported DB server version.
760 $this->serverversion = $parts[0];
763 return [
764 'description' => $this->get_mysqli_server_info(),
765 'version' => $this->serverversion
770 * Returns supported query parameter types
771 * @return int bitmask of accepted SQL_PARAMS_*
773 protected function allowed_param_types() {
774 return SQL_PARAMS_QM;
778 * Returns last error reported by database engine.
779 * @return string error message
781 public function get_last_error() {
782 return $this->mysqli->error;
786 * Return tables in database WITHOUT current prefix
787 * @param bool $usecache if true, returns list of cached tables.
788 * @return array of table names in lowercase and without prefix
790 public function get_tables($usecache=true) {
791 if ($usecache and $this->tables !== null) {
792 return $this->tables;
794 $this->tables = array();
795 $prefix = str_replace('_', '\\_', $this->prefix);
796 $sql = "SHOW TABLES LIKE '$prefix%'";
797 $this->query_start($sql, null, $usecache ? SQL_QUERY_AUX_READONLY : SQL_QUERY_AUX);
798 $result = $this->mysqli->query($sql);
799 $this->query_end($result);
800 $len = strlen($this->prefix);
801 if ($result) {
802 while ($arr = $result->fetch_assoc()) {
803 $tablename = reset($arr);
804 $tablename = substr($tablename, $len);
805 $this->tables[$tablename] = $tablename;
807 $result->close();
810 // Add the currently available temptables
811 $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
812 return $this->tables;
816 * Return table indexes - everything lowercased.
817 * @param string $table The table we want to get indexes from.
818 * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
820 public function get_indexes($table) {
821 $indexes = array();
822 $fixedtable = $this->fix_table_name($table);
823 $sql = "SHOW INDEXES FROM $fixedtable";
824 $this->query_start($sql, null, SQL_QUERY_AUX_READONLY);
825 $result = $this->mysqli->query($sql);
826 try {
827 $this->query_end($result);
828 } catch (dml_read_exception $e) {
829 return $indexes; // table does not exist - no indexes...
831 if ($result) {
832 while ($res = $result->fetch_object()) {
833 if ($res->Key_name === 'PRIMARY') {
834 continue;
836 if (!isset($indexes[$res->Key_name])) {
837 $indexes[$res->Key_name] = array('unique'=>empty($res->Non_unique), 'columns'=>array());
839 $indexes[$res->Key_name]['columns'][$res->Seq_in_index-1] = $res->Column_name;
841 $result->close();
843 return $indexes;
847 * Fetches detailed information about columns in table.
849 * @param string $table name
850 * @return database_column_info[] array of database_column_info objects indexed with column names
852 protected function fetch_columns(string $table): array {
853 $structure = array();
855 $sql = "SELECT column_name, data_type, character_maximum_length, numeric_precision,
856 numeric_scale, is_nullable, column_type, column_default, column_key, extra
857 FROM information_schema.columns
858 WHERE table_name = '" . $this->prefix.$table . "'
859 AND table_schema = '" . $this->dbname . "'
860 ORDER BY ordinal_position";
861 $this->query_start($sql, null, SQL_QUERY_AUX_READONLY);
862 $result = $this->mysqli->query($sql);
863 $this->query_end(true); // Don't want to throw anything here ever. MDL-30147
865 if ($result === false) {
866 return array();
869 if ($result->num_rows > 0) {
870 // standard table exists
871 while ($rawcolumn = $result->fetch_assoc()) {
872 // MySQL 8 BC: information_schema.* returns the fields in upper case.
873 $rawcolumn = array_change_key_case($rawcolumn, CASE_LOWER);
874 $info = (object)$this->get_column_info((object)$rawcolumn);
875 $structure[$info->name] = new database_column_info($info);
877 $result->close();
879 } else {
880 // temporary tables are not in information schema, let's try it the old way
881 $result->close();
882 $fixedtable = $this->fix_table_name($table);
883 $sql = "SHOW COLUMNS FROM $fixedtable";
884 $this->query_start($sql, null, SQL_QUERY_AUX_READONLY);
885 $result = $this->mysqli->query($sql);
886 $this->query_end(true);
887 if ($result === false) {
888 return array();
890 while ($rawcolumn = $result->fetch_assoc()) {
891 $rawcolumn = (object)array_change_key_case($rawcolumn, CASE_LOWER);
892 $rawcolumn->column_name = $rawcolumn->field; unset($rawcolumn->field);
893 $rawcolumn->column_type = $rawcolumn->type; unset($rawcolumn->type);
894 $rawcolumn->character_maximum_length = null;
895 $rawcolumn->numeric_precision = null;
896 $rawcolumn->numeric_scale = null;
897 $rawcolumn->is_nullable = $rawcolumn->null; unset($rawcolumn->null);
898 $rawcolumn->column_default = $rawcolumn->default; unset($rawcolumn->default);
899 $rawcolumn->column_key = $rawcolumn->key; unset($rawcolumn->key);
901 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
902 $rawcolumn->data_type = $matches[1];
903 $rawcolumn->character_maximum_length = $matches[2];
905 } else if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
906 $rawcolumn->data_type = $matches[1];
907 $rawcolumn->numeric_precision = $matches[2];
908 $rawcolumn->max_length = $rawcolumn->numeric_precision;
910 $type = strtoupper($matches[1]);
911 if ($type === 'BIGINT') {
912 $maxlength = 18;
913 } else if ($type === 'INT' or $type === 'INTEGER') {
914 $maxlength = 9;
915 } else if ($type === 'MEDIUMINT') {
916 $maxlength = 6;
917 } else if ($type === 'SMALLINT') {
918 $maxlength = 4;
919 } else if ($type === 'TINYINT') {
920 $maxlength = 2;
921 } else {
922 // This should not happen.
923 $maxlength = 0;
925 if ($maxlength < $rawcolumn->max_length) {
926 $rawcolumn->max_length = $maxlength;
929 } else if (preg_match('/(decimal)\((\d+),(\d+)\)/i', $rawcolumn->column_type, $matches)) {
930 $rawcolumn->data_type = $matches[1];
931 $rawcolumn->numeric_precision = $matches[2];
932 $rawcolumn->numeric_scale = $matches[3];
934 } else if (preg_match('/(double|float)(\((\d+),(\d+)\))?/i', $rawcolumn->column_type, $matches)) {
935 $rawcolumn->data_type = $matches[1];
936 $rawcolumn->numeric_precision = isset($matches[3]) ? $matches[3] : null;
937 $rawcolumn->numeric_scale = isset($matches[4]) ? $matches[4] : null;
939 } else if (preg_match('/([a-z]*text)/i', $rawcolumn->column_type, $matches)) {
940 $rawcolumn->data_type = $matches[1];
941 $rawcolumn->character_maximum_length = -1; // unknown
943 } else if (preg_match('/([a-z]*blob)/i', $rawcolumn->column_type, $matches)) {
944 $rawcolumn->data_type = $matches[1];
946 } else {
947 $rawcolumn->data_type = $rawcolumn->column_type;
950 $info = $this->get_column_info($rawcolumn);
951 $structure[$info->name] = new database_column_info($info);
953 $result->close();
956 return $structure;
960 * Indicates whether column information retrieved from `information_schema.columns` has default values quoted or not.
961 * @return boolean True when default values are quoted (breaking change); otherwise, false.
963 protected function has_breaking_change_quoted_defaults() {
964 return false;
968 * Indicates whether SQL_MODE default value has changed in a not backward compatible way.
969 * @return boolean True when SQL_MODE breaks BC; otherwise, false.
971 public function has_breaking_change_sqlmode() {
972 return false;
976 * Returns moodle column info for raw column from information schema.
977 * @param stdClass $rawcolumn
978 * @return stdClass standardised colum info
980 private function get_column_info(stdClass $rawcolumn) {
981 $rawcolumn = (object)$rawcolumn;
982 $info = new stdClass();
983 $info->name = $rawcolumn->column_name;
984 $info->type = $rawcolumn->data_type;
985 $info->meta_type = $this->mysqltype2moodletype($rawcolumn->data_type);
986 if ($this->has_breaking_change_quoted_defaults()) {
987 $info->default_value = is_null($rawcolumn->column_default) ? null : trim($rawcolumn->column_default, "'");
988 if ($info->default_value === 'NULL') {
989 $info->default_value = null;
991 } else {
992 $info->default_value = $rawcolumn->column_default;
994 $info->has_default = !is_null($info->default_value);
995 $info->not_null = ($rawcolumn->is_nullable === 'NO');
996 $info->primary_key = ($rawcolumn->column_key === 'PRI');
997 $info->binary = false;
998 $info->unsigned = null;
999 $info->auto_increment = false;
1000 $info->unique = null;
1001 $info->scale = null;
1003 if ($info->meta_type === 'C') {
1004 $info->max_length = $rawcolumn->character_maximum_length;
1006 } else if ($info->meta_type === 'I') {
1007 if ($info->primary_key) {
1008 $info->meta_type = 'R';
1009 $info->unique = true;
1011 // Return number of decimals, not bytes here.
1012 $info->max_length = $rawcolumn->numeric_precision;
1013 if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
1014 $type = strtoupper($matches[1]);
1015 if ($type === 'BIGINT') {
1016 $maxlength = 18;
1017 } else if ($type === 'INT' or $type === 'INTEGER') {
1018 $maxlength = 9;
1019 } else if ($type === 'MEDIUMINT') {
1020 $maxlength = 6;
1021 } else if ($type === 'SMALLINT') {
1022 $maxlength = 4;
1023 } else if ($type === 'TINYINT') {
1024 $maxlength = 2;
1025 } else {
1026 // This should not happen.
1027 $maxlength = 0;
1029 // It is possible that display precision is different from storage type length,
1030 // always use the smaller value to make sure our data fits.
1031 if ($maxlength < $info->max_length) {
1032 $info->max_length = $maxlength;
1035 $info->unsigned = (stripos($rawcolumn->column_type, 'unsigned') !== false);
1036 $info->auto_increment= (strpos($rawcolumn->extra, 'auto_increment') !== false);
1038 } else if ($info->meta_type === 'N') {
1039 $info->max_length = $rawcolumn->numeric_precision;
1040 $info->scale = $rawcolumn->numeric_scale;
1041 $info->unsigned = (stripos($rawcolumn->column_type, 'unsigned') !== false);
1043 } else if ($info->meta_type === 'X') {
1044 if ("$rawcolumn->character_maximum_length" === '4294967295') { // watch out for PHP max int limits!
1045 // means maximum moodle size for text column, in other drivers it may also mean unknown size
1046 $info->max_length = -1;
1047 } else {
1048 $info->max_length = $rawcolumn->character_maximum_length;
1050 $info->primary_key = false;
1052 } else if ($info->meta_type === 'B') {
1053 $info->max_length = -1;
1054 $info->primary_key = false;
1055 $info->binary = true;
1058 return $info;
1062 * Normalise column type.
1063 * @param string $mysql_type
1064 * @return string one character
1065 * @throws dml_exception
1067 private function mysqltype2moodletype($mysql_type) {
1068 $type = null;
1070 switch(strtoupper($mysql_type)) {
1071 case 'BIT':
1072 $type = 'L';
1073 break;
1075 case 'TINYINT':
1076 case 'SMALLINT':
1077 case 'MEDIUMINT':
1078 case 'INT':
1079 case 'INTEGER':
1080 case 'BIGINT':
1081 $type = 'I';
1082 break;
1084 case 'FLOAT':
1085 case 'DOUBLE':
1086 case 'DECIMAL':
1087 $type = 'N';
1088 break;
1090 case 'CHAR':
1091 case 'ENUM':
1092 case 'SET':
1093 case 'VARCHAR':
1094 $type = 'C';
1095 break;
1097 case 'TINYTEXT':
1098 case 'TEXT':
1099 case 'MEDIUMTEXT':
1100 case 'LONGTEXT':
1101 $type = 'X';
1102 break;
1104 case 'BINARY':
1105 case 'VARBINARY':
1106 case 'BLOB':
1107 case 'TINYBLOB':
1108 case 'MEDIUMBLOB':
1109 case 'LONGBLOB':
1110 $type = 'B';
1111 break;
1113 case 'DATE':
1114 case 'TIME':
1115 case 'DATETIME':
1116 case 'TIMESTAMP':
1117 case 'YEAR':
1118 $type = 'D';
1119 break;
1122 if (!$type) {
1123 throw new dml_exception('invalidmysqlnativetype', $mysql_type);
1125 return $type;
1129 * Normalise values based in RDBMS dependencies (booleans, LOBs...)
1131 * @param database_column_info $column column metadata corresponding with the value we are going to normalise
1132 * @param mixed $value value we are going to normalise
1133 * @return mixed the normalised value
1135 protected function normalise_value($column, $value) {
1136 $this->detect_objects($value);
1138 if (is_bool($value)) { // Always, convert boolean to int
1139 $value = (int)$value;
1141 } else if ($value === '') {
1142 if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
1143 $value = 0; // prevent '' problems in numeric fields
1145 // Any float value being stored in varchar or text field is converted to string to avoid
1146 // any implicit conversion by MySQL
1147 } else if (is_float($value) and ($column->meta_type == 'C' or $column->meta_type == 'X')) {
1148 $value = "$value";
1150 return $value;
1154 * Is this database compatible with utf8?
1155 * @return bool
1157 public function setup_is_unicodedb() {
1158 // All new tables are created with this collation, we just have to make sure it is utf8 compatible,
1159 // if config table already exists it has this collation too.
1160 $collation = $this->get_dbcollation();
1162 $collationinfo = explode('_', $collation);
1163 $charset = reset($collationinfo);
1165 $sql = "SHOW COLLATION WHERE Collation ='$collation' AND Charset = '$charset'";
1166 $this->query_start($sql, null, SQL_QUERY_AUX_READONLY);
1167 $result = $this->mysqli->query($sql);
1168 $this->query_end($result);
1169 if ($result->fetch_assoc()) {
1170 $return = true;
1171 } else {
1172 $return = false;
1174 $result->close();
1176 return $return;
1180 * Do NOT use in code, to be used by database_manager only!
1181 * @param string|array $sql query
1182 * @param array|null $tablenames an array of xmldb table names affected by this request.
1183 * @return bool true
1184 * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
1186 public function change_database_structure($sql, $tablenames = null) {
1187 $this->get_manager(); // Includes DDL exceptions classes ;-)
1188 if (is_array($sql)) {
1189 $sql = implode("\n;\n", $sql);
1192 try {
1193 $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
1194 $result = $this->mysqli->multi_query($sql);
1195 if ($result === false) {
1196 $this->query_end(false);
1198 while ($this->mysqli->more_results()) {
1199 $result = $this->mysqli->next_result();
1200 if ($result === false) {
1201 $this->query_end(false);
1204 $this->query_end(true);
1205 } catch (ddl_change_structure_exception $e) {
1206 while (@$this->mysqli->more_results()) {
1207 @$this->mysqli->next_result();
1209 $this->reset_caches($tablenames);
1210 throw $e;
1213 $this->reset_caches($tablenames);
1214 return true;
1218 * Very ugly hack which emulates bound parameters in queries
1219 * because prepared statements do not use query cache.
1221 protected function emulate_bound_params($sql, array $params=null) {
1222 if (empty($params)) {
1223 return $sql;
1225 // ok, we have verified sql statement with ? and correct number of params
1226 $parts = array_reverse(explode('?', $sql));
1227 $return = array_pop($parts);
1228 foreach ($params as $param) {
1229 if (is_bool($param)) {
1230 $return .= (int)$param;
1231 } else if (is_null($param)) {
1232 $return .= 'NULL';
1233 } else if (is_number($param)) {
1234 $return .= "'".$param."'"; // we have to always use strings because mysql is using weird automatic int casting
1235 } else if (is_float($param)) {
1236 $return .= $param;
1237 } else {
1238 $param = $this->mysqli->real_escape_string($param);
1239 $return .= "'$param'";
1241 $return .= array_pop($parts);
1243 return $return;
1247 * Execute general sql query. Should be used only when no other method suitable.
1248 * Do NOT use this to make changes in db structure, use database_manager methods instead!
1249 * @param string $sql query
1250 * @param array $params query parameters
1251 * @return bool true
1252 * @throws dml_exception A DML specific exception is thrown for any errors.
1254 public function execute($sql, array $params=null) {
1255 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1257 if (strpos($sql, ';') !== false) {
1258 throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
1261 $rawsql = $this->emulate_bound_params($sql, $params);
1263 $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1264 $result = $this->mysqli->query($rawsql);
1265 $this->query_end($result);
1267 if ($result === true) {
1268 return true;
1270 } else {
1271 $result->close();
1272 return true;
1277 * Get a number of records as a moodle_recordset using a SQL statement.
1279 * Since this method is a little less readable, use of it should be restricted to
1280 * code where it's possible there might be large datasets being returned. For known
1281 * small datasets use get_records_sql - it leads to simpler code.
1283 * The return type is like:
1284 * @see function get_recordset.
1286 * @param string $sql the SQL select query to execute.
1287 * @param array $params array of sql parameters
1288 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1289 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1290 * @return moodle_recordset instance
1291 * @throws dml_exception A DML specific exception is thrown for any errors.
1293 public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1295 list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
1297 if ($limitfrom or $limitnum) {
1298 if ($limitnum < 1) {
1299 $limitnum = "18446744073709551615";
1301 $sql .= " LIMIT $limitfrom, $limitnum";
1304 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1305 $rawsql = $this->emulate_bound_params($sql, $params);
1307 $this->query_start($sql, $params, SQL_QUERY_SELECT);
1308 // no MYSQLI_USE_RESULT here, it would block write ops on affected tables
1309 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
1310 $this->query_end($result);
1312 return $this->create_recordset($result);
1316 * Get all records from a table.
1318 * This method works around potential memory problems and may improve performance,
1319 * this method may block access to table until the recordset is closed.
1321 * @param string $table Name of database table.
1322 * @return moodle_recordset A moodle_recordset instance {@link function get_recordset}.
1323 * @throws dml_exception A DML specific exception is thrown for any errors.
1325 public function export_table_recordset($table) {
1326 $sql = $this->fix_table_names("SELECT * FROM {{$table}}");
1328 $this->query_start($sql, array(), SQL_QUERY_SELECT);
1329 // MYSQLI_STORE_RESULT may eat all memory for large tables, unfortunately MYSQLI_USE_RESULT blocks other queries.
1330 $result = $this->mysqli->query($sql, MYSQLI_USE_RESULT);
1331 $this->query_end($result);
1333 return $this->create_recordset($result);
1336 protected function create_recordset($result) {
1337 return new mysqli_native_moodle_recordset($result);
1341 * Get a number of records as an array of objects using a SQL statement.
1343 * Return value is like:
1344 * @see function get_records.
1346 * @param string $sql the SQL select query to execute. The first column of this SELECT statement
1347 * must be a unique value (usually the 'id' field), as it will be used as the key of the
1348 * returned array.
1349 * @param array $params array of sql parameters
1350 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1351 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1352 * @return array of objects, or empty array if no records were found
1353 * @throws dml_exception A DML specific exception is thrown for any errors.
1355 public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1357 list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
1359 if ($limitfrom or $limitnum) {
1360 if ($limitnum < 1) {
1361 $limitnum = "18446744073709551615";
1363 $sql .= " LIMIT $limitfrom, $limitnum";
1366 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1367 $rawsql = $this->emulate_bound_params($sql, $params);
1369 $this->query_start($sql, $params, SQL_QUERY_SELECT);
1370 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
1371 $this->query_end($result);
1373 $return = array();
1375 while($row = $result->fetch_assoc()) {
1376 $row = array_change_key_case($row, CASE_LOWER);
1377 $id = reset($row);
1378 if (isset($return[$id])) {
1379 $colname = key($row);
1380 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);
1382 $return[$id] = (object)$row;
1384 $result->close();
1386 return $return;
1390 * Selects records and return values (first field) as an array using a SQL statement.
1392 * @param string $sql The SQL query
1393 * @param array $params array of sql parameters
1394 * @return array of values
1395 * @throws dml_exception A DML specific exception is thrown for any errors.
1397 public function get_fieldset_sql($sql, array $params=null) {
1398 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1399 $rawsql = $this->emulate_bound_params($sql, $params);
1401 $this->query_start($sql, $params, SQL_QUERY_SELECT);
1402 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
1403 $this->query_end($result);
1405 $return = array();
1407 while($row = $result->fetch_assoc()) {
1408 $return[] = reset($row);
1410 $result->close();
1412 return $return;
1416 * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
1417 * @param string $table name
1418 * @param mixed $params data record as object or array
1419 * @param bool $returnit return it of inserted record
1420 * @param bool $bulk true means repeated inserts expected
1421 * @param bool $customsequence true if 'id' included in $params, disables $returnid
1422 * @return bool|int true or new id
1423 * @throws dml_exception A DML specific exception is thrown for any errors.
1425 public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
1426 if (!is_array($params)) {
1427 $params = (array)$params;
1430 if ($customsequence) {
1431 if (!isset($params['id'])) {
1432 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
1434 $returnid = false;
1435 } else {
1436 unset($params['id']);
1439 if (empty($params)) {
1440 throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
1443 $fields = implode(',', array_keys($params));
1444 $qms = array_fill(0, count($params), '?');
1445 $qms = implode(',', $qms);
1446 $fixedtable = $this->fix_table_name($table);
1447 $sql = "INSERT INTO $fixedtable ($fields) VALUES($qms)";
1449 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1450 $rawsql = $this->emulate_bound_params($sql, $params);
1452 $this->query_start($sql, $params, SQL_QUERY_INSERT);
1453 $result = $this->mysqli->query($rawsql);
1454 $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db
1455 $this->query_end($result);
1457 if (!$customsequence and !$id) {
1458 throw new dml_write_exception('unknown error fetching inserted id');
1461 if (!$returnid) {
1462 return true;
1463 } else {
1464 return (int)$id;
1469 * Insert a record into a table and return the "id" field if required.
1471 * Some conversions and safety checks are carried out. Lobs are supported.
1472 * If the return ID isn't required, then this just reports success as true/false.
1473 * $data is an object containing needed data
1474 * @param string $table The database table to be inserted into
1475 * @param object|array $dataobject A data object with values for one or more fields in the record
1476 * @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.
1477 * @return bool|int true or new id
1478 * @throws dml_exception A DML specific exception is thrown for any errors.
1480 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1481 $dataobject = (array)$dataobject;
1483 $columns = $this->get_columns($table);
1484 if (empty($columns)) {
1485 throw new dml_exception('ddltablenotexist', $table);
1488 $cleaned = array();
1490 foreach ($dataobject as $field=>$value) {
1491 if ($field === 'id') {
1492 continue;
1494 if (!isset($columns[$field])) {
1495 continue;
1497 $column = $columns[$field];
1498 $cleaned[$field] = $this->normalise_value($column, $value);
1501 return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
1505 * Get chunk size for multiple records insert
1506 * @return int
1508 private function insert_chunk_size(): int {
1509 // MySQL has a relatively small query length limit by default,
1510 // make sure 'max_allowed_packet' in my.cnf is high enough
1511 // if you change the following default...
1512 static $chunksize = null;
1513 if ($chunksize === null) {
1514 if (!empty($this->dboptions['bulkinsertsize'])) {
1515 $chunksize = (int)$this->dboptions['bulkinsertsize'];
1517 } else {
1518 if (PHP_INT_SIZE === 4) {
1519 // Bad luck for Windows, we cannot do any maths with large numbers.
1520 $chunksize = 5;
1521 } else {
1522 $sql = "SHOW VARIABLES LIKE 'max_allowed_packet'";
1523 $this->query_start($sql, null, SQL_QUERY_AUX);
1524 $result = $this->mysqli->query($sql);
1525 $this->query_end($result);
1526 $size = 0;
1527 if ($rec = $result->fetch_assoc()) {
1528 $size = $rec['Value'];
1530 $result->close();
1531 // Hopefully 200kb per object are enough.
1532 $chunksize = (int)($size / 200000);
1533 if ($chunksize > 50) {
1534 $chunksize = 50;
1539 return $chunksize;
1543 * Insert multiple records into database as fast as possible.
1545 * Order of inserts is maintained, but the operation is not atomic,
1546 * use transactions if necessary.
1548 * This method is intended for inserting of large number of small objects,
1549 * do not use for huge objects with text or binary fields.
1551 * @since Moodle 2.7
1553 * @param string $table The database table to be inserted into
1554 * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
1555 * @return void does not return new record ids
1557 * @throws coding_exception if data objects have different structure
1558 * @throws dml_exception A DML specific exception is thrown for any errors.
1560 public function insert_records($table, $dataobjects) {
1561 if (!is_array($dataobjects) && !$dataobjects instanceof Traversable) {
1562 throw new coding_exception('insert_records() passed non-traversable object');
1565 $chunksize = $this->insert_chunk_size();
1566 $columns = $this->get_columns($table, true);
1567 $fields = null;
1568 $count = 0;
1569 $chunk = array();
1570 foreach ($dataobjects as $dataobject) {
1571 if (!is_array($dataobject) and !is_object($dataobject)) {
1572 throw new coding_exception('insert_records() passed invalid record object');
1574 $dataobject = (array)$dataobject;
1575 if ($fields === null) {
1576 $fields = array_keys($dataobject);
1577 $columns = array_intersect_key($columns, $dataobject);
1578 unset($columns['id']);
1579 } else if ($fields !== array_keys($dataobject)) {
1580 throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
1583 $count++;
1584 $chunk[] = $dataobject;
1586 if ($count === $chunksize) {
1587 $this->insert_chunk($table, $chunk, $columns);
1588 $chunk = array();
1589 $count = 0;
1593 if ($count) {
1594 $this->insert_chunk($table, $chunk, $columns);
1599 * Insert records in chunks.
1601 * Note: can be used only from insert_records().
1603 * @param string $table
1604 * @param array $chunk
1605 * @param database_column_info[] $columns
1607 protected function insert_chunk($table, array $chunk, array $columns) {
1608 $fieldssql = '('.implode(',', array_keys($columns)).')';
1610 $valuessql = '('.implode(',', array_fill(0, count($columns), '?')).')';
1611 $valuessql = implode(',', array_fill(0, count($chunk), $valuessql));
1613 $params = array();
1614 foreach ($chunk as $dataobject) {
1615 foreach ($columns as $field => $column) {
1616 $params[] = $this->normalise_value($column, $dataobject[$field]);
1620 $fixedtable = $this->fix_table_name($table);
1621 $sql = "INSERT INTO $fixedtable $fieldssql VALUES $valuessql";
1623 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1624 $rawsql = $this->emulate_bound_params($sql, $params);
1626 $this->query_start($sql, $params, SQL_QUERY_INSERT);
1627 $result = $this->mysqli->query($rawsql);
1628 $this->query_end($result);
1632 * Import a record into a table, id field is required.
1633 * Safety checks are NOT carried out. Lobs are supported.
1635 * @param string $table name of database table to be inserted into
1636 * @param object $dataobject A data object with values for one or more fields in the record
1637 * @return bool true
1638 * @throws dml_exception A DML specific exception is thrown for any errors.
1640 public function import_record($table, $dataobject) {
1641 $dataobject = (array)$dataobject;
1643 $columns = $this->get_columns($table);
1644 $cleaned = array();
1646 foreach ($dataobject as $field=>$value) {
1647 if (!isset($columns[$field])) {
1648 continue;
1650 $cleaned[$field] = $value;
1653 return $this->insert_record_raw($table, $cleaned, false, true, true);
1657 * Update record in database, as fast as possible, no safety checks, lobs not supported.
1658 * @param string $table name
1659 * @param mixed $params data record as object or array
1660 * @param bool true means repeated updates expected
1661 * @return bool true
1662 * @throws dml_exception A DML specific exception is thrown for any errors.
1664 public function update_record_raw($table, $params, $bulk=false) {
1665 $params = (array)$params;
1667 if (!isset($params['id'])) {
1668 throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1670 $id = $params['id'];
1671 unset($params['id']);
1673 if (empty($params)) {
1674 throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1677 $sets = array();
1678 foreach ($params as $field=>$value) {
1679 $sets[] = "$field = ?";
1682 $params[] = $id; // last ? in WHERE condition
1684 $sets = implode(',', $sets);
1685 $fixedtable = $this->fix_table_name($table);
1686 $sql = "UPDATE $fixedtable SET $sets WHERE id=?";
1688 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1689 $rawsql = $this->emulate_bound_params($sql, $params);
1691 $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1692 $result = $this->mysqli->query($rawsql);
1693 $this->query_end($result);
1695 return true;
1699 * Update a record in a table
1701 * $dataobject is an object containing needed data
1702 * Relies on $dataobject having a variable "id" to
1703 * specify the record to update
1705 * @param string $table The database table to be checked against.
1706 * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1707 * @param bool true means repeated updates expected
1708 * @return bool true
1709 * @throws dml_exception A DML specific exception is thrown for any errors.
1711 public function update_record($table, $dataobject, $bulk=false) {
1712 $dataobject = (array)$dataobject;
1714 $columns = $this->get_columns($table);
1715 $cleaned = array();
1717 foreach ($dataobject as $field=>$value) {
1718 if (!isset($columns[$field])) {
1719 continue;
1721 $column = $columns[$field];
1722 $cleaned[$field] = $this->normalise_value($column, $value);
1725 return $this->update_record_raw($table, $cleaned, $bulk);
1729 * Set a single field in every table record which match a particular WHERE clause.
1731 * @param string $table The database table to be checked against.
1732 * @param string $newfield the field to set.
1733 * @param string $newvalue the value to set the field to.
1734 * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1735 * @param array $params array of sql parameters
1736 * @return bool true
1737 * @throws dml_exception A DML specific exception is thrown for any errors.
1739 public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1740 if ($select) {
1741 $select = "WHERE $select";
1743 if (is_null($params)) {
1744 $params = array();
1746 list($select, $params, $type) = $this->fix_sql_params($select, $params);
1748 // Get column metadata
1749 $columns = $this->get_columns($table);
1750 $column = $columns[$newfield];
1752 $normalised_value = $this->normalise_value($column, $newvalue);
1754 if (is_null($normalised_value)) {
1755 $newfield = "$newfield = NULL";
1756 } else {
1757 $newfield = "$newfield = ?";
1758 array_unshift($params, $normalised_value);
1760 $fixedtable = $this->fix_table_name($table);
1761 $sql = "UPDATE $fixedtable SET $newfield $select";
1762 $rawsql = $this->emulate_bound_params($sql, $params);
1764 $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1765 $result = $this->mysqli->query($rawsql);
1766 $this->query_end($result);
1768 return true;
1772 * Delete one or more records from a table which match a particular WHERE clause.
1774 * @param string $table The database table to be checked against.
1775 * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1776 * @param array $params array of sql parameters
1777 * @return bool true
1778 * @throws dml_exception A DML specific exception is thrown for any errors.
1780 public function delete_records_select($table, $select, array $params=null) {
1781 if ($select) {
1782 $select = "WHERE $select";
1784 $fixedtable = $this->fix_table_name($table);
1785 $sql = "DELETE FROM $fixedtable $select";
1787 list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1788 $rawsql = $this->emulate_bound_params($sql, $params);
1790 $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1791 $result = $this->mysqli->query($rawsql);
1792 $this->query_end($result);
1794 return true;
1798 * Deletes records using a subquery, which is done with a strange DELETE...JOIN syntax in MySQL
1799 * because it performs very badly with normal subqueries.
1801 * @param string $table Table to delete from
1802 * @param string $field Field in table to match
1803 * @param string $alias Name of single column in subquery e.g. 'id'
1804 * @param string $subquery Query that will return values of the field to delete
1805 * @param array $params Parameters for query
1806 * @throws dml_exception If there is any error
1808 public function delete_records_subquery(string $table, string $field, string $alias, string $subquery, array $params = []): void {
1809 // Aliases mysql_deltable and mysql_subquery are chosen to be unlikely to conflict.
1810 $this->execute("DELETE mysql_deltable FROM {" . $table . "} mysql_deltable JOIN " .
1811 "($subquery) mysql_subquery ON mysql_subquery.$alias = mysql_deltable.$field", $params);
1814 public function sql_cast_char2int($fieldname, $text=false) {
1815 return ' CAST(' . $fieldname . ' AS SIGNED) ';
1818 public function sql_cast_char2real($fieldname, $text=false) {
1819 // Set to 65 (max mysql 5.5 precision) with 7 as scale
1820 // because we must ensure at least 6 decimal positions
1821 // per casting given that postgres is casting to that scale (::real::).
1822 // Can be raised easily but that must be done in all DBs and tests.
1823 return ' CAST(' . $fieldname . ' AS DECIMAL(65,7)) ';
1826 public function sql_equal($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notequal = false) {
1827 $equalop = $notequal ? '<>' : '=';
1829 $collationinfo = explode('_', $this->get_dbcollation());
1830 $bincollate = reset($collationinfo) . '_bin';
1832 if ($casesensitive) {
1833 // Current MySQL versions do not support case sensitive and accent insensitive.
1834 return "$fieldname COLLATE $bincollate $equalop $param";
1835 } else if ($accentsensitive) {
1836 // Case insensitive and accent sensitive, we can force a binary comparison once all texts are using the same case.
1837 return "LOWER($fieldname) COLLATE $bincollate $equalop LOWER($param)";
1838 } else {
1839 // Case insensitive and accent insensitive. All collations are that way, but utf8_bin.
1840 $collation = '';
1841 if ($this->get_dbcollation() == 'utf8_bin') {
1842 $collation = 'COLLATE utf8_unicode_ci';
1843 } else if ($this->get_dbcollation() == 'utf8mb4_bin') {
1844 $collation = 'COLLATE utf8mb4_unicode_ci';
1846 return "$fieldname $collation $equalop $param";
1851 * Returns 'LIKE' part of a query.
1853 * Note that mysql does not support $casesensitive = true and $accentsensitive = false.
1854 * More information in http://bugs.mysql.com/bug.php?id=19567.
1856 * @param string $fieldname usually name of the table column
1857 * @param string $param usually bound query parameter (?, :named)
1858 * @param bool $casesensitive use case sensitive search
1859 * @param bool $accensensitive use accent sensitive search (ignored if $casesensitive is true)
1860 * @param bool $notlike true means "NOT LIKE"
1861 * @param string $escapechar escape char for '%' and '_'
1862 * @return string SQL code fragment
1864 public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1865 if (strpos($param, '%') !== false) {
1866 debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1868 $escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\'
1870 $collationinfo = explode('_', $this->get_dbcollation());
1871 $bincollate = reset($collationinfo) . '_bin';
1873 $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1875 if ($casesensitive) {
1876 // Current MySQL versions do not support case sensitive and accent insensitive.
1877 return "$fieldname $LIKE $param COLLATE $bincollate ESCAPE '$escapechar'";
1879 } else if ($accentsensitive) {
1880 // Case insensitive and accent sensitive, we can force a binary comparison once all texts are using the same case.
1881 return "LOWER($fieldname) $LIKE LOWER($param) COLLATE $bincollate ESCAPE '$escapechar'";
1883 } else {
1884 // Case insensitive and accent insensitive.
1885 $collation = '';
1886 if ($this->get_dbcollation() == 'utf8_bin') {
1887 // Force a case insensitive comparison if using utf8_bin.
1888 $collation = 'COLLATE utf8_unicode_ci';
1889 } else if ($this->get_dbcollation() == 'utf8mb4_bin') {
1890 // Force a case insensitive comparison if using utf8mb4_bin.
1891 $collation = 'COLLATE utf8mb4_unicode_ci';
1894 return "$fieldname $LIKE $param $collation ESCAPE '$escapechar'";
1899 * Returns the proper SQL to do CONCAT between the elements passed
1900 * Can take many parameters
1902 * @param string $str,... 1 or more fields/strings to concat
1904 * @return string The concat sql
1906 public function sql_concat() {
1907 $arr = func_get_args();
1908 $s = implode(', ', $arr);
1909 if ($s === '') {
1910 return "''";
1912 return "CONCAT($s)";
1916 * Returns the proper SQL to do CONCAT between the elements passed
1917 * with a given separator
1919 * @param string $separator The string to use as the separator
1920 * @param array $elements An array of items to concatenate
1921 * @return string The concat SQL
1923 public function sql_concat_join($separator="' '", $elements=array()) {
1924 $s = implode(', ', $elements);
1926 if ($s === '') {
1927 return "''";
1929 return "CONCAT_WS($separator, $s)";
1933 * Return SQL for performing group concatenation on given field/expression
1935 * @param string $field
1936 * @param string $separator
1937 * @param string $sort
1938 * @return string
1940 public function sql_group_concat(string $field, string $separator = ', ', string $sort = ''): string {
1941 $fieldsort = $sort ? "ORDER BY {$sort}" : '';
1942 return "GROUP_CONCAT({$field} {$fieldsort} SEPARATOR '{$separator}')";
1946 * Returns the SQL text to be used to calculate the length in characters of one expression.
1947 * @param string fieldname or expression to calculate its length in characters.
1948 * @return string the piece of SQL code to be used in the statement.
1950 public function sql_length($fieldname) {
1951 return ' CHAR_LENGTH(' . $fieldname . ')';
1955 * Does this driver support regex syntax when searching
1957 public function sql_regex_supported() {
1958 return true;
1962 * Return regex positive or negative match sql
1963 * @param bool $positivematch
1964 * @param bool $casesensitive
1965 * @return string or empty if not supported
1967 public function sql_regex($positivematch = true, $casesensitive = false) {
1968 $collation = '';
1969 if ($casesensitive) {
1970 if (substr($this->get_dbcollation(), -4) !== '_bin') {
1971 $collationinfo = explode('_', $this->get_dbcollation());
1972 $collation = 'COLLATE ' . $collationinfo[0] . '_bin ';
1974 } else {
1975 if ($this->get_dbcollation() == 'utf8_bin') {
1976 $collation = 'COLLATE utf8_unicode_ci ';
1977 } else if ($this->get_dbcollation() == 'utf8mb4_bin') {
1978 $collation = 'COLLATE utf8mb4_unicode_ci ';
1982 return $collation . ($positivematch ? 'REGEXP' : 'NOT REGEXP');
1986 * Returns the word-beginning boundary marker based on MySQL version.
1987 * @return string The word-beginning boundary marker.
1989 public function sql_regex_get_word_beginning_boundary_marker() {
1990 $ismysql = ($this->get_dbtype() == 'mysqli' || $this->get_dbtype() == 'auroramysql');
1991 $ismysqlge8d0d4 = ($ismysql && version_compare($this->get_server_info()['version'], '8.0.4', '>='));
1992 if ($ismysqlge8d0d4) {
1993 return '\\b';
1995 // Prior to MySQL 8.0.4, MySQL used the Henry Spencer regular expression library to support regular expression operations,
1996 // rather than International Components for Unicode (ICU).
1997 // MariaDB still supports the "old marker" (MDEV-5357).
1998 return '[[:<:]]';
2002 * Returns the word-end boundary marker based on MySQL version.
2003 * @return string The word-end boundary marker.
2005 public function sql_regex_get_word_end_boundary_marker() {
2006 $ismysql = ($this->get_dbtype() == 'mysqli' || $this->get_dbtype() == 'auroramysql');
2007 $ismysqlge8d0d4 = ($ismysql && version_compare($this->get_server_info()['version'], '8.0.4', '>='));
2008 if ($ismysqlge8d0d4) {
2009 return '\\b';
2011 // Prior to MySQL 8.0.4, MySQL used the Henry Spencer regular expression library to support regular expression operations,
2012 // rather than International Components for Unicode (ICU).
2013 // MariaDB still supports the "old marker" (MDEV-5357).
2014 return '[[:>:]]';
2018 * Returns the SQL to be used in order to an UNSIGNED INTEGER column to SIGNED.
2020 * @deprecated since 2.3
2021 * @param string $fieldname The name of the field to be cast
2022 * @return string The piece of SQL code to be used in your statement.
2024 public function sql_cast_2signed($fieldname) {
2025 return ' CAST(' . $fieldname . ' AS SIGNED) ';
2029 * Returns the SQL that allows to find intersection of two or more queries
2031 * @since Moodle 2.8
2033 * @param array $selects array of SQL select queries, each of them only returns fields with the names from $fields
2034 * @param string $fields comma-separated list of fields
2035 * @return string SQL query that will return only values that are present in each of selects
2037 public function sql_intersect($selects, $fields) {
2038 if (count($selects) <= 1) {
2039 return parent::sql_intersect($selects, $fields);
2041 $fields = preg_replace('/\s/', '', $fields);
2042 static $aliascnt = 0;
2043 $falias = 'intsctal'.($aliascnt++);
2044 $rv = "SELECT $falias.".
2045 preg_replace('/,/', ','.$falias.'.', $fields).
2046 " FROM ($selects[0]) $falias";
2047 for ($i = 1; $i < count($selects); $i++) {
2048 $alias = 'intsctal'.($aliascnt++);
2049 $rv .= " JOIN (".$selects[$i].") $alias ON ".
2050 join(' AND ',
2051 array_map(
2052 function($a) use ($alias, $falias) {
2053 return $falias . '.' . $a .' = ' . $alias . '.' . $a;
2055 preg_split('/,/', $fields))
2058 return $rv;
2062 * Does this driver support tool_replace?
2064 * @since Moodle 2.6.1
2065 * @return bool
2067 public function replace_all_text_supported() {
2068 return true;
2071 public function session_lock_supported() {
2072 return true;
2076 * Obtain session lock
2077 * @param int $rowid id of the row with session record
2078 * @param int $timeout max allowed time to wait for the lock in seconds
2079 * @return void
2081 public function get_session_lock($rowid, $timeout) {
2082 parent::get_session_lock($rowid, $timeout);
2084 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
2085 $sql = "SELECT GET_LOCK('$fullname', $timeout)";
2086 $this->query_start($sql, null, SQL_QUERY_AUX);
2087 $result = $this->mysqli->query($sql);
2088 $this->query_end($result);
2090 if ($result) {
2091 $arr = $result->fetch_assoc();
2092 $result->close();
2094 if (reset($arr) == 1) {
2095 return;
2096 } else {
2097 throw new dml_sessionwait_exception();
2102 public function release_session_lock($rowid) {
2103 if (!$this->used_for_db_sessions) {
2104 return;
2107 parent::release_session_lock($rowid);
2108 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
2109 $sql = "SELECT RELEASE_LOCK('$fullname')";
2110 $this->query_start($sql, null, SQL_QUERY_AUX);
2111 $result = $this->mysqli->query($sql);
2112 $this->query_end($result);
2114 if ($result) {
2115 $result->close();
2120 * Are transactions supported?
2121 * It is not responsible to run productions servers
2122 * on databases without transaction support ;-)
2124 * MyISAM does not support support transactions.
2126 * You can override this via the dbtransactions option.
2128 * @return bool
2130 protected function transactions_supported() {
2131 if (!is_null($this->transactions_supported)) {
2132 return $this->transactions_supported;
2135 // this is all just guessing, might be better to just specify it in config.php
2136 if (isset($this->dboptions['dbtransactions'])) {
2137 $this->transactions_supported = $this->dboptions['dbtransactions'];
2138 return $this->transactions_supported;
2141 $this->transactions_supported = false;
2143 $engine = $this->get_dbengine();
2145 // Only will accept transactions if using compatible storage engine (more engines can be added easily BDB, Falcon...)
2146 if (in_array($engine, array('InnoDB', 'INNOBASE', 'BDB', 'XtraDB', 'Aria', 'Falcon'))) {
2147 $this->transactions_supported = true;
2150 return $this->transactions_supported;
2154 * Driver specific start of real database transaction,
2155 * this can not be used directly in code.
2156 * @return void
2158 protected function begin_transaction() {
2159 if (!$this->transactions_supported()) {
2160 return;
2163 $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
2164 $this->query_start($sql, null, SQL_QUERY_AUX);
2165 $result = $this->mysqli->query($sql);
2166 $this->query_end($result);
2168 $sql = "START TRANSACTION";
2169 $this->query_start($sql, null, SQL_QUERY_AUX);
2170 $result = $this->mysqli->query($sql);
2171 $this->query_end($result);
2175 * Driver specific commit of real database transaction,
2176 * this can not be used directly in code.
2177 * @return void
2179 protected function commit_transaction() {
2180 if (!$this->transactions_supported()) {
2181 return;
2184 $sql = "COMMIT";
2185 $this->query_start($sql, null, SQL_QUERY_AUX);
2186 $result = $this->mysqli->query($sql);
2187 $this->query_end($result);
2191 * Driver specific abort of real database transaction,
2192 * this can not be used directly in code.
2193 * @return void
2195 protected function rollback_transaction() {
2196 if (!$this->transactions_supported()) {
2197 return;
2200 $sql = "ROLLBACK";
2201 $this->query_start($sql, null, SQL_QUERY_AUX);
2202 $result = $this->mysqli->query($sql);
2203 $this->query_end($result);
2205 return true;
2209 * Converts a table to either 'Compressed' or 'Dynamic' row format.
2211 * @param string $tablename Name of the table to convert to the new row format.
2213 public function convert_table_row_format($tablename) {
2214 $currentrowformat = $this->get_row_format($tablename);
2215 if ($currentrowformat == 'Compact' || $currentrowformat == 'Redundant') {
2216 $rowformat = ($this->is_compressed_row_format_supported(false)) ? "ROW_FORMAT=Compressed" : "ROW_FORMAT=Dynamic";
2217 $prefix = $this->get_prefix();
2218 $this->change_database_structure("ALTER TABLE {$prefix}$tablename $rowformat");
2223 * Does this mysql instance support fulltext indexes?
2225 * @return bool
2227 public function is_fulltext_search_supported() {
2228 $info = $this->get_server_info();
2230 if (version_compare($info['version'], '5.6.4', '>=')) {
2231 return true;
2233 return false;
2237 * Fixes any table names that clash with reserved words.
2239 * @param string $tablename The table name
2240 * @return string The fixed table name
2242 protected function fix_table_name($tablename) {
2243 $prefixedtablename = parent::fix_table_name($tablename);
2244 // This function quotes the table name if it matches one of the MySQL reserved
2245 // words, e.g. groups.
2246 return $this->get_manager()->generator->getEncQuoted($prefixedtablename);