Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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