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