Unverified Commit 39170302 authored by alexpott's avatar alexpott

Issue #2345451 by mondrake, daffie, david_garcia, shobhit_juyal, sokru,...

Issue #2345451 by mondrake, daffie, david_garcia, shobhit_juyal, sokru, markdorison, alexpott, Beakerboy: Introduce a Connection::prepareStatement method allowing to pass options to the Statement, deprecate Connection::prepareQuery() and Connection::prepare()
parent aece6314
......@@ -315,6 +315,11 @@ public function destroy() {
* database type. In rare cases, such as creating an SQL function, []
* characters might be needed and can be allowed by changing this option to
* TRUE.
* - pdo: By default, queries will execute with the PDO options set on the
* connection. In particular cases, it could be necessary to override the
* PDO driver options on the statement level. In such case, pass the
* required setting as an array here, and they will be passed to the
* prepared statement. See https://www.php.net/manual/en/pdo.prepare.php.
*
* @return array
* An array of default query options.
......@@ -326,6 +331,7 @@ protected function defaultOptions() {
'throw_exception' => TRUE,
'allow_delimiter_in_query' => FALSE,
'allow_square_brackets' => FALSE,
'pdo' => [],
];
}
......@@ -476,6 +482,31 @@ public function getFullQualifiedTableName($table) {
return $options['database'] . '.' . $prefix . $table;
}
/**
* Returns a prepared statement given a SQL string.
*
* This method caches prepared statements, reusing them when possible. It also
* prefixes tables names enclosed in curly braces and, optionally, quotes
* identifiers enclosed in square brackets.
*
* @param string $query
* The query string as SQL, with curly braces surrounding the table names.
* @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
* the 'pdo' key will be passed to the prepared statement.
*
* @return \Drupal\Core\Database\StatementInterface
* A PDO prepared statement ready for its execute() method.
*/
public function prepareStatement(string $query, array $options): StatementInterface {
$query = $this->prefixTables($query);
if (!($options['allow_square_brackets'] ?? FALSE)) {
$query = $this->quoteIdentifiers($query);
}
return $this->connection->prepare($query, $options['pdo'] ?? []);
}
/**
* Prepares a query string and returns the prepared statement.
*
......@@ -492,14 +523,15 @@ public function getFullQualifiedTableName($table) {
*
* @return \Drupal\Core\Database\StatementInterface
* A PDO prepared statement ready for its execute() method.
*
* @deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use
* ::prepareStatement instead.
*
* @see https://www.drupal.org/node/3137786
*/
public function prepareQuery($query, $quote_identifiers = TRUE) {
$query = $this->prefixTables($query);
if ($quote_identifiers) {
$query = $this->quoteIdentifiers($query);
}
return $this->connection->prepare($query);
@trigger_error('Connection::prepareQuery() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use ::prepareStatement() instead. See https://www.drupal.org/node/3137786', E_USER_DEPRECATED);
return $this->prepareStatement($query, ['allow_square_brackets' => !$quote_identifiers]);
}
/**
......@@ -675,9 +707,7 @@ protected function filterComment($comment = '') {
* object to this method. It is used primarily for database drivers for
* databases that require special LOB field handling.
* @param array $args
* An array of arguments for the prepared statement. If the prepared
* statement uses ? placeholders, this array must be an indexed array.
* If it contains named placeholders, it must be an associative array.
* The associative array of arguments for the prepared statement.
* @param array $options
* An associative array of options to control how the query is run. The
* given options will be merged with self::defaultOptions(). See the
......@@ -734,7 +764,7 @@ public function query($query, array $args = [], $options = []) {
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->prepareQuery($query, !$options['allow_square_brackets']);
$stmt = $this->prepareStatement($query, $options);
$stmt->execute($args, $options);
}
......@@ -1668,9 +1698,17 @@ public function commit() {
*
* @throws \PDOException
*
* @see \PDO::prepare()
* @see https://www.php.net/manual/en/pdo.prepare.php
*
* @deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Database
* drivers should instantiate \PDOStatement objects by calling
* \PDO::prepare in their Collection::prepareStatement method instead.
* \PDO::prepare should not be called outside of driver code.
*
* @see https://www.drupal.org/node/3137786
*/
public function prepare($statement, array $driver_options = []) {
@trigger_error('Connection::prepare() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Database drivers should instantiate \PDOStatement objects by calling \PDO::prepare in their Collection::prepareStatement method instead. \PDO::prepare should not be called outside of driver code. See https://www.drupal.org/node/3137786', E_USER_DEPRECATED);
return $this->connection->prepare($statement, $driver_options);
}
......
......@@ -6,6 +6,7 @@
use Drupal\Core\Database\Connection as DatabaseConnection;
use Drupal\Core\Database\DatabaseAccessDeniedException;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\StatementInterface;
/**
* @addtogroup database
......@@ -184,12 +185,16 @@ public function query($query, array $args = [], $options = []) {
return $return;
}
public function prepareQuery($query, $quote_identifiers = TRUE) {
/**
* {@inheritdoc}
*/
public function prepareStatement(string $query, array $options): StatementInterface {
// mapConditionOperator converts some operations (LIKE, REGEXP, etc.) to
// PostgreSQL equivalents (ILIKE, ~*, etc.). However PostgreSQL doesn't
// automatically cast the fields to the right type for these operators,
// so we need to alter the query and add the type-cast.
return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query), $quote_identifiers);
$query = preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query);
return parent::prepareStatement($query, $options);
}
public function queryRange($query, $from, $count, array $args = [], array $options = []) {
......
......@@ -20,7 +20,7 @@ public function execute() {
return NULL;
}
$stmt = $this->connection->prepareQuery((string) $this);
$stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
// Fetch the list of blobs and sequences used on that table.
$table_information = $this->connection->schema()->queryTableInformation($this->table);
......
......@@ -19,7 +19,7 @@ public function execute() {
return NULL;
}
$stmt = $this->connection->prepareQuery((string) $this);
$stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
// Fetch the list of blobs and sequences used on that table.
$table_information = $this->connection->schema()->queryTableInformation($this->table);
......
......@@ -18,7 +18,7 @@ public function execute() {
// Because we filter $fields the same way here and in __toString(), the
// placeholders will all match up properly.
$stmt = $this->connection->prepareQuery((string) $this);
$stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
// Fetch the list of blobs and sequences used on that table.
$table_information = $this->connection->schema()->queryTableInformation($this->table);
......
......@@ -5,6 +5,7 @@
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\Connection as DatabaseConnection;
use Drupal\Core\Database\StatementInterface;
/**
* SQLite implementation of \Drupal\Core\Database\Connection.
......@@ -336,6 +337,7 @@ public static function sqlFunctionLikeBinary($pattern, $subject) {
* {@inheritdoc}
*/
public function prepare($statement, array $driver_options = []) {
@trigger_error('Connection::prepare() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Database drivers should instantiate \PDOStatement objects by calling \PDO::prepare in their Collection::prepareStatement method instead. \PDO::prepare should not be called outside of driver code. See https://www.drupal.org/node/3137786', E_USER_DEPRECATED);
return new Statement($this->connection, $this, $statement, $driver_options);
}
......@@ -403,12 +405,12 @@ public function mapConditionOperator($operator) {
/**
* {@inheritdoc}
*/
public function prepareQuery($query, $quote_identifiers = TRUE) {
public function prepareStatement(string $query, array $options): StatementInterface {
$query = $this->prefixTables($query);
if ($quote_identifiers) {
if (!($options['allow_square_brackets'] ?? FALSE)) {
$query = $this->quoteIdentifiers($query);
}
return $this->prepare($query);
return new Statement($this->connection, $this, $query, $options['pdo'] ?? []);
}
public function nextId($existing_id = 0) {
......
......@@ -221,7 +221,7 @@ protected function throwPDOException() {
* A PDOStatement object.
*/
protected function getStatement($query, &$args = []) {
return $this->dbh->prepare($query);
return $this->dbh->prepare($query, $this->driverOptions);
}
/**
......
......@@ -14,9 +14,12 @@
class DatabaseExceptionWrapperTest extends KernelTestBase {
/**
* Tests the expected database exception thrown for prepared statements.
* Tests deprecation of Connection::prepare.
*
* @group legacy
* @expectedDeprecation Connection::prepare() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Database drivers should instantiate \PDOStatement objects by calling \PDO::prepare in their Collection::prepareStatement method instead. \PDO::prepare should not be called outside of driver code. See https://www.drupal.org/node/3137786
*/
public function testPreparedStatement() {
public function testPrepare() {
$connection = Database::getConnection();
try {
// SQLite validates the syntax upon preparing a statement already.
......@@ -40,6 +43,27 @@ public function testPreparedStatement() {
}
}
/**
* Tests deprecation of Connection::prepareQuery.
*
* @group legacy
* @expectedDeprecation Connection::prepareQuery() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use ::prepareStatement() instead. See https://www.drupal.org/node/3137786
*/
public function testPrepareQuery() {
$this->expectException(\PDOException::class);
$stmt = Database::getConnection()->prepareQuery('bananas');
$stmt->execute();
}
/**
* Tests Connection::prepareStatement exceptions.
*/
public function testPrepareStatement() {
$this->expectException(\PDOException::class);
$stmt = Database::getConnection()->prepareStatement('bananas', []);
$stmt->execute();
}
/**
* Tests the expected database exception thrown for inexistent tables.
*/
......
<?php
namespace Drupal\KernelTests\Core\Database;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\StatementInterface;
/**
* Tests the Statement classes.
*
* @group Database
*/
class StatementTest extends DatabaseTestBase {
/**
* Tests that a prepared statement object can be reused for multiple inserts.
*/
public function testRepeatedInsertStatementReuse() {
$num_records_before = $this->connection->select('test')->countQuery()->execute()->fetchField();
$sql = "INSERT INTO {test} ([name], [age]) VALUES (:name, :age)";
$args = [
':name' => 'Larry',
':age' => '30',
];
$options = [
'return' => Database::RETURN_STATEMENT,
'allow_square_brackets' => FALSE,
];
$stmt = $this->connection->prepareStatement($sql, $options);
$this->assertInstanceOf(StatementInterface::class, $stmt);
$this->assertTrue($stmt->execute($args, $options));
// We should be able to specify values in any order if named.
$args = [
':age' => '31',
':name' => 'Curly',
];
$this->assertTrue($stmt->execute($args, $options));
$num_records_after = $this->connection->select('test')->countQuery()->execute()->fetchField();
$this->assertEquals($num_records_before + 2, $num_records_after);
$this->assertSame('30', $this->connection->query('SELECT age FROM {test} WHERE name = :name', [':name' => 'Larry'])->fetchField());
$this->assertSame('31', $this->connection->query('SELECT age FROM {test} WHERE name = :name', [':name' => 'Curly'])->fetchField());
}
}
......@@ -3,6 +3,7 @@
namespace Drupal\Tests\Core\Database;
use Composer\Autoload\ClassLoader;
use Drupal\Core\Database\Statement;
use Drupal\Tests\Core\Database\Stub\StubConnection;
use Drupal\Tests\Core\Database\Stub\StubPDO;
use Drupal\Tests\UnitTestCase;
......@@ -585,13 +586,16 @@ public function testQueryTrim($expected, $query, $options) {
$mock_pdo = $this->getMockBuilder(StubPdo::class)
->setMethods(['execute', 'prepare', 'setAttribute'])
->getMock();
$mock_statement = $this->getMockBuilder(Statement::class)
->disableOriginalConstructor()
->getMock();
// Ensure that PDO::prepare() is called only once, and with the
// correctly trimmed query string.
$mock_pdo->expects($this->once())
->method('prepare')
->with($expected)
->willReturnSelf();
->willReturn($mock_statement);
$connection = new StubConnection($mock_pdo, []);
$connection->query($query, [], $options);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment