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
26defined('MOODLE_INTERNAL') || die();
27
28class 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
474SELECT * FROM {users}
475-- line 65 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to ReflectionMethod->invoke()
476EOD;
477        $this->assertEquals($this->unix_to_os_dirsep($expected), $out);
478
479        $CFG->debugsqltrace = 2;
480        $out = $fixture->four($sql);
481        $expected = <<<EOD
482SELECT * 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()
485EOD;
486        $this->assertEquals($this->unix_to_os_dirsep($expected), $out);
487
488        $CFG->debugsqltrace = 5;
489        $out = $fixture->four($sql);
490        $expected = <<<EOD
491SELECT * 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()
497EOD;
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    /**
4340     * Test DML libraries sql_group_contact method
4341     */
4342    public function test_group_concat(): void {
4343        $DB = $this->tdb;
4344        $dbman = $DB->get_manager();
4345
4346        $table = $this->get_test_table();
4347        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4348        $table->add_field('intfield', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
4349        $table->add_field('charfield', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4350        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
4351        $dbman->create_table($table);
4352
4353        $tablename = $table->getName();
4354        $DB->insert_record($tablename, (object) ['intfield' => 10, 'charfield' => 'uno']);
4355        $DB->insert_record($tablename, (object) ['intfield' => 20, 'charfield' => 'dos']);
4356        $DB->insert_record($tablename, (object) ['intfield' => 20, 'charfield' => 'tres']);
4357        $DB->insert_record($tablename, (object) ['intfield' => 30, 'charfield' => 'tres']);
4358
4359        // Test charfield => concatenated intfield ASC.
4360        $fieldsql = $DB->sql_group_concat('intfield', ', ', 'intfield ASC');
4361        $sql = "SELECT charfield, {$fieldsql} AS falias
4362                  FROM {{$tablename}}
4363              GROUP BY charfield";
4364
4365        $this->assertEquals([
4366            'dos' => '20',
4367            'tres' => '20, 30',
4368            'uno' => '10',
4369        ], $DB->get_records_sql_menu($sql));
4370
4371        // Test charfield => concatenated intfield DESC.
4372        $fieldsql = $DB->sql_group_concat('intfield', ', ', 'intfield DESC');
4373        $sql = "SELECT charfield, {$fieldsql} AS falias
4374                  FROM {{$tablename}}
4375              GROUP BY charfield";
4376
4377        $this->assertEquals([
4378            'dos' => '20',
4379            'tres' => '30, 20',
4380            'uno' => '10',
4381        ], $DB->get_records_sql_menu($sql));
4382
4383        // Test intfield => concatenated charfield ASC.
4384        $fieldsql = $DB->sql_group_concat('charfield', ', ', 'charfield ASC');
4385        $sql = "SELECT intfield, {$fieldsql} AS falias
4386                  FROM {{$tablename}}
4387              GROUP BY intfield";
4388
4389        $this->assertEquals([
4390            10 => 'uno',
4391            20 => 'dos, tres',
4392            30 => 'tres',
4393        ], $DB->get_records_sql_menu($sql));
4394
4395        // Test intfield => concatenated charfield DESC.
4396        $fieldsql = $DB->sql_group_concat('charfield', ', ', 'charfield DESC');
4397        $sql = "SELECT intfield, {$fieldsql} AS falias
4398                  FROM {{$tablename}}
4399              GROUP BY intfield";
4400
4401        $this->assertEquals([
4402            10 => 'uno',
4403            20 => 'tres, dos',
4404            30 => 'tres',
4405        ], $DB->get_records_sql_menu($sql));
4406
4407        // Assert expressions with parameters can also be used.
4408        $fieldexpr = $DB->sql_concat(':greeting', 'charfield');
4409        $fieldsql = $DB->sql_group_concat($fieldexpr, ', ', 'charfield ASC');
4410        $sql = "SELECT intfield, {$fieldsql} AS falias
4411                  FROM {{$tablename}}
4412              GROUP BY intfield";
4413        $this->assertEquals([
4414            10 => 'Hola uno',
4415            20 => 'Hola dos, Hola tres',
4416            30 => 'Hola tres',
4417        ], $DB->get_records_sql_menu($sql, ['greeting' => 'Hola ']));
4418    }
4419
4420    /**
4421     * Test DML libraries sql_group_contact method joining tables, aggregating data from each
4422     */
4423    public function test_group_concat_join_tables(): void {
4424        $DB = $this->tdb;
4425        $dbman = $DB->get_manager();
4426
4427        $tableparent = $this->get_test_table('parent');
4428        $tableparent->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4429        $tableparent->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4430        $tableparent->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
4431        $dbman->create_table($tableparent);
4432
4433        $tablechild = $this->get_test_table('child');
4434        $tablechild->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4435        $tablechild->add_field('parentid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
4436        $tablechild->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4437        $tablechild->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
4438        $tablechild->add_key('parentid', XMLDB_KEY_FOREIGN, ['parentid'], $tableparent->getName(), ['id']);
4439        $dbman->create_table($tablechild);
4440
4441        $tableparentname = $tableparent->getName();
4442        $tablechildname = $tablechild->getName();
4443
4444        $parentone = $DB->insert_record($tableparentname, (object) ['name' => 'Alice']);
4445        $DB->insert_record($tablechildname, (object) ['parentid' => $parentone, 'name' => 'Eve']);
4446        $DB->insert_record($tablechildname, (object) ['parentid' => $parentone, 'name' => 'Charlie']);
4447
4448        $parenttwo = $DB->insert_record($tableparentname, (object) ['name' => 'Bob']);
4449        $DB->insert_record($tablechildname, (object) ['parentid' => $parenttwo, 'name' => 'Dan']);
4450        $DB->insert_record($tablechildname, (object) ['parentid' => $parenttwo, 'name' => 'Grace']);
4451
4452        $tableparentalias = 'p';
4453        $tablechildalias = 'c';
4454
4455        $fieldsql = $DB->sql_group_concat("{$tablechildalias}.name", ', ', "{$tablechildalias}.name ASC");
4456
4457        $sql = "SELECT {$tableparentalias}.name, {$fieldsql} AS falias
4458                  FROM {{$tableparentname}} {$tableparentalias}
4459                  JOIN {{$tablechildname}} {$tablechildalias} ON {$tablechildalias}.parentid = {$tableparentalias}.id
4460              GROUP BY {$tableparentalias}.name";
4461
4462        $this->assertEqualsCanonicalizing([
4463            (object) [
4464                'name' => 'Alice',
4465                'falias' => 'Charlie, Eve',
4466            ],
4467            (object) [
4468                'name' => 'Bob',
4469                'falias' => 'Dan, Grace',
4470            ],
4471        ], $DB->get_records_sql($sql));
4472    }
4473
4474    public function test_sql_fullname() {
4475        $DB = $this->tdb;
4476        $sql = "SELECT ".$DB->sql_fullname(':first', ':last')." AS fullname ".$DB->sql_null_from_clause();
4477        $params = array('first'=>'Firstname', 'last'=>'Surname');
4478        $this->assertEquals("Firstname Surname", $DB->get_field_sql($sql, $params));
4479    }
4480
4481    public function test_sql_order_by_text() {
4482        $DB = $this->tdb;
4483        $dbman = $DB->get_manager();
4484
4485        $table = $this->get_test_table();
4486        $tablename = $table->getName();
4487
4488        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4489        $table->add_field('description', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
4490        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4491        $dbman->create_table($table);
4492
4493        $DB->insert_record($tablename, array('description'=>'abcd'));
4494        $DB->insert_record($tablename, array('description'=>'dxxx'));
4495        $DB->insert_record($tablename, array('description'=>'bcde'));
4496
4497        $sql = "SELECT * FROM {{$tablename}} ORDER BY ".$DB->sql_order_by_text('description');
4498        $records = $DB->get_records_sql($sql);
4499        $first = array_shift($records);
4500        $this->assertEquals(1, $first->id);
4501        $second = array_shift($records);
4502        $this->assertEquals(3, $second->id);
4503        $last = array_shift($records);
4504        $this->assertEquals(2, $last->id);
4505    }
4506
4507    public function test_sql_substring() {
4508        $DB = $this->tdb;
4509        $dbman = $DB->get_manager();
4510
4511        $table = $this->get_test_table();
4512        $tablename = $table->getName();
4513
4514        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4515        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4516        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4517        $dbman->create_table($table);
4518
4519        $string = 'abcdefghij';
4520
4521        $DB->insert_record($tablename, array('name'=>$string));
4522
4523        $sql = "SELECT id, ".$DB->sql_substr("name", 5)." AS name FROM {{$tablename}}";
4524        $record = $DB->get_record_sql($sql);
4525        $this->assertEquals(substr($string, 5-1), $record->name);
4526
4527        $sql = "SELECT id, ".$DB->sql_substr("name", 5, 2)." AS name FROM {{$tablename}}";
4528        $record = $DB->get_record_sql($sql);
4529        $this->assertEquals(substr($string, 5-1, 2), $record->name);
4530
4531        try {
4532            // Silence php warning.
4533            @$DB->sql_substr("name");
4534            $this->fail("Expecting an exception, none occurred");
4535        } catch (moodle_exception $e) {
4536            $this->assertInstanceOf('coding_exception', $e);
4537        } catch (Error $error) {
4538            // PHP 7.1 throws Error even earlier.
4539            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
4540        }
4541
4542        // Cover the function using placeholders in all positions.
4543        $start = 4;
4544        $length = 2;
4545        // 1st param (target).
4546        $sql = "SELECT id, ".$DB->sql_substr(":param1", $start)." AS name FROM {{$tablename}}";
4547        $record = $DB->get_record_sql($sql, array('param1' => $string));
4548        $this->assertEquals(substr($string, $start - 1), $record->name); // PHP's substr is 0-based.
4549        // 2nd param (start).
4550        $sql = "SELECT id, ".$DB->sql_substr("name", ":param1")." AS name FROM {{$tablename}}";
4551        $record = $DB->get_record_sql($sql, array('param1' => $start));
4552        $this->assertEquals(substr($string, $start - 1), $record->name); // PHP's substr is 0-based.
4553        // 3rd param (length).
4554        $sql = "SELECT id, ".$DB->sql_substr("name", $start, ":param1")." AS name FROM {{$tablename}}";
4555        $record = $DB->get_record_sql($sql, array('param1' => $length));
4556        $this->assertEquals(substr($string, $start - 1,  $length), $record->name); // PHP's substr is 0-based.
4557        // All together.
4558        $sql = "SELECT id, ".$DB->sql_substr(":param1", ":param2", ":param3")." AS name FROM {{$tablename}}";
4559        $record = $DB->get_record_sql($sql, array('param1' => $string, 'param2' => $start, 'param3' => $length));
4560        $this->assertEquals(substr($string, $start - 1,  $length), $record->name); // PHP's substr is 0-based.
4561
4562        // Try also with some expression passed.
4563        $sql = "SELECT id, ".$DB->sql_substr("name", "(:param1 + 1) - 1")." AS name FROM {{$tablename}}";
4564        $record = $DB->get_record_sql($sql, array('param1' => $start));
4565        $this->assertEquals(substr($string, $start - 1), $record->name); // PHP's substr is 0-based.
4566    }
4567
4568    public function test_sql_length() {
4569        $DB = $this->tdb;
4570        $this->assertEquals($DB->get_field_sql(
4571            "SELECT ".$DB->sql_length("'aeiou'").$DB->sql_null_from_clause()), 5);
4572        $this->assertEquals($DB->get_field_sql(
4573            "SELECT ".$DB->sql_length("'áéíóú'").$DB->sql_null_from_clause()), 5);
4574    }
4575
4576    public function test_sql_position() {
4577        $DB = $this->tdb;
4578        $this->assertEquals($DB->get_field_sql(
4579            "SELECT ".$DB->sql_position("'ood'", "'Moodle'").$DB->sql_null_from_clause()), 2);
4580        $this->assertEquals($DB->get_field_sql(
4581            "SELECT ".$DB->sql_position("'Oracle'", "'Moodle'").$DB->sql_null_from_clause()), 0);
4582    }
4583
4584    public function test_sql_empty() {
4585        $DB = $this->tdb;
4586        $dbman = $DB->get_manager();
4587
4588        $table = $this->get_test_table();
4589        $tablename = $table->getName();
4590
4591        $this->assertSame('', $DB->sql_empty()); // Since 2.5 the hack is applied automatically to all bound params.
4592        $this->assertDebuggingCalled();
4593
4594        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4595        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4596        $table->add_field('namenotnull', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, 'default value');
4597        $table->add_field('namenotnullnodeflt', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4598        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4599        $dbman->create_table($table);
4600
4601        $DB->insert_record($tablename, array('name'=>'', 'namenotnull'=>''));
4602        $DB->insert_record($tablename, array('name'=>null));
4603        $DB->insert_record($tablename, array('name'=>'lalala'));
4604        $DB->insert_record($tablename, array('name'=>0));
4605
4606        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE name = ?", array(''));
4607        $this->assertCount(1, $records);
4608        $record = reset($records);
4609        $this->assertSame('', $record->name);
4610
4611        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE namenotnull = ?", array(''));
4612        $this->assertCount(1, $records);
4613        $record = reset($records);
4614        $this->assertSame('', $record->namenotnull);
4615
4616        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE namenotnullnodeflt = ?", array(''));
4617        $this->assertCount(4, $records);
4618        $record = reset($records);
4619        $this->assertSame('', $record->namenotnullnodeflt);
4620    }
4621
4622    public function test_sql_isempty() {
4623        $DB = $this->tdb;
4624        $dbman = $DB->get_manager();
4625
4626        $table = $this->get_test_table();
4627        $tablename = $table->getName();
4628
4629        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4630        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4631        $table->add_field('namenull', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4632        $table->add_field('description', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL, null, null);
4633        $table->add_field('descriptionnull', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
4634        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4635        $dbman->create_table($table);
4636
4637        $DB->insert_record($tablename, array('name'=>'',   'namenull'=>'',   'description'=>'',   'descriptionnull'=>''));
4638        $DB->insert_record($tablename, array('name'=>'??', 'namenull'=>null, 'description'=>'??', 'descriptionnull'=>null));
4639        $DB->insert_record($tablename, array('name'=>'la', 'namenull'=>'la', 'description'=>'la', 'descriptionnull'=>'lalala'));
4640        $DB->insert_record($tablename, array('name'=>0,    'namenull'=>0,    'description'=>0,    'descriptionnull'=>0));
4641
4642        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isempty($tablename, 'name', false, false));
4643        $this->assertCount(1, $records);
4644        $record = reset($records);
4645        $this->assertSame('', $record->name);
4646
4647        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isempty($tablename, 'namenull', true, false));
4648        $this->assertCount(1, $records);
4649        $record = reset($records);
4650        $this->assertSame('', $record->namenull);
4651
4652        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isempty($tablename, 'description', false, true));
4653        $this->assertCount(1, $records);
4654        $record = reset($records);
4655        $this->assertSame('', $record->description);
4656
4657        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isempty($tablename, 'descriptionnull', true, true));
4658        $this->assertCount(1, $records);
4659        $record = reset($records);
4660        $this->assertSame('', $record->descriptionnull);
4661    }
4662
4663    public function test_sql_isnotempty() {
4664        $DB = $this->tdb;
4665        $dbman = $DB->get_manager();
4666
4667        $table = $this->get_test_table();
4668        $tablename = $table->getName();
4669
4670        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4671        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
4672        $table->add_field('namenull', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4673        $table->add_field('description', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL, null, null);
4674        $table->add_field('descriptionnull', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
4675        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4676        $dbman->create_table($table);
4677
4678        $DB->insert_record($tablename, array('name'=>'',   'namenull'=>'',   'description'=>'',   'descriptionnull'=>''));
4679        $DB->insert_record($tablename, array('name'=>'??', 'namenull'=>null, 'description'=>'??', 'descriptionnull'=>null));
4680        $DB->insert_record($tablename, array('name'=>'la', 'namenull'=>'la', 'description'=>'la', 'descriptionnull'=>'lalala'));
4681        $DB->insert_record($tablename, array('name'=>0,    'namenull'=>0,    'description'=>0,    'descriptionnull'=>0));
4682
4683        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isnotempty($tablename, 'name', false, false));
4684        $this->assertCount(3, $records);
4685        $record = reset($records);
4686        $this->assertSame('??', $record->name);
4687
4688        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isnotempty($tablename, 'namenull', true, false));
4689        $this->assertCount(2, $records); // Nulls aren't comparable (so they aren't "not empty"). SQL expected behaviour.
4690        $record = reset($records);
4691        $this->assertSame('la', $record->namenull); // So 'la' is the first non-empty 'namenull' record.
4692
4693        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isnotempty($tablename, 'description', false, true));
4694        $this->assertCount(3, $records);
4695        $record = reset($records);
4696        $this->assertSame('??', $record->description);
4697
4698        $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE ".$DB->sql_isnotempty($tablename, 'descriptionnull', true, true));
4699        $this->assertCount(2, $records); // Nulls aren't comparable (so they aren't "not empty"). SQL expected behaviour.
4700        $record = reset($records);
4701        $this->assertSame('lalala', $record->descriptionnull); // So 'lalala' is the first non-empty 'descriptionnull' record.
4702    }
4703
4704    public function test_sql_regex() {
4705        $DB = $this->tdb;
4706        $dbman = $DB->get_manager();
4707        if (!$DB->sql_regex_supported()) {
4708            $this->markTestSkipped($DB->get_name().' does not support regular expressions');
4709        }
4710
4711        $table = $this->get_test_table();
4712        $tablename = $table->getName();
4713
4714        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4715        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4716        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4717        $dbman->create_table($table);
4718
4719        $DB->insert_record($tablename, array('name'=>'LALALA'));
4720        $DB->insert_record($tablename, array('name'=>'holaaa'));
4721        $DB->insert_record($tablename, array('name'=>'aouch'));
4722
4723        // Regex /a$/i (case-insensitive).
4724        $sql = "SELECT * FROM {{$tablename}} WHERE name ".$DB->sql_regex()." ?";
4725        $params = array('a$');
4726        $records = $DB->get_records_sql($sql, $params);
4727        $this->assertCount(2, $records);
4728
4729        // Regex ! (not) /.a/i (case insensitive).
4730        $sql = "SELECT * FROM {{$tablename}} WHERE name ".$DB->sql_regex(false)." ?";
4731        $params = array('.a');
4732        $records = $DB->get_records_sql($sql, $params);
4733        $this->assertCount(1, $records);
4734
4735        // Regex /a$/ (case-sensitive).
4736        $sql = "SELECT * FROM {{$tablename}} WHERE name ".$DB->sql_regex(true, true)." ?";
4737        $params = array('a$');
4738        $records = $DB->get_records_sql($sql, $params);
4739        $this->assertCount(1, $records);
4740
4741        // Regex ! (not) /.a/ (case sensitive).
4742        $sql = "SELECT * FROM {{$tablename}} WHERE name ".$DB->sql_regex(false, true)." ?";
4743        $params = array('.a');
4744        $records = $DB->get_records_sql($sql, $params);
4745        $this->assertCount(2, $records);
4746
4747    }
4748
4749    /**
4750     * Test some complicated variations of set_field_select.
4751     */
4752    public function test_set_field_select_complicated() {
4753        $DB = $this->tdb;
4754        $dbman = $DB->get_manager();
4755
4756        $table = $this->get_test_table();
4757        $tablename = $table->getName();
4758
4759        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4760        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
4761        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4762        $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
4763        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4764        $dbman->create_table($table);
4765
4766        $DB->insert_record($tablename, array('course' => 3, 'content' => 'hello', 'name'=>'xyz'));
4767        $DB->insert_record($tablename, array('course' => 3, 'content' => 'world', 'name'=>'abc'));
4768        $DB->insert_record($tablename, array('course' => 5, 'content' => 'hello', 'name'=>'def'));
4769        $DB->insert_record($tablename, array('course' => 2, 'content' => 'universe', 'name'=>'abc'));
4770        // This SQL is a tricky case because we are selecting from the same table we are updating.
4771        $sql = 'id IN (SELECT outerq.id from (SELECT innerq.id from {' . $tablename . '} innerq WHERE course = 3) outerq)';
4772        $DB->set_field_select($tablename, 'name', 'ghi', $sql);
4773
4774        $this->assertSame(2, $DB->count_records_select($tablename, 'name = ?', array('ghi')));
4775
4776    }
4777
4778    /**
4779     * Test some more complex SQL syntax which moodle uses and depends on to work
4780     * useful to determine if new database libraries can be supported.
4781     */
4782    public function test_get_records_sql_complicated() {
4783        $DB = $this->tdb;
4784        $dbman = $DB->get_manager();
4785
4786        $table = $this->get_test_table();
4787        $tablename = $table->getName();
4788
4789        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4790        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
4791        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
4792        $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
4793        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4794        $dbman->create_table($table);
4795
4796        $DB->insert_record($tablename, array('course' => 3, 'content' => 'hello', 'name'=>'xyz'));
4797        $DB->insert_record($tablename, array('course' => 3, 'content' => 'world', 'name'=>'abc'));
4798        $DB->insert_record($tablename, array('course' => 5, 'content' => 'hello', 'name'=>'def'));
4799        $DB->insert_record($tablename, array('course' => 2, 'content' => 'universe', 'name'=>'abc'));
4800
4801        // Test grouping by expressions in the query. MDL-26819. Note that there are 4 ways:
4802        // - By column position (GROUP by 1) - Not supported by mssql & oracle
4803        // - By column name (GROUP by course) - Supported by all, but leading to wrong results
4804        // - By column alias (GROUP by casecol) - Not supported by mssql & oracle
4805        // - By complete expression (GROUP BY CASE ...) - 100% cross-db, this test checks it
4806        $sql = "SELECT (CASE WHEN course = 3 THEN 1 ELSE 0 END) AS casecol,
4807                       COUNT(1) AS countrecs,
4808                       MAX(name) AS maxname
4809                  FROM {{$tablename}}
4810              GROUP BY CASE WHEN course = 3 THEN 1 ELSE 0 END
4811              ORDER BY casecol DESC";
4812        $result = array(
4813            1 => (object)array('casecol' => 1, 'countrecs' => 2, 'maxname' => 'xyz'),
4814            0 => (object)array('casecol' => 0, 'countrecs' => 2, 'maxname' => 'def'));
4815        $records = $DB->get_records_sql($sql, null);
4816        $this->assertEquals($result, $records);
4817
4818        // Another grouping by CASE expression just to ensure it works ok for multiple WHEN.
4819        $sql = "SELECT CASE name
4820                            WHEN 'xyz' THEN 'last'
4821                            WHEN 'def' THEN 'mid'
4822                            WHEN 'abc' THEN 'first'
4823                       END AS casecol,
4824                       COUNT(1) AS countrecs,
4825                       MAX(name) AS maxname
4826                  FROM {{$tablename}}
4827              GROUP BY CASE name
4828                           WHEN 'xyz' THEN 'last'
4829                           WHEN 'def' THEN 'mid'
4830                           WHEN 'abc' THEN 'first'
4831                       END
4832              ORDER BY casecol DESC";
4833        $result = array(
4834            'mid'  => (object)array('casecol' => 'mid', 'countrecs' => 1, 'maxname' => 'def'),
4835            'last' => (object)array('casecol' => 'last', 'countrecs' => 1, 'maxname' => 'xyz'),
4836            'first'=> (object)array('casecol' => 'first', 'countrecs' => 2, 'maxname' => 'abc'));
4837        $records = $DB->get_records_sql($sql, null);
4838        $this->assertEquals($result, $records);
4839
4840        // Test CASE expressions in the ORDER BY clause - used by MDL-34657.
4841        $sql = "SELECT id, course, name
4842                  FROM {{$tablename}}
4843              ORDER BY CASE WHEN (course = 5 OR name  = 'xyz') THEN 0 ELSE 1 END, name, course";
4844        // First, records matching the course = 5 OR name = 'xyz', then the rest. Each.
4845        // group ordered by name and course.
4846        $result = array(
4847            3 => (object)array('id' => 3, 'course' => 5, 'name' => 'def'),
4848            1 => (object)array('id' => 1, 'course' => 3, 'name' => 'xyz'),
4849            4 => (object)array('id' => 4, 'course' => 2, 'name' => 'abc'),
4850            2 => (object)array('id' => 2, 'course' => 3, 'name' => 'abc'));
4851        $records = $DB->get_records_sql($sql, null);
4852        $this->assertEquals($result, $records);
4853        // Verify also array keys, order is important in this test.
4854        $this->assertEquals(array_keys($result), array_keys($records));
4855
4856        // Test limits in queries with DISTINCT/ALL clauses and multiple whitespace. MDL-25268.
4857        $sql = "SELECT   DISTINCT   course
4858                  FROM {{$tablename}}
4859                 ORDER BY course";
4860        // Only limitfrom.
4861        $records = $DB->get_records_sql($sql, null, 1);
4862        $this->assertCount(2, $records);
4863        $this->assertEquals(3, reset($records)->course);
4864        $this->assertEquals(5, next($records)->course);
4865        // Only limitnum.
4866        $records = $DB->get_records_sql($sql, null, 0, 2);
4867        $this->assertCount(2, $records);
4868        $this->assertEquals(2, reset($records)->course);
4869        $this->assertEquals(3, next($records)->course);
4870        // Both limitfrom and limitnum.
4871        $records = $DB->get_records_sql($sql, null, 2, 2);
4872        $this->assertCount(1, $records);
4873        $this->assertEquals(5, reset($records)->course);
4874
4875        // We have sql like this in moodle, this syntax breaks on older versions of sqlite for example..
4876        $sql = "SELECT a.id AS id, a.course AS course
4877                  FROM {{$tablename}} a
4878                  JOIN (SELECT * FROM {{$tablename}}) b ON a.id = b.id
4879                 WHERE a.course = ?";
4880
4881        $records = $DB->get_records_sql($sql, array(3));
4882        $this->assertCount(2, $records);
4883        $this->assertEquals(1, reset($records)->id);
4884        $this->assertEquals(2, next($records)->id);
4885
4886        // Do NOT try embedding sql_xxxx() helper functions in conditions array of count_records(), they don't break params/binding!
4887        $count = $DB->count_records_select($tablename, "course = :course AND ".$DB->sql_compare_text('content')." = :content", array('course' => 3, 'content' => 'hello'));
4888        $this->assertEquals(1, $count);
4889
4890        // Test int x string comparison.
4891        $sql = "SELECT *
4892                  FROM {{$tablename}} c
4893                 WHERE name = ?";
4894        $this->assertCount(0, $DB->get_records_sql($sql, array(10)));
4895        $this->assertCount(0, $DB->get_records_sql($sql, array("10")));
4896        $DB->insert_record($tablename, array('course' => 7, 'content' => 'xx', 'name'=>'1'));
4897        $DB->insert_record($tablename, array('course' => 7, 'content' => 'yy', 'name'=>'2'));
4898        $this->assertCount(1, $DB->get_records_sql($sql, array(1)));
4899        $this->assertCount(1, $DB->get_records_sql($sql, array("1")));
4900        $this->assertCount(0, $DB->get_records_sql($sql, array(10)));
4901        $this->assertCount(0, $DB->get_records_sql($sql, array("10")));
4902        $DB->insert_record($tablename, array('course' => 7, 'content' => 'xx', 'name'=>'1abc'));
4903        $this->assertCount(1, $DB->get_records_sql($sql, array(1)));
4904        $this->assertCount(1, $DB->get_records_sql($sql, array("1")));
4905
4906        // Test get_in_or_equal() with a big number of elements. Note that ideally
4907        // we should be detecting and warning about any use over, say, 200 elements
4908        // And recommend to change code to use subqueries and/or chunks instead.
4909        $currentcount = $DB->count_records($tablename);
4910        $numelements = 10000; // Verify that we can handle 10000 elements (crazy!)
4911        $values = range(1, $numelements);
4912
4913        list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM); // With QM params.
4914        $sql = "SELECT *
4915                  FROM {{$tablename}}
4916                 WHERE id $insql";
4917        $results = $DB->get_records_sql($sql, $inparams);
4918        $this->assertCount($currentcount, $results);
4919
4920        list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED); // With NAMED params.
4921        $sql = "SELECT *
4922                  FROM {{$tablename}}
4923                 WHERE id $insql";
4924        $results = $DB->get_records_sql($sql, $inparams);
4925        $this->assertCount($currentcount, $results);
4926    }
4927
4928    public function test_replace_all_text() {
4929        $DB = $this->tdb;
4930        $dbman = $DB->get_manager();
4931
4932        if (!$DB->replace_all_text_supported()) {
4933            $this->markTestSkipped($DB->get_name().' does not support replacing of texts');
4934        }
4935
4936        $table = $this->get_test_table();
4937        $tablename = $table->getName();
4938
4939        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
4940        $table->add_field('name', XMLDB_TYPE_CHAR, '20', null, null);
4941        $table->add_field('intro', XMLDB_TYPE_TEXT, 'big', null, null);
4942        // Add a CHAR field named using a word reserved for all the supported DB servers.
4943        $table->add_field('where', XMLDB_TYPE_CHAR, '20', null, null, null, 'localhost');
4944        // Add a TEXT field named using a word reserved for all the supported DB servers.
4945        $table->add_field('from', XMLDB_TYPE_TEXT, 'big', null, null);
4946        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
4947        $dbman->create_table($table);
4948
4949        $fromfield = $dbman->generator->getEncQuoted('from');
4950        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES (NULL,NULL,'localhost')");
4951        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('','','localhost')");
4952        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('xxyy','vvzz','localhost')");
4953        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('aa bb aa bb','cc dd cc aa','localhost')");
4954        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('kkllll','kkllll','localhost')");
4955
4956        $expected = $DB->get_records($tablename, array(), 'id ASC');
4957        $idx = 1;
4958        $id1 = $id2 = $id3 = $id4 = $id5 = 0;
4959        foreach (array_keys($expected) as $identifier) {
4960            ${"id$idx"} = (string)$identifier;
4961            $idx++;
4962        }
4963
4964        $columns = $DB->get_columns($tablename);
4965
4966        // Replace should work even with columns named using a reserved word.
4967        $this->assertEquals('C', $columns['where']->meta_type);
4968        $this->assertEquals('localhost', $expected[$id1]->where);
4969        $this->assertEquals('localhost', $expected[$id2]->where);
4970        $this->assertEquals('localhost', $expected[$id3]->where);
4971        $this->assertEquals('localhost', $expected[$id4]->where);
4972        $this->assertEquals('localhost', $expected[$id5]->where);
4973        $DB->replace_all_text($tablename, $columns['where'], 'localhost', '::1');
4974        $result = $DB->get_records($tablename, array(), 'id ASC');
4975        $expected[$id1]->where = '::1';
4976        $expected[$id2]->where = '::1';
4977        $expected[$id3]->where = '::1';
4978        $expected[$id4]->where = '::1';
4979        $expected[$id5]->where = '::1';
4980        $this->assertEquals($expected, $result);
4981        $this->assertEquals('X', $columns['from']->meta_type);
4982        $DB->replace_all_text($tablename, $columns['from'], 'localhost', '127.0.0.1');
4983        $result = $DB->get_records($tablename, array(), 'id ASC');
4984        $expected[$id1]->from = '127.0.0.1';
4985        $expected[$id2]->from = '127.0.0.1';
4986        $expected[$id3]->from = '127.0.0.1';
4987        $expected[$id4]->from = '127.0.0.1';
4988        $expected[$id5]->from = '127.0.0.1';
4989        $this->assertEquals($expected, $result);
4990
4991        $DB->replace_all_text($tablename, $columns['name'], 'aa', 'o');
4992        $result = $DB->get_records($tablename, array(), 'id ASC');
4993        $expected[$id4]->name = 'o bb o bb';
4994        $this->assertEquals($expected, $result);
4995
4996        $DB->replace_all_text($tablename, $columns['intro'], 'aa', 'o');
4997        $result = $DB->get_records($tablename, array(), 'id ASC');
4998        $expected[$id4]->intro = 'cc dd cc o';
4999        $this->assertEquals($expected, $result);
5000
5001        $DB->replace_all_text($tablename, $columns['name'], '_', '*');
5002        $DB->replace_all_text($tablename, $columns['name'], '?', '*');
5003        $DB->replace_all_text($tablename, $columns['name'], '%', '*');
5004        $DB->replace_all_text($tablename, $columns['intro'], '_', '*');
5005        $DB->replace_all_text($tablename, $columns['intro'], '?', '*');
5006        $DB->replace_all_text($tablename, $columns['intro'], '%', '*');
5007        $result = $DB->get_records($tablename, array(), 'id ASC');
5008        $this->assertEquals($expected, $result);
5009
5010        $long = '1234567890123456789';
5011        $DB->replace_all_text($tablename, $columns['name'], 'kk', $long);
5012        $result = $DB->get_records($tablename, array(), 'id ASC');
5013        $expected[$id5]->name = core_text::substr($long.'llll', 0, 20);
5014        $this->assertEquals($expected, $result);
5015
5016        $DB->replace_all_text($tablename, $columns['intro'], 'kk', $long);
5017        $result = $DB->get_records($tablename, array(), 'id ASC');
5018        $expected[$id5]->intro = $long.'llll';
5019        $this->assertEquals($expected, $result);
5020    }
5021
5022    public function test_onelevel_commit() {
5023        $DB = $this->tdb;
5024        $dbman = $DB->get_manager();
5025
5026        $table = $this->get_test_table();
5027        $tablename = $table->getName();
5028
5029        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5030        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5031        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5032        $dbman->create_table($table);
5033
5034        $transaction = $DB->start_delegated_transaction();
5035        $data = (object)array('course'=>3);
5036        $this->assertEquals(0, $DB->count_records($tablename));
5037        $DB->insert_record($tablename, $data);
5038        $this->assertEquals(1, $DB->count_records($tablename));
5039        $transaction->allow_commit();
5040        $this->assertEquals(1, $DB->count_records($tablename));
5041    }
5042
5043    public function test_transaction_ignore_error_trouble() {
5044        $DB = $this->tdb;
5045        $dbman = $DB->get_manager();
5046
5047        $table = $this->get_test_table();
5048        $tablename = $table->getName();
5049
5050        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5051        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5052        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5053        $table->add_index('course', XMLDB_INDEX_UNIQUE, array('course'));
5054        $dbman->create_table($table);
5055
5056        // Test error on SQL_QUERY_INSERT.
5057        $transaction = $DB->start_delegated_transaction();
5058        $this->assertEquals(0, $DB->count_records($tablename));
5059        $DB->insert_record($tablename, (object)array('course'=>1));
5060        $this->assertEquals(1, $DB->count_records($tablename));
5061        try {
5062            $DB->insert_record($tablename, (object)array('course'=>1));
5063        } catch (Exception $e) {
5064            // This must be ignored and it must not roll back the whole transaction.
5065        }
5066        $DB->insert_record($tablename, (object)array('course'=>2));
5067        $this->assertEquals(2, $DB->count_records($tablename));
5068        $transaction->allow_commit();
5069        $this->assertEquals(2, $DB->count_records($tablename));
5070        $this->assertFalse($DB->is_transaction_started());
5071
5072        // Test error on SQL_QUERY_SELECT.
5073        $DB->delete_records($tablename);
5074        $transaction = $DB->start_delegated_transaction();
5075        $this->assertEquals(0, $DB->count_records($tablename));
5076        $DB->insert_record($tablename, (object)array('course'=>1));
5077        $this->assertEquals(1, $DB->count_records($tablename));
5078        try {
5079            $DB->get_records_sql('s e l e c t');
5080        } catch (moodle_exception $e) {
5081            // This must be ignored and it must not roll back the whole transaction.
5082        }
5083        $DB->insert_record($tablename, (object)array('course'=>2));
5084        $this->assertEquals(2, $DB->count_records($tablename));
5085        $transaction->allow_commit();
5086        $this->assertEquals(2, $DB->count_records($tablename));
5087        $this->assertFalse($DB->is_transaction_started());
5088
5089        // Test error on structure SQL_QUERY_UPDATE.
5090        $DB->delete_records($tablename);
5091        $transaction = $DB->start_delegated_transaction();
5092        $this->assertEquals(0, $DB->count_records($tablename));
5093        $DB->insert_record($tablename, (object)array('course'=>1));
5094        $this->assertEquals(1, $DB->count_records($tablename));
5095        try {
5096            $DB->execute('xxxx');
5097        } catch (moodle_exception $e) {
5098            // This must be ignored and it must not roll back the whole transaction.
5099        }
5100        $DB->insert_record($tablename, (object)array('course'=>2));
5101        $this->assertEquals(2, $DB->count_records($tablename));
5102        $transaction->allow_commit();
5103        $this->assertEquals(2, $DB->count_records($tablename));
5104        $this->assertFalse($DB->is_transaction_started());
5105
5106        // Test error on structure SQL_QUERY_STRUCTURE.
5107        $DB->delete_records($tablename);
5108        $transaction = $DB->start_delegated_transaction();
5109        $this->assertEquals(0, $DB->count_records($tablename));
5110        $DB->insert_record($tablename, (object)array('course'=>1));
5111        $this->assertEquals(1, $DB->count_records($tablename));
5112        try {
5113            $DB->change_database_structure('xxxx');
5114        } catch (moodle_exception $e) {
5115            // This must be ignored and it must not roll back the whole transaction.
5116        }
5117        $DB->insert_record($tablename, (object)array('course'=>2));
5118        $this->assertEquals(2, $DB->count_records($tablename));
5119        $transaction->allow_commit();
5120        $this->assertEquals(2, $DB->count_records($tablename));
5121        $this->assertFalse($DB->is_transaction_started());
5122
5123        // NOTE: SQL_QUERY_STRUCTURE is intentionally not tested here because it should never fail.
5124    }
5125
5126    public function test_onelevel_rollback() {
5127        $DB = $this->tdb;
5128        $dbman = $DB->get_manager();
5129
5130        $table = $this->get_test_table();
5131        $tablename = $table->getName();
5132
5133        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5134        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5135        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5136        $dbman->create_table($table);
5137
5138        // This might in fact encourage ppl to migrate from myisam to innodb.
5139
5140        $transaction = $DB->start_delegated_transaction();
5141        $data = (object)array('course'=>3);
5142        $this->assertEquals(0, $DB->count_records($tablename));
5143        $DB->insert_record($tablename, $data);
5144        $this->assertEquals(1, $DB->count_records($tablename));
5145        try {
5146            $transaction->rollback(new Exception('test'));
5147            $this->fail('transaction rollback must rethrow exception');
5148        } catch (Exception $e) {
5149            // Ignored.
5150        }
5151        $this->assertEquals(0, $DB->count_records($tablename));
5152    }
5153
5154    public function test_nested_transactions() {
5155        $DB = $this->tdb;
5156        $dbman = $DB->get_manager();
5157
5158        $table = $this->get_test_table();
5159        $tablename = $table->getName();
5160
5161        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5162        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5163        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5164        $dbman->create_table($table);
5165
5166        // Two level commit.
5167        $this->assertFalse($DB->is_transaction_started());
5168        $transaction1 = $DB->start_delegated_transaction();
5169        $this->assertTrue($DB->is_transaction_started());
5170        $data = (object)array('course'=>3);
5171        $DB->insert_record($tablename, $data);
5172        $transaction2 = $DB->start_delegated_transaction();
5173        $data = (object)array('course'=>4);
5174        $DB->insert_record($tablename, $data);
5175        $transaction2->allow_commit();
5176        $this->assertTrue($DB->is_transaction_started());
5177        $transaction1->allow_commit();
5178        $this->assertFalse($DB->is_transaction_started());
5179        $this->assertEquals(2, $DB->count_records($tablename));
5180
5181        $DB->delete_records($tablename);
5182
5183        // Rollback from top level.
5184        $transaction1 = $DB->start_delegated_transaction();
5185        $data = (object)array('course'=>3);
5186        $DB->insert_record($tablename, $data);
5187        $transaction2 = $DB->start_delegated_transaction();
5188        $data = (object)array('course'=>4);
5189        $DB->insert_record($tablename, $data);
5190        $transaction2->allow_commit();
5191        try {
5192            $transaction1->rollback(new Exception('test'));
5193            $this->fail('transaction rollback must rethrow exception');
5194        } catch (Exception $e) {
5195            $this->assertEquals(get_class($e), 'Exception');
5196        }
5197        $this->assertEquals(0, $DB->count_records($tablename));
5198
5199        $DB->delete_records($tablename);
5200
5201        // Rollback from nested level.
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            $transaction2->rollback(new Exception('test'));
5210            $this->fail('transaction rollback must rethrow exception');
5211        } catch (Exception $e) {
5212            $this->assertEquals(get_class($e), 'Exception');
5213        }
5214        $this->assertEquals(2, $DB->count_records($tablename)); // Not rolled back yet.
5215        try {
5216            $transaction1->allow_commit();
5217        } catch (moodle_exception $e) {
5218            $this->assertInstanceOf('dml_transaction_exception', $e);
5219        }
5220        $this->assertEquals(2, $DB->count_records($tablename)); // Not rolled back yet.
5221        // The forced rollback is done from the default_exception handler and similar places,
5222        // let's do it manually here.
5223        $this->assertTrue($DB->is_transaction_started());
5224        $DB->force_transaction_rollback();
5225        $this->assertFalse($DB->is_transaction_started());
5226        $this->assertEquals(0, $DB->count_records($tablename)); // Finally rolled back.
5227
5228        $DB->delete_records($tablename);
5229
5230        // Test interactions of recordset and transactions - this causes problems in SQL Server.
5231        $table2 = $this->get_test_table('2');
5232        $tablename2 = $table2->getName();
5233
5234        $table2->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5235        $table2->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5236        $table2->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5237        $dbman->create_table($table2);
5238
5239        $DB->insert_record($tablename, array('course'=>1));
5240        $DB->insert_record($tablename, array('course'=>2));
5241        $DB->insert_record($tablename, array('course'=>3));
5242
5243        $DB->insert_record($tablename2, array('course'=>5));
5244        $DB->insert_record($tablename2, array('course'=>6));
5245        $DB->insert_record($tablename2, array('course'=>7));
5246        $DB->insert_record($tablename2, array('course'=>8));
5247
5248        $rs1 = $DB->get_recordset($tablename);
5249        $i = 0;
5250        foreach ($rs1 as $record1) {
5251            $i++;
5252            $rs2 = $DB->get_recordset($tablename2);
5253            $j = 0;
5254            foreach ($rs2 as $record2) {
5255                $t = $DB->start_delegated_transaction();
5256                $DB->set_field($tablename, 'course', $record1->course+1, array('id'=>$record1->id));
5257                $DB->set_field($tablename2, 'course', $record2->course+1, array('id'=>$record2->id));
5258                $t->allow_commit();
5259                $j++;
5260            }
5261            $rs2->close();
5262            $this->assertEquals(4, $j);
5263        }
5264        $rs1->close();
5265        $this->assertEquals(3, $i);
5266
5267        // Test nested recordsets isolation without transaction.
5268        $DB->delete_records($tablename);
5269        $DB->insert_record($tablename, array('course'=>1));
5270        $DB->insert_record($tablename, array('course'=>2));
5271        $DB->insert_record($tablename, array('course'=>3));
5272
5273        $DB->delete_records($tablename2);
5274        $DB->insert_record($tablename2, array('course'=>5));
5275        $DB->insert_record($tablename2, array('course'=>6));
5276        $DB->insert_record($tablename2, array('course'=>7));
5277        $DB->insert_record($tablename2, array('course'=>8));
5278
5279        $rs1 = $DB->get_recordset($tablename);
5280        $i = 0;
5281        foreach ($rs1 as $record1) {
5282            $i++;
5283            $rs2 = $DB->get_recordset($tablename2);
5284            $j = 0;
5285            foreach ($rs2 as $record2) {
5286                $DB->set_field($tablename, 'course', $record1->course+1, array('id'=>$record1->id));
5287                $DB->set_field($tablename2, 'course', $record2->course+1, array('id'=>$record2->id));
5288                $j++;
5289            }
5290            $rs2->close();
5291            $this->assertEquals(4, $j);
5292        }
5293        $rs1->close();
5294        $this->assertEquals(3, $i);
5295    }
5296
5297    public function test_transactions_forbidden() {
5298        $DB = $this->tdb;
5299        $dbman = $DB->get_manager();
5300
5301        $table = $this->get_test_table();
5302        $tablename = $table->getName();
5303
5304        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5305        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5306        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5307        $dbman->create_table($table);
5308
5309        $DB->transactions_forbidden();
5310        $transaction = $DB->start_delegated_transaction();
5311        $data = (object)array('course'=>1);
5312        $DB->insert_record($tablename, $data);
5313        try {
5314            $DB->transactions_forbidden();
5315        } catch (moodle_exception $e) {
5316            $this->assertInstanceOf('dml_transaction_exception', $e);
5317        }
5318        // The previous test does not force rollback.
5319        $transaction->allow_commit();
5320        $this->assertFalse($DB->is_transaction_started());
5321        $this->assertEquals(1, $DB->count_records($tablename));
5322    }
5323
5324    public function test_wrong_transactions() {
5325        $DB = $this->tdb;
5326        $dbman = $DB->get_manager();
5327
5328        $table = $this->get_test_table();
5329        $tablename = $table->getName();
5330
5331        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5332        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5333        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5334        $dbman->create_table($table);
5335
5336        // Wrong order of nested commits.
5337        $transaction1 = $DB->start_delegated_transaction();
5338        $data = (object)array('course'=>3);
5339        $DB->insert_record($tablename, $data);
5340        $transaction2 = $DB->start_delegated_transaction();
5341        $data = (object)array('course'=>4);
5342        $DB->insert_record($tablename, $data);
5343        try {
5344            $transaction1->allow_commit();
5345            $this->fail('wrong order of commits must throw exception');
5346        } catch (moodle_exception $e) {
5347            $this->assertInstanceOf('dml_transaction_exception', $e);
5348        }
5349        try {
5350            $transaction2->allow_commit();
5351            $this->fail('first wrong commit forces rollback');
5352        } catch (moodle_exception $e) {
5353            $this->assertInstanceOf('dml_transaction_exception', $e);
5354        }
5355        // This is done in default exception handler usually.
5356        $this->assertTrue($DB->is_transaction_started());
5357        $this->assertEquals(2, $DB->count_records($tablename)); // Not rolled back yet.
5358        $DB->force_transaction_rollback();
5359        $this->assertEquals(0, $DB->count_records($tablename));
5360        $DB->delete_records($tablename);
5361
5362        // Wrong order of nested rollbacks.
5363        $transaction1 = $DB->start_delegated_transaction();
5364        $data = (object)array('course'=>3);
5365        $DB->insert_record($tablename, $data);
5366        $transaction2 = $DB->start_delegated_transaction();
5367        $data = (object)array('course'=>4);
5368        $DB->insert_record($tablename, $data);
5369        try {
5370            // This first rollback should prevent all other rollbacks.
5371            $transaction1->rollback(new Exception('test'));
5372        } catch (Exception $e) {
5373            $this->assertEquals(get_class($e), 'Exception');
5374        }
5375        try {
5376            $transaction2->rollback(new Exception('test'));
5377        } catch (Exception $e) {
5378            $this->assertEquals(get_class($e), 'Exception');
5379        }
5380        try {
5381            $transaction1->rollback(new Exception('test'));
5382        } catch (moodle_exception $e) {
5383            $this->assertInstanceOf('dml_transaction_exception', $e);
5384        }
5385        // This is done in default exception handler usually.
5386        $this->assertTrue($DB->is_transaction_started());
5387        $DB->force_transaction_rollback();
5388        $DB->delete_records($tablename);
5389
5390        // Unknown transaction object.
5391        $transaction1 = $DB->start_delegated_transaction();
5392        $data = (object)array('course'=>3);
5393        $DB->insert_record($tablename, $data);
5394        $transaction2 = new moodle_transaction($DB);
5395        try {
5396            $transaction2->allow_commit();
5397            $this->fail('foreign transaction must fail');
5398        } catch (moodle_exception $e) {
5399            $this->assertInstanceOf('dml_transaction_exception', $e);
5400        }
5401        try {
5402            $transaction1->allow_commit();
5403            $this->fail('first wrong commit forces rollback');
5404        } catch (moodle_exception $e) {
5405            $this->assertInstanceOf('dml_transaction_exception', $e);
5406        }
5407        $DB->force_transaction_rollback();
5408        $DB->delete_records($tablename);
5409    }
5410
5411    public function test_concurent_transactions() {
5412        // Notes about this test:
5413        // 1- MySQL needs to use one engine with transactions support (InnoDB).
5414        // 2- MSSQL needs to have enabled versioning for read committed
5415        //    transactions (ALTER DATABASE xxx SET READ_COMMITTED_SNAPSHOT ON)
5416        $DB = $this->tdb;
5417        $dbman = $DB->get_manager();
5418
5419        $table = $this->get_test_table();
5420        $tablename = $table->getName();
5421
5422        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5423        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5424        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5425        $dbman->create_table($table);
5426
5427        $transaction = $DB->start_delegated_transaction();
5428        $data = (object)array('course'=>1);
5429        $this->assertEquals(0, $DB->count_records($tablename));
5430        $DB->insert_record($tablename, $data);
5431        $this->assertEquals(1, $DB->count_records($tablename));
5432
5433        // Open second connection.
5434        $cfg = $DB->export_dbconfig();
5435        if (!isset($cfg->dboptions)) {
5436            $cfg->dboptions = array();
5437        }
5438        // If we have a readonly slave situation, we need to either observe
5439        // the latency, or if the latency is not specified we need to take
5440        // the slave out because the table may not have propagated yet.
5441        if (isset($cfg->dboptions['readonly'])) {
5442            if (isset($cfg->dboptions['readonly']['latency'])) {
5443                usleep(intval(1000000 * $cfg->dboptions['readonly']['latency']));
5444            } else {
5445                unset($cfg->dboptions['readonly']);
5446            }
5447        }
5448        $DB2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
5449        $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
5450
5451        // Second instance should not see pending inserts.
5452        $this->assertEquals(0, $DB2->count_records($tablename));
5453        $data = (object)array('course'=>2);
5454        $DB2->insert_record($tablename, $data);
5455        $this->assertEquals(1, $DB2->count_records($tablename));
5456
5457        // First should see the changes done from second.
5458        $this->assertEquals(2, $DB->count_records($tablename));
5459
5460        // Now commit and we should see it finally in second connections.
5461        $transaction->allow_commit();
5462        $this->assertEquals(2, $DB2->count_records($tablename));
5463
5464        // Let's try delete all is also working on (this checks MDL-29198).
5465        // Initially both connections see all the records in the table (2).
5466        $this->assertEquals(2, $DB->count_records($tablename));
5467        $this->assertEquals(2, $DB2->count_records($tablename));
5468        $transaction = $DB->start_delegated_transaction();
5469
5470        // Delete all from within transaction.
5471        $DB->delete_records($tablename);
5472
5473        // Transactional $DB, sees 0 records now.
5474        $this->assertEquals(0, $DB->count_records($tablename));
5475
5476        // Others ($DB2) get no changes yet.
5477        $this->assertEquals(2, $DB2->count_records($tablename));
5478
5479        // Now commit and we should see changes.
5480        $transaction->allow_commit();
5481        $this->assertEquals(0, $DB2->count_records($tablename));
5482
5483        $DB2->dispose();
5484    }
5485
5486    public function test_session_locks() {
5487        $DB = $this->tdb;
5488        $dbman = $DB->get_manager();
5489
5490        // Open second connection.
5491        $cfg = $DB->export_dbconfig();
5492        if (!isset($cfg->dboptions)) {
5493            $cfg->dboptions = array();
5494        }
5495        $DB2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
5496        $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
5497
5498        // Testing that acquiring a lock effectively locks.
5499        // Get a session lock on connection1.
5500        $rowid = rand(100, 200);
5501        $timeout = 1;
5502        $DB->get_session_lock($rowid, $timeout);
5503
5504        // Try to get the same session lock on connection2.
5505        try {
5506            $DB2->get_session_lock($rowid, $timeout);
5507            $DB2->release_session_lock($rowid); // Should not be executed, but here for safety.
5508            $this->fail('An Exception is missing, expected due to session lock acquired.');
5509        } catch (moodle_exception $e) {
5510            $this->assertInstanceOf('dml_sessionwait_exception', $e);
5511            $DB->release_session_lock($rowid); // Release lock on connection1.
5512        }
5513
5514        // Testing that releasing a lock effectively frees.
5515        // Get a session lock on connection1.
5516        $rowid = rand(100, 200);
5517        $timeout = 1;
5518        $DB->get_session_lock($rowid, $timeout);
5519        // Release the lock on connection1.
5520        $DB->release_session_lock($rowid);
5521
5522        // Get the just released lock on connection2.
5523        $DB2->get_session_lock($rowid, $timeout);
5524        // Release the lock on connection2.
5525        $DB2->release_session_lock($rowid);
5526
5527        $DB2->dispose();
5528    }
5529
5530    public function test_bound_param_types() {
5531        $DB = $this->tdb;
5532        $dbman = $DB->get_manager();
5533
5534        $table = $this->get_test_table();
5535        $tablename = $table->getName();
5536
5537        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5538        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
5539        $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
5540        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5541        $dbman->create_table($table);
5542
5543        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => '1', 'content'=>'xx')));
5544        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 2, 'content'=>'yy')));
5545        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'somestring', 'content'=>'zz')));
5546        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'aa', 'content'=>'1')));
5547        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'bb', 'content'=>2)));
5548        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'cc', 'content'=>'sometext')));
5549
5550        // Conditions in CHAR columns.
5551        $this->assertTrue($DB->record_exists($tablename, array('name'=>1)));
5552        $this->assertTrue($DB->record_exists($tablename, array('name'=>'1')));
5553        $this->assertFalse($DB->record_exists($tablename, array('name'=>111)));
5554        $this->assertNotEmpty($DB->get_record($tablename, array('name'=>1)));
5555        $this->assertNotEmpty($DB->get_record($tablename, array('name'=>'1')));
5556        $this->assertEmpty($DB->get_record($tablename, array('name'=>111)));
5557        $sqlqm = "SELECT *
5558                    FROM {{$tablename}}
5559                   WHERE name = ?";
5560        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, array(1)));
5561        $this->assertCount(1, $records);
5562        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, array('1')));
5563        $this->assertCount(1, $records);
5564        $records = $DB->get_records_sql($sqlqm, array(222));
5565        $this->assertCount(0, $records);
5566        $sqlnamed = "SELECT *
5567                       FROM {{$tablename}}
5568                      WHERE name = :name";
5569        $this->assertNotEmpty($records = $DB->get_records_sql($sqlnamed, array('name' => 2)));
5570        $this->assertCount(1, $records);
5571        $this->assertNotEmpty($records = $DB->get_records_sql($sqlnamed, array('name' => '2')));
5572        $this->assertCount(1, $records);
5573
5574        // Conditions in TEXT columns always must be performed with the sql_compare_text
5575        // helper function on both sides of the condition.
5576        $sqlqm = "SELECT *
5577                    FROM {{$tablename}}
5578                   WHERE " . $DB->sql_compare_text('content') . " =  " . $DB->sql_compare_text('?');
5579        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, array('1')));
5580        $this->assertCount(1, $records);
5581        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, array(1)));
5582        $this->assertCount(1, $records);
5583        $sqlnamed = "SELECT *
5584                       FROM {{$tablename}}
5585                      WHERE " . $DB->sql_compare_text('content') . " =  " . $DB->sql_compare_text(':content');
5586        $this->assertNotEmpty($records = $DB->get_records_sql($sqlnamed, array('content' => 2)));
5587        $this->assertCount(1, $records);
5588        $this->assertNotEmpty($records = $DB->get_records_sql($sqlnamed, array('content' => '2')));
5589        $this->assertCount(1, $records);
5590    }
5591
5592    public function test_bound_param_reserved() {
5593        $DB = $this->tdb;
5594        $dbman = $DB->get_manager();
5595
5596        $table = $this->get_test_table();
5597        $tablename = $table->getName();
5598
5599        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5600        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5601        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5602        $dbman->create_table($table);
5603
5604        $DB->insert_record($tablename, array('course' => '1'));
5605
5606        // Make sure reserved words do not cause fatal problems in query parameters.
5607
5608        $DB->execute("UPDATE {{$tablename}} SET course = 1 WHERE id = :select", array('select'=>1));
5609        $DB->get_records_sql("SELECT * FROM {{$tablename}} WHERE course = :select", array('select'=>1));
5610        $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}} WHERE course = :select", array('select'=>1));
5611        $rs->close();
5612        $DB->get_fieldset_sql("SELECT id FROM {{$tablename}} WHERE course = :select", array('select'=>1));
5613        $DB->set_field_select($tablename, 'course', '1', "id = :select", array('select'=>1));
5614        $DB->delete_records_select($tablename, "id = :select", array('select'=>1));
5615
5616        // If we get here test passed ok.
5617        $this->assertTrue(true);
5618    }
5619
5620    public function test_limits_and_offsets() {
5621        $DB = $this->tdb;
5622        $dbman = $DB->get_manager();
5623
5624        $table = $this->get_test_table();
5625        $tablename = $table->getName();
5626
5627        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5628        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
5629        $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
5630        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5631        $dbman->create_table($table);
5632
5633        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'a', 'content'=>'one')));
5634        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'b', 'content'=>'two')));
5635        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'c', 'content'=>'three')));
5636        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'd', 'content'=>'four')));
5637        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'e', 'content'=>'five')));
5638        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'f', 'content'=>'six')));
5639
5640        $sqlqm = "SELECT *
5641                    FROM {{$tablename}}";
5642        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 4));
5643        $this->assertCount(2, $records);
5644        $this->assertSame('e', reset($records)->name);
5645        $this->assertSame('f', end($records)->name);
5646
5647        $sqlqm = "SELECT *
5648                    FROM {{$tablename}}";
5649        $this->assertEmpty($records = $DB->get_records_sql($sqlqm, null, 8));
5650
5651        $sqlqm = "SELECT *
5652                    FROM {{$tablename}}";
5653        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 4));
5654        $this->assertCount(4, $records);
5655        $this->assertSame('a', reset($records)->name);
5656        $this->assertSame('d', end($records)->name);
5657
5658        $sqlqm = "SELECT *
5659                    FROM {{$tablename}}";
5660        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 8));
5661        $this->assertCount(6, $records);
5662        $this->assertSame('a', reset($records)->name);
5663        $this->assertSame('f', end($records)->name);
5664
5665        $sqlqm = "SELECT *
5666                    FROM {{$tablename}}";
5667        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 1, 4));
5668        $this->assertCount(4, $records);
5669        $this->assertSame('b', reset($records)->name);
5670        $this->assertSame('e', end($records)->name);
5671
5672        $sqlqm = "SELECT *
5673                    FROM {{$tablename}}";
5674        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 4, 4));
5675        $this->assertCount(2, $records);
5676        $this->assertSame('e', reset($records)->name);
5677        $this->assertSame('f', end($records)->name);
5678
5679        $sqlqm = "SELECT t.*, t.name AS test
5680                    FROM {{$tablename}} t
5681                    ORDER BY t.id ASC";
5682        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 4, 4));
5683        $this->assertCount(2, $records);
5684        $this->assertSame('e', reset($records)->name);
5685        $this->assertSame('f', end($records)->name);
5686
5687        $sqlqm = "SELECT DISTINCT t.name, t.name AS test
5688                    FROM {{$tablename}} t
5689                    ORDER BY t.name DESC";
5690        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 4, 4));
5691        $this->assertCount(2, $records);
5692        $this->assertSame('b', reset($records)->name);
5693        $this->assertSame('a', end($records)->name);
5694
5695        $sqlqm = "SELECT 1
5696                    FROM {{$tablename}} t
5697                    WHERE t.name = 'a'";
5698        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 1));
5699        $this->assertCount(1, $records);
5700
5701        $sqlqm = "SELECT 'constant'
5702                    FROM {{$tablename}} t
5703                    WHERE t.name = 'a'";
5704        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 8));
5705        $this->assertCount(1, $records);
5706
5707        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'a', 'content'=>'one')));
5708        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'b', 'content'=>'two')));
5709        $this->assertNotEmpty($DB->insert_record($tablename, array('name' => 'c', 'content'=>'three')));
5710
5711        $sqlqm = "SELECT t.name, COUNT(DISTINCT t2.id) AS count, 'Test' AS teststring
5712                    FROM {{$tablename}} t
5713                    LEFT JOIN (
5714                        SELECT t.id, t.name
5715                        FROM {{$tablename}} t
5716                    ) t2 ON t2.name = t.name
5717                    GROUP BY t.name
5718                    ORDER BY t.name ASC";
5719        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm));
5720        $this->assertCount(6, $records);         // a,b,c,d,e,f.
5721        $this->assertEquals(2, reset($records)->count);  // a has 2 records now.
5722        $this->assertEquals(1, end($records)->count);    // f has 1 record still.
5723
5724        $this->assertNotEmpty($records = $DB->get_records_sql($sqlqm, null, 0, 2));
5725        $this->assertCount(2, $records);
5726        $this->assertEquals(2, reset($records)->count);
5727        $this->assertEquals(2, end($records)->count);
5728    }
5729
5730    /**
5731     * Test debugging messages about invalid limit number values.
5732     */
5733    public function test_invalid_limits_debugging() {
5734        $DB = $this->tdb;
5735        $dbman = $DB->get_manager();
5736
5737        // Setup test data.
5738        $table = $this->get_test_table();
5739        $tablename = $table->getName();
5740        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5741        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5742        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5743        $dbman->create_table($table);
5744        $DB->insert_record($tablename, array('course' => '1'));
5745
5746        // Verify that get_records_sql throws debug notices with invalid limit params.
5747        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 'invalid');
5748        $this->assertDebuggingCalled("Non-numeric limitfrom parameter detected: 'invalid', did you pass the correct arguments?");
5749
5750        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, 'invalid');
5751        $this->assertDebuggingCalled("Non-numeric limitnum parameter detected: 'invalid', did you pass the correct arguments?");
5752
5753        // Verify that get_recordset_sql throws debug notices with invalid limit params.
5754        $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}}", null, 'invalid');
5755        $this->assertDebuggingCalled("Non-numeric limitfrom parameter detected: 'invalid', did you pass the correct arguments?");
5756        $rs->close();
5757
5758        $rs = $DB->get_recordset_sql("SELECT * FROM {{$tablename}}", null, 1, 'invalid');
5759        $this->assertDebuggingCalled("Non-numeric limitnum parameter detected: 'invalid', did you pass the correct arguments?");
5760        $rs->close();
5761
5762        // Verify that some edge cases do no create debugging messages.
5763        // String form of integer values.
5764        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, '1');
5765        $this->assertDebuggingNotCalled();
5766        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, '2');
5767        $this->assertDebuggingNotCalled();
5768        // Empty strings.
5769        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, '');
5770        $this->assertDebuggingNotCalled();
5771        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, '');
5772        $this->assertDebuggingNotCalled();
5773        // Null values.
5774        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, null);
5775        $this->assertDebuggingNotCalled();
5776        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, null);
5777        $this->assertDebuggingNotCalled();
5778
5779        // Verify that empty arrays DO create debugging mesages.
5780        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, array());
5781        $this->assertDebuggingCalled("Non-numeric limitfrom parameter detected: array (\n), did you pass the correct arguments?");
5782        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, array());
5783        $this->assertDebuggingCalled("Non-numeric limitnum parameter detected: array (\n), did you pass the correct arguments?");
5784
5785        // Verify Negative number handling:
5786        // -1 is explicitly treated as 0 for historical reasons.
5787        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, -1);
5788        $this->assertDebuggingNotCalled();
5789        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, -1);
5790        $this->assertDebuggingNotCalled();
5791        // Any other negative values should throw debugging messages.
5792        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, -2);
5793        $this->assertDebuggingCalled("Negative limitfrom parameter detected: -2, did you pass the correct arguments?");
5794        $DB->get_records_sql("SELECT * FROM {{$tablename}}", null, 1, -2);
5795        $this->assertDebuggingCalled("Negative limitnum parameter detected: -2, did you pass the correct arguments?");
5796    }
5797
5798    public function test_queries_counter() {
5799
5800        $DB = $this->tdb;
5801        $dbman = $this->tdb->get_manager();
5802
5803        // Test database.
5804        $table = $this->get_test_table();
5805        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5806        $table->add_field('fieldvalue', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
5807        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5808
5809        $dbman->create_table($table);
5810        $tablename = $table->getName();
5811
5812        // Initial counters values.
5813        $initreads = $DB->perf_get_reads();
5814        $initwrites = $DB->perf_get_writes();
5815        $previousqueriestime = $DB->perf_get_queries_time();
5816
5817        // Selects counts as reads.
5818
5819        // The get_records_sql() method generates only 1 db query.
5820        $whatever = $DB->get_records_sql("SELECT * FROM {{$tablename}}");
5821        $this->assertEquals($initreads + 1, $DB->perf_get_reads());
5822
5823        // The get_records() method generates 2 queries the first time is called
5824        // as it is fetching the table structure.
5825        $whatever = $DB->get_records($tablename, array('id' => '1'));
5826        $this->assertEquals($initreads + 3, $DB->perf_get_reads());
5827        $this->assertEquals($initwrites, $DB->perf_get_writes());
5828
5829        // The elapsed time is counted.
5830        $lastqueriestime = $DB->perf_get_queries_time();
5831        $this->assertGreaterThanOrEqual($previousqueriestime, $lastqueriestime);
5832        $previousqueriestime = $lastqueriestime;
5833
5834        // Only 1 now, it already fetched the table columns.
5835        $whatever = $DB->get_records($tablename);
5836        $this->assertEquals($initreads + 4, $DB->perf_get_reads());
5837
5838        // And only 1 more from now.
5839        $whatever = $DB->get_records($tablename);
5840        $this->assertEquals($initreads + 5, $DB->perf_get_reads());
5841
5842        // Inserts counts as writes.
5843
5844        $rec1 = new stdClass();
5845        $rec1->fieldvalue = 11;
5846        $rec1->id = $DB->insert_record($tablename, $rec1);
5847        $this->assertEquals($initwrites + 1, $DB->perf_get_writes());
5848        $this->assertEquals($initreads + 5, $DB->perf_get_reads());
5849
5850        // The elapsed time is counted.
5851        $lastqueriestime = $DB->perf_get_queries_time();
5852        $this->assertGreaterThanOrEqual($previousqueriestime, $lastqueriestime);
5853        $previousqueriestime = $lastqueriestime;
5854
5855        $rec2 = new stdClass();
5856        $rec2->fieldvalue = 22;
5857        $rec2->id = $DB->insert_record($tablename, $rec2);
5858        $this->assertEquals($initwrites + 2, $DB->perf_get_writes());
5859
5860        // Updates counts as writes.
5861
5862        $rec1->fieldvalue = 111;
5863        $DB->update_record($tablename, $rec1);
5864        $this->assertEquals($initwrites + 3, $DB->perf_get_writes());
5865        $this->assertEquals($initreads + 5, $DB->perf_get_reads());
5866
5867        // The elapsed time is counted.
5868        $lastqueriestime = $DB->perf_get_queries_time();
5869        $this->assertGreaterThanOrEqual($previousqueriestime, $lastqueriestime);
5870        $previousqueriestime = $lastqueriestime;
5871
5872        // Sum of them.
5873        $totaldbqueries = $DB->perf_get_reads() + $DB->perf_get_writes();
5874        $this->assertEquals($totaldbqueries, $DB->perf_get_queries());
5875    }
5876
5877    public function test_sql_intersect() {
5878        $DB = $this->tdb;
5879        $dbman = $this->tdb->get_manager();
5880
5881        $tables = array();
5882        for ($i = 0; $i < 3; $i++) {
5883            $table = $this->get_test_table('i'.$i);
5884            $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5885            $table->add_field('ival', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
5886            $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, '0');
5887            $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5888            $dbman->create_table($table);
5889            $tables[$i] = $table;
5890        }
5891        $DB->insert_record($tables[0]->getName(), array('ival' => 1, 'name' => 'One'), false);
5892        $DB->insert_record($tables[0]->getName(), array('ival' => 2, 'name' => 'Two'), false);
5893        $DB->insert_record($tables[0]->getName(), array('ival' => 3, 'name' => 'Three'), false);
5894        $DB->insert_record($tables[0]->getName(), array('ival' => 4, 'name' => 'Four'), false);
5895
5896        $DB->insert_record($tables[1]->getName(), array('ival' => 1, 'name' => 'One'), false);
5897        $DB->insert_record($tables[1]->getName(), array('ival' => 2, 'name' => 'Two'), false);
5898        $DB->insert_record($tables[1]->getName(), array('ival' => 3, 'name' => 'Three'), false);
5899
5900        $DB->insert_record($tables[2]->getName(), array('ival' => 1, 'name' => 'One'), false);
5901        $DB->insert_record($tables[2]->getName(), array('ival' => 2, 'name' => 'Two'), false);
5902        $DB->insert_record($tables[2]->getName(), array('ival' => 5, 'name' => 'Five'), false);
5903
5904        // Intersection on the int column.
5905        $params = array('excludename' => 'Two');
5906        $sql1 = 'SELECT ival FROM {'.$tables[0]->getName().'}';
5907        $sql2 = 'SELECT ival FROM {'.$tables[1]->getName().'} WHERE name <> :excludename';
5908        $sql3 = 'SELECT ival FROM {'.$tables[2]->getName().'}';
5909
5910        $sql = $DB->sql_intersect(array($sql1), 'ival') . ' ORDER BY ival';
5911        $this->assertEquals(array(1, 2, 3, 4), $DB->get_fieldset_sql($sql, $params));
5912
5913        $sql = $DB->sql_intersect(array($sql1, $sql2), 'ival') . ' ORDER BY ival';
5914        $this->assertEquals(array(1, 3), $DB->get_fieldset_sql($sql, $params));
5915
5916        $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'ival') . ' ORDER BY ival';
5917        $this->assertEquals(array(1),
5918            $DB->get_fieldset_sql($sql, $params));
5919
5920        // Intersection on the char column.
5921        $params = array('excludeival' => 2);
5922        $sql1 = 'SELECT name FROM {'.$tables[0]->getName().'}';
5923        $sql2 = 'SELECT name FROM {'.$tables[1]->getName().'} WHERE ival <> :excludeival';
5924        $sql3 = 'SELECT name FROM {'.$tables[2]->getName().'}';
5925
5926        $sql = $DB->sql_intersect(array($sql1), 'name') . ' ORDER BY name';
5927        $this->assertEquals(array('Four', 'One', 'Three', 'Two'), $DB->get_fieldset_sql($sql, $params));
5928
5929        $sql = $DB->sql_intersect(array($sql1, $sql2), 'name') . ' ORDER BY name';
5930        $this->assertEquals(array('One', 'Three'), $DB->get_fieldset_sql($sql, $params));
5931
5932        $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'name') . ' ORDER BY name';
5933        $this->assertEquals(array('One'), $DB->get_fieldset_sql($sql, $params));
5934
5935        // Intersection on the several columns.
5936        $params = array('excludename' => 'Two');
5937        $sql1 = 'SELECT ival, name FROM {'.$tables[0]->getName().'}';
5938        $sql2 = 'SELECT ival, name FROM {'.$tables[1]->getName().'} WHERE name <> :excludename';
5939        $sql3 = 'SELECT ival, name FROM {'.$tables[2]->getName().'}';
5940
5941        $sql = $DB->sql_intersect(array($sql1), 'ival, name') . ' ORDER BY ival';
5942        $this->assertEquals(array(1 => 'One', 2 => 'Two', 3 => 'Three', 4 => 'Four'),
5943            $DB->get_records_sql_menu($sql, $params));
5944
5945        $sql = $DB->sql_intersect(array($sql1, $sql2), 'ival, name') . ' ORDER BY ival';
5946        $this->assertEquals(array(1 => 'One', 3 => 'Three'),
5947            $DB->get_records_sql_menu($sql, $params));
5948
5949        $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'ival, name') . ' ORDER BY ival';
5950        $this->assertEquals(array(1 => 'One'),
5951            $DB->get_records_sql_menu($sql, $params));
5952
5953        // Drop temporary tables.
5954        foreach ($tables as $table) {
5955            $dbman->drop_table($table);
5956        }
5957    }
5958
5959    /**
5960     * Test that the database has full utf8 support (4 bytes).
5961     */
5962    public function test_four_byte_character_insertion() {
5963        $DB = $this->tdb;
5964
5965        if ($DB->get_dbfamily() === 'mysql' && strpos($DB->get_dbcollation(), 'utf8_') === 0) {
5966            $this->markTestSkipped($DB->get_name() .
5967                    ' does not support 4 byte characters with only a utf8 collation.
5968                    Please change to utf8mb4 for full utf8 support.');
5969        }
5970
5971        $dbman = $this->tdb->get_manager();
5972
5973        $table = $this->get_test_table();
5974        $tablename = $table->getName();
5975
5976        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
5977        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
5978        $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
5979        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
5980        $dbman->create_table($table);
5981
5982        $data = array(
5983            'name' => 'Name with a four byte character ��る',
5984            'content' => 'Content with a four byte emoji �� memo.'
5985        );
5986
5987        $insertid = $DB->insert_record($tablename, $data);
5988        $result = $DB->get_record($tablename, array('id' => $insertid));
5989        $this->assertEquals($data['name'], $result->name);
5990        $this->assertEquals($data['content'], $result->content);
5991
5992        $dbman->drop_table($table);
5993    }
5994}
5995
5996/**
5997 * This class is not a proper subclass of moodle_database. It is
5998 * intended to be used only in unit tests, in order to gain access to the
5999 * protected methods of moodle_database, and unit test them.
6000 */
6001class moodle_database_for_testing extends moodle_database {
6002    protected $prefix = 'mdl_';
6003
6004    public function public_fix_table_names($sql) {
6005        return $this->fix_table_names($sql);
6006    }
6007
6008    public function driver_installed() {}
6009    public function get_dbfamily() {}
6010    protected function get_dbtype() {}
6011    protected function get_dblibrary() {}
6012    public function get_name() {}
6013    public function get_configuration_help() {}
6014    public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {}
6015    public function get_server_info() {}
6016    protected function allowed_param_types() {}
6017    public function get_last_error() {}
6018    public function get_tables($usecache=true) {}
6019    public function get_indexes($table) {}
6020    protected function fetch_columns(string $table): array {
6021        return [];
6022    }
6023    protected function normalise_value($column, $value) {}
6024    public function set_debug($state) {}
6025    public function get_debug() {}
6026    public function change_database_structure($sql, $tablenames = null) {}
6027    public function execute($sql, array $params=null) {}
6028    public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {}
6029    public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {}
6030    public function get_fieldset_sql($sql, array $params=null) {}
6031    public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {}
6032    public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {}
6033    public function import_record($table, $dataobject) {}
6034    public function update_record_raw($table, $params, $bulk=false) {}
6035    public function update_record($table, $dataobject, $bulk=false) {}
6036    public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {}
6037    public function delete_records_select($table, $select, array $params=null) {}
6038    public function sql_concat() {}
6039    public function sql_concat_join($separator="' '", $elements=array()) {}
6040    public function sql_group_concat(string $field, string $separator = ', ', string $sort = ''): string {
6041        return '';
6042    }
6043    public function sql_substr($expr, $start, $length=false) {}
6044    public function begin_transaction() {}
6045    public function commit_transaction() {}
6046    public function rollback_transaction() {}
6047}
6048
6049
6050/**
6051 * Dumb test class with toString() returning 1.
6052 */
6053class dml_test_object_one {
6054    public function __toString() {
6055        return 1;
6056    }
6057}
6058