Search moodle.org's
Developer Documentation

See Release Notes

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

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