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\TypedData\DataReferenceDefinitionInterface;
14 * Adds tables and fields to the SQL entity query.
16 class Tables implements TablesInterface {
19 * @var \Drupal\Core\Database\Query\SelectInterface
24 * Entity table array, key is table name, value is alias.
26 * This array contains at most two entries: one for the data, one for the
31 protected $entityTables = [];
34 * Field table array, key is table name, value is alias.
36 * This array contains one entry per field table.
40 protected $fieldTables = [];
45 * @var \Drupal\Core\Entity\EntityManager
47 protected $entityManager;
50 * List of case sensitive fields.
54 protected $caseSensitiveFields = [];
57 * @param \Drupal\Core\Database\Query\SelectInterface $sql_query
59 public function __construct(SelectInterface $sql_query) {
60 $this->sqlQuery = $sql_query;
61 $this->entityManager = \Drupal::entityManager();
67 public function addField($field, $type, $langcode) {
68 $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
69 $all_revisions = $this->sqlQuery->getMetaData('all_revisions');
70 // This variable ensures grouping works correctly. For example:
71 // ->condition('tags', 2, '>')
72 // ->condition('tags', 20, '<')
73 // ->condition('node_reference.nid.entity.tags', 2)
74 // The first two should use the same table but the last one needs to be a
75 // new table. So for the first two, the table array index will be 'tags'
76 // while the third will be 'node_reference.nid.tags'.
78 $specifiers = explode('.', $field);
79 $base_table = 'base_table';
80 $count = count($specifiers) - 1;
81 // This will contain the definitions of the last specifier seen by the
83 $propertyDefinitions = [];
84 $entity_type = $this->entityManager->getDefinition($entity_type_id);
86 $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
87 for ($key = 0; $key <= $count; $key++) {
88 // This can either be the name of an entity base field or a configurable
90 $specifier = $specifiers[$key];
91 if (isset($field_storage_definitions[$specifier])) {
92 $field_storage = $field_storage_definitions[$specifier];
95 $field_storage = FALSE;
98 // If there is revision support, only the current revisions are being
99 // queried, and the field is revisionable then use the revision id.
100 // Otherwise, the entity id will do.
101 if (($revision_key = $entity_type->getKey('revision')) && $all_revisions && $field_storage && $field_storage->isRevisionable()) {
102 // This contains the relevant SQL field to be used when joining entity
104 $entity_id_field = $revision_key;
105 // This contains the relevant SQL field to be used when joining field
107 $field_id_field = 'revision_id';
110 $entity_id_field = $entity_type->getKey('id');
111 $field_id_field = 'entity_id';
114 /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
115 $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
117 // Check whether this field is stored in a dedicated table.
118 if ($field_storage && $table_mapping->requiresDedicatedTableStorage($field_storage)) {
120 // Find the field column.
121 $column = $field_storage->getMainPropertyName();
124 $next = $specifiers[$key + 1];
125 // If this is a numeric specifier we're adding a condition on the
127 if (is_numeric($next)) {
129 $index_prefix .= ".$delta";
130 // Do not process it again.
132 $next = $specifiers[$key + 1];
134 // If this specifier is the reserved keyword "%delta" we're adding a
135 // condition on a delta range.
136 elseif ($next == TableMappingInterface::DELTA) {
137 $index_prefix .= TableMappingInterface::DELTA;
138 // Do not process it again.
140 // If there are more specifiers to work with then continue
141 // processing. If this is the last specifier then use the reserved
142 // keyword as a column name.
144 $next = $specifiers[$key + 1];
147 $column = TableMappingInterface::DELTA;
150 // Is this a field column?
151 $columns = $field_storage->getColumns();
152 if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
155 // Do not process it again.
158 // If there are more specifiers, the next one must be a
159 // relationship. Either the field name followed by a relationship
160 // specifier, for example $node->field_image->entity. Or a field
161 // column followed by a relationship specifier, for example
162 // $node->field_image->fid->entity. In both cases, prepare the
163 // property definitions for the relationship. In the first case,
164 // also use the property definitions for column.
166 $relationship_specifier = $specifiers[$key + 1];
167 $propertyDefinitions = $field_storage->getPropertyDefinitions();
169 // Prepare the next index prefix.
170 $next_index_prefix = "$relationship_specifier.$column";
173 $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta);
174 $sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
175 $property_definitions = $field_storage->getPropertyDefinitions();
176 if (isset($property_definitions[$column])) {
177 $this->caseSensitiveFields[$field] = $property_definitions[$column]->getSetting('case_sensitive');
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 if ($all_revisions && $field_storage && $field_storage->isRevisionable()) {
188 $data_table = $entity_type->getRevisionDataTable();
189 $entity_base_table = $entity_type->getRevisionTable();
192 $data_table = $entity_type->getDataTable();
193 $entity_base_table = $entity_type->getBaseTable();
196 $this->sqlQuery->addMetaData('simple_query', FALSE);
197 $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
199 $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
200 $sql_column = $specifier;
202 // If there are more specifiers, get the right sql column name if the
203 // next one is a column of this field.
205 $next = $specifiers[$key + 1];
206 // If this specifier is the reserved keyword "%delta" we're adding a
207 // condition on a delta range.
208 if ($next == TableMappingInterface::DELTA) {
211 $next = $specifiers[$key + 1];
217 // If this is a numeric specifier we're adding a condition on the
218 // specific delta. Since we know that this is a single value base
219 // field no other value than 0 makes sense.
220 if (is_numeric($next)) {
222 $this->sqlQuery->condition('1 <> 1');
225 $next = $specifiers[$key + 1];
227 // Is this a field column?
228 $columns = $field_storage->getColumns();
229 if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
231 $sql_column = $table_mapping->getFieldColumnName($field_storage, $next);
232 // Do not process it again.
237 $table = $this->ensureEntityTable($index_prefix, $sql_column, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
239 // If there is a field storage (some specifiers are not), check for case
241 if ($field_storage) {
242 $column = $field_storage->getMainPropertyName();
243 $base_field_property_definitions = $field_storage->getPropertyDefinitions();
244 if (isset($base_field_property_definitions[$column])) {
245 $this->caseSensitiveFields[$field] = $base_field_property_definitions[$column]->getSetting('case_sensitive');
250 // If there are more specifiers to come, it's a relationship.
251 if ($field_storage && $key < $count) {
252 // Computed fields have prepared their property definition already, do
253 // it for properties as well.
254 if (!$propertyDefinitions) {
255 $propertyDefinitions = $field_storage->getPropertyDefinitions();
256 $relationship_specifier = $specifiers[$key + 1];
257 $next_index_prefix = $relationship_specifier;
259 $entity_type_id = NULL;
260 // Relationship specifier can also contain the entity type ID, i.e.
261 // entity:node, entity:user or entity:taxonomy.
262 if (strpos($relationship_specifier, ':') !== FALSE) {
263 list($relationship_specifier, $entity_type_id) = explode(':', $relationship_specifier, 2);
265 // Check for a valid relationship.
266 if (isset($propertyDefinitions[$relationship_specifier]) && $propertyDefinitions[$relationship_specifier] instanceof DataReferenceDefinitionInterface) {
267 // If it is, use the entity type if specified already, otherwise use
269 $target_definition = $propertyDefinitions[$relationship_specifier]->getTargetDefinition();
270 if (!$entity_type_id && $target_definition instanceof EntityDataDefinitionInterface) {
271 $entity_type_id = $target_definition->getEntityTypeId();
273 $entity_type = $this->entityManager->getDefinition($entity_type_id);
274 $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
275 // Add the new entity base table using the table and sql column.
276 $base_table = $this->addNextBaseTable($entity_type, $table, $sql_column);
277 $propertyDefinitions = [];
279 $index_prefix .= "$next_index_prefix.";
282 throw new QueryException("Invalid specifier '$relationship_specifier'");
286 return "$table.$sql_column";
292 public function isFieldCaseSensitive($field_name) {
293 if (isset($this->caseSensitiveFields[$field_name])) {
294 return $this->caseSensitiveFields[$field_name];
299 * Join entity table if necessary and return the alias for it.
301 * @param string $property
305 * @throws \Drupal\Core\Entity\Query\QueryException
307 protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
308 foreach ($entity_tables as $table => $mapping) {
309 if (isset($mapping[$property])) {
310 if (!isset($this->entityTables[$index_prefix . $table])) {
311 $this->entityTables[$index_prefix . $table] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode);
313 return $this->entityTables[$index_prefix . $table];
316 throw new QueryException("'$property' not found");
320 * Join field table if necessary.
325 * @throws \Drupal\Core\Entity\Query\QueryException
327 protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta) {
328 $field_name = $field->getName();
329 if (!isset($this->fieldTables[$index_prefix . $field_name])) {
330 $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
331 /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
332 $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
333 $table = !$this->sqlQuery->getMetaData('all_revisions') ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field);
334 if ($field->getCardinality() != 1) {
335 $this->sqlQuery->addMetaData('simple_query', FALSE);
337 $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode, $delta);
339 return $this->fieldTables[$index_prefix . $field_name];
342 protected function addJoin($type, $table, $join_condition, $langcode, $delta = NULL) {
345 $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
346 $entity_type = $this->entityManager->getDefinition($entity_type_id);
347 // Only the data table follows the entity language key, dedicated field
348 // tables have an hard-coded 'langcode' column.
349 $langcode_key = $entity_type->getDataTable() == $table ? $entity_type->getKey('langcode') : 'langcode';
350 $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder();
351 $join_condition .= ' AND %alias.' . $langcode_key . ' = ' . $placeholder;
352 $arguments[$placeholder] = $langcode;
355 $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder();
356 $join_condition .= ' AND %alias.delta = ' . $placeholder;
357 $arguments[$placeholder] = $delta;
359 return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments);
363 * Gets the schema for the given table.
365 * @param string $table
369 * The table field mapping for the given table or FALSE if not available.
371 protected function getTableMapping($table, $entity_type_id) {
372 $storage = $this->entityManager->getStorage($entity_type_id);
373 if ($storage instanceof SqlEntityStorageInterface) {
374 $mapping = $storage->getTableMapping()->getAllColumns($table);
379 return array_flip($mapping);
383 * Add the next entity base table.
385 * For example, when building the SQL query for
387 * condition('uid.entity.name', 'foo', 'CONTAINS')
390 * this adds the users table.
392 * @param \Drupal\Core\Entity\EntityType $entity_type
393 * The entity type being joined, in the above example, User.
394 * @param string $table
395 * This is the table being joined, in the above example, {users}.
396 * @param string $sql_column
397 * This is the SQL column in the existing table being joined to.
400 * The alias of the next entity table joined in.
402 protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column) {
403 $join_condition = '%alias.' . $entity_type->getKey('id') . " = $table.$sql_column";
404 return $this->sqlQuery->leftJoin($entity_type->getBaseTable(), NULL, $join_condition);