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
 * Test specific features of the Postgres dml.
19
 *
20
 * @package core
21
 * @category test
22
 * @copyright 2020 Ruslan Kabalin
23
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
namespace core;
27
 
28
use stdClass, ReflectionClass;
29
use moodle_database, pgsql_native_moodle_database;
30
use xmldb_table;
31
use moodle_exception;
1441 ariadna 32
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
1 efrain 33
 
34
/**
35
 * Test specific features of the Postgres dml.
36
 *
37
 * @package core
38
 * @category test
39
 * @copyright 2020 Ruslan Kabalin
40
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41
 * @covers  \pgsql_native_moodle_database
42
 */
1441 ariadna 43
final class pgsql_native_moodle_database_test extends \advanced_testcase {
1 efrain 44
 
45
    /**
46
     * Setup before class.
47
     */
48
    public static function setUpBeforeClass(): void {
49
        global $CFG;
50
        require_once($CFG->libdir.'/dml/pgsql_native_moodle_database.php');
1441 ariadna 51
        parent::setUpBeforeClass();
1 efrain 52
    }
53
 
54
    /**
55
     * Set up.
56
     */
57
    public function setUp(): void {
58
        global $DB;
59
        parent::setUp();
60
        // Skip tests if not using Postgres.
61
        if (!($DB instanceof pgsql_native_moodle_database)) {
62
            $this->markTestSkipped('Postgres-only test');
63
        }
64
    }
65
 
66
    /**
67
     * Get a xmldb_table object for testing, deleting any existing table
68
     * of the same name, for example if one was left over from a previous test
69
     * run that crashed.
70
     *
71
     * @param string $suffix table name suffix, use if you need more test tables
72
     * @return xmldb_table the table object.
73
     */
74
    private function get_test_table($suffix = ''): xmldb_table {
75
        $tablename = "test_table";
76
        if ($suffix !== '') {
77
            $tablename .= $suffix;
78
        }
79
 
80
        $table = new xmldb_table($tablename);
81
        $table->setComment("This is a test'n drop table. You can drop it safely");
82
        return $table;
83
    }
84
 
85
    /**
86
     * Find out the current index used for unique SQL_PARAMS_NAMED.
87
     *
88
     * @return int
89
     */
90
    private function get_current_index(): int {
91
        global $DB;
92
        $reflector = new ReflectionClass($DB);
93
        $property = $reflector->getProperty('inorequaluniqueindex');
94
        return (int) $property->getValue($DB);
95
    }
96
 
97
    public function test_get_in_or_equal_below_limit(): void {
98
        global $DB;
99
        // Just less than 65535 values, expect fallback to parent method.
100
        $invalues = range(1, 65533);
101
        list($usql, $params) = $DB->get_in_or_equal($invalues);
102
        $this->assertSame('IN ('.implode(',', array_fill(0, count($invalues), '?')).')', $usql);
103
        $this->assertEquals(count($invalues), count($params));
104
        foreach ($params as $key => $value) {
105
            $this->assertSame($invalues[$key], $value);
106
        }
107
    }
108
 
109
    public function test_get_in_or_equal_single_array_value(): void {
110
        global $DB;
111
        // Single value (in an array), expect fallback to parent method.
112
        $invalues = array('value1');
113
        list($usql, $params) = $DB->get_in_or_equal($invalues);
114
        $this->assertEquals("= ?", $usql);
115
        $this->assertCount(1, $params);
116
        $this->assertEquals($invalues[0], $params[0]);
117
    }
118
 
119
    public function test_get_in_or_equal_single_scalar_value(): void {
120
        global $DB;
121
        // Single value (scalar), expect fallback to parent method.
122
        $invalue = 'value1';
123
        list($usql, $params) = $DB->get_in_or_equal($invalue);
124
        $this->assertEquals("= ?", $usql);
125
        $this->assertCount(1, $params);
126
        $this->assertEquals($invalue, $params[0]);
127
    }
128
 
129
    public function test_get_in_or_equal_multiple_int_value(): void {
130
        global $DB;
131
        // 65535 values, int.
132
        $invalues = range(1, 65535);
133
        list($usql, $params) = $DB->get_in_or_equal($invalues);
134
        $this->assertSame('IN (VALUES ('.implode('),(', array_fill(0, count($invalues), '?::bigint')).'))', $usql);
135
        $this->assertEquals($params, $invalues);
136
    }
137
 
138
    public function test_get_in_or_equal_multiple_int_value_not_equal(): void {
139
        global $DB;
140
        // 65535 values, not equal, int.
141
        $invalues = range(1, 65535);
142
        list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_QM, 'param', false);
143
        $this->assertSame('NOT IN (VALUES ('.implode('),(', array_fill(0, count($invalues), '?::bigint')).'))', $usql);
144
        $this->assertEquals($params, $invalues);
145
    }
146
 
147
    public function test_get_in_or_equal_named_int_value_default_name(): void {
148
        global $DB;
149
        // 65535 values, int, SQL_PARAMS_NAMED.
150
        $index = $this->get_current_index();
151
        $invalues = range(1, 65535);
152
        list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_NAMED);
153
        $regex = '/^'.
154
            preg_quote('IN (VALUES (:param'.$index.'::bigint),(:param'.++$index.'::bigint),(:param'.++$index.'::bigint)').'/';
155
        $this->assertMatchesRegularExpression($regex, $usql);
156
        foreach ($params as $value) {
157
            $this->assertEquals(current($invalues), $value);
158
            next($invalues);
159
        }
160
    }
161
 
162
    public function test_get_in_or_equal_named_int_value_specified_name(): void {
163
        global $DB;
164
        // 65535 values, int, SQL_PARAMS_NAMED, define param name.
165
        $index = $this->get_current_index();
166
        $invalues = range(1, 65535);
167
        list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_NAMED, 'ppp');
168
        // We are in same DBI instance, expect uniqie param indexes.
169
        $regex = '/^'.
170
            preg_quote('IN (VALUES (:ppp'.$index.'::bigint),(:ppp'.++$index.'::bigint),(:ppp'.++$index.'::bigint)').'/';
171
        $this->assertMatchesRegularExpression($regex, $usql);
172
        foreach ($params as $value) {
173
            $this->assertEquals(current($invalues), $value);
174
            next($invalues);
175
        }
176
    }
177
 
178
    public function test_get_in_or_equal_named_scalar_value_specified_name(): void {
179
        global $DB;
180
        // 65535 values, string.
181
        $invalues = array_fill(1, 65535, 'abc');
182
        list($usql, $params) = $DB->get_in_or_equal($invalues);
183
        $this->assertMatchesRegularExpression('/^' . preg_quote('IN (VALUES (?::text),(?::text),(?::text)') . '/', $usql);
184
        foreach ($params as $value) {
185
            $this->assertEquals(current($invalues), $value);
186
            next($invalues);
187
        }
188
    }
189
 
190
    public function test_get_in_or_equal_query_use(): void {
191
        global $DB;
192
        $this->resetAfterTest();
193
        $dbman = $DB->get_manager();
194
        $table = $this->get_test_table();
195
        $tablename = $table->getName();
196
 
197
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
198
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
199
        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
200
        $table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
201
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
202
        $dbman->create_table($table);
203
 
204
        $rec1 = ['course' => 3, 'content' => 'hello', 'name' => 'xyz'];
205
        $DB->insert_record($tablename, $rec1);
206
        $rec2 = ['course' => 3, 'content' => 'world', 'name' => 'abc'];
207
        $DB->insert_record($tablename, $rec2);
208
        $rec3 = ['course' => 5, 'content' => 'hello', 'name' => 'xyz'];
209
        $DB->insert_record($tablename, $rec3);
210
        $rec4 = ['course' => 6, 'content' => 'universe'];
211
        $DB->insert_record($tablename, $rec4);
212
 
213
        $currentcount = $DB->count_records($tablename);
214
 
215
        // Getting all 4.
216
        $values = range(1, 65535);
217
        list($insql, $inparams) = $DB->get_in_or_equal($values);
218
        $sql = "SELECT *
219
                  FROM {{$tablename}}
220
                 WHERE id $insql
221
              ORDER BY id ASC";
222
        $this->assertCount($currentcount, $DB->get_records_sql($sql, $inparams));
223
 
224
        // Getting 'hello' records (text).
225
        $values = array_fill(1, 65535, 'hello');
226
        list($insql, $inparams) = $DB->get_in_or_equal($values);
227
        $sql = "SELECT *
228
                  FROM {{$tablename}}
229
                 WHERE content $insql
230
              ORDER BY id ASC";
231
        $result = $DB->get_records_sql($sql, $inparams);
232
        $this->assertCount(2, $result);
233
        $this->assertEquals([1, 3], array_keys($result));
234
 
235
        // Getting NOT 'hello' records (text).
236
        $values = array_fill(1, 65535, 'hello');
237
        list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
238
        $sql = "SELECT *
239
                  FROM {{$tablename}}
240
                 WHERE content $insql
241
              ORDER BY id ASC";
242
        $result = $DB->get_records_sql($sql, $inparams);
243
        $this->assertCount(2, $result);
244
        $this->assertEquals([2, 4], array_keys($result));
245
 
246
        // Getting 'xyz' records (char and NULL mix).
247
        $values = array_fill(1, 65535, 'xyz');
248
        list($insql, $inparams) = $DB->get_in_or_equal($values);
249
        $sql = "SELECT *
250
                  FROM {{$tablename}}
251
                 WHERE name $insql
252
              ORDER BY id ASC";
253
        $result = $DB->get_records_sql($sql, $inparams);
254
        $this->assertCount(2, $result);
255
        $this->assertEquals([1, 3], array_keys($result));
256
 
257
        // Getting NOT 'xyz' records (char and NULL mix).
258
        $values = array_fill(1, 65535, 'xyz');
259
        list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
260
        $sql = "SELECT *
261
                  FROM {{$tablename}}
262
                 WHERE name $insql
263
              ORDER BY id ASC";
264
        $result = $DB->get_records_sql($sql, $inparams);
265
        // NULL will not be in result.
266
        $this->assertCount(1, $result);
267
        $this->assertEquals([2], array_keys($result));
268
 
269
        // Getting numbeic records.
270
        $values = array_fill(1, 65535, 3);
271
        list($insql, $inparams) = $DB->get_in_or_equal($values);
272
        $sql = "SELECT *
273
                  FROM {{$tablename}}
274
                 WHERE course $insql
275
              ORDER BY id ASC";
276
        $result = $DB->get_records_sql($sql, $inparams);
277
        $this->assertCount(2, $result);
278
        $this->assertEquals([1, 2], array_keys($result));
279
 
280
        // Getting numbeic records with NOT condition.
281
        $values = array_fill(1, 65535, 3);
282
        list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
283
        $sql = "SELECT *
284
                  FROM {{$tablename}}
285
                 WHERE course $insql
286
              ORDER BY id ASC";
287
        $result = $DB->get_records_sql($sql, $inparams);
288
        $this->assertCount(2, $result);
289
        $this->assertEquals([3, 4], array_keys($result));
290
    }
291
 
292
    public function test_get_in_or_equal_big_table_query(): void {
293
        global $DB;
294
        $this->resetAfterTest();
295
        $dbman = $DB->get_manager();
296
 
297
        $table = $this->get_test_table();
298
        $tablename = $table->getName();
299
 
300
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
301
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
302
        $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
303
        $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
304
        $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
305
        $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
306
        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
307
        $dbman->create_table($table);
308
 
309
        $record = new stdClass();
310
        $record->course = 1;
311
        $record->oneint = null;
312
        $record->onenum = 1.0;
313
        $record->onechar = 'a';
314
        $record->onetext = 'aaa';
315
 
316
        $records = [];
317
        for ($i = 1; $i <= 65535; $i++) {
318
            $rec = clone($record);
319
            $rec->oneint = $i;
320
            $records[$i] = $rec;
321
        }
322
        // Populate table with 65535 records.
323
        $DB->insert_records($tablename, $records);
324
        // And one more record.
325
        $record->oneint = -1;
326
        $DB->insert_record($tablename, $record);
327
 
328
        // Check we can fetch all.
329
        $values = range(1, 65535);
330
        list($insql, $inparams) = $DB->get_in_or_equal($values);
331
        $sql = "SELECT *
332
                  FROM {{$tablename}}
333
                 WHERE oneint $insql
334
              ORDER BY id ASC";
335
        $stored = $DB->get_records_sql($sql, $inparams);
336
 
337
        // Check we got correct set of records.
338
        $this->assertCount(65535, $stored);
339
        $oneint = array_column($stored, 'oneint');
340
        $this->assertEquals($values, $oneint);
341
 
342
        // Check we can fetch all, SQL_PARAMS_NAMED.
343
        $values = range(1, 65535);
344
        list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED);
345
        $sql = "SELECT *
346
                  FROM {{$tablename}}
347
                 WHERE oneint $insql
348
              ORDER BY id ASC";
349
        $stored = $DB->get_records_sql($sql, $inparams);
350
 
351
        // Check we got correct set of records.
352
        $this->assertCount(65535, $stored);
353
        $oneint = array_column($stored, 'oneint');
354
        $this->assertEquals($values, $oneint);
355
 
356
        // Check we can fetch one using NOT IN.
357
        list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
358
        $sql = "SELECT *
359
                  FROM {{$tablename}}
360
                 WHERE oneint $insql
361
              ORDER BY id ASC";
362
        $stored = $DB->get_records_sql($sql, $inparams);
363
 
364
        // Check we got correct set of records.
365
        $this->assertCount(1, $stored);
366
        $oneint = array_column($stored, 'oneint');
367
        $this->assertEquals([-1], $oneint);
368
    }
369
 
370
    /**
371
     * SSL connection helper.
372
     *
373
     * @param mixed $ssl
374
     * @return resource|PgSql\Connection
375
     * @throws moodle_exception
376
     */
377
    public function new_connection($ssl) {
378
        global $DB;
379
 
380
        // Open new connection.
381
        $cfg = $DB->export_dbconfig();
382
        if (!isset($cfg->dboptions)) {
383
            $cfg->dboptions = [];
384
        }
385
 
386
        $cfg->dboptions['ssl'] = $ssl;
387
 
388
        // Get a separate disposable db connection handle with guaranteed 'readonly' config.
389
        $db2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
390
        $db2->raw_connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
391
 
392
        $reflector = new ReflectionClass($db2);
393
        $rp = $reflector->getProperty('pgsql');
394
        return $rp->getValue($db2);
395
    }
396
 
397
    /**
398
     * Test SSL connection.
399
     */
1441 ariadna 400
    #[WithoutErrorHandler]
1 efrain 401
    public function test_ssl_connection(): void {
402
        $pgconnerr = 'pg_connect(): Unable to connect to PostgreSQL server:';
403
 
404
        try {
405
            $pgsql = $this->new_connection('require');
406
            // Either connect ...
407
            $this->assertNotNull($pgsql);
408
        } catch (moodle_exception $e) {
409
            // ... or fail with SSL not supported.
410
            $this->assertStringContainsString($pgconnerr, $e->debuginfo);
411
            $this->assertStringContainsString('server does not support SSL', $e->debuginfo);
412
            $this->markTestSkipped('Postgres server does not support SSL. Unable to complete the test.');
413
            return;
414
        }
415
 
416
        try {
417
            $pgsql = $this->new_connection('verify-full');
418
            // Either connect ...
419
            $this->assertNotNull($pgsql);
420
        } catch (moodle_exception $e) {
421
            // ... or fail with invalid cert.
422
            $this->assertStringContainsString($pgconnerr, $e->debuginfo);
423
            $this->assertStringContainsString('change sslmode to disable server certificate verification', $e->debuginfo);
424
        }
425
 
426
        $this->expectException(moodle_exception::class);
427
        $this->new_connection('invalid-mode');
428
    }
429
}