[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/dml/ -> mysqli_native_moodle_database.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1