Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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