See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Native mysqli class representing moodle database interface. 19 * 20 * @package core_dml 21 * @copyright 2008 Petr Skoda (http://skodak.org) 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 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'); 31 32 /** 33 * Native mysqli class representing moodle database interface. 34 * 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 38 */ 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; 42 } 43 44 /** @var mysqli $mysqli */ 45 protected $mysqli = null; 46 /** @var bool is compressed row format supported cache */ 47 protected $compressedrowformatsupported = null; 48 49 private $transactions_supported = null; 50 51 /** 52 * Attempt to create the database 53 * @param string $dbhost 54 * @param string $dbuser 55 * @param string $dbpass 56 * @param string $dbname 57 * @return bool success 58 * @throws dml_exception A DML specific exception is thrown for any errors. 59 */ 60 public function create_database($dbhost, $dbuser, $dbpass, $dbname, array $dboptions=null) { 61 $driverstatus = $this->driver_installed(); 62 63 if ($driverstatus !== true) { 64 throw new dml_exception('dbdriverproblem', $driverstatus); 65 } 66 67 if (!empty($dboptions['dbsocket']) 68 and (strpos($dboptions['dbsocket'], '/') !== false or strpos($dboptions['dbsocket'], '\\') !== false)) { 69 $dbsocket = $dboptions['dbsocket']; 70 } else { 71 $dbsocket = ini_get('mysqli.default_socket'); 72 } 73 if (empty($dboptions['dbport'])) { 74 $dbport = (int)ini_get('mysqli.default_port'); 75 } else { 76 $dbport = (int)$dboptions['dbport']; 77 } 78 // verify ini.get does not return nonsense 79 if (empty($dbport)) { 80 $dbport = 3306; 81 } 82 ob_start(); 83 $conn = new mysqli($dbhost, $dbuser, $dbpass, '', $dbport, $dbsocket); // Connect without db 84 $dberr = ob_get_contents(); 85 ob_end_clean(); 86 $errorno = @$conn->connect_errno; 87 88 if ($errorno !== 0) { 89 throw new dml_connection_exception($dberr); 90 } 91 92 // Normally a check would be done before setting utf8mb4, but the database can be created 93 // before the enviroment checks are done. We'll proceed with creating the database and then do checks next. 94 $charset = 'utf8mb4'; 95 if (isset($dboptions['dbcollation']) and (strpos($dboptions['dbcollation'], 'utf8_') === 0 96 || strpos($dboptions['dbcollation'], 'utf8mb4_') === 0)) { 97 $collation = $dboptions['dbcollation']; 98 $collationinfo = explode('_', $dboptions['dbcollation']); 99 $charset = reset($collationinfo); 100 } else { 101 $collation = 'utf8mb4_unicode_ci'; 102 } 103 104 $result = $conn->query("CREATE DATABASE $dbname DEFAULT CHARACTER SET $charset DEFAULT COLLATE ".$collation); 105 106 $conn->close(); 107 108 if (!$result) { 109 throw new dml_exception('cannotcreatedb'); 110 } 111 112 return true; 113 } 114 115 /** 116 * Detects if all needed PHP stuff installed. 117 * Note: can be used before connect() 118 * @return mixed true if ok, string if something 119 */ 120 public function driver_installed() { 121 if (!extension_loaded('mysqli')) { 122 return get_string('mysqliextensionisnotpresentinphp', 'install'); 123 } 124 return true; 125 } 126 127 /** 128 * Returns database family type - describes SQL dialect 129 * Note: can be used before connect() 130 * @return string db family name (mysql, postgres, mssql, oracle, etc.) 131 */ 132 public function get_dbfamily() { 133 return 'mysql'; 134 } 135 136 /** 137 * Returns more specific database driver type 138 * Note: can be used before connect() 139 * @return string db type mysqli, pgsql, oci, mssql, sqlsrv 140 */ 141 protected function get_dbtype() { 142 return 'mysqli'; 143 } 144 145 /** 146 * Returns general database library name 147 * Note: can be used before connect() 148 * @return string db type pdo, native 149 */ 150 protected function get_dblibrary() { 151 return 'native'; 152 } 153 154 /** 155 * Returns the current MySQL db engine. 156 * 157 * This is an ugly workaround for MySQL default engine problems, 158 * Moodle is designed to work best on ACID compliant databases 159 * with full transaction support. Do not use MyISAM. 160 * 161 * @return string or null MySQL engine name 162 */ 163 public function get_dbengine() { 164 if (isset($this->dboptions['dbengine'])) { 165 return $this->dboptions['dbengine']; 166 } 167 168 if ($this->external) { 169 return null; 170 } 171 172 $engine = null; 173 174 // Look for current engine of our config table (the first table that gets created), 175 // so that we create all tables with the same engine. 176 $sql = "SELECT engine 177 FROM INFORMATION_SCHEMA.TABLES 178 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config'"; 179 $this->query_start($sql, null, SQL_QUERY_AUX); 180 $result = $this->mysqli->query($sql); 181 $this->query_end($result); 182 if ($rec = $result->fetch_assoc()) { 183 // MySQL 8 BC: information_schema.* returns the fields in upper case. 184 $rec = array_change_key_case($rec, CASE_LOWER); 185 $engine = $rec['engine']; 186 } 187 $result->close(); 188 189 if ($engine) { 190 // Cache the result to improve performance. 191 $this->dboptions['dbengine'] = $engine; 192 return $engine; 193 } 194 195 // Get the default database engine. 196 $sql = "SELECT @@default_storage_engine engine"; 197 $this->query_start($sql, null, SQL_QUERY_AUX); 198 $result = $this->mysqli->query($sql); 199 $this->query_end($result); 200 if ($rec = $result->fetch_assoc()) { 201 $engine = $rec['engine']; 202 } 203 $result->close(); 204 205 if ($engine === 'MyISAM') { 206 // we really do not want MyISAM for Moodle, InnoDB or XtraDB is a reasonable defaults if supported 207 $sql = "SHOW STORAGE ENGINES"; 208 $this->query_start($sql, null, SQL_QUERY_AUX); 209 $result = $this->mysqli->query($sql); 210 $this->query_end($result); 211 $engines = array(); 212 while ($res = $result->fetch_assoc()) { 213 if ($res['Support'] === 'YES' or $res['Support'] === 'DEFAULT') { 214 $engines[$res['Engine']] = true; 215 } 216 } 217 $result->close(); 218 if (isset($engines['InnoDB'])) { 219 $engine = 'InnoDB'; 220 } 221 if (isset($engines['XtraDB'])) { 222 $engine = 'XtraDB'; 223 } 224 } 225 226 // Cache the result to improve performance. 227 $this->dboptions['dbengine'] = $engine; 228 return $engine; 229 } 230 231 /** 232 * Returns the current MySQL db collation. 233 * 234 * This is an ugly workaround for MySQL default collation problems. 235 * 236 * @return string or null MySQL collation name 237 */ 238 public function get_dbcollation() { 239 if (isset($this->dboptions['dbcollation'])) { 240 return $this->dboptions['dbcollation']; 241 } 242 } 243 244 /** 245 * Set 'dbcollation' option 246 * 247 * @return string|null $dbcollation 248 */ 249 private function detect_collation(): ?string { 250 if ($this->external) { 251 return null; 252 } 253 254 $collation = null; 255 256 // Look for current collation of our config table (the first table that gets created), 257 // so that we create all tables with the same collation. 258 $sql = "SELECT collation_name 259 FROM INFORMATION_SCHEMA.COLUMNS 260 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config' AND column_name = 'value'"; 261 $result = $this->mysqli->query($sql); 262 if ($rec = $result->fetch_assoc()) { 263 // MySQL 8 BC: information_schema.* returns the fields in upper case. 264 $rec = array_change_key_case($rec, CASE_LOWER); 265 $collation = $rec['collation_name']; 266 } 267 $result->close(); 268 269 270 if (!$collation) { 271 // Get the default database collation, but only if using UTF-8. 272 $sql = "SELECT @@collation_database"; 273 $result = $this->mysqli->query($sql); 274 if ($rec = $result->fetch_assoc()) { 275 if (strpos($rec['@@collation_database'], 'utf8_') === 0 || strpos($rec['@@collation_database'], 'utf8mb4_') === 0) { 276 $collation = $rec['@@collation_database']; 277 } 278 } 279 $result->close(); 280 } 281 282 if (!$collation) { 283 // We want only utf8 compatible collations. 284 $collation = null; 285 $sql = "SHOW COLLATION WHERE Collation LIKE 'utf8mb4\_%' AND Charset = 'utf8mb4'"; 286 $result = $this->mysqli->query($sql); 287 while ($res = $result->fetch_assoc()) { 288 $collation = $res['Collation']; 289 if (strtoupper($res['Default']) === 'YES') { 290 $collation = $res['Collation']; 291 break; 292 } 293 } 294 $result->close(); 295 } 296 297 // Cache the result to improve performance. 298 $this->dboptions['dbcollation'] = $collation; 299 return $collation; 300 } 301 302 /** 303 * Tests if the Antelope file format is still supported or it has been removed. 304 * When removed, only Barracuda file format is supported, given the XtraDB/InnoDB engine. 305 * 306 * @return bool True if the Antelope file format has been removed; otherwise, false. 307 */ 308 protected function is_antelope_file_format_no_more_supported() { 309 // Breaking change: Antelope file format support has been removed from both MySQL and MariaDB. 310 // The following InnoDB file format configuration parameters were deprecated and then removed: 311 // - innodb_file_format 312 // - innodb_file_format_check 313 // - innodb_file_format_max 314 // - innodb_large_prefix 315 // 1. MySQL: deprecated in 5.7.7 and removed 8.0.0+. 316 $ismysqlge8d0d0 = ($this->get_dbtype() == 'mysqli' || $this->get_dbtype() == 'auroramysql') && 317 version_compare($this->get_server_info()['version'], '8.0.0', '>='); 318 // 2. MariaDB: deprecated in 10.2.0 and removed 10.3.1+. 319 $ismariadbge10d3d1 = ($this->get_dbtype() == 'mariadb') && 320 version_compare($this->get_server_info()['version'], '10.3.1', '>='); 321 322 return $ismysqlge8d0d0 || $ismariadbge10d3d1; 323 } 324 325 /** 326 * Get the row format from the database schema. 327 * 328 * @param string $table 329 * @return string row_format name or null if not known or table does not exist. 330 */ 331 public function get_row_format($table = null) { 332 $rowformat = null; 333 if (isset($table)) { 334 $table = $this->mysqli->real_escape_string($table); 335 $sql = "SELECT row_format 336 FROM INFORMATION_SCHEMA.TABLES 337 WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}$table'"; 338 } else { 339 if ($this->is_antelope_file_format_no_more_supported()) { 340 // Breaking change: Antelope file format support has been removed, only Barracuda. 341 $dbengine = $this->get_dbengine(); 342 $supporteddbengines = array('InnoDB', 'XtraDB'); 343 if (in_array($dbengine, $supporteddbengines)) { 344 $rowformat = 'Barracuda'; 345 } 346 347 return $rowformat; 348 } 349 350 $sql = "SHOW VARIABLES LIKE 'innodb_file_format'"; 351 } 352 $this->query_start($sql, null, SQL_QUERY_AUX); 353 $result = $this->mysqli->query($sql); 354 $this->query_end($result); 355 if ($rec = $result->fetch_assoc()) { 356 // MySQL 8 BC: information_schema.* returns the fields in upper case. 357 $rec = array_change_key_case($rec, CASE_LOWER); 358 if (isset($table)) { 359 $rowformat = $rec['row_format']; 360 } else { 361 $rowformat = $rec['value']; 362 } 363 } 364 $result->close(); 365 366 return $rowformat; 367 } 368 369 /** 370 * Is this database compatible with compressed row format? 371 * This feature is necessary for support of large number of text 372 * columns in InnoDB/XtraDB database. 373 * 374 * @param bool $cached use cached result 375 * @return bool true if table can be created or changed to compressed row format. 376 */ 377 public function is_compressed_row_format_supported($cached = true) { 378 if ($cached and isset($this->compressedrowformatsupported)) { 379 return($this->compressedrowformatsupported); 380 } 381 382 $engine = strtolower($this->get_dbengine()); 383 $info = $this->get_server_info(); 384 385 if (version_compare($info['version'], '5.5.0') < 0) { 386 // MySQL 5.1 is not supported here because we cannot read the file format. 387 $this->compressedrowformatsupported = false; 388 389 } else if ($engine !== 'innodb' and $engine !== 'xtradb') { 390 // Other engines are not supported, most probably not compatible. 391 $this->compressedrowformatsupported = false; 392 393 } else if (!$this->is_file_per_table_enabled()) { 394 $this->compressedrowformatsupported = false; 395 396 } else if ($this->get_row_format() !== 'Barracuda') { 397 $this->compressedrowformatsupported = false; 398 399 } else if ($this->get_dbtype() === 'auroramysql') { 400 // Aurora MySQL doesn't support COMPRESSED and falls back to COMPACT if you try to use it. 401 $this->compressedrowformatsupported = false; 402 403 } else { 404 // All the tests passed, we can safely use ROW_FORMAT=Compressed in sql statements. 405 $this->compressedrowformatsupported = true; 406 } 407 408 return $this->compressedrowformatsupported; 409 } 410 411 /** 412 * Check the database to see if innodb_file_per_table is on. 413 * 414 * @return bool True if on otherwise false. 415 */ 416 public function is_file_per_table_enabled() { 417 if ($filepertable = $this->get_record_sql("SHOW VARIABLES LIKE 'innodb_file_per_table'")) { 418 if ($filepertable->value == 'ON') { 419 return true; 420 } 421 } 422 return false; 423 } 424 425 /** 426 * Check the database to see if innodb_large_prefix is on. 427 * 428 * @return bool True if on otherwise false. 429 */ 430 public function is_large_prefix_enabled() { 431 if ($this->is_antelope_file_format_no_more_supported()) { 432 // Breaking change: Antelope file format support has been removed, only Barracuda. 433 return true; 434 } 435 436 if ($largeprefix = $this->get_record_sql("SHOW VARIABLES LIKE 'innodb_large_prefix'")) { 437 if ($largeprefix->value == 'ON') { 438 return true; 439 } 440 } 441 return false; 442 } 443 444 /** 445 * Determine if the row format should be set to compressed, dynamic, or default. 446 * 447 * Terrible kludge. If we're using utf8mb4 AND we're using InnoDB, we need to specify row format to 448 * be either dynamic or compressed (default is compact) in order to allow for bigger indexes (MySQL 449 * errors #1709 and #1071). 450 * 451 * @param string $engine The database engine being used. Will be looked up if not supplied. 452 * @param string $collation The database collation to use. Will look up the current collation if not supplied. 453 * @return string An sql fragment to add to sql statements. 454 */ 455 public function get_row_format_sql($engine = null, $collation = null) { 456 457 if (!isset($engine)) { 458 $engine = $this->get_dbengine(); 459 } 460 $engine = strtolower($engine); 461 462 if (!isset($collation)) { 463 $collation = $this->get_dbcollation(); 464 } 465 466 $rowformat = ''; 467 if (($engine === 'innodb' || $engine === 'xtradb') && strpos($collation, 'utf8mb4_') === 0) { 468 if ($this->is_compressed_row_format_supported()) { 469 $rowformat = "ROW_FORMAT=Compressed"; 470 } else { 471 $rowformat = "ROW_FORMAT=Dynamic"; 472 } 473 } 474 return $rowformat; 475 } 476 477 /** 478 * Returns localised database type name 479 * Note: can be used before connect() 480 * @return string 481 */ 482 public function get_name() { 483 return get_string('nativemysqli', 'install'); 484 } 485 486 /** 487 * Returns localised database configuration help. 488 * Note: can be used before connect() 489 * @return string 490 */ 491 public function get_configuration_help() { 492 return get_string('nativemysqlihelp', 'install'); 493 } 494 495 /** 496 * Diagnose database and tables, this function is used 497 * to verify database and driver settings, db engine types, etc. 498 * 499 * @return string null means everything ok, string means problem found. 500 */ 501 public function diagnose() { 502 $sloppymyisamfound = false; 503 $prefix = str_replace('_', '\\_', $this->prefix); 504 $sql = "SELECT COUNT('x') 505 FROM INFORMATION_SCHEMA.TABLES 506 WHERE table_schema = DATABASE() 507 AND table_name LIKE BINARY '$prefix%' 508 AND Engine = 'MyISAM'"; 509 $this->query_start($sql, null, SQL_QUERY_AUX); 510 $result = $this->mysqli->query($sql); 511 $this->query_end($result); 512 if ($result) { 513 if ($arr = $result->fetch_assoc()) { 514 $count = reset($arr); 515 if ($count) { 516 $sloppymyisamfound = true; 517 } 518 } 519 $result->close(); 520 } 521 522 if ($sloppymyisamfound) { 523 return get_string('myisamproblem', 'error'); 524 } else { 525 return null; 526 } 527 } 528 529 /** 530 * Connect to db 531 * @param string $dbhost The database host. 532 * @param string $dbuser The database username. 533 * @param string $dbpass The database username's password. 534 * @param string $dbname The name of the database being connected to.e 535 * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used 536 * @param array $dboptions driver specific options 537 * @return bool success 538 */ 539 public function raw_connect(string $dbhost, string $dbuser, string $dbpass, string $dbname, $prefix, array $dboptions=null): bool { 540 $driverstatus = $this->driver_installed(); 541 542 if ($driverstatus !== true) { 543 throw new dml_exception('dbdriverproblem', $driverstatus); 544 } 545 546 $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); 547 548 // The dbsocket option is used ONLY if host is null or 'localhost'. 549 // You can not disable it because it is always tried if dbhost is 'localhost'. 550 if (!empty($this->dboptions['dbsocket']) 551 and (strpos($this->dboptions['dbsocket'], '/') !== false or strpos($this->dboptions['dbsocket'], '\\') !== false)) { 552 $dbsocket = $this->dboptions['dbsocket']; 553 } else { 554 $dbsocket = ini_get('mysqli.default_socket'); 555 } 556 if (empty($this->dboptions['dbport'])) { 557 $dbport = (int)ini_get('mysqli.default_port'); 558 } else { 559 $dbport = (int)$this->dboptions['dbport']; 560 } 561 // verify ini.get does not return nonsense 562 if (empty($dbport)) { 563 $dbport = 3306; 564 } 565 if ($dbhost and !empty($this->dboptions['dbpersist'])) { 566 $dbhost = "p:$dbhost"; 567 } 568 569 // We want to keep exceptions out from the native driver. 570 // TODO: See MDL-75761 for future improvements. 571 mysqli_report(MYSQLI_REPORT_OFF); // Disable reporting (default before PHP 8.1). 572 573 $this->mysqli = mysqli_init(); 574 if (!empty($this->dboptions['connecttimeout'])) { 575 $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->dboptions['connecttimeout']); 576 } 577 578 $conn = null; 579 $dberr = null; 580 try { 581 // real_connect() is doing things we don't expext. 582 $conn = @$this->mysqli->real_connect($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket); 583 } catch (\Exception $e) { 584 $dberr = "$e"; 585 } 586 if (!$conn) { 587 $dberr = $dberr ?: $this->mysqli->connect_error; 588 $this->mysqli = null; 589 throw new dml_connection_exception($dberr); 590 } 591 592 // Disable logging until we are fully setup. 593 $this->query_log_prevent(); 594 595 if (isset($dboptions['dbcollation'])) { 596 $collation = $this->dboptions['dbcollation'] = $dboptions['dbcollation']; 597 } else { 598 $collation = $this->detect_collation(); 599 } 600 $collationinfo = explode('_', $collation); 601 $charset = reset($collationinfo); 602 603 $this->mysqli->set_charset($charset); 604 605 // If available, enforce strict mode for the session. That guaranties 606 // standard behaviour under some situations, avoiding some MySQL nasty 607 // habits like truncating data or performing some transparent cast losses. 608 // With strict mode enforced, Moodle DB layer will be consistently throwing 609 // the corresponding exceptions as expected. 610 $si = $this->get_server_info(); 611 if (version_compare($si['version'], '5.0.2', '>=')) { 612 $sql = "SET SESSION sql_mode = 'STRICT_ALL_TABLES'"; 613 $result = $this->mysqli->query($sql); 614 } 615 616 // We can enable logging now. 617 $this->query_log_allow(); 618 619 // Connection stabilised and configured, going to instantiate the temptables controller 620 $this->temptables = new mysqli_native_moodle_temptables($this); 621 622 return true; 623 } 624 625 /** 626 * Close database connection and release all resources 627 * and memory (especially circular memory references). 628 * Do NOT use connect() again, create a new instance if needed. 629 */ 630 public function dispose() { 631 parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection 632 if ($this->mysqli) { 633 $this->mysqli->close(); 634 $this->mysqli = null; 635 } 636 } 637 638 /** 639 * Gets db handle currently used with queries 640 * @return resource 641 */ 642 protected function get_db_handle() { 643 return $this->mysqli; 644 } 645 646 /** 647 * Sets db handle to be used with subsequent queries 648 * @param resource $dbh 649 * @return void 650 */ 651 protected function set_db_handle($dbh): void { 652 $this->mysqli = $dbh; 653 } 654 655 /** 656 * Check if The query qualifies for readonly connection execution 657 * Logging queries are exempt, those are write operations that circumvent 658 * standard query_start/query_end paths. 659 * @param int $type type of query 660 * @param string $sql 661 * @return bool 662 */ 663 protected function can_use_readonly(int $type, string $sql): bool { 664 // ... *_LOCK queries always go to master. 665 if (preg_match('/\b(GET|RELEASE)_LOCK/i', $sql)) { 666 return false; 667 } 668 669 return $this->read_slave_can_use_readonly($type, $sql); 670 } 671 672 /** 673 * Returns database server info array 674 * @return array Array containing 'description' and 'version' info 675 */ 676 public function get_server_info() { 677 return array('description'=>$this->mysqli->server_info, 'version'=>$this->mysqli->server_info); 678 } 679 680 /** 681 * Returns supported query parameter types 682 * @return int bitmask of accepted SQL_PARAMS_* 683 */ 684 protected function allowed_param_types() { 685 return SQL_PARAMS_QM; 686 } 687 688 /** 689 * Returns last error reported by database engine. 690 * @return string error message 691 */ 692 public function get_last_error() { 693 return $this->mysqli->error; 694 } 695 696 /** 697 * Return tables in database WITHOUT current prefix 698 * @param bool $usecache if true, returns list of cached tables. 699 * @return array of table names in lowercase and without prefix 700 */ 701 public function get_tables($usecache=true) { 702 if ($usecache and $this->tables !== null) { 703 return $this->tables; 704 } 705 $this->tables = array(); 706 $prefix = str_replace('_', '\\_', $this->prefix); 707 $sql = "SHOW TABLES LIKE '$prefix%'"; 708 $this->query_start($sql, null, $usecache ? SQL_QUERY_AUX_READONLY : SQL_QUERY_AUX); 709 $result = $this->mysqli->query($sql); 710 $this->query_end($result); 711 $len = strlen($this->prefix); 712 if ($result) { 713 while ($arr = $result->fetch_assoc()) { 714 $tablename = reset($arr); 715 $tablename = substr($tablename, $len); 716 $this->tables[$tablename] = $tablename; 717 } 718 $result->close(); 719 } 720 721 // Add the currently available temptables 722 $this->tables = array_merge($this->tables, $this->temptables->get_temptables()); 723 return $this->tables; 724 } 725 726 /** 727 * Return table indexes - everything lowercased. 728 * @param string $table The table we want to get indexes from. 729 * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed 730 */ 731 public function get_indexes($table) { 732 $indexes = array(); 733 $fixedtable = $this->fix_table_name($table); 734 $sql = "SHOW INDEXES FROM $fixedtable"; 735 $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); 736 $result = $this->mysqli->query($sql); 737 try { 738 $this->query_end($result); 739 } catch (dml_read_exception $e) { 740 return $indexes; // table does not exist - no indexes... 741 } 742 if ($result) { 743 while ($res = $result->fetch_object()) { 744 if ($res->Key_name === 'PRIMARY') { 745 continue; 746 } 747 if (!isset($indexes[$res->Key_name])) { 748 $indexes[$res->Key_name] = array('unique'=>empty($res->Non_unique), 'columns'=>array()); 749 } 750 $indexes[$res->Key_name]['columns'][$res->Seq_in_index-1] = $res->Column_name; 751 } 752 $result->close(); 753 } 754 return $indexes; 755 } 756 757 /** 758 * Fetches detailed information about columns in table. 759 * 760 * @param string $table name 761 * @return database_column_info[] array of database_column_info objects indexed with column names 762 */ 763 protected function fetch_columns(string $table): array { 764 $structure = array(); 765 766 $sql = "SELECT column_name, data_type, character_maximum_length, numeric_precision, 767 numeric_scale, is_nullable, column_type, column_default, column_key, extra 768 FROM information_schema.columns 769 WHERE table_name = '" . $this->prefix.$table . "' 770 AND table_schema = '" . $this->dbname . "' 771 ORDER BY ordinal_position"; 772 $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); 773 $result = $this->mysqli->query($sql); 774 $this->query_end(true); // Don't want to throw anything here ever. MDL-30147 775 776 if ($result === false) { 777 return array(); 778 } 779 780 if ($result->num_rows > 0) { 781 // standard table exists 782 while ($rawcolumn = $result->fetch_assoc()) { 783 // MySQL 8 BC: information_schema.* returns the fields in upper case. 784 $rawcolumn = array_change_key_case($rawcolumn, CASE_LOWER); 785 $info = (object)$this->get_column_info((object)$rawcolumn); 786 $structure[$info->name] = new database_column_info($info); 787 } 788 $result->close(); 789 790 } else { 791 // temporary tables are not in information schema, let's try it the old way 792 $result->close(); 793 $fixedtable = $this->fix_table_name($table); 794 $sql = "SHOW COLUMNS FROM $fixedtable"; 795 $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); 796 $result = $this->mysqli->query($sql); 797 $this->query_end(true); 798 if ($result === false) { 799 return array(); 800 } 801 while ($rawcolumn = $result->fetch_assoc()) { 802 $rawcolumn = (object)array_change_key_case($rawcolumn, CASE_LOWER); 803 $rawcolumn->column_name = $rawcolumn->field; unset($rawcolumn->field); 804 $rawcolumn->column_type = $rawcolumn->type; unset($rawcolumn->type); 805 $rawcolumn->character_maximum_length = null; 806 $rawcolumn->numeric_precision = null; 807 $rawcolumn->numeric_scale = null; 808 $rawcolumn->is_nullable = $rawcolumn->null; unset($rawcolumn->null); 809 $rawcolumn->column_default = $rawcolumn->default; unset($rawcolumn->default); 810 $rawcolumn->column_key = $rawcolumn->key; unset($rawcolumn->key); 811 812 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) { 813 $rawcolumn->data_type = $matches[1]; 814 $rawcolumn->character_maximum_length = $matches[2]; 815 816 } else if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) { 817 $rawcolumn->data_type = $matches[1]; 818 $rawcolumn->numeric_precision = $matches[2]; 819 $rawcolumn->max_length = $rawcolumn->numeric_precision; 820 821 $type = strtoupper($matches[1]); 822 if ($type === 'BIGINT') { 823 $maxlength = 18; 824 } else if ($type === 'INT' or $type === 'INTEGER') { 825 $maxlength = 9; 826 } else if ($type === 'MEDIUMINT') { 827 $maxlength = 6; 828 } else if ($type === 'SMALLINT') { 829 $maxlength = 4; 830 } else if ($type === 'TINYINT') { 831 $maxlength = 2; 832 } else { 833 // This should not happen. 834 $maxlength = 0; 835 } 836 if ($maxlength < $rawcolumn->max_length) { 837 $rawcolumn->max_length = $maxlength; 838 } 839 840 } else if (preg_match('/(decimal)\((\d+),(\d+)\)/i', $rawcolumn->column_type, $matches)) { 841 $rawcolumn->data_type = $matches[1]; 842 $rawcolumn->numeric_precision = $matches[2]; 843 $rawcolumn->numeric_scale = $matches[3]; 844 845 } else if (preg_match('/(double|float)(\((\d+),(\d+)\))?/i', $rawcolumn->column_type, $matches)) { 846 $rawcolumn->data_type = $matches[1]; 847 $rawcolumn->numeric_precision = isset($matches[3]) ? $matches[3] : null; 848 $rawcolumn->numeric_scale = isset($matches[4]) ? $matches[4] : null; 849 850 } else if (preg_match('/([a-z]*text)/i', $rawcolumn->column_type, $matches)) { 851 $rawcolumn->data_type = $matches[1]; 852 $rawcolumn->character_maximum_length = -1; // unknown 853 854 } else if (preg_match('/([a-z]*blob)/i', $rawcolumn->column_type, $matches)) { 855 $rawcolumn->data_type = $matches[1]; 856 857 } else { 858 $rawcolumn->data_type = $rawcolumn->column_type; 859 } 860 861 $info = $this->get_column_info($rawcolumn); 862 $structure[$info->name] = new database_column_info($info); 863 } 864 $result->close(); 865 } 866 867 return $structure; 868 } 869 870 /** 871 * Indicates whether column information retrieved from `information_schema.columns` has default values quoted or not. 872 * @return boolean True when default values are quoted (breaking change); otherwise, false. 873 */ 874 protected function has_breaking_change_quoted_defaults() { 875 return false; 876 } 877 878 /** 879 * Indicates whether SQL_MODE default value has changed in a not backward compatible way. 880 * @return boolean True when SQL_MODE breaks BC; otherwise, false. 881 */ 882 public function has_breaking_change_sqlmode() { 883 return false; 884 } 885 886 /** 887 * Returns moodle column info for raw column from information schema. 888 * @param stdClass $rawcolumn 889 * @return stdClass standardised colum info 890 */ 891 private function get_column_info(stdClass $rawcolumn) { 892 $rawcolumn = (object)$rawcolumn; 893 $info = new stdClass(); 894 $info->name = $rawcolumn->column_name; 895 $info->type = $rawcolumn->data_type; 896 $info->meta_type = $this->mysqltype2moodletype($rawcolumn->data_type); 897 if ($this->has_breaking_change_quoted_defaults()) { 898 $info->default_value = is_null($rawcolumn->column_default) ? null : trim($rawcolumn->column_default, "'"); 899 if ($info->default_value === 'NULL') { 900 $info->default_value = null; 901 } 902 } else { 903 $info->default_value = $rawcolumn->column_default; 904 } 905 $info->has_default = !is_null($info->default_value); 906 $info->not_null = ($rawcolumn->is_nullable === 'NO'); 907 $info->primary_key = ($rawcolumn->column_key === 'PRI'); 908 $info->binary = false; 909 $info->unsigned = null; 910 $info->auto_increment = false; 911 $info->unique = null; 912 $info->scale = null; 913 914 if ($info->meta_type === 'C') { 915 $info->max_length = $rawcolumn->character_maximum_length; 916 917 } else if ($info->meta_type === 'I') { 918 if ($info->primary_key) { 919 $info->meta_type = 'R'; 920 $info->unique = true; 921 } 922 // Return number of decimals, not bytes here. 923 $info->max_length = $rawcolumn->numeric_precision; 924 if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) { 925 $type = strtoupper($matches[1]); 926 if ($type === 'BIGINT') { 927 $maxlength = 18; 928 } else if ($type === 'INT' or $type === 'INTEGER') { 929 $maxlength = 9; 930 } else if ($type === 'MEDIUMINT') { 931 $maxlength = 6; 932 } else if ($type === 'SMALLINT') { 933 $maxlength = 4; 934 } else if ($type === 'TINYINT') { 935 $maxlength = 2; 936 } else { 937 // This should not happen. 938 $maxlength = 0; 939 } 940 // It is possible that display precision is different from storage type length, 941 // always use the smaller value to make sure our data fits. 942 if ($maxlength < $info->max_length) { 943 $info->max_length = $maxlength; 944 } 945 } 946 $info->unsigned = (stripos($rawcolumn->column_type, 'unsigned') !== false); 947 $info->auto_increment= (strpos($rawcolumn->extra, 'auto_increment') !== false); 948 949 } else if ($info->meta_type === 'N') { 950 $info->max_length = $rawcolumn->numeric_precision; 951 $info->scale = $rawcolumn->numeric_scale; 952 $info->unsigned = (stripos($rawcolumn->column_type, 'unsigned') !== false); 953 954 } else if ($info->meta_type === 'X') { 955 if ("$rawcolumn->character_maximum_length" === '4294967295') { // watch out for PHP max int limits! 956 // means maximum moodle size for text column, in other drivers it may also mean unknown size 957 $info->max_length = -1; 958 } else { 959 $info->max_length = $rawcolumn->character_maximum_length; 960 } 961 $info->primary_key = false; 962 963 } else if ($info->meta_type === 'B') { 964 $info->max_length = -1; 965 $info->primary_key = false; 966 $info->binary = true; 967 } 968 969 return $info; 970 } 971 972 /** 973 * Normalise column type. 974 * @param string $mysql_type 975 * @return string one character 976 * @throws dml_exception 977 */ 978 private function mysqltype2moodletype($mysql_type) { 979 $type = null; 980 981 switch(strtoupper($mysql_type)) { 982 case 'BIT': 983 $type = 'L'; 984 break; 985 986 case 'TINYINT': 987 case 'SMALLINT': 988 case 'MEDIUMINT': 989 case 'INT': 990 case 'INTEGER': 991 case 'BIGINT': 992 $type = 'I'; 993 break; 994 995 case 'FLOAT': 996 case 'DOUBLE': 997 case 'DECIMAL': 998 $type = 'N'; 999 break; 1000 1001 case 'CHAR': 1002 case 'ENUM': 1003 case 'SET': 1004 case 'VARCHAR': 1005 $type = 'C'; 1006 break; 1007 1008 case 'TINYTEXT': 1009 case 'TEXT': 1010 case 'MEDIUMTEXT': 1011 case 'LONGTEXT': 1012 $type = 'X'; 1013 break; 1014 1015 case 'BINARY': 1016 case 'VARBINARY': 1017 case 'BLOB': 1018 case 'TINYBLOB': 1019 case 'MEDIUMBLOB': 1020 case 'LONGBLOB': 1021 $type = 'B'; 1022 break; 1023 1024 case 'DATE': 1025 case 'TIME': 1026 case 'DATETIME': 1027 case 'TIMESTAMP': 1028 case 'YEAR': 1029 $type = 'D'; 1030 break; 1031 } 1032 1033 if (!$type) { 1034 throw new dml_exception('invalidmysqlnativetype', $mysql_type); 1035 } 1036 return $type; 1037 } 1038 1039 /** 1040 * Normalise values based in RDBMS dependencies (booleans, LOBs...) 1041 * 1042 * @param database_column_info $column column metadata corresponding with the value we are going to normalise 1043 * @param mixed $value value we are going to normalise 1044 * @return mixed the normalised value 1045 */ 1046 protected function normalise_value($column, $value) { 1047 $this->detect_objects($value); 1048 1049 if (is_bool($value)) { // Always, convert boolean to int 1050 $value = (int)$value; 1051 1052 } else if ($value === '') { 1053 if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') { 1054 $value = 0; // prevent '' problems in numeric fields 1055 } 1056 // Any float value being stored in varchar or text field is converted to string to avoid 1057 // any implicit conversion by MySQL 1058 } else if (is_float($value) and ($column->meta_type == 'C' or $column->meta_type == 'X')) { 1059 $value = "$value"; 1060 } 1061 return $value; 1062 } 1063 1064 /** 1065 * Is this database compatible with utf8? 1066 * @return bool 1067 */ 1068 public function setup_is_unicodedb() { 1069 // All new tables are created with this collation, we just have to make sure it is utf8 compatible, 1070 // if config table already exists it has this collation too. 1071 $collation = $this->get_dbcollation(); 1072 1073 $collationinfo = explode('_', $collation); 1074 $charset = reset($collationinfo); 1075 1076 $sql = "SHOW COLLATION WHERE Collation ='$collation' AND Charset = '$charset'"; 1077 $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); 1078 $result = $this->mysqli->query($sql); 1079 $this->query_end($result); 1080 if ($result->fetch_assoc()) { 1081 $return = true; 1082 } else { 1083 $return = false; 1084 } 1085 $result->close(); 1086 1087 return $return; 1088 } 1089 1090 /** 1091 * Do NOT use in code, to be used by database_manager only! 1092 * @param string|array $sql query 1093 * @param array|null $tablenames an array of xmldb table names affected by this request. 1094 * @return bool true 1095 * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors. 1096 */ 1097 public function change_database_structure($sql, $tablenames = null) { 1098 $this->get_manager(); // Includes DDL exceptions classes ;-) 1099 if (is_array($sql)) { 1100 $sql = implode("\n;\n", $sql); 1101 } 1102 1103 try { 1104 $this->query_start($sql, null, SQL_QUERY_STRUCTURE); 1105 $result = $this->mysqli->multi_query($sql); 1106 if ($result === false) { 1107 $this->query_end(false); 1108 } 1109 while ($this->mysqli->more_results()) { 1110 $result = $this->mysqli->next_result(); 1111 if ($result === false) { 1112 $this->query_end(false); 1113 } 1114 } 1115 $this->query_end(true); 1116 } catch (ddl_change_structure_exception $e) { 1117 while (@$this->mysqli->more_results()) { 1118 @$this->mysqli->next_result(); 1119 } 1120 $this->reset_caches($tablenames); 1121 throw $e; 1122 } 1123 1124 $this->reset_caches($tablenames); 1125 return true; 1126 } 1127 1128 /** 1129 * Very ugly hack which emulates bound parameters in queries 1130 * because prepared statements do not use query cache. 1131 */ 1132 protected function emulate_bound_params($sql, array $params=null) { 1133 if (empty($params)) { 1134 return $sql; 1135 } 1136 // ok, we have verified sql statement with ? and correct number of params 1137 $parts = array_reverse(explode('?', $sql)); 1138 $return = array_pop($parts); 1139 foreach ($params as $param) { 1140 if (is_bool($param)) { 1141 $return .= (int)$param; 1142 } else if (is_null($param)) { 1143 $return .= 'NULL'; 1144 } else if (is_number($param)) { 1145 $return .= "'".$param."'"; // we have to always use strings because mysql is using weird automatic int casting 1146 } else if (is_float($param)) { 1147 $return .= $param; 1148 } else { 1149 $param = $this->mysqli->real_escape_string($param); 1150 $return .= "'$param'"; 1151 } 1152 $return .= array_pop($parts); 1153 } 1154 return $return; 1155 } 1156 1157 /** 1158 * Execute general sql query. Should be used only when no other method suitable. 1159 * Do NOT use this to make changes in db structure, use database_manager methods instead! 1160 * @param string $sql query 1161 * @param array $params query parameters 1162 * @return bool true 1163 * @throws dml_exception A DML specific exception is thrown for any errors. 1164 */ 1165 public function execute($sql, array $params=null) { 1166 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 1167 1168 if (strpos($sql, ';') !== false) { 1169 throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!'); 1170 } 1171 1172 $rawsql = $this->emulate_bound_params($sql, $params); 1173 1174 $this->query_start($sql, $params, SQL_QUERY_UPDATE); 1175 $result = $this->mysqli->query($rawsql); 1176 $this->query_end($result); 1177 1178 if ($result === true) { 1179 return true; 1180 1181 } else { 1182 $result->close(); 1183 return true; 1184 } 1185 } 1186 1187 /** 1188 * Get a number of records as a moodle_recordset using a SQL statement. 1189 * 1190 * Since this method is a little less readable, use of it should be restricted to 1191 * code where it's possible there might be large datasets being returned. For known 1192 * small datasets use get_records_sql - it leads to simpler code. 1193 * 1194 * The return type is like: 1195 * @see function get_recordset. 1196 * 1197 * @param string $sql the SQL select query to execute. 1198 * @param array $params array of sql parameters 1199 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set). 1200 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set). 1201 * @return moodle_recordset instance 1202 * @throws dml_exception A DML specific exception is thrown for any errors. 1203 */ 1204 public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) { 1205 1206 list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum); 1207 1208 if ($limitfrom or $limitnum) { 1209 if ($limitnum < 1) { 1210 $limitnum = "18446744073709551615"; 1211 } 1212 $sql .= " LIMIT $limitfrom, $limitnum"; 1213 } 1214 1215 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 1216 $rawsql = $this->emulate_bound_params($sql, $params); 1217 1218 $this->query_start($sql, $params, SQL_QUERY_SELECT); 1219 // no MYSQLI_USE_RESULT here, it would block write ops on affected tables 1220 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT); 1221 $this->query_end($result); 1222 1223 return $this->create_recordset($result); 1224 } 1225 1226 /** 1227 * Get all records from a table. 1228 * 1229 * This method works around potential memory problems and may improve performance, 1230 * this method may block access to table until the recordset is closed. 1231 * 1232 * @param string $table Name of database table. 1233 * @return moodle_recordset A moodle_recordset instance {@link function get_recordset}. 1234 * @throws dml_exception A DML specific exception is thrown for any errors. 1235 */ 1236 public function export_table_recordset($table) { 1237 $sql = $this->fix_table_names("SELECT * FROM {{$table}}"); 1238 1239 $this->query_start($sql, array(), SQL_QUERY_SELECT); 1240 // MYSQLI_STORE_RESULT may eat all memory for large tables, unfortunately MYSQLI_USE_RESULT blocks other queries. 1241 $result = $this->mysqli->query($sql, MYSQLI_USE_RESULT); 1242 $this->query_end($result); 1243 1244 return $this->create_recordset($result); 1245 } 1246 1247 protected function create_recordset($result) { 1248 return new mysqli_native_moodle_recordset($result); 1249 } 1250 1251 /** 1252 * Get a number of records as an array of objects using a SQL statement. 1253 * 1254 * Return value is like: 1255 * @see function get_records. 1256 * 1257 * @param string $sql the SQL select query to execute. The first column of this SELECT statement 1258 * must be a unique value (usually the 'id' field), as it will be used as the key of the 1259 * returned array. 1260 * @param array $params array of sql parameters 1261 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set). 1262 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set). 1263 * @return array of objects, or empty array if no records were found 1264 * @throws dml_exception A DML specific exception is thrown for any errors. 1265 */ 1266 public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) { 1267 1268 list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum); 1269 1270 if ($limitfrom or $limitnum) { 1271 if ($limitnum < 1) { 1272 $limitnum = "18446744073709551615"; 1273 } 1274 $sql .= " LIMIT $limitfrom, $limitnum"; 1275 } 1276 1277 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 1278 $rawsql = $this->emulate_bound_params($sql, $params); 1279 1280 $this->query_start($sql, $params, SQL_QUERY_SELECT); 1281 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT); 1282 $this->query_end($result); 1283 1284 $return = array(); 1285 1286 while($row = $result->fetch_assoc()) { 1287 $row = array_change_key_case($row, CASE_LOWER); 1288 $id = reset($row); 1289 if (isset($return[$id])) { 1290 $colname = key($row); 1291 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); 1292 } 1293 $return[$id] = (object)$row; 1294 } 1295 $result->close(); 1296 1297 return $return; 1298 } 1299 1300 /** 1301 * Selects records and return values (first field) as an array using a SQL statement. 1302 * 1303 * @param string $sql The SQL query 1304 * @param array $params array of sql parameters 1305 * @return array of values 1306 * @throws dml_exception A DML specific exception is thrown for any errors. 1307 */ 1308 public function get_fieldset_sql($sql, array $params=null) { 1309 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 1310 $rawsql = $this->emulate_bound_params($sql, $params); 1311 1312 $this->query_start($sql, $params, SQL_QUERY_SELECT); 1313 $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT); 1314 $this->query_end($result); 1315 1316 $return = array(); 1317 1318 while($row = $result->fetch_assoc()) { 1319 $return[] = reset($row); 1320 } 1321 $result->close(); 1322 1323 return $return; 1324 } 1325 1326 /** 1327 * Insert new record into database, as fast as possible, no safety checks, lobs not supported. 1328 * @param string $table name 1329 * @param mixed $params data record as object or array 1330 * @param bool $returnit return it of inserted record 1331 * @param bool $bulk true means repeated inserts expected 1332 * @param bool $customsequence true if 'id' included in $params, disables $returnid 1333 * @return bool|int true or new id 1334 * @throws dml_exception A DML specific exception is thrown for any errors. 1335 */ 1336 public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) { 1337 if (!is_array($params)) { 1338 $params = (array)$params; 1339 } 1340 1341 if ($customsequence) { 1342 if (!isset($params['id'])) { 1343 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.'); 1344 } 1345 $returnid = false; 1346 } else { 1347 unset($params['id']); 1348 } 1349 1350 if (empty($params)) { 1351 throw new coding_exception('moodle_database::insert_record_raw() no fields found.'); 1352 } 1353 1354 $fields = implode(',', array_keys($params)); 1355 $qms = array_fill(0, count($params), '?'); 1356 $qms = implode(',', $qms); 1357 $fixedtable = $this->fix_table_name($table); 1358 $sql = "INSERT INTO $fixedtable ($fields) VALUES($qms)"; 1359 1360 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 1361 $rawsql = $this->emulate_bound_params($sql, $params); 1362 1363 $this->query_start($sql, $params, SQL_QUERY_INSERT); 1364 $result = $this->mysqli->query($rawsql); 1365 $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db 1366 $this->query_end($result); 1367 1368 if (!$customsequence and !$id) { 1369 throw new dml_write_exception('unknown error fetching inserted id'); 1370 } 1371 1372 if (!$returnid) { 1373 return true; 1374 } else { 1375 return (int)$id; 1376 } 1377 } 1378 1379 /** 1380 * Insert a record into a table and return the "id" field if required. 1381 * 1382 * Some conversions and safety checks are carried out. Lobs are supported. 1383 * If the return ID isn't required, then this just reports success as true/false. 1384 * $data is an object containing needed data 1385 * @param string $table The database table to be inserted into 1386 * @param object|array $dataobject A data object with values for one or more fields in the record 1387 * @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. 1388 * @return bool|int true or new id 1389 * @throws dml_exception A DML specific exception is thrown for any errors. 1390 */ 1391 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) { 1392 $dataobject = (array)$dataobject; 1393 1394 $columns = $this->get_columns($table); 1395 if (empty($columns)) { 1396 throw new dml_exception('ddltablenotexist', $table); 1397 } 1398 1399 $cleaned = array(); 1400 1401 foreach ($dataobject as $field=>$value) { 1402 if ($field === 'id') { 1403 continue; 1404 } 1405 if (!isset($columns[$field])) { 1406 continue; 1407 } 1408 $column = $columns[$field]; 1409 $cleaned[$field] = $this->normalise_value($column, $value); 1410 } 1411 1412 return $this->insert_record_raw($table, $cleaned, $returnid, $bulk); 1413 } 1414 1415 /** 1416 * Get chunk size for multiple records insert 1417 * @return int 1418 */ 1419 private function insert_chunk_size(): int { 1420 // MySQL has a relatively small query length limit by default, 1421 // make sure 'max_allowed_packet' in my.cnf is high enough 1422 // if you change the following default... 1423 static $chunksize = null; 1424 if ($chunksize === null) { 1425 if (!empty($this->dboptions['bulkinsertsize'])) { 1426 $chunksize = (int)$this->dboptions['bulkinsertsize']; 1427 1428 } else { 1429 if (PHP_INT_SIZE === 4) { 1430 // Bad luck for Windows, we cannot do any maths with large numbers. 1431 $chunksize = 5; 1432 } else { 1433 $sql = "SHOW VARIABLES LIKE 'max_allowed_packet'"; 1434 $this->query_start($sql, null, SQL_QUERY_AUX); 1435 $result = $this->mysqli->query($sql); 1436 $this->query_end($result); 1437 $size = 0; 1438 if ($rec = $result->fetch_assoc()) { 1439 $size = $rec['Value']; 1440 } 1441 $result->close(); 1442 // Hopefully 200kb per object are enough. 1443 $chunksize = (int)($size / 200000); 1444 if ($chunksize > 50) { 1445 $chunksize = 50; 1446 } 1447 } 1448 } 1449 } 1450 return $chunksize; 1451 } 1452 1453 /** 1454 * Insert multiple records into database as fast as possible. 1455 * 1456 * Order of inserts is maintained, but the operation is not atomic, 1457 * use transactions if necessary. 1458 * 1459 * This method is intended for inserting of large number of small objects, 1460 * do not use for huge objects with text or binary fields. 1461 * 1462 * @since Moodle 2.7 1463 * 1464 * @param string $table The database table to be inserted into 1465 * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach 1466 * @return void does not return new record ids 1467 * 1468 * @throws coding_exception if data objects have different structure 1469 * @throws dml_exception A DML specific exception is thrown for any errors. 1470 */ 1471 public function insert_records($table, $dataobjects) { 1472 if (!is_array($dataobjects) && !$dataobjects instanceof Traversable) { 1473 throw new coding_exception('insert_records() passed non-traversable object'); 1474 } 1475 1476 $chunksize = $this->insert_chunk_size(); 1477 $columns = $this->get_columns($table, true); 1478 $fields = null; 1479 $count = 0; 1480 $chunk = array(); 1481 foreach ($dataobjects as $dataobject) { 1482 if (!is_array($dataobject) and !is_object($dataobject)) { 1483 throw new coding_exception('insert_records() passed invalid record object'); 1484 } 1485 $dataobject = (array)$dataobject; 1486 if ($fields === null) { 1487 $fields = array_keys($dataobject); 1488 $columns = array_intersect_key($columns, $dataobject); 1489 unset($columns['id']); 1490 } else if ($fields !== array_keys($dataobject)) { 1491 throw new coding_exception('All dataobjects in insert_records() must have the same structure!'); 1492 } 1493 1494 $count++; 1495 $chunk[] = $dataobject; 1496 1497 if ($count === $chunksize) { 1498 $this->insert_chunk($table, $chunk, $columns); 1499 $chunk = array(); 1500 $count = 0; 1501 } 1502 } 1503 1504 if ($count) { 1505 $this->insert_chunk($table, $chunk, $columns); 1506 } 1507 } 1508 1509 /** 1510 * Insert records in chunks. 1511 * 1512 * Note: can be used only from insert_records(). 1513 * 1514 * @param string $table 1515 * @param array $chunk 1516 * @param database_column_info[] $columns 1517 */ 1518 protected function insert_chunk($table, array $chunk, array $columns) { 1519 $fieldssql = '('.implode(',', array_keys($columns)).')'; 1520 1521 $valuessql = '('.implode(',', array_fill(0, count($columns), '?')).')'; 1522 $valuessql = implode(',', array_fill(0, count($chunk), $valuessql)); 1523 1524 $params = array(); 1525 foreach ($chunk as $dataobject) { 1526 foreach ($columns as $field => $column) { 1527 $params[] = $this->normalise_value($column, $dataobject[$field]); 1528 } 1529 } 1530 1531 $fixedtable = $this->fix_table_name($table); 1532 $sql = "INSERT INTO $fixedtable $fieldssql VALUES $valuessql"; 1533 1534 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 1535 $rawsql = $this->emulate_bound_params($sql, $params); 1536 1537 $this->query_start($sql, $params, SQL_QUERY_INSERT); 1538 $result = $this->mysqli->query($rawsql); 1539 $this->query_end($result); 1540 } 1541 1542 /** 1543 * Import a record into a table, id field is required. 1544 * Safety checks are NOT carried out. Lobs are supported. 1545 * 1546 * @param string $table name of database table to be inserted into 1547 * @param object $dataobject A data object with values for one or more fields in the record 1548 * @return bool true 1549 * @throws dml_exception A DML specific exception is thrown for any errors. 1550 */ 1551 public function import_record($table, $dataobject) { 1552 $dataobject = (array)$dataobject; 1553 1554 $columns = $this->get_columns($table); 1555 $cleaned = array(); 1556 1557 foreach ($dataobject as $field=>$value) { 1558 if (!isset($columns[$field])) { 1559 continue; 1560 } 1561 $cleaned[$field] = $value; 1562 } 1563 1564 return $this->insert_record_raw($table, $cleaned, false, true, true); 1565 } 1566 1567 /** 1568 * Update record in database, as fast as possible, no safety checks, lobs not supported. 1569 * @param string $table name 1570 * @param mixed $params data record as object or array 1571 * @param bool true means repeated updates expected 1572 * @return bool true 1573 * @throws dml_exception A DML specific exception is thrown for any errors. 1574 */ 1575 public function update_record_raw($table, $params, $bulk=false) { 1576 $params = (array)$params; 1577 1578 if (!isset($params['id'])) { 1579 throw new coding_exception('moodle_database::update_record_raw() id field must be specified.'); 1580 } 1581 $id = $params['id']; 1582 unset($params['id']); 1583 1584 if (empty($params)) { 1585 throw new coding_exception('moodle_database::update_record_raw() no fields found.'); 1586 } 1587 1588 $sets = array(); 1589 foreach ($params as $field=>$value) { 1590 $sets[] = "$field = ?"; 1591 } 1592 1593 $params[] = $id; // last ? in WHERE condition 1594 1595 $sets = implode(',', $sets); 1596 $fixedtable = $this->fix_table_name($table); 1597 $sql = "UPDATE $fixedtable SET $sets WHERE id=?"; 1598 1599 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 1600 $rawsql = $this->emulate_bound_params($sql, $params); 1601 1602 $this->query_start($sql, $params, SQL_QUERY_UPDATE); 1603 $result = $this->mysqli->query($rawsql); 1604 $this->query_end($result); 1605 1606 return true; 1607 } 1608 1609 /** 1610 * Update a record in a table 1611 * 1612 * $dataobject is an object containing needed data 1613 * Relies on $dataobject having a variable "id" to 1614 * specify the record to update 1615 * 1616 * @param string $table The database table to be checked against. 1617 * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified. 1618 * @param bool true means repeated updates expected 1619 * @return bool true 1620 * @throws dml_exception A DML specific exception is thrown for any errors. 1621 */ 1622 public function update_record($table, $dataobject, $bulk=false) { 1623 $dataobject = (array)$dataobject; 1624 1625 $columns = $this->get_columns($table); 1626 $cleaned = array(); 1627 1628 foreach ($dataobject as $field=>$value) { 1629 if (!isset($columns[$field])) { 1630 continue; 1631 } 1632 $column = $columns[$field]; 1633 $cleaned[$field] = $this->normalise_value($column, $value); 1634 } 1635 1636 return $this->update_record_raw($table, $cleaned, $bulk); 1637 } 1638 1639 /** 1640 * Set a single field in every table record which match a particular WHERE clause. 1641 * 1642 * @param string $table The database table to be checked against. 1643 * @param string $newfield the field to set. 1644 * @param string $newvalue the value to set the field to. 1645 * @param string $select A fragment of SQL to be used in a where clause in the SQL call. 1646 * @param array $params array of sql parameters 1647 * @return bool true 1648 * @throws dml_exception A DML specific exception is thrown for any errors. 1649 */ 1650 public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) { 1651 if ($select) { 1652 $select = "WHERE $select"; 1653 } 1654 if (is_null($params)) { 1655 $params = array(); 1656 } 1657 list($select, $params, $type) = $this->fix_sql_params($select, $params); 1658 1659 // Get column metadata 1660 $columns = $this->get_columns($table); 1661 $column = $columns[$newfield]; 1662 1663 $normalised_value = $this->normalise_value($column, $newvalue); 1664 1665 if (is_null($normalised_value)) { 1666 $newfield = "$newfield = NULL"; 1667 } else { 1668 $newfield = "$newfield = ?"; 1669 array_unshift($params, $normalised_value); 1670 } 1671 $fixedtable = $this->fix_table_name($table); 1672 $sql = "UPDATE $fixedtable SET $newfield $select"; 1673 $rawsql = $this->emulate_bound_params($sql, $params); 1674 1675 $this->query_start($sql, $params, SQL_QUERY_UPDATE); 1676 $result = $this->mysqli->query($rawsql); 1677 $this->query_end($result); 1678 1679 return true; 1680 } 1681 1682 /** 1683 * Delete one or more records from a table which match a particular WHERE clause. 1684 * 1685 * @param string $table The database table to be checked against. 1686 * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria). 1687 * @param array $params array of sql parameters 1688 * @return bool true 1689 * @throws dml_exception A DML specific exception is thrown for any errors. 1690 */ 1691 public function delete_records_select($table, $select, array $params=null) { 1692 if ($select) { 1693 $select = "WHERE $select"; 1694 } 1695 $fixedtable = $this->fix_table_name($table); 1696 $sql = "DELETE FROM $fixedtable $select"; 1697 1698 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 1699 $rawsql = $this->emulate_bound_params($sql, $params); 1700 1701 $this->query_start($sql, $params, SQL_QUERY_UPDATE); 1702 $result = $this->mysqli->query($rawsql); 1703 $this->query_end($result); 1704 1705 return true; 1706 } 1707 1708 /** 1709 * Deletes records using a subquery, which is done with a strange DELETE...JOIN syntax in MySQL 1710 * because it performs very badly with normal subqueries. 1711 * 1712 * @param string $table Table to delete from 1713 * @param string $field Field in table to match 1714 * @param string $alias Name of single column in subquery e.g. 'id' 1715 * @param string $subquery Query that will return values of the field to delete 1716 * @param array $params Parameters for query 1717 * @throws dml_exception If there is any error 1718 */ 1719 public function delete_records_subquery(string $table, string $field, string $alias, string $subquery, array $params = []): void { 1720 // Aliases mysql_deltable and mysql_subquery are chosen to be unlikely to conflict. 1721 $this->execute("DELETE mysql_deltable FROM {" . $table . "} mysql_deltable JOIN " . 1722 "($subquery) mysql_subquery ON mysql_subquery.$alias = mysql_deltable.$field", $params); 1723 } 1724 1725 public function sql_cast_char2int($fieldname, $text=false) { 1726 return ' CAST(' . $fieldname . ' AS SIGNED) '; 1727 } 1728 1729 public function sql_cast_char2real($fieldname, $text=false) { 1730 // Set to 65 (max mysql 5.5 precision) with 7 as scale 1731 // because we must ensure at least 6 decimal positions 1732 // per casting given that postgres is casting to that scale (::real::). 1733 // Can be raised easily but that must be done in all DBs and tests. 1734 return ' CAST(' . $fieldname . ' AS DECIMAL(65,7)) '; 1735 } 1736 1737 public function sql_equal($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notequal = false) { 1738 $equalop = $notequal ? '<>' : '='; 1739 1740 $collationinfo = explode('_', $this->get_dbcollation()); 1741 $bincollate = reset($collationinfo) . '_bin'; 1742 1743 if ($casesensitive) { 1744 // Current MySQL versions do not support case sensitive and accent insensitive. 1745 return "$fieldname COLLATE $bincollate $equalop $param"; 1746 } else if ($accentsensitive) { 1747 // Case insensitive and accent sensitive, we can force a binary comparison once all texts are using the same case. 1748 return "LOWER($fieldname) COLLATE $bincollate $equalop LOWER($param)"; 1749 } else { 1750 // Case insensitive and accent insensitive. All collations are that way, but utf8_bin. 1751 $collation = ''; 1752 if ($this->get_dbcollation() == 'utf8_bin') { 1753 $collation = 'COLLATE utf8_unicode_ci'; 1754 } else if ($this->get_dbcollation() == 'utf8mb4_bin') { 1755 $collation = 'COLLATE utf8mb4_unicode_ci'; 1756 } 1757 return "$fieldname $collation $equalop $param"; 1758 } 1759 } 1760 1761 /** 1762 * Returns 'LIKE' part of a query. 1763 * 1764 * Note that mysql does not support $casesensitive = true and $accentsensitive = false. 1765 * More information in http://bugs.mysql.com/bug.php?id=19567. 1766 * 1767 * @param string $fieldname usually name of the table column 1768 * @param string $param usually bound query parameter (?, :named) 1769 * @param bool $casesensitive use case sensitive search 1770 * @param bool $accensensitive use accent sensitive search (ignored if $casesensitive is true) 1771 * @param bool $notlike true means "NOT LIKE" 1772 * @param string $escapechar escape char for '%' and '_' 1773 * @return string SQL code fragment 1774 */ 1775 public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') { 1776 if (strpos($param, '%') !== false) { 1777 debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)'); 1778 } 1779 $escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\' 1780 1781 $collationinfo = explode('_', $this->get_dbcollation()); 1782 $bincollate = reset($collationinfo) . '_bin'; 1783 1784 $LIKE = $notlike ? 'NOT LIKE' : 'LIKE'; 1785 1786 if ($casesensitive) { 1787 // Current MySQL versions do not support case sensitive and accent insensitive. 1788 return "$fieldname $LIKE $param COLLATE $bincollate ESCAPE '$escapechar'"; 1789 1790 } else if ($accentsensitive) { 1791 // Case insensitive and accent sensitive, we can force a binary comparison once all texts are using the same case. 1792 return "LOWER($fieldname) $LIKE LOWER($param) COLLATE $bincollate ESCAPE '$escapechar'"; 1793 1794 } else { 1795 // Case insensitive and accent insensitive. 1796 $collation = ''; 1797 if ($this->get_dbcollation() == 'utf8_bin') { 1798 // Force a case insensitive comparison if using utf8_bin. 1799 $collation = 'COLLATE utf8_unicode_ci'; 1800 } else if ($this->get_dbcollation() == 'utf8mb4_bin') { 1801 // Force a case insensitive comparison if using utf8mb4_bin. 1802 $collation = 'COLLATE utf8mb4_unicode_ci'; 1803 } 1804 1805 return "$fieldname $LIKE $param $collation ESCAPE '$escapechar'"; 1806 } 1807 } 1808 1809 /** 1810 * Returns the proper SQL to do CONCAT between the elements passed 1811 * Can take many parameters 1812 * 1813 * @param string $str,... 1 or more fields/strings to concat 1814 * 1815 * @return string The concat sql 1816 */ 1817 public function sql_concat() { 1818 $arr = func_get_args(); 1819 $s = implode(', ', $arr); 1820 if ($s === '') { 1821 return "''"; 1822 } 1823 return "CONCAT($s)"; 1824 } 1825 1826 /** 1827 * Returns the proper SQL to do CONCAT between the elements passed 1828 * with a given separator 1829 * 1830 * @param string $separator The string to use as the separator 1831 * @param array $elements An array of items to concatenate 1832 * @return string The concat SQL 1833 */ 1834 public function sql_concat_join($separator="' '", $elements=array()) { 1835 $s = implode(', ', $elements); 1836 1837 if ($s === '') { 1838 return "''"; 1839 } 1840 return "CONCAT_WS($separator, $s)"; 1841 } 1842 1843 /** 1844 * Return SQL for performing group concatenation on given field/expression 1845 * 1846 * @param string $field 1847 * @param string $separator 1848 * @param string $sort 1849 * @return string 1850 */ 1851 public function sql_group_concat(string $field, string $separator = ', ', string $sort = ''): string { 1852 $fieldsort = $sort ? "ORDER BY {$sort}" : ''; 1853 return "GROUP_CONCAT({$field} {$fieldsort} SEPARATOR '{$separator}')"; 1854 } 1855 1856 /** 1857 * Returns the SQL text to be used to calculate the length in characters of one expression. 1858 * @param string fieldname or expression to calculate its length in characters. 1859 * @return string the piece of SQL code to be used in the statement. 1860 */ 1861 public function sql_length($fieldname) { 1862 return ' CHAR_LENGTH(' . $fieldname . ')'; 1863 } 1864 1865 /** 1866 * Does this driver support regex syntax when searching 1867 */ 1868 public function sql_regex_supported() { 1869 return true; 1870 } 1871 1872 /** 1873 * Return regex positive or negative match sql 1874 * @param bool $positivematch 1875 * @param bool $casesensitive 1876 * @return string or empty if not supported 1877 */ 1878 public function sql_regex($positivematch = true, $casesensitive = false) { 1879 $collation = ''; 1880 if ($casesensitive) { 1881 if (substr($this->get_dbcollation(), -4) !== '_bin') { 1882 $collationinfo = explode('_', $this->get_dbcollation()); 1883 $collation = 'COLLATE ' . $collationinfo[0] . '_bin '; 1884 } 1885 } else { 1886 if ($this->get_dbcollation() == 'utf8_bin') { 1887 $collation = 'COLLATE utf8_unicode_ci '; 1888 } else if ($this->get_dbcollation() == 'utf8mb4_bin') { 1889 $collation = 'COLLATE utf8mb4_unicode_ci '; 1890 } 1891 } 1892 1893 return $collation . ($positivematch ? 'REGEXP' : 'NOT REGEXP'); 1894 } 1895 1896 /** 1897 * Returns the word-beginning boundary marker based on MySQL version. 1898 * @return string The word-beginning boundary marker. 1899 */ 1900 public function sql_regex_get_word_beginning_boundary_marker() { 1901 $ismysql = ($this->get_dbtype() == 'mysqli' || $this->get_dbtype() == 'auroramysql'); 1902 $ismysqlge8d0d4 = ($ismysql && version_compare($this->get_server_info()['version'], '8.0.4', '>=')); 1903 if ($ismysqlge8d0d4) { 1904 return '\\b'; 1905 } 1906 // Prior to MySQL 8.0.4, MySQL used the Henry Spencer regular expression library to support regular expression operations, 1907 // rather than International Components for Unicode (ICU). 1908 // MariaDB still supports the "old marker" (MDEV-5357). 1909 return '[[:<:]]'; 1910 } 1911 1912 /** 1913 * Returns the word-end boundary marker based on MySQL version. 1914 * @return string The word-end boundary marker. 1915 */ 1916 public function sql_regex_get_word_end_boundary_marker() { 1917 $ismysql = ($this->get_dbtype() == 'mysqli' || $this->get_dbtype() == 'auroramysql'); 1918 $ismysqlge8d0d4 = ($ismysql && version_compare($this->get_server_info()['version'], '8.0.4', '>=')); 1919 if ($ismysqlge8d0d4) { 1920 return '\\b'; 1921 } 1922 // Prior to MySQL 8.0.4, MySQL used the Henry Spencer regular expression library to support regular expression operations, 1923 // rather than International Components for Unicode (ICU). 1924 // MariaDB still supports the "old marker" (MDEV-5357). 1925 return '[[:>:]]'; 1926 } 1927 1928 /** 1929 * Returns the SQL to be used in order to an UNSIGNED INTEGER column to SIGNED. 1930 * 1931 * @deprecated since 2.3 1932 * @param string $fieldname The name of the field to be cast 1933 * @return string The piece of SQL code to be used in your statement. 1934 */ 1935 public function sql_cast_2signed($fieldname) { 1936 return ' CAST(' . $fieldname . ' AS SIGNED) '; 1937 } 1938 1939 /** 1940 * Returns the SQL that allows to find intersection of two or more queries 1941 * 1942 * @since Moodle 2.8 1943 * 1944 * @param array $selects array of SQL select queries, each of them only returns fields with the names from $fields 1945 * @param string $fields comma-separated list of fields 1946 * @return string SQL query that will return only values that are present in each of selects 1947 */ 1948 public function sql_intersect($selects, $fields) { 1949 if (count($selects) <= 1) { 1950 return parent::sql_intersect($selects, $fields); 1951 } 1952 $fields = preg_replace('/\s/', '', $fields); 1953 static $aliascnt = 0; 1954 $falias = 'intsctal'.($aliascnt++); 1955 $rv = "SELECT $falias.". 1956 preg_replace('/,/', ','.$falias.'.', $fields). 1957 " FROM ($selects[0]) $falias"; 1958 for ($i = 1; $i < count($selects); $i++) { 1959 $alias = 'intsctal'.($aliascnt++); 1960 $rv .= " JOIN (".$selects[$i].") $alias ON ". 1961 join(' AND ', 1962 array_map( 1963 function($a) use ($alias, $falias) { 1964 return $falias . '.' . $a .' = ' . $alias . '.' . $a; 1965 }, 1966 preg_split('/,/', $fields)) 1967 ); 1968 } 1969 return $rv; 1970 } 1971 1972 /** 1973 * Does this driver support tool_replace? 1974 * 1975 * @since Moodle 2.6.1 1976 * @return bool 1977 */ 1978 public function replace_all_text_supported() { 1979 return true; 1980 } 1981 1982 public function session_lock_supported() { 1983 return true; 1984 } 1985 1986 /** 1987 * Obtain session lock 1988 * @param int $rowid id of the row with session record 1989 * @param int $timeout max allowed time to wait for the lock in seconds 1990 * @return void 1991 */ 1992 public function get_session_lock($rowid, $timeout) { 1993 parent::get_session_lock($rowid, $timeout); 1994 1995 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid; 1996 $sql = "SELECT GET_LOCK('$fullname', $timeout)"; 1997 $this->query_start($sql, null, SQL_QUERY_AUX); 1998 $result = $this->mysqli->query($sql); 1999 $this->query_end($result); 2000 2001 if ($result) { 2002 $arr = $result->fetch_assoc(); 2003 $result->close(); 2004 2005 if (reset($arr) == 1) { 2006 return; 2007 } else { 2008 throw new dml_sessionwait_exception(); 2009 } 2010 } 2011 } 2012 2013 public function release_session_lock($rowid) { 2014 if (!$this->used_for_db_sessions) { 2015 return; 2016 } 2017 2018 parent::release_session_lock($rowid); 2019 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid; 2020 $sql = "SELECT RELEASE_LOCK('$fullname')"; 2021 $this->query_start($sql, null, SQL_QUERY_AUX); 2022 $result = $this->mysqli->query($sql); 2023 $this->query_end($result); 2024 2025 if ($result) { 2026 $result->close(); 2027 } 2028 } 2029 2030 /** 2031 * Are transactions supported? 2032 * It is not responsible to run productions servers 2033 * on databases without transaction support ;-) 2034 * 2035 * MyISAM does not support support transactions. 2036 * 2037 * You can override this via the dbtransactions option. 2038 * 2039 * @return bool 2040 */ 2041 protected function transactions_supported() { 2042 if (!is_null($this->transactions_supported)) { 2043 return $this->transactions_supported; 2044 } 2045 2046 // this is all just guessing, might be better to just specify it in config.php 2047 if (isset($this->dboptions['dbtransactions'])) { 2048 $this->transactions_supported = $this->dboptions['dbtransactions']; 2049 return $this->transactions_supported; 2050 } 2051 2052 $this->transactions_supported = false; 2053 2054 $engine = $this->get_dbengine(); 2055 2056 // Only will accept transactions if using compatible storage engine (more engines can be added easily BDB, Falcon...) 2057 if (in_array($engine, array('InnoDB', 'INNOBASE', 'BDB', 'XtraDB', 'Aria', 'Falcon'))) { 2058 $this->transactions_supported = true; 2059 } 2060 2061 return $this->transactions_supported; 2062 } 2063 2064 /** 2065 * Driver specific start of real database transaction, 2066 * this can not be used directly in code. 2067 * @return void 2068 */ 2069 protected function begin_transaction() { 2070 if (!$this->transactions_supported()) { 2071 return; 2072 } 2073 2074 $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"; 2075 $this->query_start($sql, null, SQL_QUERY_AUX); 2076 $result = $this->mysqli->query($sql); 2077 $this->query_end($result); 2078 2079 $sql = "START TRANSACTION"; 2080 $this->query_start($sql, null, SQL_QUERY_AUX); 2081 $result = $this->mysqli->query($sql); 2082 $this->query_end($result); 2083 } 2084 2085 /** 2086 * Driver specific commit of real database transaction, 2087 * this can not be used directly in code. 2088 * @return void 2089 */ 2090 protected function commit_transaction() { 2091 if (!$this->transactions_supported()) { 2092 return; 2093 } 2094 2095 $sql = "COMMIT"; 2096 $this->query_start($sql, null, SQL_QUERY_AUX); 2097 $result = $this->mysqli->query($sql); 2098 $this->query_end($result); 2099 } 2100 2101 /** 2102 * Driver specific abort of real database transaction, 2103 * this can not be used directly in code. 2104 * @return void 2105 */ 2106 protected function rollback_transaction() { 2107 if (!$this->transactions_supported()) { 2108 return; 2109 } 2110 2111 $sql = "ROLLBACK"; 2112 $this->query_start($sql, null, SQL_QUERY_AUX); 2113 $result = $this->mysqli->query($sql); 2114 $this->query_end($result); 2115 2116 return true; 2117 } 2118 2119 /** 2120 * Converts a table to either 'Compressed' or 'Dynamic' row format. 2121 * 2122 * @param string $tablename Name of the table to convert to the new row format. 2123 */ 2124 public function convert_table_row_format($tablename) { 2125 $currentrowformat = $this->get_row_format($tablename); 2126 if ($currentrowformat == 'Compact' || $currentrowformat == 'Redundant') { 2127 $rowformat = ($this->is_compressed_row_format_supported(false)) ? "ROW_FORMAT=Compressed" : "ROW_FORMAT=Dynamic"; 2128 $prefix = $this->get_prefix(); 2129 $this->change_database_structure("ALTER TABLE {$prefix}$tablename $rowformat"); 2130 } 2131 } 2132 2133 /** 2134 * Does this mysql instance support fulltext indexes? 2135 * 2136 * @return bool 2137 */ 2138 public function is_fulltext_search_supported() { 2139 $info = $this->get_server_info(); 2140 2141 if (version_compare($info['version'], '5.6.4', '>=')) { 2142 return true; 2143 } 2144 return false; 2145 } 2146 2147 /** 2148 * Fixes any table names that clash with reserved words. 2149 * 2150 * @param string $tablename The table name 2151 * @return string The fixed table name 2152 */ 2153 protected function fix_table_name($tablename) { 2154 $prefixedtablename = parent::fix_table_name($tablename); 2155 // This function quotes the table name if it matches one of the MySQL reserved 2156 // words, e.g. groups. 2157 return $this->get_manager()->generator->getEncQuoted($prefixedtablename); 2158 } 2159 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body