diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 691a6ae84c..3473076c16 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -942,6 +942,22 @@ public function schema() { return $this->schema; } + /** + * Prepares and returns a CONDITION query object. + * + * @param string $conjunction + * The operator to use to combine conditions: 'AND' or 'OR'. + * + * @return \Drupal\Core\Database\Query\Condition + * A new Condition query object. + * + * @see \Drupal\Core\Database\Query\Condition + */ + public function condition($conjunction) { + $class = $this->getDriverClass('Condition'); + return new $class($conjunction); + } + /** * Escapes a database name string. * diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Condition.php b/core/lib/Drupal/Core/Database/Driver/mysql/Condition.php new file mode 100644 index 0000000000..5a14f7e3ec --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Condition.php @@ -0,0 +1,10 @@ +getPrefixInfo($table_name, $add_prefix); - $condition = new Condition('AND'); + $condition = $this->connection->condition('AND'); $condition->condition('table_schema', $table_info['database']); $condition->condition('table_name', $table_info['table'], $operator); return $condition; diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Condition.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Condition.php new file mode 100644 index 0000000000..d1f21d8e06 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Condition.php @@ -0,0 +1,10 @@ + 50) { + $value_fragment = []; + $arguments = []; + $json_value = json_encode($values, 0 ,1); + $placeholder = ':db_condition_placeholder_' . $queryPlaceholder->nextPlaceholder(); + $value_fragment[] = "select value from json_each($placeholder)"; + $arguments[$placeholder] = $json_value; + return [$value_fragment, $arguments]; + } + else { + return parent::compileValueList($values, $connection, $queryPlaceholder); + } + } + +} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php index fc59afde25..3da69f02d6 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php @@ -27,4 +27,32 @@ public function __toString() { return $query; } + /** + * {@inheritdoc} + */ + public function execute() { + // SQLite has a default limit of 999 placeholders, so upsert in batches + // less than that. + $remaining_insert_values = $all_insert_values = $this->insertValues; + while (count($remaining_insert_values)) { + $this->insertValues = []; + $num_placeholders = 0; + while (count($remaining_insert_values)) { + $next_insert_value = array_shift($remaining_insert_values); + if ($num_placeholders + count($next_insert_value) > 999) { + array_unshift($remaining_insert_values, $next_insert_value); + break; + } + $this->insertValues[] = $next_insert_value; + $num_placeholders += count($next_insert_value); + } + if (count($this->insertValues)) { + parent::execute(); + } + } + + $this->insertValues = $all_insert_values; + return TRUE; + } + } diff --git a/core/lib/Drupal/Core/Database/Query/Condition.php b/core/lib/Drupal/Core/Database/Query/Condition.php index e7038e23c9..968a913ad4 100644 --- a/core/lib/Drupal/Core/Database/Query/Condition.php +++ b/core/lib/Drupal/Core/Database/Query/Condition.php @@ -291,24 +291,9 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace $condition['value'] = [$condition['value']]; } // Process all individual values. - $value_fragment = []; - foreach ($condition['value'] as $value) { - if ($value instanceof SelectInterface) { - // Right hand part is a subquery. Compile, put brackets around it - // and collect any arguments. - $value->compile($connection, $queryPlaceholder); - $value_fragment[] = '(' . (string) $value . ')'; - $arguments += $value->arguments(); - } - else { - // Right hand part is a normal value. Replace the value with a - // placeholder and add the value as an argument. - $placeholder = ':db_condition_placeholder_' . $queryPlaceholder->nextPlaceholder(); - $value_fragment[] = $placeholder; - $arguments[$placeholder] = $value; - } - } + list ($value_fragment, $fragment_arguments) = $this->compileValueList($condition['value'], $connection, $queryPlaceholder); $value_fragment = $operator['prefix'] . implode($operator['delimiter'], $value_fragment) . $operator['postfix']; + $arguments += $fragment_arguments; } // Concatenate the left hand part, operator and right hand part. @@ -323,6 +308,46 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace } } + /** + * Compiles the values (right side of operator) of a conditional. + * + * @param $values + * The values to compile. + * @param $connection + * The database connection for which to compile the conditionals. + * @param $queryPlaceholder + * The query this condition belongs to. If not given, the current query is + * used. + * + * @return + * A two item array: + * - The first item is an array of SQL fragments for the list, containing + * placeholders for values as needed. + * - The second item is an associative array mapping each placeholder name + * to a value. + */ + protected function compileValueList(array $values, Connection $connection, PlaceholderInterface $queryPlaceholder) { + $value_fragment = []; + $arguments = []; + foreach ($values as $value) { + if ($value instanceof SelectInterface) { + // Right hand part is a subquery. Compile, put brackets around it + // and collect any arguments. + $value->compile($connection, $queryPlaceholder); + $value_fragment[] = '(' . (string) $value . ')'; + $arguments += $value->arguments(); + } + else { + // Right hand part is a normal value. Replace the value with a + // placeholder and add the value as an argument. + $placeholder = ':db_condition_placeholder_' . $queryPlaceholder->nextPlaceholder(); + $value_fragment[] = $placeholder; + $arguments[$placeholder] = $value; + } + } + return [$value_fragment, $arguments]; + } + /** * {@inheritdoc} */ @@ -399,7 +424,7 @@ protected function mapConditionOperator($operator) { * {@inheritdoc} */ public function conditionGroupFactory($conjunction = 'AND') { - return new Condition($conjunction); + return new static($conjunction); } /** diff --git a/core/lib/Drupal/Core/Database/Query/Delete.php b/core/lib/Drupal/Core/Database/Query/Delete.php index def88385e0..658b8342cc 100644 --- a/core/lib/Drupal/Core/Database/Query/Delete.php +++ b/core/lib/Drupal/Core/Database/Query/Delete.php @@ -36,7 +36,7 @@ public function __construct(Connection $connection, $table, array $options = []) parent::__construct($connection, $options); $this->table = $table; - $this->condition = new Condition('AND'); + $this->condition = $this->connection->condition('AND'); } /** diff --git a/core/lib/Drupal/Core/Database/Query/Merge.php b/core/lib/Drupal/Core/Database/Query/Merge.php index 6d89532472..5a9bc3db89 100644 --- a/core/lib/Drupal/Core/Database/Query/Merge.php +++ b/core/lib/Drupal/Core/Database/Query/Merge.php @@ -138,7 +138,7 @@ public function __construct(Connection $connection, $table, array $options = []) parent::__construct($connection, $options); $this->table = $table; $this->conditionTable = $table; - $this->condition = new Condition('AND'); + $this->condition = $this->connection->condition('AND'); } /** diff --git a/core/lib/Drupal/Core/Database/Query/QueryConditionTrait.php b/core/lib/Drupal/Core/Database/Query/QueryConditionTrait.php index 9053a17772..83be429fa5 100644 --- a/core/lib/Drupal/Core/Database/Query/QueryConditionTrait.php +++ b/core/lib/Drupal/Core/Database/Query/QueryConditionTrait.php @@ -108,7 +108,7 @@ public function compiled() { * {@inheritdoc} */ public function conditionGroupFactory($conjunction = 'AND') { - return new Condition($conjunction); + return $this->connection->condition($conjunction); } /** diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php index 27892ce5cb..45243d358d 100644 --- a/core/lib/Drupal/Core/Database/Query/Select.php +++ b/core/lib/Drupal/Core/Database/Query/Select.php @@ -134,8 +134,8 @@ public function __construct(Connection $connection, $table, $alias = NULL, $opti $options['return'] = Database::RETURN_STATEMENT; parent::__construct($connection, $options); $conjunction = isset($options['conjunction']) ? $options['conjunction'] : 'AND'; - $this->condition = new Condition($conjunction); - $this->having = new Condition($conjunction); + $this->condition = $this->connection->condition($conjunction); + $this->having = $this->connection->condition($conjunction); $this->addJoin(NULL, $table, $alias); } diff --git a/core/lib/Drupal/Core/Database/Query/SelectExtender.php b/core/lib/Drupal/Core/Database/Query/SelectExtender.php index dc3531df2f..e428c21d86 100644 --- a/core/lib/Drupal/Core/Database/Query/SelectExtender.php +++ b/core/lib/Drupal/Core/Database/Query/SelectExtender.php @@ -521,7 +521,7 @@ public function __call($method, $args) { * {@inheritdoc} */ public function conditionGroupFactory($conjunction = 'AND') { - return new Condition($conjunction); + return $this->connection->condition($conjunction); } /** diff --git a/core/lib/Drupal/Core/Database/Query/Update.php b/core/lib/Drupal/Core/Database/Query/Update.php index 5de5d682ec..2b08e20d00 100644 --- a/core/lib/Drupal/Core/Database/Query/Update.php +++ b/core/lib/Drupal/Core/Database/Query/Update.php @@ -65,7 +65,7 @@ public function __construct(Connection $connection, $table, array $options = []) parent::__construct($connection, $options); $this->table = $table; - $this->condition = new Condition('AND'); + $this->condition = $this->connection->condition('AND'); } /** diff --git a/core/lib/Drupal/Core/Database/Schema.php b/core/lib/Drupal/Core/Database/Schema.php index 38fde96345..56317af7e1 100644 --- a/core/lib/Drupal/Core/Database/Schema.php +++ b/core/lib/Drupal/Core/Database/Schema.php @@ -150,7 +150,7 @@ protected function buildTableNameCondition($table_name, $operator = '=', $add_pr // Retrieve the table name and schema $table_info = $this->getPrefixInfo($table_name, $add_prefix); - $condition = new Condition('AND'); + $condition = $this->connection->condition('AND'); $condition->condition('table_catalog', $info['database']); $condition->condition('table_schema', $table_info['schema']); $condition->condition('table_name', $table_info['table'], $operator); diff --git a/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php index c9ab16a551..55cd427b95 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php @@ -175,4 +175,20 @@ public function testPostgresqlReservedWords() { } } + /** + * Test that the method ::condition() returns an Condition object from the driver directory. + */ + public function testCondition() { + $connection = Database::getConnection('default', 'default'); + $namespace = (new \ReflectionObject($connection))->getNamespaceName() . "\\Condition"; + + $condition = $connection->condition('AND'); + $this->assertIdentical($namespace, get_class($condition)); + + $nested_and_condition = $condition->andConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_and_condition)); + $nested_or_condition = $condition->orConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_or_condition)); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/DeleteTruncateTest.php b/core/tests/Drupal/KernelTests/Core/Database/DeleteTruncateTest.php index b4aa0a9b14..09ae7ddd01 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DeleteTruncateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DeleteTruncateTest.php @@ -159,4 +159,22 @@ public function testSpecialColumnDelete() { $this->assertEqual($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.'); } + /** + * Tests namespace of the condition object. + */ + public function testNamespaceConditionObject() { + $namespace = (new \ReflectionObject($this->connection))->getNamespaceName() . "\\Condition"; + $delete = $this->connection->delete('test'); + + $reflection = new \ReflectionObject($delete); + $condition_property = $reflection->getProperty('condition'); + $condition_property->setAccessible(TRUE); + $this->assertIdentical($namespace, get_class($condition_property->getValue($delete))); + + $nested_and_condition = $delete->andConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_and_condition)); + $nested_or_condition = $delete->orConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_or_condition)); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php b/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php index 9f52a15266..b2cc301c36 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php @@ -70,6 +70,42 @@ public function testMultiInsert() { $this->assertIdentical($saved_age, '32', 'Can retrieve after inserting.'); } + + /** + * Tests that we can insert 1000 records in one query object. + * + * This test is motivated by SQLite's default limit of 999 placeholders, but + * it's good to ensure that all database drivers can handle large inserts. + * + * @link https://www.sqlite.org/limits.html#max_variable_number + */ + public function testLargeMultiInsert() { + $num_records_before = (int) $this->connection->query('SELECT COUNT(*) FROM {test}')->fetchField(); + + $query = $this->connection->insert('test'); + $query->fields(['name', 'age']); + + for ($i=0; $i<1000; $i++) { + $values = [ + 'name' => "Name $i", + 'age' => $i, + ]; + $query->values($values); + } + + // Check how many records are queued for insertion. + $this->assertIdentical($query->count(), 1000, '1000 records are queued for insertion.'); + + $query->execute(); + + $num_records_after = (int) $this->connection->query('SELECT COUNT(*) FROM {test}')->fetchField(); + $this->assertSame($num_records_before + 1000, $num_records_after, 'Record inserts correctly.'); + $saved_age = $this->connection->query('SELECT age FROM {test} WHERE name = :name', [':name' => 'Name 0'])->fetchField(); + $this->assertIdentical($saved_age, '0', 'Can retrieve after inserting.'); + $saved_age = $this->connection->query('SELECT age FROM {test} WHERE name = :name', [':name' => 'Name 999'])->fetchField(); + $this->assertIdentical($saved_age, '999', 'Can retrieve after inserting.'); + } + /** * Tests that an insert object can be reused with new data after it executes. */ diff --git a/core/tests/Drupal/KernelTests/Core/Database/MergeTest.php b/core/tests/Drupal/KernelTests/Core/Database/MergeTest.php index b103b5fc6f..9236c8c7a7 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/MergeTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/MergeTest.php @@ -230,4 +230,22 @@ public function testInvalidMerge() { $this->fail('No InvalidMergeQueryException thrown'); } + /** + * Tests namespace of the condition object. + */ + public function testNamespaceConditionObject() { + $namespace = (new \ReflectionObject($this->connection))->getNamespaceName() . "\\Condition"; + $merge = $this->connection->merge('test'); + + $reflection = new \ReflectionObject($merge); + $condition_property = $reflection->getProperty('condition'); + $condition_property->setAccessible(TRUE); + $this->assertIdentical($namespace, get_class($condition_property->getValue($merge))); + + $nested_and_condition = $merge->andConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_and_condition)); + $nested_or_condition = $merge->orConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_or_condition)); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php b/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php index af40cb89cc..8950e5d89c 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php @@ -4,6 +4,7 @@ use Drupal\Core\Database\InvalidQueryException; use Drupal\Core\Database\Database; +use Drupal\Core\Database\Query\Condition; /** * Tests the Select query builder. @@ -614,4 +615,83 @@ public function testEmptyInCondition() { } } + /** + * Tests queries with >1000 items in an IN list. + * + * This test is motivated by SQLite's default limit of 999 placeholders, but + * it's good to ensure that all database drivers can handle large IN lists. + * + * @link https://www.sqlite.org/limits.html#max_variable_number + */ + public function testLargeInCondition() { + $names = []; + $names[] = 'John'; + for ($i=1; $i<500; $i++) { + $names[] = "Name $i"; + } + $names[] = 'George'; + for ($i=501; $i<1000; $i++) { + $names[] = "Name $i"; + } + $names[] = 'Ringo'; + + // Find the above 3 Beatles. + $num_records = $this->connection->select('test') + ->condition('name', $names, 'IN') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($num_records, 3); + + // Find the other Beatle. + $num_records = $this->connection->select('test') + ->condition('name', $names, 'NOT IN') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($num_records, 1); + + // Find the above 3 Beatles with a nested condition. + $condition = $this->connection->condition('AND'); + $condition->condition('name', $names, 'IN'); + $num_records = $this->connection->select('test') + ->condition($condition) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($num_records, 3); + + // Find the other Beatle with a nested condition. + $condition = $this->connection->condition('AND'); + $condition->condition('name', $names, 'NOT IN'); + $num_records = $this->connection->select('test') + ->condition($condition) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($num_records, 1); + } + + /** + * Tests namespace of the condition and having objects. + */ + public function testNamespaceConditionAndHavingObjects() { + $namespace = (new \ReflectionObject($this->connection))->getNamespaceName() . "\\Condition"; + $select = $this->connection->select('test'); + $reflection = new \ReflectionObject($select); + + $condition_property = $reflection->getProperty('condition'); + $condition_property->setAccessible(TRUE); + $this->assertIdentical($namespace, get_class($condition_property->getValue($select))); + + $having_property = $reflection->getProperty('having'); + $having_property->setAccessible(TRUE); + $this->assertIdentical($namespace, get_class($having_property->getValue($select))); + + $nested_and_condition = $select->andConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_and_condition)); + $nested_or_condition = $select->orConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_or_condition)); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/UpdateTest.php b/core/tests/Drupal/KernelTests/Core/Database/UpdateTest.php index b5b28092e8..7697f530dc 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/UpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/UpdateTest.php @@ -141,4 +141,22 @@ public function testSpecialColumnUpdate() { $this->assertIdentical($saved_value, 'New offset value', 'Updated special column name value successfully.'); } + /** + * Tests namespace of the condition object. + */ + public function testNamespaceConditionObject() { + $namespace = (new \ReflectionObject($this->connection))->getNamespaceName() . "\\Condition"; + $update = $this->connection->update('test'); + + $reflection = new \ReflectionObject($update); + $condition_property = $reflection->getProperty('condition'); + $condition_property->setAccessible(TRUE); + $this->assertIdentical($namespace, get_class($condition_property->getValue($update))); + + $nested_and_condition = $update->andConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_and_condition)); + $nested_or_condition = $update->orConditionGroup(); + $this->assertIdentical($namespace, get_class($nested_or_condition)); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php b/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php index 461117797f..2631de026e 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php @@ -89,4 +89,44 @@ public function testSpecialColumnUpsert() { $this->assertEquals($record->function, 'Function 2'); } + /** + * Confirms that we can upsert 1000 records successfully. + * + * This test is motivated by SQLite's default limit of 999 placeholders, but + * it's good to ensure that all database drivers can handle large upserts. + * + * @link https://www.sqlite.org/limits.html#max_variable_number + */ + public function testLargeUpsert() { + $connection = Database::getConnection(); + $num_records_before = $connection->query('SELECT COUNT(*) FROM {test_people}')->fetchField(); + + $upsert = $connection->upsert('test_people') + ->key('job') + ->fields(['job', 'age', 'name']); + + for ($i=0; $i<1000; $i++) { + $values = [ + 'job' => "Job $i", + 'age' => $i, + 'name' => "Name $i", + ]; + $upsert->values($values); + } + + $upsert->execute(); + + $num_records_after = $connection->query('SELECT COUNT(*) FROM {test_people}')->fetchField(); + $this->assertEqual($num_records_before + 1000, $num_records_after, 'Rows were inserted properly.'); + + $person = $connection->query('SELECT * FROM {test_people} WHERE job = :job', [':job' => 'Job 0'])->fetch(); + $this->assertEqual($person->job, 'Job 0', 'First job set correctly.'); + $this->assertEqual($person->age, 0, 'First age set correctly.'); + $this->assertEqual($person->name, 'Name 0', 'First name set correctly.'); + + $person = $connection->query('SELECT * FROM {test_people} WHERE job = :job', [':job' => 'Job 999'])->fetch(); + $this->assertEqual($person->job, 'Job 999', 'Last job set correctly.'); + $this->assertEqual($person->age, 999, 'Last age set correctly.'); + $this->assertEqual($person->name, 'Name 999', 'Last name set correctly.'); + } } diff --git a/core/tests/Drupal/Tests/Core/Database/OrderByTest.php b/core/tests/Drupal/Tests/Core/Database/OrderByTest.php index ef38627d9f..ce30df25bb 100644 --- a/core/tests/Drupal/Tests/Core/Database/OrderByTest.php +++ b/core/tests/Drupal/Tests/Core/Database/OrderByTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\Core\Database; +use Drupal\Core\Database\Query\Condition; use Drupal\Core\Database\Query\Select; use Drupal\Tests\UnitTestCase; @@ -25,7 +26,11 @@ class OrderByTest extends UnitTestCase { protected function setUp() { $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') ->disableOriginalConstructor() + ->setMethods(['condition']) ->getMockForAbstractClass(); + $connection->expects($this->any()) + ->method('condition') + ->willReturn(new Condition('AND')); $this->query = new Select($connection, 'test', NULL); }