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