Proyectos de Subversion Moodle

Rev

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