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();
391 $actual_definition = $this->entityManager->getDefinition($entity_type->id());
392 // @todo Instead of switching the wrapped entity type, we should be able to
393 // instantiate a new table mapping for each entity type definition. See
394 // https://www.drupal.org/node/2274017.
395 $this->storage->setEntityType($entity_type);
397 // Delete entity tables.
398 foreach ($this->getEntitySchemaTables() as $table_name) {
399 if ($schema_handler->tableExists($table_name)) {
400 $schema_handler->dropTable($table_name);
404 // Delete dedicated field tables.
405 $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
406 $this->originalDefinitions = $field_storage_definitions;
407 $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
408 foreach ($field_storage_definitions as $field_storage_definition) {
409 // If we have a field having dedicated storage we need to drop it,
410 // otherwise we just remove the related schema data.
411 if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
412 $this->deleteDedicatedTableSchema($field_storage_definition);
414 elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
415 $this->deleteFieldSchemaData($field_storage_definition);
418 $this->originalDefinitions = NULL;
420 $this->storage->setEntityType($actual_definition);
422 // Delete the entity schema.
423 $this->deleteEntitySchemaData($entity_type);
429 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
430 $this->performFieldSchemaOperation('create', $storage_definition);
436 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
437 // Store original definitions so that switching between shared and dedicated
438 // field table layout works.
439 $this->performFieldSchemaOperation('update', $storage_definition, $original);
445 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
447 $has_data = $this->storage->countFieldData($storage_definition, TRUE);
449 catch (DatabaseExceptionWrapper $e) {
450 // This may happen when changing field storage schema, since we are not
451 // able to use a table mapping matching the passed storage definition.
452 // @todo Revisit this once we are able to instantiate the table mapping
453 // properly. See https://www.drupal.org/node/2274017.
457 // If the field storage does not have any data, we can safely delete its
460 $this->performFieldSchemaOperation('delete', $storage_definition);
464 // There's nothing else we can do if the field storage has a custom storage.
465 if ($storage_definition->hasCustomStorage()) {
469 // Retrieve a table mapping which contains the deleted field still.
470 $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
471 $table_mapping = $this->storage->getTableMapping($storage_definitions);
472 $field_table_name = $table_mapping->getFieldTableName($storage_definition->getName());
474 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
475 // Move the table to a unique name while the table contents are being
477 $table = $table_mapping->getDedicatedDataTableName($storage_definition);
478 $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
479 $this->database->schema()->renameTable($table, $new_table);
480 if ($this->entityType->isRevisionable()) {
481 $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
482 $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
483 $this->database->schema()->renameTable($revision_table, $revision_new_table);
487 // Move the field data from the shared table to a dedicated one in order
488 // to allow it to be purged like any other field.
489 $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName());
491 // Refresh the table mapping to use the deleted storage definition.
492 $deleted_storage_definition = $this->deletedFieldsRepository()->getFieldStorageDefinitions()[$storage_definition->getUniqueStorageIdentifier()];
493 $original_storage_definitions = [$storage_definition->getName() => $deleted_storage_definition] + $storage_definitions;
494 $table_mapping = $this->storage->getTableMapping($original_storage_definitions);
496 $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition);
497 $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName());
499 $dedicated_table_name = $table_mapping->getDedicatedDataTableName($deleted_storage_definition, TRUE);
500 $dedicated_table_name_mapping[$table_mapping->getDedicatedDataTableName($deleted_storage_definition)] = $dedicated_table_name;
501 if ($this->entityType->isRevisionable()) {
502 $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_storage_definition, TRUE);
503 $dedicated_table_name_mapping[$table_mapping->getDedicatedRevisionTableName($deleted_storage_definition)] = $dedicated_revision_table_name;
506 // Create the dedicated field tables using "deleted" table names.
507 foreach ($dedicated_table_field_schema as $name => $table) {
508 if (!$this->database->schema()->tableExists($dedicated_table_name_mapping[$name])) {
509 $this->database->schema()->createTable($dedicated_table_name_mapping[$name], $table);
512 throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.');
516 if ($this->database->supportsTransactionalDDL()) {
517 // If the database supports transactional DDL, we can go ahead and rely
518 // on it. If not, we will have to rollback manually if something fails.
519 $transaction = $this->database->startTransaction();
522 // Copy the data from the base table.
523 $this->database->insert($dedicated_table_name)
524 ->from($this->getSelectQueryForFieldStorageDeletion($field_table_name, $shared_table_field_columns, $dedicated_table_field_columns))
527 // Copy the data from the revision table.
528 if (isset($dedicated_revision_table_name)) {
529 if ($this->entityType->isTranslatable()) {
530 $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionDataTable() : $this->storage->getDataTable();
533 $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionTable() : $this->storage->getBaseTable();
535 $this->database->insert($dedicated_revision_table_name)
536 ->from($this->getSelectQueryForFieldStorageDeletion($revision_table, $shared_table_field_columns, $dedicated_table_field_columns, $field_table_name))
540 catch (\Exception $e) {
541 if (isset($transaction)) {
542 $transaction->rollBack();
545 // Delete the dedicated tables.
546 foreach ($dedicated_table_field_schema as $name => $table) {
547 $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]);
553 // Delete the field from the shared tables.
554 $this->deleteSharedTableSchema($storage_definition);
561 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
562 $this->performFieldSchemaOperation('delete', $storage_definition);
566 * Returns a SELECT query suitable for inserting data into a dedicated table.
568 * @param string $table_name
569 * The entity table name to select from.
570 * @param array $shared_table_field_columns
571 * An array of field column names for a shared table schema.
572 * @param array $dedicated_table_field_columns
573 * An array of field column names for a dedicated table schema.
574 * @param string $base_table
575 * (optional) The name of the base entity table. Defaults to NULL.
577 * @return \Drupal\Core\Database\Query\SelectInterface
578 * A database select query.
580 protected function getSelectQueryForFieldStorageDeletion($table_name, array $shared_table_field_columns, array $dedicated_table_field_columns, $base_table = NULL) {
581 // Create a SELECT query that generates a result suitable for writing into
582 // a dedicated field table.
583 $select = $this->database->select($table_name, 'entity_table');
585 // Add the bundle column.
586 if ($bundle = $this->entityType->getKey('bundle')) {
588 $select->join($base_table, 'base_table', "entity_table.{$this->entityType->getKey('id')} = %alias.{$this->entityType->getKey('id')}");
589 $select->addField('base_table', $bundle, 'bundle');
592 $select->addField('entity_table', $bundle, 'bundle');
596 $select->addExpression(':bundle', 'bundle', [':bundle' => $this->entityType->id()]);
599 // Add the deleted column.
600 $select->addExpression(':deleted', 'deleted', [':deleted' => 1]);
602 // Add the entity_id column.
603 $select->addField('entity_table', $this->entityType->getKey('id'), 'entity_id');
605 // Add the revision_id column.
606 if ($this->entityType->isRevisionable()) {
607 $select->addField('entity_table', $this->entityType->getKey('revision'), 'revision_id');
610 $select->addField('entity_table', $this->entityType->getKey('id'), 'revision_id');
613 // Add the langcode column.
614 if ($langcode = $this->entityType->getKey('langcode')) {
615 $select->addField('entity_table', $langcode, 'langcode');
618 $select->addExpression(':langcode', 'langcode', [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]);
621 // Add the delta column and set it to 0 because we are only dealing with
622 // single cardinality fields.
623 $select->addExpression(':delta', 'delta', [':delta' => 0]);
625 // Add all the dynamic field columns.
626 $or = $select->orConditionGroup();
627 foreach ($shared_table_field_columns as $field_column_name => $schema_column_name) {
628 $select->addField('entity_table', $schema_column_name, $dedicated_table_field_columns[$field_column_name]);
629 $or->isNotNull('entity_table.' . $schema_column_name);
631 $select->condition($or);
633 // Lock the table rows.
634 $select->forUpdate(TRUE);
640 * Checks that we are dealing with the correct entity type.
642 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
643 * The entity type to be checked.
646 * TRUE if the entity type matches the current one.
648 * @throws \Drupal\Core\Entity\EntityStorageException
650 protected function checkEntityType(EntityTypeInterface $entity_type) {
651 if ($entity_type->id() != $this->entityType->id()) {
652 throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
658 * Gets the entity schema for the specified entity type.
660 * Entity types may override this method in order to optimize the generated
661 * schema of the entity tables. However, only cross-field optimizations should
662 * be added here; e.g., an index spanning multiple fields. Optimizations that
663 * apply to a single field have to be added via
664 * SqlContentEntityStorageSchema::getSharedTableFieldSchema() instead.
666 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
667 * The entity type definition.
669 * (optional) If set to TRUE static cache will be ignored and a new schema
670 * array generation will be performed. Defaults to FALSE.
673 * A Schema API array describing the entity schema, excluding dedicated
676 * @throws \Drupal\Core\Field\FieldException
678 protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
679 $this->checkEntityType($entity_type);
680 $entity_type_id = $entity_type->id();
682 if (!isset($this->schema[$entity_type_id]) || $reset) {
683 // Back up the storage definition and replace it with the passed one.
684 // @todo Instead of switching the wrapped entity type, we should be able
685 // to instantiate a new table mapping for each entity type definition.
686 // See https://www.drupal.org/node/2274017.
687 $actual_definition = $this->entityManager->getDefinition($entity_type_id);
688 $this->storage->setEntityType($entity_type);
690 // Prepare basic information about the entity type.
691 $tables = $this->getEntitySchemaTables();
693 // Initialize the table schema.
694 $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type);
695 if (isset($tables['revision_table'])) {
696 $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type);
698 if (isset($tables['data_table'])) {
699 $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
701 if (isset($tables['revision_data_table'])) {
702 $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
705 // We need to act only on shared entity schema tables.
706 $table_mapping = $this->storage->getTableMapping();
707 $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
708 foreach ($table_names as $table_name) {
709 if (!isset($schema[$table_name])) {
710 $schema[$table_name] = [];
712 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
713 if (!isset($this->fieldStorageDefinitions[$field_name])) {
714 throw new FieldException("Field storage definition for '$field_name' could not be found.");
716 // Add the schema for base field definitions.
717 elseif ($table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) {
718 $column_names = $table_mapping->getColumnNames($field_name);
719 $storage_definition = $this->fieldStorageDefinitions[$field_name];
720 $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
725 // Process tables after having gathered field information.
726 $this->processBaseTable($entity_type, $schema[$tables['base_table']]);
727 if (isset($tables['revision_table'])) {
728 $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
730 if (isset($tables['data_table'])) {
731 $this->processDataTable($entity_type, $schema[$tables['data_table']]);
733 if (isset($tables['revision_data_table'])) {
734 $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
737 // Add an index for the 'published' entity key.
738 if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) {
739 $published_key = $entity_type->getKey('published');
740 if ($published_key && !$this->fieldStorageDefinitions[$published_key]->hasCustomStorage()) {
741 $published_field_table = $table_mapping->getFieldTableName($published_key);
742 $id_key = $entity_type->getKey('id');
743 if ($bundle_key = $entity_type->getKey('bundle')) {
744 $key = "{$published_key}_{$bundle_key}";
745 $columns = [$published_key, $bundle_key, $id_key];
748 $key = $published_key;
749 $columns = [$published_key, $id_key];
751 $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns;
755 $this->schema[$entity_type_id] = $schema;
757 // Restore the actual definition.
758 $this->storage->setEntityType($actual_definition);
761 return $this->schema[$entity_type_id];
765 * Gets a list of entity type tables.
768 * A list of entity type tables, keyed by table key.
770 protected function getEntitySchemaTables() {
771 return array_filter([
772 'base_table' => $this->storage->getBaseTable(),
773 'revision_table' => $this->storage->getRevisionTable(),
774 'data_table' => $this->storage->getDataTable(),
775 'revision_data_table' => $this->storage->getRevisionDataTable(),
780 * Gets entity schema definitions for index and key definitions.
782 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
783 * The entity type definition.
784 * @param array $schema
785 * The entity schema array.
788 * A stripped down version of the $schema Schema API array containing, for
789 * each table, only the key and index definitions not derived from field
790 * storage definitions.
792 protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
793 $entity_type_id = $entity_type->id();
795 // Collect all possible field schema identifiers for shared table fields.
796 // These will be used to detect entity schema data in the subsequent loop.
797 $field_schema_identifiers = [];
798 $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
799 foreach ($this->fieldStorageDefinitions as $field_name => $storage_definition) {
800 if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
801 // Make sure both base identifier names and suffixed names are listed.
802 $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name);
803 $field_schema_identifiers[$name] = $name;
804 foreach ($storage_definition->getColumns() as $key => $columns) {
805 $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
806 $field_schema_identifiers[$name] = $name;
811 // Extract entity schema data from the Schema API definition.
813 $keys = ['indexes', 'unique keys'];
814 $unused_keys = array_flip(['description', 'fields', 'foreign keys']);
815 foreach ($schema as $table_name => $table_schema) {
816 $table_schema = array_diff_key($table_schema, $unused_keys);
817 foreach ($keys as $key) {
818 // Exclude data generated from field storage definitions, we will check
820 if ($field_schema_identifiers && !empty($table_schema[$key])) {
821 $table_schema[$key] = array_diff_key($table_schema[$key], $field_schema_identifiers);
824 $schema_data[$table_name] = array_filter($table_schema);
831 * Gets an index schema array for a given field.
833 * @param string $field_name
834 * The name of the field.
835 * @param array $field_schema
836 * The schema of the field.
837 * @param string[] $column_mapping
838 * A mapping of field column names to database column names.
841 * The schema definition for the indexes.
843 protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) {
844 return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes');
848 * Gets a unique key schema array for a given field.
850 * @param string $field_name
851 * The name of the field.
852 * @param array $field_schema
853 * The schema of the field.
854 * @param string[] $column_mapping
855 * A mapping of field column names to database column names.
858 * The schema definition for the unique keys.
860 protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) {
861 return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys');
865 * Gets field schema data for the given key.
867 * @param string $field_name
868 * The name of the field.
869 * @param array $field_schema
870 * The schema of the field.
871 * @param string[] $column_mapping
872 * A mapping of field column names to database column names.
873 * @param string $schema_key
874 * The type of schema data. Either 'indexes' or 'unique keys'.
877 * The schema definition for the specified key.
879 protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) {
882 $entity_type_id = $this->entityType->id();
883 foreach ($field_schema[$schema_key] as $key => $columns) {
884 // To avoid clashes with entity-level indexes or unique keys we use
885 // "{$entity_type_id}_field__" as a prefix instead of just
886 // "{$entity_type_id}__". We additionally namespace the specifier by the
887 // field name to avoid clashes when multiple fields of the same type are
888 // added to an entity type.
889 $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
890 foreach ($columns as $column) {
891 // Allow for indexes and unique keys to specified as an array of column
893 if (is_array($column)) {
894 list($column_name, $length) = $column;
895 $data[$real_key][] = [$column_mapping[$column_name], $length];
898 $data[$real_key][] = $column_mapping[$column];
907 * Generates a safe schema identifier (name of an index, column name etc.).
909 * @param string $entity_type_id
910 * The ID of the entity type.
911 * @param string $field_name
912 * The name of the field.
913 * @param string|null $key
914 * (optional) A further key to append to the name.
917 * The field identifier name.
919 protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key = NULL) {
920 $real_key = isset($key) ? "{$entity_type_id}_field__{$field_name}__{$key}" : "{$entity_type_id}_field__{$field_name}";
921 // Limit the string to 48 characters, keeping a 16 characters margin for db
923 if (strlen($real_key) > 48) {
924 // Use a shorter separator, a truncated entity_type, and a hash of the
926 // Truncate to the same length for the current and revision tables.
927 $entity_type = substr($entity_type_id, 0, 36);
928 $field_hash = substr(hash('sha256', $real_key), 0, 10);
929 $real_key = $entity_type . '__' . $field_hash;
935 * Gets field foreign keys.
937 * @param string $field_name
938 * The name of the field.
939 * @param array $field_schema
940 * The schema of the field.
941 * @param string[] $column_mapping
942 * A mapping of field column names to database column names.
945 * The schema definition for the foreign keys.
947 protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) {
950 foreach ($field_schema['foreign keys'] as $specifier => $specification) {
951 // To avoid clashes with entity-level foreign keys we use
952 // "{$entity_type_id}_field__" as a prefix instead of just
953 // "{$entity_type_id}__". We additionally namespace the specifier by the
954 // field name to avoid clashes when multiple fields of the same type are
955 // added to an entity type.
956 $entity_type_id = $this->entityType->id();
957 $real_specifier = "{$entity_type_id}_field__{$field_name}__{$specifier}";
958 $foreign_keys[$real_specifier]['table'] = $specification['table'];
959 foreach ($specification['columns'] as $column => $referenced) {
960 $foreign_keys[$real_specifier]['columns'][$column_mapping[$column]] = $referenced;
964 return $foreign_keys;
968 * Loads stored schema data for the given entity type definition.
970 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
971 * The entity type definition.
974 * The entity schema data array.
976 protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
977 return $this->installedStorageSchema()->get($entity_type->id() . '.entity_schema_data', []);
981 * Stores schema data for the given entity type definition.
983 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
984 * The entity type definition.
985 * @param array $schema
986 * The entity schema data array.
988 protected function saveEntitySchemaData(EntityTypeInterface $entity_type, $schema) {
989 $data = $this->getEntitySchemaData($entity_type, $schema);
990 $this->installedStorageSchema()->set($entity_type->id() . '.entity_schema_data', $data);
994 * Deletes schema data for the given entity type definition.
996 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
997 * The entity type definition.
999 protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
1000 $this->installedStorageSchema()->delete($entity_type->id() . '.entity_schema_data');
1004 * Loads stored schema data for the given field storage definition.
1006 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1007 * The field storage definition.
1010 * The field schema data array.
1012 protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
1013 return $this->installedStorageSchema()->get($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), []);
1017 * Stores schema data for the given field storage definition.
1019 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1020 * The field storage definition.
1021 * @param array $schema
1022 * The field schema data array.
1024 protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) {
1025 $this->processFieldStorageSchema($schema);
1026 $this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema);
1030 * Deletes schema data for the given field storage definition.
1032 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1033 * The field storage definition.
1035 protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
1036 $this->installedStorageSchema()->delete($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName());
1040 * Initializes common information for a base table.
1042 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1046 * A partial schema array for the base table.
1048 protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
1049 $entity_type_id = $entity_type->id();
1052 'description' => "The base table for $entity_type_id entities.",
1053 'primary key' => [$entity_type->getKey('id')],
1055 'foreign keys' => [],
1058 if ($entity_type->hasKey('revision')) {
1059 $revision_key = $entity_type->getKey('revision');
1060 $key_name = $this->getEntityIndexName($entity_type, $revision_key);
1061 $schema['unique keys'][$key_name] = [$revision_key];
1062 $schema['foreign keys'][$entity_type_id . '__revision'] = [
1063 'table' => $this->storage->getRevisionTable(),
1064 'columns' => [$revision_key => $revision_key],
1068 $this->addTableDefaults($schema);
1074 * Initializes common information for a revision table.
1076 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1080 * A partial schema array for the revision table.
1082 protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) {
1083 $entity_type_id = $entity_type->id();
1084 $id_key = $entity_type->getKey('id');
1085 $revision_key = $entity_type->getKey('revision');
1088 'description' => "The revision table for $entity_type_id entities.",
1089 'primary key' => [$revision_key],
1092 $entity_type_id . '__revisioned' => [
1093 'table' => $this->storage->getBaseTable(),
1094 'columns' => [$id_key => $id_key],
1099 $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = [$id_key];
1101 $this->addTableDefaults($schema);
1107 * Initializes common information for a data table.
1109 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1113 * A partial schema array for the data table.
1115 protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
1116 $entity_type_id = $entity_type->id();
1117 $id_key = $entity_type->getKey('id');
1120 'description' => "The data table for $entity_type_id entities.",
1121 'primary key' => [$id_key, $entity_type->getKey('langcode')],
1123 $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
1126 $entity_type_id => [
1127 'table' => $this->storage->getBaseTable(),
1128 'columns' => [$id_key => $id_key],
1133 if ($entity_type->hasKey('revision')) {
1134 $key = $entity_type->getKey('revision');
1135 $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = [$key];
1138 $this->addTableDefaults($schema);
1144 * Initializes common information for a revision data table.
1146 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1150 * A partial schema array for the revision data table.
1152 protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) {
1153 $entity_type_id = $entity_type->id();
1154 $id_key = $entity_type->getKey('id');
1155 $revision_key = $entity_type->getKey('revision');
1158 'description' => "The revision data table for $entity_type_id entities.",
1159 'primary key' => [$revision_key, $entity_type->getKey('langcode')],
1161 $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
1164 $entity_type_id => [
1165 'table' => $this->storage->getBaseTable(),
1166 'columns' => [$id_key => $id_key],
1168 $entity_type_id . '__revision' => [
1169 'table' => $this->storage->getRevisionTable(),
1170 'columns' => [$revision_key => $revision_key],
1175 $this->addTableDefaults($schema);
1181 * Adds defaults to a table schema definition.
1184 * The schema definition array for a single table, passed by reference.
1186 protected function addTableDefaults(&$schema) {
1189 'unique keys' => [],
1191 'foreign keys' => [],
1196 * Processes the gathered schema for a base table.
1198 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1200 * @param array $schema
1201 * The table schema, passed by reference.
1204 * A partial schema array for the base table.
1206 protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1207 $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
1211 * Processes the gathered schema for a base table.
1213 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1215 * @param array $schema
1216 * The table schema, passed by reference.
1219 * A partial schema array for the base table.
1221 protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1222 $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
1226 * Processes the gathered schema for a base table.
1228 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1230 * @param array $schema
1231 * The table schema, passed by reference.
1234 * A partial schema array for the base table.
1236 protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1237 // Marking the respective fields as NOT NULL makes the indexes more
1239 $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1243 * Processes the gathered schema for a base table.
1245 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1247 * @param array $schema
1248 * The table schema, passed by reference.
1251 * A partial schema array for the base table.
1253 protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1254 // Marking the respective fields as NOT NULL makes the indexes more
1256 $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1260 * Processes the specified entity key.
1262 * @param array $schema
1263 * The table schema, passed by reference.
1264 * @param string $key
1265 * The entity key name.
1267 protected function processIdentifierSchema(&$schema, $key) {
1268 if ($schema['fields'][$key]['type'] == 'int') {
1269 $schema['fields'][$key]['type'] = 'serial';
1271 $schema['fields'][$key]['not null'] = TRUE;
1272 unset($schema['fields'][$key]['default']);
1276 * Processes the schema for a field storage definition.
1278 * @param array &$field_storage_schema
1279 * An array that contains the schema data for a field storage definition.
1281 protected function processFieldStorageSchema(array &$field_storage_schema) {
1282 // Clean up some schema properties that should not be taken into account
1283 // after a field storage has been created.
1284 foreach ($field_storage_schema as $table_name => $table_schema) {
1285 foreach ($table_schema['fields'] as $key => $schema) {
1286 unset($field_storage_schema[$table_name]['fields'][$key]['initial']);
1287 unset($field_storage_schema[$table_name]['fields'][$key]['initial_from_field']);
1293 * Performs the specified operation on a field.
1295 * This figures out whether the field is stored in a dedicated or shared table
1296 * and forwards the call to the proper handler.
1298 * @param string $operation
1299 * The name of the operation to be performed.
1300 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1301 * The field storage definition.
1302 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1303 * (optional) The original field storage definition. This is relevant (and
1304 * required) only for updates. Defaults to NULL.
1306 protected function performFieldSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) {
1307 $table_mapping = $this->storage->getTableMapping();
1308 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1309 $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original);
1311 elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1312 $this->{$operation . 'SharedTableSchema'}($storage_definition, $original);
1317 * Creates the schema for a field stored in a dedicated table.
1319 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1320 * The storage definition of the field being created.
1322 protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1323 $schema = $this->getDedicatedTableSchema($storage_definition);
1324 foreach ($schema as $name => $table) {
1325 // Check if the table exists because it might already have been
1326 // created as part of the earlier entity type update event.
1327 if (!$this->database->schema()->tableExists($name)) {
1328 $this->database->schema()->createTable($name, $table);
1331 $this->saveFieldSchemaData($storage_definition, $schema);
1335 * Creates the schema for a field stored in a shared table.
1337 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1338 * The storage definition of the field being created.
1339 * @param bool $only_save
1340 * (optional) Whether to skip modification of database tables and only save
1341 * the schema data for future comparison. For internal use only. This is
1342 * used by onEntityTypeCreate() after it has already fully created the
1345 protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) {
1346 $created_field_name = $storage_definition->getName();
1347 $table_mapping = $this->storage->getTableMapping();
1348 $column_names = $table_mapping->getColumnNames($created_field_name);
1349 $schema_handler = $this->database->schema();
1350 $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
1352 // Iterate over the mapped table to find the ones that will host the created
1355 foreach ($shared_table_names as $table_name) {
1356 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1357 if ($field_name == $created_field_name) {
1358 // Create field columns.
1359 $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1361 foreach ($schema[$table_name]['fields'] as $name => $specifier) {
1362 // Check if the field exists because it might already have been
1363 // created as part of the earlier entity type update event.
1364 if (!$schema_handler->fieldExists($table_name, $name)) {
1365 $schema_handler->addField($table_name, $name, $specifier);
1368 if (!empty($schema[$table_name]['indexes'])) {
1369 foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
1370 // Check if the index exists because it might already have been
1371 // created as part of the earlier entity type update event.
1372 $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
1375 if (!empty($schema[$table_name]['unique keys'])) {
1376 foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
1377 $schema_handler->addUniqueKey($table_name, $name, $specifier);
1381 // After creating the field schema skip to the next table.
1387 $this->saveFieldSchemaData($storage_definition, $schema);
1390 // Make sure any entity index involving this field is re-created if
1392 $this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
1397 * Deletes the schema for a field stored in a dedicated table.
1399 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1400 * The storage definition of the field being deleted.
1402 protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1403 $table_mapping = $this->storage->getTableMapping();
1404 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
1405 if ($this->database->schema()->tableExists($table_name)) {
1406 $this->database->schema()->dropTable($table_name);
1408 if ($this->entityType->isRevisionable()) {
1409 $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $storage_definition->isDeleted());
1410 if ($this->database->schema()->tableExists($revision_table_name)) {
1411 $this->database->schema()->dropTable($revision_table_name);
1414 $this->deleteFieldSchemaData($storage_definition);
1418 * Deletes the schema for a field stored in a shared table.
1420 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1421 * The storage definition of the field being deleted.
1423 protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1424 // Make sure any entity index involving this field is dropped.
1425 $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
1427 $deleted_field_name = $storage_definition->getName();
1428 $table_mapping = $this->storage->getTableMapping(
1429 $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1431 $column_names = $table_mapping->getColumnNames($deleted_field_name);
1432 $schema_handler = $this->database->schema();
1433 $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
1435 // Iterate over the mapped table to find the ones that host the deleted
1437 foreach ($shared_table_names as $table_name) {
1438 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1439 if ($field_name == $deleted_field_name) {
1440 $schema = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1442 // Drop indexes and unique keys first.
1443 if (!empty($schema['indexes'])) {
1444 foreach ($schema['indexes'] as $name => $specifier) {
1445 $schema_handler->dropIndex($table_name, $name);
1448 if (!empty($schema['unique keys'])) {
1449 foreach ($schema['unique keys'] as $name => $specifier) {
1450 $schema_handler->dropUniqueKey($table_name, $name);
1454 foreach ($column_names as $column_name) {
1455 $schema_handler->dropField($table_name, $column_name);
1457 // After deleting the field schema skip to the next table.
1463 $this->deleteFieldSchemaData($storage_definition);
1467 * Updates the schema for a field stored in a shared table.
1469 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1470 * The storage definition of the field being updated.
1471 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1472 * The original storage definition; i.e., the definition before the update.
1474 * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
1475 * Thrown when the update to the field is forbidden.
1476 * @throws \Exception
1477 * Rethrown exception if the table recreation fails.
1479 protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1480 if (!$this->storage->countFieldData($original, TRUE)) {
1481 // There is no data. Re-create the tables completely.
1482 if ($this->database->supportsTransactionalDDL()) {
1483 // If the database supports transactional DDL, we can go ahead and rely
1484 // on it. If not, we will have to rollback manually if something fails.
1485 $transaction = $this->database->startTransaction();
1488 // Since there is no data we may be switching from a shared table schema
1489 // to a dedicated table schema, hence we should use the proper API.
1490 $this->performFieldSchemaOperation('delete', $original);
1491 $this->performFieldSchemaOperation('create', $storage_definition);
1493 catch (\Exception $e) {
1494 if ($this->database->supportsTransactionalDDL()) {
1495 $transaction->rollBack();
1499 $this->performFieldSchemaOperation('create', $original);
1505 if ($this->hasColumnChanges($storage_definition, $original)) {
1506 throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
1508 // There is data, so there are no column changes. Drop all the prior
1509 // indexes and create all the new ones, except for all the priors that
1511 $table_mapping = $this->storage->getTableMapping();
1512 $table = $table_mapping->getDedicatedDataTableName($original);
1513 $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
1515 // Get the field schemas.
1516 $schema = $storage_definition->getSchema();
1517 $original_schema = $original->getSchema();
1519 // Gets the SQL schema for a dedicated tables.
1520 $actual_schema = $this->getDedicatedTableSchema($storage_definition);
1522 foreach ($original_schema['indexes'] as $name => $columns) {
1523 if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
1524 $real_name = $this->getFieldIndexName($storage_definition, $name);
1525 $this->database->schema()->dropIndex($table, $real_name);
1526 $this->database->schema()->dropIndex($revision_table, $real_name);
1529 $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1530 $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1531 foreach ($schema['indexes'] as $name => $columns) {
1532 if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
1533 $real_name = $this->getFieldIndexName($storage_definition, $name);
1535 foreach ($columns as $column_name) {
1536 // Indexes can be specified as either a column name or an array with
1537 // column name and length. Allow for either case.
1538 if (is_array($column_name)) {
1540 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1545 $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1548 // Check if the index exists because it might already have been
1549 // created as part of the earlier entity type update event.
1550 $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
1551 $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
1554 $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
1559 * Updates the schema for a field stored in a shared table.
1561 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1562 * The storage definition of the field being updated.
1563 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1564 * The original storage definition; i.e., the definition before the update.
1566 * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
1567 * Thrown when the update to the field is forbidden.
1568 * @throws \Exception
1569 * Rethrown exception if the table recreation fails.
1571 protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1572 if (!$this->storage->countFieldData($original, TRUE)) {
1573 if ($this->database->supportsTransactionalDDL()) {
1574 // If the database supports transactional DDL, we can go ahead and rely
1575 // on it. If not, we will have to rollback manually if something fails.
1576 $transaction = $this->database->startTransaction();
1579 // Since there is no data we may be switching from a dedicated table
1580 // to a schema table schema, hence we should use the proper API.
1581 $this->performFieldSchemaOperation('delete', $original);
1582 $this->performFieldSchemaOperation('create', $storage_definition);
1584 catch (\Exception $e) {
1585 if ($this->database->supportsTransactionalDDL()) {
1586 $transaction->rollBack();
1589 // Recreate original schema.
1590 $this->createSharedTableSchema($original);
1596 if ($this->hasColumnChanges($storage_definition, $original)) {
1597 throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
1600 $updated_field_name = $storage_definition->getName();
1601 $table_mapping = $this->storage->getTableMapping();
1602 $column_names = $table_mapping->getColumnNames($updated_field_name);
1603 $schema_handler = $this->database->schema();
1605 // Iterate over the mapped table to find the ones that host the deleted
1607 $original_schema = $this->loadFieldSchemaData($original);
1609 foreach ($table_mapping->getTableNames() as $table_name) {
1610 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1611 if ($field_name == $updated_field_name) {
1612 $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1614 // Handle NOT NULL constraints.
1615 foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
1616 $not_null = !empty($specifier['not null']);
1617 $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
1618 if ($not_null !== $original_not_null) {
1619 if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
1620 throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
1622 $column_schema = $original_schema[$table_name]['fields'][$column_name];
1623 $column_schema['not null'] = $not_null;
1624 $schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
1628 // Drop original indexes and unique keys.
1629 if (!empty($original_schema[$table_name]['indexes'])) {
1630 foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
1631 $schema_handler->dropIndex($table_name, $name);
1634 if (!empty($original_schema[$table_name]['unique keys'])) {
1635 foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) {
1636 $schema_handler->dropUniqueKey($table_name, $name);
1639 // Create new indexes and unique keys.
1640 if (!empty($schema[$table_name]['indexes'])) {
1641 foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
1642 // Check if the index exists because it might already have been
1643 // created as part of the earlier entity type update event.
1644 $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
1648 if (!empty($schema[$table_name]['unique keys'])) {
1649 foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
1650 $schema_handler->addUniqueKey($table_name, $name, $specifier);
1653 // After deleting the field schema skip to the next table.
1658 $this->saveFieldSchemaData($storage_definition, $schema);
1663 * Creates the specified entity schema indexes and keys.
1665 * @param array $entity_schema
1666 * The entity schema definition.
1667 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
1668 * (optional) If a field storage definition is specified, only indexes and
1669 * keys involving its columns will be processed. Otherwise all defined
1670 * entity indexes and keys will be processed.
1672 protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
1673 $schema_handler = $this->database->schema();
1675 if ($storage_definition) {
1676 $table_mapping = $this->storage->getTableMapping();
1677 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1681 'indexes' => 'addIndex',
1682 'unique keys' => 'addUniqueKey',
1685 foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
1686 // Add fields schema because database driver may depend on this data to
1687 // perform index normalization.
1688 $schema['fields'] = $entity_schema[$table_name]['fields'];
1690 foreach ($index_keys as $key => $add_method) {
1691 if (!empty($schema[$key])) {
1692 foreach ($schema[$key] as $name => $specifier) {
1693 // If a set of field columns were specified we process only indexes
1694 // involving them. Only indexes for which all columns exist are
1695 // actually created.
1697 $specifier_columns = array_map(function ($item) {
1698 return is_string($item) ? $item : reset($item);
1700 if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1702 foreach ($specifier_columns as $specifier_column_name) {
1703 // This may happen when adding more than one field in the same
1704 // update run. Eventually when all field columns have been
1705 // created the index will be created too.
1706 if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
1713 $this->{$add_method}($table_name, $name, $specifier, $schema);
1722 * Deletes the specified entity schema indexes and keys.
1724 * @param array $entity_schema_data
1725 * The entity schema data definition.
1726 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
1727 * (optional) If a field storage definition is specified, only indexes and
1728 * keys involving its columns will be processed. Otherwise all defined
1729 * entity indexes and keys will be processed.
1731 protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
1732 $schema_handler = $this->database->schema();
1734 if ($storage_definition) {
1735 $table_mapping = $this->storage->getTableMapping();
1736 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1740 'indexes' => 'dropIndex',
1741 'unique keys' => 'dropUniqueKey',
1744 foreach ($entity_schema_data as $table_name => $schema) {
1745 foreach ($index_keys as $key => $drop_method) {
1746 if (!empty($schema[$key])) {
1747 foreach ($schema[$key] as $name => $specifier) {
1748 $specifier_columns = array_map(function ($item) {
1749 return is_string($item) ? $item : reset($item);
1751 if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1752 $schema_handler->{$drop_method}($table_name, $name);
1761 * Checks whether a field property has NULL values.
1763 * @param string $table_name
1764 * The name of the table to inspect.
1765 * @param string $column_name
1766 * The name of the column holding the field property data.
1769 * TRUE if NULL data is found, FALSE otherwise.
1771 protected function hasNullFieldPropertyData($table_name, $column_name) {
1772 $query = $this->database->select($table_name, 't')
1773 ->fields('t', [$column_name])
1775 $query->isNull('t.' . $column_name);
1776 $result = $query->execute()->fetchAssoc();
1777 return (bool) $result;
1781 * Gets the schema for a single field definition.
1783 * Entity types may override this method in order to optimize the generated
1784 * schema for given field. While all optimizations that apply to a single
1785 * field have to be added here, all cross-field optimizations should be via
1786 * SqlContentEntityStorageSchema::getEntitySchema() instead; e.g.,
1787 * an index spanning multiple fields.
1789 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1790 * The storage definition of the field whose schema has to be returned.
1791 * @param string $table_name
1792 * The name of the table columns will be added to.
1793 * @param string[] $column_mapping
1794 * A mapping of field column names to database column names.
1797 * The schema definition for the table with the following keys:
1798 * - fields: The schema definition for the each field columns.
1799 * - indexes: The schema definition for the indexes.
1800 * - unique keys: The schema definition for the unique keys.
1801 * - foreign keys: The schema definition for the foreign keys.
1803 * @throws \Drupal\Core\Field\FieldException
1804 * Exception thrown if the schema contains reserved column names or if the
1805 * initial values definition is invalid.
1807 protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
1809 $table_mapping = $this->storage->getTableMapping();
1810 $field_schema = $storage_definition->getSchema();
1812 // Check that the schema does not include forbidden column names.
1813 if (array_intersect(array_keys($field_schema['columns']), $table_mapping->getReservedColumns())) {
1814 throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
1817 $field_name = $storage_definition->getName();
1818 $base_table = $this->storage->getBaseTable();
1820 // Define the initial values, if any.
1821 $initial_value = $initial_value_from_field = [];
1822 $storage_definition_is_new = empty($this->loadFieldSchemaData($storage_definition));
1823 if ($storage_definition_is_new && $storage_definition instanceof BaseFieldDefinition && $table_mapping->allowsSharedTableStorage($storage_definition)) {
1824 if (($initial_storage_value = $storage_definition->getInitialValue()) && !empty($initial_storage_value)) {
1825 // We only support initial values for fields that are stored in shared
1826 // tables (i.e. single-value fields).
1827 // @todo Implement initial value support for multi-value fields in
1828 // https://www.drupal.org/node/2883851.
1829 $initial_value = reset($initial_storage_value);
1832 if ($initial_value_field_name = $storage_definition->getInitialValueFromField()) {
1833 // Check that the field used for populating initial values is valid. We
1834 // must use the last installed version of that, as the new field might
1835 // be created in an update function and the storage definition of the
1836 // "from" field might get changed later.
1837 $last_installed_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
1838 if (!isset($last_installed_storage_definitions[$initial_value_field_name])) {
1839 throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field $initial_value_field_name does not exist.");
1842 if ($storage_definition->getType() !== $last_installed_storage_definitions[$initial_value_field_name]->getType()) {
1843 throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field types do not match.");
1846 if (!$table_mapping->allowsSharedTableStorage($last_installed_storage_definitions[$initial_value_field_name])) {
1847 throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: Both fields have to be stored in the shared entity tables.");
1850 $initial_value_from_field = $table_mapping->getColumnNames($initial_value_field_name);
1854 // A shared table contains rows for entities where the field is empty
1855 // (since other fields stored in the same table might not be empty), thus
1856 // the only columns that can be 'not null' are those for required
1857 // properties of required fields. For now, we only hardcode 'not null' to a
1858 // few "entity keys", in order to keep their indexes optimized.
1859 // @todo Fix this in https://www.drupal.org/node/2841291.
1860 $not_null_keys = $this->entityType->getKeys();
1861 // Label and the 'revision_translation_affected' fields are not necessarily
1863 unset($not_null_keys['label'], $not_null_keys['revision_translation_affected']);
1864 // Because entity ID and revision ID are both serial fields in the base and
1865 // revision table respectively, the revision ID is not known yet, when
1866 // inserting data into the base table. Instead the revision ID in the base
1867 // table is updated after the data has been inserted into the revision
1868 // table. For this reason the revision ID field cannot be marked as NOT
1870 if ($table_name == $base_table) {
1871 unset($not_null_keys['revision']);
1874 foreach ($column_mapping as $field_column_name => $schema_field_name) {
1875 $column_schema = $field_schema['columns'][$field_column_name];
1877 $schema['fields'][$schema_field_name] = $column_schema;
1878 $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
1880 // Use the initial value of the field storage, if available.
1881 if ($initial_value && isset($initial_value[$field_column_name])) {
1882 $schema['fields'][$schema_field_name]['initial'] = drupal_schema_get_field_value($column_schema, $initial_value[$field_column_name]);
1884 elseif (!empty($initial_value_from_field)) {
1885 $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name];
1889 if (!empty($field_schema['indexes'])) {
1890 $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
1893 if (!empty($field_schema['unique keys'])) {
1894 $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
1897 if (!empty($field_schema['foreign keys'])) {
1898 $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
1905 * Adds an index for the specified field to the given schema definition.
1907 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1908 * The storage definition of the field for which an index should be added.
1909 * @param array $schema
1910 * A reference to the schema array to be updated.
1911 * @param bool $not_null
1912 * (optional) Whether to also add a 'not null' constraint to the column
1913 * being indexed. Doing so improves index performance. Defaults to FALSE,
1914 * in case the field needs to support NULL values.
1916 * (optional) The index size. Defaults to no limit.
1918 protected function addSharedTableFieldIndex(FieldStorageDefinitionInterface $storage_definition, &$schema, $not_null = FALSE, $size = NULL) {
1919 $name = $storage_definition->getName();
1920 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1921 $schema['indexes'][$real_key] = [$size ? [$name, $size] : $name];
1923 $schema['fields'][$name]['not null'] = TRUE;
1928 * Adds a unique key for the specified field to the given schema definition.
1930 * Also adds a 'not null' constraint, because many databases do not reliably
1931 * support unique keys on null columns.
1933 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1934 * The storage definition of the field to which to add a unique key.
1935 * @param array $schema
1936 * A reference to the schema array to be updated.
1938 protected function addSharedTableFieldUniqueKey(FieldStorageDefinitionInterface $storage_definition, &$schema) {
1939 $name = $storage_definition->getName();
1940 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1941 $schema['unique keys'][$real_key] = [$name];
1942 $schema['fields'][$name]['not null'] = TRUE;
1946 * Adds a foreign key for the specified field to the given schema definition.
1948 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1949 * The storage definition of the field to which to add a foreign key.
1950 * @param array $schema
1951 * A reference to the schema array to be updated.
1952 * @param string $foreign_table
1953 * The foreign table.
1954 * @param string $foreign_column
1955 * The foreign column.
1957 protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface $storage_definition, &$schema, $foreign_table, $foreign_column) {
1958 $name = $storage_definition->getName();
1959 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1960 $schema['foreign keys'][$real_key] = [
1961 'table' => $foreign_table,
1962 'columns' => [$name => $foreign_column],
1967 * Gets the SQL schema for a dedicated table.
1969 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1970 * The field storage definition.
1971 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1972 * (optional) The entity type definition. Defaults to the one returned by
1973 * the entity manager.
1976 * The schema definition for the table with the following keys:
1977 * - fields: The schema definition for the each field columns.
1978 * - indexes: The schema definition for the indexes.
1979 * - unique keys: The schema definition for the unique keys.
1980 * - foreign keys: The schema definition for the foreign keys.
1982 * @throws \Drupal\Core\Field\FieldException
1983 * Exception thrown if the schema contains reserved column names.
1985 * @see hook_schema()
1987 protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
1988 $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
1989 $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
1991 $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')];
1992 if ($id_definition->getType() == 'integer') {
1997 'description' => 'The entity id this data is attached to',
2002 'type' => 'varchar_ascii',
2005 'description' => 'The entity id this data is attached to',
2009 // Define the revision ID schema.
2010 if (!$this->entityType->isRevisionable()) {
2011 $revision_id_schema = $id_schema;
2012 $revision_id_schema['description'] = 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id';
2014 elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') {
2015 $revision_id_schema = [
2019 'description' => 'The entity revision id this data is attached to',
2023 $revision_id_schema = [
2024 'type' => 'varchar',
2027 'description' => 'The entity revision id this data is attached to',
2032 'description' => $description_current,
2035 'type' => 'varchar_ascii',
2039 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
2046 'description' => 'A boolean indicating whether this data item has been deleted'
2048 'entity_id' => $id_schema,
2049 'revision_id' => $revision_id_schema,
2051 'type' => 'varchar_ascii',
2055 'description' => 'The language code for this data item.',
2061 'description' => 'The sequence number for this data item, used for multi-value fields',
2064 'primary key' => ['entity_id', 'deleted', 'delta', 'langcode'],
2066 'bundle' => ['bundle'],
2067 'revision_id' => ['revision_id'],
2071 // Check that the schema does not include forbidden column names.
2072 $schema = $storage_definition->getSchema();
2073 $properties = $storage_definition->getPropertyDefinitions();
2074 $table_mapping = $this->storage->getTableMapping();
2075 if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
2076 throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
2079 // Add field columns.
2080 foreach ($schema['columns'] as $column_name => $attributes) {
2081 $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
2082 $data_schema['fields'][$real_name] = $attributes;
2083 // A dedicated table only contain rows for actual field values, and no
2084 // rows for entities where the field is empty. Thus, we can safely
2085 // enforce 'not null' on the columns for the field's required properties.
2086 $data_schema['fields'][$real_name]['not null'] = $properties[$column_name]->isRequired();
2090 foreach ($schema['indexes'] as $index_name => $columns) {
2091 $real_name = $this->getFieldIndexName($storage_definition, $index_name);
2092 foreach ($columns as $column_name) {
2093 // Indexes can be specified as either a column name or an array with
2094 // column name and length. Allow for either case.
2095 if (is_array($column_name)) {
2096 $data_schema['indexes'][$real_name][] = [
2097 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
2102 $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
2108 foreach ($schema['unique keys'] as $index_name => $columns) {
2109 $real_name = $this->getFieldIndexName($storage_definition, $index_name);
2110 foreach ($columns as $column_name) {
2111 // Unique keys can be specified as either a column name or an array with
2112 // column name and length. Allow for either case.
2113 if (is_array($column_name)) {
2114 $data_schema['unique keys'][$real_name][] = [
2115 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
2120 $data_schema['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
2125 // Add foreign keys.
2126 foreach ($schema['foreign keys'] as $specifier => $specification) {
2127 $real_name = $this->getFieldIndexName($storage_definition, $specifier);
2128 $data_schema['foreign keys'][$real_name]['table'] = $specification['table'];
2129 foreach ($specification['columns'] as $column_name => $referenced) {
2130 $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name);
2131 $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
2135 $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema];
2137 // If the entity type is revisionable, construct the revision table.
2138 $entity_type = $entity_type ?: $this->entityType;
2139 if ($entity_type->isRevisionable()) {
2140 $revision_schema = $data_schema;
2141 $revision_schema['description'] = $description_revision;
2142 $revision_schema['primary key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode'];
2143 $revision_schema['fields']['revision_id']['not null'] = TRUE;
2144 $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
2145 $dedicated_table_schema += [$table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema];
2148 return $dedicated_table_schema;
2152 * Gets the name to be used for the given entity index.
2154 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
2156 * @param string $index
2157 * The index column name.
2162 protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
2163 return $entity_type->id() . '__' . $index;
2167 * Generates an index name for a field data table.
2169 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
2170 * The field storage definition.
2171 * @param string $index
2172 * The name of the index.
2175 * A string containing a generated index name for a field data table that is
2176 * unique among all other fields.
2178 protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
2179 return $storage_definition->getName() . '_' . $index;
2183 * Checks whether a database table is non-existent or empty.
2185 * Empty tables can be dropped and recreated without data loss.
2187 * @param string $table_name
2188 * The database table to check.
2191 * TRUE if the table is empty, FALSE otherwise.
2193 protected function isTableEmpty($table_name) {
2194 return !$this->database->schema()->tableExists($table_name) ||
2195 !$this->database->select($table_name)
2203 * Compares schemas to check for changes in the column definitions.
2205 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
2206 * Current field storage definition.
2207 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
2208 * The original field storage definition.
2211 * Returns TRUE if there are schema changes in the column definitions.
2213 protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
2214 if ($storage_definition->getColumns() != $original->getColumns()) {
2215 // Base field definitions have schema data stored in the original
2220 if (!$storage_definition->hasCustomStorage()) {
2221 $keys = array_flip($this->getColumnSchemaRelevantKeys());
2222 $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
2223 foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
2224 foreach ($table_schema['fields'] as $name => $spec) {
2225 $definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
2226 $stored_spec = array_intersect_key($spec, $keys);
2227 if ($definition_spec != $stored_spec) {
2238 * Returns a list of column schema keys affecting data storage.
2240 * When comparing schema definitions, only changes in certain properties
2241 * actually affect how data is stored and thus, if applied, may imply data
2245 * An array of key names.
2247 protected function getColumnSchemaRelevantKeys() {
2248 return ['type', 'size', 'length', 'unsigned'];
2252 * Creates an index, dropping it if already existing.
2254 * @param string $table
2256 * @param string $name
2258 * @param array $specifier
2259 * The fields to index.
2260 * @param array $schema
2261 * The table specification.
2263 * @see \Drupal\Core\Database\Schema::addIndex()
2265 protected function addIndex($table, $name, array $specifier, array $schema) {
2266 $schema_handler = $this->database->schema();
2267 $schema_handler->dropIndex($table, $name);
2268 $schema_handler->addIndex($table, $name, $specifier, $schema);
2272 * Creates a unique key, dropping it if already existing.
2274 * @param string $table
2276 * @param string $name
2278 * @param array $specifier
2279 * The unique fields.
2281 * @see \Drupal\Core\Database\Schema::addUniqueKey()
2283 protected function addUniqueKey($table, $name, array $specifier) {
2284 $schema_handler = $this->database->schema();
2285 $schema_handler->dropUniqueKey($table, $name);
2286 $schema_handler->addUniqueKey($table, $name, $specifier);