Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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

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