3 namespace Drupal\Core\Entity\Sql;
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;
21 * Defines a schema handler that supports revisionable, translatable entities.
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
28 class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorageSchemaInterface {
30 use DependencySerializationTrait;
35 * @var \Drupal\Core\Entity\EntityManagerInterface
37 protected $entityManager;
40 * The entity type this schema builder is responsible for.
42 * @var \Drupal\Core\Entity\ContentEntityTypeInterface
44 protected $entityType;
47 * The storage field definitions for this entity type.
49 * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
51 protected $fieldStorageDefinitions;
54 * The original storage field definitions for this entity type. Used during
55 * field schema updates.
57 * @var \Drupal\Core\Field\FieldDefinitionInterface[]
59 protected $originalDefinitions;
62 * The storage object for the given entity type.
64 * @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage
69 * A static cache of the generated schema array.
76 * The database connection to be used.
78 * @var \Drupal\Core\Database\Connection
83 * The key-value collection for tracking installed storage schema.
85 * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
87 protected $installedStorageSchema;
90 * The deleted fields repository.
92 * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
94 protected $deletedFieldsRepository;
97 * Constructs a SqlContentEntityStorageSchema.
99 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
100 * The entity manager.
101 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $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.
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;
117 * Gets the keyvalue collection for tracking the installed schema.
119 * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
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.
125 protected function installedStorageSchema() {
126 if (!isset($this->installedStorageSchema)) {
127 $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
129 return $this->installedStorageSchema;
133 * Gets the deleted fields repository.
135 * @return \Drupal\Core\Field\DeletedFieldsRepositoryInterface
136 * The deleted fields repository.
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.
142 protected function deletedFieldsRepository() {
143 if (!isset($this->deletedFieldsRepository)) {
144 $this->deletedFieldsRepository = \Drupal::service('entity_field.deleted_fields_repository');
146 return $this->deletedFieldsRepository;
152 public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
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);
160 * Detects whether there is a change in the shared table structure.
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.
168 * Returns TRUE if either the revisionable or translatable flag changes or
169 * a table has been renamed.
171 protected function hasSharedTableStructureChange(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
173 $entity_type->isRevisionable() != $original->isRevisionable() ||
174 $entity_type->isTranslatable() != $original->isTranslatable() ||
175 $this->hasSharedTableNameChanges($entity_type, $original);
179 * Detects whether any table name got renamed in an entity type update.
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.
187 * Returns TRUE if there have been changes.
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());
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
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());
208 public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
209 $table_mapping = $this->storage->getTableMapping();
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)
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
229 $current_schema = $this->getSchemaFromStorageDefinition($storage_definition);
230 $this->processFieldStorageSchema($current_schema);
231 $installed_schema = $this->loadFieldSchemaData($original);
232 $this->processFieldStorageSchema($installed_schema);
234 return $current_schema != $installed_schema;
238 * Gets the schema data for the given field storage definition.
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.
247 protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
248 assert(!$storage_definition->hasCustomStorage());
249 $table_mapping = $this->storage->getTableMapping();
251 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
252 $schema = $this->getDedicatedTableSchema($storage_definition);
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);
269 public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
270 // Check if the entity type specifies that data migration is being handled
272 if ($entity_type->get('requires_data_migration') === FALSE) {
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)) {
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)) {
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();
297 public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
298 return !$this->storage->countFieldData($original, TRUE);
304 public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
305 $this->checkEntityType($entity_type);
306 $schema_handler = $this->database->schema();
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);
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);
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);
329 // Save data about entity indexes and keys.
330 $this->saveEntitySchemaData($entity_type, $schema);
336 public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
337 $this->checkEntityType($entity_type);
338 $this->checkEntityType($original);
340 // If no schema changes are needed, we don't need to do anything.
341 if (!$this->requiresEntityStorageSchemaChanges($entity_type, $original)) {
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.');
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();
358 $this->onEntityTypeDelete($original);
359 $this->onEntityTypeCreate($entity_type);
361 catch (\Exception $e) {
362 if ($this->database->supportsTransactionalDDL()) {
363 $transaction->rollBack();
366 // Recreate original schema.
367 $this->onEntityTypeCreate($original);
373 // Drop original indexes and unique keys.
374 $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
376 // Create new indexes and unique keys.
377 $entity_schema = $this->getEntitySchema($entity_type, TRUE);
378 $this->createEntitySchemaIndexes($entity_schema);
380 // Store the updated entity schema.
381 $this->saveEntitySchemaData($entity_type, $entity_schema);
388 public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
389 $this->checkEntityType($entity_type);
390 $schema_handler = $this->database->schema();
392 $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
393 $table_mapping = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions);
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);
402 // Delete the field schema data.
403 foreach ($field_storage_definitions as $field_storage_definition) {
404 $this->deleteFieldSchemaData($field_storage_definition);
407 // Delete the entity schema.
408 $this->deleteEntitySchemaData($entity_type);
414 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
415 $this->performFieldSchemaOperation('create', $storage_definition);
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);
430 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
432 $has_data = $this->storage->countFieldData($storage_definition, TRUE);
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.
442 // If the field storage does not have any data, we can safely delete its
445 $this->performFieldSchemaOperation('delete', $storage_definition);
449 // There's nothing else we can do if the field storage has a custom storage.
450 if ($storage_definition->hasCustomStorage()) {
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());
459 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
460 // Move the table to a unique name while the table contents are being
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);
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());
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);
481 $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition);
482 $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName());
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;
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);
497 throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.');
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();
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))
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();
518 $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionTable() : $this->storage->getBaseTable();
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))
525 catch (\Exception $e) {
526 if (isset($transaction)) {
527 $transaction->rollBack();
530 // Delete the dedicated tables.
531 foreach ($dedicated_table_field_schema as $name => $table) {
532 $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]);
538 // Delete the field from the shared tables.
539 $this->deleteSharedTableSchema($storage_definition);
546 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
547 $this->performFieldSchemaOperation('delete', $storage_definition);
551 * Returns a SELECT query suitable for inserting data into a dedicated table.
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.
562 * @return \Drupal\Core\Database\Query\SelectInterface
563 * A database select query.
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');
570 // Add the bundle column.
571 if ($bundle = $this->entityType->getKey('bundle')) {
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');
577 $select->addField('entity_table', $bundle, 'bundle');
581 $select->addExpression(':bundle', 'bundle', [':bundle' => $this->entityType->id()]);
584 // Add the deleted column.
585 $select->addExpression(':deleted', 'deleted', [':deleted' => 1]);
587 // Add the entity_id column.
588 $select->addField('entity_table', $this->entityType->getKey('id'), 'entity_id');
590 // Add the revision_id column.
591 if ($this->entityType->isRevisionable()) {
592 $select->addField('entity_table', $this->entityType->getKey('revision'), 'revision_id');
595 $select->addField('entity_table', $this->entityType->getKey('id'), 'revision_id');
598 // Add the langcode column.
599 if ($langcode = $this->entityType->getKey('langcode')) {
600 $select->addField('entity_table', $langcode, 'langcode');
603 $select->addExpression(':langcode', 'langcode', [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]);
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]);
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);
616 $select->condition($or);
618 // Lock the table rows.
619 $select->forUpdate(TRUE);
625 * Checks that we are dealing with the correct entity type.
627 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
628 * The entity type to be checked.
631 * TRUE if the entity type matches the current one.
633 * @throws \Drupal\Core\Entity\EntityStorageException
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()}");
643 * Gets the entity schema for the specified entity type.
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.
651 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
652 * The entity type definition.
654 * (optional) If set to TRUE static cache will be ignored and a new schema
655 * array generation will be performed. Defaults to FALSE.
658 * A Schema API array describing the entity schema, excluding dedicated
661 * @throws \Drupal\Core\Field\FieldException
663 protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
664 $this->checkEntityType($entity_type);
665 $entity_type_id = $entity_type->id();
667 if (!isset($this->schema[$entity_type_id]) || $reset) {
668 // Prepare basic information about the entity type.
669 $tables = $this->getEntitySchemaTables();
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);
676 if (isset($tables['data_table'])) {
677 $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
679 if (isset($tables['revision_data_table'])) {
680 $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
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] = [];
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.");
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));
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']]);
708 if (isset($tables['data_table'])) {
709 $this->processDataTable($entity_type, $schema[$tables['data_table']]);
711 if (isset($tables['revision_data_table'])) {
712 $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
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];
726 $key = $published_key;
727 $columns = [$published_key, $id_key];
729 $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns;
733 $this->schema[$entity_type_id] = $schema;
736 return $this->schema[$entity_type_id];
740 * Gets a list of entity type tables.
743 * A list of entity type tables, keyed by table key.
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(),
755 * Gets entity schema definitions for index and key definitions.
757 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
758 * The entity type definition.
759 * @param array $schema
760 * The entity schema 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.
767 protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
768 $entity_type_id = $entity_type->id();
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;
786 // Extract entity schema data from the Schema API definition.
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
795 if ($field_schema_identifiers && !empty($table_schema[$key])) {
796 $table_schema[$key] = array_diff_key($table_schema[$key], $field_schema_identifiers);
799 $schema_data[$table_name] = array_filter($table_schema);
806 * Gets an index schema array for a given field.
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.
816 * The schema definition for the indexes.
818 protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) {
819 return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes');
823 * Gets a unique key schema array for a given field.
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.
833 * The schema definition for the unique keys.
835 protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) {
836 return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys');
840 * Gets field schema data for the given key.
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'.
852 * The schema definition for the specified key.
854 protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) {
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
868 if (is_array($column)) {
869 list($column_name, $length) = $column;
870 $data[$real_key][] = [$column_mapping[$column_name], $length];
873 $data[$real_key][] = $column_mapping[$column];
882 * Generates a safe schema identifier (name of an index, column name etc.).
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.
892 * The field identifier name.
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
898 if (strlen($real_key) > 48) {
899 // Use a shorter separator, a truncated entity_type, and a hash of the
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;
910 * Gets field foreign keys.
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.
920 * The schema definition for the foreign keys.
922 protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) {
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;
939 return $foreign_keys;
943 * Loads stored schema data for the given entity type definition.
945 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
946 * The entity type definition.
949 * The entity schema data array.
951 protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
952 return $this->installedStorageSchema()->get($entity_type->id() . '.entity_schema_data', []);
956 * Stores schema data for the given entity type definition.
958 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
959 * The entity type definition.
960 * @param array $schema
961 * The entity schema data array.
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);
969 * Deletes schema data for the given entity type definition.
971 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
972 * The entity type definition.
974 protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
975 $this->installedStorageSchema()->delete($entity_type->id() . '.entity_schema_data');
979 * Loads stored schema data for the given field storage definition.
981 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
982 * The field storage definition.
985 * The field schema data array.
987 protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
988 return $this->installedStorageSchema()->get($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), []);
992 * Stores schema data for the given field storage definition.
994 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
995 * The field storage definition.
996 * @param array $schema
997 * The field schema data array.
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);
1005 * Deletes schema data for the given field storage definition.
1007 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1008 * The field storage definition.
1010 protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
1011 $this->installedStorageSchema()->delete($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName());
1015 * Initializes common information for a base table.
1017 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1021 * A partial schema array for the base table.
1023 protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
1024 $entity_type_id = $entity_type->id();
1027 'description' => "The base table for $entity_type_id entities.",
1028 'primary key' => [$entity_type->getKey('id')],
1030 'foreign keys' => [],
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],
1043 $this->addTableDefaults($schema);
1049 * Initializes common information for a revision table.
1051 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1055 * A partial schema array for the revision table.
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');
1063 'description' => "The revision table for $entity_type_id entities.",
1064 'primary key' => [$revision_key],
1067 $entity_type_id . '__revisioned' => [
1068 'table' => $this->storage->getBaseTable(),
1069 'columns' => [$id_key => $id_key],
1074 $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = [$id_key];
1076 $this->addTableDefaults($schema);
1082 * Initializes common information for a data table.
1084 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1088 * A partial schema array for the data table.
1090 protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
1091 $entity_type_id = $entity_type->id();
1092 $id_key = $entity_type->getKey('id');
1095 'description' => "The data table for $entity_type_id entities.",
1096 'primary key' => [$id_key, $entity_type->getKey('langcode')],
1098 $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
1101 $entity_type_id => [
1102 'table' => $this->storage->getBaseTable(),
1103 'columns' => [$id_key => $id_key],
1108 if ($entity_type->hasKey('revision')) {
1109 $key = $entity_type->getKey('revision');
1110 $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = [$key];
1113 $this->addTableDefaults($schema);
1119 * Initializes common information for a revision data table.
1121 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1125 * A partial schema array for the revision data table.
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');
1133 'description' => "The revision data table for $entity_type_id entities.",
1134 'primary key' => [$revision_key, $entity_type->getKey('langcode')],
1136 $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
1139 $entity_type_id => [
1140 'table' => $this->storage->getBaseTable(),
1141 'columns' => [$id_key => $id_key],
1143 $entity_type_id . '__revision' => [
1144 'table' => $this->storage->getRevisionTable(),
1145 'columns' => [$revision_key => $revision_key],
1150 $this->addTableDefaults($schema);
1156 * Adds defaults to a table schema definition.
1159 * The schema definition array for a single table, passed by reference.
1161 protected function addTableDefaults(&$schema) {
1164 'unique keys' => [],
1166 'foreign keys' => [],
1171 * Processes the gathered schema for a base table.
1173 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1175 * @param array $schema
1176 * The table schema, passed by reference.
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'));
1186 * Processes the gathered schema for a base table.
1188 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1190 * @param array $schema
1191 * The table schema, passed by reference.
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'));
1201 * Processes the gathered schema for a base table.
1203 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1205 * @param array $schema
1206 * The table schema, passed by reference.
1209 * A partial schema array for the base table.
1211 protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1212 // Marking the respective fields as NOT NULL makes the indexes more
1214 $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1218 * Processes the gathered schema for a base table.
1220 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1222 * @param array $schema
1223 * The table schema, passed by reference.
1226 * A partial schema array for the base table.
1228 protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1229 // Marking the respective fields as NOT NULL makes the indexes more
1231 $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1235 * Processes the specified entity key.
1237 * @param array $schema
1238 * The table schema, passed by reference.
1239 * @param string $key
1240 * The entity key name.
1242 protected function processIdentifierSchema(&$schema, $key) {
1243 if ($schema['fields'][$key]['type'] == 'int') {
1244 $schema['fields'][$key]['type'] = 'serial';
1246 $schema['fields'][$key]['not null'] = TRUE;
1247 unset($schema['fields'][$key]['default']);
1251 * Processes the schema for a field storage definition.
1253 * @param array &$field_storage_schema
1254 * An array that contains the schema data for a field storage definition.
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']);
1268 * Performs the specified operation on a field.
1270 * This figures out whether the field is stored in a dedicated or shared table
1271 * and forwards the call to the proper handler.
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.
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);
1286 elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1287 $this->{$operation . 'SharedTableSchema'}($storage_definition, $original);
1292 * Creates the schema for a field stored in a dedicated table.
1294 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1295 * The storage definition of the field being created.
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);
1306 $this->saveFieldSchemaData($storage_definition, $schema);
1310 * Creates the schema for a field stored in a shared table.
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
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());
1327 // Iterate over the mapped table to find the ones that will host the created
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);
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()
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']];
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);
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]);
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);
1368 // After creating the field schema skip to the next table.
1374 $this->saveFieldSchemaData($storage_definition, $schema);
1377 // Make sure any entity index involving this field is re-created if
1379 $this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
1384 * Deletes the schema for a field stored in a dedicated table.
1386 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1387 * The storage definition of the field being deleted.
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);
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);
1401 $this->deleteFieldSchemaData($storage_definition);
1405 * Deletes the schema for a field stored in a shared table.
1407 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1408 * The storage definition of the field being deleted.
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);
1414 $deleted_field_name = $storage_definition->getName();
1415 $table_mapping = $this->storage->getTableMapping(
1416 $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
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());
1422 // Iterate over the mapped table to find the ones that host the deleted
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);
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);
1435 if (!empty($schema['unique keys'])) {
1436 foreach ($schema['unique keys'] as $name => $specifier) {
1437 $schema_handler->dropUniqueKey($table_name, $name);
1441 foreach ($column_names as $column_name) {
1442 $schema_handler->dropField($table_name, $column_name);
1444 // After deleting the field schema skip to the next table.
1450 $this->deleteFieldSchemaData($storage_definition);
1454 * Updates the schema for a field stored in a shared table.
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.
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.
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();
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);
1480 catch (\Exception $e) {
1481 if ($this->database->supportsTransactionalDDL()) {
1482 $transaction->rollBack();
1486 $this->performFieldSchemaOperation('create', $original);
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.');
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
1498 $table_mapping = $this->storage->getTableMapping();
1499 $table = $table_mapping->getDedicatedDataTableName($original);
1500 $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
1502 // Get the field schemas.
1503 $schema = $storage_definition->getSchema();
1504 $original_schema = $original->getSchema();
1506 // Gets the SQL schema for a dedicated tables.
1507 $actual_schema = $this->getDedicatedTableSchema($storage_definition);
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);
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);
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)) {
1527 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1532 $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
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]);
1541 $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
1546 * Updates the schema for a field stored in a shared table.
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.
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.
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();
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);
1571 catch (\Exception $e) {
1572 if ($this->database->supportsTransactionalDDL()) {
1573 $transaction->rollBack();
1576 // Recreate original schema.
1577 $this->createSharedTableSchema($original);
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.');
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();
1592 // Iterate over the mapped table to find the ones that host the deleted
1594 $original_schema = $this->loadFieldSchemaData($original);
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);
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.");
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);
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);
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);
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]);
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);
1640 // After deleting the field schema skip to the next table.
1645 $this->saveFieldSchemaData($storage_definition, $schema);
1650 * Creates the specified entity schema indexes and keys.
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.
1659 protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
1660 $schema_handler = $this->database->schema();
1662 if ($storage_definition) {
1663 $table_mapping = $this->storage->getTableMapping();
1664 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1668 'indexes' => 'addIndex',
1669 'unique keys' => 'addUniqueKey',
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'];
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.
1684 $specifier_columns = array_map(function ($item) {
1685 return is_string($item) ? $item : reset($item);
1687 if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
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)) {
1700 $this->{$add_method}($table_name, $name, $specifier, $schema);
1709 * Deletes the specified entity schema indexes and keys.
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.
1718 protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
1719 $schema_handler = $this->database->schema();
1721 if ($storage_definition) {
1722 $table_mapping = $this->storage->getTableMapping();
1723 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1727 'indexes' => 'dropIndex',
1728 'unique keys' => 'dropUniqueKey',
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);
1738 if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1739 $schema_handler->{$drop_method}($table_name, $name);
1748 * Checks whether a field property has NULL values.
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.
1756 * TRUE if NULL data is found, FALSE otherwise.
1758 protected function hasNullFieldPropertyData($table_name, $column_name) {
1759 $query = $this->database->select($table_name, 't')
1760 ->fields('t', [$column_name])
1762 $query->isNull('t.' . $column_name);
1763 $result = $query->execute()->fetchAssoc();
1764 return (bool) $result;
1768 * Gets the schema for a single field definition.
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.
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.
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.
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.
1794 protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
1796 $table_mapping = $this->storage->getTableMapping();
1797 $field_schema = $storage_definition->getSchema();
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()}");
1804 $field_name = $storage_definition->getName();
1805 $base_table = $this->storage->getBaseTable();
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);
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.");
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.");
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.");
1837 $initial_value_from_field = $table_mapping->getColumnNames($initial_value_field_name);
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
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
1857 if ($table_name == $base_table) {
1858 unset($not_null_keys['revision']);
1861 foreach ($column_mapping as $field_column_name => $schema_field_name) {
1862 $column_schema = $field_schema['columns'][$field_column_name];
1864 $schema['fields'][$schema_field_name] = $column_schema;
1865 $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
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]);
1871 if (!empty($initial_value_from_field)) {
1872 $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name];
1876 if (!empty($field_schema['indexes'])) {
1877 $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
1880 if (!empty($field_schema['unique keys'])) {
1881 $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
1884 if (!empty($field_schema['foreign keys'])) {
1885 $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
1892 * Adds an index for the specified field to the given schema definition.
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.
1903 * (optional) The index size. Defaults to no limit.
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];
1910 $schema['fields'][$name]['not null'] = TRUE;
1915 * Adds a unique key for the specified field to the given schema definition.
1917 * Also adds a 'not null' constraint, because many databases do not reliably
1918 * support unique keys on null columns.
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.
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;
1933 * Adds a foreign key for the specified field to the given schema definition.
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.
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],
1954 * Gets the SQL schema for a dedicated table.
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.
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.
1969 * @throws \Drupal\Core\Field\FieldException
1970 * Exception thrown if the schema contains reserved column names.
1972 * @see hook_schema()
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()}.";
1978 $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')];
1979 if ($id_definition->getType() == 'integer') {
1984 'description' => 'The entity id this data is attached to',
1989 'type' => 'varchar_ascii',
1992 'description' => 'The entity id this data is attached to',
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';
2001 elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') {
2002 $revision_id_schema = [
2006 'description' => 'The entity revision id this data is attached to',
2010 $revision_id_schema = [
2011 'type' => 'varchar',
2014 'description' => 'The entity revision id this data is attached to',
2019 'description' => $description_current,
2022 'type' => 'varchar_ascii',
2026 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
2033 'description' => 'A boolean indicating whether this data item has been deleted',
2035 'entity_id' => $id_schema,
2036 'revision_id' => $revision_id_schema,
2038 'type' => 'varchar_ascii',
2042 'description' => 'The language code for this data item.',
2048 'description' => 'The sequence number for this data item, used for multi-value fields',
2051 'primary key' => ['entity_id', 'deleted', 'delta', 'langcode'],
2053 'bundle' => ['bundle'],
2054 'revision_id' => ['revision_id'],
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()}");
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();
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]),
2089 $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
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]),
2107 $data_schema['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
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;
2122 $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema];
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];
2135 return $dedicated_table_schema;
2139 * Gets the name to be used for the given entity index.
2141 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
2143 * @param string $index
2144 * The index column name.
2149 protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
2150 return $entity_type->id() . '__' . $index;
2154 * Generates an index name for a field data table.
2156 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
2157 * The field storage definition.
2158 * @param string $index
2159 * The name of the index.
2162 * A string containing a generated index name for a field data table that is
2163 * unique among all other fields.
2165 protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
2166 return $storage_definition->getName() . '_' . $index;
2170 * Checks whether a database table is non-existent or empty.
2172 * Empty tables can be dropped and recreated without data loss.
2174 * @param string $table_name
2175 * The database table to check.
2178 * TRUE if the table is empty, FALSE otherwise.
2180 protected function isTableEmpty($table_name) {
2181 return !$this->database->schema()->tableExists($table_name) ||
2182 !$this->database->select($table_name)
2190 * Compares schemas to check for changes in the column definitions.
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.
2198 * Returns TRUE if there are schema changes in the column definitions.
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
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) {
2225 * Returns a list of column schema keys affecting data storage.
2227 * When comparing schema definitions, only changes in certain properties
2228 * actually affect how data is stored and thus, if applied, may imply data
2232 * An array of key names.
2234 protected function getColumnSchemaRelevantKeys() {
2235 return ['type', 'size', 'length', 'unsigned'];
2239 * Creates an index, dropping it if already existing.
2241 * @param string $table
2243 * @param string $name
2245 * @param array $specifier
2246 * The fields to index.
2247 * @param array $schema
2248 * The table specification.
2250 * @see \Drupal\Core\Database\Schema::addIndex()
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);
2259 * Creates a unique key, dropping it if already existing.
2261 * @param string $table
2263 * @param string $name
2265 * @param array $specifier
2266 * The unique fields.
2268 * @see \Drupal\Core\Database\Schema::addUniqueKey()
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);