Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * DML layer tests.
  19   *
  20   * @package    core
  21   * @category   test
  22   * @copyright  2008 Nicolas Connault
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core;
  27  
  28  use dml_exception;
  29  use dml_missing_record_exception;
  30  use dml_multiple_records_exception;
  31  use moodle_database;
  32  use moodle_transaction;
  33  use xmldb_key;
  34  use xmldb_table;
  35  
  36  defined('MOODLE_INTERNAL') || die();
  37  
  38  /**
  39   * DML layer tests.
  40   *
  41   * @package    core
  42   * @category   test
  43   * @copyright  2008 Nicolas Connault
  44   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   * @coversDefaultClass \moodle_database
  46   */
  47  class dml_test extends \database_driver_testcase {
  48  
  49      protected function setUp(): void {
  50          parent::setUp();
  51          $dbman = $this->tdb->get_manager(); // Loads DDL libs.
  52      }
  53  
  54      /**
  55       * Get a xmldb_table object for testing, deleting any existing table
  56       * of the same name, for example if one was left over from a previous test
  57       * run that crashed.
  58       *
  59       * @param string $suffix table name suffix, use if you need more test tables
  60       * @return xmldb_table the table object.
  61       */
  62      private function get_test_table($suffix = '') {
  63          $tablename = "test_table";
  64          if ($suffix !== '') {
  65              $tablename .= $suffix;
  66          }
  67  
  68          $table = new xmldb_table($tablename);
  69          $table->setComment("This is a test'n drop table. You can drop it safely");
  70          return $table;
  71      }
  72  
  73      /**
  74       * Convert a unix string to a OS (dir separator) dependent string.
  75       *
  76       * @param string $source the original srting, using unix dir separators and newlines.
  77       * @return string the resulting string, using current OS dir separators newlines.
  78       */
  79      private function unix_to_os_dirsep(string $source): string {
  80          if (DIRECTORY_SEPARATOR !== '/') {
  81              return str_replace('/', DIRECTORY_SEPARATOR, $source);
  82          }
  83          return $source; // No changes, so far.
  84      }
  85  
  86      public function test_diagnose() {
  87          $DB = $this->tdb;
  88          $result = $DB->diagnose();
  89          $this->assertNull($result, 'Database self diagnostics failed %s');
  90      }
  91  
  92      public function test_get_server_info() {
  93          $DB = $this->tdb;
  94          $result = $DB->get_server_info();
  95          $this->assertIsArray($result);
  96          $this->assertArrayHasKey('description', $result);
  97          $this->assertArrayHasKey('version', $result);
  98      }
  99  
 100      public function test_get_in_or_equal() {
 101          $DB = $this->tdb;
 102  
 103          // SQL_PARAMS_QM - IN or =.
 104  
 105          // Correct usage of multiple values.
 106          $in_values = array('value1', 'value2', '3', 4, null, false, true);
 107          list($usql, $params) = $DB->get_in_or_equal($in_values);
 108          $this->assertSame('IN ('.implode(',', array_fill(0, count($in_values), '?')).')', $usql);
 109          $this->assertEquals(count($in_values), count($params));
 110          foreach ($params as $key => $value) {
 111              $this->assertSame($in_values[$key], $value);
 112          }
 113  
 114          // Correct usage of single value (in an array).
 115          $in_values = array('value1');
 116          list($usql, $params) = $DB->get_in_or_equal($in_values);
 117          $this->assertEquals("= ?", $usql);
 118          $this->assertCount(1, $params);
 119          $this->assertEquals($in_values[0], $params[0]);
 120  
 121          // Correct usage of single value.
 122          $in_value = 'value1';
 123          list($usql, $params) = $DB->get_in_or_equal($in_values);
 124          $this->assertEquals("= ?", $usql);
 125          $this->assertCount(1, $params);
 126          $this->assertEquals($in_value, $params[0]);
 127  
 128          // SQL_PARAMS_QM - NOT IN or <>.
 129  
 130          // Correct usage of multiple values.
 131          $in_values = array('value1', 'value2', 'value3', 'value4');
 132          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_QM, null, false);
 133          $this->assertEquals("NOT IN (?,?,?,?)", $usql);
 134          $this->assertCount(4, $params);
 135          foreach ($params as $key => $value) {
 136              $this->assertEquals($in_values[$key], $value);
 137          }
 138  
 139          // Correct usage of single value (in array().
 140          $in_values = array('value1');
 141          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_QM, null, false);
 142          $this->assertEquals("<> ?", $usql);
 143          $this->assertCount(1, $params);
 144          $this->assertEquals($in_values[0], $params[0]);
 145  
 146          // Correct usage of single value.
 147          $in_value = 'value1';
 148          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_QM, null, false);
 149          $this->assertEquals("<> ?", $usql);
 150          $this->assertCount(1, $params);
 151          $this->assertEquals($in_value, $params[0]);
 152  
 153          // SQL_PARAMS_NAMED - IN or =.
 154  
 155          // Correct usage of multiple values.
 156          $in_values = array('value1', 'value2', 'value3', 'value4');
 157          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', true);
 158          $this->assertCount(4, $params);
 159          reset($in_values);
 160          $ps = array();
 161          foreach ($params as $key => $value) {
 162              $this->assertEquals(current($in_values), $value);
 163              next($in_values);
 164              $ps[] = ':'.$key;
 165          }
 166          $this->assertEquals("IN (".implode(',', $ps).")", $usql);
 167  
 168          // Correct usage of single values (in array).
 169          $in_values = array('value1');
 170          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', true);
 171          $this->assertCount(1, $params);
 172          $value = reset($params);
 173          $key = key($params);
 174          $this->assertEquals("= :$key", $usql);
 175          $this->assertEquals($in_value, $value);
 176  
 177          // Correct usage of single value.
 178          $in_value = 'value1';
 179          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', true);
 180          $this->assertCount(1, $params);
 181          $value = reset($params);
 182          $key = key($params);
 183          $this->assertEquals("= :$key", $usql);
 184          $this->assertEquals($in_value, $value);
 185  
 186          // SQL_PARAMS_NAMED - NOT IN or <>.
 187  
 188          // Correct usage of multiple values.
 189          $in_values = array('value1', 'value2', 'value3', 'value4');
 190          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', false);
 191          $this->assertCount(4, $params);
 192          reset($in_values);
 193          $ps = array();
 194          foreach ($params as $key => $value) {
 195              $this->assertEquals(current($in_values), $value);
 196              next($in_values);
 197              $ps[] = ':'.$key;
 198          }
 199          $this->assertEquals("NOT IN (".implode(',', $ps).")", $usql);
 200  
 201          // Correct usage of single values (in array).
 202          $in_values = array('value1');
 203          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', false);
 204          $this->assertCount(1, $params);
 205          $value = reset($params);
 206          $key = key($params);
 207          $this->assertEquals("<> :$key", $usql);
 208          $this->assertEquals($in_value, $value);
 209  
 210          // Correct usage of single value.
 211          $in_value = 'value1';
 212          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', false);
 213          $this->assertCount(1, $params);
 214          $value = reset($params);
 215          $key = key($params);
 216          $this->assertEquals("<> :$key", $usql);
 217          $this->assertEquals($in_value, $value);
 218  
 219          // Make sure the param names are unique.
 220          list($usql1, $params1) = $DB->get_in_or_equal(array(1, 2, 3), SQL_PARAMS_NAMED, 'param');
 221          list($usql2, $params2) = $DB->get_in_or_equal(array(1, 2, 3), SQL_PARAMS_NAMED, 'param');
 222          $params1 = array_keys($params1);
 223          $params2 = array_keys($params2);
 224          $common = array_intersect($params1, $params2);
 225          $this->assertCount(0, $common);
 226  
 227          // Some incorrect tests.
 228  
 229          // Incorrect usage passing not-allowed params type.
 230          $in_values = array(1, 2, 3);
 231          try {
 232              list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_DOLLAR, 'param', false);
 233              $this->fail('An Exception is missing, expected due to not supported SQL_PARAMS_DOLLAR');
 234          } catch (\moodle_exception $e) {
 235              $this->assertInstanceOf('dml_exception', $e);
 236              $this->assertSame('typenotimplement', $e->errorcode);
 237          }
 238  
 239          // Incorrect usage passing empty array.
 240          $in_values = array();
 241          try {
 242              list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', false);
 243              $this->fail('An Exception is missing, expected due to empty array of items');
 244          } catch (\moodle_exception $e) {
 245              $this->assertInstanceOf('coding_exception', $e);
 246          }
 247  
 248          // Test using $onemptyitems.
 249  
 250          // Correct usage passing empty array and $onemptyitems = null (equal = true, QM).
 251          $in_values = array();
 252          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_QM, 'param', true, null);
 253          $this->assertSame(' IS NULL', $usql);
 254          $this->assertSame(array(), $params);
 255  
 256          // Correct usage passing empty array and $onemptyitems = null (equal = false, NAMED).
 257          $in_values = array();
 258          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', false, null);
 259          $this->assertSame(' IS NOT NULL', $usql);
 260          $this->assertSame(array(), $params);
 261  
 262          // Correct usage passing empty array and $onemptyitems = true (equal = true, QM).
 263          $in_values = array();
 264          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_QM, 'param', true, true);
 265          $this->assertSame('= ?', $usql);
 266          $this->assertSame(array(true), $params);
 267  
 268          // Correct usage passing empty array and $onemptyitems = true (equal = false, NAMED).
 269          $in_values = array();
 270          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', false, true);
 271          $this->assertCount(1, $params);
 272          $value = reset($params);
 273          $key = key($params);
 274          $this->assertSame('<> :'.$key, $usql);
 275          $this->assertSame($value, true);
 276  
 277          // Correct usage passing empty array and $onemptyitems = -1 (equal = true, QM).
 278          $in_values = array();
 279          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_QM, 'param', true, -1);
 280          $this->assertSame('= ?', $usql);
 281          $this->assertSame(array(-1), $params);
 282  
 283          // Correct usage passing empty array and $onemptyitems = -1 (equal = false, NAMED).
 284          $in_values = array();
 285          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', false, -1);
 286          $this->assertCount(1, $params);
 287          $value = reset($params);
 288          $key = key($params);
 289          $this->assertSame('<> :'.$key, $usql);
 290          $this->assertSame($value, -1);
 291  
 292          // Correct usage passing empty array and $onemptyitems = 'onevalue' (equal = true, QM).
 293          $in_values = array();
 294          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_QM, 'param', true, 'onevalue');
 295          $this->assertSame('= ?', $usql);
 296          $this->assertSame(array('onevalue'), $params);
 297  
 298          // Correct usage passing empty array and $onemptyitems = 'onevalue' (equal = false, NAMED).
 299          $in_values = array();
 300          list($usql, $params) = $DB->get_in_or_equal($in_values, SQL_PARAMS_NAMED, 'param', false, 'onevalue');
 301          $this->assertCount(1, $params);
 302          $value = reset($params);
 303          $key = key($params);
 304          $this->assertSame('<> :'.$key, $usql);
 305          $this->assertSame($value, 'onevalue');
 306      }
 307  
 308      public function test_fix_table_names() {
 309          $DB = new moodle_database_for_testing();
 310          $prefix = $DB->get_prefix();
 311  
 312          // Simple placeholder.
 313          $placeholder = "{user_123}";
 314          $this->assertSame($prefix."user_123", $DB->public_fix_table_names($placeholder));
 315  
 316          // Wrong table name.
 317          $placeholder = "{user-a}";
 318          $this->assertSame($placeholder, $DB->public_fix_table_names($placeholder));
 319  
 320          // Wrong table name.
 321          $placeholder = "{123user}";
 322          $this->assertSame($placeholder, $DB->public_fix_table_names($placeholder));
 323  
 324          // Full SQL.
 325          $sql = "SELECT * FROM {user}, {funny_table_name}, {mdl_stupid_table} WHERE {user}.id = {funny_table_name}.userid";
 326          $expected = "SELECT * FROM {$prefix}user, {$prefix}funny_table_name, {$prefix}mdl_stupid_table WHERE {$prefix}user.id = {$prefix}funny_table_name.userid";
 327          $this->assertSame($expected, $DB->public_fix_table_names($sql));
 328      }
 329  
 330      public function test_fix_sql_params() {
 331          $DB = $this->tdb;
 332          $prefix = $DB->get_prefix();
 333  
 334          $table = $this->get_test_table();
 335          $tablename = $table->getName();
 336  
 337          // Correct table placeholder substitution.
 338          $sql = "SELECT * FROM {{$tablename}}";
 339          $sqlarray = $DB->fix_sql_params($sql);
 340          $this->assertEquals("SELECT * FROM {$prefix}".$tablename, $sqlarray[0]);
 341  
 342          // Conversions of all param types.
 343          $sql = array();
 344          $sql[SQL_PARAMS_NAMED]  = "SELECT * FROM {$prefix}testtable WHERE name = :param1, course = :param2";
 345          $sql[SQL_PARAMS_QM]     = "SELECT * FROM {$prefix}testtable WHERE name = ?, course = ?";
 346          $sql[SQL_PARAMS_DOLLAR] = "SELECT * FROM {$prefix}testtable WHERE name = \$1, course = \$2";
 347  
 348          $params = array();
 349          $params[SQL_PARAMS_NAMED]  = array('param1'=>'first record', 'param2'=>1);
 350          $params[SQL_PARAMS_QM]     = array('first record', 1);
 351          $params[SQL_PARAMS_DOLLAR] = array('first record', 1);
 352  
 353          list($rsql, $rparams, $rtype) = $DB->fix_sql_params($sql[SQL_PARAMS_NAMED], $params[SQL_PARAMS_NAMED]);
 354          $this->assertSame($rsql, $sql[$rtype]);
 355          $this->assertSame($rparams, $params[$rtype]);
 356  
 357          list($rsql, $rparams, $rtype) = $DB->fix_sql_params($sql[SQL_PARAMS_QM], $params[SQL_PARAMS_QM]);
 358          $this->assertSame($rsql, $sql[$rtype]);
 359          $this->assertSame($rparams, $params[$rtype]);
 360  
 361          list($rsql, $rparams, $rtype) = $DB->fix_sql_params($sql[SQL_PARAMS_DOLLAR], $params[SQL_PARAMS_DOLLAR]);
 362          $this->assertSame($rsql, $sql[$rtype]);
 363          $this->assertSame($rparams, $params[$rtype]);
 364  
 365          // Malformed table placeholder.
 366          $sql = "SELECT * FROM [testtable]";
 367          $sqlarray = $DB->fix_sql_params($sql);
 368          $this->assertSame($sql, $sqlarray[0]);
 369  
 370          // Mixed param types (colon and dollar).
 371          $sql = "SELECT * FROM {{$tablename}} WHERE name = :param1, course = \$1";
 372          $params = array('param1' => 'record1', 'param2' => 3);
 373          try {
 374              $DB->fix_sql_params($sql, $params);
 375              $this->fail("Expecting an exception, none occurred");
 376          } catch (\moodle_exception $e) {
 377              $this->assertInstanceOf('dml_exception', $e);
 378          }
 379  
 380          // Mixed param types (question and dollar).
 381          $sql = "SELECT * FROM {{$tablename}} WHERE name = ?, course = \$1";
 382          $params = array('param1' => 'record2', 'param2' => 5);
 383          try {
 384              $DB->fix_sql_params($sql, $params);
 385              $this->fail("Expecting an exception, none occurred");
 386          } catch (\moodle_exception $e) {
 387              $this->assertInstanceOf('dml_exception', $e);
 388          }
 389  
 390          // Too few params in sql.
 391          $sql = "SELECT * FROM {{$tablename}} WHERE name = ?, course = ?, id = ?";
 392          $params = array('record2', 3);
 393          try {
 394              $DB->fix_sql_params($sql, $params);
 395              $this->fail("Expecting an exception, none occurred");
 396          } catch (\moodle_exception $e) {
 397              $this->assertInstanceOf('dml_exception', $e);
 398          }
 399  
 400          // Too many params in array: no error, just use what is necessary.
 401          $params[] = 1;
 402          $params[] = time();
 403          $sqlarray = $DB->fix_sql_params($sql, $params);
 404          $this->assertIsArray($sqlarray);
 405          $this->assertCount(3, $sqlarray[1]);
 406  
 407          // Named params missing from array.
 408          $sql = "SELECT * FROM {{$tablename}} WHERE name = :name, course = :course";
 409          $params = array('wrongname' => 'record1', 'course' => 1);
 410          try {
 411              $DB->fix_sql_params($sql, $params);
 412              $this->fail("Expecting an exception, none occurred");
 413          } catch (\moodle_exception $e) {
 414              $this->assertInstanceOf('dml_exception', $e);
 415          }
 416  
 417          // Duplicate named param in query - this is a very important feature!!
 418          // it helps with debugging of sloppy code.
 419          $sql = "SELECT * FROM {{$tablename}} WHERE name = :name, course = :name";
 420          $params = array('name' => 'record2', 'course' => 3);
 421          try {
 422              $DB->fix_sql_params($sql, $params);
 423              $this->fail("Expecting an exception, none occurred");
 424          } catch (\moodle_exception $e) {
 425              $this->assertInstanceOf('dml_exception', $e);
 426          }
 427  
 428          // Extra named param is ignored.
 429          $sql = "SELECT * FROM {{$tablename}} WHERE name = :name, course = :course";
 430          $params = array('name' => 'record1', 'course' => 1, 'extrastuff'=>'haha');
 431          $sqlarray = $DB->fix_sql_params($sql, $params);
 432          $this->assertIsArray($sqlarray);
 433          $this->assertCount(2, $sqlarray[1]);
 434  
 435          // Params exceeding 30 chars length.
 436          $sql = "SELECT * FROM {{$tablename}} WHERE name = :long_placeholder_with_more_than_30";
 437          $params = array('long_placeholder_with_more_than_30' => 'record1');
 438          try {
 439              $DB->fix_sql_params($sql, $params);
 440              $this->fail("Expecting an exception, none occurred");
 441          } catch (\moodle_exception $e) {
 442              $this->assertInstanceOf('coding_exception', $e);
 443          }
 444  
 445          // Booleans in NAMED params are casting to 1/0 int.
 446          $sql = "SELECT * FROM {{$tablename}} WHERE course = ? OR course = ?";
 447          $params = array(true, false);
 448          list($sql, $params) = $DB->fix_sql_params($sql, $params);
 449          $this->assertTrue(reset($params) === 1);
 450          $this->assertTrue(next($params) === 0);
 451  
 452          // Booleans in QM params are casting to 1/0 int.
 453          $sql = "SELECT * FROM {{$tablename}} WHERE course = :course1 OR course = :course2";
 454          $params = array('course1' => true, 'course2' => false);
 455          list($sql, $params) = $DB->fix_sql_params($sql, $params);
 456          $this->assertTrue(reset($params) === 1);
 457          $this->assertTrue(next($params) === 0);
 458  
 459          // Booleans in DOLLAR params are casting to 1/0 int.
 460          $sql = "SELECT * FROM {{$tablename}} WHERE course = \$1 OR course = \$2";
 461          $params = array(true, false);
 462          list($sql, $params) = $DB->fix_sql_params($sql, $params);
 463          $this->assertTrue(reset($params) === 1);
 464          $this->assertTrue(next($params) === 0);
 465  
 466          // No data types are touched except bool.
 467          $sql = "SELECT * FROM {{$tablename}} WHERE name IN (?,?,?,?,?,?)";
 468          $inparams = array('abc', 'ABC', null, '1', 1, 1.4);
 469          list($sql, $params) = $DB->fix_sql_params($sql, $inparams);
 470          $this->assertSame(array_values($params), array_values($inparams));
 471      }
 472  
 473      /**
 474       * Test the database debugging as SQL comment.
 475       */
 476      public function test_add_sql_debugging() {
 477          global $CFG;
 478          $DB = $this->tdb;
 479  
 480          require_once($CFG->dirroot . '/lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php');
 481          $fixture = new \test_dml_sql_debugging_fixture($this);
 482  
 483          $sql = "SELECT * FROM {users}";
 484  
 485          $out = $fixture->four($sql);
 486  
 487          $CFG->debugsqltrace = 0;
 488          $this->assertEquals("SELECT * FROM {users}", $out);
 489  
 490          $CFG->debugsqltrace = 1;
 491          $out = $fixture->four($sql);
 492          $expected = <<<EOD
 493  SELECT * FROM {users}
 494  -- line 65 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to ReflectionMethod->invoke()
 495  EOD;
 496          $this->assertEquals($this->unix_to_os_dirsep($expected), $out);
 497  
 498          $CFG->debugsqltrace = 2;
 499          $out = $fixture->four($sql);
 500          $expected = <<<EOD
 501  SELECT * FROM {users}
 502  -- line 65 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to ReflectionMethod->invoke()
 503  -- line 74 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to test_dml_sql_debugging_fixture->one()
 504  EOD;
 505          $this->assertEquals($this->unix_to_os_dirsep($expected), $out);
 506  
 507          $CFG->debugsqltrace = 5;
 508          $out = $fixture->four($sql);
 509          $expected = <<<EOD
 510  SELECT * FROM {users}
 511  -- line 65 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to ReflectionMethod->invoke()
 512  -- line 74 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to test_dml_sql_debugging_fixture->one()
 513  -- line 83 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to test_dml_sql_debugging_fixture->two()
 514  -- line 92 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to test_dml_sql_debugging_fixture->three()
 515  -- line 508 of /lib/dml/tests/dml_test.php: call to test_dml_sql_debugging_fixture->four()
 516  EOD;
 517          $this->assertEquals($this->unix_to_os_dirsep($expected), $out);
 518  
 519          $CFG->debugsqltrace = 0;
 520      }
 521  
 522      /**
 523       * Test the database debugging as SQL comment in anon class
 524       *
 525       * @covers ::add_sql_debugging
 526       */
 527      public function test_sql_debugging_anon_class() {
 528          global $CFG;
 529          $CFG->debugsqltrace = 100;
 530  
 531          // A anon class.
 532          $another = new class {
 533              /**
 534               * Just a test log function
 535               */
 536              public function get_site() {
 537                  global $DB;
 538  
 539                  return $DB->get_record('course', ['category' => 0]);
 540              }
 541          };
 542          $site = $another->get_site();
 543          $CFG->debugsqltrace = 0;
 544          $this->assertEquals(get_site(), $site);
 545      }
 546  
 547      public function test_strtok() {
 548          // Strtok was previously used by bound emulation, make sure it is not used any more.
 549          $DB = $this->tdb;
 550          $dbman = $this->tdb->get_manager();
 551  
 552          $table = $this->get_test_table();
 553          $tablename = $table->getName();
 554  
 555          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 556          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
 557          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala');
 558          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 559          $dbman->create_table($table);
 560  
 561          $str = 'a?b?c?d';
 562          $this->assertSame(strtok($str, '?'), 'a');
 563  
 564          $DB->get_records($tablename, array('id'=>1));
 565  
 566          $this->assertSame(strtok('?'), 'b');
 567      }
 568  
 569      public function test_tweak_param_names() {
 570          // Note the tweak_param_names() method is only available in the oracle driver,
 571          // hence we look for expected results indirectly, by testing various DML methods.
 572          // with some "extreme" conditions causing the tweak to happen.
 573          $DB = $this->tdb;
 574          $dbman = $this->tdb->get_manager();
 575  
 576          $table = $this->get_test_table();
 577          $tablename = $table->getName();
 578  
 579          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 580          // Add some columns with 28 chars in the name.
 581          $table->add_field('long_int_columnname_with_28c', XMLDB_TYPE_INTEGER, '10');
 582          $table->add_field('long_dec_columnname_with_28c', XMLDB_TYPE_NUMBER, '10,2');
 583          $table->add_field('long_str_columnname_with_28c', XMLDB_TYPE_CHAR, '100');
 584          // Add some columns with 30 chars in the name.
 585          $table->add_field('long_int_columnname_with_30cxx', XMLDB_TYPE_INTEGER, '10');
 586          $table->add_field('long_dec_columnname_with_30cxx', XMLDB_TYPE_NUMBER, '10,2');
 587          $table->add_field('long_str_columnname_with_30cxx', XMLDB_TYPE_CHAR, '100');
 588  
 589          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 590  
 591          $dbman->create_table($table);
 592  
 593          $this->assertTrue($dbman->table_exists($tablename));
 594  
 595          // Test insert record.
 596          $rec1 = new \stdClass();
 597          $rec1->long_int_columnname_with_28c = 28;
 598          $rec1->long_dec_columnname_with_28c = 28.28;
 599          $rec1->long_str_columnname_with_28c = '28';
 600          $rec1->long_int_columnname_with_30cxx = 30;
 601          $rec1->long_dec_columnname_with_30cxx = 30.30;
 602          $rec1->long_str_columnname_with_30cxx = '30';
 603  
 604          // Insert_record().
 605          $rec1->id = $DB->insert_record($tablename, $rec1);
 606          $this->assertEquals($rec1, $DB->get_record($tablename, array('id' => $rec1->id)));
 607  
 608          // Update_record().
 609          $DB->update_record($tablename, $rec1);
 610          $this->assertEquals($rec1, $DB->get_record($tablename, array('id' => $rec1->id)));
 611  
 612          // Set_field().
 613          $rec1->long_int_columnname_with_28c = 280;
 614          $DB->set_field($tablename, 'long_int_columnname_with_28c', $rec1->long_int_columnname_with_28c,
 615              array('id' => $rec1->id, 'long_int_columnname_with_28c' => 28));
 616          $rec1->long_dec_columnname_with_28c = 280.28;
 617          $DB->set_field($tablename, 'long_dec_columnname_with_28c', $rec1->long_dec_columnname_with_28c,
 618              array('id' => $rec1->id, 'long_dec_columnname_with_28c' => 28.28));
 619          $rec1->long_str_columnname_with_28c = '280';
 620          $DB->set_field($tablename, 'long_str_columnname_with_28c', $rec1->long_str_columnname_with_28c,
 621              array('id' => $rec1->id, 'long_str_columnname_with_28c' => '28'));
 622          $rec1->long_int_columnname_with_30cxx = 300;
 623          $DB->set_field($tablename, 'long_int_columnname_with_30cxx', $rec1->long_int_columnname_with_30cxx,
 624              array('id' => $rec1->id, 'long_int_columnname_with_30cxx' => 30));
 625          $rec1->long_dec_columnname_with_30cxx = 300.30;
 626          $DB->set_field($tablename, 'long_dec_columnname_with_30cxx', $rec1->long_dec_columnname_with_30cxx,
 627              array('id' => $rec1->id, 'long_dec_columnname_with_30cxx' => 30.30));
 628          $rec1->long_str_columnname_with_30cxx = '300';
 629          $DB->set_field($tablename, 'long_str_columnname_with_30cxx', $rec1->long_str_columnname_with_30cxx,
 630              array('id' => $rec1->id, 'long_str_columnname_with_30cxx' => '30'));
 631          $this->assertEquals($rec1, $DB->get_record($tablename, array('id' => $rec1->id)));
 632  
 633          // Delete_records().
 634          $rec2 = $DB->get_record($tablename, array('id' => $rec1->id));
 635          $rec2->id = $DB->insert_record($tablename, $rec2);
 636          $this->assertEquals(2, $DB->count_records($tablename));
 637          $DB->delete_records($tablename, (array) $rec2);
 638          $this->assertEquals(1, $DB->count_records($tablename));
 639  
 640          // Get_recordset().
 641          $rs = $DB->get_recordset($tablename, (array) $rec1);
 642          $iterations = 0;
 643          foreach ($rs as $rec2) {
 644              $iterations++;
 645          }
 646          $rs->close();
 647          $this->assertEquals(1, $iterations);
 648          $this->assertEquals($rec1, $rec2);
 649  
 650          // Get_records().
 651          $recs = $DB->get_records($tablename, (array) $rec1);
 652          $this->assertCount(1, $recs);
 653          $this->assertEquals($rec1, reset($recs));
 654  
 655          // Get_fieldset_select().
 656          $select = 'id = :id AND
 657                     long_int_columnname_with_28c = :long_int_columnname_with_28c AND
 658                     long_dec_columnname_with_28c = :long_dec_columnname_with_28c AND
 659                     long_str_columnname_with_28c = :long_str_columnname_with_28c AND
 660                     long_int_columnname_with_30cxx = :long_int_columnname_with_30cxx AND
 661                     long_dec_columnname_with_30cxx = :long_dec_columnname_with_30cxx AND
 662                     long_str_columnname_with_30cxx = :long_str_columnname_with_30cxx';
 663          $fields = $DB->get_fieldset_select($tablename, 'long_int_columnname_with_28c', $select, (array)$rec1);
 664          $this->assertCount(1, $fields);
 665          $this->assertEquals($rec1->long_int_columnname_with_28c, reset($fields));
 666          $fields = $DB->get_fieldset_select($tablename, 'long_dec_columnname_with_28c', $select, (array)$rec1);
 667          $this->assertEquals($rec1->long_dec_columnname_with_28c, reset($fields));
 668          $fields = $DB->get_fieldset_select($tablename, 'long_str_columnname_with_28c', $select, (array)$rec1);
 669          $this->assertEquals($rec1->long_str_columnname_with_28c, reset($fields));
 670          $fields = $DB->get_fieldset_select($tablename, 'long_int_columnname_with_30cxx', $select, (array)$rec1);
 671          $this->assertEquals($rec1->long_int_columnname_with_30cxx, reset($fields));
 672          $fields = $DB->get_fieldset_select($tablename, 'long_dec_columnname_with_30cxx', $select, (array)$rec1);
 673          $this->assertEquals($rec1->long_dec_columnname_with_30cxx, reset($fields));
 674          $fields = $DB->get_fieldset_select($tablename, 'long_str_columnname_with_30cxx', $select, (array)$rec1);
 675          $this->assertEquals($rec1->long_str_columnname_with_30cxx, reset($fields));
 676  
 677          // Overlapping placeholders (progressive str_replace).
 678          $overlapselect = 'id = :p AND
 679                     long_int_columnname_with_28c = :param1 AND
 680                     long_dec_columnname_with_28c = :param2 AND
 681                     long_str_columnname_with_28c = :param_with_29_characters_long AND
 682                     long_int_columnname_with_30cxx = :param_with_30_characters_long_ AND
 683                     long_dec_columnname_with_30cxx = :param_ AND
 684                     long_str_columnname_with_30cxx = :param__';
 685          $overlapparams = array(
 686              'p' => $rec1->id,
 687              'param1' => $rec1->long_int_columnname_with_28c,
 688              'param2' => $rec1->long_dec_columnname_with_28c,
 689              'param_with_29_characters_long' => $rec1->long_str_columnname_with_28c,
 690              'param_with_30_characters_long_' => $rec1->long_int_columnname_with_30cxx,
 691              'param_' => $rec1->long_dec_columnname_with_30cxx,
 692              'param__' => $rec1->long_str_columnname_with_30cxx);
 693          $recs = $DB->get_records_select($tablename, $overlapselect, $overlapparams);
 694          $this->assertCount(1, $recs);
 695          $this->assertEquals($rec1, reset($recs));
 696  
 697          // Execute().
 698          $DB->execute("DELETE FROM {{$tablename}} WHERE $select", (array)$rec1);
 699          $this->assertEquals(0, $DB->count_records($tablename));
 700      }
 701  
 702      public function test_get_tables() {
 703          $DB = $this->tdb;
 704          $dbman = $this->tdb->get_manager();
 705  
 706          // Need to test with multiple DBs.
 707          $table = $this->get_test_table();
 708          $tablename = $table->getName();
 709  
 710          $original_count = count($DB->get_tables());
 711  
 712          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 713          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 714  
 715          $dbman->create_table($table);
 716          $this->assertTrue(count($DB->get_tables()) == $original_count + 1);
 717  
 718          $dbman->drop_table($table);
 719          $this->assertTrue(count($DB->get_tables()) == $original_count);
 720      }
 721  
 722      public function test_get_indexes() {
 723          $DB = $this->tdb;
 724          $dbman = $this->tdb->get_manager();
 725  
 726          $table = $this->get_test_table();
 727          $tablename = $table->getName();
 728  
 729          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 730          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
 731          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 732          $table->add_index('course', XMLDB_INDEX_NOTUNIQUE, array('course'));
 733          $table->add_index('course-id', XMLDB_INDEX_UNIQUE, array('course', 'id'));
 734          $dbman->create_table($table);
 735  
 736          $indices = $DB->get_indexes($tablename);
 737          $this->assertIsArray($indices);
 738          $this->assertCount(2, $indices);
 739          // We do not care about index names for now.
 740          $first = array_shift($indices);
 741          $second = array_shift($indices);
 742          if (count($first['columns']) == 2) {
 743              $composed = $first;
 744              $single   = $second;
 745          } else {
 746              $composed = $second;
 747              $single   = $first;
 748          }
 749          $this->assertFalse($single['unique']);
 750          $this->assertTrue($composed['unique']);
 751          $this->assertCount(1, $single['columns']);
 752          $this->assertCount(2, $composed['columns']);
 753          $this->assertSame('course', $single['columns'][0]);
 754          $this->assertSame('course', $composed['columns'][0]);
 755          $this->assertSame('id', $composed['columns'][1]);
 756      }
 757  
 758      /**
 759       * Let's verify get_indexes() when we mix null and not null columns in unique indexes.
 760       *
 761       * Some databases, for unique indexes of this type, need to create function indexes to
 762       * provide cross-db behaviour. Here we check that those indexes don't break get_indexes().
 763       *
 764       * Note that, strictly speaking, unique indexes on null columns are far from ideal. Both
 765       * conceptually and also in practice, because they cause DBs to use full scans in a
 766       * number of situations. But if we support them, we need to ensure get_indexes() work on them.
 767       */
 768      public function test_get_indexes_unique_mixed_nullability() {
 769          $DB = $this->tdb;
 770          $dbman = $this->tdb->get_manager();
 771          $table = $this->get_test_table();
 772          $tablename = $table->getName();
 773  
 774          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 775          $table->add_field('nullable01', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
 776          $table->add_field('nullable02', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
 777          $table->add_field('nonullable01', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
 778          $table->add_field('nonullable02', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
 779          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 780          $indexcolumns = ['nullable01', 'nonullable01', 'nullable02', 'nonullable02'];
 781          $table->add_index('course-id', XMLDB_INDEX_UNIQUE, $indexcolumns);
 782          $dbman->create_table($table);
 783  
 784          $indexes = $DB->get_indexes($tablename);
 785          $this->assertIsArray($indexes);
 786          $this->assertCount(1, $indexes);
 787  
 788          $index = array_shift($indexes);
 789          $this->assertTrue($index['unique']);
 790          $this->assertSame($indexcolumns, $index['columns']);
 791      }
 792  
 793      public function test_get_columns() {
 794          $DB = $this->tdb;
 795          $dbman = $this->tdb->get_manager();
 796  
 797          $table = $this->get_test_table();
 798          $tablename = $table->getName();
 799  
 800          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 801          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
 802          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala');
 803          $table->add_field('description', XMLDB_TYPE_TEXT, 'small', null, null, null, null);
 804          $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
 805          $table->add_field('oneintnodefault', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null);
 806          $table->add_field('enumfield', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, 'test2');
 807          $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
 808          $table->add_field('onenumnodefault', XMLDB_TYPE_NUMBER, '10,2', null, null, null);
 809          $table->add_field('onefloat', XMLDB_TYPE_FLOAT, '10,2', null, XMLDB_NOTNULL, null, 300);
 810          $table->add_field('onefloatnodefault', XMLDB_TYPE_FLOAT, '10,2', null, XMLDB_NOTNULL, null);
 811          $table->add_field('anotherfloat', XMLDB_TYPE_FLOAT, null, null, null, null, 400);
 812          $table->add_field('negativedfltint', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '-1');
 813          $table->add_field('negativedfltnumber', XMLDB_TYPE_NUMBER, '10', null, XMLDB_NOTNULL, null, '-2');
 814          $table->add_field('negativedfltfloat', XMLDB_TYPE_FLOAT, '10', null, XMLDB_NOTNULL, null, '-3');
 815          $table->add_field('someint1', XMLDB_TYPE_INTEGER, '1', null, null, null, '0');
 816          $table->add_field('someint2', XMLDB_TYPE_INTEGER, '2', null, null, null, '0');
 817          $table->add_field('someint3', XMLDB_TYPE_INTEGER, '3', null, null, null, '0');
 818          $table->add_field('someint4', XMLDB_TYPE_INTEGER, '4', null, null, null, '0');
 819          $table->add_field('someint5', XMLDB_TYPE_INTEGER, '5', null, null, null, '0');
 820          $table->add_field('someint6', XMLDB_TYPE_INTEGER, '6', null, null, null, '0');
 821          $table->add_field('someint7', XMLDB_TYPE_INTEGER, '7', null, null, null, '0');
 822          $table->add_field('someint8', XMLDB_TYPE_INTEGER, '8', null, null, null, '0');
 823          $table->add_field('someint9', XMLDB_TYPE_INTEGER, '9', null, null, null, '0');
 824          $table->add_field('someint10', XMLDB_TYPE_INTEGER, '10', null, null, null, '0');
 825          $table->add_field('someint18', XMLDB_TYPE_INTEGER, '18', null, null, null, '0');
 826          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 827          $dbman->create_table($table);
 828  
 829          $columns = $DB->get_columns($tablename);
 830          $this->assertIsArray($columns);
 831  
 832          $fields = $table->getFields();
 833          $this->assertCount(count($columns), $fields);
 834  
 835          $field = $columns['id'];
 836          $this->assertSame('R', $field->meta_type);
 837          $this->assertTrue($field->auto_increment);
 838          $this->assertTrue($field->unique);
 839  
 840          $field = $columns['course'];
 841          $this->assertSame('I', $field->meta_type);
 842          $this->assertFalse($field->auto_increment);
 843          $this->assertTrue($field->has_default);
 844          $this->assertEquals(0, $field->default_value);
 845          $this->assertTrue($field->not_null);
 846  
 847          for ($i=1; $i<=10; $i++) {
 848              $field = $columns['someint'.$i];
 849              $this->assertSame('I', $field->meta_type);
 850              $this->assertGreaterThanOrEqual($i, $field->max_length);
 851          }
 852          $field = $columns['someint18'];
 853          $this->assertSame('I', $field->meta_type);
 854          $this->assertGreaterThanOrEqual(18, $field->max_length);
 855  
 856          $field = $columns['name'];
 857          $this->assertSame('C', $field->meta_type);
 858          $this->assertFalse($field->auto_increment);
 859          $this->assertEquals(255, $field->max_length);
 860          $this->assertTrue($field->has_default);
 861          $this->assertSame('lala', $field->default_value);
 862          $this->assertFalse($field->not_null);
 863  
 864          $field = $columns['description'];
 865          $this->assertSame('X', $field->meta_type);
 866          $this->assertFalse($field->auto_increment);
 867          $this->assertFalse($field->has_default);
 868          $this->assertNull($field->default_value);
 869          $this->assertFalse($field->not_null);
 870  
 871          $field = $columns['oneint'];
 872          $this->assertSame('I', $field->meta_type);
 873          $this->assertFalse($field->auto_increment);
 874          $this->assertTrue($field->has_default);
 875          $this->assertEquals(0, $field->default_value);
 876          $this->assertTrue($field->not_null);
 877  
 878          $field = $columns['oneintnodefault'];
 879          $this->assertSame('I', $field->meta_type);
 880          $this->assertFalse($field->auto_increment);
 881          $this->assertFalse($field->has_default);
 882          $this->assertNull($field->default_value);
 883          $this->assertTrue($field->not_null);
 884  
 885          $field = $columns['enumfield'];
 886          $this->assertSame('C', $field->meta_type);
 887          $this->assertFalse($field->auto_increment);
 888          $this->assertSame('test2', $field->default_value);
 889          $this->assertTrue($field->not_null);
 890  
 891          $field = $columns['onenum'];
 892          $this->assertSame('N', $field->meta_type);
 893          $this->assertFalse($field->auto_increment);
 894          $this->assertEquals(10, $field->max_length);
 895          $this->assertEquals(2, $field->scale);
 896          $this->assertTrue($field->has_default);
 897          $this->assertEquals(200.0, $field->default_value);
 898          $this->assertFalse($field->not_null);
 899  
 900          $field = $columns['onenumnodefault'];
 901          $this->assertSame('N', $field->meta_type);
 902          $this->assertFalse($field->auto_increment);
 903          $this->assertEquals(10, $field->max_length);
 904          $this->assertEquals(2, $field->scale);
 905          $this->assertFalse($field->has_default);
 906          $this->assertNull($field->default_value);
 907          $this->assertFalse($field->not_null);
 908  
 909          $field = $columns['onefloat'];
 910          $this->assertSame('N', $field->meta_type);
 911          $this->assertFalse($field->auto_increment);
 912          $this->assertTrue($field->has_default);
 913          $this->assertEquals(300.0, $field->default_value);
 914          $this->assertTrue($field->not_null);
 915  
 916          $field = $columns['onefloatnodefault'];
 917          $this->assertSame('N', $field->meta_type);
 918          $this->assertFalse($field->auto_increment);
 919          $this->assertFalse($field->has_default);
 920          $this->assertNull($field->default_value);
 921          $this->assertTrue($field->not_null);
 922  
 923          $field = $columns['anotherfloat'];
 924          $this->assertSame('N', $field->meta_type);
 925          $this->assertFalse($field->auto_increment);
 926          $this->assertTrue($field->has_default);
 927          $this->assertEquals(400.0, $field->default_value);
 928          $this->assertFalse($field->not_null);
 929  
 930          // Test negative defaults in numerical columns.
 931          $field = $columns['negativedfltint'];
 932          $this->assertTrue($field->has_default);
 933          $this->assertEquals(-1, $field->default_value);
 934  
 935          $field = $columns['negativedfltnumber'];
 936          $this->assertTrue($field->has_default);
 937          $this->assertEquals(-2, $field->default_value);
 938  
 939          $field = $columns['negativedfltfloat'];
 940          $this->assertTrue($field->has_default);
 941          $this->assertEquals(-3, $field->default_value);
 942  
 943          for ($i = 0; $i < count($columns); $i++) {
 944              if ($i == 0) {
 945                  $next_column = reset($columns);
 946                  $next_field  = reset($fields);
 947              } else {
 948                  $next_column = next($columns);
 949                  $next_field  = next($fields);
 950              }
 951  
 952              $this->assertEquals($next_column->name, $next_field->getName());
 953          }
 954  
 955          // Test get_columns for non-existing table returns empty array. MDL-30147.
 956          $columns = $DB->get_columns('xxxx');
 957          $this->assertEquals(array(), $columns);
 958  
 959          // Create something similar to "context_temp" with id column without sequence.
 960          $dbman->drop_table($table);
 961          $table = $this->get_test_table();
 962          $tablename = $table->getName();
 963          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
 964          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
 965          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 966          $dbman->create_table($table);
 967  
 968          $columns = $DB->get_columns($tablename);
 969          $this->assertFalse($columns['id']->auto_increment);
 970      }
 971  
 972      public function test_get_manager() {
 973          $DB = $this->tdb;
 974          $dbman = $this->tdb->get_manager();
 975  
 976          $this->assertInstanceOf('database_manager', $dbman);
 977      }
 978  
 979      public function test_setup_is_unicodedb() {
 980          $DB = $this->tdb;
 981          $this->assertTrue($DB->setup_is_unicodedb());
 982      }
 983  
 984      public function test_set_debug() { // Tests get_debug() too.
 985          $DB = $this->tdb;
 986          $dbman = $this->tdb->get_manager();
 987  
 988          $table = $this->get_test_table();
 989          $tablename = $table->getName();
 990  
 991          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 992          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
 993          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 994          $dbman->create_table($table);
 995  
 996          $sql = "SELECT * FROM {{$tablename}}";
 997  
 998          $prevdebug = $DB->get_debug();
 999  
1000          ob_start();
1001          $DB->set_debug(true);
1002          $this->assertTrue($DB->get_debug());
1003          $DB->execute($sql);
1004          $DB->set_debug(false);
1005          $this->assertFalse($DB->get_debug());
1006          $debuginfo = ob_get_contents();
1007          ob_end_clean();
1008          $this->assertFalse($debuginfo === '');
1009  
1010          ob_start();
1011          $DB->execute($sql);
1012          $debuginfo = ob_get_contents();
1013          ob_end_clean();
1014          $this->assertTrue($debuginfo === '');
1015  
1016          $DB->set_debug($prevdebug);
1017      }
1018  
1019      public function test_execute() {
1020          $DB = $this->tdb;
1021          $dbman = $this->tdb->get_manager();
1022  
1023          $table1 = $this->get_test_table('1');
1024          $tablename1 = $table1->getName();
1025          $table1->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1026          $table1->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1027          $table1->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, '0');
1028          $table1->add_index('course', XMLDB_INDEX_NOTUNIQUE, array('course'));
1029          $table1->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1030          $dbman->create_table($table1);
1031  
1032          $table2 = $this->get_test_table('2');
1033          $tablename2 = $table2->getName();
1034          $table2->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1035          $table2->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1036          $table2->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
1037          $table2->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1038          $dbman->create_table($table2);
1039  
1040          $DB->insert_record($tablename1, array('course' => 3, 'name' => 'aaa'));
1041          $DB->insert_record($tablename1, array('course' => 1, 'name' => 'bbb'));
1042          $DB->insert_record($tablename1, array('course' => 7, 'name' => 'ccc'));
1043          $DB->insert_record($tablename1, array('course' => 3, 'name' => 'ddd'));
1044  
1045          // Select results are ignored.
1046          $sql = "SELECT * FROM {{$tablename1}} WHERE course = :course";
1047          $this->assertTrue($DB->execute($sql, array('course'=>3)));
1048  
1049          // Throw exception on error.
1050          $sql = "XXUPDATE SET XSSD";
1051          try {
1052              $DB->execute($sql);
1053              $this->fail("Expecting an exception, none occurred");
1054          } catch (\moodle_exception $e) {
1055              $this->assertInstanceOf('dml_exception', $e);
1056          }
1057  
1058          // Update records.
1059          $sql = "UPDATE {{$tablename1}}
1060                     SET course = 6
1061                   WHERE course = ?";
1062          $this->assertTrue($DB->execute($sql, array('3')));
1063          $this->assertEquals(2, $DB->count_records($tablename1, array('course' => 6)));
1064  
1065          // Update records with subquery condition.
1066          // Confirm that the option not using table aliases is cross-db.
1067          $sql = "UPDATE {{$tablename1}}
1068                     SET course = 0
1069                   WHERE NOT EXISTS (
1070                             SELECT course
1071                               FROM {{$tablename2}} tbl2
1072                              WHERE tbl2.course = {{$tablename1}}.course
1073                                AND 1 = 0)"; // Really we don't update anything, but verify the syntax is allowed.
1074          $this->assertTrue($DB->execute($sql));
1075  
1076          // Insert from one into second table.
1077          $sql = "INSERT INTO {{$tablename2}} (course)
1078  
1079                  SELECT course
1080                    FROM {{$tablename1}}";
1081          $this->assertTrue($DB->execute($sql));
1082          $this->assertEquals(4, $DB->count_records($tablename2));
1083  
1084          // Insert a TEXT with raw SQL, binding TEXT params.
1085          $course = 9999;
1086          $onetext = file_get_contents(__DIR__ . '/fixtures/clob.txt');
1087          $sql = "INSERT INTO {{$tablename2}} (course, onetext)
1088                  VALUES (:course, :onetext)";
1089          $DB->execute($sql, array('course' => $course, 'onetext' => $onetext));
1090          $records = $DB->get_records($tablename2, array('course' => $course));
1091          $this->assertCount(1, $records);
1092          $record = reset($records);
1093          $this->assertSame($onetext, $record->onetext);
1094  
1095          // Update a TEXT with raw SQL, binding TEXT params.
1096          $newcourse = 10000;
1097          $newonetext = file_get_contents(__DIR__ . '/fixtures/clob.txt') . '- updated';
1098          $sql = "UPDATE {{$tablename2}} SET course = :newcourse, onetext = :newonetext
1099                  WHERE course = :oldcourse";
1100          $DB->execute($sql, array('oldcourse' => $course, 'newcourse' => $newcourse, 'newonetext' => $newonetext));
1101          $records = $DB->get_records($tablename2, array('course' => $course));
1102          $this->assertCount(0, $records);
1103          $records = $DB->get_records($tablename2, array('course' => $newcourse));
1104          $this->assertCount(1, $records);
1105          $record = reset($records);
1106          $this->assertSame($newonetext, $record->onetext);
1107      }
1108  
1109      public function test_get_recordset() {
1110          $DB = $this->tdb;
1111          $dbman = $DB->get_manager();
1112  
1113          $table = $this->get_test_table();
1114          $tablename = $table->getName();
1115  
1116          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1117          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1118          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, '0');
1119          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
1120          $table->add_index('course', XMLDB_INDEX_NOTUNIQUE, array('course'));
1121          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1122          $dbman->create_table($table);
1123  
1124          $data = array(array('course' => 3, 'name' => 'record1', 'onetext'=>'abc'),
1125              array('course' => 3, 'name' => 'record2', 'onetext'=>'abcd'),
1126              array('course' => 5, 'name' => 'record3', 'onetext'=>'abcde'));
1127  
1128          foreach ($data as $key => $record) {
1129              $data[$key]['id'] = $DB->insert_record($tablename, $record);
1130          }
1131  
1132          // Standard recordset iteration.
1133          $rs = $DB->get_recordset($tablename);
1134          $this->assertInstanceOf('moodle_recordset', $rs);
1135          reset($data);
1136          foreach ($rs as $record) {
1137              $data_record = current($data);
1138              foreach ($record as $k => $v) {
1139                  $this->assertEquals($data_record[$k], $v);
1140              }
1141              next($data);
1142          }
1143          $rs->close();
1144  
1145          // Iterator style usage.
1146          $rs = $DB->get_recordset($tablename);
1147          $this->assertInstanceOf('moodle_recordset', $rs);
1148          reset($data);
1149          while ($rs->valid()) {
1150              $record = $rs->current();
1151              $data_record = current($data);
1152              foreach ($record as $k => $v) {
1153                  $this->assertEquals($data_record[$k], $v);
1154              }
1155              next($data);
1156              $rs->next();
1157          }
1158          $rs->close();
1159  
1160          // Make sure rewind is ignored.
1161          $rs = $DB->get_recordset($tablename);
1162          $this->assertInstanceOf('moodle_recordset', $rs);
1163          reset($data);
1164          $i = 0;
1165          foreach ($rs as $record) {
1166              $i++;
1167              $rs->rewind();
1168              if ($i > 10) {
1169                  $this->fail('revind not ignored in recordsets');
1170                  break;
1171              }
1172              $data_record = current($data);
1173              foreach ($record as $k => $v) {
1174                  $this->assertEquals($data_record[$k], $v);
1175              }
1176              next($data);
1177          }
1178          $rs->close();
1179  
1180          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
1181          $conditions = array('onetext' => '1');
1182          try {
1183              $rs = $DB->get_recordset($tablename, $conditions);
1184              $this->fail('An Exception is missing, expected due to equating of text fields');
1185          } catch (\moodle_exception $e) {
1186              $this->assertInstanceOf('dml_exception', $e);
1187              $this->assertSame('textconditionsnotallowed', $e->errorcode);
1188          }
1189  
1190          // Test nested iteration.
1191          $rs1 = $DB->get_recordset($tablename);
1192          $i = 0;
1193          foreach ($rs1 as $record1) {
1194              $rs2 = $DB->get_recordset($tablename);
1195              $i++;
1196              $j = 0;
1197              foreach ($rs2 as $record2) {
1198                  $j++;
1199              }
1200              $rs2->close();
1201              $this->assertCount($j, $data);
1202          }
1203          $rs1->close();
1204          $this->assertCount($i, $data);
1205  
1206          // Notes:
1207          //  * limits are tested in test_get_recordset_sql()
1208          //  * where_clause() is used internally and is tested in test_get_records()
1209      }
1210  
1211      public function test_get_recordset_static() {
1212          $DB = $this->tdb;
1213          $dbman = $DB->get_manager();
1214  
1215          $table = $this->get_test_table();
1216          $tablename = $table->getName();
1217  
1218          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1219          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1220          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1221          $dbman->create_table($table);
1222  
1223          $DB->insert_record($tablename, array('course' => 1));
1224          $DB->insert_record($tablename, array('course' => 2));
1225          $DB->insert_record($tablename, array('course' => 3));
1226          $DB->insert_record($tablename, array('course' => 4));
1227  
1228          $rs = $DB->get_recordset($tablename, array(), 'id');
1229  
1230          $DB->set_field($tablename, 'course', 666, array('course'=>1));
1231          $DB->delete_records($tablename, array('course'=>2));
1232  
1233          $i = 0;
1234          foreach ($rs as $record) {
1235              $i++;
1236              $this->assertEquals($i, $record->course);
1237          }
1238          $rs->close();
1239          $this->assertEquals(4, $i);
1240  
1241          // Now repeat with limits because it may use different code.
1242          $DB->delete_records($tablename, array());
1243  
1244          $DB->insert_record($tablename, array('course' => 1));
1245          $DB->insert_record($tablename, array('course' => 2));
1246          $DB->insert_record($tablename, array('course' => 3));
1247          $DB->insert_record($tablename, array('course' => 4));
1248  
1249          $rs = $DB->get_recordset($tablename, array(), 'id', '*', 0, 3);
1250  
1251          $DB->set_field($tablename, 'course', 666, array('course'=>1));
1252          $DB->delete_records($tablename, array('course'=>2));
1253  
1254          $i = 0;
1255          foreach ($rs as $record) {
1256              $i++;
1257              $this->assertEquals($i, $record->course);
1258          }
1259          $rs->close();
1260          $this->assertEquals(3, $i);
1261      }
1262  
1263      public function test_get_recordset_iterator_keys() {
1264          $DB = $this->tdb;
1265          $dbman = $DB->get_manager();
1266  
1267          $table = $this->get_test_table();
1268          $tablename = $table->getName();
1269  
1270          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1271          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1272          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, '0');
1273          $table->add_index('course', XMLDB_INDEX_NOTUNIQUE, array('course'));
1274          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1275          $dbman->create_table($table);
1276  
1277          $data = array(array('course' => 3, 'name' => 'record1'),
1278              array('course' => 3, 'name' => 'record2'),
1279              array('course' => 5, 'name' => 'record3'));
1280          foreach ($data as $key => $record) {
1281              $data[$key]['id'] = $DB->insert_record($tablename, $record);
1282          }
1283  
1284          // Test repeated numeric keys are returned ok.
1285          $rs = $DB->get_recordset($tablename, null, null, 'course, name, id');
1286  
1287          reset($data);
1288          $count = 0;
1289          foreach ($rs as $key => $record) {
1290              $data_record = current($data);
1291              $this->assertEquals($data_record['course'], $key);
1292              next($data);
1293              $count++;
1294          }
1295          $rs->close();
1296          $this->assertEquals(3, $count);
1297  
1298          // Test string keys are returned ok.
1299          $rs = $DB->get_recordset($tablename, null, null, 'name, course, id');
1300  
1301          reset($data);
1302          $count = 0;
1303          foreach ($rs as $key => $record) {
1304              $data_record = current($data);
1305              $this->assertEquals($data_record['name'], $key);
1306              next($data);
1307              $count++;
1308          }
1309          $rs->close();
1310          $this->assertEquals(3, $count);
1311  
1312          // Test numeric not starting in 1 keys are returned ok.
1313          $rs = $DB->get_recordset($tablename, null, 'id DESC', 'id, course, name');
1314  
1315          $data = array_reverse($data);
1316          reset($data);
1317          $count = 0;
1318          foreach ($rs as $key => $record) {
1319              $data_record = current($data);
1320              $this->assertEquals($data_record['id'], $key);
1321              next($data);
1322              $count++;
1323          }
1324          $rs->close();
1325          $this->assertEquals(3, $count);
1326      }
1327  
1328      public function test_get_recordset_list() {
1329          $DB = $this->tdb;
1330          $dbman = $DB->get_manager();
1331  
1332          $table = $this->get_test_table();
1333          $tablename = $table->getName();
1334  
1335          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1336          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, null, null, '0');
1337          $table->add_index('course', XMLDB_INDEX_NOTUNIQUE, array('course'));
1338          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1339          $dbman->create_table($table);
1340  
1341          $DB->insert_record($tablename, array('course' => 3));
1342          $DB->insert_record($tablename, array('course' => 3));
1343          $DB->insert_record($tablename, array('course' => 5));
1344          $DB->insert_record($tablename, array('course' => 2));
1345          $DB->insert_record($tablename, array('course' => null));
1346          $DB->insert_record($tablename, array('course' => 1));
1347          $DB->insert_record($tablename, array('course' => 0));
1348  
1349          $rs = $DB->get_recordset_list($tablename, 'course', array(3, 2));
1350          $counter = 0;
1351          foreach ($rs as $record) {
1352              $counter++;
1353          }
1354          $this->assertEquals(3, $counter);
1355          $rs->close();
1356  
1357          $rs = $DB->get_recordset_list($tablename, 'course', array(3));
1358          $counter = 0;
1359          foreach ($rs as $record) {
1360              $counter++;
1361          }
1362          $this->assertEquals(2, $counter);
1363          $rs->close();
1364  
1365          $rs = $DB->get_recordset_list($tablename, 'course', array(null));
1366          $counter = 0;
1367          foreach ($rs as $record) {
1368              $counter++;
1369          }
1370          $this->assertEquals(1, $counter);
1371          $rs->close();
1372  
1373          $rs = $DB->get_recordset_list($tablename, 'course', array(6, null));
1374          $counter = 0;
1375          foreach ($rs as $record) {
1376              $counter++;
1377          }
1378          $this->assertEquals(1, $counter);
1379          $rs->close();
1380  
1381          $rs = $DB->get_recordset_list($tablename, 'course', array(null, 5, 5, 5));
1382          $counter = 0;
1383          foreach ($rs as $record) {
1384              $counter++;
1385          }
1386          $this->assertEquals(2, $counter);
1387          $rs->close();
1388  
1389          $rs = $DB->get_recordset_list($tablename, 'course', array(true));
1390          $counter = 0;
1391          foreach ($rs as $record) {
1392              $counter++;
1393          }
1394          $this->assertEquals(1, $counter);
1395          $rs->close();
1396  
1397          $rs = $DB->get_recordset_list($tablename, 'course', array(false));
1398          $counter = 0;
1399          foreach ($rs as $record) {
1400              $counter++;
1401          }
1402          $this->assertEquals(1, $counter);
1403          $rs->close();
1404  
1405          $rs = $DB->get_recordset_list($tablename, 'course', array()); // Must return 0 rows without conditions. MDL-17645.
1406  
1407          $counter = 0;
1408          foreach ($rs as $record) {
1409              $counter++;
1410          }
1411          $rs->close();
1412          $this->assertEquals(0, $counter);
1413  
1414          // Notes:
1415          //  * limits are tested in test_get_recordset_sql()
1416          //  * where_clause() is used internally and is tested in test_get_records()
1417      }
1418  
1419      public function test_get_recordset_select() {
1420          $DB = $this->tdb;
1421          $dbman = $DB->get_manager();
1422  
1423          $table = $this->get_test_table();
1424          $tablename = $table->getName();
1425  
1426          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1427          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1428          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1429          $dbman->create_table($table);
1430  
1431          $DB->insert_record($tablename, array('course' => 3));
1432          $DB->insert_record($tablename, array('course' => 3));
1433          $DB->insert_record($tablename, array('course' => 5));
1434          $DB->insert_record($tablename, array('course' => 2));
1435  
1436          $rs = $DB->get_recordset_select($tablename, '');
1437          $counter = 0;
1438          foreach ($rs as $record) {
1439              $counter++;
1440          }
1441          $rs->close();
1442          $this->assertEquals(4, $counter);
1443  
1444          $this->assertNotEmpty($rs = $DB->get_recordset_select($tablename, 'course = 3'));
1445          $counter = 0;
1446          foreach ($rs as $record) {
1447              $counter++;
1448          }
1449          $rs->close();
1450          $this->assertEquals(2, $counter);
1451  
1452          // Notes:
1453          //  * limits are tested in test_get_recordset_sql()
1454      }
1455  
1456      public function test_get_recordset_sql() {
1457          $DB = $this->tdb;
1458          $dbman = $DB->get_manager();
1459  
1460          $table = $this->get_test_table();
1461          $tablename = $table->getName();
1462  
1463          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1464          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1465          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1466          $dbman->create_table($table);
1467  
1468          $inskey1 = $DB->insert_record($tablename, array('course' => 3));
1469          $inskey2 = $DB->insert_record($tablename, array('course' => 5));
1470          $inskey3 = $DB->insert_record($tablename, array('course' => 4));
1471          $inskey4 = $DB->insert_record($tablename, array('course' => 3));
1472          $inskey5 = $DB->insert_record($tablename, array('course' => 2));
1473          $inskey6 = $DB->insert_record($tablename, array('course' => 1));
1474          $inskey7 = $DB->insert_record($tablename, array('course' => 0));
1475  
1476          $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}} WHERE course = ?", array(3));
1477          $counter = 0;
1478          foreach ($rs as $record) {
1479              $counter++;
1480          }
1481          $rs->close();
1482          $this->assertEquals(2, $counter);
1483  
1484          // Limits - only need to test this case, the rest have been tested by test_get_records_sql()
1485          // only limitfrom = skips that number of records.
1486          $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}} ORDER BY id", null, 2, 0);
1487          $records = array();
1488          foreach ($rs as $key => $record) {
1489              $records[$key] = $record;
1490          }
1491          $rs->close();
1492          $this->assertCount(5, $records);
1493          $this->assertEquals($inskey3, reset($records)->id);
1494          $this->assertEquals($inskey7, end($records)->id);
1495  
1496          // Note: fetching nulls, empties, LOBs already tested by test_insert_record() no needed here.
1497      }
1498  
1499      public function test_export_table_recordset() {
1500          $DB = $this->tdb;
1501          $dbman = $DB->get_manager();
1502  
1503          $table = $this->get_test_table();
1504          $tablename = $table->getName();
1505  
1506          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1507          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1508          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1509          $dbman->create_table($table);
1510  
1511          $ids = array();
1512          $ids[] = $DB->insert_record($tablename, array('course' => 3));
1513          $ids[] = $DB->insert_record($tablename, array('course' => 5));
1514          $ids[] = $DB->insert_record($tablename, array('course' => 4));
1515          $ids[] = $DB->insert_record($tablename, array('course' => 3));
1516          $ids[] = $DB->insert_record($tablename, array('course' => 2));
1517          $ids[] = $DB->insert_record($tablename, array('course' => 1));
1518          $ids[] = $DB->insert_record($tablename, array('course' => 0));
1519  
1520          $rs = $DB->export_table_recordset($tablename);
1521          $rids = array();
1522          foreach ($rs as $record) {
1523              $rids[] = $record->id;
1524          }
1525          $rs->close();
1526          $this->assertEqualsCanonicalizing($ids, $rids);
1527      }
1528  
1529      public function test_get_records() {
1530          $DB = $this->tdb;
1531          $dbman = $DB->get_manager();
1532  
1533          $table = $this->get_test_table();
1534          $tablename = $table->getName();
1535  
1536          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1537          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1538          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
1539          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1540          $dbman->create_table($table);
1541  
1542          $DB->insert_record($tablename, array('course' => 3));
1543          $DB->insert_record($tablename, array('course' => 3));
1544          $DB->insert_record($tablename, array('course' => 5));
1545          $DB->insert_record($tablename, array('course' => 2));
1546  
1547          // All records.
1548          $records = $DB->get_records($tablename);
1549          $this->assertCount(4, $records);
1550          $this->assertEquals(3, $records[1]->course);
1551          $this->assertEquals(3, $records[2]->course);
1552          $this->assertEquals(5, $records[3]->course);
1553          $this->assertEquals(2, $records[4]->course);
1554  
1555          // Records matching certain conditions.
1556          $records = $DB->get_records($tablename, array('course' => 3));
1557          $this->assertCount(2, $records);
1558          $this->assertEquals(3, $records[1]->course);
1559          $this->assertEquals(3, $records[2]->course);
1560  
1561          // All records sorted by course.
1562          $records = $DB->get_records($tablename, null, 'course');
1563          $this->assertCount(4, $records);
1564          $current_record = reset($records);
1565          $this->assertEquals(4, $current_record->id);
1566          $current_record = next($records);
1567          $this->assertEquals(1, $current_record->id);
1568          $current_record = next($records);
1569          $this->assertEquals(2, $current_record->id);
1570          $current_record = next($records);
1571          $this->assertEquals(3, $current_record->id);
1572  
1573          // All records, but get only one field.
1574          $records = $DB->get_records($tablename, null, '', 'id');
1575          $this->assertFalse(isset($records[1]->course));
1576          $this->assertTrue(isset($records[1]->id));
1577          $this->assertCount(4, $records);
1578  
1579          // Booleans into params.
1580          $records = $DB->get_records($tablename, array('course' => true));
1581          $this->assertCount(0, $records);
1582          $records = $DB->get_records($tablename, array('course' => false));
1583          $this->assertCount(0, $records);
1584  
1585          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
1586          $conditions = array('onetext' => '1');
1587          try {
1588              $records = $DB->get_records($tablename, $conditions);
1589              if (debugging()) {
1590                  // Only in debug mode - hopefully all devs test code in debug mode...
1591                  $this->fail('An Exception is missing, expected due to equating of text fields');
1592              }
1593          } catch (\moodle_exception $e) {
1594              $this->assertInstanceOf('dml_exception', $e);
1595              $this->assertSame('textconditionsnotallowed', $e->errorcode);
1596          }
1597  
1598          // Test get_records passing non-existing table.
1599          // with params.
1600          try {
1601              $records = $DB->get_records('xxxx', array('id' => 0));
1602              $this->fail('An Exception is missing, expected due to query against non-existing table');
1603          } catch (\moodle_exception $e) {
1604              $this->assertInstanceOf('dml_exception', $e);
1605              if (debugging()) {
1606                  // Information for developers only, normal users get general error message.
1607                  $this->assertSame('ddltablenotexist', $e->errorcode);
1608              }
1609          }
1610  
1611          try {
1612              $records = $DB->get_records('xxxx', array('id' => '1'));
1613              $this->fail('An Exception is missing, expected due to query against non-existing table');
1614          } catch (\moodle_exception $e) {
1615              $this->assertInstanceOf('dml_exception', $e);
1616              if (debugging()) {
1617                  // Information for developers only, normal users get general error message.
1618                  $this->assertSame('ddltablenotexist', $e->errorcode);
1619              }
1620          }
1621  
1622          // Test get_records passing non-existing column.
1623          try {
1624              $records = $DB->get_records($tablename, array('xxxx' => 0));
1625              $this->fail('An Exception is missing, expected due to query against non-existing column');
1626          } catch (\moodle_exception $e) {
1627              $this->assertInstanceOf('dml_exception', $e);
1628              if (debugging()) {
1629                  // Information for developers only, normal users get general error message.
1630                  $this->assertSame('ddlfieldnotexist', $e->errorcode);
1631              }
1632          }
1633  
1634          // Note: delegate limits testing to test_get_records_sql().
1635      }
1636  
1637      public function test_get_records_list() {
1638          $DB = $this->tdb;
1639          $dbman = $DB->get_manager();
1640  
1641          $table = $this->get_test_table();
1642          $tablename = $table->getName();
1643  
1644          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1645          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1646          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1647          $dbman->create_table($table);
1648  
1649          $DB->insert_record($tablename, array('course' => 3));
1650          $DB->insert_record($tablename, array('course' => 3));
1651          $DB->insert_record($tablename, array('course' => 5));
1652          $DB->insert_record($tablename, array('course' => 2));
1653  
1654          $records = $DB->get_records_list($tablename, 'course', array(3, 2));
1655          $this->assertIsArray($records);
1656          $this->assertCount(3, $records);
1657          $this->assertEquals(1, reset($records)->id);
1658          $this->assertEquals(2, next($records)->id);
1659          $this->assertEquals(4, next($records)->id);
1660  
1661          $this->assertSame(array(), $records = $DB->get_records_list($tablename, 'course', array())); // Must return 0 rows without conditions. MDL-17645.
1662          $this->assertCount(0, $records);
1663  
1664          // Note: delegate limits testing to test_get_records_sql().
1665      }
1666  
1667      public function test_get_records_sql() {
1668          $DB = $this->tdb;
1669          $dbman = $DB->get_manager();
1670  
1671          $table = $this->get_test_table();
1672          $tablename = $table->getName();
1673  
1674          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1675          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1676          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1677          $dbman->create_table($table);
1678  
1679          $inskey1 = $DB->insert_record($tablename, array('course' => 3));
1680          $inskey2 = $DB->insert_record($tablename, array('course' => 5));
1681          $inskey3 = $DB->insert_record($tablename, array('course' => 4));
1682          $inskey4 = $DB->insert_record($tablename, array('course' => 3));
1683          $inskey5 = $DB->insert_record($tablename, array('course' => 2));
1684          $inskey6 = $DB->insert_record($tablename, array('course' => 1));
1685          $inskey7 = $DB->insert_record($tablename, array('course' => 0));
1686  
1687          $table2 = $this->get_test_table("2");
1688          $tablename2 = $table2->getName();
1689          $table2->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1690          $table2->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1691          $table2->add_field('nametext', XMLDB_TYPE_TEXT, 'small', null, null, null, null);
1692          $table2->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1693          $dbman->create_table($table2);
1694  
1695          $DB->insert_record($tablename2, array('course'=>3, 'nametext'=>'badabing'));
1696          $DB->insert_record($tablename2, array('course'=>4, 'nametext'=>'badabang'));
1697          $DB->insert_record($tablename2, array('course'=>5, 'nametext'=>'badabung'));
1698          $DB->insert_record($tablename2, array('course'=>6, 'nametext'=>'badabong'));
1699  
1700          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE course = ?", array(3));
1701          $this->assertCount(2, $records);
1702          $this->assertEquals($inskey1, reset($records)->id);
1703          $this->assertEquals($inskey4, next($records)->id);
1704  
1705          // Awful test, requires debug enabled and sent to browser. Let's do that and restore after test.
1706          $records = $DB->get_records_sql("SELECT course AS id, course AS course FROM {{$tablename}}", null);
1707          $this->assertDebuggingCalled();
1708          $this->assertCount(6, $records);
1709          set_debugging(DEBUG_MINIMAL);
1710          $records = $DB->get_records_sql("SELECT course AS id, course AS course FROM {{$tablename}}", null);
1711          $this->assertDebuggingNotCalled();
1712          $this->assertCount(6, $records);
1713          set_debugging(DEBUG_DEVELOPER);
1714  
1715          // Negative limits = no limits.
1716          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} ORDER BY id", null, -1, -1);
1717          $this->assertCount(7, $records);
1718  
1719          // Zero limits = no limits.
1720          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} ORDER BY id", null, 0, 0);
1721          $this->assertCount(7, $records);
1722  
1723          // Only limitfrom = skips that number of records.
1724          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} ORDER BY id", null, 2, 0);
1725          $this->assertCount(5, $records);
1726          $this->assertEquals($inskey3, reset($records)->id);
1727          $this->assertEquals($inskey7, end($records)->id);
1728  
1729          // Only limitnum = fetches that number of records.
1730          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} ORDER BY id", null, 0, 3);
1731          $this->assertCount(3, $records);
1732          $this->assertEquals($inskey1, reset($records)->id);
1733          $this->assertEquals($inskey3, end($records)->id);
1734  
1735          // Both limitfrom and limitnum = skips limitfrom records and fetches limitnum ones.
1736          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} ORDER BY id", null, 3, 2);
1737          $this->assertCount(2, $records);
1738          $this->assertEquals($inskey4, reset($records)->id);
1739          $this->assertEquals($inskey5, end($records)->id);
1740  
1741          // Both limitfrom and limitnum in query having subqueris.
1742          // Note the subquery skips records with course = 0 and 3.
1743          $sql = "SELECT * FROM {{$tablename}}
1744                   WHERE course NOT IN (
1745                       SELECT course FROM {{$tablename}}
1746                        WHERE course IN (0, 3))
1747                  ORDER BY course";
1748          $records = $DB->get_records_sql($sql, null, 0, 2); // Skip 0, get 2.
1749          $this->assertCount(2, $records);
1750          $this->assertEquals($inskey6, reset($records)->id);
1751          $this->assertEquals($inskey5, end($records)->id);
1752          $records = $DB->get_records_sql($sql, null, 2, 2); // Skip 2, get 2.
1753          $this->assertCount(2, $records);
1754          $this->assertEquals($inskey3, reset($records)->id);
1755          $this->assertEquals($inskey2, end($records)->id);
1756  
1757          // Test 2 tables with aliases and limits with order bys.
1758          $sql = "SELECT t1.id, t1.course AS cid, t2.nametext
1759                    FROM {{$tablename}} t1, {{$tablename2}} t2
1760                   WHERE t2.course=t1.course
1761                ORDER BY t1.course, ". $DB->sql_compare_text('t2.nametext');
1762          $records = $DB->get_records_sql($sql, null, 2, 2); // Skip courses 3 and 6, get 4 and 5.
1763          $this->assertCount(2, $records);
1764          $this->assertSame('5', end($records)->cid);
1765          $this->assertSame('4', reset($records)->cid);
1766  
1767          // Test 2 tables with aliases and limits with the highest INT limit works.
1768          $records = $DB->get_records_sql($sql, null, 2, PHP_INT_MAX); // Skip course {3,6}, get {4,5}.
1769          $this->assertCount(2, $records);
1770          $this->assertSame('5', end($records)->cid);
1771          $this->assertSame('4', reset($records)->cid);
1772  
1773          // Test 2 tables with aliases and limits with order bys (limit which is highest INT number).
1774          $records = $DB->get_records_sql($sql, null, PHP_INT_MAX, 2); // Skip all courses.
1775          $this->assertCount(0, $records);
1776  
1777          // Test 2 tables with aliases and limits with order bys (limit which s highest INT number).
1778          $records = $DB->get_records_sql($sql, null, PHP_INT_MAX, PHP_INT_MAX); // Skip all courses.
1779          $this->assertCount(0, $records);
1780  
1781          // TODO: Test limits in queries having DISTINCT clauses.
1782  
1783          // Note: fetching nulls, empties, LOBs already tested by test_update_record() no needed here.
1784      }
1785  
1786      public function test_get_records_menu() {
1787          $DB = $this->tdb;
1788          $dbman = $DB->get_manager();
1789  
1790          $table = $this->get_test_table();
1791          $tablename = $table->getName();
1792  
1793          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1794          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1795          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1796          $dbman->create_table($table);
1797  
1798          $DB->insert_record($tablename, array('course' => 3));
1799          $DB->insert_record($tablename, array('course' => 3));
1800          $DB->insert_record($tablename, array('course' => 5));
1801          $DB->insert_record($tablename, array('course' => 2));
1802  
1803          $records = $DB->get_records_menu($tablename, array('course' => 3));
1804          $this->assertIsArray($records);
1805          $this->assertCount(2, $records);
1806          $this->assertNotEmpty($records[1]);
1807          $this->assertNotEmpty($records[2]);
1808          $this->assertEquals(3, $records[1]);
1809          $this->assertEquals(3, $records[2]);
1810  
1811          // Note: delegate limits testing to test_get_records_sql().
1812      }
1813  
1814      public function test_get_records_select_menu() {
1815          $DB = $this->tdb;
1816          $dbman = $DB->get_manager();
1817  
1818          $table = $this->get_test_table();
1819          $tablename = $table->getName();
1820  
1821          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1822          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1823          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1824          $dbman->create_table($table);
1825  
1826          $DB->insert_record($tablename, array('course' => 3));
1827          $DB->insert_record($tablename, array('course' => 2));
1828          $DB->insert_record($tablename, array('course' => 3));
1829          $DB->insert_record($tablename, array('course' => 5));
1830  
1831          $records = $DB->get_records_select_menu($tablename, "course > ?", array(2));
1832          $this->assertIsArray($records);
1833  
1834          $this->assertCount(3, $records);
1835          $this->assertArrayHasKey(1, $records);
1836          $this->assertArrayNotHasKey(2, $records);
1837          $this->assertArrayHasKey(3, $records);
1838          $this->assertArrayHasKey(4, $records);
1839          $this->assertSame('3', $records[1]);
1840          $this->assertSame('3', $records[3]);
1841          $this->assertSame('5', $records[4]);
1842  
1843          // Note: delegate limits testing to test_get_records_sql().
1844      }
1845  
1846      public function test_get_records_sql_menu() {
1847          $DB = $this->tdb;
1848          $dbman = $DB->get_manager();
1849  
1850          $table = $this->get_test_table();
1851          $tablename = $table->getName();
1852  
1853          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1854          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1855          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1856          $dbman->create_table($table);
1857  
1858          $DB->insert_record($tablename, array('course' => 3));
1859          $DB->insert_record($tablename, array('course' => 2));
1860          $DB->insert_record($tablename, array('course' => 3));
1861          $DB->insert_record($tablename, array('course' => 5));
1862  
1863          $records = $DB->get_records_sql_menu("SELECT * FROM {{$tablename}} WHERE course > ?", array(2));
1864          $this->assertIsArray($records);
1865  
1866          $this->assertCount(3, $records);
1867          $this->assertArrayHasKey(1, $records);
1868          $this->assertArrayNotHasKey(2, $records);
1869          $this->assertArrayHasKey(3, $records);
1870          $this->assertArrayHasKey(4, $records);
1871          $this->assertSame('3', $records[1]);
1872          $this->assertSame('3', $records[3]);
1873          $this->assertSame('5', $records[4]);
1874  
1875          // Note: delegate limits testing to test_get_records_sql().
1876      }
1877  
1878      public function test_get_record() {
1879          $DB = $this->tdb;
1880          $dbman = $DB->get_manager();
1881  
1882          $table = $this->get_test_table();
1883          $tablename = $table->getName();
1884  
1885          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1886          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1887          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1888          $dbman->create_table($table);
1889  
1890          $DB->insert_record($tablename, array('course' => 3));
1891          $DB->insert_record($tablename, array('course' => 2));
1892  
1893          $record = $DB->get_record($tablename, array('id' => 2));
1894          $this->assertInstanceOf(\stdClass::class, $record);
1895  
1896          $this->assertEquals(2, $record->course);
1897          $this->assertEquals(2, $record->id);
1898      }
1899  
1900  
1901      public function test_get_record_select() {
1902          $DB = $this->tdb;
1903          $dbman = $DB->get_manager();
1904  
1905          $table = $this->get_test_table();
1906          $tablename = $table->getName();
1907  
1908          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1909          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1910          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1911          $dbman->create_table($table);
1912  
1913          $DB->insert_record($tablename, array('course' => 3));
1914          $DB->insert_record($tablename, array('course' => 2));
1915  
1916          $record = $DB->get_record_select($tablename, "id = ?", array(2));
1917          $this->assertInstanceOf(\stdClass::class, $record);
1918  
1919          $this->assertEquals(2, $record->course);
1920  
1921          // Note: delegates limit testing to test_get_records_sql().
1922      }
1923  
1924      public function test_get_record_sql() {
1925          $DB = $this->tdb;
1926          $dbman = $DB->get_manager();
1927  
1928          $table = $this->get_test_table();
1929          $tablename = $table->getName();
1930  
1931          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1932          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1933          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1934          $dbman->create_table($table);
1935  
1936          $DB->insert_record($tablename, array('course' => 3));
1937          $DB->insert_record($tablename, array('course' => 2));
1938  
1939          // Standard use.
1940          $record = $DB->get_record_sql("SELECT * FROM {{$tablename}} WHERE id = ?", array(2));
1941          $this->assertInstanceOf(\stdClass::class, $record);
1942          $this->assertEquals(2, $record->course);
1943          $this->assertEquals(2, $record->id);
1944  
1945          // Backwards compatibility with $ignoremultiple.
1946          $this->assertFalse((bool)IGNORE_MISSING);
1947          $this->assertTrue((bool)IGNORE_MULTIPLE);
1948  
1949          // Record not found - ignore.
1950          $this->assertFalse($DB->get_record_sql("SELECT * FROM {{$tablename}} WHERE id = ?", array(666), IGNORE_MISSING));
1951          $this->assertFalse($DB->get_record_sql("SELECT * FROM {{$tablename}} WHERE id = ?", array(666), IGNORE_MULTIPLE));
1952  
1953          // Record not found error.
1954          try {
1955              $DB->get_record_sql("SELECT * FROM {{$tablename}} WHERE id = ?", array(666), MUST_EXIST);
1956              $this->fail("Exception expected");
1957          } catch (dml_missing_record_exception $e) {
1958              $this->assertTrue(true);
1959          }
1960  
1961          $this->assertNotEmpty($DB->get_record_sql("SELECT * FROM {{$tablename}}", array(), IGNORE_MISSING));
1962          $this->assertDebuggingCalled();
1963          set_debugging(DEBUG_MINIMAL);
1964          $this->assertNotEmpty($DB->get_record_sql("SELECT * FROM {{$tablename}}", array(), IGNORE_MISSING));
1965          $this->assertDebuggingNotCalled();
1966          set_debugging(DEBUG_DEVELOPER);
1967  
1968          // Multiple matches ignored.
1969          $this->assertNotEmpty($DB->get_record_sql("SELECT * FROM {{$tablename}}", array(), IGNORE_MULTIPLE));
1970  
1971          // Multiple found error.
1972          try {
1973              $DB->get_record_sql("SELECT * FROM {{$tablename}}", array(), MUST_EXIST);
1974              $this->fail("Exception expected");
1975          } catch (dml_multiple_records_exception $e) {
1976              $this->assertTrue(true);
1977          }
1978      }
1979  
1980      public function test_get_field() {
1981          $DB = $this->tdb;
1982          $dbman = $DB->get_manager();
1983  
1984          $table = $this->get_test_table();
1985          $tablename = $table->getName();
1986  
1987          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1988          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
1989          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
1990          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1991          $dbman->create_table($table);
1992  
1993          $id1 = $DB->insert_record($tablename, array('course' => 3));
1994          $DB->insert_record($tablename, array('course' => 5));
1995          $DB->insert_record($tablename, array('course' => 5));
1996  
1997          $this->assertEquals(3, $DB->get_field($tablename, 'course', array('id' => $id1)));
1998          $this->assertEquals(3, $DB->get_field($tablename, 'course', array('course' => 3)));
1999  
2000          $this->assertFalse($DB->get_field($tablename, 'course', array('course' => 11), IGNORE_MISSING));
2001          try {
2002              $DB->get_field($tablename, 'course', array('course' => 4), MUST_EXIST);
2003              $this->fail('Exception expected due to missing record');
2004          } catch (dml_exception $ex) {
2005              $this->assertTrue(true);
2006          }
2007  
2008          $this->assertEquals(5, $DB->get_field($tablename, 'course', array('course' => 5), IGNORE_MULTIPLE));
2009          $this->assertDebuggingNotCalled();
2010  
2011          $this->assertEquals(5, $DB->get_field($tablename, 'course', array('course' => 5), IGNORE_MISSING));
2012          $this->assertDebuggingCalled();
2013  
2014          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
2015          $conditions = array('onetext' => '1');
2016          try {
2017              $DB->get_field($tablename, 'course', $conditions);
2018              if (debugging()) {
2019                  // Only in debug mode - hopefully all devs test code in debug mode...
2020                  $this->fail('An Exception is missing, expected due to equating of text fields');
2021              }
2022          } catch (\moodle_exception $e) {
2023              $this->assertInstanceOf('dml_exception', $e);
2024              $this->assertSame('textconditionsnotallowed', $e->errorcode);
2025          }
2026      }
2027  
2028      public function test_get_field_select() {
2029          $DB = $this->tdb;
2030          $dbman = $DB->get_manager();
2031  
2032          $table = $this->get_test_table();
2033          $tablename = $table->getName();
2034  
2035          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2036          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2037          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2038          $dbman->create_table($table);
2039  
2040          $DB->insert_record($tablename, array('course' => 3));
2041  
2042          $this->assertEquals(3, $DB->get_field_select($tablename, 'course', "id = ?", array(1)));
2043      }
2044  
2045      public function test_get_field_sql() {
2046          $DB = $this->tdb;
2047          $dbman = $DB->get_manager();
2048  
2049          $table = $this->get_test_table();
2050          $tablename = $table->getName();
2051  
2052          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2053          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2054          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2055          $dbman->create_table($table);
2056  
2057          $DB->insert_record($tablename, array('course' => 3));
2058  
2059          $this->assertEquals(3, $DB->get_field_sql("SELECT course FROM {{$tablename}} WHERE id = ?", array(1)));
2060      }
2061  
2062      public function test_get_fieldset_select() {
2063          $DB = $this->tdb;
2064          $dbman = $DB->get_manager();
2065  
2066          $table = $this->get_test_table();
2067          $tablename = $table->getName();
2068  
2069          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2070          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2071          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2072          $dbman->create_table($table);
2073  
2074          $DB->insert_record($tablename, array('course' => 1));
2075          $DB->insert_record($tablename, array('course' => 3));
2076          $DB->insert_record($tablename, array('course' => 2));
2077          $DB->insert_record($tablename, array('course' => 6));
2078  
2079          $fieldset = $DB->get_fieldset_select($tablename, 'course', "course > ?", array(1));
2080          $this->assertIsArray($fieldset);
2081  
2082          $this->assertCount(3, $fieldset);
2083          $this->assertEquals(3, $fieldset[0]);
2084          $this->assertEquals(2, $fieldset[1]);
2085          $this->assertEquals(6, $fieldset[2]);
2086      }
2087  
2088      public function test_get_fieldset_sql() {
2089          $DB = $this->tdb;
2090          $dbman = $DB->get_manager();
2091  
2092          $table = $this->get_test_table();
2093          $tablename = $table->getName();
2094  
2095          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2096          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2097          $table->add_field('onebinary', XMLDB_TYPE_BINARY, 'big', null, null, null);
2098          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2099          $dbman->create_table($table);
2100  
2101          $binarydata = '\\'.chr(241);
2102  
2103          $DB->insert_record($tablename, array('course' => 1, 'onebinary' => $binarydata));
2104          $DB->insert_record($tablename, array('course' => 3, 'onebinary' => $binarydata));
2105          $DB->insert_record($tablename, array('course' => 2, 'onebinary' => $binarydata));
2106          $DB->insert_record($tablename, array('course' => 6, 'onebinary' => $binarydata));
2107  
2108          $fieldset = $DB->get_fieldset_sql("SELECT * FROM {{$tablename}} WHERE course > ?", array(1));
2109          $this->assertIsArray($fieldset);
2110  
2111          $this->assertCount(3, $fieldset);
2112          $this->assertEquals(2, $fieldset[0]);
2113          $this->assertEquals(3, $fieldset[1]);
2114          $this->assertEquals(4, $fieldset[2]);
2115  
2116          $fieldset = $DB->get_fieldset_sql("SELECT onebinary FROM {{$tablename}} WHERE course > ?", array(1));
2117          $this->assertIsArray($fieldset);
2118  
2119          $this->assertCount(3, $fieldset);
2120          $this->assertEquals($binarydata, $fieldset[0]);
2121          $this->assertEquals($binarydata, $fieldset[1]);
2122          $this->assertEquals($binarydata, $fieldset[2]);
2123      }
2124  
2125      public function test_insert_record_raw() {
2126          $DB = $this->tdb;
2127          $dbman = $DB->get_manager();
2128  
2129          $table = $this->get_test_table();
2130          $tablename = $table->getName();
2131  
2132          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2133          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2134          $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
2135          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2136          $dbman->create_table($table);
2137  
2138          $record = (object)array('course' => 1, 'onechar' => 'xx');
2139          $before = clone($record);
2140          $result = $DB->insert_record_raw($tablename, $record);
2141          $this->assertSame(1, $result);
2142          $this->assertEquals($record, $before);
2143  
2144          $record = $DB->get_record($tablename, array('course' => 1));
2145          $this->assertInstanceOf(\stdClass::class, $record);
2146          $this->assertSame('xx', $record->onechar);
2147  
2148          $result = $DB->insert_record_raw($tablename, array('course' => 2, 'onechar' => 'yy'), false);
2149          $this->assertTrue($result);
2150  
2151          // Note: bulk not implemented yet.
2152          $DB->insert_record_raw($tablename, array('course' => 3, 'onechar' => 'zz'), true, true);
2153          $record = $DB->get_record($tablename, array('course' => 3));
2154          $this->assertInstanceOf(\stdClass::class, $record);
2155          $this->assertSame('zz', $record->onechar);
2156  
2157          // Custom sequence (id) - returnid is ignored.
2158          $result = $DB->insert_record_raw($tablename, array('id' => 10, 'course' => 3, 'onechar' => 'bb'), true, false, true);
2159          $this->assertTrue($result);
2160          $record = $DB->get_record($tablename, array('id' => 10));
2161          $this->assertInstanceOf(\stdClass::class, $record);
2162          $this->assertSame('bb', $record->onechar);
2163  
2164          // Custom sequence - missing id error.
2165          try {
2166              $DB->insert_record_raw($tablename, array('course' => 3, 'onechar' => 'bb'), true, false, true);
2167              $this->fail('Exception expected due to missing record');
2168          } catch (\coding_exception $ex) {
2169              $this->assertTrue(true);
2170          }
2171  
2172          // Wrong column error.
2173          try {
2174              $DB->insert_record_raw($tablename, array('xxxxx' => 3, 'onechar' => 'bb'));
2175              $this->fail('Exception expected due to invalid column');
2176          } catch (dml_exception $ex) {
2177              $this->assertTrue(true);
2178          }
2179  
2180          // Create something similar to "context_temp" with id column without sequence.
2181          $dbman->drop_table($table);
2182          $table = $this->get_test_table();
2183          $tablename = $table->getName();
2184          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
2185          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2186          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2187          $dbman->create_table($table);
2188  
2189          $record = (object)array('id'=>5, 'course' => 1);
2190          $DB->insert_record_raw($tablename, $record, false, false, true);
2191          $record = $DB->get_record($tablename, array());
2192          $this->assertEquals(5, $record->id);
2193      }
2194  
2195      public function test_insert_record() {
2196          // All the information in this test is fetched from DB by get_recordset() so we
2197          // have such method properly tested against nulls, empties and friends...
2198  
2199          $DB = $this->tdb;
2200          $dbman = $DB->get_manager();
2201  
2202          $table = $this->get_test_table();
2203          $tablename = $table->getName();
2204  
2205          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2206          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2207          $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
2208          $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
2209          $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
2210          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
2211          $table->add_field('onebinary', XMLDB_TYPE_BINARY, 'big', null, null, null);
2212          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2213          $dbman->create_table($table);
2214  
2215          $this->assertSame(1, $DB->insert_record($tablename, array('course' => 1), true));
2216          $record = $DB->get_record($tablename, array('course' => 1));
2217          $this->assertEquals(1, $record->id);
2218          $this->assertEquals(100, $record->oneint); // Just check column defaults have been applied.
2219          $this->assertEquals(200, $record->onenum);
2220          $this->assertSame('onestring', $record->onechar);
2221          $this->assertNull($record->onetext);
2222          $this->assertNull($record->onebinary);
2223  
2224          // Without returning id, bulk not implemented.
2225          $result = $this->assertTrue($DB->insert_record($tablename, array('course' => 99), false, true));
2226          $record = $DB->get_record($tablename, array('course' => 99));
2227          $this->assertEquals(2, $record->id);
2228          $this->assertEquals(99, $record->course);
2229  
2230          // Check nulls are set properly for all types.
2231          $record = new \stdClass();
2232          $record->oneint = null;
2233          $record->onenum = null;
2234          $record->onechar = null;
2235          $record->onetext = null;
2236          $record->onebinary = null;
2237          $recid = $DB->insert_record($tablename, $record);
2238          $record = $DB->get_record($tablename, array('id' => $recid));
2239          $this->assertEquals(0, $record->course);
2240          $this->assertNull($record->oneint);
2241          $this->assertNull($record->onenum);
2242          $this->assertNull($record->onechar);
2243          $this->assertNull($record->onetext);
2244          $this->assertNull($record->onebinary);
2245  
2246          // Check zeros are set properly for all types.
2247          $record = new \stdClass();
2248          $record->oneint = 0;
2249          $record->onenum = 0;
2250          $recid = $DB->insert_record($tablename, $record);
2251          $record = $DB->get_record($tablename, array('id' => $recid));
2252          $this->assertEquals(0, $record->oneint);
2253          $this->assertEquals(0, $record->onenum);
2254  
2255          // Check booleans are set properly for all types.
2256          $record = new \stdClass();
2257          $record->oneint = true; // Trues.
2258          $record->onenum = true;
2259          $record->onechar = true;
2260          $record->onetext = true;
2261          $recid = $DB->insert_record($tablename, $record);
2262          $record = $DB->get_record($tablename, array('id' => $recid));
2263          $this->assertEquals(1, $record->oneint);
2264          $this->assertEquals(1, $record->onenum);
2265          $this->assertEquals(1, $record->onechar);
2266          $this->assertEquals(1, $record->onetext);
2267  
2268          $record = new \stdClass();
2269          $record->oneint = false; // Falses.
2270          $record->onenum = false;
2271          $record->onechar = false;
2272          $record->onetext = false;
2273          $recid = $DB->insert_record($tablename, $record);
2274          $record = $DB->get_record($tablename, array('id' => $recid));
2275          $this->assertEquals(0, $record->oneint);
2276          $this->assertEquals(0, $record->onenum);
2277          $this->assertEquals(0, $record->onechar);
2278          $this->assertEquals(0, $record->onetext);
2279  
2280          // Check string data causes exception in numeric types.
2281          $record = new \stdClass();
2282          $record->oneint = 'onestring';
2283          $record->onenum = 0;
2284          try {
2285              $DB->insert_record($tablename, $record);
2286              $this->fail("Expecting an exception, none occurred");
2287          } catch (\moodle_exception $e) {
2288              $this->assertInstanceOf('dml_exception', $e);
2289          }
2290          $record = new \stdClass();
2291          $record->oneint = 0;
2292          $record->onenum = 'onestring';
2293          try {
2294              $DB->insert_record($tablename, $record);
2295              $this->fail("Expecting an exception, none occurred");
2296          } catch (\moodle_exception $e) {
2297              $this->assertInstanceOf('dml_exception', $e);
2298          }
2299  
2300          // Check empty string data is stored as 0 in numeric datatypes.
2301          $record = new \stdClass();
2302          $record->oneint = ''; // Empty string.
2303          $record->onenum = 0;
2304          $recid = $DB->insert_record($tablename, $record);
2305          $record = $DB->get_record($tablename, array('id' => $recid));
2306          $this->assertTrue(is_numeric($record->oneint) && $record->oneint == 0);
2307  
2308          $record = new \stdClass();
2309          $record->oneint = 0;
2310          $record->onenum = ''; // Empty string.
2311          $recid = $DB->insert_record($tablename, $record);
2312          $record = $DB->get_record($tablename, array('id' => $recid));
2313          $this->assertTrue(is_numeric($record->onenum) && $record->onenum == 0);
2314  
2315          // Check empty strings are set properly in string types.
2316          $record = new \stdClass();
2317          $record->oneint = 0;
2318          $record->onenum = 0;
2319          $record->onechar = '';
2320          $record->onetext = '';
2321          $recid = $DB->insert_record($tablename, $record);
2322          $record = $DB->get_record($tablename, array('id' => $recid));
2323          $this->assertTrue($record->onechar === '');
2324          $this->assertTrue($record->onetext === '');
2325  
2326          // Check operation ((210.10 + 39.92) - 150.02) against numeric types.
2327          $record = new \stdClass();
2328          $record->oneint = ((210.10 + 39.92) - 150.02);
2329          $record->onenum = ((210.10 + 39.92) - 150.02);
2330          $recid = $DB->insert_record($tablename, $record);
2331          $record = $DB->get_record($tablename, array('id' => $recid));
2332          $this->assertEquals(100, $record->oneint);
2333          $this->assertEquals(100, $record->onenum);
2334  
2335          // Check various quotes/backslashes combinations in string types.
2336          $teststrings = array(
2337              'backslashes and quotes alone (even): "" \'\' \\\\',
2338              'backslashes and quotes alone (odd): """ \'\'\' \\\\\\',
2339              'backslashes and quotes sequences (even): \\"\\" \\\'\\\'',
2340              'backslashes and quotes sequences (odd): \\"\\"\\" \\\'\\\'\\\'');
2341          foreach ($teststrings as $teststring) {
2342              $record = new \stdClass();
2343              $record->onechar = $teststring;
2344              $record->onetext = $teststring;
2345              $recid = $DB->insert_record($tablename, $record);
2346              $record = $DB->get_record($tablename, array('id' => $recid));
2347              $this->assertEquals($teststring, $record->onechar);
2348              $this->assertEquals($teststring, $record->onetext);
2349          }
2350  
2351          // Check LOBs in text/binary columns.
2352          $clob = file_get_contents(__DIR__ . '/fixtures/clob.txt');
2353          $blob = file_get_contents(__DIR__ . '/fixtures/randombinary');
2354          $record = new \stdClass();
2355          $record->onetext = $clob;
2356          $record->onebinary = $blob;
2357          $recid = $DB->insert_record($tablename, $record);
2358          $rs = $DB->get_recordset($tablename, array('id' => $recid));
2359          $record = $rs->current();
2360          $rs->close();
2361          $this->assertEquals($clob, $record->onetext, 'Test CLOB insert (full contents output disabled)');
2362          $this->assertEquals($blob, $record->onebinary, 'Test BLOB insert (full contents output disabled)');
2363  
2364          // And "small" LOBs too, just in case.
2365          $newclob = substr($clob, 0, 500);
2366          $newblob = substr($blob, 0, 250);
2367          $record = new \stdClass();
2368          $record->onetext = $newclob;
2369          $record->onebinary = $newblob;
2370          $recid = $DB->insert_record($tablename, $record);
2371          $rs = $DB->get_recordset($tablename, array('id' => $recid));
2372          $record = $rs->current();
2373          $rs->close();
2374          $this->assertEquals($newclob, $record->onetext, 'Test "small" CLOB insert (full contents output disabled)');
2375          $this->assertEquals($newblob, $record->onebinary, 'Test "small" BLOB insert (full contents output disabled)');
2376          $this->assertEquals(false, $rs->key()); // Ensure recordset key() method to be working ok after closing.
2377  
2378          // And "diagnostic" LOBs too, just in case.
2379          $newclob = '\'"\\;/ěščřžýáíé';
2380          $newblob = '\'"\\;/ěščřžýáíé';
2381          $record = new \stdClass();
2382          $record->onetext = $newclob;
2383          $record->onebinary = $newblob;
2384          $recid = $DB->insert_record($tablename, $record);
2385          $rs = $DB->get_recordset($tablename, array('id' => $recid));
2386          $record = $rs->current();
2387          $rs->close();
2388          $this->assertSame($newclob, $record->onetext);
2389          $this->assertSame($newblob, $record->onebinary);
2390          $this->assertEquals(false, $rs->key()); // Ensure recordset key() method to be working ok after closing.
2391  
2392          // Test data is not modified.
2393          $record = new \stdClass();
2394          $record->id     = -1; // Has to be ignored.
2395          $record->course = 3;
2396          $record->lalala = 'lalal'; // Unused.
2397          $before = clone($record);
2398          $DB->insert_record($tablename, $record);
2399          $this->assertEquals($record, $before);
2400  
2401          // Make sure the id is always increasing and never reuses the same id.
2402          $id1 = $DB->insert_record($tablename, array('course' => 3));
2403          $id2 = $DB->insert_record($tablename, array('course' => 3));
2404          $this->assertTrue($id1 < $id2);
2405          $DB->delete_records($tablename, array('id'=>$id2));
2406          $id3 = $DB->insert_record($tablename, array('course' => 3));
2407          $this->assertTrue($id2 < $id3);
2408          $DB->delete_records($tablename, array());
2409          $id4 = $DB->insert_record($tablename, array('course' => 3));
2410          $this->assertTrue($id3 < $id4);
2411  
2412          // Test saving a float in a CHAR column, and reading it back.
2413          $id = $DB->insert_record($tablename, array('onechar' => 1.0));
2414          $this->assertEquals(1.0, $DB->get_field($tablename, 'onechar', array('id' => $id)));
2415          $id = $DB->insert_record($tablename, array('onechar' => 1e20));
2416          $this->assertEquals(1e20, $DB->get_field($tablename, 'onechar', array('id' => $id)));
2417          $id = $DB->insert_record($tablename, array('onechar' => 1e-4));
2418          $this->assertEquals(1e-4, $DB->get_field($tablename, 'onechar', array('id' => $id)));
2419          $id = $DB->insert_record($tablename, array('onechar' => 1e-5));
2420          $this->assertEquals(1e-5, $DB->get_field($tablename, 'onechar', array('id' => $id)));
2421          $id = $DB->insert_record($tablename, array('onechar' => 1e-300));
2422          $this->assertEquals(1e-300, $DB->get_field($tablename, 'onechar', array('id' => $id)));
2423          $id = $DB->insert_record($tablename, array('onechar' => 1e300));
2424          $this->assertEquals(1e300, $DB->get_field($tablename, 'onechar', array('id' => $id)));
2425  
2426          // Test saving a float in a TEXT column, and reading it back.
2427          $id = $DB->insert_record($tablename, array('onetext' => 1.0));
2428          $this->assertEquals(1.0, $DB->get_field($tablename, 'onetext', array('id' => $id)));
2429          $id = $DB->insert_record($tablename, array('onetext' => 1e20));
2430          $this->assertEquals(1e20, $DB->get_field($tablename, 'onetext', array('id' => $id)));
2431          $id = $DB->insert_record($tablename, array('onetext' => 1e-4));
2432          $this->assertEquals(1e-4, $DB->get_field($tablename, 'onetext', array('id' => $id)));
2433          $id = $DB->insert_record($tablename, array('onetext' => 1e-5));
2434          $this->assertEquals(1e-5, $DB->get_field($tablename, 'onetext', array('id' => $id)));
2435          $id = $DB->insert_record($tablename, array('onetext' => 1e-300));
2436          $this->assertEquals(1e-300, $DB->get_field($tablename, 'onetext', array('id' => $id)));
2437          $id = $DB->insert_record($tablename, array('onetext' => 1e300));
2438          $this->assertEquals(1e300, $DB->get_field($tablename, 'onetext', array('id' => $id)));
2439  
2440          // Test that inserting data violating one unique key leads to error.
2441          // Empty the table completely.
2442          $this->assertTrue($DB->delete_records($tablename));
2443  
2444          // Add one unique constraint (index).
2445          $key = new xmldb_key('testuk', XMLDB_KEY_UNIQUE, array('course', 'oneint'));
2446          $dbman->add_key($table, $key);
2447  
2448          // Let's insert one record violating the constraint multiple times.
2449          $record = (object)array('course' => 1, 'oneint' => 1);
2450          $this->assertTrue($DB->insert_record($tablename, $record, false)); // Insert 1st. No problem expected.
2451  
2452          // Re-insert same record, not returning id. dml_exception expected.
2453          try {
2454              $DB->insert_record($tablename, $record, false);
2455              $this->fail("Expecting an exception, none occurred");
2456          } catch (\moodle_exception $e) {
2457              $this->assertInstanceOf('dml_exception', $e);
2458          }
2459  
2460          // Re-insert same record, returning id. dml_exception expected.
2461          try {
2462              $DB->insert_record($tablename, $record, true);
2463              $this->fail("Expecting an exception, none occurred");
2464          } catch (\moodle_exception $e) {
2465              $this->assertInstanceOf('dml_exception', $e);
2466          }
2467  
2468          // Try to insert a record into a non-existent table. dml_exception expected.
2469          try {
2470              $DB->insert_record('nonexistenttable', $record, true);
2471              $this->fail("Expecting an exception, none occurred");
2472          } catch (\Exception $e) {
2473              $this->assertTrue($e instanceof dml_exception);
2474          }
2475      }
2476  
2477      public function test_insert_records() {
2478          $DB = $this->tdb;
2479          $dbman = $DB->get_manager();
2480  
2481          $table = $this->get_test_table();
2482          $tablename = $table->getName();
2483  
2484          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2485          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2486          $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
2487          $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
2488          $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
2489          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
2490          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2491          $dbman->create_table($table);
2492  
2493          $this->assertCount(0, $DB->get_records($tablename));
2494  
2495          $record = new \stdClass();
2496          $record->id = '1';
2497          $record->course = '1';
2498          $record->oneint = null;
2499          $record->onenum = 1.0;
2500          $record->onechar = 'a';
2501          $record->onetext = 'aaa';
2502  
2503          $expected = array();
2504          $records = array();
2505          for ($i = 1; $i <= 2000; $i++) { // This may take a while, it should be higher than defaults in DML drivers.
2506              $rec = clone($record);
2507              $rec->id = (string)$i;
2508              $rec->oneint = (string)$i;
2509              $expected[$i] = $rec;
2510              $rec = clone($rec);
2511              unset($rec->id);
2512              $records[$i] = $rec;
2513          }
2514  
2515          $DB->insert_records($tablename, $records);
2516          $stored = $DB->get_records($tablename, array(), 'id ASC');
2517          $this->assertEquals($expected, $stored);
2518  
2519          // Test there can be some extra properties including id.
2520          $count = $DB->count_records($tablename);
2521          $rec1 = (array)$record;
2522          $rec1['xxx'] = 1;
2523          $rec2 = (array)$record;
2524          $rec2['xxx'] = 2;
2525  
2526          $records = array($rec1, $rec2);
2527          $DB->insert_records($tablename, $records);
2528          $this->assertEquals($count + 2, $DB->count_records($tablename));
2529  
2530          // Test not all properties are necessary.
2531          $rec1 = (array)$record;
2532          unset($rec1['course']);
2533          $rec2 = (array)$record;
2534          unset($rec2['course']);
2535  
2536          $records = array($rec1, $rec2);
2537          $DB->insert_records($tablename, $records);
2538  
2539          // Make sure no changes in data object structure are tolerated.
2540          $rec1 = (array)$record;
2541          unset($rec1['id']);
2542          $rec2 = (array)$record;
2543          unset($rec2['id']);
2544  
2545          $records = array($rec1, $rec2);
2546          $DB->insert_records($tablename, $records);
2547  
2548          $rec2['xx'] = '1';
2549          $records = array($rec1, $rec2);
2550          try {
2551              $DB->insert_records($tablename, $records);
2552              $this->fail('coding_exception expected when insert_records receives different object data structures');
2553          } catch (\moodle_exception $e) {
2554              $this->assertInstanceOf('coding_exception', $e);
2555          }
2556  
2557          unset($rec2['xx']);
2558          unset($rec2['course']);
2559          $rec2['course'] = '1';
2560          $records = array($rec1, $rec2);
2561          try {
2562              $DB->insert_records($tablename, $records);
2563              $this->fail('coding_exception expected when insert_records receives different object data structures');
2564          } catch (\moodle_exception $e) {
2565              $this->assertInstanceOf('coding_exception', $e);
2566          }
2567  
2568          $records = 1;
2569          try {
2570              $DB->insert_records($tablename, $records);
2571              $this->fail('coding_exception expected when insert_records receives non-traversable data');
2572          } catch (\moodle_exception $e) {
2573              $this->assertInstanceOf('coding_exception', $e);
2574          }
2575  
2576          $records = array(1);
2577          try {
2578              $DB->insert_records($tablename, $records);
2579              $this->fail('coding_exception expected when insert_records receives non-objet record');
2580          } catch (\moodle_exception $e) {
2581              $this->assertInstanceOf('coding_exception', $e);
2582          }
2583      }
2584  
2585      public function test_insert_record_with_nullable_unique_index() {
2586          $DB = $this->tdb;
2587          $dbman = $DB->get_manager();
2588  
2589          $table = $this->get_test_table();
2590          $tablename = $table->getName();
2591  
2592          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2593          $table->add_field('notnull1', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2594          $table->add_field('nullable1', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
2595          $table->add_field('nullable2', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
2596          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2597          $table->add_index('notnull1-nullable1-nullable2', XMLDB_INDEX_UNIQUE,
2598                  array('notnull1', 'nullable1', 'nullable2'));
2599          $dbman->create_table($table);
2600  
2601          // Insert one record. Should be OK (no exception).
2602          $DB->insert_record($tablename, (object) ['notnull1' => 1, 'nullable1' => 1, 'nullable2' => 1]);
2603  
2604          $this->assertEquals(1, $DB->count_records($table->getName()));
2605          $this->assertEquals(1, $DB->count_records($table->getName(), ['nullable1' => 1]));
2606  
2607          // Inserting a duplicate should fail.
2608          try {
2609              $DB->insert_record($tablename, (object) ['notnull1' => 1, 'nullable1' => 1, 'nullable2' => 1]);
2610              $this->fail('dml_write_exception expected when a record violates a unique index');
2611          } catch (\moodle_exception $e) {
2612              $this->assertInstanceOf('dml_write_exception', $e);
2613          }
2614  
2615          $this->assertEquals(1, $DB->count_records($table->getName()));
2616          $this->assertEquals(1, $DB->count_records($table->getName(), ['nullable1' => 1]));
2617  
2618          // Inserting a record with nulls in the nullable columns should work.
2619          $DB->insert_record($tablename, (object) ['notnull1' => 1, 'nullable1' => null, 'nullable2' => null]);
2620  
2621          $this->assertEquals(2, $DB->count_records($table->getName()));
2622          $this->assertEquals(1, $DB->count_records($table->getName(), ['nullable1' => 1]));
2623          $this->assertEquals(1, $DB->count_records($table->getName(), ['nullable1' => null]));
2624  
2625          // And it should be possible to insert a duplicate.
2626          $DB->insert_record($tablename, (object) ['notnull1' => 1, 'nullable1' => null, 'nullable2' => null]);
2627  
2628          $this->assertEquals(3, $DB->count_records($table->getName()));
2629          $this->assertEquals(1, $DB->count_records($table->getName(), ['nullable1' => 1]));
2630          $this->assertEquals(2, $DB->count_records($table->getName(), ['nullable1' => null]));
2631  
2632          // Same, but with only one of the nullable columns being null.
2633          $DB->insert_record($tablename, (object) ['notnull1' => 1, 'nullable1' => 1, 'nullable2' => null]);
2634  
2635          $this->assertEquals(4, $DB->count_records($table->getName()));
2636          $this->assertEquals(2, $DB->count_records($table->getName(), ['nullable1' => 1]));
2637          $this->assertEquals(2, $DB->count_records($table->getName(), ['nullable1' => null]));
2638  
2639          $DB->insert_record($tablename, (object) ['notnull1' => 1, 'nullable1' => 1, 'nullable2' => null]);
2640  
2641          $this->assertEquals(5, $DB->count_records($table->getName()));
2642          $this->assertEquals(3, $DB->count_records($table->getName(), ['nullable1' => 1]));
2643          $this->assertEquals(2, $DB->count_records($table->getName(), ['nullable1' => null]));
2644  
2645      }
2646  
2647      public function test_import_record() {
2648          // All the information in this test is fetched from DB by get_recordset() so we
2649          // have such method properly tested against nulls, empties and friends...
2650  
2651          $DB = $this->tdb;
2652          $dbman = $DB->get_manager();
2653  
2654          $table = $this->get_test_table();
2655          $tablename = $table->getName();
2656  
2657          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2658          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2659          $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
2660          $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
2661          $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
2662          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
2663          $table->add_field('onebinary', XMLDB_TYPE_BINARY, 'big', null, null, null);
2664          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2665          $dbman->create_table($table);
2666  
2667          $this->assertSame(1, $DB->insert_record($tablename, array('course' => 1), true));
2668          $record = $DB->get_record($tablename, array('course' => 1));
2669          $this->assertEquals(1, $record->id);
2670          $this->assertEquals(100, $record->oneint); // Just check column defaults have been applied.
2671          $this->assertEquals(200, $record->onenum);
2672          $this->assertSame('onestring', $record->onechar);
2673          $this->assertNull($record->onetext);
2674          $this->assertNull($record->onebinary);
2675  
2676          // Ignore extra columns.
2677          $record = (object)array('id'=>13, 'course'=>2, 'xxxx'=>788778);
2678          $before = clone($record);
2679          $this->assertTrue($DB->import_record($tablename, $record));
2680          $this->assertEquals($record, $before);
2681          $records = $DB->get_records($tablename);
2682          $this->assertEquals(2, $records[13]->course);
2683  
2684          // Check nulls are set properly for all types.
2685          $record = new \stdClass();
2686          $record->id = 20;
2687          $record->oneint = null;
2688          $record->onenum = null;
2689          $record->onechar = null;
2690          $record->onetext = null;
2691          $record->onebinary = null;
2692          $this->assertTrue($DB->import_record($tablename, $record));
2693          $record = $DB->get_record($tablename, array('id' => 20));
2694          $this->assertEquals(0, $record->course);
2695          $this->assertNull($record->oneint);
2696          $this->assertNull($record->onenum);
2697          $this->assertNull($record->onechar);
2698          $this->assertNull($record->onetext);
2699          $this->assertNull($record->onebinary);
2700  
2701          // Check zeros are set properly for all types.
2702          $record = new \stdClass();
2703          $record->id = 23;
2704          $record->oneint = 0;
2705          $record->onenum = 0;
2706          $this->assertTrue($DB->import_record($tablename, $record));
2707          $record = $DB->get_record($tablename, array('id' => 23));
2708          $this->assertEquals(0, $record->oneint);
2709          $this->assertEquals(0, $record->onenum);
2710  
2711          // Check string data causes exception in numeric types.
2712          $record = new \stdClass();
2713          $record->id = 32;
2714          $record->oneint = 'onestring';
2715          $record->onenum = 0;
2716          try {
2717              $DB->import_record($tablename, $record);
2718              $this->fail("Expecting an exception, none occurred");
2719          } catch (\moodle_exception $e) {
2720              $this->assertInstanceOf('dml_exception', $e);
2721          }
2722          $record = new \stdClass();
2723          $record->id = 35;
2724          $record->oneint = 0;
2725          $record->onenum = 'onestring';
2726          try {
2727              $DB->import_record($tablename, $record);
2728              $this->fail("Expecting an exception, none occurred");
2729          } catch (\moodle_exception $e) {
2730              $this->assertInstanceOf('dml_exception', $e);
2731          }
2732  
2733          // Check empty strings are set properly in string types.
2734          $record = new \stdClass();
2735          $record->id = 44;
2736          $record->oneint = 0;
2737          $record->onenum = 0;
2738          $record->onechar = '';
2739          $record->onetext = '';
2740          $this->assertTrue($DB->import_record($tablename, $record));
2741          $record = $DB->get_record($tablename, array('id' => 44));
2742          $this->assertTrue($record->onechar === '');
2743          $this->assertTrue($record->onetext === '');
2744  
2745          // Check operation ((210.10 + 39.92) - 150.02) against numeric types.
2746          $record = new \stdClass();
2747          $record->id = 47;
2748          $record->oneint = ((210.10 + 39.92) - 150.02);
2749          $record->onenum = ((210.10 + 39.92) - 150.02);
2750          $this->assertTrue($DB->import_record($tablename, $record));
2751          $record = $DB->get_record($tablename, array('id' => 47));
2752          $this->assertEquals(100, $record->oneint);
2753          $this->assertEquals(100, $record->onenum);
2754  
2755          // Check various quotes/backslashes combinations in string types.
2756          $i = 50;
2757          $teststrings = array(
2758              'backslashes and quotes alone (even): "" \'\' \\\\',
2759              'backslashes and quotes alone (odd): """ \'\'\' \\\\\\',
2760              'backslashes and quotes sequences (even): \\"\\" \\\'\\\'',
2761              'backslashes and quotes sequences (odd): \\"\\"\\" \\\'\\\'\\\'');
2762          foreach ($teststrings as $teststring) {
2763              $record = new \stdClass();
2764              $record->id = $i;
2765              $record->onechar = $teststring;
2766              $record->onetext = $teststring;
2767              $this->assertTrue($DB->import_record($tablename, $record));
2768              $record = $DB->get_record($tablename, array('id' => $i));
2769              $this->assertEquals($teststring, $record->onechar);
2770              $this->assertEquals($teststring, $record->onetext);
2771              $i = $i + 3;
2772          }
2773  
2774          // Check LOBs in text/binary columns.
2775          $clob = file_get_contents(__DIR__ . '/fixtures/clob.txt');
2776          $record = new \stdClass();
2777          $record->id = 70;
2778          $record->onetext = $clob;
2779          $record->onebinary = '';
2780          $this->assertTrue($DB->import_record($tablename, $record));
2781          $rs = $DB->get_recordset($tablename, array('id' => 70));
2782          $record = $rs->current();
2783          $rs->close();
2784          $this->assertEquals($clob, $record->onetext, 'Test CLOB insert (full contents output disabled)');
2785  
2786          $blob = file_get_contents(__DIR__ . '/fixtures/randombinary');
2787          $record = new \stdClass();
2788          $record->id = 71;
2789          $record->onetext = '';
2790          $record->onebinary = $blob;
2791          $this->assertTrue($DB->import_record($tablename, $record));
2792          $rs = $DB->get_recordset($tablename, array('id' => 71));
2793          $record = $rs->current();
2794          $rs->close();
2795          $this->assertEquals($blob, $record->onebinary, 'Test BLOB insert (full contents output disabled)');
2796  
2797          // And "small" LOBs too, just in case.
2798          $newclob = substr($clob, 0, 500);
2799          $newblob = substr($blob, 0, 250);
2800          $record = new \stdClass();
2801          $record->id = 73;
2802          $record->onetext = $newclob;
2803          $record->onebinary = $newblob;
2804          $this->assertTrue($DB->import_record($tablename, $record));
2805          $rs = $DB->get_recordset($tablename, array('id' => 73));
2806          $record = $rs->current();
2807          $rs->close();
2808          $this->assertEquals($newclob, $record->onetext, 'Test "small" CLOB insert (full contents output disabled)');
2809          $this->assertEquals($newblob, $record->onebinary, 'Test "small" BLOB insert (full contents output disabled)');
2810          $this->assertEquals(false, $rs->key()); // Ensure recordset key() method to be working ok after closing.
2811      }
2812  
2813      public function test_update_record_raw() {
2814          $DB = $this->tdb;
2815          $dbman = $DB->get_manager();
2816  
2817          $table = $this->get_test_table();
2818          $tablename = $table->getName();
2819  
2820          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2821          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2822          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2823          $dbman->create_table($table);
2824  
2825          $DB->insert_record($tablename, array('course' => 1));
2826          $DB->insert_record($tablename, array('course' => 3));
2827  
2828          $record = $DB->get_record($tablename, array('course' => 1));
2829          $record->course = 2;
2830          $this->assertTrue($DB->update_record_raw($tablename, $record));
2831          $this->assertEquals(0, $DB->count_records($tablename, array('course' => 1)));
2832          $this->assertEquals(1, $DB->count_records($tablename, array('course' => 2)));
2833          $this->assertEquals(1, $DB->count_records($tablename, array('course' => 3)));
2834  
2835          $record = $DB->get_record($tablename, array('course' => 3));
2836          $record->xxxxx = 2;
2837          try {
2838              $DB->update_record_raw($tablename, $record);
2839              $this->fail("Expecting an exception, none occurred");
2840          } catch (\moodle_exception $e) {
2841              $this->assertInstanceOf('moodle_exception', $e);
2842          }
2843  
2844          $record = $DB->get_record($tablename, array('course' => 3));
2845          unset($record->id);
2846          try {
2847              $DB->update_record_raw($tablename, $record);
2848              $this->fail("Expecting an exception, none occurred");
2849          } catch (\moodle_exception $e) {
2850              $this->assertInstanceOf('coding_exception', $e);
2851          }
2852      }
2853  
2854      public function test_update_record() {
2855  
2856          // All the information in this test is fetched from DB by get_record() so we
2857          // have such method properly tested against nulls, empties and friends...
2858  
2859          $DB = $this->tdb;
2860          $dbman = $DB->get_manager();
2861  
2862          $table = $this->get_test_table();
2863          $tablename = $table->getName();
2864  
2865          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
2866          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
2867          $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
2868          $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
2869          $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
2870          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
2871          $table->add_field('onebinary', XMLDB_TYPE_BINARY, 'big', null, null, null);
2872          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
2873          $dbman->create_table($table);
2874  
2875          $DB->insert_record($tablename, array('course' => 1));
2876          $record = $DB->get_record($tablename, array('course' => 1));
2877          $record->course = 2;
2878  
2879          $this->assertTrue($DB->update_record($tablename, $record));
2880          $this->assertFalse($record = $DB->get_record($tablename, array('course' => 1)));
2881          $this->assertNotEmpty($record = $DB->get_record($tablename, array('course' => 2)));
2882          $this->assertEquals(100, $record->oneint); // Just check column defaults have been applied.
2883          $this->assertEquals(200, $record->onenum);
2884          $this->assertSame('onestring', $record->onechar);
2885          $this->assertNull($record->onetext);
2886          $this->assertNull($record->onebinary);
2887  
2888          // Check nulls are set properly for all types.
2889          $record->oneint = null;
2890          $record->onenum = null;
2891          $record->onechar = null;
2892          $record->onetext = null;
2893          $record->onebinary = null;
2894          $DB->update_record($tablename, $record);
2895          $record = $DB->get_record($tablename, array('course' => 2));
2896          $this->assertNull($record->oneint);
2897          $this->assertNull($record->onenum);
2898          $this->assertNull($record->onechar);
2899          $this->assertNull($record->onetext);
2900          $this->assertNull($record->onebinary);
2901  
2902          // Check zeros are set properly for all types.
2903          $record->oneint = 0;
2904          $record->onenum = 0;
2905          $DB->update_record($tablename, $record);
2906          $record = $DB->get_record($tablename, array('course' => 2));
2907          $this->assertEquals(0, $record->oneint);
2908          $this->assertEquals(0, $record->onenum);
2909  
2910          // Check booleans are set properly for all types.
2911          $record->oneint = true; // Trues.
2912          $record->onenum = true;
2913          $record->onechar = true;
2914          $record->onetext = true;
2915          $DB->update_record($tablename, $record);
2916          $record = $DB->get_record($tablename, array('course' => 2));
2917          $this->assertEquals(1, $record->oneint);
2918          $this->assertEquals(1, $record->onenum);
2919          $this->assertEquals(1, $record->onechar);
2920          $this->assertEquals(1, $record->onetext);
2921  
2922          $record->oneint = false; // Falses.
2923          $record->onenum = false;
2924          $record->onechar = false;
2925          $record->onetext = false;
2926          $DB->update_record($tablename, $record);
2927          $record = $DB->get_record($tablename, array('course' => 2));
2928          $this->assertEquals(0, $record->oneint);
2929          $this->assertEquals(0, $record->onenum);
2930          $this->assertEquals(0, $record->onechar);
2931          $this->assertEquals(0, $record->onetext);
2932  
2933          // Check string data causes exception in numeric types.
2934          $record->oneint = 'onestring';
2935          $record->onenum = 0;
2936          try {
2937              $DB->update_record($tablename, $record);
2938              $this->fail("Expecting an exception, none occurred");
2939          } catch (\moodle_exception $e) {
2940              $this->assertInstanceOf('dml_exception', $e);
2941          }
2942          $record->oneint = 0;
2943          $record->onenum = 'onestring';
2944          try {
2945              $DB->update_record($tablename, $record);
2946              $this->fail("Expecting an exception, none occurred");
2947          } catch (\moodle_exception $e) {
2948              $this->assertInstanceOf('dml_exception', $e);
2949          }
2950  
2951          // Check empty string data is stored as 0 in numeric datatypes.
2952          $record->oneint = ''; // Empty string.
2953          $record->onenum = 0;
2954          $DB->update_record($tablename, $record);
2955          $record = $DB->get_record($tablename, array('course' => 2));
2956          $this->assertTrue(is_numeric($record->oneint) && $record->oneint == 0);
2957  
2958          $record->oneint = 0;
2959          $record->onenum = ''; // Empty string.
2960          $DB->update_record($tablename, $record);
2961          $record = $DB->get_record($tablename, array('course' => 2));
2962          $this->assertTrue(is_numeric($record->onenum) && $record->onenum == 0);
2963  
2964          // Check empty strings are set properly in string types.
2965          $record->oneint = 0;
2966          $record->onenum = 0;
2967          $record->onechar = '';
2968          $record->onetext = '';
2969          $DB->update_record($tablename, $record);
2970          $record = $DB->get_record($tablename, array('course' => 2));
2971          $this->assertTrue($record->onechar === '');
2972          $this->assertTrue($record->onetext === '');
2973  
2974          // Check operation ((210.10 + 39.92) - 150.02) against numeric types.
2975          $record->oneint = ((210.10 + 39.92) - 150.02);
2976          $record->onenum = ((210.10 + 39.92) - 150.02);
2977          $DB->update_record($tablename, $record);
2978          $record = $DB->get_record($tablename, array('course' => 2));
2979          $this->assertEquals(100, $record->oneint);
2980          $this->assertEquals(100, $record->onenum);
2981  
2982          // Check various quotes/backslashes combinations in string types.
2983          $teststrings = array(
2984              'backslashes and quotes alone (even): "" \'\' \\\\',
2985              'backslashes and quotes alone (odd): """ \'\'\' \\\\\\',
2986              'backslashes and quotes sequences (even): \\"\\" \\\'\\\'',
2987              'backslashes and quotes sequences (odd): \\"\\"\\" \\\'\\\'\\\'');
2988          foreach ($teststrings as $teststring) {
2989              $record->onechar = $teststring;
2990              $record->onetext = $teststring;
2991              $DB->update_record($tablename, $record);
2992              $record = $DB->get_record($tablename, array('course' => 2));
2993              $this->assertEquals($teststring, $record->onechar);
2994              $this->assertEquals($teststring, $record->onetext);
2995          }
2996  
2997          // Check LOBs in text/binary columns.
2998          $clob = file_get_contents(__DIR__ . '/fixtures/clob.txt');
2999          $blob = file_get_contents(__DIR__ . '/fixtures/randombinary');
3000          $record->onetext = $clob;
3001          $record->onebinary = $blob;
3002          $DB->update_record($tablename, $record);
3003          $record = $DB->get_record($tablename, array('course' => 2));
3004          $this->assertEquals($clob, $record->onetext, 'Test CLOB update (full contents output disabled)');
3005          $this->assertEquals($blob, $record->onebinary, 'Test BLOB update (full contents output disabled)');
3006  
3007          // And "small" LOBs too, just in case.
3008          $newclob = substr($clob, 0, 500);
3009          $newblob = substr($blob, 0, 250);
3010          $record->onetext = $newclob;
3011          $record->onebinary = $newblob;
3012          $DB->update_record($tablename, $record);
3013          $record = $DB->get_record($tablename, array('course' => 2));
3014          $this->assertEquals($newclob, $record->onetext, 'Test "small" CLOB update (full contents output disabled)');
3015          $this->assertEquals($newblob, $record->onebinary, 'Test "small" BLOB update (full contents output disabled)');
3016  
3017          // Test saving a float in a CHAR column, and reading it back.
3018          $id = $DB->insert_record($tablename, array('onechar' => 'X'));
3019          $DB->update_record($tablename, array('id' => $id, 'onechar' => 1.0));
3020          $this->assertEquals(1.0, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3021          $DB->update_record($tablename, array('id' => $id, 'onechar' => 1e20));
3022          $this->assertEquals(1e20, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3023          $DB->update_record($tablename, array('id' => $id, 'onechar' => 1e-4));
3024          $this->assertEquals(1e-4, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3025          $DB->update_record($tablename, array('id' => $id, 'onechar' => 1e-5));
3026          $this->assertEquals(1e-5, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3027          $DB->update_record($tablename, array('id' => $id, 'onechar' => 1e-300));
3028          $this->assertEquals(1e-300, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3029          $DB->update_record($tablename, array('id' => $id, 'onechar' => 1e300));
3030          $this->assertEquals(1e300, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3031  
3032          // Test saving a float in a TEXT column, and reading it back.
3033          $id = $DB->insert_record($tablename, array('onetext' => 'X'));
3034          $DB->update_record($tablename, array('id' => $id, 'onetext' => 1.0));
3035          $this->assertEquals(1.0, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3036          $DB->update_record($tablename, array('id' => $id, 'onetext' => 1e20));
3037          $this->assertEquals(1e20, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3038          $DB->update_record($tablename, array('id' => $id, 'onetext' => 1e-4));
3039          $this->assertEquals(1e-4, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3040          $DB->update_record($tablename, array('id' => $id, 'onetext' => 1e-5));
3041          $this->assertEquals(1e-5, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3042          $DB->update_record($tablename, array('id' => $id, 'onetext' => 1e-300));
3043          $this->assertEquals(1e-300, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3044          $DB->update_record($tablename, array('id' => $id, 'onetext' => 1e300));
3045          $this->assertEquals(1e300, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3046      }
3047  
3048      public function test_set_field() {
3049          $DB = $this->tdb;
3050          $dbman = $DB->get_manager();
3051  
3052          $table = $this->get_test_table();
3053          $tablename = $table->getName();
3054  
3055          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3056          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3057          $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null);
3058          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
3059          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3060          $dbman->create_table($table);
3061  
3062          // Simple set_field.
3063          $id1 = $DB->insert_record($tablename, array('course' => 1));
3064          $id2 = $DB->insert_record($tablename, array('course' => 1));
3065          $id3 = $DB->insert_record($tablename, array('course' => 3));
3066          $this->assertTrue($DB->set_field($tablename, 'course', 2, array('id' => $id1)));
3067          $this->assertEquals(2, $DB->get_field($tablename, 'course', array('id' => $id1)));
3068          $this->assertEquals(1, $DB->get_field($tablename, 'course', array('id' => $id2)));
3069          $this->assertEquals(3, $DB->get_field($tablename, 'course', array('id' => $id3)));
3070          $DB->delete_records($tablename, array());
3071  
3072          // Multiple fields affected.
3073          $id1 = $DB->insert_record($tablename, array('course' => 1));
3074          $id2 = $DB->insert_record($tablename, array('course' => 1));
3075          $id3 = $DB->insert_record($tablename, array('course' => 3));
3076          $DB->set_field($tablename, 'course', '5', array('course' => 1));
3077          $this->assertEquals(5, $DB->get_field($tablename, 'course', array('id' => $id1)));
3078          $this->assertEquals(5, $DB->get_field($tablename, 'course', array('id' => $id2)));
3079          $this->assertEquals(3, $DB->get_field($tablename, 'course', array('id' => $id3)));
3080          $DB->delete_records($tablename, array());
3081  
3082          // No field affected.
3083          $id1 = $DB->insert_record($tablename, array('course' => 1));
3084          $id2 = $DB->insert_record($tablename, array('course' => 1));
3085          $id3 = $DB->insert_record($tablename, array('course' => 3));
3086          $DB->set_field($tablename, 'course', '5', array('course' => 0));
3087          $this->assertEquals(1, $DB->get_field($tablename, 'course', array('id' => $id1)));
3088          $this->assertEquals(1, $DB->get_field($tablename, 'course', array('id' => $id2)));
3089          $this->assertEquals(3, $DB->get_field($tablename, 'course', array('id' => $id3)));
3090          $DB->delete_records($tablename, array());
3091  
3092          // All fields - no condition.
3093          $id1 = $DB->insert_record($tablename, array('course' => 1));
3094          $id2 = $DB->insert_record($tablename, array('course' => 1));
3095          $id3 = $DB->insert_record($tablename, array('course' => 3));
3096          $DB->set_field($tablename, 'course', 5, array());
3097          $this->assertEquals(5, $DB->get_field($tablename, 'course', array('id' => $id1)));
3098          $this->assertEquals(5, $DB->get_field($tablename, 'course', array('id' => $id2)));
3099          $this->assertEquals(5, $DB->get_field($tablename, 'course', array('id' => $id3)));
3100  
3101          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
3102          $conditions = array('onetext' => '1');
3103          try {
3104              $DB->set_field($tablename, 'onechar', 'frog', $conditions);
3105              if (debugging()) {
3106                  // Only in debug mode - hopefully all devs test code in debug mode...
3107                  $this->fail('An Exception is missing, expected due to equating of text fields');
3108              }
3109          } catch (\moodle_exception $e) {
3110              $this->assertInstanceOf('dml_exception', $e);
3111              $this->assertSame('textconditionsnotallowed', $e->errorcode);
3112          }
3113  
3114          // Test saving a float in a CHAR column, and reading it back.
3115          $id = $DB->insert_record($tablename, array('onechar' => 'X'));
3116          $DB->set_field($tablename, 'onechar', 1.0, array('id' => $id));
3117          $this->assertEquals(1.0, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3118          $DB->set_field($tablename, 'onechar', 1e20, array('id' => $id));
3119          $this->assertEquals(1e20, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3120          $DB->set_field($tablename, 'onechar', 1e-4, array('id' => $id));
3121          $this->assertEquals(1e-4, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3122          $DB->set_field($tablename, 'onechar', 1e-5, array('id' => $id));
3123          $this->assertEquals(1e-5, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3124          $DB->set_field($tablename, 'onechar', 1e-300, array('id' => $id));
3125          $this->assertEquals(1e-300, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3126          $DB->set_field($tablename, 'onechar', 1e300, array('id' => $id));
3127          $this->assertEquals(1e300, $DB->get_field($tablename, 'onechar', array('id' => $id)));
3128  
3129          // Test saving a float in a TEXT column, and reading it back.
3130          $id = $DB->insert_record($tablename, array('onetext' => 'X'));
3131          $DB->set_field($tablename, 'onetext', 1.0, array('id' => $id));
3132          $this->assertEquals(1.0, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3133          $DB->set_field($tablename, 'onetext', 1e20, array('id' => $id));
3134          $this->assertEquals(1e20, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3135          $DB->set_field($tablename, 'onetext', 1e-4, array('id' => $id));
3136          $this->assertEquals(1e-4, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3137          $DB->set_field($tablename, 'onetext', 1e-5, array('id' => $id));
3138          $this->assertEquals(1e-5, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3139          $DB->set_field($tablename, 'onetext', 1e-300, array('id' => $id));
3140          $this->assertEquals(1e-300, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3141          $DB->set_field($tablename, 'onetext', 1e300, array('id' => $id));
3142          $this->assertEquals(1e300, $DB->get_field($tablename, 'onetext', array('id' => $id)));
3143  
3144          // Note: All the nulls, booleans, empties, quoted and backslashes tests
3145          // go to set_field_select() because set_field() is just one wrapper over it.
3146      }
3147  
3148      public function test_set_field_select() {
3149  
3150          // All the information in this test is fetched from DB by get_field() so we
3151          // have such method properly tested against nulls, empties and friends...
3152  
3153          $DB = $this->tdb;
3154          $dbman = $DB->get_manager();
3155  
3156          $table = $this->get_test_table();
3157          $tablename = $table->getName();
3158  
3159          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3160          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3161          $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null);
3162          $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null);
3163          $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null);
3164          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
3165          $table->add_field('onebinary', XMLDB_TYPE_BINARY, 'big', null, null, null);
3166          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3167          $dbman->create_table($table);
3168  
3169          $DB->insert_record($tablename, array('course' => 1));
3170  
3171          $this->assertTrue($DB->set_field_select($tablename, 'course', 2, 'id = ?', array(1)));
3172          $this->assertEquals(2, $DB->get_field($tablename, 'course', array('id' => 1)));
3173  
3174          // Check nulls are set properly for all types.
3175          $DB->set_field_select($tablename, 'oneint', null, 'id = ?', array(1)); // Trues.
3176          $DB->set_field_select($tablename, 'onenum', null, 'id = ?', array(1));
3177          $DB->set_field_select($tablename, 'onechar', null, 'id = ?', array(1));
3178          $DB->set_field_select($tablename, 'onetext', null, 'id = ?', array(1));
3179          $DB->set_field_select($tablename, 'onebinary', null, 'id = ?', array(1));
3180          $this->assertNull($DB->get_field($tablename, 'oneint', array('id' => 1)));
3181          $this->assertNull($DB->get_field($tablename, 'onenum', array('id' => 1)));
3182          $this->assertNull($DB->get_field($tablename, 'onechar', array('id' => 1)));
3183          $this->assertNull($DB->get_field($tablename, 'onetext', array('id' => 1)));
3184          $this->assertNull($DB->get_field($tablename, 'onebinary', array('id' => 1)));
3185  
3186          // Check zeros are set properly for all types.
3187          $DB->set_field_select($tablename, 'oneint', 0, 'id = ?', array(1));
3188          $DB->set_field_select($tablename, 'onenum', 0, 'id = ?', array(1));
3189          $this->assertEquals(0, $DB->get_field($tablename, 'oneint', array('id' => 1)));
3190          $this->assertEquals(0, $DB->get_field($tablename, 'onenum', array('id' => 1)));
3191  
3192          // Check booleans are set properly for all types.
3193          $DB->set_field_select($tablename, 'oneint', true, 'id = ?', array(1)); // Trues.
3194          $DB->set_field_select($tablename, 'onenum', true, 'id = ?', array(1));
3195          $DB->set_field_select($tablename, 'onechar', true, 'id = ?', array(1));
3196          $DB->set_field_select($tablename, 'onetext', true, 'id = ?', array(1));
3197          $this->assertEquals(1, $DB->get_field($tablename, 'oneint', array('id' => 1)));
3198          $this->assertEquals(1, $DB->get_field($tablename, 'onenum', array('id' => 1)));
3199          $this->assertEquals(1, $DB->get_field($tablename, 'onechar', array('id' => 1)));
3200          $this->assertEquals(1, $DB->get_field($tablename, 'onetext', array('id' => 1)));
3201  
3202          $DB->set_field_select($tablename, 'oneint', false, 'id = ?', array(1)); // Falses.
3203          $DB->set_field_select($tablename, 'onenum', false, 'id = ?', array(1));
3204          $DB->set_field_select($tablename, 'onechar', false, 'id = ?', array(1));
3205          $DB->set_field_select($tablename, 'onetext', false, 'id = ?', array(1));
3206          $this->assertEquals(0, $DB->get_field($tablename, 'oneint', array('id' => 1)));
3207          $this->assertEquals(0, $DB->get_field($tablename, 'onenum', array('id' => 1)));
3208          $this->assertEquals(0, $DB->get_field($tablename, 'onechar', array('id' => 1)));
3209          $this->assertEquals(0, $DB->get_field($tablename, 'onetext', array('id' => 1)));
3210  
3211          // Check string data causes exception in numeric types.
3212          try {
3213              $DB->set_field_select($tablename, 'oneint', 'onestring', 'id = ?', array(1));
3214              $this->fail("Expecting an exception, none occurred");
3215          } catch (\moodle_exception $e) {
3216              $this->assertInstanceOf('dml_exception', $e);
3217          }
3218          try {
3219              $DB->set_field_select($tablename, 'onenum', 'onestring', 'id = ?', array(1));
3220              $this->fail("Expecting an exception, none occurred");
3221          } catch (\moodle_exception $e) {
3222              $this->assertInstanceOf('dml_exception', $e);
3223          }
3224  
3225          // Check empty string data is stored as 0 in numeric datatypes.
3226          $DB->set_field_select($tablename, 'oneint', '', 'id = ?', array(1));
3227          $field = $DB->get_field($tablename, 'oneint', array('id' => 1));
3228          $this->assertTrue(is_numeric($field) && $field == 0);
3229  
3230          $DB->set_field_select($tablename, 'onenum', '', 'id = ?', array(1));
3231          $field = $DB->get_field($tablename, 'onenum', array('id' => 1));
3232          $this->assertTrue(is_numeric($field) && $field == 0);
3233  
3234          // Check empty strings are set properly in string types.
3235          $DB->set_field_select($tablename, 'onechar', '', 'id = ?', array(1));
3236          $DB->set_field_select($tablename, 'onetext', '', 'id = ?', array(1));
3237          $this->assertTrue($DB->get_field($tablename, 'onechar', array('id' => 1)) === '');
3238          $this->assertTrue($DB->get_field($tablename, 'onetext', array('id' => 1)) === '');
3239  
3240          // Check operation ((210.10 + 39.92) - 150.02) against numeric types.
3241          $DB->set_field_select($tablename, 'oneint', ((210.10 + 39.92) - 150.02), 'id = ?', array(1));
3242          $DB->set_field_select($tablename, 'onenum', ((210.10 + 39.92) - 150.02), 'id = ?', array(1));
3243          $this->assertEquals(100, $DB->get_field($tablename, 'oneint', array('id' => 1)));
3244          $this->assertEquals(100, $DB->get_field($tablename, 'onenum', array('id' => 1)));
3245  
3246          // Check various quotes/backslashes combinations in string types.
3247          $teststrings = array(
3248              'backslashes and quotes alone (even): "" \'\' \\\\',
3249              'backslashes and quotes alone (odd): """ \'\'\' \\\\\\',
3250              'backslashes and quotes sequences (even): \\"\\" \\\'\\\'',
3251              'backslashes and quotes sequences (odd): \\"\\"\\" \\\'\\\'\\\'');
3252          foreach ($teststrings as $teststring) {
3253              $DB->set_field_select($tablename, 'onechar', $teststring, 'id = ?', array(1));
3254              $DB->set_field_select($tablename, 'onetext', $teststring, 'id = ?', array(1));
3255              $this->assertEquals($teststring, $DB->get_field($tablename, 'onechar', array('id' => 1)));
3256              $this->assertEquals($teststring, $DB->get_field($tablename, 'onetext', array('id' => 1)));
3257          }
3258  
3259          // Check LOBs in text/binary columns.
3260          $clob = file_get_contents(__DIR__ . '/fixtures/clob.txt');
3261          $blob = file_get_contents(__DIR__ . '/fixtures/randombinary');
3262          $DB->set_field_select($tablename, 'onetext', $clob, 'id = ?', array(1));
3263          $DB->set_field_select($tablename, 'onebinary', $blob, 'id = ?', array(1));
3264          $this->assertEquals($clob, $DB->get_field($tablename, 'onetext', array('id' => 1)), 'Test CLOB set_field (full contents output disabled)');
3265          $this->assertEquals($blob, $DB->get_field($tablename, 'onebinary', array('id' => 1)), 'Test BLOB set_field (full contents output disabled)');
3266  
3267          // Empty data in binary columns works.
3268          $DB->set_field_select($tablename, 'onebinary', '', 'id = ?', array(1));
3269          $this->assertEquals('', $DB->get_field($tablename, 'onebinary', array('id' => 1)), 'Blobs need to accept empty values.');
3270  
3271          // And "small" LOBs too, just in case.
3272          $newclob = substr($clob, 0, 500);
3273          $newblob = substr($blob, 0, 250);
3274          $DB->set_field_select($tablename, 'onetext', $newclob, 'id = ?', array(1));
3275          $DB->set_field_select($tablename, 'onebinary', $newblob, 'id = ?', array(1));
3276          $this->assertEquals($newclob, $DB->get_field($tablename, 'onetext', array('id' => 1)), 'Test "small" CLOB set_field (full contents output disabled)');
3277          $this->assertEquals($newblob, $DB->get_field($tablename, 'onebinary', array('id' => 1)), 'Test "small" BLOB set_field (full contents output disabled)');
3278  
3279          // This is the failure from MDL-24863. This was giving an error on MSSQL,
3280          // which converts the '1' to an integer, which cannot then be compared with
3281          // onetext cast to a varchar. This should be fixed and working now.
3282          $newchar = 'frog';
3283          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
3284          $params = array('onetext' => '1');
3285          try {
3286              $DB->set_field_select($tablename, 'onechar', $newchar, $DB->sql_compare_text('onetext') . ' = ?', $params);
3287              $this->assertTrue(true, 'No exceptions thrown with numerical text param comparison for text field.');
3288          } catch (dml_exception $e) {
3289              $this->assertFalse(true, 'We have an unexpected exception.');
3290              throw $e;
3291          }
3292      }
3293  
3294      public function test_count_records() {
3295          $DB = $this->tdb;
3296  
3297          $dbman = $DB->get_manager();
3298  
3299          $table = $this->get_test_table();
3300          $tablename = $table->getName();
3301  
3302          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3303          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3304          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
3305          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3306          $dbman->create_table($table);
3307  
3308          $this->assertSame(0, $DB->count_records($tablename));
3309  
3310          $DB->insert_record($tablename, array('course' => 3));
3311          $DB->insert_record($tablename, array('course' => 4));
3312          $DB->insert_record($tablename, array('course' => 5));
3313  
3314          $this->assertSame(3, $DB->count_records($tablename));
3315  
3316          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
3317          $conditions = array('onetext' => '1');
3318          try {
3319              $DB->count_records($tablename, $conditions);
3320              if (debugging()) {
3321                  // Only in debug mode - hopefully all devs test code in debug mode...
3322                  $this->fail('An Exception is missing, expected due to equating of text fields');
3323              }
3324          } catch (\moodle_exception $e) {
3325              $this->assertInstanceOf('dml_exception', $e);
3326              $this->assertSame('textconditionsnotallowed', $e->errorcode);
3327          }
3328      }
3329  
3330      public function test_count_records_select() {
3331          $DB = $this->tdb;
3332  
3333          $dbman = $DB->get_manager();
3334  
3335          $table = $this->get_test_table();
3336          $tablename = $table->getName();
3337  
3338          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3339          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3340          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3341          $dbman->create_table($table);
3342  
3343          $this->assertSame(0, $DB->count_records($tablename));
3344  
3345          $DB->insert_record($tablename, array('course' => 3));
3346          $DB->insert_record($tablename, array('course' => 4));
3347          $DB->insert_record($tablename, array('course' => 5));
3348  
3349          $this->assertSame(2, $DB->count_records_select($tablename, 'course > ?', array(3)));
3350      }
3351  
3352      public function test_count_records_sql() {
3353          $DB = $this->tdb;
3354          $dbman = $DB->get_manager();
3355  
3356          $table = $this->get_test_table();
3357          $tablename = $table->getName();
3358  
3359          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3360          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3361          $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null);
3362          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3363          $dbman->create_table($table);
3364  
3365          $this->assertSame(0, $DB->count_records($tablename));
3366  
3367          $DB->insert_record($tablename, array('course' => 3, 'onechar' => 'a'));
3368          $DB->insert_record($tablename, array('course' => 4, 'onechar' => 'b'));
3369          $DB->insert_record($tablename, array('course' => 5, 'onechar' => 'c'));
3370  
3371          $this->assertSame(2, $DB->count_records_sql("SELECT COUNT(*) FROM {{$tablename}} WHERE course > ?", array(3)));
3372  
3373          // Test invalid use.
3374          try {
3375              $DB->count_records_sql("SELECT onechar FROM {{$tablename}} WHERE course = ?", array(3));
3376              $this->fail('Exception expected when non-number field used in count_records_sql');
3377          } catch (\moodle_exception $e) {
3378              $this->assertInstanceOf('coding_exception', $e);
3379          }
3380  
3381          try {
3382              $DB->count_records_sql("SELECT course FROM {{$tablename}} WHERE 1 = 2");
3383              $this->fail('Exception expected when non-number field used in count_records_sql');
3384          } catch (\moodle_exception $e) {
3385              $this->assertInstanceOf('coding_exception', $e);
3386          }
3387      }
3388  
3389      public function test_record_exists() {
3390          $DB = $this->tdb;
3391          $dbman = $DB->get_manager();
3392  
3393          $table = $this->get_test_table();
3394          $tablename = $table->getName();
3395  
3396          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3397          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3398          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
3399          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3400          $dbman->create_table($table);
3401  
3402          $this->assertEquals(0, $DB->count_records($tablename));
3403  
3404          $this->assertFalse($DB->record_exists($tablename, array('course' => 3)));
3405          $DB->insert_record($tablename, array('course' => 3));
3406  
3407          $this->assertTrue($DB->record_exists($tablename, array('course' => 3)));
3408  
3409          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
3410          $conditions = array('onetext' => '1');
3411          try {
3412              $DB->record_exists($tablename, $conditions);
3413              if (debugging()) {
3414                  // Only in debug mode - hopefully all devs test code in debug mode...
3415                  $this->fail('An Exception is missing, expected due to equating of text fields');
3416              }
3417          } catch (\moodle_exception $e) {
3418              $this->assertInstanceOf('dml_exception', $e);
3419              $this->assertSame('textconditionsnotallowed', $e->errorcode);
3420          }
3421      }
3422  
3423      public function test_record_exists_select() {
3424          $DB = $this->tdb;
3425          $dbman = $DB->get_manager();
3426  
3427          $table = $this->get_test_table();
3428          $tablename = $table->getName();
3429  
3430          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3431          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3432          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3433          $dbman->create_table($table);
3434  
3435          $this->assertEquals(0, $DB->count_records($tablename));
3436  
3437          $this->assertFalse($DB->record_exists_select($tablename, "course = ?", array(3)));
3438          $DB->insert_record($tablename, array('course' => 3));
3439  
3440          $this->assertTrue($DB->record_exists_select($tablename, "course = ?", array(3)));
3441      }
3442  
3443      public function test_record_exists_sql() {
3444          $DB = $this->tdb;
3445          $dbman = $DB->get_manager();
3446  
3447          $table = $this->get_test_table();
3448          $tablename = $table->getName();
3449  
3450          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3451          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3452          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3453          $dbman->create_table($table);
3454  
3455          $this->assertEquals(0, $DB->count_records($tablename));
3456  
3457          $this->assertFalse($DB->record_exists_sql("SELECT * FROM {{$tablename}} WHERE course = ?", array(3)));
3458          $DB->insert_record($tablename, array('course' => 3));
3459  
3460          $this->assertTrue($DB->record_exists_sql("SELECT * FROM {{$tablename}} WHERE course = ?", array(3)));
3461      }
3462  
3463      public function test_recordset_locks_delete() {
3464          $DB = $this->tdb;
3465          $dbman = $DB->get_manager();
3466  
3467          // Setup.
3468          $table = $this->get_test_table();
3469          $tablename = $table->getName();
3470  
3471          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3472          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3473          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3474          $dbman->create_table($table);
3475  
3476          $DB->insert_record($tablename, array('course' => 1));
3477          $DB->insert_record($tablename, array('course' => 2));
3478          $DB->insert_record($tablename, array('course' => 3));
3479          $DB->insert_record($tablename, array('course' => 4));
3480          $DB->insert_record($tablename, array('course' => 5));
3481          $DB->insert_record($tablename, array('course' => 6));
3482  
3483          // Test against db write locking while on an open recordset.
3484          $rs = $DB->get_recordset($tablename, array(), null, 'course', 2, 2); // Get courses = {3,4}.
3485          foreach ($rs as $record) {
3486              $cid = $record->course;
3487              $DB->delete_records($tablename, array('course' => $cid));
3488              $this->assertFalse($DB->record_exists($tablename, array('course' => $cid)));
3489          }
3490          $rs->close();
3491  
3492          $this->assertEquals(4, $DB->count_records($tablename, array()));
3493      }
3494  
3495      public function test_recordset_locks_update() {
3496          $DB = $this->tdb;
3497          $dbman = $DB->get_manager();
3498  
3499          // Setup.
3500          $table = $this->get_test_table();
3501          $tablename = $table->getName();
3502  
3503          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3504          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3505          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3506          $dbman->create_table($table);
3507  
3508          $DB->insert_record($tablename, array('course' => 1));
3509          $DB->insert_record($tablename, array('course' => 2));
3510          $DB->insert_record($tablename, array('course' => 3));
3511          $DB->insert_record($tablename, array('course' => 4));
3512          $DB->insert_record($tablename, array('course' => 5));
3513          $DB->insert_record($tablename, array('course' => 6));
3514  
3515          // Test against db write locking while on an open recordset.
3516          $rs = $DB->get_recordset($tablename, array(), null, 'course', 2, 2); // Get courses = {3,4}.
3517          foreach ($rs as $record) {
3518              $cid = $record->course;
3519              $DB->set_field($tablename, 'course', 10, array('course' => $cid));
3520              $this->assertFalse($DB->record_exists($tablename, array('course' => $cid)));
3521          }
3522          $rs->close();
3523  
3524          $this->assertEquals(2, $DB->count_records($tablename, array('course' => 10)));
3525      }
3526  
3527      public function test_delete_records() {
3528          $DB = $this->tdb;
3529          $dbman = $DB->get_manager();
3530  
3531          $table = $this->get_test_table();
3532          $tablename = $table->getName();
3533  
3534          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3535          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3536          $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
3537          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3538          $dbman->create_table($table);
3539  
3540          $DB->insert_record($tablename, array('course' => 3));
3541          $DB->insert_record($tablename, array('course' => 2));
3542          $DB->insert_record($tablename, array('course' => 2));
3543  
3544          // Delete all records.
3545          $this->assertTrue($DB->delete_records($tablename));
3546          $this->assertEquals(0, $DB->count_records($tablename));
3547  
3548          // Delete subset of records.
3549          $DB->insert_record($tablename, array('course' => 3));
3550          $DB->insert_record($tablename, array('course' => 2));
3551          $DB->insert_record($tablename, array('course' => 2));
3552  
3553          $this->assertTrue($DB->delete_records($tablename, array('course' => 2)));
3554          $this->assertEquals(1, $DB->count_records($tablename));
3555  
3556          // Delete all.
3557          $this->assertTrue($DB->delete_records($tablename, array()));
3558          $this->assertEquals(0, $DB->count_records($tablename));
3559  
3560          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
3561          $conditions = array('onetext'=>'1');
3562          try {
3563              $DB->delete_records($tablename, $conditions);
3564              if (debugging()) {
3565                  // Only in debug mode - hopefully all devs test code in debug mode...
3566                  $this->fail('An Exception is missing, expected due to equating of text fields');
3567              }
3568          } catch (\moodle_exception $e) {
3569              $this->assertInstanceOf('dml_exception', $e);
3570              $this->assertSame('textconditionsnotallowed', $e->errorcode);
3571          }
3572  
3573          // Test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int).
3574          $conditions = array('onetext' => 1);
3575          try {
3576              $DB->delete_records($tablename, $conditions);
3577              if (debugging()) {
3578                  // Only in debug mode - hopefully all devs test code in debug mode...
3579                  $this->fail('An Exception is missing, expected due to equating of text fields');
3580              }
3581          } catch (\moodle_exception $e) {
3582              $this->assertInstanceOf('dml_exception', $e);
3583              $this->assertSame('textconditionsnotallowed', $e->errorcode);
3584          }
3585      }
3586  
3587      public function test_delete_records_select() {
3588          $DB = $this->tdb;
3589          $dbman = $DB->get_manager();
3590  
3591          $table = $this->get_test_table();
3592          $tablename = $table->getName();
3593  
3594          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3595          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3596          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3597          $dbman->create_table($table);
3598  
3599          $DB->insert_record($tablename, array('course' => 3));
3600          $DB->insert_record($tablename, array('course' => 2));
3601          $DB->insert_record($tablename, array('course' => 2));
3602  
3603          $this->assertTrue($DB->delete_records_select($tablename, 'course = ?', array(2)));
3604          $this->assertEquals(1, $DB->count_records($tablename));
3605      }
3606  
3607      public function test_delete_records_subquery() {
3608          $DB = $this->tdb;
3609          $dbman = $DB->get_manager();
3610  
3611          $table = $this->get_test_table();
3612          $tablename = $table->getName();
3613  
3614          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3615          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3616          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3617          $dbman->create_table($table);
3618  
3619          $DB->insert_record($tablename, array('course' => 3));
3620          $DB->insert_record($tablename, array('course' => 2));
3621          $DB->insert_record($tablename, array('course' => 2));
3622  
3623          // This is not a useful scenario for using a subquery, but it will be sufficient for testing.
3624          // Use the 'frog' alias just to make it clearer when we are testing the alias parameter.
3625          $DB->delete_records_subquery($tablename, 'id', 'frog',
3626                  'SELECT id AS frog FROM {' . $tablename . '} WHERE course = ?', [2]);
3627          $this->assertEquals(1, $DB->count_records($tablename));
3628      }
3629  
3630      public function test_delete_records_list() {
3631          $DB = $this->tdb;
3632          $dbman = $DB->get_manager();
3633  
3634          $table = $this->get_test_table();
3635          $tablename = $table->getName();
3636  
3637          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3638          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3639          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3640          $dbman->create_table($table);
3641  
3642          $DB->insert_record($tablename, array('course' => 1));
3643          $DB->insert_record($tablename, array('course' => 2));
3644          $DB->insert_record($tablename, array('course' => 3));
3645  
3646          $this->assertTrue($DB->delete_records_list($tablename, 'course', array(2, 3)));
3647          $this->assertEquals(1, $DB->count_records($tablename));
3648  
3649          $this->assertTrue($DB->delete_records_list($tablename, 'course', array())); // Must delete 0 rows without conditions. MDL-17645.
3650          $this->assertEquals(1, $DB->count_records($tablename));
3651      }
3652  
3653      public function test_object_params() {
3654          $DB = $this->tdb;
3655          $dbman = $DB->get_manager();
3656  
3657          $table = $this->get_test_table();
3658          $tablename = $table->getName();
3659          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3660          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3661          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3662          $dbman->create_table($table);
3663  
3664          $o = new \stdClass(); // Objects without __toString - never worked.
3665          try {
3666              $DB->fix_sql_params("SELECT {{$tablename}} WHERE course = ? ", array($o));
3667              $this->fail('coding_exception expected');
3668          } catch (\moodle_exception $e) {
3669              $this->assertInstanceOf('coding_exception', $e);
3670          }
3671  
3672          // Objects with __toString() forbidden everywhere since 2.3.
3673          $o = new dml_test_object_one();
3674          try {
3675              $DB->fix_sql_params("SELECT {{$tablename}} WHERE course = ? ", array($o));
3676              $this->fail('coding_exception expected');
3677          } catch (\moodle_exception $e) {
3678              $this->assertInstanceOf('coding_exception', $e);
3679          }
3680  
3681          try {
3682              $DB->execute("SELECT {{$tablename}} WHERE course = ? ", array($o));
3683              $this->fail('coding_exception expected');
3684          } catch (\moodle_exception $e) {
3685              $this->assertInstanceOf('coding_exception', $e);
3686          }
3687  
3688          try {
3689              $DB->get_recordset_sql("SELECT {{$tablename}} WHERE course = ? ", array($o));
3690              $this->fail('coding_exception expected');
3691          } catch (\moodle_exception $e) {
3692              $this->assertInstanceOf('coding_exception', $e);
3693          }
3694  
3695          try {
3696              $DB->get_records_sql("SELECT {{$tablename}} WHERE course = ? ", array($o));
3697              $this->fail('coding_exception expected');
3698          } catch (\moodle_exception $e) {
3699              $this->assertInstanceOf('coding_exception', $e);
3700          }
3701  
3702          try {
3703              $record = new \stdClass();
3704              $record->course = $o;
3705              $DB->insert_record_raw($tablename, $record);
3706              $this->fail('coding_exception expected');
3707          } catch (\moodle_exception $e) {
3708              $this->assertInstanceOf('coding_exception', $e);
3709          }
3710  
3711          try {
3712              $record = new \stdClass();
3713              $record->course = $o;
3714              $DB->insert_record($tablename, $record);
3715              $this->fail('coding_exception expected');
3716          } catch (\moodle_exception $e) {
3717              $this->assertInstanceOf('coding_exception', $e);
3718          }
3719  
3720          try {
3721              $record = new \stdClass();
3722              $record->course = $o;
3723              $DB->import_record($tablename, $record);
3724              $this->fail('coding_exception expected');
3725          } catch (\moodle_exception $e) {
3726              $this->assertInstanceOf('coding_exception', $e);
3727          }
3728  
3729          try {
3730              $record = new \stdClass();
3731              $record->id = 1;
3732              $record->course = $o;
3733              $DB->update_record_raw($tablename, $record);
3734              $this->fail('coding_exception expected');
3735          } catch (\moodle_exception $e) {
3736              $this->assertInstanceOf('coding_exception', $e);
3737          }
3738  
3739          try {
3740              $record = new \stdClass();
3741              $record->id = 1;
3742              $record->course = $o;
3743              $DB->update_record($tablename, $record);
3744              $this->fail('coding_exception expected');
3745          } catch (\moodle_exception $e) {
3746              $this->assertInstanceOf('coding_exception', $e);
3747          }
3748  
3749          try {
3750              $DB->set_field_select($tablename, 'course', 1, "course = ? ", array($o));
3751              $this->fail('coding_exception expected');
3752          } catch (\moodle_exception $e) {
3753              $this->assertInstanceOf('coding_exception', $e);
3754          }
3755  
3756          try {
3757              $DB->delete_records_select($tablename, "course = ? ", array($o));
3758              $this->fail('coding_exception expected');
3759          } catch (\moodle_exception $e) {
3760              $this->assertInstanceOf('coding_exception', $e);
3761          }
3762      }
3763  
3764      public function test_sql_null_from_clause() {
3765          $DB = $this->tdb;
3766          $sql = "SELECT 1 AS id ".$DB->sql_null_from_clause();
3767          $this->assertEquals(1, $DB->get_field_sql($sql));
3768      }
3769  
3770      public function test_sql_bitand() {
3771          $DB = $this->tdb;
3772          $dbman = $DB->get_manager();
3773  
3774          $table = $this->get_test_table();
3775          $tablename = $table->getName();
3776  
3777          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3778          $table->add_field('col1', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3779          $table->add_field('col2', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3780          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3781          $dbman->create_table($table);
3782  
3783          $DB->insert_record($tablename, array('col1' => 3, 'col2' => 10));
3784  
3785          $sql = "SELECT ".$DB->sql_bitand(10, 3)." AS res ".$DB->sql_null_from_clause();
3786          $this->assertEquals(2, $DB->get_field_sql($sql));
3787  
3788          $sql = "SELECT id, ".$DB->sql_bitand('col1', 'col2')." AS res FROM {{$tablename}}";
3789          $result = $DB->get_records_sql($sql);
3790          $this->assertCount(1, $result);
3791          $this->assertEquals(2, reset($result)->res);
3792  
3793          $sql = "SELECT id, ".$DB->sql_bitand('col1', '?')." AS res FROM {{$tablename}}";
3794          $result = $DB->get_records_sql($sql, array(10));
3795          $this->assertCount(1, $result);
3796          $this->assertEquals(2, reset($result)->res);
3797      }
3798  
3799      public function test_sql_bitnot() {
3800          $DB = $this->tdb;
3801  
3802          $not = $DB->sql_bitnot(2);
3803          $notlimited = $DB->sql_bitand($not, 7); // Might be positive or negative number which can not fit into PHP INT!
3804  
3805          $sql = "SELECT $notlimited AS res ".$DB->sql_null_from_clause();
3806          $this->assertEquals(5, $DB->get_field_sql($sql));
3807      }
3808  
3809      public function test_sql_bitor() {
3810          $DB = $this->tdb;
3811          $dbman = $DB->get_manager();
3812  
3813          $table = $this->get_test_table();
3814          $tablename = $table->getName();
3815  
3816          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3817          $table->add_field('col1', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3818          $table->add_field('col2', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3819          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3820          $dbman->create_table($table);
3821  
3822          $DB->insert_record($tablename, array('col1' => 3, 'col2' => 10));
3823  
3824          $sql = "SELECT ".$DB->sql_bitor(10, 3)." AS res ".$DB->sql_null_from_clause();
3825          $this->assertEquals(11, $DB->get_field_sql($sql));
3826  
3827          $sql = "SELECT id, ".$DB->sql_bitor('col1', 'col2')." AS res FROM {{$tablename}}";
3828          $result = $DB->get_records_sql($sql);
3829          $this->assertCount(1, $result);
3830          $this->assertEquals(11, reset($result)->res);
3831  
3832          $sql = "SELECT id, ".$DB->sql_bitor('col1', '?')." AS res FROM {{$tablename}}";
3833          $result = $DB->get_records_sql($sql, array(10));
3834          $this->assertCount(1, $result);
3835          $this->assertEquals(11, reset($result)->res);
3836      }
3837  
3838      public function test_sql_bitxor() {
3839          $DB = $this->tdb;
3840          $dbman = $DB->get_manager();
3841  
3842          $table = $this->get_test_table();
3843          $tablename = $table->getName();
3844  
3845          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3846          $table->add_field('col1', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3847          $table->add_field('col2', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3848          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3849          $dbman->create_table($table);
3850  
3851          $DB->insert_record($tablename, array('col1' => 3, 'col2' => 10));
3852  
3853          $sql = "SELECT ".$DB->sql_bitxor(10, 3)." AS res ".$DB->sql_null_from_clause();
3854          $this->assertEquals(9, $DB->get_field_sql($sql));
3855  
3856          $sql = "SELECT id, ".$DB->sql_bitxor('col1', 'col2')." AS res FROM {{$tablename}}";
3857          $result = $DB->get_records_sql($sql);
3858          $this->assertCount(1, $result);
3859          $this->assertEquals(9, reset($result)->res);
3860  
3861          $sql = "SELECT id, ".$DB->sql_bitxor('col1', '?')." AS res FROM {{$tablename}}";
3862          $result = $DB->get_records_sql($sql, array(10));
3863          $this->assertCount(1, $result);
3864          $this->assertEquals(9, reset($result)->res);
3865      }
3866  
3867      public function test_sql_modulo() {
3868          $DB = $this->tdb;
3869          $sql = "SELECT ".$DB->sql_modulo(10, 7)." AS res ".$DB->sql_null_from_clause();
3870          $this->assertEquals(3, $DB->get_field_sql($sql));
3871      }
3872  
3873      public function test_sql_ceil() {
3874          $DB = $this->tdb;
3875          $sql = "SELECT ".$DB->sql_ceil(665.666)." AS res ".$DB->sql_null_from_clause();
3876          $this->assertEquals(666, $DB->get_field_sql($sql));
3877      }
3878  
3879      /**
3880       * Test DML libraries sql_cast_to_char method
3881       *
3882       * @covers ::sql_cast_to_char
3883       */
3884      public function test_cast_to_char(): void {
3885          $DB = $this->tdb;
3886          $dbman = $DB->get_manager();
3887  
3888          $tableone = $this->get_test_table('one');
3889          $tableone->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3890          $tableone->add_field('intfield', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
3891          $tableone->add_field('details', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
3892          $tableone->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
3893          $dbman->create_table($tableone);
3894  
3895          $tableonename = $tableone->getName();
3896          $DB->insert_record($tableonename, (object) ['intfield' => 10, 'details' => 'uno']);
3897          $DB->insert_record($tableonename, (object) ['intfield' => 20, 'details' => 'dos']);
3898  
3899          $tabletwo = $this->get_test_table('two');
3900          $tabletwo->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3901          $tabletwo->add_field('charfield', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
3902          $tabletwo->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
3903          $dbman->create_table($tabletwo);
3904  
3905          $tabletwoname = $tabletwo->getName();
3906          $DB->insert_record($tabletwoname, (object) ['charfield' => '10']);
3907  
3908          // Test by joining a char field to a cast int field (mixing types not supported across databases).
3909          $sql = "SELECT t1.details
3910                    FROM {{$tableonename}} t1
3911                    JOIN {{$tabletwoname}} t2 ON t2.charfield = " . $DB->sql_cast_to_char('t1.intfield');
3912  
3913          $fieldset = $DB->get_fieldset_sql($sql);
3914          $this->assertEquals(['uno'], $fieldset);
3915      }
3916  
3917      public function test_cast_char2int() {
3918          $DB = $this->tdb;
3919          $dbman = $DB->get_manager();
3920  
3921          $table1 = $this->get_test_table("1");
3922          $tablename1 = $table1->getName();
3923  
3924          $table1->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3925          $table1->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
3926          $table1->add_field('nametext', XMLDB_TYPE_TEXT, 'small', null, null, null, null);
3927          $table1->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3928          $dbman->create_table($table1);
3929  
3930          $DB->insert_record($tablename1, array('name'=>'0100', 'nametext'=>'0200'));
3931          $DB->insert_record($tablename1, array('name'=>'10',   'nametext'=>'20'));
3932  
3933          $table2 = $this->get_test_table("2");
3934          $tablename2 = $table2->getName();
3935          $table2->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3936          $table2->add_field('res', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3937          $table2->add_field('restext', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
3938          $table2->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3939          $dbman->create_table($table2);
3940  
3941          $DB->insert_record($tablename2, array('res'=>100, 'restext'=>200));
3942  
3943          // Casting varchar field.
3944          $sql = "SELECT *
3945                    FROM {".$tablename1."} t1
3946                    JOIN {".$tablename2."} t2 ON ".$DB->sql_cast_char2int("t1.name")." = t2.res ";
3947          $records = $DB->get_records_sql($sql);
3948          $this->assertCount(1, $records);
3949          // Also test them in order clauses.
3950          $sql = "SELECT * FROM {{$tablename1}} ORDER BY ".$DB->sql_cast_char2int('name');
3951          $records = $DB->get_records_sql($sql);
3952          $this->assertCount(2, $records);
3953          $this->assertSame('10', reset($records)->name);
3954          $this->assertSame('0100', next($records)->name);
3955  
3956          // Casting text field.
3957          $sql = "SELECT *
3958                    FROM {".$tablename1."} t1
3959                    JOIN {".$tablename2."} t2 ON ".$DB->sql_cast_char2int("t1.nametext", true)." = t2.restext ";
3960          $records = $DB->get_records_sql($sql);
3961          $this->assertCount(1, $records);
3962          // Also test them in order clauses.
3963          $sql = "SELECT * FROM {{$tablename1}} ORDER BY ".$DB->sql_cast_char2int('nametext', true);
3964          $records = $DB->get_records_sql($sql);
3965          $this->assertCount(2, $records);
3966          $this->assertSame('20', reset($records)->nametext);
3967          $this->assertSame('0200', next($records)->nametext);
3968      }
3969  
3970      public function test_cast_char2real() {
3971          $DB = $this->tdb;
3972          $dbman = $DB->get_manager();
3973  
3974          $table = $this->get_test_table();
3975          $tablename = $table->getName();
3976  
3977          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
3978          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
3979          $table->add_field('nametext', XMLDB_TYPE_TEXT, 'small', null, null, null, null);
3980          $table->add_field('res', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null);
3981          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
3982          $dbman->create_table($table);
3983  
3984          $DB->insert_record($tablename, array('name'=>'10.10', 'nametext'=>'10.10', 'res'=>5.1));
3985          $DB->insert_record($tablename, array('name'=>'91.10', 'nametext'=>'91.10', 'res'=>666));
3986          $DB->insert_record($tablename, array('name'=>'011.13333333', 'nametext'=>'011.13333333', 'res'=>10.1));
3987  
3988          // Casting varchar field.
3989          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_cast_char2real('name')." > res";
3990          $records = $DB->get_records_sql($sql);
3991          $this->assertCount(2, $records);
3992          // Also test them in order clauses.
3993          $sql = "SELECT * FROM {{$tablename}} ORDER BY ".$DB->sql_cast_char2real('name');
3994          $records = $DB->get_records_sql($sql);
3995          $this->assertCount(3, $records);
3996          $this->assertSame('10.10', reset($records)->name);
3997          $this->assertSame('011.13333333', next($records)->name);
3998          $this->assertSame('91.10', next($records)->name);
3999          // And verify we can operate with them without too much problem with at least 6 decimals scale accuracy.
4000          $sql = "SELECT AVG(" . $DB->sql_cast_char2real('name') . ") FROM {{$tablename}}";
4001          $this->assertEqualsWithDelta(37.44444443333333, (float)$DB->get_field_sql($sql), 1.0E-6);
4002  
4003          // Casting text field.
4004          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_cast_char2real('nametext', true)." > res";
4005          $records = $DB->get_records_sql($sql);
4006          $this->assertCount(2, $records);
4007          // Also test them in order clauses.
4008          $sql = "SELECT * FROM {{$tablename}} ORDER BY ".$DB->sql_cast_char2real('nametext', true);
4009          $records = $DB->get_records_sql($sql);
4010          $this->assertCount(3, $records);
4011          $this->assertSame('10.10', reset($records)->nametext);
4012          $this->assertSame('011.13333333', next($records)->nametext);
4013          $this->assertSame('91.10', next($records)->nametext);
4014          // And verify we can operate with them without too much problem with at least 6 decimals scale accuracy.
4015          $sql = "SELECT AVG(" . $DB->sql_cast_char2real('nametext', true) . ") FROM {{$tablename}}";
4016          $this->assertEqualsWithDelta(37.44444443333333, (float)$DB->get_field_sql($sql), 1.0E-6);
4017  
4018          // Check it works with values passed as param.
4019          $sql = "SELECT name FROM {{$tablename}} WHERE FLOOR(res - " . $DB->sql_cast_char2real(':param') . ") = 0";
4020          $this->assertEquals('011.13333333', $DB->get_field_sql($sql, array('param' => '10.09999')));
4021  
4022          // And also, although not recommended, with directly passed values.
4023          $sql = "SELECT name FROM {{$tablename}} WHERE FLOOR(res - " . $DB->sql_cast_char2real('10.09999') . ") = 0";
4024          $this->assertEquals('011.13333333', $DB->get_field_sql($sql));
4025      }
4026  
4027      public function test_sql_compare_text() {
4028          $DB = $this->tdb;
4029          $dbman = $DB->get_manager();
4030  
4031          $table = $this->get_test_table();
4032          $tablename = $table->getName();
4033  
4034          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4035          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4036          $table->add_field('description', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
4037          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4038          $dbman->create_table($table);
4039  
4040          $DB->insert_record($tablename, array('name'=>'abcd',   'description'=>'abcd'));
4041          $DB->insert_record($tablename, array('name'=>'abcdef', 'description'=>'bbcdef'));
4042          $DB->insert_record($tablename, array('name'=>'aaaa', 'description'=>'aaaacccccccccccccccccc'));
4043          $DB->insert_record($tablename, array('name'=>'xxxx',   'description'=>'123456789a123456789b123456789c123456789d'));
4044  
4045          // Only some supported databases truncate TEXT fields for comparisons, currently MSSQL and Oracle.
4046          $dbtruncatestextfields = ($DB->get_dbfamily() == 'mssql' || $DB->get_dbfamily() == 'oracle');
4047  
4048          if ($dbtruncatestextfields) {
4049              // Ensure truncation behaves as expected.
4050  
4051              $sql = "SELECT " . $DB->sql_compare_text('description') . " AS field FROM {{$tablename}} WHERE name = ?";
4052              $description = $DB->get_field_sql($sql, array('xxxx'));
4053  
4054              // Should truncate to 32 chars (the default).
4055              $this->assertEquals('123456789a123456789b123456789c12', $description);
4056  
4057              $sql = "SELECT " . $DB->sql_compare_text('description', 35) . " AS field FROM {{$tablename}} WHERE name = ?";
4058              $description = $DB->get_field_sql($sql, array('xxxx'));
4059  
4060              // Should truncate to the specified number of chars.
4061              $this->assertEquals('123456789a123456789b123456789c12345', $description);
4062          }
4063  
4064          // Ensure text field comparison is successful.
4065          $sql = "SELECT * FROM {{$tablename}} WHERE name = ".$DB->sql_compare_text('description');
4066          $records = $DB->get_records_sql($sql);
4067          $this->assertCount(1, $records);
4068  
4069          $sql = "SELECT * FROM {{$tablename}} WHERE name = ".$DB->sql_compare_text('description', 4);
4070          $records = $DB->get_records_sql($sql);
4071          if ($dbtruncatestextfields) {
4072              // Should truncate description to 4 characters before comparing.
4073              $this->assertCount(2, $records);
4074          } else {
4075              // Should leave untruncated, so one less match.
4076              $this->assertCount(1, $records);
4077          }
4078  
4079          // Now test the function with really big content and params.
4080          $clob = file_get_contents(__DIR__ . '/fixtures/clob.txt');
4081          $DB->insert_record($tablename, array('name' => 'zzzz', 'description' => $clob));
4082          $sql = "SELECT * FROM {{$tablename}}
4083                   WHERE " . $DB->sql_compare_text('description') . " = " . $DB->sql_compare_text(':clob');
4084          $records = $DB->get_records_sql($sql, array('clob' => $clob));
4085          $this->assertCount(1, $records);
4086          $record = reset($records);
4087          $this->assertSame($clob, $record->description);
4088      }
4089  
4090      public function test_unique_index_collation_trouble() {
4091          // Note: this is a work in progress, we should probably move this to ddl test.
4092  
4093          $DB = $this->tdb;
4094          $dbman = $DB->get_manager();
4095  
4096          $table = $this->get_test_table();
4097          $tablename = $table->getName();
4098  
4099          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4100          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4101          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4102          $table->add_index('name', XMLDB_INDEX_UNIQUE, array('name'));
4103          $dbman->create_table($table);
4104  
4105          $DB->insert_record($tablename, array('name'=>'aaa'));
4106  
4107          try {
4108              $DB->insert_record($tablename, array('name'=>'AAA'));
4109          } catch (\moodle_exception $e) {
4110              // TODO: ignore case insensitive uniqueness problems for now.
4111              // $this->fail("Unique index is case sensitive - this may cause problems in some tables");
4112          }
4113  
4114          try {
4115              $DB->insert_record($tablename, array('name'=>'aäa'));
4116              $DB->insert_record($tablename, array('name'=>'aáa'));
4117              $this->assertTrue(true);
4118          } catch (\moodle_exception $e) {
4119              $family = $DB->get_dbfamily();
4120              if ($family === 'mysql' or $family === 'mssql') {
4121                  $this->fail("Unique index is accent insensitive, this may cause problems for non-ascii languages. This is usually caused by accent insensitive default collation.");
4122              } else {
4123                  // This should not happen, PostgreSQL and Oracle do not support accent insensitive uniqueness.
4124                  $this->fail("Unique index is accent insensitive, this may cause problems for non-ascii languages.");
4125              }
4126              throw($e);
4127          }
4128      }
4129  
4130      public function test_sql_equal() {
4131          $DB = $this->tdb;
4132          $dbman = $DB->get_manager();
4133  
4134          $table = $this->get_test_table();
4135          $tablename = $table->getName();
4136  
4137          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4138          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4139          $table->add_field('name2', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4140          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4141          $dbman->create_table($table);
4142  
4143          $DB->insert_record($tablename, array('name' => 'one', 'name2' => 'one'));
4144          $DB->insert_record($tablename, array('name' => 'ONE', 'name2' => 'ONE'));
4145          $DB->insert_record($tablename, array('name' => 'two', 'name2' => 'TWO'));
4146          $DB->insert_record($tablename, array('name' => 'öne', 'name2' => 'one'));
4147          $DB->insert_record($tablename, array('name' => 'öne', 'name2' => 'ÖNE'));
4148  
4149          // Case sensitive and accent sensitive (equal and not equal).
4150          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', '?', true, true, false);
4151          $records = $DB->get_records_sql($sql, array('one'));
4152          $this->assertCount(1, $records);
4153          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', ':name', true, true, true);
4154          $records = $DB->get_records_sql($sql, array('name' => 'one'));
4155          $this->assertCount(4, $records);
4156          // And with column comparison instead of params.
4157          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', 'name2', true, true, false);
4158          $records = $DB->get_records_sql($sql);
4159          $this->assertCount(2, $records);
4160  
4161          // Case insensitive and accent sensitive (equal and not equal).
4162          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', '?', false, true, false);
4163          $records = $DB->get_records_sql($sql, array('one'));
4164          $this->assertCount(2, $records);
4165          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', ':name', false, true, true);
4166          $records = $DB->get_records_sql($sql, array('name' => 'one'));
4167          $this->assertCount(3, $records);
4168          // And with column comparison instead of params.
4169          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', 'name2', false, true, false);
4170          $records = $DB->get_records_sql($sql);
4171          $this->assertCount(4, $records);
4172  
4173          // TODO: Accent insensitive is not cross-db, only some drivers support it, so just verify the queries work.
4174          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', '?', true, false);
4175          $records = $DB->get_records_sql($sql, array('one'));
4176          $this->assertGreaterThanOrEqual(1, count($records)); // At very least, there is 1 record with CS/AI "one".
4177          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', '?', false, false);
4178          $records = $DB->get_records_sql($sql, array('one'));
4179          $this->assertGreaterThanOrEqual(2, count($records)); // At very least, there are 2 records with CI/AI "one".
4180          // And with column comparison instead of params.
4181          $sql = "SELECT * FROM {{$tablename}} WHERE " . $DB->sql_equal('name', 'name2', false, false);
4182          $records = $DB->get_records_sql($sql);
4183          $this->assertGreaterThanOrEqual(4, count($records)); // At very least, there are 4 records with CI/AI names matching.
4184      }
4185  
4186      public function test_sql_like() {
4187          $DB = $this->tdb;
4188          $dbman = $DB->get_manager();
4189  
4190          $table = $this->get_test_table();
4191          $tablename = $table->getName();
4192  
4193          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4194          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4195          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4196          $dbman->create_table($table);
4197  
4198          $DB->insert_record($tablename, array('name'=>'SuperDuperRecord'));
4199          $DB->insert_record($tablename, array('name'=>'Nodupor'));
4200          $DB->insert_record($tablename, array('name'=>'ouch'));
4201          $DB->insert_record($tablename, array('name'=>'ouc_'));
4202          $DB->insert_record($tablename, array('name'=>'ouc%'));
4203          $DB->insert_record($tablename, array('name'=>'aui'));
4204          $DB->insert_record($tablename, array('name'=>'aüi'));
4205          $DB->insert_record($tablename, array('name'=>'aÜi'));
4206  
4207          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', false);
4208          $records = $DB->get_records_sql($sql, array("%dup_r%"));
4209          $this->assertCount(2, $records);
4210  
4211          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true);
4212          $records = $DB->get_records_sql($sql, array("%dup%"));
4213          $this->assertCount(1, $records);
4214  
4215          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?'); // Defaults.
4216          $records = $DB->get_records_sql($sql, array("%dup%"));
4217          $this->assertCount(1, $records);
4218  
4219          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true);
4220          $records = $DB->get_records_sql($sql, array("ouc\\_"));
4221          $this->assertCount(1, $records);
4222  
4223          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true, true, false, '|');
4224          $records = $DB->get_records_sql($sql, array($DB->sql_like_escape("ouc%", '|')));
4225          $this->assertCount(1, $records);
4226  
4227          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true, true);
4228          $records = $DB->get_records_sql($sql, array('aui'));
4229          $this->assertCount(1, $records);
4230  
4231          // Test LIKE under unusual collations.
4232          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', false, false);
4233          $records = $DB->get_records_sql($sql, array("%dup_r%"));
4234          $this->assertCount(2, $records);
4235  
4236          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true, true, true); // NOT LIKE.
4237          $records = $DB->get_records_sql($sql, array("%o%"));
4238          $this->assertCount(3, $records);
4239  
4240          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', false, true, true); // NOT ILIKE.
4241          $records = $DB->get_records_sql($sql, array("%D%"));
4242          $this->assertCount(6, $records);
4243  
4244          // Verify usual escaping characters work fine.
4245          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true, true, false, '\\');
4246          $records = $DB->get_records_sql($sql, array("ouc\\_"));
4247          $this->assertCount(1, $records);
4248          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true, true, false, '|');
4249          $records = $DB->get_records_sql($sql, array("ouc|%"));
4250          $this->assertCount(1, $records);
4251  
4252          // TODO: we do not require accent insensitivness yet, just make sure it does not throw errors.
4253          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true, false);
4254          $records = $DB->get_records_sql($sql, array('aui'));
4255          // $this->assertEquals(2, count($records), 'Accent insensitive LIKE searches may not be supported in all databases, this is not a problem.');
4256          $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', false, false);
4257          $records = $DB->get_records_sql($sql, array('aui'));
4258          // $this->assertEquals(3, count($records), 'Accent insensitive LIKE searches may not be supported in all databases, this is not a problem.');
4259      }
4260  
4261      /**
4262       * Test DML libraries sql_like_escape method
4263       */
4264      public function test_sql_like_escape(): void {
4265          $DB = $this->tdb;
4266          $dbman = $DB->get_manager();
4267  
4268          $table = $this->get_test_table();
4269          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4270          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4271          $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
4272          $dbman->create_table($table);
4273  
4274          $tablename = $table->getName();
4275  
4276          // Two of the records contain LIKE characters (%_), plus square brackets supported only by SQL Server (and '^-' which
4277          // should be ignored by SQL Server given they only have meaning inside square brackets).
4278          $DB->insert_record($tablename, (object) ['name' => 'lionel']);
4279          $DB->insert_record($tablename, (object) ['name' => 'lionel%_^-[0]']);
4280          $DB->insert_record($tablename, (object) ['name' => 'rick']);
4281          $DB->insert_record($tablename, (object) ['name' => 'rick%_^-[0]']);
4282  
4283          $select = $DB->sql_like('name', ':namelike');
4284          $params = ['namelike' => '%' . $DB->sql_like_escape('%_^-[0]')];
4285  
4286          // All drivers should return our two records containing wildcard characters.
4287          $this->assertEqualsCanonicalizing([
4288              'lionel%_^-[0]',
4289              'rick%_^-[0]',
4290          ], $DB->get_fieldset_select($tablename, 'name', $select, $params));
4291  
4292          // Test for unbalanced brackets.
4293          $select = $DB->sql_like('name', ':namelike');
4294          $params = ['namelike' => '%' . $DB->sql_like_escape('[') . '%'];
4295  
4296          $this->assertEqualsCanonicalizing([
4297              'lionel%_^-[0]',
4298              'rick%_^-[0]',
4299          ], $DB->get_fieldset_select($tablename, 'name', $select, $params));
4300      }
4301  
4302      public function test_coalesce() {
4303          $DB = $this->tdb;
4304  
4305          // Testing not-null occurrences, return 1st.
4306          $sql = "SELECT COALESCE('returnthis', 'orthis', 'orwhynotthis') AS test" . $DB->sql_null_from_clause();
4307          $this->assertSame('returnthis', $DB->get_field_sql($sql, array()));
4308          $sql = "SELECT COALESCE(:paramvalue, 'orthis', 'orwhynotthis') AS test" . $DB->sql_null_from_clause();
4309          $this->assertSame('returnthis', $DB->get_field_sql($sql, array('paramvalue' => 'returnthis')));
4310  
4311          // Testing null occurrences, return 2nd.
4312          $sql = "SELECT COALESCE(null, 'returnthis', 'orthis') AS test" . $DB->sql_null_from_clause();
4313          $this->assertSame('returnthis', $DB->get_field_sql($sql, array()));
4314          $sql = "SELECT COALESCE(:paramvalue, 'returnthis', 'orthis') AS test" . $DB->sql_null_from_clause();
4315          $this->assertSame('returnthis', $DB->get_field_sql($sql, array('paramvalue' => null)));
4316          $sql = "SELECT COALESCE(null, :paramvalue, 'orthis') AS test" . $DB->sql_null_from_clause();
4317          $this->assertSame('returnthis', $DB->get_field_sql($sql, array('paramvalue' => 'returnthis')));
4318  
4319          // Testing null occurrences, return 3rd.
4320          $sql = "SELECT COALESCE(null, null, 'returnthis') AS test" . $DB->sql_null_from_clause();
4321          $this->assertSame('returnthis', $DB->get_field_sql($sql, array()));
4322          $sql = "SELECT COALESCE(null, :paramvalue, 'returnthis') AS test" . $DB->sql_null_from_clause();
4323          $this->assertSame('returnthis', $DB->get_field_sql($sql, array('paramvalue' => null)));
4324          $sql = "SELECT COALESCE(null, null, :paramvalue) AS test" . $DB->sql_null_from_clause();
4325          $this->assertSame('returnthis', $DB->get_field_sql($sql, array('paramvalue' => 'returnthis')));
4326  
4327          // Testing all null occurrences, return null.
4328          // Note: under mssql, if all elements are nulls, at least one must be a "typed" null, hence
4329          // we cannot test this in a cross-db way easily, so next 2 tests are using
4330          // different queries depending of the DB family.
4331          $customnull = $DB->get_dbfamily() == 'mssql' ? 'CAST(null AS varchar)' : 'null';
4332          $sql = "SELECT COALESCE(null, null, " . $customnull . ") AS test" . $DB->sql_null_from_clause();
4333          $this->assertNull($DB->get_field_sql($sql, array()));
4334          $sql = "SELECT COALESCE(null, :paramvalue, " . $customnull . ") AS test" . $DB->sql_null_from_clause();
4335          $this->assertNull($DB->get_field_sql($sql, array('paramvalue' => null)));
4336  
4337          // Check there are not problems with whitespace strings.
4338          $sql = "SELECT COALESCE(null, :paramvalue, null) AS test" . $DB->sql_null_from_clause();
4339          $this->assertSame('', $DB->get_field_sql($sql, array('paramvalue' => '')));
4340      }
4341  
4342      public function test_sql_concat() {
4343          $DB = $this->tdb;
4344          $dbman = $DB->get_manager();
4345  
4346          // Testing all sort of values.
4347          $sql = "SELECT ".$DB->sql_concat("?", "?", "?")." AS fullname ". $DB->sql_null_from_clause();
4348          // String, some unicode chars.
4349          $params = array('name', 'áéíóú', 'name3');
4350          $this->assertSame('nameáéíóúname3', $DB->get_field_sql($sql, $params));
4351          // String, spaces and numbers.
4352          $params = array('name', '  ', 12345);
4353          $this->assertSame('name  12345', $DB->get_field_sql($sql, $params));
4354          // Float, empty and strings.
4355          $params = array(123.45, '', 'test');
4356          $this->assertSame('123.45test', $DB->get_field_sql($sql, $params));
4357          // Only integers.
4358          $params = array(12, 34, 56);
4359          $this->assertSame('123456', $DB->get_field_sql($sql, $params));
4360          // Float, null and strings.
4361          $params = array(123.45, null, 'test');
4362          $this->assertNull($DB->get_field_sql($sql, $params)); // Concatenate null with anything result = null.
4363  
4364          // Testing fieldnames + values and also integer fieldnames.
4365          $table = $this->get_test_table();
4366          $tablename = $table->getName();
4367  
4368          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4369          $table->add_field('charshort', XMLDB_TYPE_CHAR, '255');
4370          $table->add_field('charlong', XMLDB_TYPE_CHAR, '1333');
4371          $table->add_field('description', XMLDB_TYPE_TEXT, 'big');
4372          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4373          $dbman->create_table($table);
4374  
4375          // Regarding 1300 length - all drivers except Oracle support larger values (2K+), but this hits a limit on Oracle.
4376          $DB->insert_record($tablename, [
4377              'charshort' => 'áéíóú',
4378              'charlong' => str_repeat('A', 512),
4379              'description' => str_repeat('X', 1300),
4380          ]);
4381          $DB->insert_record($tablename, [
4382              'charshort' => 'dxxx',
4383              'charlong' => str_repeat('B', 512),
4384              'description' => str_repeat('Y', 1300),
4385          ]);
4386          $DB->insert_record($tablename, [
4387              'charshort' => 'bcde',
4388              'charlong' => str_repeat('C', 512),
4389              'description' => str_repeat('Z', 1300),
4390          ]);
4391  
4392          // Char (short) fieldnames and values.
4393          $fieldsql = $DB->sql_concat('charshort', "'harcoded'", '?', '?');
4394          $this->assertEqualsCanonicalizing([
4395              'áéíóúharcoded123.45test',
4396              'dxxxharcoded123.45test',
4397              'bcdeharcoded123.45test',
4398          ], $DB->get_fieldset_select($tablename, $fieldsql, '', [123.45, 'test']));
4399  
4400          // Char (long) fieldnames and values.
4401          $fieldsql = $DB->sql_concat('charlong', "'harcoded'", '?', '?');
4402          $this->assertEqualsCanonicalizing([
4403              str_repeat('A', 512) . 'harcoded123.45test',
4404              str_repeat('B', 512) . 'harcoded123.45test',
4405              str_repeat('C', 512) . 'harcoded123.45test',
4406          ], $DB->get_fieldset_select($tablename, $fieldsql, '', [123.45, 'test']));
4407  
4408          // Text fieldnames and values.
4409          $fieldsql = $DB->sql_concat('description', "'harcoded'", '?', '?');
4410          $this->assertEqualsCanonicalizing([
4411              str_repeat('X', 1300) . 'harcoded123.45test',
4412              str_repeat('Y', 1300) . 'harcoded123.45test',
4413              str_repeat('Z', 1300) . 'harcoded123.45test',
4414          ], $DB->get_fieldset_select($tablename, $fieldsql, '', [123.45, 'test']));
4415  
4416          // Integer fieldnames and values.
4417          $fieldsql = $DB->sql_concat('id', "'harcoded'", '?', '?');
4418          $this->assertEqualsCanonicalizing([
4419              '1harcoded123.45test',
4420              '2harcoded123.45test',
4421              '3harcoded123.45test',
4422          ], $DB->get_fieldset_select($tablename, $fieldsql, '', [123.45, 'test']));
4423  
4424          // All integer fieldnames.
4425          $fieldsql = $DB->sql_concat('id', 'id', 'id');
4426          $this->assertEqualsCanonicalizing([
4427              '111',
4428              '222',
4429              '333',
4430          ], $DB->get_fieldset_select($tablename, $fieldsql, ''));
4431  
4432      }
4433  
4434      public function sql_concat_join_provider() {
4435          return array(
4436              // All strings.
4437              array(
4438                  "' '",
4439                  array("'name'", "'name2'", "'name3'"),
4440                  array(),
4441                  'name name2 name3',
4442              ),
4443              // All strings using placeholders
4444              array(
4445                  "' '",
4446                  array("?", "?", "?"),
4447                  array('name', 'name2', 'name3'),
4448                  'name name2 name3',
4449              ),
4450              // All integers.
4451              array(
4452                  "' '",
4453                  array(1, 2, 3),
4454                  array(),
4455                  '1 2 3',
4456              ),
4457              // All integers using placeholders
4458              array(
4459                  "' '",
4460                  array("?", "?", "?"),
4461                  array(1, 2, 3),
4462                  '1 2 3',
4463              ),
4464              // Mix of strings and integers.
4465              array(
4466                  "' '",
4467                  array(1, "'2'", 3),
4468                  array(),
4469                  '1 2 3',
4470              ),
4471              // Mix of strings and integers using placeholders.
4472              array(
4473                  "' '",
4474                  array(1, '2', 3),
4475                  array(),
4476                  '1 2 3',
4477              ),
4478          );
4479      }
4480  
4481      /**
4482       * @dataProvider sql_concat_join_provider
4483       * @param string $concat The string to use when concatanating.
4484       * @param array $fields The fields to concatanate
4485       * @param array $params Any parameters to provide to the query
4486       * @param @string $expected The expected result
4487       */
4488      public function test_concat_join($concat, $fields, $params, $expected) {
4489          $DB = $this->tdb;
4490          $sql = "SELECT " . $DB->sql_concat_join($concat, $fields) . " AS result" . $DB->sql_null_from_clause();
4491          $result = $DB->get_field_sql($sql, $params);
4492          $this->assertEquals($expected, $result);
4493      }
4494  
4495      /**
4496       * Test DML libraries sql_group_contact method
4497       */
4498      public function test_group_concat(): void {
4499          $DB = $this->tdb;
4500          $dbman = $DB->get_manager();
4501  
4502          $table = $this->get_test_table();
4503          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4504          $table->add_field('intfield', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
4505          $table->add_field('charfield', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4506          $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
4507          $dbman->create_table($table);
4508  
4509          $tablename = $table->getName();
4510          $DB->insert_record($tablename, (object) ['intfield' => 10, 'charfield' => 'uno']);
4511          $DB->insert_record($tablename, (object) ['intfield' => 20, 'charfield' => 'dos']);
4512          $DB->insert_record($tablename, (object) ['intfield' => 20, 'charfield' => 'tres']);
4513          $DB->insert_record($tablename, (object) ['intfield' => 30, 'charfield' => 'tres']);
4514  
4515          // Test charfield => concatenated intfield ASC.
4516          $fieldsql = $DB->sql_group_concat('intfield', ', ', 'intfield ASC');
4517          $sql = "SELECT charfield, {$fieldsql} AS falias
4518                    FROM {{$tablename}}
4519                GROUP BY charfield";
4520  
4521          $this->assertEquals([
4522              'dos' => '20',
4523              'tres' => '20, 30',
4524              'uno' => '10',
4525          ], $DB->get_records_sql_menu($sql));
4526  
4527          // Test charfield => concatenated intfield DESC.
4528          $fieldsql = $DB->sql_group_concat('intfield', ', ', 'intfield DESC');
4529          $sql = "SELECT charfield, {$fieldsql} AS falias
4530                    FROM {{$tablename}}
4531                GROUP BY charfield";
4532  
4533          $this->assertEquals([
4534              'dos' => '20',
4535              'tres' => '30, 20',
4536              'uno' => '10',
4537          ], $DB->get_records_sql_menu($sql));
4538  
4539          // Test intfield => concatenated charfield ASC.
4540          $fieldsql = $DB->sql_group_concat('charfield', ', ', 'charfield ASC');
4541          $sql = "SELECT intfield, {$fieldsql} AS falias
4542                    FROM {{$tablename}}
4543                GROUP BY intfield";
4544  
4545          $this->assertEquals([
4546              10 => 'uno',
4547              20 => 'dos, tres',
4548              30 => 'tres',
4549          ], $DB->get_records_sql_menu($sql));
4550  
4551          // Test intfield => concatenated charfield DESC.
4552          $fieldsql = $DB->sql_group_concat('charfield', ', ', 'charfield DESC');
4553          $sql = "SELECT intfield, {$fieldsql} AS falias
4554                    FROM {{$tablename}}
4555                GROUP BY intfield";
4556  
4557          $this->assertEquals([
4558              10 => 'uno',
4559              20 => 'tres, dos',
4560              30 => 'tres',
4561          ], $DB->get_records_sql_menu($sql));
4562  
4563          // Assert expressions with parameters can also be used.
4564          $fieldexpr = $DB->sql_concat(':greeting', 'charfield');
4565          $fieldsql = $DB->sql_group_concat($fieldexpr, ', ', 'charfield ASC');
4566          $sql = "SELECT intfield, {$fieldsql} AS falias
4567                    FROM {{$tablename}}
4568                GROUP BY intfield";
4569          $this->assertEquals([
4570              10 => 'Hola uno',
4571              20 => 'Hola dos, Hola tres',
4572              30 => 'Hola tres',
4573          ], $DB->get_records_sql_menu($sql, ['greeting' => 'Hola ']));
4574      }
4575  
4576      /**
4577       * Test DML libraries sql_group_contact method joining tables, aggregating data from each
4578       */
4579      public function test_group_concat_join_tables(): void {
4580          $DB = $this->tdb;
4581          $dbman = $DB->get_manager();
4582  
4583          $tableparent = $this->get_test_table('parent');
4584          $tableparent->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4585          $tableparent->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4586          $tableparent->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
4587          $dbman->create_table($tableparent);
4588  
4589          $tablechild = $this->get_test_table('child');
4590          $tablechild->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4591          $tablechild->add_field('parentid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
4592          $tablechild->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4593          $tablechild->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
4594          $tablechild->add_key('parentid', XMLDB_KEY_FOREIGN, ['parentid'], $tableparent->getName(), ['id']);
4595          $dbman->create_table($tablechild);
4596  
4597          $tableparentname = $tableparent->getName();
4598          $tablechildname = $tablechild->getName();
4599  
4600          $parentone = $DB->insert_record($tableparentname, (object) ['name' => 'Alice']);
4601          $DB->insert_record($tablechildname, (object) ['parentid' => $parentone, 'name' => 'Eve']);
4602          $DB->insert_record($tablechildname, (object) ['parentid' => $parentone, 'name' => 'Charlie']);
4603  
4604          $parenttwo = $DB->insert_record($tableparentname, (object) ['name' => 'Bob']);
4605          $DB->insert_record($tablechildname, (object) ['parentid' => $parenttwo, 'name' => 'Dan']);
4606          $DB->insert_record($tablechildname, (object) ['parentid' => $parenttwo, 'name' => 'Grace']);
4607  
4608          $tableparentalias = 'p';
4609          $tablechildalias = 'c';
4610  
4611          $fieldsql = $DB->sql_group_concat("{$tablechildalias}.name", ', ', "{$tablechildalias}.name ASC");
4612  
4613          $sql = "SELECT {$tableparentalias}.name, {$fieldsql} AS falias
4614                    FROM {{$tableparentname}} {$tableparentalias}
4615                    JOIN {{$tablechildname}} {$tablechildalias} ON {$tablechildalias}.parentid = {$tableparentalias}.id
4616                GROUP BY {$tableparentalias}.name";
4617  
4618          $this->assertEqualsCanonicalizing([
4619              (object) [
4620                  'name' => 'Alice',
4621                  'falias' => 'Charlie, Eve',
4622              ],
4623              (object) [
4624                  'name' => 'Bob',
4625                  'falias' => 'Dan, Grace',
4626              ],
4627          ], $DB->get_records_sql($sql));
4628      }
4629  
4630      public function test_sql_fullname() {
4631          $DB = $this->tdb;
4632          $sql = "SELECT ".$DB->sql_fullname(':first', ':last')." AS fullname ".$DB->sql_null_from_clause();
4633          $params = array('first'=>'Firstname', 'last'=>'Surname');
4634          $this->assertEquals("Firstname Surname", $DB->get_field_sql($sql, $params));
4635      }
4636  
4637      public function test_sql_order_by_text() {
4638          $DB = $this->tdb;
4639          $dbman = $DB->get_manager();
4640  
4641          $table = $this->get_test_table();
4642          $tablename = $table->getName();
4643  
4644          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4645          $table->add_field('description', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
4646          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4647          $dbman->create_table($table);
4648  
4649          $DB->insert_record($tablename, array('description'=>'abcd'));
4650          $DB->insert_record($tablename, array('description'=>'dxxx'));
4651          $DB->insert_record($tablename, array('description'=>'bcde'));
4652  
4653          $sql = "SELECT * FROM {{$tablename}} ORDER BY ".$DB->sql_order_by_text('description');
4654          $records = $DB->get_records_sql($sql);
4655          $first = array_shift($records);
4656          $this->assertEquals(1, $first->id);
4657          $second = array_shift($records);
4658          $this->assertEquals(3, $second->id);
4659          $last = array_shift($records);
4660          $this->assertEquals(2, $last->id);
4661      }
4662  
4663      /**
4664       * Test DML libraries sql_order_by_null method
4665       */
4666      public function test_sql_order_by_null(): void {
4667          $DB = $this->tdb;
4668          $dbman = $DB->get_manager();
4669  
4670          $table = $this->get_test_table();
4671          $tablename = $table->getName();
4672  
4673          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4674          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4675          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4676          $dbman->create_table($table);
4677  
4678          $DB->insert_record($tablename, array('name' => 'aaaa'));
4679          $DB->insert_record($tablename, array('name' => 'bbbb'));
4680          $DB->insert_record($tablename, array('name' => ''));
4681          $DB->insert_record($tablename, array('name' => null));
4682  
4683          $sql = "SELECT * FROM {{$tablename}} ORDER BY ".$DB->sql_order_by_null('name');
4684          $records = $DB->get_records_sql($sql);
4685          $this->assertEquals(null, array_shift($records)->name);
4686          $this->assertEquals('', array_shift($records)->name);
4687          $this->assertEquals('aaaa', array_shift($records)->name);
4688          $this->assertEquals('bbbb', array_shift($records)->name);
4689  
4690          $sql = "SELECT * FROM {{$tablename}} ORDER BY ".$DB->sql_order_by_null('name', SORT_DESC);
4691          $records = $DB->get_records_sql($sql);
4692          $this->assertEquals('bbbb', array_shift($records)->name);
4693          $this->assertEquals('aaaa', array_shift($records)->name);
4694          $this->assertEquals('', array_shift($records)->name);
4695          $this->assertEquals(null, array_shift($records)->name);
4696      }
4697  
4698      public function test_sql_substring() {
4699          $DB = $this->tdb;
4700          $dbman = $DB->get_manager();
4701  
4702          $table = $this->get_test_table();
4703          $tablename = $table->getName();
4704  
4705          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4706          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4707          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4708          $dbman->create_table($table);
4709  
4710          $string = 'abcdefghij';
4711  
4712          $DB->insert_record($tablename, array('name'=>$string));
4713  
4714          $sql = "SELECT id, ".$DB->sql_substr("name", 5)." AS name FROM {{$tablename}}";
4715          $record = $DB->get_record_sql($sql);
4716          $this->assertEquals(substr($string, 5-1), $record->name);
4717  
4718          $sql = "SELECT id, ".$DB->sql_substr("name", 5, 2)." AS name FROM {{$tablename}}";
4719          $record = $DB->get_record_sql($sql);
4720          $this->assertEquals(substr($string, 5-1, 2), $record->name);
4721  
4722          try {
4723              // Silence php warning.
4724              @$DB->sql_substr("name");
4725              $this->fail("Expecting an exception, none occurred");
4726          } catch (\moodle_exception $e) {
4727              $this->assertInstanceOf('coding_exception', $e);
4728          } catch (\Error $error) {
4729              // PHP 7.1 throws Error even earlier.
4730              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
4731          }
4732  
4733          // Cover the function using placeholders in all positions.
4734          $start = 4;
4735          $length = 2;
4736          // 1st param (target).
4737          $sql = "SELECT id, ".$DB->sql_substr(":param1", $start)." AS name FROM {{$tablename}}";
4738          $record = $DB->get_record_sql($sql, array('param1' => $string));
4739          $this->assertEquals(substr($string, $start - 1), $record->name); // PHP's substr is 0-based.
4740          // 2nd param (start).
4741          $sql = "SELECT id, ".$DB->sql_substr("name", ":param1")." AS name FROM {{$tablename}}";
4742          $record = $DB->get_record_sql($sql, array('param1' => $start));
4743          $this->assertEquals(substr($string, $start - 1), $record->name); // PHP's substr is 0-based.
4744          // 3rd param (length).
4745          $sql = "SELECT id, ".$DB->sql_substr("name", $start, ":param1")." AS name FROM {{$tablename}}";
4746          $record = $DB->get_record_sql($sql, array('param1' => $length));
4747          $this->assertEquals(substr($string, $start - 1,  $length), $record->name); // PHP's substr is 0-based.
4748          // All together.
4749          $sql = "SELECT id, ".$DB->sql_substr(":param1", ":param2", ":param3")." AS name FROM {{$tablename}}";
4750          $record = $DB->get_record_sql($sql, array('param1' => $string, 'param2' => $start, 'param3' => $length));
4751          $this->assertEquals(substr($string, $start - 1,  $length), $record->name); // PHP's substr is 0-based.
4752  
4753          // Try also with some expression passed.
4754          $sql = "SELECT id, ".$DB->sql_substr("name", "(:param1 + 1) - 1")." AS name FROM {{$tablename}}";
4755          $record = $DB->get_record_sql($sql, array('param1' => $start));
4756          $this->assertEquals(substr($string, $start - 1), $record->name); // PHP's substr is 0-based.
4757      }
4758  
4759      public function test_sql_length() {
4760          $DB = $this->tdb;
4761          $this->assertEquals($DB->get_field_sql(
4762              "SELECT ".$DB->sql_length("'aeiou'").$DB->sql_null_from_clause()), 5);
4763          $this->assertEquals($DB->get_field_sql(
4764              "SELECT ".$DB->sql_length("'áéíóú'").$DB->sql_null_from_clause()), 5);
4765      }
4766  
4767      public function test_sql_position() {
4768          $DB = $this->tdb;
4769          $this->assertEquals($DB->get_field_sql(
4770              "SELECT ".$DB->sql_position("'ood'", "'Moodle'").$DB->sql_null_from_clause()), 2);
4771          $this->assertEquals($DB->get_field_sql(
4772              "SELECT ".$DB->sql_position("'Oracle'", "'Moodle'").$DB->sql_null_from_clause()), 0);
4773      }
4774  
4775      public function test_sql_empty() {
4776          $DB = $this->tdb;
4777          $dbman = $DB->get_manager();
4778  
4779          $table = $this->get_test_table();
4780          $tablename = $table->getName();
4781  
4782          $this->assertSame('', $DB->sql_empty()); // Since 2.5 the hack is applied automatically to all bound params.
4783          $this->assertDebuggingCalled();
4784  
4785          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4786          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4787          $table->add_field('namenotnull', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, 'default value');
4788          $table->add_field('namenotnullnodeflt', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4789          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4790          $dbman->create_table($table);
4791  
4792          $DB->insert_record($tablename, array('name'=>'', 'namenotnull'=>''));
4793          $DB->insert_record($tablename, array('name'=>null));
4794          $DB->insert_record($tablename, array('name'=>'lalala'));
4795          $DB->insert_record($tablename, array('name'=>0));
4796  
4797          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE name = ?", array(''));
4798          $this->assertCount(1, $records);
4799          $record = reset($records);
4800          $this->assertSame('', $record->name);
4801  
4802          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE namenotnull = ?", array(''));
4803          $this->assertCount(1, $records);
4804          $record = reset($records);
4805          $this->assertSame('', $record->namenotnull);
4806  
4807          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE namenotnullnodeflt = ?", array(''));
4808          $this->assertCount(4, $records);
4809          $record = reset($records);
4810          $this->assertSame('', $record->namenotnullnodeflt);
4811      }
4812  
4813      public function test_sql_isempty() {
4814          $DB = $this->tdb;
4815          $dbman = $DB->get_manager();
4816  
4817          $table = $this->get_test_table();
4818          $tablename = $table->getName();
4819  
4820          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4821          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4822          $table->add_field('namenull', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4823          $table->add_field('description', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL, null, null);
4824          $table->add_field('descriptionnull', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
4825          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4826          $dbman->create_table($table);
4827  
4828          $DB->insert_record($tablename, array('name'=>'',   'namenull'=>'',   'description'=>'',   'descriptionnull'=>''));
4829          $DB->insert_record($tablename, array('name'=>'??', 'namenull'=>null, 'description'=>'??', 'descriptionnull'=>null));
4830          $DB->insert_record($tablename, array('name'=>'la', 'namenull'=>'la', 'description'=>'la', 'descriptionnull'=>'lalala'));
4831          $DB->insert_record($tablename, array('name'=>0,    'namenull'=>0,    'description'=>0,    'descriptionnull'=>0));
4832  
4833          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isempty($tablename, 'name', false, false));
4834          $this->assertCount(1, $records);
4835          $record = reset($records);
4836          $this->assertSame('', $record->name);
4837  
4838          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isempty($tablename, 'namenull', true, false));
4839          $this->assertCount(1, $records);
4840          $record = reset($records);
4841          $this->assertSame('', $record->namenull);
4842  
4843          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isempty($tablename, 'description', false, true));
4844          $this->assertCount(1, $records);
4845          $record = reset($records);
4846          $this->assertSame('', $record->description);
4847  
4848          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isempty($tablename, 'descriptionnull', true, true));
4849          $this->assertCount(1, $records);
4850          $record = reset($records);
4851          $this->assertSame('', $record->descriptionnull);
4852      }
4853  
4854      public function test_sql_isnotempty() {
4855          $DB = $this->tdb;
4856          $dbman = $DB->get_manager();
4857  
4858          $table = $this->get_test_table();
4859          $tablename = $table->getName();
4860  
4861          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4862          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4863          $table->add_field('namenull', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4864          $table->add_field('description', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL, null, null);
4865          $table->add_field('descriptionnull', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
4866          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4867          $dbman->create_table($table);
4868  
4869          $DB->insert_record($tablename, array('name'=>'',   'namenull'=>'',   'description'=>'',   'descriptionnull'=>''));
4870          $DB->insert_record($tablename, array('name'=>'??', 'namenull'=>null, 'description'=>'??', 'descriptionnull'=>null));
4871          $DB->insert_record($tablename, array('name'=>'la', 'namenull'=>'la', 'description'=>'la', 'descriptionnull'=>'lalala'));
4872          $DB->insert_record($tablename, array('name'=>0,    'namenull'=>0,    'description'=>0,    'descriptionnull'=>0));
4873  
4874          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isnotempty($tablename, 'name', false, false));
4875          $this->assertCount(3, $records);
4876          $record = reset($records);
4877          $this->assertSame('??', $record->name);
4878  
4879          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isnotempty($tablename, 'namenull', true, false));
4880          $this->assertCount(2, $records); // Nulls aren't comparable (so they aren't "not empty"). SQL expected behaviour.
4881          $record = reset($records);
4882          $this->assertSame('la', $record->namenull); // So 'la' is the first non-empty 'namenull' record.
4883  
4884          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isnotempty($tablename, 'description', false, true));
4885          $this->assertCount(3, $records);
4886          $record = reset($records);
4887          $this->assertSame('??', $record->description);
4888  
4889          $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isnotempty($tablename, 'descriptionnull', true, true));
4890          $this->assertCount(2, $records); // Nulls aren't comparable (so they aren't "not empty"). SQL expected behaviour.
4891          $record = reset($records);
4892          $this->assertSame('lalala', $record->descriptionnull); // So 'lalala' is the first non-empty 'descriptionnull' record.
4893      }
4894  
4895      public function test_sql_regex() {
4896          $DB = $this->tdb;
4897          $dbman = $DB->get_manager();
4898          if (!$DB->sql_regex_supported()) {
4899              $this->markTestSkipped($DB->get_name().' does not support regular expressions');
4900          }
4901  
4902          $table = $this->get_test_table();
4903          $tablename = $table->getName();
4904  
4905          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4906          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4907          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4908          $dbman->create_table($table);
4909  
4910          $DB->insert_record($tablename, array('name'=>'LALALA'));
4911          $DB->insert_record($tablename, array('name'=>'holaaa'));
4912          $DB->insert_record($tablename, array('name'=>'aouch'));
4913  
4914          // Regex /a$/i (case-insensitive).
4915          $sql = "SELECT * FROM {{$tablename}} WHERE name ".$DB->sql_regex()." ?";
4916          $params = array('a$');
4917          $records = $DB->get_records_sql($sql, $params);
4918          $this->assertCount(2, $records);
4919  
4920          // Regex ! (not) /.a/i (case insensitive).
4921          $sql = "SELECT * FROM {{$tablename}} WHERE name ".$DB->sql_regex(false)." ?";
4922          $params = array('.a');
4923          $records = $DB->get_records_sql($sql, $params);
4924          $this->assertCount(1, $records);
4925  
4926          // Regex /a$/ (case-sensitive).
4927          $sql = "SELECT * FROM {{$tablename}} WHERE name ".$DB->sql_regex(true, true)." ?";
4928          $params = array('a$');
4929          $records = $DB->get_records_sql($sql, $params);
4930          $this->assertCount(1, $records);
4931  
4932          // Regex ! (not) /.a/ (case sensitive).
4933          $sql = "SELECT * FROM {{$tablename}} WHERE name ".$DB->sql_regex(false, true)." ?";
4934          $params = array('.a');
4935          $records = $DB->get_records_sql($sql, $params);
4936          $this->assertCount(2, $records);
4937  
4938      }
4939  
4940      /**
4941       * Test some complicated variations of set_field_select.
4942       */
4943      public function test_set_field_select_complicated() {
4944          $DB = $this->tdb;
4945          $dbman = $DB->get_manager();
4946  
4947          $table = $this->get_test_table();
4948          $tablename = $table->getName();
4949  
4950          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4951          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
4952          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4953          $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
4954          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4955          $dbman->create_table($table);
4956  
4957          $DB->insert_record($tablename, array('course' => 3, 'content' => 'hello', 'name'=>'xyz'));
4958          $DB->insert_record($tablename, array('course' => 3, 'content' => 'world', 'name'=>'abc'));
4959          $DB->insert_record($tablename, array('course' => 5, 'content' => 'hello', 'name'=>'def'));
4960          $DB->insert_record($tablename, array('course' => 2, 'content' => 'universe', 'name'=>'abc'));
4961          // This SQL is a tricky case because we are selecting from the same table we are updating.
4962          $sql = 'id IN (SELECT outerq.id from (SELECT innerq.id from {' . $tablename . '} innerq WHERE course = 3) outerq)';
4963          $DB->set_field_select($tablename, 'name', 'ghi', $sql);
4964  
4965          $this->assertSame(2, $DB->count_records_select($tablename, 'name = ?', array('ghi')));
4966  
4967      }
4968  
4969      /**
4970       * Test some more complex SQL syntax which moodle uses and depends on to work
4971       * useful to determine if new database libraries can be supported.
4972       */
4973      public function test_get_records_sql_complicated() {
4974          $DB = $this->tdb;
4975          $dbman = $DB->get_manager();
4976  
4977          $table = $this->get_test_table();
4978          $tablename = $table->getName();
4979  
4980          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4981          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
4982          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4983          $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
4984          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4985          $dbman->create_table($table);
4986  
4987          $DB->insert_record($tablename, array('course' => 3, 'content' => 'hello', 'name'=>'xyz'));
4988          $DB->insert_record($tablename, array('course' => 3, 'content' => 'world', 'name'=>'abc'));
4989          $DB->insert_record($tablename, array('course' => 5, 'content' => 'hello', 'name'=>'def'));
4990          $DB->insert_record($tablename, array('course' => 2, 'content' => 'universe', 'name'=>'abc'));
4991  
4992          // Test grouping by expressions in the query. MDL-26819. Note that there are 4 ways:
4993          // - By column position (GROUP by 1) - Not supported by mssql & oracle
4994          // - By column name (GROUP by course) - Supported by all, but leading to wrong results
4995          // - By column alias (GROUP by casecol) - Not supported by mssql & oracle
4996          // - By complete expression (GROUP BY CASE ...) - 100% cross-db, this test checks it
4997          $sql = "SELECT (CASE WHEN course = 3 THEN 1 ELSE 0 END) AS casecol,
4998                         COUNT(1) AS countrecs,
4999                         MAX(name) AS maxname
5000                    FROM {{$tablename}}
5001                GROUP BY CASE WHEN course = 3 THEN 1 ELSE 0 END
5002                ORDER BY casecol DESC";
5003          $result = array(
5004              1 => (object)array('casecol' => 1, 'countrecs' => 2, 'maxname' => 'xyz'),
5005              0 => (object)array('casecol' => 0, 'countrecs' => 2, 'maxname' => 'def'));
5006          $records = $DB->get_records_sql($sql, null);
5007          $this->assertEquals($result, $records);
5008  
5009          // Another grouping by CASE expression just to ensure it works ok for multiple WHEN.
5010          $sql = "SELECT CASE name
5011                              WHEN 'xyz' THEN 'last'
5012                              WHEN 'def' THEN 'mid'
5013                              WHEN 'abc' THEN 'first'
5014                         END AS casecol,
5015                         COUNT(1) AS countrecs,
5016                         MAX(name) AS maxname
5017                    FROM {{$tablename}}
5018                GROUP BY CASE name
5019                             WHEN 'xyz' THEN 'last'
5020                             WHEN 'def' THEN 'mid'
5021                             WHEN 'abc' THEN 'first'
5022                         END
5023                ORDER BY casecol DESC";
5024          $result = array(
5025              'mid'  => (object)array('casecol' => 'mid', 'countrecs' => 1, 'maxname' => 'def'),
5026              'last' => (object)array('casecol' => 'last', 'countrecs' => 1, 'maxname' => 'xyz'),
5027              'first'=> (object)array('casecol' => 'first', 'countrecs' => 2, 'maxname' => 'abc'));
5028          $records = $DB->get_records_sql($sql, null);
5029          $this->assertEquals($result, $records);
5030  
5031          // Test CASE expressions in the ORDER BY clause - used by MDL-34657.
5032          $sql = "SELECT id, course, name
5033                    FROM {{$tablename}}
5034                ORDER BY CASE WHEN (course = 5 OR name  = 'xyz') THEN 0 ELSE 1 END, name, course";
5035          // First, records matching the course = 5 OR name = 'xyz', then the rest. Each.
5036          // group ordered by name and course.
5037          $result = array(
5038              3 => (object)array('id' => 3, 'course' => 5, 'name' => 'def'),
5039              1 => (object)array('id' => 1, 'course' => 3, 'name' => 'xyz'),
5040              4 => (object)array('id' => 4, 'course' => 2, 'name' => 'abc'),
5041              2 => (object)array('id' => 2, 'course' => 3, 'name' => 'abc'));
5042          $records = $DB->get_records_sql($sql, null);
5043          $this->assertEquals($result, $records);
5044          // Verify also array keys, order is important in this test.
5045          $this->assertEquals(array_keys($result), array_keys($records));
5046  
5047          // Test limits in queries with DISTINCT/ALL clauses and multiple whitespace. MDL-25268.
5048          $sql = "SELECT   DISTINCT   course
5049                    FROM {{$tablename}}
5050                   ORDER BY course";
5051          // Only limitfrom.
5052          $records = $DB->get_records_sql($sql, null, 1);
5053          $this->assertCount(2, $records);
5054          $this->assertEquals(3, reset($records)->course);
5055          $this->assertEquals(5, next($records)->course);
5056          // Only limitnum.
5057          $records = $DB->get_records_sql($sql, null, 0, 2);
5058          $this->assertCount(2, $records);
5059          $this->assertEquals(2, reset($records)->course);
5060          $this->assertEquals(3, next($records)->course);
5061          // Both limitfrom and limitnum.
5062          $records = $DB->get_records_sql($sql, null, 2, 2);
5063          $this->assertCount(1, $records);
5064          $this->assertEquals(5, reset($records)->course);
5065  
5066          // We have sql like this in moodle, this syntax breaks on older versions of sqlite for example..
5067          $sql = "SELECT a.id AS id, a.course AS course
5068                    FROM {{$tablename}} a
5069                    JOIN (SELECT * FROM {{$tablename}}) b ON a.id = b.id
5070                   WHERE a.course = ?";
5071  
5072          $records = $DB->get_records_sql($sql, array(3));
5073          $this->assertCount(2, $records);
5074          $this->assertEquals(1, reset($records)->id);
5075          $this->assertEquals(2, next($records)->id);
5076  
5077          // Do NOT try embedding sql_xxxx() helper functions in conditions array of count_records(), they don't break params/binding!
5078          $count = $DB->count_records_select($tablename, "course = :course AND ".$DB->sql_compare_text('content')." = :content", array('course' => 3, 'content' => 'hello'));
5079          $this->assertEquals(1, $count);
5080  
5081          // Test int x string comparison.
5082          $sql = "SELECT *
5083                    FROM {{$tablename}} c
5084                   WHERE name = ?";
5085          $this->assertCount(0, $DB->get_records_sql($sql, array(10)));
5086          $this->assertCount(0, $DB->get_records_sql($sql, array("10")));
5087          $DB->insert_record($tablename, array('course' => 7, 'content' => 'xx', 'name'=>'1'));
5088          $DB->insert_record($tablename, array('course' => 7, 'content' => 'yy', 'name'=>'2'));
5089          $this->assertCount(1, $DB->get_records_sql($sql, array(1)));
5090          $this->assertCount(1, $DB->get_records_sql($sql, array("1")));
5091          $this->assertCount(0, $DB->get_records_sql($sql, array(10)));
5092          $this->assertCount(0, $DB->get_records_sql($sql, array("10")));
5093          $DB->insert_record($tablename, array('course' => 7, 'content' => 'xx', 'name'=>'1abc'));
5094          $this->assertCount(1, $DB->get_records_sql($sql, array(1)));
5095          $this->assertCount(1, $DB->get_records_sql($sql, array("1")));
5096  
5097          // Test get_in_or_equal() with a big number of elements. Note that ideally
5098          // we should be detecting and warning about any use over, say, 200 elements
5099          // And recommend to change code to use subqueries and/or chunks instead.
5100          $currentcount = $DB->count_records($tablename);
5101          $numelements = 10000; // Verify that we can handle 10000 elements (crazy!)
5102          $values = range(1, $numelements);
5103  
5104          list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM); // With QM params.
5105          $sql = "SELECT *
5106                    FROM {{$tablename}}
5107                   WHERE id $insql";
5108          $results = $DB->get_records_sql($sql, $inparams);
5109          $this->assertCount($currentcount, $results);
5110  
5111          list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED); // With NAMED params.
5112          $sql = "SELECT *
5113                    FROM {{$tablename}}
5114                   WHERE id $insql";
5115          $results = $DB->get_records_sql($sql, $inparams);
5116          $this->assertCount($currentcount, $results);
5117      }
5118  
5119      public function test_replace_all_text() {
5120          $DB = $this->tdb;
5121          $dbman = $DB->get_manager();
5122  
5123          if (!$DB->replace_all_text_supported()) {
5124              $this->markTestSkipped($DB->get_name().' does not support replacing of texts');
5125          }
5126  
5127          $table = $this->get_test_table();
5128          $tablename = $table->getName();
5129  
5130          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5131          $table->add_field('name', XMLDB_TYPE_CHAR, '20', null, null);
5132          $table->add_field('intro', XMLDB_TYPE_TEXT, 'big', null, null);
5133          // Add a CHAR field named using a word reserved for all the supported DB servers.
5134          $table->add_field('where', XMLDB_TYPE_CHAR, '20', null, null, null, 'localhost');
5135          // Add a TEXT field named using a word reserved for all the supported DB servers.
5136          $table->add_field('from', XMLDB_TYPE_TEXT, 'big', null, null);
5137          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5138          $dbman->create_table($table);
5139  
5140          $fromfield = $dbman->generator->getEncQuoted('from');
5141          $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES (NULL,NULL,'localhost')");
5142          $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('','','localhost')");
5143          $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('xxyy','vvzz','localhost')");
5144          $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('aa bb aa bb','cc dd cc aa','localhost')");
5145          $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('kkllll','kkllll','localhost')");
5146  
5147          $expected = $DB->get_records($tablename, array(), 'id ASC');
5148          $idx = 1;
5149          $id1 = $id2 = $id3 = $id4 = $id5 = 0;
5150          foreach (array_keys($expected) as $identifier) {
5151              ${"id$idx"} = (string)$identifier;
5152              $idx++;
5153          }
5154  
5155          $columns = $DB->get_columns($tablename);
5156  
5157          // Replace should work even with columns named using a reserved word.
5158          $this->assertEquals('C', $columns['where']->meta_type);
5159          $this->assertEquals('localhost', $expected[$id1]->where);
5160          $this->assertEquals('localhost', $expected[$id2]->where);
5161          $this->assertEquals('localhost', $expected[$id3]->where);
5162          $this->assertEquals('localhost', $expected[$id4]->where);
5163          $this->assertEquals('localhost', $expected[$id5]->where);
5164          $DB->replace_all_text($tablename, $columns['where'], 'localhost', '::1');
5165          $result = $DB->get_records($tablename, array(), 'id ASC');
5166          $expected[$id1]->where = '::1';
5167          $expected[$id2]->where = '::1';
5168          $expected[$id3]->where = '::1';
5169          $expected[$id4]->where = '::1';
5170          $expected[$id5]->where = '::1';
5171          $this->assertEquals($expected, $result);
5172          $this->assertEquals('X', $columns['from']->meta_type);
5173          $DB->replace_all_text($tablename, $columns['from'], 'localhost', '127.0.0.1');
5174          $result = $DB->get_records($tablename, array(), 'id ASC');
5175          $expected[$id1]->from = '127.0.0.1';
5176          $expected[$id2]->from = '127.0.0.1';
5177          $expected[$id3]->from = '127.0.0.1';
5178          $expected[$id4]->from = '127.0.0.1';
5179          $expected[$id5]->from = '127.0.0.1';
5180          $this->assertEquals($expected, $result);
5181  
5182          $DB->replace_all_text($tablename, $columns['name'], 'aa', 'o');
5183          $result = $DB->get_records($tablename, array(), 'id ASC');
5184          $expected[$id4]->name = 'o bb o bb';
5185          $this->assertEquals($expected, $result);
5186  
5187          $DB->replace_all_text($tablename, $columns['intro'], 'aa', 'o');
5188          $result = $DB->get_records($tablename, array(), 'id ASC');
5189          $expected[$id4]->intro = 'cc dd cc o';
5190          $this->assertEquals($expected, $result);
5191  
5192          $DB->replace_all_text($tablename, $columns['name'], '_', '*');
5193          $DB->replace_all_text($tablename, $columns['name'], '?', '*');
5194          $DB->replace_all_text($tablename, $columns['name'], '%', '*');
5195          $DB->replace_all_text($tablename, $columns['intro'], '_', '*');
5196          $DB->replace_all_text($tablename, $columns['intro'], '?', '*');
5197          $DB->replace_all_text($tablename, $columns['intro'], '%', '*');
5198          $result = $DB->get_records($tablename, array(), 'id ASC');
5199          $this->assertEquals($expected, $result);
5200  
5201          $long = '1234567890123456789';
5202          $DB->replace_all_text($tablename, $columns['name'], 'kk', $long);
5203          $result = $DB->get_records($tablename, array(), 'id ASC');
5204          $expected[$id5]->name = \core_text::substr($long.'llll', 0, 20);
5205          $this->assertEquals($expected, $result);
5206  
5207          $DB->replace_all_text($tablename, $columns['intro'], 'kk', $long);
5208          $result = $DB->get_records($tablename, array(), 'id ASC');
5209          $expected[$id5]->intro = $long.'llll';
5210          $this->assertEquals($expected, $result);
5211      }
5212  
5213      public function test_onelevel_commit() {
5214          $DB = $this->tdb;
5215          $dbman = $DB->get_manager();
5216  
5217          $table = $this->get_test_table();
5218          $tablename = $table->getName();
5219  
5220          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5221          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5222          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5223          $dbman->create_table($table);
5224  
5225          $transaction = $DB->start_delegated_transaction();
5226          $data = (object)array('course'=>3);
5227          $this->assertEquals(0, $DB->count_records($tablename));
5228          $DB->insert_record($tablename, $data);
5229          $this->assertEquals(1, $DB->count_records($tablename));
5230          $transaction->allow_commit();
5231          $this->assertEquals(1, $DB->count_records($tablename));
5232      }
5233  
5234      public function test_transaction_ignore_error_trouble() {
5235          $DB = $this->tdb;
5236          $dbman = $DB->get_manager();
5237  
5238          $table = $this->get_test_table();
5239          $tablename = $table->getName();
5240  
5241          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5242          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5243          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5244          $table->add_index('course', XMLDB_INDEX_UNIQUE, array('course'));
5245          $dbman->create_table($table);
5246  
5247          // Test error on SQL_QUERY_INSERT.
5248          $transaction = $DB->start_delegated_transaction();
5249          $this->assertEquals(0, $DB->count_records($tablename));
5250          $DB->insert_record($tablename, (object)array('course'=>1));
5251          $this->assertEquals(1, $DB->count_records($tablename));
5252          try {
5253              $DB->insert_record($tablename, (object)array('course'=>1));
5254          } catch (\Exception $e) {
5255              // This must be ignored and it must not roll back the whole transaction.
5256          }
5257          $DB->insert_record($tablename, (object)array('course'=>2));
5258          $this->assertEquals(2, $DB->count_records($tablename));
5259          $transaction->allow_commit();
5260          $this->assertEquals(2, $DB->count_records($tablename));
5261          $this->assertFalse($DB->is_transaction_started());
5262  
5263          // Test error on SQL_QUERY_SELECT.
5264          $DB->delete_records($tablename);
5265          $transaction = $DB->start_delegated_transaction();
5266          $this->assertEquals(0, $DB->count_records($tablename));
5267          $DB->insert_record($tablename, (object)array('course'=>1));
5268          $this->assertEquals(1, $DB->count_records($tablename));
5269          try {
5270              $DB->get_records_sql('s e l e c t');
5271          } catch (\moodle_exception $e) {
5272              // This must be ignored and it must not roll back the whole transaction.
5273          }
5274          $DB->insert_record($tablename, (object)array('course'=>2));
5275          $this->assertEquals(2, $DB->count_records($tablename));
5276          $transaction->allow_commit();
5277          $this->assertEquals(2, $DB->count_records($tablename));
5278          $this->assertFalse($DB->is_transaction_started());
5279  
5280          // Test error on structure SQL_QUERY_UPDATE.
5281          $DB->delete_records($tablename);
5282          $transaction = $DB->start_delegated_transaction();
5283          $this->assertEquals(0, $DB->count_records($tablename));
5284          $DB->insert_record($tablename, (object)array('course'=>1));
5285          $this->assertEquals(1, $DB->count_records($tablename));
5286          try {
5287              $DB->execute('xxxx');
5288          } catch (\moodle_exception $e) {
5289              // This must be ignored and it must not roll back the whole transaction.
5290          }
5291          $DB->insert_record($tablename, (object)array('course'=>2));
5292          $this->assertEquals(2, $DB->count_records($tablename));
5293          $transaction->allow_commit();
5294          $this->assertEquals(2, $DB->count_records($tablename));
5295          $this->assertFalse($DB->is_transaction_started());
5296  
5297          // Test error on structure SQL_QUERY_STRUCTURE.
5298          $DB->delete_records($tablename);
5299          $transaction = $DB->start_delegated_transaction();
5300          $this->assertEquals(0, $DB->count_records($tablename));
5301          $DB->insert_record($tablename, (object)array('course'=>1));
5302          $this->assertEquals(1, $DB->count_records($tablename));
5303          try {
5304              $DB->change_database_structure('xxxx');
5305          } catch (\moodle_exception $e) {
5306              // This must be ignored and it must not roll back the whole transaction.
5307          }
5308          $DB->insert_record($tablename, (object)array('course'=>2));
5309          $this->assertEquals(2, $DB->count_records($tablename));
5310          $transaction->allow_commit();
5311          $this->assertEquals(2, $DB->count_records($tablename));
5312          $this->assertFalse($DB->is_transaction_started());
5313  
5314          // NOTE: SQL_QUERY_STRUCTURE is intentionally not tested here because it should never fail.
5315      }
5316  
5317      public function test_onelevel_rollback() {
5318          $DB = $this->tdb;
5319          $dbman = $DB->get_manager();
5320  
5321          $table = $this->get_test_table();
5322          $tablename = $table->getName();
5323  
5324          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5325          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5326          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5327          $dbman->create_table($table);
5328  
5329          // This might in fact encourage ppl to migrate from myisam to innodb.
5330  
5331          $transaction = $DB->start_delegated_transaction();
5332          $data = (object)array('course'=>3);
5333          $this->assertEquals(0, $DB->count_records($tablename));
5334          $DB->insert_record($tablename, $data);
5335          $this->assertEquals(1, $DB->count_records($tablename));
5336          try {
5337              $transaction->rollback(new \Exception('test'));
5338              $this->fail('transaction rollback must rethrow exception');
5339          } catch (\Exception $e) {
5340              // Ignored.
5341          }
5342          $this->assertEquals(0, $DB->count_records($tablename));
5343      }
5344  
5345      public function test_nested_transactions() {
5346          $DB = $this->tdb;
5347          $dbman = $DB->get_manager();
5348  
5349          $table = $this->get_test_table();
5350          $tablename = $table->getName();
5351  
5352          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5353          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5354          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5355          $dbman->create_table($table);
5356  
5357          // Two level commit.
5358          $this->assertFalse($DB->is_transaction_started());
5359          $transaction1 = $DB->start_delegated_transaction();
5360          $this->assertTrue($DB->is_transaction_started());
5361          $data = (object)array('course'=>3);
5362          $DB->insert_record($tablename, $data);
5363          $transaction2 = $DB->start_delegated_transaction();
5364          $data = (object)array('course'=>4);
5365          $DB->insert_record($tablename, $data);
5366          $transaction2->allow_commit();
5367          $this->assertTrue($DB->is_transaction_started());
5368          $transaction1->allow_commit();
5369          $this->assertFalse($DB->is_transaction_started());
5370          $this->assertEquals(2, $DB->count_records($tablename));
5371  
5372          $DB->delete_records($tablename);
5373  
5374          // Rollback from top level.
5375          $transaction1 = $DB->start_delegated_transaction();
5376          $data = (object)array('course'=>3);
5377          $DB->insert_record($tablename, $data);
5378          $transaction2 = $DB->start_delegated_transaction();
5379          $data = (object)array('course'=>4);
5380          $DB->insert_record($tablename, $data);
5381          $transaction2->allow_commit();
5382          try {
5383              $transaction1->rollback(new \Exception('test'));
5384              $this->fail('transaction rollback must rethrow exception');
5385          } catch (\Exception $e) {
5386              $this->assertEquals(get_class($e), 'Exception');
5387          }
5388          $this->assertEquals(0, $DB->count_records($tablename));
5389  
5390          $DB->delete_records($tablename);
5391  
5392          // Rollback from nested level.
5393          $transaction1 = $DB->start_delegated_transaction();
5394          $data = (object)array('course'=>3);
5395          $DB->insert_record($tablename, $data);
5396          $transaction2 = $DB->start_delegated_transaction();
5397          $data = (object)array('course'=>4);
5398          $DB->insert_record($tablename, $data);
5399          try {
5400              $transaction2->rollback(new \Exception('test'));
5401              $this->fail('transaction rollback must rethrow exception');
5402          } catch (\Exception $e) {
5403              $this->assertEquals(get_class($e), 'Exception');
5404          }
5405          $this->assertEquals(2, $DB->count_records($tablename)); // Not rolled back yet.
5406          try {
5407              $transaction1->allow_commit();
5408          } catch (\moodle_exception $e) {
5409              $this->assertInstanceOf('dml_transaction_exception', $e);
5410          }
5411          $this->assertEquals(2, $DB->count_records($tablename)); // Not rolled back yet.
5412          // The forced rollback is done from the default_exception handler and similar places,
5413          // let's do it manually here.
5414          $this->assertTrue($DB->is_transaction_started());
5415          $DB->force_transaction_rollback();
5416          $this->assertFalse($DB->is_transaction_started());
5417          $this->assertEquals(0, $DB->count_records($tablename)); // Finally rolled back.
5418  
5419          $DB->delete_records($tablename);
5420  
5421          // Test interactions of recordset and transactions - this causes problems in SQL Server.
5422          $table2 = $this->get_test_table('2');
5423          $tablename2 = $table2->getName();
5424  
5425          $table2->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5426          $table2->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5427          $table2->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5428          $dbman->create_table($table2);
5429  
5430          $DB->insert_record($tablename, array('course'=>1));
5431          $DB->insert_record($tablename, array('course'=>2));
5432          $DB->insert_record($tablename, array('course'=>3));
5433  
5434          $DB->insert_record($tablename2, array('course'=>5));
5435          $DB->insert_record($tablename2, array('course'=>6));
5436          $DB->insert_record($tablename2, array('course'=>7));
5437          $DB->insert_record($tablename2, array('course'=>8));
5438  
5439          $rs1 = $DB->get_recordset($tablename);
5440          $i = 0;
5441          foreach ($rs1 as $record1) {
5442              $i++;
5443              $rs2 = $DB->get_recordset($tablename2);
5444              $j = 0;
5445              foreach ($rs2 as $record2) {
5446                  $t = $DB->start_delegated_transaction();
5447                  $DB->set_field($tablename, 'course', $record1->course+1, array('id'=>$record1->id));
5448                  $DB->set_field($tablename2, 'course', $record2->course+1, array('id'=>$record2->id));
5449                  $t->allow_commit();
5450                  $j++;
5451              }
5452              $rs2->close();
5453              $this->assertEquals(4, $j);
5454          }
5455          $rs1->close();
5456          $this->assertEquals(3, $i);
5457  
5458          // Test nested recordsets isolation without transaction.
5459          $DB->delete_records($tablename);
5460          $DB->insert_record($tablename, array('course'=>1));
5461          $DB->insert_record($tablename, array('course'=>2));
5462          $DB->insert_record($tablename, array('course'=>3));
5463  
5464          $DB->delete_records($tablename2);
5465          $DB->insert_record($tablename2, array('course'=>5));
5466          $DB->insert_record($tablename2, array('course'=>6));
5467          $DB->insert_record($tablename2, array('course'=>7));
5468          $DB->insert_record($tablename2, array('course'=>8));
5469  
5470          $rs1 = $DB->get_recordset($tablename);
5471          $i = 0;
5472          foreach ($rs1 as $record1) {
5473              $i++;
5474              $rs2 = $DB->get_recordset($tablename2);
5475              $j = 0;
5476              foreach ($rs2 as $record2) {
5477                  $DB->set_field($tablename, 'course', $record1->course+1, array('id'=>$record1->id));
5478                  $DB->set_field($tablename2, 'course', $record2->course+1, array('id'=>$record2->id));
5479                  $j++;
5480              }
5481              $rs2->close();
5482              $this->assertEquals(4, $j);
5483          }
5484          $rs1->close();
5485          $this->assertEquals(3, $i);
5486      }
5487  
5488      public function test_transactions_forbidden() {
5489          $DB = $this->tdb;
5490          $dbman = $DB->get_manager();
5491  
5492          $table = $this->get_test_table();
5493          $tablename = $table->getName();
5494  
5495          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5496          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5497          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5498          $dbman->create_table($table);
5499  
5500          $DB->transactions_forbidden();
5501          $transaction = $DB->start_delegated_transaction();
5502          $data = (object)array('course'=>1);
5503          $DB->insert_record($tablename, $data);
5504          try {
5505              $DB->transactions_forbidden();
5506          } catch (\moodle_exception $e) {
5507              $this->assertInstanceOf('dml_transaction_exception', $e);
5508          }
5509          // The previous test does not force rollback.
5510          $transaction->allow_commit();
5511          $this->assertFalse($DB->is_transaction_started());
5512          $this->assertEquals(1, $DB->count_records($tablename));
5513      }
5514  
5515      public function test_wrong_transactions() {
5516          $DB = $this->tdb;
5517          $dbman = $DB->get_manager();
5518  
5519          $table = $this->get_test_table();
5520          $tablename = $table->getName();
5521  
5522          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5523          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5524          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5525          $dbman->create_table($table);
5526  
5527          // Wrong order of nested commits.
5528          $transaction1 = $DB->start_delegated_transaction();
5529          $data = (object)array('course'=>3);
5530          $DB->insert_record($tablename, $data);
5531          $transaction2 = $DB->start_delegated_transaction();
5532          $data = (object)array('course'=>4);
5533          $DB->insert_record($tablename, $data);
5534          try {
5535              $transaction1->allow_commit();
5536              $this->fail('wrong order of commits must throw exception');
5537          } catch (\moodle_exception $e) {
5538              $this->assertInstanceOf('dml_transaction_exception', $e);
5539          }
5540          try {
5541              $transaction2->allow_commit();
5542              $this->fail('first wrong commit forces rollback');
5543          } catch (\moodle_exception $e) {
5544              $this->assertInstanceOf('dml_transaction_exception', $e);
5545          }
5546          // This is done in default exception handler usually.
5547          $this->assertTrue($DB->is_transaction_started());
5548          $this->assertEquals(2, $DB->count_records($tablename)); // Not rolled back yet.
5549          $DB->force_transaction_rollback();
5550          $this->assertEquals(0, $DB->count_records($tablename));
5551          $DB->delete_records($tablename);
5552  
5553          // Wrong order of nested rollbacks.
5554          $transaction1 = $DB->start_delegated_transaction();
5555          $data = (object)array('course'=>3);
5556          $DB->insert_record($tablename, $data);
5557          $transaction2 = $DB->start_delegated_transaction();
5558          $data = (object)array('course'=>4);
5559          $DB->insert_record($tablename, $data);
5560          try {
5561              // This first rollback should prevent all other rollbacks.
5562              $transaction1->rollback(new \Exception('test'));
5563          } catch (\Exception $e) {
5564              $this->assertEquals(get_class($e), 'Exception');
5565          }
5566          try {
5567              $transaction2->rollback(new \Exception('test'));
5568          } catch (\Exception $e) {
5569              $this->assertEquals(get_class($e), 'Exception');
5570          }
5571          try {
5572              $transaction1->rollback(new \Exception('test'));
5573          } catch (\moodle_exception $e) {
5574              $this->assertInstanceOf('dml_transaction_exception', $e);
5575          }
5576          // This is done in default exception handler usually.
5577          $this->assertTrue($DB->is_transaction_started());
5578          $DB->force_transaction_rollback();
5579          $DB->delete_records($tablename);
5580  
5581          // Unknown transaction object.
5582          $transaction1 = $DB->start_delegated_transaction();
5583          $data = (object)array('course'=>3);
5584          $DB->insert_record($tablename, $data);
5585          $transaction2 = new moodle_transaction($DB);
5586          try {
5587              $transaction2->allow_commit();
5588              $this->fail('foreign transaction must fail');
5589          } catch (\moodle_exception $e) {
5590              $this->assertInstanceOf('dml_transaction_exception', $e);
5591          }
5592          try {
5593              $transaction1->allow_commit();
5594              $this->fail('first wrong commit forces rollback');
5595          } catch (\moodle_exception $e) {
5596              $this->assertInstanceOf('dml_transaction_exception', $e);
5597          }
5598          $DB->force_transaction_rollback();
5599          $DB->delete_records($tablename);
5600      }
5601  
5602      public function test_concurent_transactions() {
5603          // Notes about this test:
5604          // 1- MySQL needs to use one engine with transactions support (InnoDB).
5605          // 2- MSSQL needs to have enabled versioning for read committed
5606          //    transactions (ALTER DATABASE xxx SET READ_COMMITTED_SNAPSHOT ON)
5607          $DB = $this->tdb;
5608          $dbman = $DB->get_manager();
5609  
5610          $table = $this->get_test_table();
5611          $tablename = $table->getName();
5612  
5613          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5614          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5615          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5616          $dbman->create_table($table);
5617  
5618          $transaction = $DB->start_delegated_transaction();
5619          $data = (object)array('course'=>1);
5620          $this->assertEquals(0, $DB->count_records($tablename));
5621          $DB->insert_record($tablename, $data);
5622          $this->assertEquals(1, $DB->count_records($tablename));
5623  
5624          // Open second connection.
5625          $cfg = $DB->export_dbconfig();
5626          if (!isset($cfg->dboptions)) {
5627              $cfg->dboptions = array();
5628          }
5629          // If we have a readonly slave situation, we need to either observe
5630          // the latency, or if the latency is not specified we need to take
5631          // the slave out because the table may not have propagated yet.
5632          if (isset($cfg->dboptions['readonly'])) {
5633              if (isset($cfg->dboptions['readonly']['latency'])) {
5634                  usleep(intval(1000000 * $cfg->dboptions['readonly']['latency']));
5635              } else {
5636                  unset($cfg->dboptions['readonly']);
5637              }
5638          }
5639          $DB2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
5640          $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
5641  
5642          // Second instance should not see pending inserts.
5643          $this->assertEquals(0, $DB2->count_records($tablename));
5644          $data = (object)array('course'=>2);
5645          $DB2->insert_record($tablename, $data);
5646          $this->assertEquals(1, $DB2->count_records($tablename));
5647  
5648          // First should see the changes done from second.
5649          $this->assertEquals(2, $DB->count_records($tablename));
5650  
5651          // Now commit and we should see it finally in second connections.
5652          $transaction->allow_commit();
5653          $this->assertEquals(2, $DB2->count_records($tablename));
5654  
5655          // Let's try delete all is also working on (this checks MDL-29198).
5656          // Initially both connections see all the records in the table (2).
5657          $this->assertEquals(2, $DB->count_records($tablename));
5658          $this->assertEquals(2, $DB2->count_records($tablename));
5659          $transaction = $DB->start_delegated_transaction();
5660  
5661          // Delete all from within transaction.
5662          $DB->delete_records($tablename);
5663  
5664          // Transactional $DB, sees 0 records now.
5665          $this->assertEquals(0, $DB->count_records($tablename));
5666  
5667          // Others ($DB2) get no changes yet.
5668          $this->assertEquals(2, $DB2->count_records($tablename));
5669  
5670          // Now commit and we should see changes.
5671          $transaction->allow_commit();
5672          $this->assertEquals(0, $DB2->count_records($tablename));
5673  
5674          $DB2->dispose();
5675      }
5676  
5677      public function test_session_locks() {
5678          $DB = $this->tdb;
5679          $dbman = $DB->get_manager();
5680  
5681          // Open second connection.
5682          $cfg = $DB->export_dbconfig();
5683          if (!isset($cfg->dboptions)) {
5684              $cfg->dboptions = array();
5685          }
5686          $DB2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
5687          $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
5688  
5689          // Testing that acquiring a lock effectively locks.
5690          // Get a session lock on connection1.
5691          $rowid = rand(100, 200);
5692          $timeout = 1;
5693          $DB->get_session_lock($rowid, $timeout);
5694  
5695          // Try to get the same session lock on connection2.
5696          try {
5697              $DB2->get_session_lock($rowid, $timeout);
5698              $DB2->release_session_lock($rowid); // Should not be executed, but here for safety.
5699              $this->fail('An Exception is missing, expected due to session lock acquired.');
5700          } catch (\moodle_exception $e) {
5701              $this->assertInstanceOf('dml_sessionwait_exception', $e);
5702              $DB->release_session_lock($rowid); // Release lock on connection1.
5703          }
5704  
5705          // Testing that releasing a lock effectively frees.
5706          // Get a session lock on connection1.
5707          $rowid = rand(100, 200);
5708          $timeout = 1;
5709          $DB->get_session_lock($rowid, $timeout);
5710          // Release the lock on connection1.
5711          $DB->release_session_lock($rowid);
5712  
5713          // Get the just released lock on connection2.
5714          $DB2->get_session_lock($rowid, $timeout);
5715          // Release the lock on connection2.
5716          $DB2->release_session_lock($rowid);
5717  
5718          $DB2->dispose();
5719      }
5720  
5721      public function test_bound_param_types() {
5722          $DB = $this->tdb;
5723          $dbman = $DB->get_manager();
5724  
5725          $table = $this->get_test_table();
5726          $tablename = $table->getName();
5727  
5728          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5729          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
5730          $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
5731          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5732          $dbman->create_table($table);
5733  
5734          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => '1', 'content'=>'xx')));
5735          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 2, 'content'=>'yy')));
5736          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'somestring', 'content'=>'zz')));
5737          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'aa', 'content'=>'1')));
5738          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'bb', 'content'=>2)));
5739          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'cc', 'content'=>'sometext')));
5740  
5741          // Conditions in CHAR columns.
5742          $this->assertTrue($DB->record_exists($tablename, array('name'=>1)));
5743          $this->assertTrue($DB->record_exists($tablename, array('name'=>'1')));
5744          $this->assertFalse($DB->record_exists($tablename, array('name'=>111)));
5745          $this->assertNotEmpty($DB->get_record($tablename, array('name'=>1)));
5746          $this->assertNotEmpty($DB->get_record($tablename, array('name'=>'1')));
5747          $this->assertEmpty($DB->get_record($tablename, array('name'=>111)));
5748          $sqlqm = "SELECT *
5749                      FROM {{$tablename}}
5750                     WHERE name = ?";
5751          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, array(1)));
5752          $this->assertCount(1, $records);
5753          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, array('1')));
5754          $this->assertCount(1, $records);
5755          $records = $DB->get_records_sql($sqlqm, array(222));
5756          $this->assertCount(0, $records);
5757          $sqlnamed = "SELECT *
5758                         FROM {{$tablename}}
5759                        WHERE name = :name";
5760          $this->assertNotEmpty($records = $DB->get_records_sql($sqlnamed, array('name' => 2)));
5761          $this->assertCount(1, $records);
5762          $this->assertNotEmpty($records = $DB->get_records_sql($sqlnamed, array('name' => '2')));
5763          $this->assertCount(1, $records);
5764  
5765          // Conditions in TEXT columns always must be performed with the sql_compare_text
5766          // helper function on both sides of the condition.
5767          $sqlqm = "SELECT *
5768                      FROM {{$tablename}}
5769                     WHERE " . $DB->sql_compare_text('content') . " =  " . $DB->sql_compare_text('?');
5770          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, array('1')));
5771          $this->assertCount(1, $records);
5772          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, array(1)));
5773          $this->assertCount(1, $records);
5774          $sqlnamed = "SELECT *
5775                         FROM {{$tablename}}
5776                        WHERE " . $DB->sql_compare_text('content') . " =  " . $DB->sql_compare_text(':content');
5777          $this->assertNotEmpty($records = $DB->get_records_sql($sqlnamed, array('content' => 2)));
5778          $this->assertCount(1, $records);
5779          $this->assertNotEmpty($records = $DB->get_records_sql($sqlnamed, array('content' => '2')));
5780          $this->assertCount(1, $records);
5781      }
5782  
5783      public function test_bound_param_reserved() {
5784          $DB = $this->tdb;
5785          $dbman = $DB->get_manager();
5786  
5787          $table = $this->get_test_table();
5788          $tablename = $table->getName();
5789  
5790          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5791          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5792          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5793          $dbman->create_table($table);
5794  
5795          $DB->insert_record($tablename, array('course' => '1'));
5796  
5797          // Make sure reserved words do not cause fatal problems in query parameters.
5798  
5799          $DB->execute("UPDATE {{$tablename}} SET course = 1 WHERE id = :select", array('select'=>1));
5800          $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE course = :select", array('select'=>1));
5801          $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}} WHERE course = :select", array('select'=>1));
5802          $rs->close();
5803          $DB->get_fieldset_sql("SELECT id FROM {{$tablename}} WHERE course = :select", array('select'=>1));
5804          $DB->set_field_select($tablename, 'course', '1', "id = :select", array('select'=>1));
5805          $DB->delete_records_select($tablename, "id = :select", array('select'=>1));
5806  
5807          // If we get here test passed ok.
5808          $this->assertTrue(true);
5809      }
5810  
5811      public function test_limits_and_offsets() {
5812          $DB = $this->tdb;
5813          $dbman = $DB->get_manager();
5814  
5815          $table = $this->get_test_table();
5816          $tablename = $table->getName();
5817  
5818          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5819          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
5820          $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
5821          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5822          $dbman->create_table($table);
5823  
5824          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'a', 'content'=>'one')));
5825          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'b', 'content'=>'two')));
5826          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'c', 'content'=>'three')));
5827          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'd', 'content'=>'four')));
5828          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'e', 'content'=>'five')));
5829          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'f', 'content'=>'six')));
5830  
5831          $sqlqm = "SELECT *
5832                      FROM {{$tablename}}";
5833          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 4));
5834          $this->assertCount(2, $records);
5835          $this->assertSame('e', reset($records)->name);
5836          $this->assertSame('f', end($records)->name);
5837  
5838          $sqlqm = "SELECT *
5839                      FROM {{$tablename}}";
5840          $this->assertEmpty($records = $DB->get_records_sql($sqlqm, null, 8));
5841  
5842          $sqlqm = "SELECT *
5843                      FROM {{$tablename}}";
5844          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 4));
5845          $this->assertCount(4, $records);
5846          $this->assertSame('a', reset($records)->name);
5847          $this->assertSame('d', end($records)->name);
5848  
5849          $sqlqm = "SELECT *
5850                      FROM {{$tablename}}";
5851          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 8));
5852          $this->assertCount(6, $records);
5853          $this->assertSame('a', reset($records)->name);
5854          $this->assertSame('f', end($records)->name);
5855  
5856          $sqlqm = "SELECT *
5857                      FROM {{$tablename}}";
5858          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 1, 4));
5859          $this->assertCount(4, $records);
5860          $this->assertSame('b', reset($records)->name);
5861          $this->assertSame('e', end($records)->name);
5862  
5863          $sqlqm = "SELECT *
5864                      FROM {{$tablename}}";
5865          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 4, 4));
5866          $this->assertCount(2, $records);
5867          $this->assertSame('e', reset($records)->name);
5868          $this->assertSame('f', end($records)->name);
5869  
5870          $sqlqm = "SELECT t.*, t.name AS test
5871                      FROM {{$tablename}} t
5872                      ORDER BY t.id ASC";
5873          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 4, 4));
5874          $this->assertCount(2, $records);
5875          $this->assertSame('e', reset($records)->name);
5876          $this->assertSame('f', end($records)->name);
5877  
5878          $sqlqm = "SELECT DISTINCT t.name, t.name AS test
5879                      FROM {{$tablename}} t
5880                      ORDER BY t.name DESC";
5881          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 4, 4));
5882          $this->assertCount(2, $records);
5883          $this->assertSame('b', reset($records)->name);
5884          $this->assertSame('a', end($records)->name);
5885  
5886          $sqlqm = "SELECT 1
5887                      FROM {{$tablename}} t
5888                      WHERE t.name = 'a'";
5889          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 1));
5890          $this->assertCount(1, $records);
5891  
5892          $sqlqm = "SELECT 'constant'
5893                      FROM {{$tablename}} t
5894                      WHERE t.name = 'a'";
5895          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 8));
5896          $this->assertCount(1, $records);
5897  
5898          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'a', 'content'=>'one')));
5899          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'b', 'content'=>'two')));
5900          $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'c', 'content'=>'three')));
5901  
5902          $sqlqm = "SELECT t.name, COUNT(DISTINCT t2.id) AS count, 'Test' AS teststring
5903                      FROM {{$tablename}} t
5904                      LEFT JOIN (
5905                          SELECT t.id, t.name
5906                          FROM {{$tablename}} t
5907                      ) t2 ON t2.name = t.name
5908                      GROUP BY t.name
5909                      ORDER BY t.name ASC";
5910          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm));
5911          $this->assertCount(6, $records);         // a,b,c,d,e,f.
5912          $this->assertEquals(2, reset($records)->count);  // a has 2 records now.
5913          $this->assertEquals(1, end($records)->count);    // f has 1 record still.
5914  
5915          $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 2));
5916          $this->assertCount(2, $records);
5917          $this->assertEquals(2, reset($records)->count);
5918          $this->assertEquals(2, end($records)->count);
5919      }
5920  
5921      /**
5922       * Test debugging messages about invalid limit number values.
5923       */
5924      public function test_invalid_limits_debugging() {
5925          $DB = $this->tdb;
5926          $dbman = $DB->get_manager();
5927  
5928          // Setup test data.
5929          $table = $this->get_test_table();
5930          $tablename = $table->getName();
5931          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5932          $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5933          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5934          $dbman->create_table($table);
5935          $DB->insert_record($tablename, array('course' => '1'));
5936  
5937          // Verify that get_records_sql throws debug notices with invalid limit params.
5938          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 'invalid');
5939          $this->assertDebuggingCalled("Non-numeric limitfrom parameter detected: 'invalid', did you pass the correct arguments?");
5940  
5941          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, 'invalid');
5942          $this->assertDebuggingCalled("Non-numeric limitnum parameter detected: 'invalid', did you pass the correct arguments?");
5943  
5944          // Verify that get_recordset_sql throws debug notices with invalid limit params.
5945          $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}}", null, 'invalid');
5946          $this->assertDebuggingCalled("Non-numeric limitfrom parameter detected: 'invalid', did you pass the correct arguments?");
5947          $rs->close();
5948  
5949          $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}}", null, 1, 'invalid');
5950          $this->assertDebuggingCalled("Non-numeric limitnum parameter detected: 'invalid', did you pass the correct arguments?");
5951          $rs->close();
5952  
5953          // Verify that some edge cases do no create debugging messages.
5954          // String form of integer values.
5955          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, '1');
5956          $this->assertDebuggingNotCalled();
5957          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, '2');
5958          $this->assertDebuggingNotCalled();
5959          // Empty strings.
5960          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, '');
5961          $this->assertDebuggingNotCalled();
5962          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, '');
5963          $this->assertDebuggingNotCalled();
5964          // Null values.
5965          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, null);
5966          $this->assertDebuggingNotCalled();
5967          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, null);
5968          $this->assertDebuggingNotCalled();
5969  
5970          // Verify that empty arrays DO create debugging mesages.
5971          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, array());
5972          $this->assertDebuggingCalled("Non-numeric limitfrom parameter detected: array (\n), did you pass the correct arguments?");
5973          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, array());
5974          $this->assertDebuggingCalled("Non-numeric limitnum parameter detected: array (\n), did you pass the correct arguments?");
5975  
5976          // Verify Negative number handling:
5977          // -1 is explicitly treated as 0 for historical reasons.
5978          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, -1);
5979          $this->assertDebuggingNotCalled();
5980          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, -1);
5981          $this->assertDebuggingNotCalled();
5982          // Any other negative values should throw debugging messages.
5983          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, -2);
5984          $this->assertDebuggingCalled("Negative limitfrom parameter detected: -2, did you pass the correct arguments?");
5985          $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, -2);
5986          $this->assertDebuggingCalled("Negative limitnum parameter detected: -2, did you pass the correct arguments?");
5987      }
5988  
5989      public function test_queries_counter() {
5990  
5991          $DB = $this->tdb;
5992          $dbman = $this->tdb->get_manager();
5993  
5994          // Test database.
5995          $table = $this->get_test_table();
5996          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5997          $table->add_field('fieldvalue', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5998          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5999  
6000          $dbman->create_table($table);
6001          $tablename = $table->getName();
6002  
6003          // Initial counters values.
6004          $initreads = $DB->perf_get_reads();
6005          $initwrites = $DB->perf_get_writes();
6006          $previousqueriestime = $DB->perf_get_queries_time();
6007  
6008          // Selects counts as reads.
6009  
6010          // The get_records_sql() method generates only 1 db query.
6011          $whatever = $DB->get_records_sql("SELECT * FROM {{$tablename}}");
6012          $this->assertEquals($initreads + 1, $DB->perf_get_reads());
6013  
6014          // The get_records() method generates 2 queries the first time is called
6015          // as it is fetching the table structure.
6016          $whatever = $DB->get_records($tablename, array('id' => '1'));
6017          $this->assertEquals($initreads + 3, $DB->perf_get_reads());
6018          $this->assertEquals($initwrites, $DB->perf_get_writes());
6019  
6020          // The elapsed time is counted.
6021          $lastqueriestime = $DB->perf_get_queries_time();
6022          $this->assertGreaterThanOrEqual($previousqueriestime, $lastqueriestime);
6023          $previousqueriestime = $lastqueriestime;
6024  
6025          // Only 1 now, it already fetched the table columns.
6026          $whatever = $DB->get_records($tablename);
6027          $this->assertEquals($initreads + 4, $DB->perf_get_reads());
6028  
6029          // And only 1 more from now.
6030          $whatever = $DB->get_records($tablename);
6031          $this->assertEquals($initreads + 5, $DB->perf_get_reads());
6032  
6033          // Inserts counts as writes.
6034  
6035          $rec1 = new \stdClass();
6036          $rec1->fieldvalue = 11;
6037          $rec1->id = $DB->insert_record($tablename, $rec1);
6038          $this->assertEquals($initwrites + 1, $DB->perf_get_writes());
6039          $this->assertEquals($initreads + 5, $DB->perf_get_reads());
6040  
6041          // The elapsed time is counted.
6042          $lastqueriestime = $DB->perf_get_queries_time();
6043          $this->assertGreaterThanOrEqual($previousqueriestime, $lastqueriestime);
6044          $previousqueriestime = $lastqueriestime;
6045  
6046          $rec2 = new \stdClass();
6047          $rec2->fieldvalue = 22;
6048          $rec2->id = $DB->insert_record($tablename, $rec2);
6049          $this->assertEquals($initwrites + 2, $DB->perf_get_writes());
6050  
6051          // Updates counts as writes.
6052  
6053          $rec1->fieldvalue = 111;
6054          $DB->update_record($tablename, $rec1);
6055          $this->assertEquals($initwrites + 3, $DB->perf_get_writes());
6056          $this->assertEquals($initreads + 5, $DB->perf_get_reads());
6057  
6058          // The elapsed time is counted.
6059          $lastqueriestime = $DB->perf_get_queries_time();
6060          $this->assertGreaterThanOrEqual($previousqueriestime, $lastqueriestime);
6061          $previousqueriestime = $lastqueriestime;
6062  
6063          // Sum of them.
6064          $totaldbqueries = $DB->perf_get_reads() + $DB->perf_get_writes();
6065          $this->assertEquals($totaldbqueries, $DB->perf_get_queries());
6066      }
6067  
6068      public function test_sql_intersect() {
6069          $DB = $this->tdb;
6070          $dbman = $this->tdb->get_manager();
6071  
6072          $tables = array();
6073          for ($i = 0; $i < 3; $i++) {
6074              $table = $this->get_test_table('i'.$i);
6075              $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
6076              $table->add_field('ival', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
6077              $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, '0');
6078              $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
6079              $dbman->create_table($table);
6080              $tables[$i] = $table;
6081          }
6082          $DB->insert_record($tables[0]->getName(), array('ival' => 1, 'name' => 'One'), false);
6083          $DB->insert_record($tables[0]->getName(), array('ival' => 2, 'name' => 'Two'), false);
6084          $DB->insert_record($tables[0]->getName(), array('ival' => 3, 'name' => 'Three'), false);
6085          $DB->insert_record($tables[0]->getName(), array('ival' => 4, 'name' => 'Four'), false);
6086  
6087          $DB->insert_record($tables[1]->getName(), array('ival' => 1, 'name' => 'One'), false);
6088          $DB->insert_record($tables[1]->getName(), array('ival' => 2, 'name' => 'Two'), false);
6089          $DB->insert_record($tables[1]->getName(), array('ival' => 3, 'name' => 'Three'), false);
6090  
6091          $DB->insert_record($tables[2]->getName(), array('ival' => 1, 'name' => 'One'), false);
6092          $DB->insert_record($tables[2]->getName(), array('ival' => 2, 'name' => 'Two'), false);
6093          $DB->insert_record($tables[2]->getName(), array('ival' => 5, 'name' => 'Five'), false);
6094  
6095          // Intersection on the int column.
6096          $params = array('excludename' => 'Two');
6097          $sql1 = 'SELECT ival FROM {'.$tables[0]->getName().'}';
6098          $sql2 = 'SELECT ival FROM {'.$tables[1]->getName().'} WHERE name <> :excludename';
6099          $sql3 = 'SELECT ival FROM {'.$tables[2]->getName().'}';
6100  
6101          $sql = $DB->sql_intersect(array($sql1), 'ival') . ' ORDER BY ival';
6102          $this->assertEquals(array(1, 2, 3, 4), $DB->get_fieldset_sql($sql, $params));
6103  
6104          $sql = $DB->sql_intersect(array($sql1, $sql2), 'ival') . ' ORDER BY ival';
6105          $this->assertEquals(array(1, 3), $DB->get_fieldset_sql($sql, $params));
6106  
6107          $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'ival') . ' ORDER BY ival';
6108          $this->assertEquals(array(1),
6109              $DB->get_fieldset_sql($sql, $params));
6110  
6111          // Intersection on the char column.
6112          $params = array('excludeival' => 2);
6113          $sql1 = 'SELECT name FROM {'.$tables[0]->getName().'}';
6114          $sql2 = 'SELECT name FROM {'.$tables[1]->getName().'} WHERE ival <> :excludeival';
6115          $sql3 = 'SELECT name FROM {'.$tables[2]->getName().'}';
6116  
6117          $sql = $DB->sql_intersect(array($sql1), 'name') . ' ORDER BY name';
6118          $this->assertEquals(array('Four', 'One', 'Three', 'Two'), $DB->get_fieldset_sql($sql, $params));
6119  
6120          $sql = $DB->sql_intersect(array($sql1, $sql2), 'name') . ' ORDER BY name';
6121          $this->assertEquals(array('One', 'Three'), $DB->get_fieldset_sql($sql, $params));
6122  
6123          $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'name') . ' ORDER BY name';
6124          $this->assertEquals(array('One'), $DB->get_fieldset_sql($sql, $params));
6125  
6126          // Intersection on the several columns.
6127          $params = array('excludename' => 'Two');
6128          $sql1 = 'SELECT ival, name FROM {'.$tables[0]->getName().'}';
6129          $sql2 = 'SELECT ival, name FROM {'.$tables[1]->getName().'} WHERE name <> :excludename';
6130          $sql3 = 'SELECT ival, name FROM {'.$tables[2]->getName().'}';
6131  
6132          $sql = $DB->sql_intersect(array($sql1), 'ival, name') . ' ORDER BY ival';
6133          $this->assertEquals(array(1 => 'One', 2 => 'Two', 3 => 'Three', 4 => 'Four'),
6134              $DB->get_records_sql_menu($sql, $params));
6135  
6136          $sql = $DB->sql_intersect(array($sql1, $sql2), 'ival, name') . ' ORDER BY ival';
6137          $this->assertEquals(array(1 => 'One', 3 => 'Three'),
6138              $DB->get_records_sql_menu($sql, $params));
6139  
6140          $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'ival, name') . ' ORDER BY ival';
6141          $this->assertEquals(array(1 => 'One'),
6142              $DB->get_records_sql_menu($sql, $params));
6143  
6144          // Drop temporary tables.
6145          foreach ($tables as $table) {
6146              $dbman->drop_table($table);
6147          }
6148      }
6149  
6150      /**
6151       * Test that the database has full utf8 support (4 bytes).
6152       */
6153      public function test_four_byte_character_insertion() {
6154          $DB = $this->tdb;
6155  
6156          if ($DB->get_dbfamily() === 'mysql' && strpos($DB->get_dbcollation(), 'utf8_') === 0) {
6157              $this->markTestSkipped($DB->get_name() .
6158                      ' does not support 4 byte characters with only a utf8 collation.
6159                      Please change to utf8mb4 for full utf8 support.');
6160          }
6161  
6162          $dbman = $this->tdb->get_manager();
6163  
6164          $table = $this->get_test_table();
6165          $tablename = $table->getName();
6166  
6167          $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
6168          $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
6169          $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
6170          $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
6171          $dbman->create_table($table);
6172  
6173          $data = array(
6174              'name' => 'Name with a four byte character 𠮟る',
6175              'content' => 'Content with a four byte emoji 📝 memo.'
6176          );
6177  
6178          $insertid = $DB->insert_record($tablename, $data);
6179          $result = $DB->get_record($tablename, array('id' => $insertid));
6180          $this->assertEquals($data['name'], $result->name);
6181          $this->assertEquals($data['content'], $result->content);
6182  
6183          $dbman->drop_table($table);
6184      }
6185  
6186      /**
6187       * Mock the methods used by {@see \mysqli_native_moodle_database::get_server_info()}.
6188       *
6189       * Mocking allows to test it without the need of an actual MySQL-ish running DB server.
6190       *
6191       * @param string $mysqliserverinfo A string representing the server info as provided by the MySQLi extension.
6192       * @param string $versionfromdb A string representing the result of VERSION function.
6193       * @param bool $cfgversionfromdb A boolean representing !empty($CFG->dboptions['versionfromdb']).
6194       * @param string $expecteddbversion A string representing the expected DB version.
6195       * @see \mysqli_native_moodle_database::get_server_info()
6196       * @covers \mysqli_native_moodle_database::get_server_info
6197       * @dataProvider get_server_info_mysql_provider
6198       */
6199      public function test_get_server_info_mysql(
6200          string $mysqliserverinfo, string $versionfromdb, bool $cfgversionfromdb, string $expecteddbversion) {
6201          // Avoid to run MySQL-ish related tests when running tests on other DB families.
6202          $DB = $this->tdb;
6203          if ($DB->get_dbfamily() != 'mysql') {
6204              $this->markTestSkipped("Not MySQL family");
6205          }
6206  
6207          // Mock the methods used by get_server_info() to simulate different MySQL-ish DB servers.
6208          $methods = [
6209              'get_mysqli_server_info',
6210              'get_version_from_db',
6211              'should_db_version_be_read_from_db',
6212          ];
6213          $mysqlinativemoodledatabase = $this->getMockBuilder('\mysqli_native_moodle_database')
6214              ->onlyMethods($methods)
6215              ->getMock();
6216          $mysqlinativemoodledatabase->method('get_mysqli_server_info')->willReturn($mysqliserverinfo);
6217          $mysqlinativemoodledatabase->method('get_version_from_db')->willReturn($versionfromdb);
6218          $mysqlinativemoodledatabase->method('should_db_version_be_read_from_db')->willReturn($cfgversionfromdb);
6219  
6220          ['description' => $description, 'version' => $version] = $mysqlinativemoodledatabase->get_server_info();
6221          $this->assertEquals($mysqliserverinfo, $description);
6222          $this->assertEquals($expecteddbversion, $version);
6223      }
6224  
6225      /**
6226       * Data provider to test {@see \mysqli_native_moodle_database::get_server_info} when mocking
6227       * the results of a connection to the DB server.
6228       *
6229       * The set of the data is represented by the following array items:
6230       * - a string representing the server info as provided by the MySQLi extension
6231       * - a string representing the result of VERSION function
6232       * - a boolean representing !empty($CFG->dboptions['versionfromdb'])
6233       * - a string representing the expected DB version
6234       *
6235       * @return array[]
6236       * @see \mysqli_native_moodle_database::get_server_info
6237       */
6238      public function get_server_info_mysql_provider() {
6239          return [
6240              'MySQL 5.7.39 - MySQLi version' => [
6241                  '5.7.39-log',
6242                  '',
6243                  false,
6244                  '5.7.39'
6245              ],
6246              'MySQL 5.7.40 - MySQLi version' => [
6247                  '5.7.40',
6248                  '',
6249                  false,
6250                  '5.7.40'
6251              ],
6252              'MySQL 8.0.31 - MySQLi version' => [
6253                  '8.0.31',
6254                  '',
6255                  false,
6256                  '8.0.31'
6257              ],
6258              'MariaDB 10.4.26 (https://moodle.org/mod/forum/discuss.php?d=441156#p1774957) - MySQLi version' => [
6259                  '10.4.26-MariaDB-1:10.4.26+mariadb~deb10',
6260                  '',
6261                  false,
6262                  '10.4.26'
6263              ],
6264              'MariaDB 10.4.27 - MySQLi version' => [
6265                  '5.5.5-10.4.27-MariaDB',
6266                  '',
6267                  false,
6268                  '10.4.27'
6269              ],
6270              'MariaDB 10.4.27 - DB version' => [
6271                  '',
6272                  '10.4.27-MariaDB',
6273                  true,
6274                  '10.4.27'
6275              ],
6276              'MariaDB 10.7.7 - MySQLi version' => [
6277                  '10.7.7-MariaDB-1:10.7.7+maria~ubu2004',
6278                  '',
6279                  false,
6280                  '10.7.7'
6281              ],
6282              'MariaDB 10.7.7 - DB version' => [
6283                  '',
6284                  '10.7.7-MariaDB-1:10.7.7+maria~ubu2004',
6285                  true,
6286                  '10.7.7'
6287              ],
6288              'MariaDB 10.2.32 on Azure via gateway - MySQLi version' => [
6289                  '5.6.42.0',
6290                  '10.2.32-MariaDB',
6291                  false,
6292                  '5.6.42.0'
6293              ],
6294              'MariaDB 10.2.32 on Azure via gateway - DB version' => [
6295                  '5.6.42.0',
6296                  '10.2.32-MariaDB',
6297                  true,
6298                  '10.2.32'
6299              ],
6300              'MariaDB 10.3.23 on Azure via gateway - DB version' => [
6301                  '5.6.47.0',
6302                  '10.3.23-MariaDB',
6303                  true,
6304                  '10.3.23'
6305              ],
6306          ];
6307      }
6308  
6309      /**
6310       * Test {@see \mysqli_native_moodle_database::get_server_info()} with the actual DB Server.
6311       * @see \mysqli_native_moodle_database::get_server_info
6312       * @covers \mysqli_native_moodle_database::get_server_info
6313       */
6314      public function test_get_server_info_dbfamily_mysql() {
6315          $DB = $this->tdb;
6316          if ($DB->get_dbfamily() != 'mysql') {
6317              $this->markTestSkipped("Not MySQL family");
6318          }
6319  
6320          $cfg = $DB->export_dbconfig();
6321          if (!isset($cfg->dboptions)) {
6322              $cfg->dboptions = [];
6323          }
6324          // By default, DB Server version is read from the PHP client.
6325          $this->assertTrue(empty($cfg->dboptions['versionfromdb']));
6326          $rc = new \ReflectionClass(\mysqli_native_moodle_database::class);
6327          $rcm = $rc->getMethod('should_db_version_be_read_from_db');
6328          $rcm->setAccessible(true);
6329          $this->assertFalse($rcm->invokeArgs($DB, []));
6330  
6331          ['description' => $description, 'version' => $version] = $DB->get_server_info();
6332          // MariaDB RPL_VERSION_HACK sanity check: "5.5.5" has never been released!
6333          $this->assertNotSame('5.5.5', $version,
6334              "Found invalid DB server version i.e. RPL_VERSION_HACK: '{$version}' ({$description}).");
6335          // DB version format is: "X.Y.Z".
6336          $this->assertMatchesRegularExpression('/^\d+\.\d+\.\d+$/', $version,
6337              "Found invalid DB server version format: '{$version}' ({$description}).");
6338  
6339          // Alter the DB options to force the read from DB and check for the same assertions above.
6340          $cfg->dboptions['versionfromdb'] = true;
6341          // Open a new DB connection with the forced setting.
6342          $db2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
6343          $db2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
6344          $cfg2 = $db2->export_dbconfig();
6345          $cfg = null;
6346          $this->assertNotEmpty($cfg2->dboptions);
6347          $this->assertFalse(empty($cfg2->dboptions['versionfromdb']), 'Invalid test state!');
6348          $this->assertTrue($rcm->invokeArgs($db2, []), 'Invalid test state!');
6349          ['description' => $description, 'version' => $version] = $db2->get_server_info();
6350          $this->assertNotSame('5.5.5', $version,
6351              "Found invalid DB server version when reading version from DB i.e. RPL_VERSION_HACK: '{$version}' ({$description}).");
6352          $this->assertMatchesRegularExpression('/^\d+\.\d+\.\d+$/', $version,
6353              "Found invalid DB server version format when reading version from DB: '{$version}' ({$description}).");
6354          $db2->dispose();
6355      }
6356  }
6357  
6358  /**
6359   * This class is not a proper subclass of moodle_database. It is
6360   * intended to be used only in unit tests, in order to gain access to the
6361   * protected methods of moodle_database, and unit test them.
6362   */
6363  class moodle_database_for_testing extends moodle_database {
6364      protected $prefix = 'mdl_';
6365  
6366      public function public_fix_table_names($sql) {
6367          return $this->fix_table_names($sql);
6368      }
6369  
6370      public function driver_installed() {}
6371      public function get_dbfamily() {}
6372      protected function get_dbtype() {}
6373      protected function get_dblibrary() {}
6374      public function get_name() {}
6375      public function get_configuration_help() {}
6376      public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {}
6377      public function get_server_info() {}
6378      protected function allowed_param_types() {}
6379      public function get_last_error() {}
6380      public function get_tables($usecache=true) {}
6381      public function get_indexes($table) {}
6382      protected function fetch_columns(string $table): array {
6383          return [];
6384      }
6385      protected function normalise_value($column, $value) {}
6386      public function set_debug($state) {}
6387      public function get_debug() {}
6388      public function change_database_structure($sql, $tablenames = null) {}
6389      public function execute($sql, array $params=null) {}
6390      public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {}
6391      public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {}
6392      public function get_fieldset_sql($sql, array $params=null) {}
6393      public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {}
6394      public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {}
6395      public function import_record($table, $dataobject) {}
6396      public function update_record_raw($table, $params, $bulk=false) {}
6397      public function update_record($table, $dataobject, $bulk=false) {}
6398      public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {}
6399      public function delete_records_select($table, $select, array $params=null) {}
6400      public function sql_concat() {}
6401      public function sql_concat_join($separator="' '", $elements=array()) {}
6402      public function sql_group_concat(string $field, string $separator = ', ', string $sort = ''): string {
6403          return '';
6404      }
6405      public function sql_substr($expr, $start, $length=false) {}
6406      public function begin_transaction() {}
6407      public function commit_transaction() {}
6408      public function rollback_transaction() {}
6409  }
6410  
6411  
6412  /**
6413   * Dumb test class with toString() returning 1.
6414   */
6415  class dml_test_object_one {
6416      public function __toString() {
6417          return 1;
6418      }
6419  }