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..7f78e87 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,32 +48,15 @@ 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 - // supports revisions. - $id_key = 'id'; $id_field = $entity_info['entity_keys']['id']; $fields[$id_field] = TRUE; if (empty($entity_info['entity_keys']['revision'])) { @@ -87,10 +70,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 +95,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/QueryFactory.php b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php index 22739df..d8b36ba 100644 --- a/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php +++ b/core/modules/field/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php @@ -18,7 +18,7 @@ function __construct(Connection $connection) { $this->connection = $connection; } - function get($entity_type, $conjunction) { + function get($entity_type, $conjunction = 'AND') { return new Query($entity_type, $conjunction, $this->connection); } } 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..8a90a8f 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,140 @@ 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 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. + $column = FALSE; + 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++; + } + // If there are more specifiers, the next one must be a + // relationship. Either the field name followed by a relationship + // specifier, for example $node->field_image->entity. Or a field + // column followed by a relationship specifier, for example + // $node->field_image->fid->entity. In both cases, prepare the + // property definitions for the relationship. In the first case, + // also use the property definitions for column. + if ($key < $count) { + $relationship_specifier = $specifiers[$key + 1]; + $propertyDefinitions = typed_data() + ->create(array('type' => $field['type'] . '_field')) + ->getPropertyDefinitions(); + // If the column is not yet known, ie. the + // $node->field_image->entity case then use the id source as the + // column. + if (!$column && isset($propertyDefinitions[$relationship_specifier]['settings']['id source'])) { + // If this is a valid relationship, use the id source. + // Otherwise, the code executing the relationship will throw an + // exception anyways so no need to do it here. + $column = $propertyDefinitions[$relationship_specifier]['settings']['id source']; + } + } + } + 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); + } + // If there are more specifiers to come, it's a relationship. + if ($key < $count) { + // Computed fields have prepared their property definition already, do + // it for properties as well. + if (!$propertyDefinitions) { + // Create a relevant entity to find the definition for this + // property. + $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(); + $relationship_specifier = $specifiers[$key + 1]; + } + // Check for a valid relationship. + if (isset($propertyDefinitions[$relationship_specifier]['constraints']['entity type']) && isset($propertyDefinitions[$relationship_specifier]['settings']['id source'])) { + // If it is, use the entity type. + $entity_type = $propertyDefinitions[$relationship_specifier]['constraints']['entity type']; + $entity_info = entity_get_info($entity_type); + // Add the new entity base table using the table and sql column. + $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(); + $key++; + } + else { + throw new QueryException(format_string('Invalid specifier @next.', array('@next' => $next))); + } + } } return "$table.$sql_column"; } @@ -83,18 +204,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 +224,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..231692f --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php @@ -0,0 +1,156 @@ + 'Entity Query relationship', + 'description' => 'Tests the Entity Query relationship API', + 'group' => 'Entity API', + ); + } + + /** + * Overrides \Drupal\simpletest\WebTestBase::setUp(). + */ + protected function setUp() { + parent::setUp(); + $vocabulary = entity_create('taxonomy_vocabulary', array( + 'machine_name' => strtolower($this->randomName()), + )); + $vocabulary->save(); + $this->fieldName = strtolower($this->randomName()); + $field = array( + 'field_name' => $this->fieldName, + 'type' => 'taxonomy_term_reference', + ); + $field['settings']['allowed_values']['vocabulary'] = $vocabulary->machine_name; + field_create_field($field); + $instance = array( + 'entity_type' => 'entity_test', + 'field_name' => $this->fieldName, + 'bundle' => 'entity_test', + ); + field_create_instance($instance); + for ($i = 0; $i <= 1; $i++) { + $term = entity_create('taxonomy_term', array( + 'name' => $this->randomName(), + 'vid' => $vocabulary->vid, + )); + $term->save(); + $this->terms[] = $term; + $this->accounts[] = $this->drupalCreateUser(); + } + for ($i = 0; $i <= 2; $i++) { + $entity = entity_create('entity_test', array()); + $entity->name->value = $this->randomName(); + $index = $i ? 1 : 0; + $entity->user_id->value = $this->accounts[$index]->uid; + $entity->{$this->fieldName}->tid = $this->terms[$index]->tid; + $entity->save(); + $this->entities[] = $entity; + } + $this->factory = drupal_container()->get('entity.query'); + } + + /** + * Tests querying. + */ + public function testQuery() { + $this->queryResults = $this->factory->get('entity_test') + ->condition("user_id.entity.name", $this->accounts[0]->name) + ->execute(); + $this->assertResults(array(0)); + $this->queryResults = $this->factory->get('entity_test') + ->condition("user_id.entity.name", $this->accounts[0]->name, '<>') + ->execute(); + $this->assertResults(array(1, 2)); + $this->queryResults = $this->factory->get('entity_test') + ->exists("user_id.entity.name") + ->execute(); + $this->assertResults(array(0, 1, 2)); + $this->queryResults = $this->factory->get('entity_test') + ->notExists("user_id.entity.name") + ->execute(); + $this->assertEqual(count($this->queryResults), 0); + $this->queryResults = $this->factory->get('entity_test') + ->condition("$this->fieldName.entity.name", $this->terms[0]->name) + ->execute(); + $this->assertResults(array(0)); + $this->queryResults = $this->factory->get('entity_test') + ->condition("$this->fieldName.tid.entity.name", $this->terms[0]->name) + ->execute(); + $this->assertResults(array(0)); + $this->queryResults = $this->factory->get('entity_test') + ->condition("$this->fieldName.entity.name", $this->terms[0]->name, '<>') + ->execute(); + $this->assertResults(array(1, 2)); + } + + protected function assertResults($expected) { + $this->assertEqual(count($this->queryResults), count($expected)); + foreach ($expected as $key) { + $id = $this->entities[$key]->id(); + $this->assertEqual($this->queryResults[$id], $id); + } + } +}