Files
moodle/lib/dml/tests/pgsql_native_moodle_database_test.php
T
Ruslan Kabalin 921f8aee81 MDL-70055 pgsql DML: Support large number of SQL-IN parameters.
This patch adds a solution for Postgres that allows overriding IN
statement limit of 16bit (65535 values) by wrapping items in VALUES
list. This only takes place in number of items exceeds 65535, otherwise
parent class get_in_or_equal() function is used.
2020-12-24 14:40:22 +00:00

362 lines
14 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test specific features of the Postgres dml.
*
* @package core
* @category test
* @copyright 2020 Ruslan Kabalin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Test specific features of the Postgres dml.
*
* @package core
* @category test
* @copyright 2020 Ruslan Kabalin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class pgsql_native_moodle_database_testcase extends advanced_testcase {
/**
* Setup before class.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->libdir.'/dml/pgsql_native_moodle_database.php');
}
/**
* Set up.
*/
public function setUp(): void {
global $DB;
parent::setUp();
// Skip tests if not using Postgres.
if (!($DB instanceof pgsql_native_moodle_database)) {
$this->markTestSkipped('Postgres-only test');
}
}
/**
* Get a xmldb_table object for testing, deleting any existing table
* of the same name, for example if one was left over from a previous test
* run that crashed.
*
* @param string $suffix table name suffix, use if you need more test tables
* @return xmldb_table the table object.
*/
private function get_test_table($suffix = ''): xmldb_table {
$tablename = "test_table";
if ($suffix !== '') {
$tablename .= $suffix;
}
$table = new xmldb_table($tablename);
$table->setComment("This is a test'n drop table. You can drop it safely");
return $table;
}
/**
* Find out the current index used for unique SQL_PARAMS_NAMED.
*
* @return int
*/
private function get_current_index(): int {
global $DB;
$reflector = new ReflectionClass($DB);
$property = $reflector->getProperty('inorequaluniqueindex');
$property->setAccessible(true);
return (int) $property->getValue($DB);
}
public function test_get_in_or_equal_below_limit(): void {
global $DB;
// Just less than 65535 values, expect fallback to parent method.
$invalues = range(1, 65533);
list($usql, $params) = $DB->get_in_or_equal($invalues);
$this->assertSame('IN ('.implode(',', array_fill(0, count($invalues), '?')).')', $usql);
$this->assertEquals(count($invalues), count($params));
foreach ($params as $key => $value) {
$this->assertSame($invalues[$key], $value);
}
}
public function test_get_in_or_equal_single_array_value(): void {
global $DB;
// Single value (in an array), expect fallback to parent method.
$invalues = array('value1');
list($usql, $params) = $DB->get_in_or_equal($invalues);
$this->assertEquals("= ?", $usql);
$this->assertCount(1, $params);
$this->assertEquals($invalues[0], $params[0]);
}
public function test_get_in_or_equal_single_scalar_value(): void {
global $DB;
// Single value (scalar), expect fallback to parent method.
$invalue = 'value1';
list($usql, $params) = $DB->get_in_or_equal($invalue);
$this->assertEquals("= ?", $usql);
$this->assertCount(1, $params);
$this->assertEquals($invalue, $params[0]);
}
public function test_get_in_or_equal_multiple_int_value(): void {
global $DB;
// 65535 values, int.
$invalues = range(1, 65535);
list($usql, $params) = $DB->get_in_or_equal($invalues);
$this->assertSame('IN (VALUES ('.implode('),(', array_fill(0, count($invalues), '?::bigint')).'))', $usql);
$this->assertEquals($params, $invalues);
}
public function test_get_in_or_equal_multiple_int_value_not_equal(): void {
global $DB;
// 65535 values, not equal, int.
$invalues = range(1, 65535);
list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_QM, 'param', false);
$this->assertSame('NOT IN (VALUES ('.implode('),(', array_fill(0, count($invalues), '?::bigint')).'))', $usql);
$this->assertEquals($params, $invalues);
}
public function test_get_in_or_equal_named_int_value_default_name(): void {
global $DB;
// 65535 values, int, SQL_PARAMS_NAMED.
$index = $this->get_current_index();
$invalues = range(1, 65535);
list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_NAMED);
$regex = '/^'.
preg_quote('IN (VALUES (:param'.$index.'::bigint),(:param'.++$index.'::bigint),(:param'.++$index.'::bigint)').'/';
$this->assertRegExp($regex, $usql);
foreach ($params as $value) {
$this->assertEquals(current($invalues), $value);
next($invalues);
}
}
public function test_get_in_or_equal_named_int_value_specified_name(): void {
global $DB;
// 65535 values, int, SQL_PARAMS_NAMED, define param name.
$index = $this->get_current_index();
$invalues = range(1, 65535);
list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_NAMED, 'ppp');
// We are in same DBI instance, expect uniqie param indexes.
$regex = '/^'.
preg_quote('IN (VALUES (:ppp'.$index.'::bigint),(:ppp'.++$index.'::bigint),(:ppp'.++$index.'::bigint)').'/';
$this->assertRegExp($regex, $usql);
foreach ($params as $value) {
$this->assertEquals(current($invalues), $value);
next($invalues);
}
}
public function test_get_in_or_equal_named_scalar_value_specified_name(): void {
global $DB;
// 65535 values, string.
$invalues = array_fill(1, 65535, 'abc');
list($usql, $params) = $DB->get_in_or_equal($invalues);
$this->assertRegExp('/^' . preg_quote('IN (VALUES (?::text),(?::text),(?::text)') . '/', $usql);
foreach ($params as $value) {
$this->assertEquals(current($invalues), $value);
next($invalues);
}
}
public function test_get_in_or_equal_query_use(): void {
global $DB;
$this->resetAfterTest();
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
$table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$rec1 = ['course' => 3, 'content' => 'hello', 'name' => 'xyz'];
$DB->insert_record($tablename, $rec1);
$rec2 = ['course' => 3, 'content' => 'world', 'name' => 'abc'];
$DB->insert_record($tablename, $rec2);
$rec3 = ['course' => 5, 'content' => 'hello', 'name' => 'xyz'];
$DB->insert_record($tablename, $rec3);
$rec4 = ['course' => 6, 'content' => 'universe'];
$DB->insert_record($tablename, $rec4);
$currentcount = $DB->count_records($tablename);
// Getting all 4.
$values = range(1, 65535);
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE id $insql
ORDER BY id ASC";
$this->assertCount($currentcount, $DB->get_records_sql($sql, $inparams));
// Getting 'hello' records (text).
$values = array_fill(1, 65535, 'hello');
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE content $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([1, 3], array_keys($result));
// Getting NOT 'hello' records (text).
$values = array_fill(1, 65535, 'hello');
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
$sql = "SELECT *
FROM {{$tablename}}
WHERE content $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([2, 4], array_keys($result));
// Getting 'xyz' records (char and NULL mix).
$values = array_fill(1, 65535, 'xyz');
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE name $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([1, 3], array_keys($result));
// Getting NOT 'xyz' records (char and NULL mix).
$values = array_fill(1, 65535, 'xyz');
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
$sql = "SELECT *
FROM {{$tablename}}
WHERE name $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
// NULL will not be in result.
$this->assertCount(1, $result);
$this->assertEquals([2], array_keys($result));
// Getting numbeic records.
$values = array_fill(1, 65535, 3);
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE course $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([1, 2], array_keys($result));
// Getting numbeic records with NOT condition.
$values = array_fill(1, 65535, 3);
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
$sql = "SELECT *
FROM {{$tablename}}
WHERE course $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([3, 4], array_keys($result));
}
public function test_get_in_or_equal_big_table_query(): void {
global $DB;
$this->resetAfterTest();
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
$table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
$table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
$table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$dbman->create_table($table);
$record = new stdClass();
$record->course = 1;
$record->oneint = null;
$record->onenum = 1.0;
$record->onechar = 'a';
$record->onetext = 'aaa';
$records = [];
for ($i = 1; $i <= 65535; $i++) {
$rec = clone($record);
$rec->oneint = $i;
$records[$i] = $rec;
}
// Populate table with 65535 records.
$DB->insert_records($tablename, $records);
// And one more record.
$record->oneint = -1;
$DB->insert_record($tablename, $record);
// Check we can fetch all.
$values = range(1, 65535);
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE oneint $insql
ORDER BY id ASC";
$stored = $DB->get_records_sql($sql, $inparams);
// Check we got correct set of records.
$this->assertCount(65535, $stored);
$oneint = array_column($stored, 'oneint');
$this->assertEquals($values, $oneint);
// Check we can fetch all, SQL_PARAMS_NAMED.
$values = range(1, 65535);
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED);
$sql = "SELECT *
FROM {{$tablename}}
WHERE oneint $insql
ORDER BY id ASC";
$stored = $DB->get_records_sql($sql, $inparams);
// Check we got correct set of records.
$this->assertCount(65535, $stored);
$oneint = array_column($stored, 'oneint');
$this->assertEquals($values, $oneint);
// Check we can fetch one using NOT IN.
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
$sql = "SELECT *
FROM {{$tablename}}
WHERE oneint $insql
ORDER BY id ASC";
$stored = $DB->get_records_sql($sql, $inparams);
// Check we got correct set of records.
$this->assertCount(1, $stored);
$oneint = array_column($stored, 'oneint');
$this->assertEquals([-1], $oneint);
}
}