From 9fd8864cf47fabe6b3ad9d3af79626f21e5aa7e4 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Mon, 7 Jan 2013 13:03:23 +0000
Subject: [PATCH] Issue #1446600 by chx: Add EntityFieldQuery (pseudo-)join
 support.

---
 .../Drupal/field_sql_storage/Entity/Query.php |  30 +--
 .../field_sql_storage/Entity/QueryFactory.php |   2 +-
 .../field_sql_storage/Entity/Tables.php       | 189 ++++++++++++++----
 .../Entity/EntityQueryRelationshipTest.php    | 178 +++++++++++++++++
 4 files changed, 328 insertions(+), 71 deletions(-)
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php

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 4a0b44062482..7f78e87d4cc1 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,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_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 22739dfe8486..d8b36ba99fdc 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) {
+  function get($entity_type, $conjunction = 'AND') {
     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 7ba89b3b792f..bf02e2b69a86 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,19 +59,145 @@ 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 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)));
+        }
+      }
     }
     return "$table.$sql_column";
   }
@@ -83,18 +209,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 +229,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 000000000000..4ca53c1e659e
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryRelationshipTest.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Entity\Tests\EntityQueryRelationshipTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests Entity Query API relationship functionality.
+ */
+class EntityQueryRelationshipTest extends WebTestBase  {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('entity_test', 'taxonomy');
+
+  /**
+   * @var \Drupal\field_sql_storage\Entity\QueryFactory
+   */
+  protected $factory;
+
+  /**
+   * Term entities.
+   *
+   * @var array
+   */
+  protected $terms;
+
+  /**
+   * User entities.
+   *
+   * @var array
+   */
+  public $accounts;
+
+  /**
+   * entity_test entities.
+   *
+   * @var array
+   */
+  protected $entities;
+
+  /**
+   * The name of the taxonomy field used for test.
+   *
+   * @var string
+   */
+  protected $fieldName;
+
+  /**
+   * The results returned by EntityQuery.
+   *
+   * @var array
+   */
+  protected $queryResults;
+
+  public static function getInfo() {
+    return array(
+      'name' => '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);
+    }
+  }
+}
-- 
GitLab