3 namespace Drupal\Core\Entity\Query\Sql;
5 use Drupal\Core\Database\Query\SelectInterface;
6 use Drupal\Core\Entity\EntityType;
7 use Drupal\Core\Entity\Query\QueryException;
8 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
9 use Drupal\Core\Entity\Sql\TableMappingInterface;
10 use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
11 use Drupal\Core\Field\FieldStorageDefinitionInterface;
12 use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
15 * Adds tables and fields to the SQL entity query.
17 class Tables implements TablesInterface {
20 * @var \Drupal\Core\Database\Query\SelectInterface
27 * This array contains at most two entries: one for the data, one for the
28 * properties. Its keys are unique references to the tables, values are
31 * @see \Drupal\Core\Entity\Query\Sql\Tables::ensureEntityTable().
35 protected $entityTables = [];
38 * Field table array, key is table name, value is alias.
40 * This array contains one entry per field table.
44 protected $fieldTables = [];
49 * @var \Drupal\Core\Entity\EntityManager
51 protected $entityManager;
54 * List of case sensitive fields.
58 protected $caseSensitiveFields = [];
61 * @param \Drupal\Core\Database\Query\SelectInterface $sql_query
63 public function __construct(SelectInterface $sql_query) {
64 $this->sqlQuery = $sql_query;
65 $this->entityManager = \Drupal::entityManager();
71 public function addField($field, $type, $langcode) {
72 $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
73 $all_revisions = $this->sqlQuery->getMetaData('all_revisions');
74 // This variable ensures grouping works correctly. For example:
75 // ->condition('tags', 2, '>')
76 // ->condition('tags', 20, '<')
77 // ->condition('node_reference.nid.entity.tags', 2)
78 // The first two should use the same table but the last one needs to be a
79 // new table. So for the first two, the table array index will be 'tags'
80 // while the third will be 'node_reference.nid.tags'.
82 $specifiers = explode('.', $field);
83 $base_table = 'base_table';
84 $count = count($specifiers) - 1;
85 // This will contain the definitions of the last specifier seen by the
87 $propertyDefinitions = [];
88 $entity_type = $this->entityManager->getDefinition($entity_type_id);
90 $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
91 for ($key = 0; $key <= $count; $key++) {
92 // This can either be the name of an entity base field or a configurable
94 $specifier = $specifiers[$key];
95 if (isset($field_storage_definitions[$specifier])) {
96 $field_storage = $field_storage_definitions[$specifier];
97 $column = $field_storage->getMainPropertyName();
100 $field_storage = FALSE;
104 // If there is revision support, only the current revisions are being
105 // queried, and the field is revisionable then use the revision id.
106 // Otherwise, the entity id will do.
107 if (($revision_key = $entity_type->getKey('revision')) && $all_revisions && $field_storage && $field_storage->isRevisionable()) {
108 // This contains the relevant SQL field to be used when joining entity
110 $entity_id_field = $revision_key;
111 // This contains the relevant SQL field to be used when joining field
113 $field_id_field = 'revision_id';
116 $entity_id_field = $entity_type->getKey('id');
117 $field_id_field = 'entity_id';
120 /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
121 $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
123 // Check whether this field is stored in a dedicated table.
124 if ($field_storage && $table_mapping->requiresDedicatedTableStorage($field_storage)) {
128 $next = $specifiers[$key + 1];
129 // If this is a numeric specifier we're adding a condition on the
131 if (is_numeric($next)) {
133 $index_prefix .= ".$delta";
134 // Do not process it again.
136 $next = $specifiers[$key + 1];
138 // If this specifier is the reserved keyword "%delta" we're adding a
139 // condition on a delta range.
140 elseif ($next == TableMappingInterface::DELTA) {
141 $index_prefix .= TableMappingInterface::DELTA;
142 // Do not process it again.
144 // If there are more specifiers to work with then continue
145 // processing. If this is the last specifier then use the reserved
146 // keyword as a column name.
148 $next = $specifiers[$key + 1];
151 $column = TableMappingInterface::DELTA;
154 // Is this a field column?
155 $columns = $field_storage->getColumns();
156 if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
159 // Do not process it again.
162 // If there are more specifiers, the next one must be a
163 // relationship. Either the field name followed by a relationship
164 // specifier, for example $node->field_image->entity. Or a field
165 // column followed by a relationship specifier, for example
166 // $node->field_image->fid->entity. In both cases, prepare the
167 // property definitions for the relationship. In the first case,
168 // also use the property definitions for column.
170 $relationship_specifier = $specifiers[$key + 1];
171 $propertyDefinitions = $field_storage->getPropertyDefinitions();
173 // Prepare the next index prefix.
174 $next_index_prefix = "$relationship_specifier.$column";
177 $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta);
178 $sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
180 // The field is stored in a shared table.
182 // ensureEntityTable() decides whether an entity property will be
183 // queried from the data table or the base table based on where it
184 // finds the property first. The data table is preferred, which is why
185 // it gets added before the base table.
187 $revision_table = NULL;
188 if ($all_revisions && $field_storage && $field_storage->isRevisionable()) {
189 $data_table = $entity_type->getRevisionDataTable();
190 $entity_base_table = $entity_type->getRevisionTable();
193 $data_table = $entity_type->getDataTable();
194 $entity_base_table = $entity_type->getBaseTable();
196 if ($field_storage && $field_storage->isRevisionable() && in_array($field_storage->getName(), $entity_type->getRevisionMetadataKeys())) {
197 $revision_table = $entity_type->getRevisionTable();
201 $this->sqlQuery->addMetaData('simple_query', FALSE);
202 $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
204 if ($revision_table) {
205 $entity_tables[$revision_table] = $this->getTableMapping($revision_table, $entity_type_id);
207 $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
208 $sql_column = $specifier;
210 // If there are more specifiers, get the right sql column name if the
211 // next one is a column of this field.
213 $next = $specifiers[$key + 1];
214 // If this specifier is the reserved keyword "%delta" we're adding a
215 // condition on a delta range.
216 if ($next == TableMappingInterface::DELTA) {
219 $next = $specifiers[$key + 1];
225 // If this is a numeric specifier we're adding a condition on the
226 // specific delta. Since we know that this is a single value base
227 // field no other value than 0 makes sense.
228 if (is_numeric($next)) {
230 $this->sqlQuery->condition('1 <> 1');
233 $next = $specifiers[$key + 1];
235 // Is this a field column?
236 $columns = $field_storage->getColumns();
237 if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
239 $sql_column = $table_mapping->getFieldColumnName($field_storage, $next);
240 // Do not process it again.
245 $table = $this->ensureEntityTable($index_prefix, $sql_column, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
248 // If there is a field storage (some specifiers are not) and a field
249 // column, check for case sensitivity.
250 if ($field_storage && $column) {
251 $property_definitions = $field_storage->getPropertyDefinitions();
252 if (isset($property_definitions[$column])) {
253 $this->caseSensitiveFields[$field] = $property_definitions[$column]->getSetting('case_sensitive');
257 // If there are more specifiers to come, it's a relationship.
258 if ($field_storage && $key < $count) {
259 // Computed fields have prepared their property definition already, do
260 // it for properties as well.
261 if (!$propertyDefinitions) {
262 $propertyDefinitions = $field_storage->getPropertyDefinitions();
263 $relationship_specifier = $specifiers[$key + 1];
264 $next_index_prefix = $relationship_specifier;
266 $entity_type_id = NULL;
267 // Relationship specifier can also contain the entity type ID, i.e.
268 // entity:node, entity:user or entity:taxonomy.
269 if (strpos($relationship_specifier, ':') !== FALSE) {
270 list($relationship_specifier, $entity_type_id) = explode(':', $relationship_specifier, 2);
272 // Check for a valid relationship.
273 if (isset($propertyDefinitions[$relationship_specifier]) && $propertyDefinitions[$relationship_specifier] instanceof DataReferenceDefinitionInterface) {
274 // If it is, use the entity type if specified already, otherwise use
276 $target_definition = $propertyDefinitions[$relationship_specifier]->getTargetDefinition();
277 if (!$entity_type_id && $target_definition instanceof EntityDataDefinitionInterface) {
278 $entity_type_id = $target_definition->getEntityTypeId();
280 $entity_type = $this->entityManager->getDefinition($entity_type_id);
281 $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
282 // Add the new entity base table using the table and sql column.
283 $base_table = $this->addNextBaseTable($entity_type, $table, $sql_column, $field_storage);
284 $propertyDefinitions = [];
286 $index_prefix .= "$next_index_prefix.";
289 throw new QueryException("Invalid specifier '$relationship_specifier'");
293 return "$table.$sql_column";
299 public function isFieldCaseSensitive($field_name) {
300 if (isset($this->caseSensitiveFields[$field_name])) {
301 return $this->caseSensitiveFields[$field_name];
306 * Joins the entity table, if necessary, and returns the alias for it.
308 * @param string $index_prefix
309 * The table array index prefix. For a base table this will be empty,
310 * for a target entity reference like 'field_tags.entity:taxonomy_term.name'
311 * this will be 'entity:taxonomy_term.target_id.'.
312 * @param string $property
313 * The field property/column.
314 * @param string $type
315 * The join type, can either be INNER or LEFT.
316 * @param string $langcode
317 * The langcode we use on the join.
318 * @param string $base_table
319 * The table to join to. It can be either the table name, its alias or the
320 * 'base_table' placeholder.
321 * @param string $id_field
322 * The name of the ID field/property for the current entity. For instance:
324 * @param array $entity_tables
325 * Array of entity tables (data and base tables) where decide the entity
326 * property will be queried from. The first table containing the property
327 * will be used, so the order is important and the data table is always
331 * The alias of the joined table.
333 * @throws \Drupal\Core\Entity\Query\QueryException
334 * When an invalid property has been passed.
336 protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
337 foreach ($entity_tables as $table => $mapping) {
338 if (isset($mapping[$property])) {
339 // Ensure a table joined multiple times through different index prefixes
340 // has unique entityTables entries by concatenating the index prefix
341 // and the base table alias. In this way i.e. if we join to the same
342 // entity table several times for different entity reference fields,
343 // each join gets a separate alias.
344 $key = $index_prefix . ($base_table === 'base_table' ? $table : $base_table);
345 if (!isset($this->entityTables[$key])) {
346 $this->entityTables[$key] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode);
348 return $this->entityTables[$key];
351 throw new QueryException("'$property' not found");
355 * Join field table if necessary.
360 * @throws \Drupal\Core\Entity\Query\QueryException
362 protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta) {
363 $field_name = $field->getName();
364 if (!isset($this->fieldTables[$index_prefix . $field_name])) {
365 $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
366 /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
367 $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
368 $table = !$this->sqlQuery->getMetaData('all_revisions') ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field);
369 if ($field->getCardinality() != 1) {
370 $this->sqlQuery->addMetaData('simple_query', FALSE);
372 $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode, $delta);
374 return $this->fieldTables[$index_prefix . $field_name];
378 * Adds a join to a given table.
380 * @param string $type
382 * @param string $table
383 * The table to join to.
384 * @param string $join_condition
385 * The condition on which to join to.
386 * @param string $langcode
387 * The langcode we use on the join.
388 * @param string|null $delta
389 * (optional) A delta which should be used as additional condition.
392 * Returns the alias of the joined table.
394 protected function addJoin($type, $table, $join_condition, $langcode, $delta = NULL) {
397 $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
398 $entity_type = $this->entityManager->getDefinition($entity_type_id);
399 // Only the data table follows the entity language key, dedicated field
400 // tables have an hard-coded 'langcode' column.
401 $langcode_key = $entity_type->getDataTable() == $table ? $entity_type->getKey('langcode') : 'langcode';
402 $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder();
403 $join_condition .= ' AND %alias.' . $langcode_key . ' = ' . $placeholder;
404 $arguments[$placeholder] = $langcode;
407 $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder();
408 $join_condition .= ' AND %alias.delta = ' . $placeholder;
409 $arguments[$placeholder] = $delta;
411 return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments);
415 * Gets the schema for the given table.
417 * @param string $table
420 * @return array|false
421 * An associative array of table field mapping for the given table, keyed by
422 * columns name and values are just incrementing integers. If the table
423 * mapping is not available, FALSE is returned.
425 protected function getTableMapping($table, $entity_type_id) {
426 $storage = $this->entityManager->getStorage($entity_type_id);
427 if ($storage instanceof SqlEntityStorageInterface) {
428 $mapping = $storage->getTableMapping()->getAllColumns($table);
433 return array_flip($mapping);
437 * Add the next entity base table.
439 * For example, when building the SQL query for
441 * condition('uid.entity.name', 'foo', 'CONTAINS')
444 * this adds the users table.
446 * @param \Drupal\Core\Entity\EntityType $entity_type
447 * The entity type being joined, in the above example, User.
448 * @param string $table
449 * This is the table being joined, in the above example, {users}.
450 * @param string $sql_column
451 * This is the SQL column in the existing table being joined to.
452 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage
453 * The field storage definition for the field referencing this column.
456 * The alias of the next entity table joined in.
458 protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column, FieldStorageDefinitionInterface $field_storage) {
459 $join_condition = '%alias.' . $entity_type->getKey('id') . " = $table.$sql_column";
460 return $this->sqlQuery->leftJoin($entity_type->getBaseTable(), NULL, $join_condition);