diff --git a/core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php b/core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..2e6bd0fb06c5e2131bd0fd7bc6cb0dbd6a8584b2 --- /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 9921ab84d833b9f8065f748aaff7a54eb7465825..a5c4d82f34289c9e994c121f62fb5b0646bc5285 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 910387eb5f6c4d27182f8447c8dccbe4a617f8c0..3e8437e8f9fa6a29dac2f28e29445d60fdff2178 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 0000000000000000000000000000000000000000..484ccc75227e98e72d0fda04c68afd0e42eef008 --- /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 67ed8016316eaa9fd891b3606b258a2e516d3fc6..58d500e361d3257aaf74fc8670cf0c4fa7972ec9 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 0000000000000000000000000000000000000000..03284e75f5a42c4674494c2275b8477c7d2899cd --- /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 583eeea0a82ffb2193f3b30e8bcc8ac59934e4f7..1f2cf1fd191b8fb69897c95bc194523cb3e2a25b 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 0000000000000000000000000000000000000000..053e7f16adc2f32c5cbb774a4f0d25256d7cddc6 --- /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 0000000000000000000000000000000000000000..0ec77b70ac0c55b765485372351f75d48f6bc133 --- /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.'); + } + +}