Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Entity / Sql / SqlContentEntityStorage.php
1 <?php
2
3 namespace Drupal\Core\Entity\Sql;
4
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\Database\Database;
8 use Drupal\Core\Database\DatabaseExceptionWrapper;
9 use Drupal\Core\Database\SchemaException;
10 use Drupal\Core\Entity\ContentEntityInterface;
11 use Drupal\Core\Entity\ContentEntityStorageBase;
12 use Drupal\Core\Entity\EntityBundleListenerInterface;
13 use Drupal\Core\Entity\EntityInterface;
14 use Drupal\Core\Entity\EntityManagerInterface;
15 use Drupal\Core\Entity\EntityStorageException;
16 use Drupal\Core\Entity\EntityTypeInterface;
17 use Drupal\Core\Entity\Query\QueryInterface;
18 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
19 use Drupal\Core\Field\FieldDefinitionInterface;
20 use Drupal\Core\Field\FieldStorageDefinitionInterface;
21 use Drupal\Core\Language\LanguageInterface;
22 use Drupal\field\FieldStorageConfigInterface;
23 use Drupal\Core\Language\LanguageManagerInterface;
24 use Symfony\Component\DependencyInjection\ContainerInterface;
25
26 /**
27  * A content entity database storage implementation.
28  *
29  * This class can be used as-is by most content entity types. Entity types
30  * requiring special handling can extend the class.
31  *
32  * The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
33  * internally in order to automatically generate the database schema based on
34  * the defined base fields. Entity types can override the schema handler to
35  * customize the generated schema; e.g., to add additional indexes.
36  *
37  * @ingroup entity_api
38  */
39 class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
40
41   /**
42    * The mapping of field columns to SQL tables.
43    *
44    * @var \Drupal\Core\Entity\Sql\TableMappingInterface
45    */
46   protected $tableMapping;
47
48   /**
49    * Name of entity's revision database table field, if it supports revisions.
50    *
51    * Has the value FALSE if this entity does not use revisions.
52    *
53    * @var string
54    */
55   protected $revisionKey = FALSE;
56
57   /**
58    * The entity langcode key.
59    *
60    * @var string|bool
61    */
62   protected $langcodeKey = FALSE;
63
64   /**
65    * The default language entity key.
66    *
67    * @var string
68    */
69   protected $defaultLangcodeKey = FALSE;
70
71   /**
72    * The base table of the entity.
73    *
74    * @var string
75    */
76   protected $baseTable;
77
78   /**
79    * The table that stores revisions, if the entity supports revisions.
80    *
81    * @var string
82    */
83   protected $revisionTable;
84
85   /**
86    * The table that stores properties, if the entity has multilingual support.
87    *
88    * @var string
89    */
90   protected $dataTable;
91
92   /**
93    * The table that stores revision field data if the entity supports revisions.
94    *
95    * @var string
96    */
97   protected $revisionDataTable;
98
99   /**
100    * Active database connection.
101    *
102    * @var \Drupal\Core\Database\Connection
103    */
104   protected $database;
105
106   /**
107    * The entity type's storage schema object.
108    *
109    * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
110    */
111   protected $storageSchema;
112
113   /**
114    * The language manager.
115    *
116    * @var \Drupal\Core\Language\LanguageManagerInterface
117    */
118   protected $languageManager;
119
120   /**
121    * Whether this storage should use the temporary table mapping.
122    *
123    * @var bool
124    */
125   protected $temporary = FALSE;
126
127   /**
128    * {@inheritdoc}
129    */
130   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
131     return new static(
132       $entity_type,
133       $container->get('database'),
134       $container->get('entity.manager'),
135       $container->get('cache.entity'),
136       $container->get('language_manager')
137     );
138   }
139
140   /**
141    * Gets the base field definitions for a content entity type.
142    *
143    * @return \Drupal\Core\Field\FieldDefinitionInterface[]
144    *   The array of base field definitions for the entity type, keyed by field
145    *   name.
146    */
147   public function getFieldStorageDefinitions() {
148     return $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
149   }
150
151   /**
152    * Constructs a SqlContentEntityStorage object.
153    *
154    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
155    *   The entity type definition.
156    * @param \Drupal\Core\Database\Connection $database
157    *   The database connection to be used.
158    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
159    *   The entity manager.
160    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
161    *   The cache backend to be used.
162    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
163    *   The language manager.
164    */
165   public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
166     parent::__construct($entity_type, $entity_manager, $cache);
167     $this->database = $database;
168     $this->languageManager = $language_manager;
169     $this->initTableLayout();
170   }
171
172   /**
173    * Initializes table name variables.
174    */
175   protected function initTableLayout() {
176     // Reset table field values to ensure changes in the entity type definition
177     // are correctly reflected in the table layout.
178     $this->tableMapping = NULL;
179     $this->revisionKey = NULL;
180     $this->revisionTable = NULL;
181     $this->dataTable = NULL;
182     $this->revisionDataTable = NULL;
183
184     // @todo Remove table names from the entity type definition in
185     //   https://www.drupal.org/node/2232465.
186     $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
187     $revisionable = $this->entityType->isRevisionable();
188     if ($revisionable) {
189       $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
190       $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision';
191     }
192     $translatable = $this->entityType->isTranslatable();
193     if ($translatable) {
194       $this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
195       $this->langcodeKey = $this->entityType->getKey('langcode');
196       $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
197     }
198     if ($revisionable && $translatable) {
199       $this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
200     }
201   }
202
203   /**
204    * Gets the base table name.
205    *
206    * @return string
207    *   The table name.
208    */
209   public function getBaseTable() {
210     return $this->baseTable;
211   }
212
213   /**
214    * Gets the revision table name.
215    *
216    * @return string|false
217    *   The table name or FALSE if it is not available.
218    */
219   public function getRevisionTable() {
220     return $this->revisionTable;
221   }
222
223   /**
224    * Gets the data table name.
225    *
226    * @return string|false
227    *   The table name or FALSE if it is not available.
228    */
229   public function getDataTable() {
230     return $this->dataTable;
231   }
232
233   /**
234    * Gets the revision data table name.
235    *
236    * @return string|false
237    *   The table name or FALSE if it is not available.
238    */
239   public function getRevisionDataTable() {
240     return $this->revisionDataTable;
241   }
242
243   /**
244    * Gets the entity type's storage schema object.
245    *
246    * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
247    *   The schema object.
248    */
249   protected function getStorageSchema() {
250     if (!isset($this->storageSchema)) {
251       $class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
252       $this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
253     }
254     return $this->storageSchema;
255   }
256
257   /**
258    * Updates the wrapped entity type definition.
259    *
260    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
261    *   The update entity type.
262    *
263    * @internal Only to be used internally by Entity API. Expected to be
264    *   removed by https://www.drupal.org/node/2274017.
265    */
266   public function setEntityType(EntityTypeInterface $entity_type) {
267     if ($this->entityType->id() == $entity_type->id()) {
268       $this->entityType = $entity_type;
269       $this->initTableLayout();
270     }
271     else {
272       throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
273     }
274   }
275
276   /**
277    * Sets the wrapped table mapping definition.
278    *
279    * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
280    *   The table mapping.
281    *
282    * @internal Only to be used internally by Entity API. Expected to be removed
283    *   by https://www.drupal.org/node/2554235.
284    */
285   public function setTableMapping(TableMappingInterface $table_mapping) {
286     $this->tableMapping = $table_mapping;
287   }
288
289   /**
290    * Changes the temporary state of the storage.
291    *
292    * @param bool $temporary
293    *   Whether to use a temporary table mapping or not.
294    *
295    * @internal Only to be used internally by Entity API.
296    */
297   public function setTemporary($temporary) {
298     $this->temporary = $temporary;
299   }
300
301   /**
302    * {@inheritdoc}
303    */
304   public function getTableMapping(array $storage_definitions = NULL) {
305     $table_mapping = $this->tableMapping;
306
307     // If we are using our internal storage definitions, which is our main use
308     // case, we can statically cache the computed table mapping. If a new set
309     // of field storage definitions is passed, for instance when comparing old
310     // and new storage schema, we compute the table mapping without caching.
311     // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
312     //   easily instantiate a new table mapping whenever needed.
313     if (!isset($this->tableMapping) || $storage_definitions) {
314       $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
315       $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
316       /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
317       $table_mapping = new $table_mapping_class($this->entityType, $definitions);
318
319       $shared_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
320         return $table_mapping->allowsSharedTableStorage($definition);
321       });
322
323       $key_fields = array_values(array_filter([$this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey]));
324       $all_fields = array_keys($shared_table_definitions);
325       $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
326         return $definition->isRevisionable();
327       }));
328       // Make sure the key fields come first in the list of fields.
329       $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
330
331       // If the entity is revisionable, gather the fields that need to be put
332       // in the revision table.
333       $revisionable = $this->entityType->isRevisionable();
334       $revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
335
336       $translatable = $this->entityType->isTranslatable();
337       if (!$revisionable && !$translatable) {
338         // The base layout stores all the base field values in the base table.
339         $table_mapping->setFieldNames($this->baseTable, $all_fields);
340       }
341       elseif ($revisionable && !$translatable) {
342         // The revisionable layout stores all the base field values in the base
343         // table, except for revision metadata fields. Revisionable fields
344         // denormalized in the base table but also stored in the revision table
345         // together with the entity ID and the revision ID as identifiers.
346         $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
347         $revision_key_fields = [$this->idKey, $this->revisionKey];
348         $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
349       }
350       elseif (!$revisionable && $translatable) {
351         // Multilingual layouts store key field values in the base table. The
352         // other base field values are stored in the data table, no matter
353         // whether they are translatable or not. The data table holds also a
354         // denormalized copy of the bundle field value to allow for more
355         // performant queries. This means that only the UUID is not stored on
356         // the data table.
357         $table_mapping
358           ->setFieldNames($this->baseTable, $key_fields)
359           ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, [$this->uuidKey])));
360       }
361       elseif ($revisionable && $translatable) {
362         // The revisionable multilingual layout stores key field values in the
363         // base table, except for language, which is stored in the revision
364         // table along with revision metadata. The revision data table holds
365         // data field values for all the revisionable fields and the data table
366         // holds the data field values for all non-revisionable fields. The data
367         // field values of revisionable fields are denormalized in the data
368         // table, as well.
369         $table_mapping->setFieldNames($this->baseTable, array_values($key_fields));
370
371         // Like in the multilingual, non-revisionable case the UUID is not
372         // in the data table. Additionally, do not store revision metadata
373         // fields in the data table.
374         $data_fields = array_values(array_diff($all_fields, [$this->uuidKey], $revision_metadata_fields));
375         $table_mapping->setFieldNames($this->dataTable, $data_fields);
376
377         $revision_base_fields = array_merge([$this->idKey, $this->revisionKey, $this->langcodeKey], $revision_metadata_fields);
378         $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
379
380         $revision_data_key_fields = [$this->idKey, $this->revisionKey, $this->langcodeKey];
381         $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$this->langcodeKey]);
382         $table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
383       }
384
385       // Add dedicated tables.
386       $dedicated_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
387         return $table_mapping->requiresDedicatedTableStorage($definition);
388       });
389       $extra_columns = [
390         'bundle',
391         'deleted',
392         'entity_id',
393         'revision_id',
394         'langcode',
395         'delta',
396       ];
397       foreach ($dedicated_table_definitions as $field_name => $definition) {
398         $tables = [$table_mapping->getDedicatedDataTableName($definition)];
399         if ($revisionable && $definition->isRevisionable()) {
400           $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
401         }
402         foreach ($tables as $table_name) {
403           $table_mapping->setFieldNames($table_name, [$field_name]);
404           $table_mapping->setExtraColumns($table_name, $extra_columns);
405         }
406       }
407
408       // Cache the computed table mapping only if we are using our internal
409       // storage definitions.
410       if (!$storage_definitions) {
411         $this->tableMapping = $table_mapping;
412       }
413     }
414
415     return $table_mapping;
416   }
417
418   /**
419    * {@inheritdoc}
420    */
421   protected function doLoadMultiple(array $ids = NULL) {
422     // Attempt to load entities from the persistent cache. This will remove IDs
423     // that were loaded from $ids.
424     $entities_from_cache = $this->getFromPersistentCache($ids);
425
426     // Load any remaining entities from the database.
427     if ($entities_from_storage = $this->getFromStorage($ids)) {
428       $this->invokeStorageLoadHook($entities_from_storage);
429       $this->setPersistentCache($entities_from_storage);
430     }
431
432     return $entities_from_cache + $entities_from_storage;
433   }
434
435   /**
436    * Gets entities from the storage.
437    *
438    * @param array|null $ids
439    *   If not empty, return entities that match these IDs. Return all entities
440    *   when NULL.
441    *
442    * @return \Drupal\Core\Entity\ContentEntityInterface[]
443    *   Array of entities from the storage.
444    */
445   protected function getFromStorage(array $ids = NULL) {
446     $entities = [];
447
448     if (!empty($ids)) {
449       // Sanitize IDs. Before feeding ID array into buildQuery, check whether
450       // it is empty as this would load all entities.
451       $ids = $this->cleanIds($ids);
452     }
453
454     if ($ids === NULL || $ids) {
455       // Build and execute the query.
456       $query_result = $this->buildQuery($ids)->execute();
457       $records = $query_result->fetchAllAssoc($this->idKey);
458
459       // Map the loaded records into entity objects and according fields.
460       if ($records) {
461         $entities = $this->mapFromStorageRecords($records);
462       }
463     }
464
465     return $entities;
466   }
467
468   /**
469    * Maps from storage records to entity objects, and attaches fields.
470    *
471    * @param array $records
472    *   Associative array of query results, keyed on the entity ID.
473    * @param bool $load_from_revision
474    *   Flag to indicate whether revisions should be loaded or not.
475    *
476    * @return array
477    *   An array of entity objects implementing the EntityInterface.
478    */
479   protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
480     if (!$records) {
481       return [];
482     }
483
484     $values = [];
485     foreach ($records as $id => $record) {
486       $values[$id] = [];
487       // Skip the item delta and item value levels (if possible) but let the
488       // field assign the value as suiting. This avoids unnecessary array
489       // hierarchies and saves memory here.
490       foreach ($record as $name => $value) {
491         // Handle columns named [field_name]__[column_name] (e.g for field types
492         // that store several properties).
493         if ($field_name = strstr($name, '__', TRUE)) {
494           $property_name = substr($name, strpos($name, '__') + 2);
495           $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
496         }
497         else {
498           // Handle columns named directly after the field (e.g if the field
499           // type only stores one property).
500           $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
501         }
502       }
503     }
504
505     // Initialize translations array.
506     $translations = array_fill_keys(array_keys($values), []);
507
508     // Load values from shared and dedicated tables.
509     $this->loadFromSharedTables($values, $translations);
510     $this->loadFromDedicatedTables($values, $load_from_revision);
511
512     $entities = [];
513     foreach ($values as $id => $entity_values) {
514       $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
515       // Turn the record into an entity class.
516       $entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
517     }
518
519     return $entities;
520   }
521
522   /**
523    * Loads values for fields stored in the shared data tables.
524    *
525    * @param array &$values
526    *   Associative array of entities values, keyed on the entity ID.
527    * @param array &$translations
528    *   List of translations, keyed on the entity ID.
529    */
530   protected function loadFromSharedTables(array &$values, array &$translations) {
531     if ($this->dataTable) {
532       // If a revision table is available, we need all the properties of the
533       // latest revision. Otherwise we fall back to the data table.
534       $table = $this->revisionDataTable ?: $this->dataTable;
535       $alias = $this->revisionDataTable ? 'revision' : 'data';
536       $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
537         ->fields($alias)
538         ->condition($alias . '.' . $this->idKey, array_keys($values), 'IN')
539         ->orderBy($alias . '.' . $this->idKey);
540
541       $table_mapping = $this->getTableMapping();
542       if ($this->revisionDataTable) {
543         // Find revisioned fields that are not entity keys. Exclude the langcode
544         // key as the base table holds only the default language.
545         $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]);
546         $revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
547
548         // Find fields that are not revisioned or entity keys. Data fields have
549         // the same value regardless of entity revision.
550         $data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $revisioned_fields, $base_fields);
551         // If there are no data fields then only revisioned fields are needed
552         // else both data fields and revisioned fields are needed to map the
553         // entity values.
554         $all_fields = $revisioned_fields;
555         if ($data_fields) {
556           $all_fields = array_merge($revisioned_fields, $data_fields);
557           $query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
558           $column_names = [];
559           // Some fields can have more then one columns in the data table so
560           // column names are needed.
561           foreach ($data_fields as $data_field) {
562             // \Drupal\Core\Entity\Sql\TableMappingInterface:: getColumNames()
563             // returns an array keyed by property names so remove the keys
564             // before array_merge() to avoid losing data with fields having the
565             // same columns i.e. value.
566             $column_names = array_merge($column_names, array_values($table_mapping->getColumnNames($data_field)));
567           }
568           $query->fields('data', $column_names);
569         }
570
571         // Get the revision IDs.
572         $revision_ids = [];
573         foreach ($values as $entity_values) {
574           $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
575         }
576         $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
577       }
578       else {
579         $all_fields = $table_mapping->getFieldNames($this->dataTable);
580       }
581
582       $result = $query->execute();
583       foreach ($result as $row) {
584         $id = $row[$this->idKey];
585
586         // Field values in default language are stored with
587         // LanguageInterface::LANGCODE_DEFAULT as key.
588         $langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
589
590         $translations[$id][$langcode] = TRUE;
591
592         foreach ($all_fields as $field_name) {
593           $columns = $table_mapping->getColumnNames($field_name);
594           // Do not key single-column fields by property name.
595           if (count($columns) == 1) {
596             $values[$id][$field_name][$langcode] = $row[reset($columns)];
597           }
598           else {
599             foreach ($columns as $property_name => $column_name) {
600               $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
601             }
602           }
603         }
604       }
605     }
606   }
607
608   /**
609    * {@inheritdoc}
610    */
611   protected function doLoadRevisionFieldItems($revision_id) {
612     $revision = NULL;
613
614     // Build and execute the query.
615     $query_result = $this->buildQuery([], $revision_id)->execute();
616     $records = $query_result->fetchAllAssoc($this->idKey);
617
618     if (!empty($records)) {
619       // Convert the raw records to entity objects.
620       $entities = $this->mapFromStorageRecords($records, TRUE);
621       $revision = reset($entities) ?: NULL;
622     }
623
624     return $revision;
625   }
626
627   /**
628    * {@inheritdoc}
629    */
630   protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
631     $this->database->delete($this->revisionTable)
632       ->condition($this->revisionKey, $revision->getRevisionId())
633       ->execute();
634
635     if ($this->revisionDataTable) {
636       $this->database->delete($this->revisionDataTable)
637         ->condition($this->revisionKey, $revision->getRevisionId())
638         ->execute();
639     }
640
641     $this->deleteRevisionFromDedicatedTables($revision);
642   }
643
644   /**
645    * {@inheritdoc}
646    */
647   protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
648     if ($this->dataTable) {
649       // @todo We should not be using a condition to specify whether conditions
650       //   apply to the default language. See
651       //   https://www.drupal.org/node/1866330.
652       // Default to the original entity language if not explicitly specified
653       // otherwise.
654       if (!array_key_exists($this->defaultLangcodeKey, $values)) {
655         $values[$this->defaultLangcodeKey] = 1;
656       }
657       // If the 'default_langcode' flag is explicitly not set, we do not care
658       // whether the queried values are in the original entity language or not.
659       elseif ($values[$this->defaultLangcodeKey] === NULL) {
660         unset($values[$this->defaultLangcodeKey]);
661       }
662     }
663
664     parent::buildPropertyQuery($entity_query, $values);
665   }
666
667   /**
668    * Builds the query to load the entity.
669    *
670    * This has full revision support. For entities requiring special queries,
671    * the class can be extended, and the default query can be constructed by
672    * calling parent::buildQuery(). This is usually necessary when the object
673    * being loaded needs to be augmented with additional data from another
674    * table, such as loading node type into comments or vocabulary machine name
675    * into terms, however it can also support $conditions on different tables.
676    * See Drupal\comment\CommentStorage::buildQuery() for an example.
677    *
678    * @param array|null $ids
679    *   An array of entity IDs, or NULL to load all entities.
680    * @param $revision_id
681    *   The ID of the revision to load, or FALSE if this query is asking for the
682    *   most current revision(s).
683    *
684    * @return \Drupal\Core\Database\Query\Select
685    *   A SelectQuery object for loading the entity.
686    */
687   protected function buildQuery($ids, $revision_id = FALSE) {
688     $query = $this->database->select($this->entityType->getBaseTable(), 'base');
689
690     $query->addTag($this->entityTypeId . '_load_multiple');
691
692     if ($revision_id) {
693       $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", [':revisionId' => $revision_id]);
694     }
695     elseif ($this->revisionTable) {
696       $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
697     }
698
699     // Add fields from the {entity} table.
700     $table_mapping = $this->getTableMapping();
701     $entity_fields = $table_mapping->getAllColumns($this->baseTable);
702
703     if ($this->revisionTable) {
704       // Add all fields from the {entity_revision} table.
705       $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
706       $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
707       // The ID field is provided by entity, so remove it.
708       unset($entity_revision_fields[$this->idKey]);
709
710       // Remove all fields from the base table that are also fields by the same
711       // name in the revision table.
712       $entity_field_keys = array_flip($entity_fields);
713       foreach ($entity_revision_fields as $name) {
714         if (isset($entity_field_keys[$name])) {
715           unset($entity_fields[$entity_field_keys[$name]]);
716         }
717       }
718       $query->fields('revision', $entity_revision_fields);
719
720       // Compare revision ID of the base and revision table, if equal then this
721       // is the default revision.
722       $query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
723     }
724
725     $query->fields('base', $entity_fields);
726
727     if ($ids) {
728       $query->condition("base.{$this->idKey}", $ids, 'IN');
729     }
730
731     return $query;
732   }
733
734   /**
735    * {@inheritdoc}
736    */
737   public function delete(array $entities) {
738     if (!$entities) {
739       // If no IDs or invalid IDs were passed, do nothing.
740       return;
741     }
742
743     $transaction = $this->database->startTransaction();
744     try {
745       parent::delete($entities);
746
747       // Ignore replica server temporarily.
748       db_ignore_replica();
749     }
750     catch (\Exception $e) {
751       $transaction->rollBack();
752       watchdog_exception($this->entityTypeId, $e);
753       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
754     }
755   }
756
757   /**
758    * {@inheritdoc}
759    */
760   protected function doDeleteFieldItems($entities) {
761     $ids = array_keys($entities);
762
763     $this->database->delete($this->entityType->getBaseTable())
764       ->condition($this->idKey, $ids, 'IN')
765       ->execute();
766
767     if ($this->revisionTable) {
768       $this->database->delete($this->revisionTable)
769         ->condition($this->idKey, $ids, 'IN')
770         ->execute();
771     }
772
773     if ($this->dataTable) {
774       $this->database->delete($this->dataTable)
775         ->condition($this->idKey, $ids, 'IN')
776         ->execute();
777     }
778
779     if ($this->revisionDataTable) {
780       $this->database->delete($this->revisionDataTable)
781         ->condition($this->idKey, $ids, 'IN')
782         ->execute();
783     }
784
785     foreach ($entities as $entity) {
786       $this->deleteFromDedicatedTables($entity);
787     }
788   }
789
790   /**
791    * {@inheritdoc}
792    */
793   public function save(EntityInterface $entity) {
794     $transaction = $this->database->startTransaction();
795     try {
796       $return = parent::save($entity);
797
798       // Ignore replica server temporarily.
799       db_ignore_replica();
800       return $return;
801     }
802     catch (\Exception $e) {
803       $transaction->rollBack();
804       watchdog_exception($this->entityTypeId, $e);
805       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
806     }
807   }
808
809   /**
810    * {@inheritdoc}
811    */
812   protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
813     $full_save = empty($names);
814     $update = !$full_save || !$entity->isNew();
815
816     if ($full_save) {
817       $shared_table_fields = TRUE;
818       $dedicated_table_fields = TRUE;
819     }
820     else {
821       $table_mapping = $this->getTableMapping();
822       $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
823       $shared_table_fields = FALSE;
824       $dedicated_table_fields = [];
825
826       // Collect the name of fields to be written in dedicated tables and check
827       // whether shared table records need to be updated.
828       foreach ($names as $name) {
829         $storage_definition = $storage_definitions[$name];
830         if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
831           $shared_table_fields = TRUE;
832         }
833         elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
834           $dedicated_table_fields[] = $name;
835         }
836       }
837     }
838
839     // Update shared table records if necessary.
840     if ($shared_table_fields) {
841       $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
842       // Create the storage record to be saved.
843       if ($update) {
844         $default_revision = $entity->isDefaultRevision();
845         if ($default_revision) {
846           $this->database
847             ->update($this->baseTable)
848             ->fields((array) $record)
849             ->condition($this->idKey, $record->{$this->idKey})
850             ->execute();
851         }
852         if ($this->revisionTable) {
853           if ($full_save) {
854             $entity->{$this->revisionKey} = $this->saveRevision($entity);
855           }
856           else {
857             $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
858             $entity->preSaveRevision($this, $record);
859             $this->database
860               ->update($this->revisionTable)
861               ->fields((array) $record)
862               ->condition($this->revisionKey, $record->{$this->revisionKey})
863               ->execute();
864           }
865         }
866         if ($default_revision && $this->dataTable) {
867           $this->saveToSharedTables($entity);
868         }
869         if ($this->revisionDataTable) {
870           $new_revision = $full_save && $entity->isNewRevision();
871           $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
872         }
873       }
874       else {
875         $insert_id = $this->database
876           ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
877           ->fields((array) $record)
878           ->execute();
879         // Even if this is a new entity the ID key might have been set, in which
880         // case we should not override the provided ID. An ID key that is not set
881         // to any value is interpreted as NULL (or DEFAULT) and thus overridden.
882         if (!isset($record->{$this->idKey})) {
883           $record->{$this->idKey} = $insert_id;
884         }
885         $entity->{$this->idKey} = (string) $record->{$this->idKey};
886         if ($this->revisionTable) {
887           $record->{$this->revisionKey} = $this->saveRevision($entity);
888         }
889         if ($this->dataTable) {
890           $this->saveToSharedTables($entity);
891         }
892         if ($this->revisionDataTable) {
893           $this->saveToSharedTables($entity, $this->revisionDataTable);
894         }
895       }
896     }
897
898     // Update dedicated table records if necessary.
899     if ($dedicated_table_fields) {
900       $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
901       $this->saveToDedicatedTables($entity, $update, $names);
902     }
903   }
904
905   /**
906    * {@inheritdoc}
907    */
908   protected function has($id, EntityInterface $entity) {
909     return !$entity->isNew();
910   }
911
912   /**
913    * Saves fields that use the shared tables.
914    *
915    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
916    *   The entity object.
917    * @param string $table_name
918    *   (optional) The table name to save to. Defaults to the data table.
919    * @param bool $new_revision
920    *   (optional) Whether we are dealing with a new revision. By default fetches
921    *   the information from the entity object.
922    */
923   protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
924     if (!isset($table_name)) {
925       $table_name = $this->dataTable;
926     }
927     if (!isset($new_revision)) {
928       $new_revision = $entity->isNewRevision();
929     }
930     $revision = $table_name != $this->dataTable;
931
932     if (!$revision || !$new_revision) {
933       $key = $revision ? $this->revisionKey : $this->idKey;
934       $value = $revision ? $entity->getRevisionId() : $entity->id();
935       // Delete and insert to handle removed values.
936       $this->database->delete($table_name)
937         ->condition($key, $value)
938         ->execute();
939     }
940
941     $query = $this->database->insert($table_name);
942
943     foreach ($entity->getTranslationLanguages() as $langcode => $language) {
944       $translation = $entity->getTranslation($langcode);
945       $record = $this->mapToDataStorageRecord($translation, $table_name);
946       $values = (array) $record;
947       $query
948         ->fields(array_keys($values))
949         ->values($values);
950     }
951
952     $query->execute();
953   }
954
955   /**
956    * Maps from an entity object to the storage record.
957    *
958    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
959    *   The entity object.
960    * @param string $table_name
961    *   (optional) The table name to map records to. Defaults to the base table.
962    *
963    * @return \stdClass
964    *   The record to store.
965    */
966   protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
967     if (!isset($table_name)) {
968       $table_name = $this->baseTable;
969     }
970
971     $record = new \stdClass();
972     $table_mapping = $this->getTableMapping();
973     foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
974
975       if (empty($this->getFieldStorageDefinitions()[$field_name])) {
976         throw new EntityStorageException("Table mapping contains invalid field $field_name.");
977       }
978       $definition = $this->getFieldStorageDefinitions()[$field_name];
979       $columns = $table_mapping->getColumnNames($field_name);
980
981       foreach ($columns as $column_name => $schema_name) {
982         // If there is no main property and only a single column, get all
983         // properties from the first field item and assume that they will be
984         // stored serialized.
985         // @todo Give field types more control over this behavior in
986         //   https://www.drupal.org/node/2232427.
987         if (!$definition->getMainPropertyName() && count($columns) == 1) {
988           $value = ($item = $entity->$field_name->first()) ? $item->getValue() : [];
989         }
990         else {
991           $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
992         }
993         if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
994           $value = serialize($value);
995         }
996
997         // Do not set serial fields if we do not have a value. This supports all
998         // SQL database drivers.
999         // @see https://www.drupal.org/node/2279395
1000         $value = drupal_schema_get_field_value($definition->getSchema()['columns'][$column_name], $value);
1001         if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
1002           $record->$schema_name = $value;
1003         }
1004       }
1005     }
1006
1007     return $record;
1008   }
1009
1010   /**
1011    * Checks whether a field column should be treated as serial.
1012    *
1013    * @param $table_name
1014    *   The name of the table the field column belongs to.
1015    * @param $schema_name
1016    *   The schema name of the field column.
1017    *
1018    * @return bool
1019    *   TRUE if the column is serial, FALSE otherwise.
1020    *
1021    * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
1022    * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
1023    */
1024   protected function isColumnSerial($table_name, $schema_name) {
1025     $result = FALSE;
1026
1027     switch ($table_name) {
1028       case $this->baseTable:
1029         $result = $schema_name == $this->idKey;
1030         break;
1031
1032       case $this->revisionTable:
1033         $result = $schema_name == $this->revisionKey;
1034         break;
1035     }
1036
1037     return $result;
1038   }
1039
1040   /**
1041    * Maps from an entity object to the storage record of the field data.
1042    *
1043    * @param \Drupal\Core\Entity\EntityInterface $entity
1044    *   The entity object.
1045    * @param string $table_name
1046    *   (optional) The table name to map records to. Defaults to the data table.
1047    *
1048    * @return \stdClass
1049    *   The record to store.
1050    */
1051   protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
1052     if (!isset($table_name)) {
1053       $table_name = $this->dataTable;
1054     }
1055     $record = $this->mapToStorageRecord($entity, $table_name);
1056     return $record;
1057   }
1058
1059   /**
1060    * Saves an entity revision.
1061    *
1062    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1063    *   The entity object.
1064    *
1065    * @return int
1066    *   The revision id.
1067    */
1068   protected function saveRevision(ContentEntityInterface $entity) {
1069     $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
1070
1071     $entity->preSaveRevision($this, $record);
1072
1073     if ($entity->isNewRevision()) {
1074       $insert_id = $this->database
1075         ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
1076         ->fields((array) $record)
1077         ->execute();
1078       // Even if this is a new revision, the revision ID key might have been
1079       // set in which case we should not override the provided revision ID.
1080       if (!isset($record->{$this->revisionKey})) {
1081         $record->{$this->revisionKey} = $insert_id;
1082       }
1083       if ($entity->isDefaultRevision()) {
1084         $this->database->update($this->entityType->getBaseTable())
1085           ->fields([$this->revisionKey => $record->{$this->revisionKey}])
1086           ->condition($this->idKey, $record->{$this->idKey})
1087           ->execute();
1088       }
1089     }
1090     else {
1091       $this->database
1092         ->update($this->revisionTable)
1093         ->fields((array) $record)
1094         ->condition($this->revisionKey, $record->{$this->revisionKey})
1095         ->execute();
1096     }
1097
1098     // Make sure to update the new revision key for the entity.
1099     $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
1100
1101     return $record->{$this->revisionKey};
1102   }
1103
1104   /**
1105    * {@inheritdoc}
1106    */
1107   protected function getQueryServiceName() {
1108     return 'entity.query.sql';
1109   }
1110
1111   /**
1112    * Loads values of fields stored in dedicated tables for a group of entities.
1113    *
1114    * @param array &$values
1115    *   An array of values keyed by entity ID.
1116    * @param bool $load_from_revision
1117    *   (optional) Flag to indicate whether revisions should be loaded or not,
1118    *   defaults to FALSE.
1119    */
1120   protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
1121     if (empty($values)) {
1122       return;
1123     }
1124
1125     // Collect entities ids, bundles and languages.
1126     $bundles = [];
1127     $ids = [];
1128     $default_langcodes = [];
1129     foreach ($values as $key => $entity_values) {
1130       $bundles[$this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId] = TRUE;
1131       $ids[] = !$load_from_revision ? $key : $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
1132       if ($this->langcodeKey && isset($entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) {
1133         $default_langcodes[$key] = $entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT];
1134       }
1135     }
1136
1137     // Collect impacted fields.
1138     $storage_definitions = [];
1139     $definitions = [];
1140     $table_mapping = $this->getTableMapping();
1141     foreach ($bundles as $bundle => $v) {
1142       $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
1143       foreach ($definitions[$bundle] as $field_name => $field_definition) {
1144         $storage_definition = $field_definition->getFieldStorageDefinition();
1145         if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1146           $storage_definitions[$field_name] = $storage_definition;
1147         }
1148       }
1149     }
1150
1151     // Load field data.
1152     $langcodes = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
1153     foreach ($storage_definitions as $field_name => $storage_definition) {
1154       $table = !$load_from_revision ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
1155
1156       // Ensure that only values having valid languages are retrieved. Since we
1157       // are loading values for multiple entities, we cannot limit the query to
1158       // the available translations.
1159       $results = $this->database->select($table, 't')
1160         ->fields('t')
1161         ->condition(!$load_from_revision ? 'entity_id' : 'revision_id', $ids, 'IN')
1162         ->condition('deleted', 0)
1163         ->condition('langcode', $langcodes, 'IN')
1164         ->orderBy('delta')
1165         ->execute();
1166
1167       foreach ($results as $row) {
1168         $bundle = $row->bundle;
1169
1170         // Field values in default language are stored with
1171         // LanguageInterface::LANGCODE_DEFAULT as key.
1172         $langcode = LanguageInterface::LANGCODE_DEFAULT;
1173         if ($this->langcodeKey && isset($default_langcodes[$row->entity_id]) && $row->langcode != $default_langcodes[$row->entity_id]) {
1174           $langcode = $row->langcode;
1175         }
1176
1177         if (!isset($values[$row->entity_id][$field_name][$langcode])) {
1178           $values[$row->entity_id][$field_name][$langcode] = [];
1179         }
1180
1181         // Ensure that records for non-translatable fields having invalid
1182         // languages are skipped.
1183         if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
1184           if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$row->entity_id][$field_name][$langcode]) < $storage_definition->getCardinality()) {
1185             $item = [];
1186             // For each column declared by the field, populate the item from the
1187             // prefixed database column.
1188             foreach ($storage_definition->getColumns() as $column => $attributes) {
1189               $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1190               // Unserialize the value if specified in the column schema.
1191               $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
1192             }
1193
1194             // Add the item to the field values for the entity.
1195             $values[$row->entity_id][$field_name][$langcode][] = $item;
1196           }
1197         }
1198       }
1199     }
1200   }
1201
1202   /**
1203    * Saves values of fields that use dedicated tables.
1204    *
1205    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1206    *   The entity.
1207    * @param bool $update
1208    *   TRUE if the entity is being updated, FALSE if it is being inserted.
1209    * @param string[] $names
1210    *   (optional) The names of the fields to be stored. Defaults to all the
1211    *   available fields.
1212    */
1213   protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = []) {
1214     $vid = $entity->getRevisionId();
1215     $id = $entity->id();
1216     $bundle = $entity->bundle();
1217     $entity_type = $entity->getEntityTypeId();
1218     $default_langcode = $entity->getUntranslated()->language()->getId();
1219     $translation_langcodes = array_keys($entity->getTranslationLanguages());
1220     $table_mapping = $this->getTableMapping();
1221
1222     if (!isset($vid)) {
1223       $vid = $id;
1224     }
1225
1226     $original = !empty($entity->original) ? $entity->original : NULL;
1227
1228     // Determine which fields should be actually stored.
1229     $definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
1230     if ($names) {
1231       $definitions = array_intersect_key($definitions, array_flip($names));
1232     }
1233
1234     foreach ($definitions as $field_name => $field_definition) {
1235       $storage_definition = $field_definition->getFieldStorageDefinition();
1236       if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1237         continue;
1238       }
1239
1240       // When updating an existing revision, keep the existing records if the
1241       // field values did not change.
1242       if (!$entity->isNewRevision() && $original && !$this->hasFieldValueChanged($field_definition, $entity, $original)) {
1243         continue;
1244       }
1245
1246       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1247       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1248
1249       // Delete and insert, rather than update, in case a value was added.
1250       if ($update) {
1251         // Only overwrite the field's base table if saving the default revision
1252         // of an entity.
1253         if ($entity->isDefaultRevision()) {
1254           $this->database->delete($table_name)
1255             ->condition('entity_id', $id)
1256             ->execute();
1257         }
1258         if ($this->entityType->isRevisionable()) {
1259           $this->database->delete($revision_name)
1260             ->condition('entity_id', $id)
1261             ->condition('revision_id', $vid)
1262             ->execute();
1263         }
1264       }
1265
1266       // Prepare the multi-insert query.
1267       $do_insert = FALSE;
1268       $columns = ['entity_id', 'revision_id', 'bundle', 'delta', 'langcode'];
1269       foreach ($storage_definition->getColumns() as $column => $attributes) {
1270         $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
1271       }
1272       $query = $this->database->insert($table_name)->fields($columns);
1273       if ($this->entityType->isRevisionable()) {
1274         $revision_query = $this->database->insert($revision_name)->fields($columns);
1275       }
1276
1277       $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : [$default_langcode];
1278       foreach ($langcodes as $langcode) {
1279         $delta_count = 0;
1280         $items = $entity->getTranslation($langcode)->get($field_name);
1281         $items->filterEmptyItems();
1282         foreach ($items as $delta => $item) {
1283           // We now know we have something to insert.
1284           $do_insert = TRUE;
1285           $record = [
1286             'entity_id' => $id,
1287             'revision_id' => $vid,
1288             'bundle' => $bundle,
1289             'delta' => $delta,
1290             'langcode' => $langcode,
1291           ];
1292           foreach ($storage_definition->getColumns() as $column => $attributes) {
1293             $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1294             // Serialize the value if specified in the column schema.
1295             $value = $item->$column;
1296             if (!empty($attributes['serialize'])) {
1297               $value = serialize($value);
1298             }
1299             $record[$column_name] = drupal_schema_get_field_value($attributes, $value);
1300           }
1301           $query->values($record);
1302           if ($this->entityType->isRevisionable()) {
1303             $revision_query->values($record);
1304           }
1305
1306           if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
1307             break;
1308           }
1309         }
1310       }
1311
1312       // Execute the query if we have values to insert.
1313       if ($do_insert) {
1314         // Only overwrite the field's base table if saving the default revision
1315         // of an entity.
1316         if ($entity->isDefaultRevision()) {
1317           $query->execute();
1318         }
1319         if ($this->entityType->isRevisionable()) {
1320           $revision_query->execute();
1321         }
1322       }
1323     }
1324   }
1325
1326   /**
1327    * Deletes values of fields in dedicated tables for all revisions.
1328    *
1329    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1330    *   The entity.
1331    */
1332   protected function deleteFromDedicatedTables(ContentEntityInterface $entity) {
1333     $table_mapping = $this->getTableMapping();
1334     foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1335       $storage_definition = $field_definition->getFieldStorageDefinition();
1336       if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1337         continue;
1338       }
1339       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1340       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1341       $this->database->delete($table_name)
1342         ->condition('entity_id', $entity->id())
1343         ->execute();
1344       if ($this->entityType->isRevisionable()) {
1345         $this->database->delete($revision_name)
1346           ->condition('entity_id', $entity->id())
1347           ->execute();
1348       }
1349     }
1350   }
1351
1352   /**
1353    * Deletes values of fields in dedicated tables for all revisions.
1354    *
1355    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1356    *   The entity. It must have a revision ID.
1357    */
1358   protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) {
1359     $vid = $entity->getRevisionId();
1360     if (isset($vid)) {
1361       $table_mapping = $this->getTableMapping();
1362       foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1363         $storage_definition = $field_definition->getFieldStorageDefinition();
1364         if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1365           continue;
1366         }
1367         $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1368         $this->database->delete($revision_name)
1369           ->condition('entity_id', $entity->id())
1370           ->condition('revision_id', $vid)
1371           ->execute();
1372       }
1373     }
1374   }
1375
1376   /**
1377    * {@inheritdoc}
1378    */
1379   public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1380     return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
1381   }
1382
1383   /**
1384    * {@inheritdoc}
1385    */
1386   public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1387     return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
1388   }
1389
1390   /**
1391    * {@inheritdoc}
1392    */
1393   public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1394     return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
1395   }
1396
1397   /**
1398    * {@inheritdoc}
1399    */
1400   public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1401     return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
1402   }
1403
1404   /**
1405    * {@inheritdoc}
1406    */
1407   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
1408     $this->wrapSchemaException(function () use ($entity_type) {
1409       $this->getStorageSchema()->onEntityTypeCreate($entity_type);
1410     });
1411   }
1412
1413   /**
1414    * {@inheritdoc}
1415    */
1416   public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1417     // Ensure we have an updated entity type definition.
1418     $this->entityType = $entity_type;
1419     // The table layout may have changed depending on the new entity type
1420     // definition.
1421     $this->initTableLayout();
1422     // Let the schema handler adapt to possible table layout changes.
1423     $this->wrapSchemaException(function () use ($entity_type, $original) {
1424       $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
1425     });
1426   }
1427
1428   /**
1429    * {@inheritdoc}
1430    */
1431   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
1432     $this->wrapSchemaException(function () use ($entity_type) {
1433       $this->getStorageSchema()->onEntityTypeDelete($entity_type);
1434     });
1435   }
1436
1437   /**
1438    * {@inheritdoc}
1439    */
1440   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
1441     // If we are adding a field stored in a shared table we need to recompute
1442     // the table mapping.
1443     // @todo This does not belong here. Remove it once we are able to generate a
1444     //   fresh table mapping in the schema handler. See
1445     //   https://www.drupal.org/node/2274017.
1446     if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
1447       $this->tableMapping = NULL;
1448     }
1449     $this->wrapSchemaException(function () use ($storage_definition) {
1450       $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
1451     });
1452   }
1453
1454   /**
1455    * {@inheritdoc}
1456    */
1457   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1458     $this->wrapSchemaException(function () use ($storage_definition, $original) {
1459       $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
1460     });
1461   }
1462
1463   /**
1464    * {@inheritdoc}
1465    */
1466   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
1467     $table_mapping = $this->getTableMapping(
1468       $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1469     );
1470
1471     // @todo Remove the FieldStorageConfigInterface check when non-configurable
1472     //   fields support purging: https://www.drupal.org/node/2282119.
1473     if ($storage_definition instanceof FieldStorageConfigInterface && $table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1474       // Mark all data associated with the field for deletion.
1475       $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1476       $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1477       $this->database->update($table)
1478         ->fields(['deleted' => 1])
1479         ->execute();
1480       if ($this->entityType->isRevisionable()) {
1481         $this->database->update($revision_table)
1482           ->fields(['deleted' => 1])
1483           ->execute();
1484       }
1485     }
1486
1487     // Update the field schema.
1488     $this->wrapSchemaException(function () use ($storage_definition) {
1489       $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
1490     });
1491   }
1492
1493   /**
1494    * Wraps a database schema exception into an entity storage exception.
1495    *
1496    * @param callable $callback
1497    *   The callback to be executed.
1498    *
1499    * @throws \Drupal\Core\Entity\EntityStorageException
1500    *   When a database schema exception is thrown.
1501    */
1502   protected function wrapSchemaException(callable $callback) {
1503     $message = 'Exception thrown while performing a schema update.';
1504     try {
1505       $callback();
1506     }
1507     catch (SchemaException $e) {
1508       $message .= ' ' . $e->getMessage();
1509       throw new EntityStorageException($message, 0, $e);
1510     }
1511     catch (DatabaseExceptionWrapper $e) {
1512       $message .= ' ' . $e->getMessage();
1513       throw new EntityStorageException($message, 0, $e);
1514     }
1515   }
1516
1517   /**
1518    * {@inheritdoc}
1519    */
1520   public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
1521     $table_mapping = $this->getTableMapping();
1522     $storage_definition = $field_definition->getFieldStorageDefinition();
1523     // Mark field data as deleted.
1524     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1525       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1526       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1527       $this->database->update($table_name)
1528         ->fields(['deleted' => 1])
1529         ->condition('bundle', $field_definition->getTargetBundle())
1530         ->execute();
1531       if ($this->entityType->isRevisionable()) {
1532         $this->database->update($revision_name)
1533           ->fields(['deleted' => 1])
1534           ->condition('bundle', $field_definition->getTargetBundle())
1535           ->execute();
1536       }
1537     }
1538   }
1539
1540   /**
1541    * {@inheritdoc}
1542    */
1543   public function onBundleCreate($bundle, $entity_type_id) {}
1544
1545   /**
1546    * {@inheritdoc}
1547    */
1548   public function onBundleDelete($bundle, $entity_type_id) {}
1549
1550   /**
1551    * {@inheritdoc}
1552    */
1553   protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
1554     // Check whether the whole field storage definition is gone, or just some
1555     // bundle fields.
1556     $storage_definition = $field_definition->getFieldStorageDefinition();
1557     $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
1558     $table_mapping = $this->getTableMapping();
1559     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1560
1561     // Get the entities which we want to purge first.
1562     $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
1563     $or = $entity_query->orConditionGroup();
1564     foreach ($storage_definition->getColumns() as $column_name => $data) {
1565       $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1566     }
1567     $entity_query
1568       ->distinct(TRUE)
1569       ->fields('t', ['entity_id'])
1570       ->condition('bundle', $field_definition->getTargetBundle())
1571       ->range(0, $batch_size);
1572
1573     // Create a map of field data table column names to field column names.
1574     $column_map = [];
1575     foreach ($storage_definition->getColumns() as $column_name => $data) {
1576       $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
1577     }
1578
1579     $entities = [];
1580     $items_by_entity = [];
1581     foreach ($entity_query->execute() as $row) {
1582       $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC])
1583         ->fields('t')
1584         ->condition('entity_id', $row['entity_id'])
1585         ->condition('deleted', 1)
1586         ->orderBy('delta');
1587
1588       foreach ($item_query->execute() as $item_row) {
1589         if (!isset($entities[$item_row['revision_id']])) {
1590           // Create entity with the right revision id and entity id combination.
1591           $item_row['entity_type'] = $this->entityTypeId;
1592           // @todo: Replace this by an entity object created via an entity
1593           // factory, see https://www.drupal.org/node/1867228.
1594           $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
1595         }
1596         $item = [];
1597         foreach ($column_map as $db_column => $field_column) {
1598           $item[$field_column] = $item_row[$db_column];
1599         }
1600         $items_by_entity[$item_row['revision_id']][] = $item;
1601       }
1602     }
1603
1604     // Create field item objects and return.
1605     foreach ($items_by_entity as $revision_id => $values) {
1606       $entity_adapter = $entities[$revision_id]->getTypedData();
1607       $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entity_adapter);
1608     }
1609     return $items_by_entity;
1610   }
1611
1612   /**
1613    * {@inheritdoc}
1614    */
1615   protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
1616     $storage_definition = $field_definition->getFieldStorageDefinition();
1617     $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
1618     $table_mapping = $this->getTableMapping();
1619     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1620     $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1621     $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id();
1622     $this->database->delete($table_name)
1623       ->condition('revision_id', $revision_id)
1624       ->condition('deleted', 1)
1625       ->execute();
1626     if ($this->entityType->isRevisionable()) {
1627       $this->database->delete($revision_name)
1628         ->condition('revision_id', $revision_id)
1629         ->condition('deleted', 1)
1630         ->execute();
1631     }
1632   }
1633
1634   /**
1635    * {@inheritdoc}
1636    */
1637   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
1638     $this->getStorageSchema()->finalizePurge($storage_definition);
1639   }
1640
1641   /**
1642    * {@inheritdoc}
1643    */
1644   public function countFieldData($storage_definition, $as_bool = FALSE) {
1645     // The table mapping contains stale data during a request when a field
1646     // storage definition is added, so bypass the internal storage definitions
1647     // and fetch the table mapping using the passed in storage definition.
1648     // @todo Fix this in https://www.drupal.org/node/2705205.
1649     $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
1650     $storage_definitions[$storage_definition->getName()] = $storage_definition;
1651     $table_mapping = $this->getTableMapping($storage_definitions);
1652
1653     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1654       $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
1655       if ($this->entityType->isRevisionable()) {
1656         $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1657       }
1658       else {
1659         $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1660       }
1661       $query = $this->database->select($table_name, 't');
1662       $or = $query->orConditionGroup();
1663       foreach ($storage_definition->getColumns() as $column_name => $data) {
1664         $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1665       }
1666       $query->condition($or);
1667       if (!$as_bool) {
1668         $query
1669           ->fields('t', ['entity_id'])
1670           ->distinct(TRUE);
1671       }
1672     }
1673     elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1674       // Ascertain the table this field is mapped too.
1675       $field_name = $storage_definition->getName();
1676       try {
1677         $table_name = $table_mapping->getFieldTableName($field_name);
1678       }
1679       catch (SqlContentEntityStorageException $e) {
1680         // This may happen when changing field storage schema, since we are not
1681         // able to use a table mapping matching the passed storage definition.
1682         // @todo Revisit this once we are able to instantiate the table mapping
1683         //   properly. See https://www.drupal.org/node/2274017.
1684         $table_name = $this->dataTable ?: $this->baseTable;
1685       }
1686       $query = $this->database->select($table_name, 't');
1687       $or = $query->orConditionGroup();
1688       foreach (array_keys($storage_definition->getColumns()) as $property_name) {
1689         $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name));
1690       }
1691       $query->condition($or);
1692       if (!$as_bool) {
1693         $query
1694           ->fields('t', [$this->idKey])
1695           ->distinct(TRUE);
1696       }
1697     }
1698
1699     // @todo Find a way to count field data also for fields having custom
1700     //   storage. See https://www.drupal.org/node/2337753.
1701     $count = 0;
1702     if (isset($query)) {
1703       // If we are performing the query just to check if the field has data
1704       // limit the number of rows.
1705       if ($as_bool) {
1706         $query
1707           ->range(0, 1)
1708           ->addExpression('1');
1709       }
1710       else {
1711         // Otherwise count the number of rows.
1712         $query = $query->countQuery();
1713       }
1714       $count = $query->execute()->fetchField();
1715     }
1716     return $as_bool ? (bool) $count : (int) $count;
1717   }
1718
1719   /**
1720    * Determines whether the passed field has been already deleted.
1721    *
1722    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1723    *   The field storage definition.
1724    *
1725    * @return bool
1726    *   Whether the field has been already deleted.
1727    */
1728   protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
1729     // Configurable fields are marked for deletion.
1730     if ($storage_definition instanceof FieldStorageConfigInterface) {
1731       return $storage_definition->isDeleted();
1732     }
1733     // For non configurable fields check whether they are still in the last
1734     // installed schema repository.
1735     return !array_key_exists($storage_definition->getName(), $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityTypeId));
1736   }
1737
1738 }