Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Entity / Sql / SqlContentEntityStorageSchema.php
1 <?php
2
3 namespace Drupal\Core\Entity\Sql;
4
5 use Drupal\Core\Database\Connection;
6 use Drupal\Core\Database\DatabaseException;
7 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
8 use Drupal\Core\Entity\ContentEntityTypeInterface;
9 use Drupal\Core\Entity\EntityManagerInterface;
10 use Drupal\Core\Entity\EntityPublishedInterface;
11 use Drupal\Core\Entity\EntityStorageException;
12 use Drupal\Core\Entity\EntityTypeInterface;
13 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
14 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
15 use Drupal\Core\Field\BaseFieldDefinition;
16 use Drupal\Core\Field\FieldException;
17 use Drupal\Core\Field\FieldStorageDefinitionInterface;
18 use Drupal\field\FieldStorageConfigInterface;
19
20 /**
21  * Defines a schema handler that supports revisionable, translatable entities.
22  *
23  * Entity types may extend this class and optimize the generated schema for all
24  * entity base tables by overriding getEntitySchema() for cross-field
25  * optimizations and getSharedTableFieldSchema() for optimizations applying to
26  * a single field.
27  */
28 class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorageSchemaInterface {
29
30   use DependencySerializationTrait;
31
32   /**
33    * The entity manager.
34    *
35    * @var \Drupal\Core\Entity\EntityManagerInterface
36    */
37   protected $entityManager;
38
39   /**
40    * The entity type this schema builder is responsible for.
41    *
42    * @var \Drupal\Core\Entity\ContentEntityTypeInterface
43    */
44   protected $entityType;
45
46   /**
47    * The storage field definitions for this entity type.
48    *
49    * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
50    */
51   protected $fieldStorageDefinitions;
52
53   /**
54    * The original storage field definitions for this entity type. Used during
55    * field schema updates.
56    *
57    * @var \Drupal\Core\Field\FieldDefinitionInterface[]
58    */
59   protected $originalDefinitions;
60
61   /**
62    * The storage object for the given entity type.
63    *
64    * @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage
65    */
66   protected $storage;
67
68   /**
69    * A static cache of the generated schema array.
70    *
71    * @var array
72    */
73   protected $schema;
74
75   /**
76    * The database connection to be used.
77    *
78    * @var \Drupal\Core\Database\Connection
79    */
80   protected $database;
81
82   /**
83    * The key-value collection for tracking installed storage schema.
84    *
85    * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
86    */
87   protected $installedStorageSchema;
88
89   /**
90    * Constructs a SqlContentEntityStorageSchema.
91    *
92    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
93    *   The entity manager.
94    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
95    *   The entity type.
96    * @param \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage
97    *   The storage of the entity type. This must be an SQL-based storage.
98    * @param \Drupal\Core\Database\Connection $database
99    *   The database connection to be used.
100    */
101   public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, SqlContentEntityStorage $storage, Connection $database) {
102     $this->entityManager = $entity_manager;
103     $this->entityType = $entity_type;
104     $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
105     $this->storage = $storage;
106     $this->database = $database;
107   }
108
109   /**
110    * Gets the keyvalue collection for tracking the installed schema.
111    *
112    * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
113    *
114    * @todo Inject this dependency in the constructor once this class can be
115    *   instantiated as a regular entity handler:
116    *   https://www.drupal.org/node/2332857.
117    */
118   protected function installedStorageSchema() {
119     if (!isset($this->installedStorageSchema)) {
120       $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
121     }
122     return $this->installedStorageSchema;
123   }
124
125   /**
126    * {@inheritdoc}
127    */
128   public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
129     return
130       $this->hasSharedTableStructureChange($entity_type, $original) ||
131       // Detect changes in key or index definitions.
132       $this->getEntitySchemaData($entity_type, $this->getEntitySchema($entity_type, TRUE)) != $this->loadEntitySchemaData($original);
133   }
134
135   /**
136    * Detects whether there is a change in the shared table structure.
137    *
138    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
139    *   The new entity type.
140    * @param \Drupal\Core\Entity\EntityTypeInterface $original
141    *   The origin entity type.
142    *
143    * @return bool
144    *   Returns TRUE if either the revisionable or translatable flag changes or
145    *   a table has been renamed.
146    */
147   protected function hasSharedTableStructureChange(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
148     return
149       $entity_type->isRevisionable() != $original->isRevisionable() ||
150       $entity_type->isTranslatable() != $original->isTranslatable() ||
151       $this->hasSharedTableNameChanges($entity_type, $original);
152   }
153
154   /**
155    * Detects whether any table name got renamed in an entity type update.
156    *
157    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
158    *   The new entity type.
159    * @param \Drupal\Core\Entity\EntityTypeInterface $original
160    *   The origin entity type.
161    *
162    * @return bool
163    *   Returns TRUE if there have been changes.
164    */
165   protected function hasSharedTableNameChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
166     $base_table = $this->database->schema()->tableExists($entity_type->getBaseTable());
167     $data_table = $this->database->schema()->tableExists($entity_type->getDataTable());
168     $revision_table = $this->database->schema()->tableExists($entity_type->getRevisionTable());
169     $revision_data_table = $this->database->schema()->tableExists($entity_type->getRevisionDataTable());
170
171     // We first check if the new table already exists because the storage might
172     // have created it even though it wasn't specified in the entity type
173     // definition.
174     return
175       (!$base_table && $entity_type->getBaseTable() != $original->getBaseTable()) ||
176       (!$data_table && $entity_type->getDataTable() != $original->getDataTable()) ||
177       (!$revision_table && $entity_type->getRevisionTable() != $original->getRevisionTable()) ||
178       (!$revision_data_table && $entity_type->getRevisionDataTable() != $original->getRevisionDataTable());
179   }
180
181   /**
182    * {@inheritdoc}
183    */
184   public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
185     $table_mapping = $this->storage->getTableMapping();
186
187     if (
188       $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
189       $storage_definition->getSchema() != $original->getSchema() ||
190       $storage_definition->isRevisionable() != $original->isRevisionable() ||
191       $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) ||
192       $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original)
193     ) {
194       return TRUE;
195     }
196
197     if ($storage_definition->hasCustomStorage()) {
198       // The field has custom storage, so we don't know if a schema change is
199       // needed or not, but since per the initial checks earlier in this
200       // function, nothing about the definition changed that we manage, we
201       // return FALSE.
202       return FALSE;
203     }
204
205     $current_schema = $this->getSchemaFromStorageDefinition($storage_definition);
206     $this->processFieldStorageSchema($current_schema);
207     $installed_schema = $this->loadFieldSchemaData($original);
208     $this->processFieldStorageSchema($installed_schema);
209
210     return $current_schema != $installed_schema;
211   }
212
213   /**
214    * Gets the schema data for the given field storage definition.
215    *
216    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
217    *   The field storage definition. The field that must not have custom
218    *   storage, i.e. the storage must take care of storing the field.
219    *
220    * @return array
221    *   The schema data.
222    */
223   protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
224     assert('!$storage_definition->hasCustomStorage();');
225     $table_mapping = $this->storage->getTableMapping();
226     $schema = [];
227     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
228       $schema = $this->getDedicatedTableSchema($storage_definition);
229     }
230     elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
231       $field_name = $storage_definition->getName();
232       foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
233         if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
234           $column_names = $table_mapping->getColumnNames($storage_definition->getName());
235           $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
236         }
237       }
238     }
239     return $schema;
240   }
241
242   /**
243    * {@inheritdoc}
244    */
245   public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
246     // Check if the entity type specifies that data migration is being handled
247     // elsewhere.
248     if ($entity_type->get('requires_data_migration') === FALSE) {
249       return FALSE;
250     }
251
252     // If the original storage has existing entities, or it is impossible to
253     // determine if that is the case, require entity data to be migrated.
254     $original_storage_class = $original->getStorageClass();
255     if (!class_exists($original_storage_class)) {
256       return TRUE;
257     }
258
259     // Data migration is not needed when only indexes changed, as they can be
260     // applied if there is data.
261     if (!$this->hasSharedTableStructureChange($entity_type, $original)) {
262       return FALSE;
263     }
264
265     // Use the original entity type since the storage has not been updated.
266     $original_storage = $this->entityManager->createHandlerInstance($original_storage_class, $original);
267     return $original_storage->hasData();
268   }
269
270   /**
271    * {@inheritdoc}
272    */
273   public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
274     return !$this->storage->countFieldData($original, TRUE);
275   }
276
277   /**
278    * {@inheritdoc}
279    */
280   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
281     $this->checkEntityType($entity_type);
282     $schema_handler = $this->database->schema();
283
284     // Create entity tables.
285     $schema = $this->getEntitySchema($entity_type, TRUE);
286     foreach ($schema as $table_name => $table_schema) {
287       if (!$schema_handler->tableExists($table_name)) {
288         $schema_handler->createTable($table_name, $table_schema);
289       }
290     }
291
292     // Create dedicated field tables.
293     $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
294     foreach ($this->fieldStorageDefinitions as $field_storage_definition) {
295       if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
296         $this->createDedicatedTableSchema($field_storage_definition);
297       }
298       elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
299         // The shared tables are already fully created, but we need to save the
300         // per-field schema definitions for later use.
301         $this->createSharedTableSchema($field_storage_definition, TRUE);
302       }
303     }
304
305     // Save data about entity indexes and keys.
306     $this->saveEntitySchemaData($entity_type, $schema);
307   }
308
309   /**
310    * {@inheritdoc}
311    */
312   public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
313     $this->checkEntityType($entity_type);
314     $this->checkEntityType($original);
315
316     // If no schema changes are needed, we don't need to do anything.
317     if (!$this->requiresEntityStorageSchemaChanges($entity_type, $original)) {
318       return;
319     }
320
321     // If a migration is required, we can't proceed.
322     if ($this->requiresEntityDataMigration($entity_type, $original)) {
323       throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type->id() . ') with data.');
324     }
325
326     // If we have no data just recreate the entity schema from scratch.
327     if ($this->isTableEmpty($this->storage->getBaseTable())) {
328       if ($this->database->supportsTransactionalDDL()) {
329         // If the database supports transactional DDL, we can go ahead and rely
330         // on it. If not, we will have to rollback manually if something fails.
331         $transaction = $this->database->startTransaction();
332       }
333       try {
334         $this->onEntityTypeDelete($original);
335         $this->onEntityTypeCreate($entity_type);
336       }
337       catch (\Exception $e) {
338         if ($this->database->supportsTransactionalDDL()) {
339           $transaction->rollBack();
340         }
341         else {
342           // Recreate original schema.
343           $this->onEntityTypeCreate($original);
344         }
345         throw $e;
346       }
347     }
348     else {
349       // Drop original indexes and unique keys.
350       $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
351
352       // Create new indexes and unique keys.
353       $entity_schema = $this->getEntitySchema($entity_type, TRUE);
354       $this->createEntitySchemaIndexes($entity_schema);
355
356       // Store the updated entity schema.
357       $this->saveEntitySchemaData($entity_type, $entity_schema);
358     }
359   }
360
361   /**
362    * {@inheritdoc}
363    */
364   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
365     $this->checkEntityType($entity_type);
366     $schema_handler = $this->database->schema();
367     $actual_definition = $this->entityManager->getDefinition($entity_type->id());
368     // @todo Instead of switching the wrapped entity type, we should be able to
369     //   instantiate a new table mapping for each entity type definition. See
370     //   https://www.drupal.org/node/2274017.
371     $this->storage->setEntityType($entity_type);
372
373     // Delete entity tables.
374     foreach ($this->getEntitySchemaTables() as $table_name) {
375       if ($schema_handler->tableExists($table_name)) {
376         $schema_handler->dropTable($table_name);
377       }
378     }
379
380     // Delete dedicated field tables.
381     $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
382     $this->originalDefinitions = $field_storage_definitions;
383     $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
384     foreach ($field_storage_definitions as $field_storage_definition) {
385       // If we have a field having dedicated storage we need to drop it,
386       // otherwise we just remove the related schema data.
387       if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
388         $this->deleteDedicatedTableSchema($field_storage_definition);
389       }
390       elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
391         $this->deleteFieldSchemaData($field_storage_definition);
392       }
393     }
394     $this->originalDefinitions = NULL;
395
396     $this->storage->setEntityType($actual_definition);
397
398     // Delete the entity schema.
399     $this->deleteEntitySchemaData($entity_type);
400   }
401
402   /**
403    * {@inheritdoc}
404    */
405   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
406     $this->performFieldSchemaOperation('create', $storage_definition);
407   }
408
409   /**
410    * {@inheritdoc}
411    */
412   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
413     // Store original definitions so that switching between shared and dedicated
414     // field table layout works.
415     $this->originalDefinitions = $this->fieldStorageDefinitions;
416     $this->originalDefinitions[$original->getName()] = $original;
417     $this->performFieldSchemaOperation('update', $storage_definition, $original);
418     $this->originalDefinitions = NULL;
419   }
420
421   /**
422    * {@inheritdoc}
423    */
424   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
425     // Only configurable fields currently support purging, so prevent deletion
426     // of ones we can't purge if they have existing data.
427     // @todo Add purging to all fields: https://www.drupal.org/node/2282119.
428     try {
429       if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
430         throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
431       }
432     }
433     catch (DatabaseException $e) {
434       // This may happen when changing field storage schema, since we are not
435       // able to use a table mapping matching the passed storage definition.
436       // @todo Revisit this once we are able to instantiate the table mapping
437       //   properly. See https://www.drupal.org/node/2274017.
438       return;
439     }
440
441     // Retrieve a table mapping which contains the deleted field still.
442     $table_mapping = $this->storage->getTableMapping(
443       $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
444     );
445     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
446       // Move the table to a unique name while the table contents are being
447       // deleted.
448       $table = $table_mapping->getDedicatedDataTableName($storage_definition);
449       $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
450       $this->database->schema()->renameTable($table, $new_table);
451       if ($this->entityType->isRevisionable()) {
452         $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
453         $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
454         $this->database->schema()->renameTable($revision_table, $revision_new_table);
455       }
456     }
457
458     // @todo Remove when finalizePurge() is invoked from the outside for all
459     //   fields: https://www.drupal.org/node/2282119.
460     if (!($storage_definition instanceof FieldStorageConfigInterface)) {
461       $this->performFieldSchemaOperation('delete', $storage_definition);
462     }
463   }
464
465   /**
466    * {@inheritdoc}
467    */
468   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
469     $this->performFieldSchemaOperation('delete', $storage_definition);
470   }
471
472   /**
473    * Checks that we are dealing with the correct entity type.
474    *
475    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
476    *   The entity type to be checked.
477    *
478    * @return bool
479    *   TRUE if the entity type matches the current one.
480    *
481    * @throws \Drupal\Core\Entity\EntityStorageException
482    */
483   protected function checkEntityType(EntityTypeInterface $entity_type) {
484     if ($entity_type->id() != $this->entityType->id()) {
485       throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
486     }
487     return TRUE;
488   }
489
490   /**
491    * Gets the entity schema for the specified entity type.
492    *
493    * Entity types may override this method in order to optimize the generated
494    * schema of the entity tables. However, only cross-field optimizations should
495    * be added here; e.g., an index spanning multiple fields. Optimizations that
496    * apply to a single field have to be added via
497    * SqlContentEntityStorageSchema::getSharedTableFieldSchema() instead.
498    *
499    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
500    *   The entity type definition.
501    * @param bool $reset
502    *   (optional) If set to TRUE static cache will be ignored and a new schema
503    *   array generation will be performed. Defaults to FALSE.
504    *
505    * @return array
506    *   A Schema API array describing the entity schema, excluding dedicated
507    *   field tables.
508    *
509    * @throws \Drupal\Core\Field\FieldException
510    */
511   protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
512     $this->checkEntityType($entity_type);
513     $entity_type_id = $entity_type->id();
514
515     if (!isset($this->schema[$entity_type_id]) || $reset) {
516       // Back up the storage definition and replace it with the passed one.
517       // @todo Instead of switching the wrapped entity type, we should be able
518       //   to instantiate a new table mapping for each entity type definition.
519       //   See https://www.drupal.org/node/2274017.
520       $actual_definition = $this->entityManager->getDefinition($entity_type_id);
521       $this->storage->setEntityType($entity_type);
522
523       // Prepare basic information about the entity type.
524       $tables = $this->getEntitySchemaTables();
525
526       // Initialize the table schema.
527       $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type);
528       if (isset($tables['revision_table'])) {
529         $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type);
530       }
531       if (isset($tables['data_table'])) {
532         $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
533       }
534       if (isset($tables['revision_data_table'])) {
535         $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
536       }
537
538       // We need to act only on shared entity schema tables.
539       $table_mapping = $this->storage->getTableMapping();
540       $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
541       foreach ($table_names as $table_name) {
542         if (!isset($schema[$table_name])) {
543           $schema[$table_name] = [];
544         }
545         foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
546           if (!isset($this->fieldStorageDefinitions[$field_name])) {
547             throw new FieldException("Field storage definition for '$field_name' could not be found.");
548           }
549           // Add the schema for base field definitions.
550           elseif ($table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) {
551             $column_names = $table_mapping->getColumnNames($field_name);
552             $storage_definition = $this->fieldStorageDefinitions[$field_name];
553             $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
554           }
555         }
556       }
557
558       // Process tables after having gathered field information.
559       $this->processBaseTable($entity_type, $schema[$tables['base_table']]);
560       if (isset($tables['revision_table'])) {
561         $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
562       }
563       if (isset($tables['data_table'])) {
564         $this->processDataTable($entity_type, $schema[$tables['data_table']]);
565       }
566       if (isset($tables['revision_data_table'])) {
567         $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
568       }
569
570       // Add an index for the 'published' entity key.
571       if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) {
572         $published_key = $entity_type->getKey('published');
573         if ($published_key && !$this->fieldStorageDefinitions[$published_key]->hasCustomStorage()) {
574           $published_field_table = $table_mapping->getFieldTableName($published_key);
575           $id_key = $entity_type->getKey('id');
576           if ($bundle_key = $entity_type->getKey('bundle')) {
577             $key = "{$published_key}_{$bundle_key}";
578             $columns = [$published_key, $bundle_key, $id_key];
579           }
580           else {
581             $key = $published_key;
582             $columns = [$published_key, $id_key];
583           }
584           $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns;
585         }
586       }
587
588       $this->schema[$entity_type_id] = $schema;
589
590       // Restore the actual definition.
591       $this->storage->setEntityType($actual_definition);
592     }
593
594     return $this->schema[$entity_type_id];
595   }
596
597   /**
598    * Gets a list of entity type tables.
599    *
600    * @return array
601    *   A list of entity type tables, keyed by table key.
602    */
603   protected function getEntitySchemaTables() {
604     return array_filter([
605       'base_table' => $this->storage->getBaseTable(),
606       'revision_table' => $this->storage->getRevisionTable(),
607       'data_table' => $this->storage->getDataTable(),
608       'revision_data_table' => $this->storage->getRevisionDataTable(),
609     ]);
610   }
611
612   /**
613    * Gets entity schema definitions for index and key definitions.
614    *
615    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
616    *   The entity type definition.
617    * @param array $schema
618    *   The entity schema array.
619    *
620    * @return array
621    *   A stripped down version of the $schema Schema API array containing, for
622    *   each table, only the key and index definitions not derived from field
623    *   storage definitions.
624    */
625   protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
626     $entity_type_id = $entity_type->id();
627
628     // Collect all possible field schema identifiers for shared table fields.
629     // These will be used to detect entity schema data in the subsequent loop.
630     $field_schema_identifiers = [];
631     $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
632     foreach ($this->fieldStorageDefinitions as $field_name => $storage_definition) {
633       if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
634         // Make sure both base identifier names and suffixed names are listed.
635         $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name);
636         $field_schema_identifiers[$name] = $name;
637         foreach ($storage_definition->getColumns() as $key => $columns) {
638           $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
639           $field_schema_identifiers[$name] = $name;
640         }
641       }
642     }
643
644     // Extract entity schema data from the Schema API definition.
645     $schema_data = [];
646     $keys = ['indexes', 'unique keys'];
647     $unused_keys = array_flip(['description', 'fields', 'foreign keys']);
648     foreach ($schema as $table_name => $table_schema) {
649       $table_schema = array_diff_key($table_schema, $unused_keys);
650       foreach ($keys as $key) {
651         // Exclude data generated from field storage definitions, we will check
652         // that separately.
653         if ($field_schema_identifiers && !empty($table_schema[$key])) {
654           $table_schema[$key] = array_diff_key($table_schema[$key], $field_schema_identifiers);
655         }
656       }
657       $schema_data[$table_name] = array_filter($table_schema);
658     }
659
660     return $schema_data;
661   }
662
663   /**
664    * Gets an index schema array for a given field.
665    *
666    * @param string $field_name
667    *   The name of the field.
668    * @param array $field_schema
669    *   The schema of the field.
670    * @param string[] $column_mapping
671    *   A mapping of field column names to database column names.
672    *
673    * @return array
674    *   The schema definition for the indexes.
675    */
676   protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) {
677     return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes');
678   }
679
680   /**
681    * Gets a unique key schema array for a given field.
682    *
683    * @param string $field_name
684    *   The name of the field.
685    * @param array $field_schema
686    *   The schema of the field.
687    * @param string[] $column_mapping
688    *   A mapping of field column names to database column names.
689    *
690    * @return array
691    *   The schema definition for the unique keys.
692    */
693   protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) {
694     return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys');
695   }
696
697   /**
698    * Gets field schema data for the given key.
699    *
700    * @param string $field_name
701    *   The name of the field.
702    * @param array $field_schema
703    *   The schema of the field.
704    * @param string[] $column_mapping
705    *   A mapping of field column names to database column names.
706    * @param string $schema_key
707    *   The type of schema data. Either 'indexes' or 'unique keys'.
708    *
709    * @return array
710    *   The schema definition for the specified key.
711    */
712   protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) {
713     $data = [];
714
715     $entity_type_id = $this->entityType->id();
716     foreach ($field_schema[$schema_key] as $key => $columns) {
717       // To avoid clashes with entity-level indexes or unique keys we use
718       // "{$entity_type_id}_field__" as a prefix instead of just
719       // "{$entity_type_id}__". We additionally namespace the specifier by the
720       // field name to avoid clashes when multiple fields of the same type are
721       // added to an entity type.
722       $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
723       foreach ($columns as $column) {
724         // Allow for indexes and unique keys to specified as an array of column
725         // name and length.
726         if (is_array($column)) {
727           list($column_name, $length) = $column;
728           $data[$real_key][] = [$column_mapping[$column_name], $length];
729         }
730         else {
731           $data[$real_key][] = $column_mapping[$column];
732         }
733       }
734     }
735
736     return $data;
737   }
738
739   /**
740    * Generates a safe schema identifier (name of an index, column name etc.).
741    *
742    * @param string $entity_type_id
743    *   The ID of the entity type.
744    * @param string $field_name
745    *   The name of the field.
746    * @param string|null $key
747    *   (optional) A further key to append to the name.
748    *
749    * @return string
750    *   The field identifier name.
751    */
752   protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key = NULL) {
753     $real_key = isset($key) ? "{$entity_type_id}_field__{$field_name}__{$key}" : "{$entity_type_id}_field__{$field_name}";
754     // Limit the string to 48 characters, keeping a 16 characters margin for db
755     // prefixes.
756     if (strlen($real_key) > 48) {
757       // Use a shorter separator, a truncated entity_type, and a hash of the
758       // field name.
759       // Truncate to the same length for the current and revision tables.
760       $entity_type = substr($entity_type_id, 0, 36);
761       $field_hash = substr(hash('sha256', $real_key), 0, 10);
762       $real_key = $entity_type . '__' . $field_hash;
763     }
764     return $real_key;
765   }
766
767   /**
768    * Gets field foreign keys.
769    *
770    * @param string $field_name
771    *   The name of the field.
772    * @param array $field_schema
773    *   The schema of the field.
774    * @param string[] $column_mapping
775    *   A mapping of field column names to database column names.
776    *
777    * @return array
778    *   The schema definition for the foreign keys.
779    */
780   protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) {
781     $foreign_keys = [];
782
783     foreach ($field_schema['foreign keys'] as $specifier => $specification) {
784       // To avoid clashes with entity-level foreign keys we use
785       // "{$entity_type_id}_field__" as a prefix instead of just
786       // "{$entity_type_id}__". We additionally namespace the specifier by the
787       // field name to avoid clashes when multiple fields of the same type are
788       // added to an entity type.
789       $entity_type_id = $this->entityType->id();
790       $real_specifier = "{$entity_type_id}_field__{$field_name}__{$specifier}";
791       $foreign_keys[$real_specifier]['table'] = $specification['table'];
792       foreach ($specification['columns'] as $column => $referenced) {
793         $foreign_keys[$real_specifier]['columns'][$column_mapping[$column]] = $referenced;
794       }
795     }
796
797     return $foreign_keys;
798   }
799
800   /**
801    * Loads stored schema data for the given entity type definition.
802    *
803    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
804    *   The entity type definition.
805    *
806    * @return array
807    *   The entity schema data array.
808    */
809   protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
810     return $this->installedStorageSchema()->get($entity_type->id() . '.entity_schema_data', []);
811   }
812
813   /**
814    * Stores schema data for the given entity type definition.
815    *
816    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
817    *   The entity type definition.
818    * @param array $schema
819    *   The entity schema data array.
820    */
821   protected function saveEntitySchemaData(EntityTypeInterface $entity_type, $schema) {
822     $data = $this->getEntitySchemaData($entity_type, $schema);
823     $this->installedStorageSchema()->set($entity_type->id() . '.entity_schema_data', $data);
824   }
825
826   /**
827    * Deletes schema data for the given entity type definition.
828    *
829    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
830    *   The entity type definition.
831    */
832   protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
833     $this->installedStorageSchema()->delete($entity_type->id() . '.entity_schema_data');
834   }
835
836   /**
837    * Loads stored schema data for the given field storage definition.
838    *
839    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
840    *   The field storage definition.
841    *
842    * @return array
843    *   The field schema data array.
844    */
845   protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
846     return $this->installedStorageSchema()->get($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), []);
847   }
848
849   /**
850    * Stores schema data for the given field storage definition.
851    *
852    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
853    *   The field storage definition.
854    * @param array $schema
855    *   The field schema data array.
856    */
857   protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) {
858     $this->processFieldStorageSchema($schema);
859     $this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema);
860   }
861
862   /**
863    * Deletes schema data for the given field storage definition.
864    *
865    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
866    *   The field storage definition.
867    */
868   protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
869     $this->installedStorageSchema()->delete($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName());
870   }
871
872   /**
873    * Initializes common information for a base table.
874    *
875    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
876    *   The entity type.
877    *
878    * @return array
879    *   A partial schema array for the base table.
880    */
881   protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
882     $entity_type_id = $entity_type->id();
883
884     $schema = [
885       'description' => "The base table for $entity_type_id entities.",
886       'primary key' => [$entity_type->getKey('id')],
887       'indexes' => [],
888       'foreign keys' => [],
889     ];
890
891     if ($entity_type->hasKey('revision')) {
892       $revision_key = $entity_type->getKey('revision');
893       $key_name = $this->getEntityIndexName($entity_type, $revision_key);
894       $schema['unique keys'][$key_name] = [$revision_key];
895       $schema['foreign keys'][$entity_type_id . '__revision'] = [
896         'table' => $this->storage->getRevisionTable(),
897         'columns' => [$revision_key => $revision_key],
898       ];
899     }
900
901     $this->addTableDefaults($schema);
902
903     return $schema;
904   }
905
906   /**
907    * Initializes common information for a revision table.
908    *
909    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
910    *   The entity type.
911    *
912    * @return array
913    *   A partial schema array for the revision table.
914    */
915   protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) {
916     $entity_type_id = $entity_type->id();
917     $id_key = $entity_type->getKey('id');
918     $revision_key = $entity_type->getKey('revision');
919
920     $schema = [
921       'description' => "The revision table for $entity_type_id entities.",
922       'primary key' => [$revision_key],
923       'indexes' => [],
924       'foreign keys' => [
925         $entity_type_id . '__revisioned' => [
926           'table' => $this->storage->getBaseTable(),
927           'columns' => [$id_key => $id_key],
928         ],
929       ],
930     ];
931
932     $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = [$id_key];
933
934     $this->addTableDefaults($schema);
935
936     return $schema;
937   }
938
939   /**
940    * Initializes common information for a data table.
941    *
942    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
943    *   The entity type.
944    *
945    * @return array
946    *   A partial schema array for the data table.
947    */
948   protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
949     $entity_type_id = $entity_type->id();
950     $id_key = $entity_type->getKey('id');
951
952     $schema = [
953       'description' => "The data table for $entity_type_id entities.",
954       'primary key' => [$id_key, $entity_type->getKey('langcode')],
955       'indexes' => [
956         $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
957       ],
958       'foreign keys' => [
959         $entity_type_id => [
960           'table' => $this->storage->getBaseTable(),
961           'columns' => [$id_key => $id_key],
962         ],
963       ],
964     ];
965
966     if ($entity_type->hasKey('revision')) {
967       $key = $entity_type->getKey('revision');
968       $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = [$key];
969     }
970
971     $this->addTableDefaults($schema);
972
973     return $schema;
974   }
975
976   /**
977    * Initializes common information for a revision data table.
978    *
979    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
980    *   The entity type.
981    *
982    * @return array
983    *   A partial schema array for the revision data table.
984    */
985   protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) {
986     $entity_type_id = $entity_type->id();
987     $id_key = $entity_type->getKey('id');
988     $revision_key = $entity_type->getKey('revision');
989
990     $schema = [
991       'description' => "The revision data table for $entity_type_id entities.",
992       'primary key' => [$revision_key, $entity_type->getKey('langcode')],
993       'indexes' => [
994         $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
995       ],
996       'foreign keys' => [
997         $entity_type_id => [
998           'table' => $this->storage->getBaseTable(),
999           'columns' => [$id_key => $id_key],
1000         ],
1001         $entity_type_id . '__revision' => [
1002           'table' => $this->storage->getRevisionTable(),
1003           'columns' => [$revision_key => $revision_key],
1004         ]
1005       ],
1006     ];
1007
1008     $this->addTableDefaults($schema);
1009
1010     return $schema;
1011   }
1012
1013   /**
1014    * Adds defaults to a table schema definition.
1015    *
1016    * @param $schema
1017    *   The schema definition array for a single table, passed by reference.
1018    */
1019   protected function addTableDefaults(&$schema) {
1020     $schema += [
1021       'fields' => [],
1022       'unique keys' => [],
1023       'indexes' => [],
1024       'foreign keys' => [],
1025     ];
1026   }
1027
1028   /**
1029    * Processes the gathered schema for a base table.
1030    *
1031    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1032    *   The entity type.
1033    * @param array $schema
1034    *   The table schema, passed by reference.
1035    *
1036    * @return array
1037    *   A partial schema array for the base table.
1038    */
1039   protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1040     $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
1041   }
1042
1043   /**
1044    * Processes the gathered schema for a base table.
1045    *
1046    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1047    *   The entity type.
1048    * @param array $schema
1049    *   The table schema, passed by reference.
1050    *
1051    * @return array
1052    *   A partial schema array for the base table.
1053    */
1054   protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1055     $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
1056   }
1057
1058   /**
1059    * Processes the gathered schema for a base table.
1060    *
1061    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1062    *   The entity type.
1063    * @param array $schema
1064    *   The table schema, passed by reference.
1065    *
1066    * @return array
1067    *   A partial schema array for the base table.
1068    */
1069   protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1070     // Marking the respective fields as NOT NULL makes the indexes more
1071     // performant.
1072     $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1073   }
1074
1075   /**
1076    * Processes the gathered schema for a base table.
1077    *
1078    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1079    *   The entity type.
1080    * @param array $schema
1081    *   The table schema, passed by reference.
1082    *
1083    * @return array
1084    *   A partial schema array for the base table.
1085    */
1086   protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1087     // Marking the respective fields as NOT NULL makes the indexes more
1088     // performant.
1089     $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1090   }
1091
1092   /**
1093    * Processes the specified entity key.
1094    *
1095    * @param array $schema
1096    *   The table schema, passed by reference.
1097    * @param string $key
1098    *   The entity key name.
1099    */
1100   protected function processIdentifierSchema(&$schema, $key) {
1101     if ($schema['fields'][$key]['type'] == 'int') {
1102       $schema['fields'][$key]['type'] = 'serial';
1103     }
1104     $schema['fields'][$key]['not null'] = TRUE;
1105     unset($schema['fields'][$key]['default']);
1106   }
1107
1108   /**
1109    * Processes the schema for a field storage definition.
1110    *
1111    * @param array &$field_storage_schema
1112    *   An array that contains the schema data for a field storage definition.
1113    */
1114   protected function processFieldStorageSchema(array &$field_storage_schema) {
1115     // Clean up some schema properties that should not be taken into account
1116     // after a field storage has been created.
1117     foreach ($field_storage_schema as $table_name => $table_schema) {
1118       foreach ($table_schema['fields'] as $key => $schema) {
1119         unset($field_storage_schema[$table_name]['fields'][$key]['initial']);
1120         unset($field_storage_schema[$table_name]['fields'][$key]['initial_from_field']);
1121       }
1122     }
1123   }
1124
1125   /**
1126    * Performs the specified operation on a field.
1127    *
1128    * This figures out whether the field is stored in a dedicated or shared table
1129    * and forwards the call to the proper handler.
1130    *
1131    * @param string $operation
1132    *   The name of the operation to be performed.
1133    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1134    *   The field storage definition.
1135    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1136    *   (optional) The original field storage definition. This is relevant (and
1137    *   required) only for updates. Defaults to NULL.
1138    */
1139   protected function performFieldSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) {
1140     $table_mapping = $this->storage->getTableMapping();
1141     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1142       $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original);
1143     }
1144     elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1145       $this->{$operation . 'SharedTableSchema'}($storage_definition, $original);
1146     }
1147   }
1148
1149   /**
1150    * Creates the schema for a field stored in a dedicated table.
1151    *
1152    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1153    *   The storage definition of the field being created.
1154    */
1155   protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1156     $schema = $this->getDedicatedTableSchema($storage_definition);
1157     foreach ($schema as $name => $table) {
1158       // Check if the table exists because it might already have been
1159       // created as part of the earlier entity type update event.
1160       if (!$this->database->schema()->tableExists($name)) {
1161         $this->database->schema()->createTable($name, $table);
1162       }
1163     }
1164     $this->saveFieldSchemaData($storage_definition, $schema);
1165   }
1166
1167   /**
1168    * Creates the schema for a field stored in a shared table.
1169    *
1170    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1171    *   The storage definition of the field being created.
1172    * @param bool $only_save
1173    *   (optional) Whether to skip modification of database tables and only save
1174    *   the schema data for future comparison. For internal use only. This is
1175    *   used by onEntityTypeCreate() after it has already fully created the
1176    *   shared tables.
1177    */
1178   protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) {
1179     $created_field_name = $storage_definition->getName();
1180     $table_mapping = $this->storage->getTableMapping();
1181     $column_names = $table_mapping->getColumnNames($created_field_name);
1182     $schema_handler = $this->database->schema();
1183     $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
1184
1185     // Iterate over the mapped table to find the ones that will host the created
1186     // field schema.
1187     $schema = [];
1188     foreach ($shared_table_names as $table_name) {
1189       foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1190         if ($field_name == $created_field_name) {
1191           // Create field columns.
1192           $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1193           if (!$only_save) {
1194             foreach ($schema[$table_name]['fields'] as $name => $specifier) {
1195               // Check if the field exists because it might already have been
1196               // created as part of the earlier entity type update event.
1197               if (!$schema_handler->fieldExists($table_name, $name)) {
1198                 $schema_handler->addField($table_name, $name, $specifier);
1199               }
1200             }
1201             if (!empty($schema[$table_name]['indexes'])) {
1202               foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
1203                 // Check if the index exists because it might already have been
1204                 // created as part of the earlier entity type update event.
1205                 $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
1206               }
1207             }
1208             if (!empty($schema[$table_name]['unique keys'])) {
1209               foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
1210                 $schema_handler->addUniqueKey($table_name, $name, $specifier);
1211               }
1212             }
1213           }
1214           // After creating the field schema skip to the next table.
1215           break;
1216         }
1217       }
1218     }
1219
1220     $this->saveFieldSchemaData($storage_definition, $schema);
1221
1222     if (!$only_save) {
1223       // Make sure any entity index involving this field is re-created if
1224       // needed.
1225       $this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
1226     }
1227   }
1228
1229   /**
1230    * Deletes the schema for a field stored in a dedicated table.
1231    *
1232    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1233    *   The storage definition of the field being deleted.
1234    */
1235   protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1236     // When switching from dedicated to shared field table layout we need need
1237     // to delete the field tables with their regular names. When this happens
1238     // original definitions will be defined.
1239     $deleted = !$this->originalDefinitions;
1240     $table_mapping = $this->storage->getTableMapping();
1241     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted);
1242     if ($this->database->schema()->tableExists($table_name)) {
1243       $this->database->schema()->dropTable($table_name);
1244     }
1245     if ($this->entityType->isRevisionable()) {
1246       $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted);
1247       if ($this->database->schema()->tableExists($revision_table_name)) {
1248         $this->database->schema()->dropTable($revision_table_name);
1249       }
1250     }
1251     $this->deleteFieldSchemaData($storage_definition);
1252   }
1253
1254   /**
1255    * Deletes the schema for a field stored in a shared table.
1256    *
1257    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1258    *   The storage definition of the field being deleted.
1259    */
1260   protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1261     // Make sure any entity index involving this field is dropped.
1262     $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
1263
1264     $deleted_field_name = $storage_definition->getName();
1265     $table_mapping = $this->storage->getTableMapping(
1266       $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1267     );
1268     $column_names = $table_mapping->getColumnNames($deleted_field_name);
1269     $schema_handler = $this->database->schema();
1270     $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
1271
1272     // Iterate over the mapped table to find the ones that host the deleted
1273     // field schema.
1274     foreach ($shared_table_names as $table_name) {
1275       foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1276         if ($field_name == $deleted_field_name) {
1277           $schema = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1278
1279           // Drop indexes and unique keys first.
1280           if (!empty($schema['indexes'])) {
1281             foreach ($schema['indexes'] as $name => $specifier) {
1282               $schema_handler->dropIndex($table_name, $name);
1283             }
1284           }
1285           if (!empty($schema['unique keys'])) {
1286             foreach ($schema['unique keys'] as $name => $specifier) {
1287               $schema_handler->dropUniqueKey($table_name, $name);
1288             }
1289           }
1290           // Drop columns.
1291           foreach ($column_names as $column_name) {
1292             $schema_handler->dropField($table_name, $column_name);
1293           }
1294           // After deleting the field schema skip to the next table.
1295           break;
1296         }
1297       }
1298     }
1299
1300     $this->deleteFieldSchemaData($storage_definition);
1301   }
1302
1303   /**
1304    * Updates the schema for a field stored in a shared table.
1305    *
1306    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1307    *   The storage definition of the field being updated.
1308    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1309    *   The original storage definition; i.e., the definition before the update.
1310    *
1311    * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
1312    *   Thrown when the update to the field is forbidden.
1313    * @throws \Exception
1314    *   Rethrown exception if the table recreation fails.
1315    */
1316   protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1317     if (!$this->storage->countFieldData($original, TRUE)) {
1318       // There is no data. Re-create the tables completely.
1319       if ($this->database->supportsTransactionalDDL()) {
1320         // If the database supports transactional DDL, we can go ahead and rely
1321         // on it. If not, we will have to rollback manually if something fails.
1322         $transaction = $this->database->startTransaction();
1323       }
1324       try {
1325         // Since there is no data we may be switching from a shared table schema
1326         // to a dedicated table schema, hence we should use the proper API.
1327         $this->performFieldSchemaOperation('delete', $original);
1328         $this->performFieldSchemaOperation('create', $storage_definition);
1329       }
1330       catch (\Exception $e) {
1331         if ($this->database->supportsTransactionalDDL()) {
1332           $transaction->rollBack();
1333         }
1334         else {
1335           // Recreate tables.
1336           $this->performFieldSchemaOperation('create', $original);
1337         }
1338         throw $e;
1339       }
1340     }
1341     else {
1342       if ($this->hasColumnChanges($storage_definition, $original)) {
1343         throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
1344       }
1345       // There is data, so there are no column changes. Drop all the prior
1346       // indexes and create all the new ones, except for all the priors that
1347       // exist unchanged.
1348       $table_mapping = $this->storage->getTableMapping();
1349       $table = $table_mapping->getDedicatedDataTableName($original);
1350       $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
1351
1352       // Get the field schemas.
1353       $schema = $storage_definition->getSchema();
1354       $original_schema = $original->getSchema();
1355
1356       // Gets the SQL schema for a dedicated tables.
1357       $actual_schema = $this->getDedicatedTableSchema($storage_definition);
1358
1359       foreach ($original_schema['indexes'] as $name => $columns) {
1360         if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
1361           $real_name = $this->getFieldIndexName($storage_definition, $name);
1362           $this->database->schema()->dropIndex($table, $real_name);
1363           $this->database->schema()->dropIndex($revision_table, $real_name);
1364         }
1365       }
1366       $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1367       $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1368       foreach ($schema['indexes'] as $name => $columns) {
1369         if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
1370           $real_name = $this->getFieldIndexName($storage_definition, $name);
1371           $real_columns = [];
1372           foreach ($columns as $column_name) {
1373             // Indexes can be specified as either a column name or an array with
1374             // column name and length. Allow for either case.
1375             if (is_array($column_name)) {
1376               $real_columns[] = [
1377                 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1378                 $column_name[1],
1379               ];
1380             }
1381             else {
1382               $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1383             }
1384           }
1385           // Check if the index exists because it might already have been
1386           // created as part of the earlier entity type update event.
1387           $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
1388           $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
1389         }
1390       }
1391       $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
1392     }
1393   }
1394
1395   /**
1396    * Updates the schema for a field stored in a shared table.
1397    *
1398    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1399    *   The storage definition of the field being updated.
1400    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1401    *   The original storage definition; i.e., the definition before the update.
1402    *
1403    * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
1404    *   Thrown when the update to the field is forbidden.
1405    * @throws \Exception
1406    *   Rethrown exception if the table recreation fails.
1407    */
1408   protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1409     if (!$this->storage->countFieldData($original, TRUE)) {
1410       if ($this->database->supportsTransactionalDDL()) {
1411         // If the database supports transactional DDL, we can go ahead and rely
1412         // on it. If not, we will have to rollback manually if something fails.
1413         $transaction = $this->database->startTransaction();
1414       }
1415       try {
1416         // Since there is no data we may be switching from a dedicated table
1417         // to a schema table schema, hence we should use the proper API.
1418         $this->performFieldSchemaOperation('delete', $original);
1419         $this->performFieldSchemaOperation('create', $storage_definition);
1420       }
1421       catch (\Exception $e) {
1422         if ($this->database->supportsTransactionalDDL()) {
1423           $transaction->rollBack();
1424         }
1425         else {
1426           // Recreate original schema.
1427           $this->createSharedTableSchema($original);
1428         }
1429         throw $e;
1430       }
1431     }
1432     else {
1433       if ($this->hasColumnChanges($storage_definition, $original)) {
1434         throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
1435       }
1436
1437       $updated_field_name = $storage_definition->getName();
1438       $table_mapping = $this->storage->getTableMapping();
1439       $column_names = $table_mapping->getColumnNames($updated_field_name);
1440       $schema_handler = $this->database->schema();
1441
1442       // Iterate over the mapped table to find the ones that host the deleted
1443       // field schema.
1444       $original_schema = $this->loadFieldSchemaData($original);
1445       $schema = [];
1446       foreach ($table_mapping->getTableNames() as $table_name) {
1447         foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1448           if ($field_name == $updated_field_name) {
1449             $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1450
1451             // Handle NOT NULL constraints.
1452             foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
1453               $not_null = !empty($specifier['not null']);
1454               $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
1455               if ($not_null !== $original_not_null) {
1456                 if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
1457                   throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
1458                 }
1459                 $column_schema = $original_schema[$table_name]['fields'][$column_name];
1460                 $column_schema['not null'] = $not_null;
1461                 $schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
1462               }
1463             }
1464
1465             // Drop original indexes and unique keys.
1466             if (!empty($original_schema[$table_name]['indexes'])) {
1467               foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
1468                 $schema_handler->dropIndex($table_name, $name);
1469               }
1470             }
1471             if (!empty($original_schema[$table_name]['unique keys'])) {
1472               foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) {
1473                 $schema_handler->dropUniqueKey($table_name, $name);
1474               }
1475             }
1476             // Create new indexes and unique keys.
1477             if (!empty($schema[$table_name]['indexes'])) {
1478               foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
1479                 // Check if the index exists because it might already have been
1480                 // created as part of the earlier entity type update event.
1481                 $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
1482
1483               }
1484             }
1485             if (!empty($schema[$table_name]['unique keys'])) {
1486               foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
1487                 $schema_handler->addUniqueKey($table_name, $name, $specifier);
1488               }
1489             }
1490             // After deleting the field schema skip to the next table.
1491             break;
1492           }
1493         }
1494       }
1495       $this->saveFieldSchemaData($storage_definition, $schema);
1496     }
1497   }
1498
1499   /**
1500    * Creates the specified entity schema indexes and keys.
1501    *
1502    * @param array $entity_schema
1503    *   The entity schema definition.
1504    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
1505    *   (optional) If a field storage definition is specified, only indexes and
1506    *   keys involving its columns will be processed. Otherwise all defined
1507    *   entity indexes and keys will be processed.
1508    */
1509   protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
1510     $schema_handler = $this->database->schema();
1511
1512     if ($storage_definition) {
1513       $table_mapping = $this->storage->getTableMapping();
1514       $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1515     }
1516
1517     $index_keys = [
1518       'indexes' => 'addIndex',
1519       'unique keys' => 'addUniqueKey',
1520     ];
1521
1522     foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
1523       // Add fields schema because database driver may depend on this data to
1524       // perform index normalization.
1525       $schema['fields'] = $entity_schema[$table_name]['fields'];
1526
1527       foreach ($index_keys as $key => $add_method) {
1528         if (!empty($schema[$key])) {
1529           foreach ($schema[$key] as $name => $specifier) {
1530             // If a set of field columns were specified we process only indexes
1531             // involving them. Only indexes for which all columns exist are
1532             // actually created.
1533             $create = FALSE;
1534             $specifier_columns = array_map(function ($item) {
1535               return is_string($item) ? $item : reset($item);
1536             }, $specifier);
1537             if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1538               $create = TRUE;
1539               foreach ($specifier_columns as $specifier_column_name) {
1540                 // This may happen when adding more than one field in the same
1541                 // update run. Eventually when all field columns have been
1542                 // created the index will be created too.
1543                 if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
1544                   $create = FALSE;
1545                   break;
1546                 }
1547               }
1548             }
1549             if ($create) {
1550               $this->{$add_method}($table_name, $name, $specifier, $schema);
1551             }
1552           }
1553         }
1554       }
1555     }
1556   }
1557
1558   /**
1559    * Deletes the specified entity schema indexes and keys.
1560    *
1561    * @param array $entity_schema_data
1562    *   The entity schema data definition.
1563    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
1564    *   (optional) If a field storage definition is specified, only indexes and
1565    *   keys involving its columns will be processed. Otherwise all defined
1566    *   entity indexes and keys will be processed.
1567    */
1568   protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
1569     $schema_handler = $this->database->schema();
1570
1571     if ($storage_definition) {
1572       $table_mapping = $this->storage->getTableMapping();
1573       $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1574     }
1575
1576     $index_keys = [
1577       'indexes' => 'dropIndex',
1578       'unique keys' => 'dropUniqueKey',
1579     ];
1580
1581     foreach ($entity_schema_data as $table_name => $schema) {
1582       foreach ($index_keys as $key => $drop_method) {
1583         if (!empty($schema[$key])) {
1584           foreach ($schema[$key] as $name => $specifier) {
1585             $specifier_columns = array_map(function ($item) {
1586               return is_string($item) ? $item : reset($item);
1587             }, $specifier);
1588             if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1589               $schema_handler->{$drop_method}($table_name, $name);
1590             }
1591           }
1592         }
1593       }
1594     }
1595   }
1596
1597   /**
1598    * Checks whether a field property has NULL values.
1599    *
1600    * @param string $table_name
1601    *   The name of the table to inspect.
1602    * @param string $column_name
1603    *   The name of the column holding the field property data.
1604    *
1605    * @return bool
1606    *   TRUE if NULL data is found, FALSE otherwise.
1607    */
1608   protected function hasNullFieldPropertyData($table_name, $column_name) {
1609     $query = $this->database->select($table_name, 't')
1610       ->fields('t', [$column_name])
1611       ->range(0, 1);
1612     $query->isNull('t.' . $column_name);
1613     $result = $query->execute()->fetchAssoc();
1614     return (bool) $result;
1615   }
1616
1617   /**
1618    * Gets the schema for a single field definition.
1619    *
1620    * Entity types may override this method in order to optimize the generated
1621    * schema for given field. While all optimizations that apply to a single
1622    * field have to be added here, all cross-field optimizations should be via
1623    * SqlContentEntityStorageSchema::getEntitySchema() instead; e.g.,
1624    * an index spanning multiple fields.
1625    *
1626    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1627    *   The storage definition of the field whose schema has to be returned.
1628    * @param string $table_name
1629    *   The name of the table columns will be added to.
1630    * @param string[] $column_mapping
1631    *   A mapping of field column names to database column names.
1632    *
1633    * @return array
1634    *   The schema definition for the table with the following keys:
1635    *   - fields: The schema definition for the each field columns.
1636    *   - indexes: The schema definition for the indexes.
1637    *   - unique keys: The schema definition for the unique keys.
1638    *   - foreign keys: The schema definition for the foreign keys.
1639    *
1640    * @throws \Drupal\Core\Field\FieldException
1641    *   Exception thrown if the schema contains reserved column names or if the
1642    *   initial values definition is invalid.
1643    */
1644   protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
1645     $schema = [];
1646     $table_mapping = $this->storage->getTableMapping();
1647     $field_schema = $storage_definition->getSchema();
1648
1649     // Check that the schema does not include forbidden column names.
1650     if (array_intersect(array_keys($field_schema['columns']), $table_mapping->getReservedColumns())) {
1651       throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
1652     }
1653
1654     $field_name = $storage_definition->getName();
1655     $base_table = $this->storage->getBaseTable();
1656
1657     // Define the initial values, if any.
1658     $initial_value = $initial_value_from_field = [];
1659     $storage_definition_is_new = empty($this->loadFieldSchemaData($storage_definition));
1660     if ($storage_definition_is_new && $storage_definition instanceof BaseFieldDefinition && $table_mapping->allowsSharedTableStorage($storage_definition)) {
1661       if (($initial_storage_value = $storage_definition->getInitialValue()) && !empty($initial_storage_value)) {
1662         // We only support initial values for fields that are stored in shared
1663         // tables (i.e. single-value fields).
1664         // @todo Implement initial value support for multi-value fields in
1665         //   https://www.drupal.org/node/2883851.
1666         $initial_value = reset($initial_storage_value);
1667       }
1668
1669       if ($initial_value_field_name = $storage_definition->getInitialValueFromField()) {
1670         // Check that the field used for populating initial values is valid. We
1671         // must use the last installed version of that, as the new field might
1672         // be created in an update function and the storage definition of the
1673         // "from" field might get changed later.
1674         $last_installed_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
1675         if (!isset($last_installed_storage_definitions[$initial_value_field_name])) {
1676           throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field $initial_value_field_name does not exist.");
1677         }
1678
1679         if ($storage_definition->getType() !== $last_installed_storage_definitions[$initial_value_field_name]->getType()) {
1680           throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field types do not match.");
1681         }
1682
1683         if (!$table_mapping->allowsSharedTableStorage($last_installed_storage_definitions[$initial_value_field_name])) {
1684           throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: Both fields have to be stored in the shared entity tables.");
1685         }
1686
1687         $initial_value_from_field = $table_mapping->getColumnNames($initial_value_field_name);
1688       }
1689     }
1690
1691     // A shared table contains rows for entities where the field is empty
1692     // (since other fields stored in the same table might not be empty), thus
1693     // the only columns that can be 'not null' are those for required
1694     // properties of required fields. For now, we only hardcode 'not null' to a
1695     // few "entity keys", in order to keep their indexes optimized.
1696     // @todo Fix this in https://www.drupal.org/node/2841291.
1697     $not_null_keys = $this->entityType->getKeys();
1698     // Label and the 'revision_translation_affected' fields are not necessarily
1699     // required.
1700     unset($not_null_keys['label'], $not_null_keys['revision_translation_affected']);
1701     // Because entity ID and revision ID are both serial fields in the base and
1702     // revision table respectively, the revision ID is not known yet, when
1703     // inserting data into the base table. Instead the revision ID in the base
1704     // table is updated after the data has been inserted into the revision
1705     // table. For this reason the revision ID field cannot be marked as NOT
1706     // NULL.
1707     if ($table_name == $base_table) {
1708       unset($not_null_keys['revision']);
1709     }
1710
1711     foreach ($column_mapping as $field_column_name => $schema_field_name) {
1712       $column_schema = $field_schema['columns'][$field_column_name];
1713
1714       $schema['fields'][$schema_field_name] = $column_schema;
1715       $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
1716
1717       // Use the initial value of the field storage, if available.
1718       if ($initial_value && isset($initial_value[$field_column_name])) {
1719         $schema['fields'][$schema_field_name]['initial'] = drupal_schema_get_field_value($column_schema, $initial_value[$field_column_name]);
1720       }
1721       elseif (!empty($initial_value_from_field)) {
1722         $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name];
1723       }
1724     }
1725
1726     if (!empty($field_schema['indexes'])) {
1727       $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
1728     }
1729
1730     if (!empty($field_schema['unique keys'])) {
1731       $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
1732     }
1733
1734     if (!empty($field_schema['foreign keys'])) {
1735       $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
1736     }
1737
1738     return $schema;
1739   }
1740
1741   /**
1742    * Adds an index for the specified field to the given schema definition.
1743    *
1744    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1745    *   The storage definition of the field for which an index should be added.
1746    * @param array $schema
1747    *   A reference to the schema array to be updated.
1748    * @param bool $not_null
1749    *   (optional) Whether to also add a 'not null' constraint to the column
1750    *   being indexed. Doing so improves index performance. Defaults to FALSE,
1751    *   in case the field needs to support NULL values.
1752    * @param int $size
1753    *   (optional) The index size. Defaults to no limit.
1754    */
1755   protected function addSharedTableFieldIndex(FieldStorageDefinitionInterface $storage_definition, &$schema, $not_null = FALSE, $size = NULL) {
1756     $name = $storage_definition->getName();
1757     $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1758     $schema['indexes'][$real_key] = [$size ? [$name, $size] : $name];
1759     if ($not_null) {
1760       $schema['fields'][$name]['not null'] = TRUE;
1761     }
1762   }
1763
1764   /**
1765    * Adds a unique key for the specified field to the given schema definition.
1766    *
1767    * Also adds a 'not null' constraint, because many databases do not reliably
1768    * support unique keys on null columns.
1769    *
1770    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1771    *   The storage definition of the field to which to add a unique key.
1772    * @param array $schema
1773    *   A reference to the schema array to be updated.
1774    */
1775   protected function addSharedTableFieldUniqueKey(FieldStorageDefinitionInterface $storage_definition, &$schema) {
1776     $name = $storage_definition->getName();
1777     $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1778     $schema['unique keys'][$real_key] = [$name];
1779     $schema['fields'][$name]['not null'] = TRUE;
1780   }
1781
1782   /**
1783    * Adds a foreign key for the specified field to the given schema definition.
1784    *
1785    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1786    *   The storage definition of the field to which to add a foreign key.
1787    * @param array $schema
1788    *   A reference to the schema array to be updated.
1789    * @param string $foreign_table
1790    *   The foreign table.
1791    * @param string $foreign_column
1792    *   The foreign column.
1793    */
1794   protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface $storage_definition, &$schema, $foreign_table, $foreign_column) {
1795     $name = $storage_definition->getName();
1796     $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1797     $schema['foreign keys'][$real_key] = [
1798       'table' => $foreign_table,
1799       'columns' => [$name => $foreign_column],
1800     ];
1801   }
1802
1803   /**
1804    * Gets the SQL schema for a dedicated table.
1805    *
1806    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1807    *   The field storage definition.
1808    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1809    *   (optional) The entity type definition. Defaults to the one returned by
1810    *   the entity manager.
1811    *
1812    * @return array
1813    *   The schema definition for the table with the following keys:
1814    *   - fields: The schema definition for the each field columns.
1815    *   - indexes: The schema definition for the indexes.
1816    *   - unique keys: The schema definition for the unique keys.
1817    *   - foreign keys: The schema definition for the foreign keys.
1818    *
1819    * @throws \Drupal\Core\Field\FieldException
1820    *   Exception thrown if the schema contains reserved column names.
1821    *
1822    * @see hook_schema()
1823    */
1824   protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
1825     $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
1826     $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
1827
1828     $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')];
1829     if ($id_definition->getType() == 'integer') {
1830       $id_schema = [
1831         'type' => 'int',
1832         'unsigned' => TRUE,
1833         'not null' => TRUE,
1834         'description' => 'The entity id this data is attached to',
1835       ];
1836     }
1837     else {
1838       $id_schema = [
1839         'type' => 'varchar_ascii',
1840         'length' => 128,
1841         'not null' => TRUE,
1842         'description' => 'The entity id this data is attached to',
1843       ];
1844     }
1845
1846     // Define the revision ID schema.
1847     if (!$this->entityType->isRevisionable()) {
1848       $revision_id_schema = $id_schema;
1849       $revision_id_schema['description'] = 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id';
1850     }
1851     elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') {
1852       $revision_id_schema = [
1853         'type' => 'int',
1854         'unsigned' => TRUE,
1855         'not null' => TRUE,
1856         'description' => 'The entity revision id this data is attached to',
1857       ];
1858     }
1859     else {
1860       $revision_id_schema = [
1861         'type' => 'varchar',
1862         'length' => 128,
1863         'not null' => TRUE,
1864         'description' => 'The entity revision id this data is attached to',
1865       ];
1866     }
1867
1868     $data_schema = [
1869       'description' => $description_current,
1870       'fields' => [
1871         'bundle' => [
1872           'type' => 'varchar_ascii',
1873           'length' => 128,
1874           'not null' => TRUE,
1875           'default' => '',
1876           'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
1877         ],
1878         'deleted' => [
1879           'type' => 'int',
1880           'size' => 'tiny',
1881           'not null' => TRUE,
1882           'default' => 0,
1883           'description' => 'A boolean indicating whether this data item has been deleted'
1884         ],
1885         'entity_id' => $id_schema,
1886         'revision_id' => $revision_id_schema,
1887         'langcode' => [
1888           'type' => 'varchar_ascii',
1889           'length' => 32,
1890           'not null' => TRUE,
1891           'default' => '',
1892           'description' => 'The language code for this data item.',
1893         ],
1894         'delta' => [
1895           'type' => 'int',
1896           'unsigned' => TRUE,
1897           'not null' => TRUE,
1898           'description' => 'The sequence number for this data item, used for multi-value fields',
1899         ],
1900       ],
1901       'primary key' => ['entity_id', 'deleted', 'delta', 'langcode'],
1902       'indexes' => [
1903         'bundle' => ['bundle'],
1904         'revision_id' => ['revision_id'],
1905       ],
1906     ];
1907
1908     // Check that the schema does not include forbidden column names.
1909     $schema = $storage_definition->getSchema();
1910     $properties = $storage_definition->getPropertyDefinitions();
1911     $table_mapping = $this->storage->getTableMapping();
1912     if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
1913       throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
1914     }
1915
1916     // Add field columns.
1917     foreach ($schema['columns'] as $column_name => $attributes) {
1918       $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1919       $data_schema['fields'][$real_name] = $attributes;
1920       // A dedicated table only contain rows for actual field values, and no
1921       // rows for entities where the field is empty. Thus, we can safely
1922       // enforce 'not null' on the columns for the field's required properties.
1923       $data_schema['fields'][$real_name]['not null'] = $properties[$column_name]->isRequired();
1924     }
1925
1926     // Add indexes.
1927     foreach ($schema['indexes'] as $index_name => $columns) {
1928       $real_name = $this->getFieldIndexName($storage_definition, $index_name);
1929       foreach ($columns as $column_name) {
1930         // Indexes can be specified as either a column name or an array with
1931         // column name and length. Allow for either case.
1932         if (is_array($column_name)) {
1933           $data_schema['indexes'][$real_name][] = [
1934             $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1935             $column_name[1],
1936           ];
1937         }
1938         else {
1939           $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1940         }
1941       }
1942     }
1943
1944     // Add unique keys.
1945     foreach ($schema['unique keys'] as $index_name => $columns) {
1946       $real_name = $this->getFieldIndexName($storage_definition, $index_name);
1947       foreach ($columns as $column_name) {
1948         // Unique keys can be specified as either a column name or an array with
1949         // column name and length. Allow for either case.
1950         if (is_array($column_name)) {
1951           $data_schema['unique keys'][$real_name][] = [
1952             $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1953             $column_name[1],
1954           ];
1955         }
1956         else {
1957           $data_schema['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1958         }
1959       }
1960     }
1961
1962     // Add foreign keys.
1963     foreach ($schema['foreign keys'] as $specifier => $specification) {
1964       $real_name = $this->getFieldIndexName($storage_definition, $specifier);
1965       $data_schema['foreign keys'][$real_name]['table'] = $specification['table'];
1966       foreach ($specification['columns'] as $column_name => $referenced) {
1967         $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1968         $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
1969       }
1970     }
1971
1972     $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema];
1973
1974     // If the entity type is revisionable, construct the revision table.
1975     $entity_type = $entity_type ?: $this->entityType;
1976     if ($entity_type->isRevisionable()) {
1977       $revision_schema = $data_schema;
1978       $revision_schema['description'] = $description_revision;
1979       $revision_schema['primary key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode'];
1980       $revision_schema['fields']['revision_id']['not null'] = TRUE;
1981       $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
1982       $dedicated_table_schema += [$table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema];
1983     }
1984
1985     return $dedicated_table_schema;
1986   }
1987
1988   /**
1989    * Gets the name to be used for the given entity index.
1990    *
1991    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1992    *   The entity type.
1993    * @param string $index
1994    *   The index column name.
1995    *
1996    * @return string
1997    *   The index name.
1998    */
1999   protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
2000     return $entity_type->id() . '__' . $index;
2001   }
2002
2003   /**
2004    * Generates an index name for a field data table.
2005    *
2006    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
2007    *   The field storage definition.
2008    * @param string $index
2009    *   The name of the index.
2010    *
2011    * @return string
2012    *   A string containing a generated index name for a field data table that is
2013    *   unique among all other fields.
2014    */
2015   protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
2016     return $storage_definition->getName() . '_' . $index;
2017   }
2018
2019   /**
2020    * Checks whether a database table is non-existent or empty.
2021    *
2022    * Empty tables can be dropped and recreated without data loss.
2023    *
2024    * @param string $table_name
2025    *   The database table to check.
2026    *
2027    * @return bool
2028    *   TRUE if the table is empty, FALSE otherwise.
2029    */
2030   protected function isTableEmpty($table_name) {
2031     return !$this->database->schema()->tableExists($table_name) ||
2032       !$this->database->select($table_name)
2033         ->countQuery()
2034         ->range(0, 1)
2035         ->execute()
2036         ->fetchField();
2037   }
2038
2039   /**
2040    * Compares schemas to check for changes in the column definitions.
2041    *
2042    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
2043    *   Current field storage definition.
2044    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
2045    *   The original field storage definition.
2046    *
2047    * @return bool
2048    *   Returns TRUE if there are schema changes in the column definitions.
2049    */
2050   protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
2051     if ($storage_definition->getColumns() != $original->getColumns()) {
2052       // Base field definitions have schema data stored in the original
2053       // definition.
2054       return TRUE;
2055     }
2056
2057     if (!$storage_definition->hasCustomStorage()) {
2058       $keys = array_flip($this->getColumnSchemaRelevantKeys());
2059       $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
2060       foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
2061         foreach ($table_schema['fields'] as $name => $spec) {
2062           $definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
2063           $stored_spec = array_intersect_key($spec, $keys);
2064           if ($definition_spec != $stored_spec) {
2065             return TRUE;
2066           }
2067         }
2068       }
2069     }
2070
2071     return FALSE;
2072   }
2073
2074   /**
2075    * Returns a list of column schema keys affecting data storage.
2076    *
2077    * When comparing schema definitions, only changes in certain properties
2078    * actually affect how data is stored and thus, if applied, may imply data
2079    * manipulation.
2080    *
2081    * @return string[]
2082    *   An array of key names.
2083    */
2084   protected function getColumnSchemaRelevantKeys() {
2085     return ['type', 'size', 'length', 'unsigned'];
2086   }
2087
2088   /**
2089    * Creates an index, dropping it if already existing.
2090    *
2091    * @param string $table
2092    *   The table name.
2093    * @param string $name
2094    *   The index name.
2095    * @param array $specifier
2096    *   The fields to index.
2097    * @param array $schema
2098    *   The table specification.
2099    *
2100    * @see \Drupal\Core\Database\Schema::addIndex()
2101    */
2102   protected function addIndex($table, $name, array $specifier, array $schema) {
2103     $schema_handler = $this->database->schema();
2104     $schema_handler->dropIndex($table, $name);
2105     $schema_handler->addIndex($table, $name, $specifier, $schema);
2106   }
2107
2108   /**
2109    * Creates a unique key, dropping it if already existing.
2110    *
2111    * @param string $table
2112    *   The table name.
2113    * @param string $name
2114    *   The index name.
2115    * @param array $specifier
2116    *   The unique fields.
2117    *
2118    * @see \Drupal\Core\Database\Schema::addUniqueKey()
2119    */
2120   protected function addUniqueKey($table, $name, array $specifier) {
2121     $schema_handler = $this->database->schema();
2122     $schema_handler->dropUniqueKey($table, $name);
2123     $schema_handler->addUniqueKey($table, $name, $specifier);
2124   }
2125
2126 }