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