diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php index 4a0b440..868961e 100644 --- a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php @@ -48,27 +48,14 @@ public function execute() { $entity_type = $this->entityType; $entity_info = entity_get_info($entity_type); if (!isset($entity_info['base_table'])) { - throw new QueryException("No base table, nothing to query."); + throw new QueryException("No base table, invalid query."); } - $configurable_fields = array_map(function ($data) use ($entity_type) { - return isset($data['bundles'][$entity_type]); - }, field_info_field_map()); $base_table = $entity_info['base_table']; - // Assemble a list of entity tables, primarily for use in - // \Drupal\field_sql_storage\Entity\Tables::ensureEntityTable(). - $entity_tables = array(); $simple_query = TRUE; - // ensureEntityTable() decides whether an entity property will be queried - // from the data table or the base table based on where it finds the - // property first. The data table is prefered, which is why it gets added - // before the base table. if (isset($entity_info['data_table'])) { - $entity_tables[$entity_info['data_table']] = drupal_get_schema($entity_info['data_table']); $simple_query = FALSE; } - $entity_tables[$base_table] = drupal_get_schema($base_table); $sqlQuery = $this->connection->select($base_table, 'base_table', array('conjunction' => $this->conjunction)); - $sqlQuery->addMetaData('configurable_fields', $configurable_fields); $sqlQuery->addMetaData('entity_type', $entity_type); // Determines the key of the column to join on. This is either the entity // id key or the revision id key, depending on whether the entity type @@ -87,10 +74,6 @@ public function execute() { $revision_field = $entity_info['entity_keys']['revision']; $fields[$revision_field] = TRUE; $sqlQuery->addField('base_table', $revision_field); - // Now revision id is column 0 and the value column is 1. - if ($this->age == FIELD_LOAD_CURRENT) { - $id_key = 'revision'; - } } // Now add the value column for fetchAllKeyed(). This is always the // entity id. @@ -116,14 +99,7 @@ public function execute() { } // This now contains first the table containing entity properties and // last the entity base table. They might be the same. - $sqlQuery->addMetaData('entity_tables', $entity_tables); $sqlQuery->addMetaData('age', $this->age); - // This contains the relevant SQL field to be used when joining entity - // tables. - $sqlQuery->addMetaData('entity_id_field', $entity_info['entity_keys'][$id_key]); - // This contains the relevant SQL field to be used when joining field - // tables. - $sqlQuery->addMetaData('field_id_field', $id_key == 'id' ? 'entity_id' : 'revision_id'); $sqlQuery->addMetaData('simple_query', $simple_query); $this->condition->compile($sqlQuery); if ($this->count) { diff --git a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php index 7ba89b3..0a3c071 100644 --- a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php @@ -59,19 +59,137 @@ function __construct(SelectInterface $sql_query) { * of this in a query for a condition or sort. */ function addField($field, $type, $langcode) { - $parts = explode('.', $field); - $property = $parts[0]; - $configurable_fields = $this->sqlQuery->getMetaData('configurable_fields'); - if (!empty($configurable_fields[$property]) || substr($property, 0, 3) == 'id:') { - $field_name = $property; - $table = $this->ensureFieldTable($field_name, $type, $langcode); - // Default to .value. - $column = isset($parts[1]) ? $parts[1] : 'value'; - $sql_column = _field_sql_storage_columnname($field_name, $column); - } - else { - $sql_column = $property; - $table = $this->ensureEntityTable($property, $type, $langcode); + $entity_type = $this->sqlQuery->getMetaData('entity_type'); + $age = $this->sqlQuery->getMetaData('age'); + // This variables ensures grouping works correctly. For example: + // ->condition('tags', 2, '>') + // ->condition('tags', 20, '<') + // ->condition('node_reference.nid.entity.tags', 2) + // The first two should use the same table but the last one needs to be a + // new table. So for the first two, the table array index will be '0.tags' + // while the third will be '1.tags'. + $index_prefix = 0; + $specifiers = explode('.', $field); + $base_table = 'base_table'; + $count = count($specifiers) - 1; + // This will contain the definitions of the last specifier seen by the + // system. + $propertyDefinitions = array(); + $entity_info = entity_get_info($entity_type); + for ($key = 0; $key <= $count; $key ++) { + // This can either be the name of an entity property (non-configurable + // field), a field API field (a configurable field) or an indication + // that the next specifier will be on another entity (here it'll be + // called relationship field). + $specifier = $specifiers[$key]; + // If property definitions is set, it's a relationship. + if ($propertyDefinitions) { + // But ss it a valid relationship? If so, the previous field defines + // it as an entity. + if (isset($propertyDefinitions[$specifier]['constraints']['entity type']) && $propertyDefinitions[$specifier]['settings']['id source']) { + // Move forward in the relationship chain. + $entity_type = $propertyDefinitions[$specifier]['constraints']['entity type']; + $entity_info = entity_get_info($entity_type); + // Add the new entity base table. + $join_condition= '%alias.' . $entity_info['entity_keys']['id'] . " = $table.$sql_column"; + $base_table = $this->sqlQuery->leftJoin($entity_info['base_table'], NULL, $join_condition); + $index_prefix++; + $propertyDefinitions = array(); + continue; + } + else { + throw new QueryException(format_string('Invalid specifier @specifier.', array('@specifier' => $specifier))); + } + } + // If there is revision support and only the current revision is being + // queried then use the revision id. Otherwise, the entity id will do. + if (!empty($entity_info['entity_keys']['revision']) && $age == FIELD_LOAD_CURRENT) { + // This contains the relevant SQL field to be used when joining entity + // tables. + $entity_id_field = $entity_info['entity_keys']['revision']; + // This contains the relevant SQL field to be used when joining field + // tables. + $field_id_field = 'revision_id'; + } + else { + $entity_id_field = $entity_info['entity_keys']['id']; + $field_id_field = 'entity_id'; + } + // field_purge_batch() is passing in id:$field_id. + if (substr($specifier, 0, 3) == 'id:') { + $field = field_info_field_by_id(substr($specifier, 3)); + } + else { + $field = field_info_field($specifier); + } + if ($field) { + // Find the field column. + if ($key < $count) { + $next = $specifiers[$key + 1]; + // Is this a field column? + if (isset($field['columns'][$next]) || in_array($next, field_reserved_columns())) { + // Use it. + $column = $next; + // Do not process it again. + $key++; + } + else { + // Must be a relationship. Create a relevant entity and figure out + // what the relationship column is. + $values = array(); + // If there are bundles, find one this field is attached to. + // Relationship settings must be field level so it doesn't matter + // which. + if (!empty($entity_info['entity keys']['bundle'])) { + $bundle = reset($field['bundles'][$entity_type]); + $values[$entity_info['entity keys']['bundle']] = $bundle; + } + $entity = entity_create($entity_type, $values); + $propertyDefinitions = $entity->$specifier->getPropertyDefinitions(); + // Does this look like a valid relationship? + if (isset($propertyDefinitions[$next]['constraints']['entity type']) && $propertyDefinitions[$next]['settings']['id source']) { + // Great, use the id source. + $column = $propertyDefinitions[$next]['settings']['id source']; + } + else { + throw new QueryException(format_string('Unknown specifier @next', array('@next' => $next))); + } + } + } + else { + // If this is the last specifier, default to value. + $column = 'value'; + } + $table = $this->ensureFieldTable("$index_prefix.", $field, $type, $langcode, $base_table, $entity_id_field, $field_id_field); + $sql_column = _field_sql_storage_columnname($field['field_name'], $column); + } + else { + // ensureEntityTable() decides whether an entity property will be + // queried from the data table or the base table based on where it + // finds the property first. The data table is prefered, which is why + // it gets added before the base table. + $entity_tables = array(); + if (isset($entity_info['data_table'])) { + $this->sqlQuery->addMetaData('simple_query', FALSE); + $entity_tables[$entity_info['data_table']] = drupal_get_schema($entity_info['data_table']); + } + $entity_tables[$entity_info['base_table']] = drupal_get_schema($entity_info['base_table']); + $sql_column = $specifier; + $table = $this->ensureEntityTable("$index_prefix.", $specifier, $type, $langcode, $base_table, $entity_id_field, $entity_tables); + // Prepare $propertyDefinitions for a relationship if this was not the + // last specifier. + if ($key < $count) { + $values = array(); + // If there are bundles, pick one. It does not matter which, + // properties exist on all bundles. + if (!empty($entity_info['entity keys']['bundle'])) { + $bundles = array_keys($entity_info['bundles']); + $values[$entity_info['entity keys']['bundle']] = reset($bundles); + } + $entity = entity_create($entity_type, $values); + $propertyDefinitions = $entity->$specifier->getPropertyDefinitions(); + } + } } return "$table.$sql_column"; } @@ -83,18 +201,13 @@ function addField($field, $type, $langcode) { * @return string * @throws \Drupal\Core\Entity\Query\QueryException */ - protected function ensureEntityTable($property, $type, $langcode) { - $entity_tables = $this->sqlQuery->getMetaData('entity_tables'); - if (!$entity_tables) { - throw new QueryException('Can not query entity properties without entity tables.'); - } + protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) { foreach ($entity_tables as $table => $schema) { if (isset($schema['fields'][$property])) { - if (!isset($this->entityTables[$table])) { - $id_field = $this->sqlQuery->getMetaData('entity_id_field'); - $this->entityTables[$table] = $this->addJoin($type, $table, "%alias.$id_field = base_table.$id_field", $langcode); + if (!isset($this->entityTables[$index_prefix . $table])) { + $this->entityTables[$index_prefix . $table] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode); } - return $this->entityTables[$table]; + return $this->entityTables[$index_prefix . $table]; } } throw new QueryException(format_string('@property not found', array('@property' => $property))); @@ -108,31 +221,17 @@ protected function ensureEntityTable($property, $type, $langcode) { * @return string * @throws \Drupal\Core\Entity\Query\QueryException */ - protected function ensureFieldTable(&$field_name, $type, $langcode) { - if (!isset($this->fieldTables[$field_name])) { - // This is field_purge_batch() passing in a field id. - if (substr($field_name, 0, 3) == 'id:') { - $field = field_info_field_by_id(substr($field_name, 3)); - } - else { - $field = field_info_field($field_name); - } - if (!$field) { - throw new QueryException(format_string('field @field_name not found', array('@field_name' => $field_name))); - } - // This is really necessary only for the id: case but it can't be run - // before throwing the exception. - $field_name = $field['field_name']; + protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) { + $field_name = $field['field_name']; + if (!isset($this->fieldTables[$index_prefix . $field_name])) { $table = $this->sqlQuery->getMetaData('age') == FIELD_LOAD_CURRENT ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field); - $field_id_field = $this->sqlQuery->getMetaData('field_id_field'); - $entity_id_field = $this->sqlQuery->getMetaData('entity_id_field'); if ($field['cardinality'] != 1) { $this->sqlQuery->addMetaData('simple_query', FALSE); } $entity_type = $this->sqlQuery->getMetaData('entity_type'); - $this->fieldTables[$field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = base_table.$entity_id_field AND %alias.entity_type = '$entity_type'", $langcode); + $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field AND %alias.entity_type = '$entity_type'", $langcode); } - return $this->fieldTables[$field_name]; + return $this->fieldTables[$index_prefix . $field_name]; } protected function addJoin($type, $table, $join_condition, $langcode) { diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php new file mode 100644 index 0000000..b42cf2d --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php @@ -0,0 +1,82 @@ + 'Entity Query relationship', + 'description' => 'Tests the Entity Query relationship API', + 'group' => 'Entity API', + ); + } + + /** + * Overrides \Drupal\simpletest\WebTestBase::setUp(). + */ + protected function setUp() { + parent::setUp(); + for ($i = 0; $i <= 1; $i++) { + $this->accounts[] = $this->drupalCreateUser(); + } + for ($i = 0; $i <= 2; $i++) { + $entity = entity_create('entity_test', array()); + $entity->name->value = $this->randomName(); + $entity->user_id->value = $this->accounts[$i ? 1 : 0]->uid; + $entity->save(); + $this->entities[] = $entity; + } + $this->factory = drupal_container()->get('entity.query'); + } + + /** + * Tests querying. + */ + public function testQuery() { + $results = $this->factory->get('entity_test') + ->condition("user_id.entity.name", $this->accounts[0]->name) + ->execute(); + $this->assertEqual(count($results), 1); + $id = $this->entities[0]->id(); + $this->assertEqual($results, array($id => $id)); + $results = $this->factory->get('entity_test') + ->condition("user_id.entity.name", $this->accounts[0]->name, '<>') + ->execute(); + $this->assertEqual(count($results), 2); + $this->assertFalse(isset($results[$id])); + for ($i = 1; $i <=2 ; $i++) { + $id = $this->entities[$i]->id(); + $this->assertEqual($results[$id], $id); + } + $results = $this->factory->get('entity_test') + ->exists("user_id.entity.name") + ->execute(); + $this->assertEqual(count($results), 3); + for ($i = 0; $i <=2 ; $i++) { + $id = $this->entities[$i]->id(); + $this->assertEqual($results[$id], $id); + } + $results = $this->factory->get('entity_test') + ->notExists("user_id.entity.name") + ->execute(); + $this->assertEqual(count($results), 0); + } +}