Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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