From 22a9ced97118e43385130fc5eb199174161447d2 Mon Sep 17 00:00:00 2001 From: catch <catch56@gmail.com> Date: Thu, 20 Oct 2022 17:01:22 +0100 Subject: [PATCH] Issue #3312641 by nkoporec, Ratan Priya, Anchal_gupta, daffie, mglaman: Bring back temporary tables (Connection::queryTemporary()) --- .../SupportsTemporaryTablesInterface.php | 39 ++++++++++++++ core/misc/cspell/dictionary.txt | 2 + .../src/Driver/Database/mysql/Connection.php | 12 ++++- .../src/Kernel/mysql/TemporaryQueryTest.php | 39 ++++++++++++++ .../src/Driver/Database/pgsql/Connection.php | 12 ++++- .../src/Kernel/pgsql/TemporaryQueryTest.php | 39 ++++++++++++++ .../src/Driver/Database/sqlite/Connection.php | 19 ++++++- .../src/Kernel/sqlite/TemporaryQueryTest.php | 38 ++++++++++++++ .../Core/Database/TemporaryQueryTestBase.php | 51 +++++++++++++++++++ 9 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php create mode 100644 core/modules/mysql/tests/src/Kernel/mysql/TemporaryQueryTest.php create mode 100644 core/modules/pgsql/tests/src/Kernel/pgsql/TemporaryQueryTest.php create mode 100644 core/modules/sqlite/tests/src/Kernel/sqlite/TemporaryQueryTest.php create mode 100644 core/tests/Drupal/KernelTests/Core/Database/TemporaryQueryTestBase.php diff --git a/core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php b/core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php new file mode 100644 index 000000000000..2e6bd0fb06c5 --- /dev/null +++ b/core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\Core\Database; + +/** + * Adds support for temporary tables. + * + * @ingroup database + */ +interface SupportsTemporaryTablesInterface { + + /** + * Runs a SELECT query and stores its results in a temporary table. + * + * Use this as a substitute for ->query() when the results need to stored + * in a temporary table. Temporary tables exist for the duration of the page + * request. User-supplied arguments to the query should be passed in as + * separate parameters so that they can be properly escaped to avoid SQL + * injection attacks. + * + * Note that if you need to know how many results were returned, you should do + * a SELECT COUNT(*) on the temporary table afterwards. + * + * @param string $query + * A string containing a normal SELECT SQL query. + * @param array $args + * (optional) An array of values to substitute into the query at placeholder + * markers. + * @param array $options + * (optional) An associative array of options to control how the query is + * run. See the documentation for DatabaseConnection::defaultOptions() for + * details. + * + * @return string + * The name of the temporary table. + */ + public function queryTemporary($query, array $args = [], array $options = []); + +} diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt index 9921ab84d833..a5c4d82f3428 100644 --- a/core/misc/cspell/dictionary.txt +++ b/core/misc/cspell/dictionary.txt @@ -1024,6 +1024,7 @@ renderered renormalize reparenting reparsed +relpersistence replyto resave resaved @@ -1042,6 +1043,7 @@ revisionid revisioning revlog revpub +relname ribisi ritchie rolename diff --git a/core/modules/mysql/src/Driver/Database/mysql/Connection.php b/core/modules/mysql/src/Driver/Database/mysql/Connection.php index 910387eb5f6c..3e8437e8f9fa 100644 --- a/core/modules/mysql/src/Driver/Database/mysql/Connection.php +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php @@ -9,6 +9,7 @@ use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\DatabaseException; use Drupal\Core\Database\Connection as DatabaseConnection; +use Drupal\Core\Database\SupportsTemporaryTablesInterface; use Drupal\Core\Database\TransactionNoActiveException; /** @@ -19,7 +20,7 @@ /** * MySQL implementation of \Drupal\Core\Database\Connection. */ -class Connection extends DatabaseConnection { +class Connection extends DatabaseConnection implements SupportsTemporaryTablesInterface { /** * Error code for "Unknown database" error. @@ -222,6 +223,15 @@ public function queryRange($query, $from, $count, array $args = [], array $optio return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); } + /** + * {@inheritdoc} + */ + public function queryTemporary($query, array $args = [], array $options = []) { + $tablename = 'db_temporary_' . uniqid(); + $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options); + return $tablename; + } + public function driver() { return 'mysql'; } diff --git a/core/modules/mysql/tests/src/Kernel/mysql/TemporaryQueryTest.php b/core/modules/mysql/tests/src/Kernel/mysql/TemporaryQueryTest.php new file mode 100644 index 000000000000..484ccc75227e --- /dev/null +++ b/core/modules/mysql/tests/src/Kernel/mysql/TemporaryQueryTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\Tests\mysql\Kernel\mysql; + +use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase; + +/** + * Tests the temporary query functionality. + * + * @group Database + */ +class TemporaryQueryTest extends TemporaryQueryTestBase { + + /** + * Confirms that temporary tables work. + */ + public function testTemporaryQuery() { + parent::testTemporaryQuery(); + + $connection = $this->getConnection(); + + $table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []); + + // Assert that the table is indeed a temporary one. + $temporary_table_info = $connection->query("SHOW CREATE TABLE {" . $table_name_test . "}")->fetchAssoc(); + $this->stringContains($temporary_table_info["Create Table"], "CREATE TEMPORARY TABLE"); + + // Assert that both have the same field names. + $normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch(); + $temp_table_name = $connection->queryTemporary('SELECT * FROM {test}'); + $temp_table_fields = $connection->query("SELECT * FROM {" . $temp_table_name . "}")->fetch(); + + $normal_table_fields = array_keys(get_object_vars($normal_table_fields)); + $temp_table_fields = array_keys(get_object_vars($temp_table_fields)); + + $this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields)); + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php index 67ed8016316e..58d500e361d3 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php @@ -8,6 +8,7 @@ use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\StatementInterface; use Drupal\Core\Database\StatementWrapper; +use Drupal\Core\Database\SupportsTemporaryTablesInterface; // cSpell:ignore ilike nextval @@ -19,7 +20,7 @@ /** * PostgreSQL implementation of \Drupal\Core\Database\Connection. */ -class Connection extends DatabaseConnection { +class Connection extends DatabaseConnection implements SupportsTemporaryTablesInterface { /** * The name by which to obtain a lock for retrieve the next insert id. @@ -209,6 +210,15 @@ public function queryRange($query, $from, $count, array $args = [], array $optio return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options); } + /** + * {@inheritdoc} + */ + public function queryTemporary($query, array $args = [], array $options = []) { + $tablename = 'db_temporary_' . uniqid(); + $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options); + return $tablename; + } + public function driver() { return 'pgsql'; } diff --git a/core/modules/pgsql/tests/src/Kernel/pgsql/TemporaryQueryTest.php b/core/modules/pgsql/tests/src/Kernel/pgsql/TemporaryQueryTest.php new file mode 100644 index 000000000000..03284e75f5a4 --- /dev/null +++ b/core/modules/pgsql/tests/src/Kernel/pgsql/TemporaryQueryTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\Tests\pgsql\Kernel\pgsql; + +use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase; + +/** + * Tests the temporary query functionality. + * + * @group Database + */ +class TemporaryQueryTest extends TemporaryQueryTestBase { + + /** + * Confirms that temporary tables work. + */ + public function testTemporaryQuery() { + parent::testTemporaryQuery(); + + $connection = $this->getConnection(); + + $table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []); + + // Assert that the table is indeed a temporary one. + $temporary_table_info = $connection->query("SELECT * FROM pg_class WHERE relname LIKE '%$table_name_test%'")->fetch(); + $this->assertEquals("t", $temporary_table_info->relpersistence); + + // Assert that both have the same field names. + $normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch(); + $temp_table_name = $connection->queryTemporary('SELECT * FROM {test}'); + $temp_table_fields = $connection->query("SELECT * FROM {" . $temp_table_name . "}")->fetch(); + + $normal_table_fields = array_keys(get_object_vars($normal_table_fields)); + $temp_table_fields = array_keys(get_object_vars($temp_table_fields)); + + $this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields)); + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php index 583eeea0a82f..1f2cf1fd191b 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php @@ -5,11 +5,12 @@ use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\Connection as DatabaseConnection; use Drupal\Core\Database\StatementInterface; +use Drupal\Core\Database\SupportsTemporaryTablesInterface; /** * SQLite implementation of \Drupal\Core\Database\Connection. */ -class Connection extends DatabaseConnection { +class Connection extends DatabaseConnection implements SupportsTemporaryTablesInterface { /** * Error code for "Unable to open database file" error. @@ -352,6 +353,22 @@ public function queryRange($query, $from, $count, array $args = [], array $optio return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); } + /** + * {@inheritdoc} + */ + public function queryTemporary($query, array $args = [], array $options = []) { + $tablename = 'db_temporary_' . uniqid(); + + $this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options); + + // Temporary tables always live in the temp database, which means that + // they cannot be fully qualified table names since they do not live + // in the main SQLite database. We provide the fully-qualified name + // ourselves to prevent Drupal from applying prefixes. + // @see https://www.sqlite.org/lang_createtable.html + return 'temp.' . $tablename; + } + public function driver() { return 'sqlite'; } diff --git a/core/modules/sqlite/tests/src/Kernel/sqlite/TemporaryQueryTest.php b/core/modules/sqlite/tests/src/Kernel/sqlite/TemporaryQueryTest.php new file mode 100644 index 000000000000..053e7f16adc2 --- /dev/null +++ b/core/modules/sqlite/tests/src/Kernel/sqlite/TemporaryQueryTest.php @@ -0,0 +1,38 @@ +<?php + +namespace Drupal\Tests\sqlite\Kernel\sqlite; + +use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase; + +/** + * Tests the temporary query functionality. + * + * @group Database + */ +class TemporaryQueryTest extends TemporaryQueryTestBase { + + /** + * Confirms that temporary tables work. + */ + public function testTemporaryQuery() { + parent::testTemporaryQuery(); + + $connection = $this->getConnection(); + + $table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []); + + // Assert that the table is indeed a temporary one. + $this->stringContains("temp.", $table_name_test); + + // Assert that both have the same field names. + $normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch(); + $temp_table_name = $connection->queryTemporary('SELECT * FROM {test}'); + $temp_table_fields = $connection->query("SELECT * FROM $temp_table_name")->fetch(); + + $normal_table_fields = array_keys(get_object_vars($normal_table_fields)); + $temp_table_fields = array_keys(get_object_vars($temp_table_fields)); + + $this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields)); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Database/TemporaryQueryTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/TemporaryQueryTestBase.php new file mode 100644 index 000000000000..0ec77b70ac0c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Database/TemporaryQueryTestBase.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\KernelTests\Core\Database; + +use Drupal\Core\Database\Database; + +/** + * Tests the temporary query functionality. + * + * @group Database + */ +abstract class TemporaryQueryTestBase extends DriverSpecificDatabaseTestBase { + + /** + * Returns the connection. + */ + public function getConnection() { + return Database::getConnection(); + } + + /** + * Returns the number of rows of a table. + */ + public function countTableRows($table_name) { + return Database::getConnection()->select($table_name)->countQuery()->execute()->fetchField(); + } + + /** + * Confirms that temporary tables work. + */ + public function testTemporaryQuery() { + $connection = $this->getConnection(); + + // Now try to run two temporary queries in the same request. + $table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []); + $table_name_task = $connection->queryTemporary('SELECT [pid] FROM {test_task}', []); + + $this->assertEquals($this->countTableRows('test'), $this->countTableRows($table_name_test), 'A temporary table was created successfully in this request.'); + $this->assertEquals($this->countTableRows('test_task'), $this->countTableRows($table_name_task), 'A second temporary table was created successfully in this request.'); + + // Check that leading whitespace and comments do not cause problems + // in the modified query. + $sql = " + -- Let's select some rows into a temporary table + SELECT [name] FROM {test} + "; + $table_name_test = $connection->queryTemporary($sql, []); + $this->assertEquals($this->countTableRows('test'), $this->countTableRows($table_name_test), 'Leading white space and comments do not interfere with temporary table creation.'); + } + +} -- GitLab