diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php index 7f78e87..4a0b440 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php @@ -48,15 +48,32 @@ 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, invalid query."); + throw new QueryException("No base table, nothing to 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'])) { @@ -70,6 +87,10 @@ 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. @@ -95,7 +116,14 @@ 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_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php index d8b36ba..22739df 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php +++ b/core/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 = 'AND') { + function get($entity_type, $conjunction) { return new Query($entity_type, $conjunction, $this->connection); } } diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php index bf02e2b..7ba89b3 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php @@ -59,145 +59,19 @@ function __construct(SelectInterface $sql_query) { * of this in a query for a condition or sort. */ function addField($field, $type, $langcode) { - $entity_type = $this->sqlQuery->getMetaData('entity_type'); - $age = $this->sqlQuery->getMetaData('age'); - // This variable 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 'tags' - // while the third will be 'node_reference.nid.tags'. - $index_prefix = ''; - $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 ++) { - // 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'; - } - // This can either be the name of an entity property (non-configurable - // field), a field API field (a configurable field). - $specifier = $specifiers[$key]; - // First, check for field API fields by trying to retrieve the field specified. - // Normally it is a field name, but field_purge_batch() is passing in - // id:$field_id so check that first. - if (substr($specifier, 0, 3) == 'id:') { - $field = field_info_field_by_id(substr($specifier, 3)); - } - else { - $field = field_info_field($specifier); - } - // If we managed to retrieve the field, process it. - 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']; - } - // Prepare the next index prefix. - $next_index_prefix = "$relationship_specifier.$column"; - } - } - 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); - } - // This is an entity property (non-configurable field). - 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]; - $next_index_prefix = $relationship_specifier; - } - // 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); - $propertyDefinitions = array(); - $key++; - $index_prefix .= "$next_index_prefix."; - } - else { - throw new QueryException(format_string('Invalid specifier @next.', array('@next' => $next))); - } - } + $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); } return "$table.$sql_column"; } @@ -209,13 +83,18 @@ function addField($field, $type, $langcode) { * @return string * @throws \Drupal\Core\Entity\Query\QueryException */ - protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) { + 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.'); + } foreach ($entity_tables as $table => $schema) { if (isset($schema['fields'][$property])) { - if (!isset($this->entityTables[$index_prefix . $table])) { - $this->entityTables[$index_prefix . $table] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode); + 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); } - return $this->entityTables[$index_prefix . $table]; + return $this->entityTables[$table]; } } throw new QueryException(format_string('@property not found', array('@property' => $property))); @@ -229,17 +108,31 @@ protected function ensureEntityTable($index_prefix, $property, $type, $langcode, * @return string * @throws \Drupal\Core\Entity\Query\QueryException */ - 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])) { + 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']; $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[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field AND %alias.entity_type = '$entity_type'", $langcode); + $this->fieldTables[$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[$index_prefix . $field_name]; + return $this->fieldTables[$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 deleted file mode 100644 index 4ca53c1..0000000 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php +++ /dev/null @@ -1,178 +0,0 @@ - 'Entity Query relationship', - 'description' => 'Tests the Entity Query relationship API', - 'group' => 'Entity API', - ); - } - - protected function setUp() { - parent::setUp(); - // We want a taxonomy term reference field. It needs a vocabulary, terms, - // a field and an instance. First, create the vocabulary. - $vocabulary = entity_create('taxonomy_vocabulary', array( - 'vid' => drupal_strtolower($this->randomName()), - )); - $vocabulary->save(); - // Second, create the field. - $this->fieldName = strtolower($this->randomName()); - $field = array( - 'field_name' => $this->fieldName, - 'type' => 'taxonomy_term_reference', - ); - $field['settings']['allowed_values']['vocabulary'] = $vocabulary->id(); - field_create_field($field); - // Third, create the instance. - $instance = array( - 'entity_type' => 'entity_test', - 'field_name' => $this->fieldName, - 'bundle' => 'entity_test', - ); - field_create_instance($instance); - // Create two terms and also two accounts. - for ($i = 0; $i <= 1; $i++) { - $term = entity_create('taxonomy_term', array( - 'name' => $this->randomName(), - 'vid' => $vocabulary->id(), - )); - $term->save(); - $this->terms[] = $term; - $this->accounts[] = $this->drupalCreateUser(); - } - // Create three entity_test entities, the 0th entity will point to the - // 0th account and 0th term, the 1st and 2nd entity will point to the - // 1st account and 1st term. - 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 returns the 0th entity as that's only one pointing to the 0th - // account. - $this->queryResults = $this->factory->get('entity_test') - ->condition("user_id.entity.name", $this->accounts[0]->name) - ->execute(); - $this->assertResults(array(0)); - // This returns the 1st and 2nd entity as those point to the 1st account. - $this->queryResults = $this->factory->get('entity_test') - ->condition("user_id.entity.name", $this->accounts[0]->name, '<>') - ->execute(); - $this->assertResults(array(1, 2)); - // This returns all three entities because all of them point to an - // account. - $this->queryResults = $this->factory->get('entity_test') - ->exists("user_id.entity.name") - ->execute(); - $this->assertResults(array(0, 1, 2)); - // This returns no entities because all of them point to an account. - $this->queryResults = $this->factory->get('entity_test') - ->notExists("user_id.entity.name") - ->execute(); - $this->assertEqual(count($this->queryResults), 0); - // This returns the 0th entity as that's only one pointing to the 0th - // term (test without specifying the field column). - $this->queryResults = $this->factory->get('entity_test') - ->condition("$this->fieldName.entity.name", $this->terms[0]->name) - ->execute(); - $this->assertResults(array(0)); - // This returns the 0th entity as that's only one pointing to the 0th - // term (test with specifying the column name). - $this->queryResults = $this->factory->get('entity_test') - ->condition("$this->fieldName.tid.entity.name", $this->terms[0]->name) - ->execute(); - $this->assertResults(array(0)); - // This returns the 1st and 2nd entity as those point to the 1st term. - $this->queryResults = $this->factory->get('entity_test') - ->condition("$this->fieldName.entity.name", $this->terms[0]->name, '<>') - ->execute(); - $this->assertResults(array(1, 2)); - } - - /** - * Assert the results. - * - * @param array $expected - * A list of indexes in the $this->entities array. - */ - 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); - } - } -}