diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index 03705beb7ba907a7157fa989d8f7efd68bd14009..e3c91dbcd944414b77527b27dac2d956e4f30e1c 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -538,7 +538,8 @@ public function getFullQualifiedTableName($table) {
    * identifiers enclosed in square brackets.
    *
    * @param string $query
-   *   The query string as SQL, with curly braces surrounding the table names.
+   *   The query string as SQL, with curly braces surrounding the table names,
+   *   and square brackets surrounding identifiers.
    * @param array $options
    *   An associative array of options to control how the query is run. See
    *   the documentation for self::defaultOptions() for details. The content of
@@ -547,14 +548,15 @@ public function getFullQualifiedTableName($table) {
    * @return \Drupal\Core\Database\StatementInterface
    *   A PDO prepared statement ready for its execute() method.
    *
+   * @throws \InvalidArgumentException
+   *   If multiple statements are included in the string, and delimiters are
+   *   not allowed in the query.
    * @throws \Drupal\Core\Database\DatabaseExceptionWrapper
    */
   public function prepareStatement(string $query, array $options): StatementInterface {
-    $query = $this->prefixTables($query);
-    if (!($options['allow_square_brackets'] ?? FALSE)) {
-      $query = $this->quoteIdentifiers($query);
-    }
     try {
+      $query = $this->preprocessStatement($query, $options);
+
       // @todo in Drupal 10, only return the StatementWrapper.
       // @see https://www.drupal.org/node/3177490
       $statement = $this->statementWrapperClass ?
@@ -574,6 +576,52 @@ public function prepareStatement(string $query, array $options): StatementInterf
     return $statement;
   }
 
+  /**
+   * Returns a string SQL statement ready for preparation.
+   *
+   * This method replaces table names in curly braces and identifiers in square
+   * brackets with platform specific replacements, appropriately escaping them
+   * and wrapping them with platform quote characters.
+   *
+   * @param string $query
+   *   The query string as SQL, with curly braces surrounding the table names,
+   *   and square brackets surrounding identifiers.
+   * @param array $options
+   *   An associative array of options to control how the query is run. See
+   *   the documentation for self::defaultOptions() for details.
+   *
+   * @return string
+   *   A string SQL statement ready for preparation.
+   *
+   * @throws \InvalidArgumentException
+   *   If multiple statements are included in the string, and delimiters are
+   *   not allowed in the query.
+   */
+  protected function preprocessStatement(string $query, array $options): string {
+    // To protect against SQL injection, Drupal only supports executing one
+    // statement at a time.  Thus, the presence of a SQL delimiter (the
+    // semicolon) is not allowed unless the option is set.  Allowing semicolons
+    // should only be needed for special cases like defining a function or
+    // stored procedure in SQL. Trim any trailing delimiter to minimize false
+    // positives unless delimiter is allowed.
+    $trim_chars = " \xA0\t\n\r\0\x0B";
+    if (empty($options['allow_delimiter_in_query'])) {
+      $trim_chars .= ';';
+    }
+    $query = rtrim($query, $trim_chars);
+    if (strpos($query, ';') !== FALSE && empty($options['allow_delimiter_in_query'])) {
+      throw new \InvalidArgumentException('; is not supported in SQL strings. Use only one statement at a time.');
+    }
+
+    // Resolve {tables} and [identifiers] to the platform specific syntax.
+    $query = $this->prefixTables($query);
+    if (!($options['allow_square_brackets'] ?? FALSE)) {
+      $query = $this->quoteIdentifiers($query);
+    }
+
+    return $query;
+  }
+
   /**
    * Prepares a query string and returns the prepared statement.
    *
@@ -813,20 +861,6 @@ public function query($query, array $args = [], $options = []) {
     // object, which we pass to StatementInterface::execute.
     if (is_string($query)) {
       $this->expandArguments($query, $args);
-      // To protect against SQL injection, Drupal only supports executing one
-      // statement at a time.  Thus, the presence of a SQL delimiter (the
-      // semicolon) is not allowed unless the option is set.  Allowing
-      // semicolons should only be needed for special cases like defining a
-      // function or stored procedure in SQL. Trim any trailing delimiter to
-      // minimize false positives unless delimiter is allowed.
-      $trim_chars = " \xA0\t\n\r\0\x0B";
-      if (empty($options['allow_delimiter_in_query'])) {
-        $trim_chars .= ';';
-      }
-      $query = rtrim($query, $trim_chars);
-      if (strpos($query, ';') !== FALSE && empty($options['allow_delimiter_in_query'])) {
-        throw new \InvalidArgumentException('; is not supported in SQL strings. Use only one statement at a time.');
-      }
       $stmt = $this->prepareStatement($query, $options);
     }
     elseif ($query instanceof StatementInterface) {
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
index 9b926522156be66f5bc965fbe48ec8829399ba8e..ab450982b7cdf4ee6001c9c0e002120dca4cf7dc 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
@@ -428,11 +428,14 @@ public function mapConditionOperator($operator) {
    * {@inheritdoc}
    */
   public function prepareStatement(string $query, array $options): StatementInterface {
-    $query = $this->prefixTables($query);
-    if (!($options['allow_square_brackets'] ?? FALSE)) {
-      $query = $this->quoteIdentifiers($query);
+    try {
+      $query = $this->preprocessStatement($query, $options);
+      $statement = new Statement($this->connection, $this, $query, $options['pdo'] ?? []);
+    }
+    catch (\Exception $e) {
+      $this->exceptionHandler()->handleStatementException($e, $query, $options);
     }
-    return new Statement($this->connection, $this, $query, $options['pdo'] ?? []);
+    return $statement;
   }
 
   public function nextId($existing_id = 0) {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
index 7f614de9ae65f9c58f153d7ca2053d0771f0db88..3cec967383d0a8ede8cf5ab1c7c9e9b09237e1ef 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
@@ -181,13 +181,21 @@ public function testMultipleStatementsForNewPhp() {
   }
 
   /**
-   * Ensure that you cannot execute multiple statements.
+   * Ensure that you cannot execute multiple statements in a query.
    */
-  public function testMultipleStatements() {
+  public function testMultipleStatementsQuery() {
     $this->expectException(\InvalidArgumentException::class);
     Database::getConnection('default', 'default')->query('SELECT * FROM {test}; SELECT * FROM {test_people}');
   }
 
+  /**
+   * Ensure that you cannot prepare multiple statements.
+   */
+  public function testMultipleStatements() {
+    $this->expectException(\InvalidArgumentException::class);
+    Database::getConnection('default', 'default')->prepareStatement('SELECT * FROM {test}; SELECT * FROM {test_people}', []);
+  }
+
   /**
    * Test that the method ::condition() returns a Condition object.
    */