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