summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2016-05-08 15:32:56 (GMT)
committerAlex Pott2016-05-08 15:32:56 (GMT)
commit71094542507ef546ef3fe04e90740bf857395f2a (patch)
treea4016e3b1065a69e1d45d6bfaef25362ba4010ea
parent133c212ed9d3b2fad28d53e04858fc1421db3233 (diff)
Issue #2384459 by mikran, mbovan, slashrsm, chx, Berdir, yched: Add entity query condition for delta in EFQ
-rw-r--r--core/lib/Drupal/Core/Entity/Query/QueryInterface.php33
-rw-r--r--core/lib/Drupal/Core/Entity/Query/Sql/Tables.php61
-rw-r--r--core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php6
-rw-r--r--core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php7
-rw-r--r--core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php5
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php83
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();