diff options
author | Alex Pott | 2016-05-08 15:32:56 (GMT) |
---|---|---|
committer | Alex Pott | 2016-05-08 15:32:56 (GMT) |
commit | 71094542507ef546ef3fe04e90740bf857395f2a (patch) | |
tree | a4016e3b1065a69e1d45d6bfaef25362ba4010ea | |
parent | 133c212ed9d3b2fad28d53e04858fc1421db3233 (diff) |
Issue #2384459 by mikran, mbovan, slashrsm, chx, Berdir, yched: Add entity query condition for delta in EFQ
6 files changed, 183 insertions, 12 deletions
diff --git a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php index e673db1..8852f0b 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php @@ -35,17 +35,40 @@ interface QueryInterface extends AlterableInterface { * @endcode * * @param $field - * Name of the field being queried. It must contain a field name, - * optionally followed by a column name. The column can be "entity" for - * reference fields and that can be followed similarly by a field name - * and so on. Some examples: + * Name of the field being queried. It must contain a field name, optionally + * followed by a column name. The column can be "entity" for reference + * fields and that can be followed similarly by a field name and so on. Some + * examples: * - nid * - tags.value * - tags * - uid.entity.name * "tags" "is the same as "tags.value" as value is the default column. * If two or more conditions have the same field names they apply to the - * same delta within that field. + * same delta within that field. In order to limit the condition to a + * specific item a numeric delta should be added between the field name and + * the column name. + * @code + * ->condition('tags.5.value', 'news') + * @endcode + * This will require condition to be satisfied on a specific delta of the + * field. The condition above will require the 6th value of the field to + * match the provided value. Further, it's possible to create a condition on + * the delta itself by using '%delta'. For example, + * @code + * ->condition('tags.%delta', 5) + * @endcode + * will find only entities which have at least six tags. Finally, the + * condition on the delta itself accompanied with a condition on the value + * will require the value to appear in the specific delta range. For + * example, + * @code + * ->condition('tags.%delta', 0, '>')) + * ->condition('tags.%delta.value', 'news')) + * @endcode + * will only find the "news" tag if it is not the first value. It should be + * noted that conditions on specific deltas and delta ranges are only + * supported when querying content entities. * @param $value * The value for $field. In most cases, this is a scalar and it's treated as * case-insensitive. For more complex operators, it is an array. The meaning diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php index ca25fb9..c8bf59d 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php @@ -5,6 +5,7 @@ namespace Drupal\Core\Entity\Query\Sql; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\Query\QueryException; use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; +use Drupal\Core\Entity\Sql\TableMappingInterface; /** * Adds tables and fields to the SQL entity query. @@ -112,11 +113,37 @@ class Tables implements TablesInterface { // Check whether this field is stored in a dedicated table. if ($field_storage && $table_mapping->requiresDedicatedTableStorage($field_storage)) { + $delta = NULL; // Find the field column. $column = $field_storage->getMainPropertyName(); if ($key < $count) { $next = $specifiers[$key + 1]; + // If this is a numeric specifier we're adding a condition on the + // specific delta. + if (is_numeric($next)) { + $delta = $next; + $index_prefix .= ".$delta"; + // Do not process it again. + $key++; + $next = $specifiers[$key + 1]; + } + // If this specifier is the reserved keyword "%delta" we're adding a + // condition on a delta range. + elseif ($next == TableMappingInterface::DELTA) { + $index_prefix .= TableMappingInterface::DELTA; + // Do not process it again. + $key++; + // If there are more specifiers to work with then continue + // processing. If this is the last specifier then use the reserved + // keyword as a column name. + if ($key < $count) { + $next = $specifiers[$key + 1]; + } + else { + $column = TableMappingInterface::DELTA; + } + } // Is this a field column? $columns = $field_storage->getColumns(); if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) { @@ -140,7 +167,7 @@ class Tables implements TablesInterface { $next_index_prefix = "$relationship_specifier.$column"; } } - $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field); + $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta); $sql_column = $table_mapping->getFieldColumnName($field_storage, $column); $property_definitions = $field_storage->getPropertyDefinitions(); if (isset($property_definitions[$column])) { @@ -173,6 +200,27 @@ class Tables implements TablesInterface { // next one is a column of this field. if ($key < $count) { $next = $specifiers[$key + 1]; + // If this specifier is the reserved keyword "%delta" we're adding a + // condition on a delta range. + if ($next == TableMappingInterface::DELTA) { + $key++; + if ($key < $count) { + $next = $specifiers[$key + 1]; + } + else { + return 0; + } + } + // If this is a numeric specifier we're adding a condition on the + // specific delta. Since we know that this is a single value base + // field no other value than 0 makes sense. + if (is_numeric($next)) { + if ($next > 0) { + $this->sqlQuery->condition('1 <> 1'); + } + $key++; + $next = $specifiers[$key + 1]; + } // Is this a field column? $columns = $field_storage->getColumns(); if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) { @@ -264,7 +312,7 @@ class Tables implements TablesInterface { * @return string * @throws \Drupal\Core\Entity\Query\QueryException */ - protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) { + protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta) { $field_name = $field->getName(); if (!isset($this->fieldTables[$index_prefix . $field_name])) { $entity_type_id = $this->sqlQuery->getMetaData('entity_type'); @@ -274,12 +322,12 @@ class Tables implements TablesInterface { if ($field->getCardinality() != 1) { $this->sqlQuery->addMetaData('simple_query', FALSE); } - $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode); + $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode, $delta); } return $this->fieldTables[$index_prefix . $field_name]; } - protected function addJoin($type, $table, $join_condition, $langcode) { + protected function addJoin($type, $table, $join_condition, $langcode, $delta = NULL) { $arguments = array(); if ($langcode) { $entity_type_id = $this->sqlQuery->getMetaData('entity_type'); @@ -291,6 +339,11 @@ class Tables implements TablesInterface { $join_condition .= ' AND %alias.' . $langcode_key . ' = ' . $placeholder; $arguments[$placeholder] = $langcode; } + if (isset($delta)) { + $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder(); + $join_condition .= ' AND %alias.delta = ' . $placeholder; + $arguments[$placeholder] = $delta; + } return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments); } diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php b/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php index 214e130..bd8e0de 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php @@ -11,8 +11,10 @@ interface TablesInterface { * Adds a field to a database query. * * @param string $field - * If it contains a dot, then field name dot field column. If it doesn't - * then entity property name. + * If it doesn't contain a dot, then an entity base field name. If it + * contains a dot, then either field name dot field column or field name dot + * delta dot field column. Delta can be a numeric value or a "%delta" for + * any value. * @param string $type * Join type, can either be INNER or LEFT. * @param string $langcode diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php index d71949e..aa6fa88 100644 --- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php +++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php @@ -204,7 +204,12 @@ class DefaultTableMapping implements TableMappingInterface { $column_name = count($storage_definition->getColumns()) == 1 ? $field_name : $field_name . '__' . $property_name; } elseif ($this->requiresDedicatedTableStorage($storage_definition)) { - $column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name; + if ($property_name == TableMappingInterface::DELTA) { + $column_name = 'delta'; + } + else { + $column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name; + } } else { throw new SqlContentEntityStorageException("Column information not available for the '$field_name' field."); diff --git a/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php index f21c270..3c553bf 100644 --- a/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php +++ b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php @@ -20,6 +20,11 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; interface TableMappingInterface { /** + * A property that represents delta used in entity query conditions. + */ + const DELTA = '%delta'; + + /** * Gets a list of table names for this mapping. * * @return string[] diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php index 09c9f84..d7a160a 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php @@ -512,6 +512,89 @@ class EntityQueryTest extends EntityKernelTestBase { $this->assertResult(6, 14); } + /** + * Test queries with delta conditions. + */ + public function testDelta() { + $figures = $this->figures; + // Test numeric delta value in field condition. + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("$figures.0.color", 'red') + ->sort('id') + ->execute(); + // As unit 0 at delta 0 was the red triangle bit 0 needs to be set. + $this->assertResult(1, 3, 5, 7, 9, 11, 13, 15); + + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("$figures.1.color", 'red') + ->sort('id') + ->execute(); + // Delta 1 is not red. + $this->assertResult(); + + // Test on two different deltas. + $query = $this->factory->get('entity_test_mulrev'); + $or = $query->andConditionGroup() + ->condition("$figures.0.color", 'red') + ->condition("$figures.1.color", 'blue'); + $this->queryResults = $query + ->condition($or) + ->sort('id') + ->execute(); + $this->assertResult(3, 7, 11, 15); + + // Test the delta range condition. + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("$figures.%delta.color", array('blue', 'red'), 'IN') + ->condition("$figures.%delta", array(0, 1), 'IN') + ->sort('id') + ->execute(); + // Figure delta 0 or 1 can be blue or red, this matches a lot of entities. + $this->assertResult(1, 2, 3, 5, 6, 7, 9, 10, 11, 13, 14, 15); + + // Test the delta range condition without conditions on the value. + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("$figures.%delta", 1) + ->sort('id') + ->execute(); + // Entity needs to have atleast two figures. + $this->assertResult(3, 7, 11, 15); + + // Numeric delta on single value base field should return results only if + // the first item is being targeted. + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("id.0.value", array(1, 3, 5), 'IN') + ->sort('id') + ->execute(); + $this->assertResult(1, 3, 5); + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("id.1.value", array(1, 3, 5), 'IN') + ->sort('id') + ->execute(); + $this->assertResult(); + + // Delta range condition on single value base field should return results + // only if just the field value is targeted. + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("id.%delta.value", array(1, 3, 5), 'IN') + ->sort('id') + ->execute(); + $this->assertResult(1, 3, 5); + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("id.%delta.value", array(1, 3, 5), 'IN') + ->condition("id.%delta", 0, '=') + ->sort('id') + ->execute(); + $this->assertResult(1, 3, 5); + $this->queryResults = $this->factory->get('entity_test_mulrev') + ->condition("id.%delta.value", array(1, 3, 5), 'IN') + ->condition("id.%delta", 1, '=') + ->sort('id') + ->execute(); + $this->assertResult(); + + } + protected function assertResult() { $assert = array(); $expected = func_get_args(); |