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