3 namespace Drupal\Core\Entity\Sql;
5 use Drupal\Core\Database\Connection;
6 use Drupal\Core\Database\DatabaseException;
7 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
8 use Drupal\Core\Entity\ContentEntityTypeInterface;
9 use Drupal\Core\Entity\EntityManagerInterface;
10 use Drupal\Core\Entity\EntityPublishedInterface;
11 use Drupal\Core\Entity\EntityStorageException;
12 use Drupal\Core\Entity\EntityTypeInterface;
13 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
14 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
15 use Drupal\Core\Field\FieldException;
16 use Drupal\Core\Field\FieldStorageDefinitionInterface;
17 use Drupal\field\FieldStorageConfigInterface;
20 * Defines a schema handler that supports revisionable, translatable entities.
22 * Entity types may extend this class and optimize the generated schema for all
23 * entity base tables by overriding getEntitySchema() for cross-field
24 * optimizations and getSharedTableFieldSchema() for optimizations applying to
27 class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorageSchemaInterface {
29 use DependencySerializationTrait;
34 * @var \Drupal\Core\Entity\EntityManagerInterface
36 protected $entityManager;
39 * The entity type this schema builder is responsible for.
41 * @var \Drupal\Core\Entity\ContentEntityTypeInterface
43 protected $entityType;
46 * The storage field definitions for this entity type.
48 * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
50 protected $fieldStorageDefinitions;
53 * The original storage field definitions for this entity type. Used during
54 * field schema updates.
56 * @var \Drupal\Core\Field\FieldDefinitionInterface[]
58 protected $originalDefinitions;
61 * The storage object for the given entity type.
63 * @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage
68 * A static cache of the generated schema array.
75 * The database connection to be used.
77 * @var \Drupal\Core\Database\Connection
82 * The key-value collection for tracking installed storage schema.
84 * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
86 protected $installedStorageSchema;
89 * Constructs a SqlContentEntityStorageSchema.
91 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
93 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
95 * @param \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage
96 * The storage of the entity type. This must be an SQL-based storage.
97 * @param \Drupal\Core\Database\Connection $database
98 * The database connection to be used.
100 public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, SqlContentEntityStorage $storage, Connection $database) {
101 $this->entityManager = $entity_manager;
102 $this->entityType = $entity_type;
103 $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
104 $this->storage = $storage;
105 $this->database = $database;
109 * Gets the keyvalue collection for tracking the installed schema.
111 * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
113 * @todo Inject this dependency in the constructor once this class can be
114 * instantiated as a regular entity handler:
115 * https://www.drupal.org/node/2332857.
117 protected function installedStorageSchema() {
118 if (!isset($this->installedStorageSchema)) {
119 $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
121 return $this->installedStorageSchema;
127 public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
129 $this->hasSharedTableStructureChange($entity_type, $original) ||
130 // Detect changes in key or index definitions.
131 $this->getEntitySchemaData($entity_type, $this->getEntitySchema($entity_type, TRUE)) != $this->loadEntitySchemaData($original);
135 * Detects whether there is a change in the shared table structure.
137 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
138 * The new entity type.
139 * @param \Drupal\Core\Entity\EntityTypeInterface $original
140 * The origin entity type.
143 * Returns TRUE if either the revisionable or translatable flag changes or
144 * a table has been renamed.
146 protected function hasSharedTableStructureChange(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
148 $entity_type->isRevisionable() != $original->isRevisionable() ||
149 $entity_type->isTranslatable() != $original->isTranslatable() ||
150 $this->hasSharedTableNameChanges($entity_type, $original);
154 * Detects whether any table name got renamed in an entity type update.
156 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
157 * The new entity type.
158 * @param \Drupal\Core\Entity\EntityTypeInterface $original
159 * The origin entity type.
162 * Returns TRUE if there have been changes.
164 protected function hasSharedTableNameChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
165 $base_table = $this->database->schema()->tableExists($entity_type->getBaseTable());
166 $data_table = $this->database->schema()->tableExists($entity_type->getDataTable());
167 $revision_table = $this->database->schema()->tableExists($entity_type->getRevisionTable());
168 $revision_data_table = $this->database->schema()->tableExists($entity_type->getRevisionDataTable());
170 // We first check if the new table already exists because the storage might
171 // have created it even though it wasn't specified in the entity type
174 (!$base_table && $entity_type->getBaseTable() != $original->getBaseTable()) ||
175 (!$data_table && $entity_type->getDataTable() != $original->getDataTable()) ||
176 (!$revision_table && $entity_type->getRevisionTable() != $original->getRevisionTable()) ||
177 (!$revision_data_table && $entity_type->getRevisionDataTable() != $original->getRevisionDataTable());
183 public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
184 $table_mapping = $this->storage->getTableMapping();
187 $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
188 $storage_definition->getSchema() != $original->getSchema() ||
189 $storage_definition->isRevisionable() != $original->isRevisionable() ||
190 $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) ||
191 $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original)
196 if ($storage_definition->hasCustomStorage()) {
197 // The field has custom storage, so we don't know if a schema change is
198 // needed or not, but since per the initial checks earlier in this
199 // function, nothing about the definition changed that we manage, we
204 return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original);
208 * Gets the schema data for the given field storage definition.
210 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
211 * The field storage definition. The field that must not have custom
212 * storage, i.e. the storage must take care of storing the field.
217 protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
218 assert('!$storage_definition->hasCustomStorage();');
219 $table_mapping = $this->storage->getTableMapping();
221 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
222 $schema = $this->getDedicatedTableSchema($storage_definition);
224 elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
225 $field_name = $storage_definition->getName();
226 foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
227 if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
228 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
229 $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
239 public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
240 // If the original storage has existing entities, or it is impossible to
241 // determine if that is the case, require entity data to be migrated.
242 $original_storage_class = $original->getStorageClass();
243 if (!class_exists($original_storage_class)) {
247 // Data migration is not needed when only indexes changed, as they can be
248 // applied if there is data.
249 if (!$this->hasSharedTableStructureChange($entity_type, $original)) {
253 // Use the original entity type since the storage has not been updated.
254 $original_storage = $this->entityManager->createHandlerInstance($original_storage_class, $original);
255 return $original_storage->hasData();
261 public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
262 return !$this->storage->countFieldData($original, TRUE);
268 public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
269 $this->checkEntityType($entity_type);
270 $schema_handler = $this->database->schema();
272 // Create entity tables.
273 $schema = $this->getEntitySchema($entity_type, TRUE);
274 foreach ($schema as $table_name => $table_schema) {
275 if (!$schema_handler->tableExists($table_name)) {
276 $schema_handler->createTable($table_name, $table_schema);
280 // Create dedicated field tables.
281 $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type->id());
282 $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
283 foreach ($field_storage_definitions as $field_storage_definition) {
284 if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
285 $this->createDedicatedTableSchema($field_storage_definition);
287 elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
288 // The shared tables are already fully created, but we need to save the
289 // per-field schema definitions for later use.
290 $this->createSharedTableSchema($field_storage_definition, TRUE);
294 // Save data about entity indexes and keys.
295 $this->saveEntitySchemaData($entity_type, $schema);
301 public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
302 $this->checkEntityType($entity_type);
303 $this->checkEntityType($original);
305 // If no schema changes are needed, we don't need to do anything.
306 if (!$this->requiresEntityStorageSchemaChanges($entity_type, $original)) {
310 // If a migration is required, we can't proceed.
311 if ($this->requiresEntityDataMigration($entity_type, $original)) {
312 throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type->id() . ') with data.');
315 // If we have no data just recreate the entity schema from scratch.
316 if ($this->isTableEmpty($this->storage->getBaseTable())) {
317 if ($this->database->supportsTransactionalDDL()) {
318 // If the database supports transactional DDL, we can go ahead and rely
319 // on it. If not, we will have to rollback manually if something fails.
320 $transaction = $this->database->startTransaction();
323 $this->onEntityTypeDelete($original);
324 $this->onEntityTypeCreate($entity_type);
326 catch (\Exception $e) {
327 if ($this->database->supportsTransactionalDDL()) {
328 $transaction->rollBack();
331 // Recreate original schema.
332 $this->onEntityTypeCreate($original);
338 // Drop original indexes and unique keys.
339 $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
341 // Create new indexes and unique keys.
342 $entity_schema = $this->getEntitySchema($entity_type, TRUE);
343 $this->createEntitySchemaIndexes($entity_schema);
345 // Store the updated entity schema.
346 $this->saveEntitySchemaData($entity_type, $entity_schema);
353 public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
354 $this->checkEntityType($entity_type);
355 $schema_handler = $this->database->schema();
356 $actual_definition = $this->entityManager->getDefinition($entity_type->id());
357 // @todo Instead of switching the wrapped entity type, we should be able to
358 // instantiate a new table mapping for each entity type definition. See
359 // https://www.drupal.org/node/2274017.
360 $this->storage->setEntityType($entity_type);
362 // Delete entity tables.
363 foreach ($this->getEntitySchemaTables() as $table_name) {
364 if ($schema_handler->tableExists($table_name)) {
365 $schema_handler->dropTable($table_name);
369 // Delete dedicated field tables.
370 $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
371 $this->originalDefinitions = $field_storage_definitions;
372 $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
373 foreach ($field_storage_definitions as $field_storage_definition) {
374 // If we have a field having dedicated storage we need to drop it,
375 // otherwise we just remove the related schema data.
376 if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
377 $this->deleteDedicatedTableSchema($field_storage_definition);
379 elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
380 $this->deleteFieldSchemaData($field_storage_definition);
383 $this->originalDefinitions = NULL;
385 $this->storage->setEntityType($actual_definition);
387 // Delete the entity schema.
388 $this->deleteEntitySchemaData($entity_type);
394 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
395 $this->performFieldSchemaOperation('create', $storage_definition);
401 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
402 // Store original definitions so that switching between shared and dedicated
403 // field table layout works.
404 $this->originalDefinitions = $this->fieldStorageDefinitions;
405 $this->originalDefinitions[$original->getName()] = $original;
406 $this->performFieldSchemaOperation('update', $storage_definition, $original);
407 $this->originalDefinitions = NULL;
413 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
414 // Only configurable fields currently support purging, so prevent deletion
415 // of ones we can't purge if they have existing data.
416 // @todo Add purging to all fields: https://www.drupal.org/node/2282119.
418 if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
419 throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
422 catch (DatabaseException $e) {
423 // This may happen when changing field storage schema, since we are not
424 // able to use a table mapping matching the passed storage definition.
425 // @todo Revisit this once we are able to instantiate the table mapping
426 // properly. See https://www.drupal.org/node/2274017.
430 // Retrieve a table mapping which contains the deleted field still.
431 $table_mapping = $this->storage->getTableMapping(
432 $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
434 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
435 // Move the table to a unique name while the table contents are being
437 $table = $table_mapping->getDedicatedDataTableName($storage_definition);
438 $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
439 $this->database->schema()->renameTable($table, $new_table);
440 if ($this->entityType->isRevisionable()) {
441 $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
442 $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
443 $this->database->schema()->renameTable($revision_table, $revision_new_table);
447 // @todo Remove when finalizePurge() is invoked from the outside for all
448 // fields: https://www.drupal.org/node/2282119.
449 if (!($storage_definition instanceof FieldStorageConfigInterface)) {
450 $this->performFieldSchemaOperation('delete', $storage_definition);
457 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
458 $this->performFieldSchemaOperation('delete', $storage_definition);
462 * Checks that we are dealing with the correct entity type.
464 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
465 * The entity type to be checked.
468 * TRUE if the entity type matches the current one.
470 * @throws \Drupal\Core\Entity\EntityStorageException
472 protected function checkEntityType(EntityTypeInterface $entity_type) {
473 if ($entity_type->id() != $this->entityType->id()) {
474 throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
480 * Gets the entity schema for the specified entity type.
482 * Entity types may override this method in order to optimize the generated
483 * schema of the entity tables. However, only cross-field optimizations should
484 * be added here; e.g., an index spanning multiple fields. Optimizations that
485 * apply to a single field have to be added via
486 * SqlContentEntityStorageSchema::getSharedTableFieldSchema() instead.
488 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
489 * The entity type definition.
491 * (optional) If set to TRUE static cache will be ignored and a new schema
492 * array generation will be performed. Defaults to FALSE.
495 * A Schema API array describing the entity schema, excluding dedicated
498 * @throws \Drupal\Core\Field\FieldException
500 protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
501 $this->checkEntityType($entity_type);
502 $entity_type_id = $entity_type->id();
504 if (!isset($this->schema[$entity_type_id]) || $reset) {
505 // Back up the storage definition and replace it with the passed one.
506 // @todo Instead of switching the wrapped entity type, we should be able
507 // to instantiate a new table mapping for each entity type definition.
508 // See https://www.drupal.org/node/2274017.
509 $actual_definition = $this->entityManager->getDefinition($entity_type_id);
510 $this->storage->setEntityType($entity_type);
512 // Prepare basic information about the entity type.
513 $tables = $this->getEntitySchemaTables();
515 // Initialize the table schema.
516 $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type);
517 if (isset($tables['revision_table'])) {
518 $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type);
520 if (isset($tables['data_table'])) {
521 $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
523 if (isset($tables['revision_data_table'])) {
524 $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
527 // We need to act only on shared entity schema tables.
528 $table_mapping = $this->storage->getTableMapping();
529 $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
530 $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
531 foreach ($table_names as $table_name) {
532 if (!isset($schema[$table_name])) {
533 $schema[$table_name] = [];
535 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
536 if (!isset($storage_definitions[$field_name])) {
537 throw new FieldException("Field storage definition for '$field_name' could not be found.");
539 // Add the schema for base field definitions.
540 elseif ($table_mapping->allowsSharedTableStorage($storage_definitions[$field_name])) {
541 $column_names = $table_mapping->getColumnNames($field_name);
542 $storage_definition = $storage_definitions[$field_name];
543 $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
548 // Process tables after having gathered field information.
549 $this->processBaseTable($entity_type, $schema[$tables['base_table']]);
550 if (isset($tables['revision_table'])) {
551 $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
553 if (isset($tables['data_table'])) {
554 $this->processDataTable($entity_type, $schema[$tables['data_table']]);
556 if (isset($tables['revision_data_table'])) {
557 $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
560 // Add an index for the 'published' entity key.
561 if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) {
562 $published_key = $entity_type->getKey('published');
563 if ($published_key && !$storage_definitions[$published_key]->hasCustomStorage()) {
564 $published_field_table = $table_mapping->getFieldTableName($published_key);
565 $id_key = $entity_type->getKey('id');
566 if ($bundle_key = $entity_type->getKey('bundle')) {
567 $key = "{$published_key}_{$bundle_key}";
568 $columns = [$published_key, $bundle_key, $id_key];
571 $key = $published_key;
572 $columns = [$published_key, $id_key];
574 $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns;
578 $this->schema[$entity_type_id] = $schema;
580 // Restore the actual definition.
581 $this->storage->setEntityType($actual_definition);
584 return $this->schema[$entity_type_id];
588 * Gets a list of entity type tables.
591 * A list of entity type tables, keyed by table key.
593 protected function getEntitySchemaTables() {
594 return array_filter([
595 'base_table' => $this->storage->getBaseTable(),
596 'revision_table' => $this->storage->getRevisionTable(),
597 'data_table' => $this->storage->getDataTable(),
598 'revision_data_table' => $this->storage->getRevisionDataTable(),
603 * Gets entity schema definitions for index and key definitions.
605 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
606 * The entity type definition.
607 * @param array $schema
608 * The entity schema array.
611 * A stripped down version of the $schema Schema API array containing, for
612 * each table, only the key and index definitions not derived from field
613 * storage definitions.
615 protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
616 $entity_type_id = $entity_type->id();
618 // Collect all possible field schema identifiers for shared table fields.
619 // These will be used to detect entity schema data in the subsequent loop.
620 $field_schema_identifiers = [];
621 $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
622 $table_mapping = $this->storage->getTableMapping($storage_definitions);
623 foreach ($storage_definitions as $field_name => $storage_definition) {
624 if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
625 // Make sure both base identifier names and suffixed names are listed.
626 $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name);
627 $field_schema_identifiers[$name] = $name;
628 foreach ($storage_definition->getColumns() as $key => $columns) {
629 $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
630 $field_schema_identifiers[$name] = $name;
635 // Extract entity schema data from the Schema API definition.
637 $keys = ['indexes', 'unique keys'];
638 $unused_keys = array_flip(['description', 'fields', 'foreign keys']);
639 foreach ($schema as $table_name => $table_schema) {
640 $table_schema = array_diff_key($table_schema, $unused_keys);
641 foreach ($keys as $key) {
642 // Exclude data generated from field storage definitions, we will check
644 if ($field_schema_identifiers && !empty($table_schema[$key])) {
645 $table_schema[$key] = array_diff_key($table_schema[$key], $field_schema_identifiers);
648 $schema_data[$table_name] = array_filter($table_schema);
655 * Gets an index schema array for a given field.
657 * @param string $field_name
658 * The name of the field.
659 * @param array $field_schema
660 * The schema of the field.
661 * @param string[] $column_mapping
662 * A mapping of field column names to database column names.
665 * The schema definition for the indexes.
667 protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) {
668 return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes');
672 * Gets a unique key schema array for a given field.
674 * @param string $field_name
675 * The name of the field.
676 * @param array $field_schema
677 * The schema of the field.
678 * @param string[] $column_mapping
679 * A mapping of field column names to database column names.
682 * The schema definition for the unique keys.
684 protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) {
685 return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys');
689 * Gets field schema data for the given key.
691 * @param string $field_name
692 * The name of the field.
693 * @param array $field_schema
694 * The schema of the field.
695 * @param string[] $column_mapping
696 * A mapping of field column names to database column names.
697 * @param string $schema_key
698 * The type of schema data. Either 'indexes' or 'unique keys'.
701 * The schema definition for the specified key.
703 protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) {
706 $entity_type_id = $this->entityType->id();
707 foreach ($field_schema[$schema_key] as $key => $columns) {
708 // To avoid clashes with entity-level indexes or unique keys we use
709 // "{$entity_type_id}_field__" as a prefix instead of just
710 // "{$entity_type_id}__". We additionally namespace the specifier by the
711 // field name to avoid clashes when multiple fields of the same type are
712 // added to an entity type.
713 $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
714 foreach ($columns as $column) {
715 // Allow for indexes and unique keys to specified as an array of column
717 if (is_array($column)) {
718 list($column_name, $length) = $column;
719 $data[$real_key][] = [$column_mapping[$column_name], $length];
722 $data[$real_key][] = $column_mapping[$column];
731 * Generates a safe schema identifier (name of an index, column name etc.).
733 * @param string $entity_type_id
734 * The ID of the entity type.
735 * @param string $field_name
736 * The name of the field.
737 * @param string|null $key
738 * (optional) A further key to append to the name.
741 * The field identifier name.
743 protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key = NULL) {
744 $real_key = isset($key) ? "{$entity_type_id}_field__{$field_name}__{$key}" : "{$entity_type_id}_field__{$field_name}";
745 // Limit the string to 48 characters, keeping a 16 characters margin for db
747 if (strlen($real_key) > 48) {
748 // Use a shorter separator, a truncated entity_type, and a hash of the
750 // Truncate to the same length for the current and revision tables.
751 $entity_type = substr($entity_type_id, 0, 36);
752 $field_hash = substr(hash('sha256', $real_key), 0, 10);
753 $real_key = $entity_type . '__' . $field_hash;
759 * Gets field foreign keys.
761 * @param string $field_name
762 * The name of the field.
763 * @param array $field_schema
764 * The schema of the field.
765 * @param string[] $column_mapping
766 * A mapping of field column names to database column names.
769 * The schema definition for the foreign keys.
771 protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) {
774 foreach ($field_schema['foreign keys'] as $specifier => $specification) {
775 // To avoid clashes with entity-level foreign keys we use
776 // "{$entity_type_id}_field__" as a prefix instead of just
777 // "{$entity_type_id}__". We additionally namespace the specifier by the
778 // field name to avoid clashes when multiple fields of the same type are
779 // added to an entity type.
780 $entity_type_id = $this->entityType->id();
781 $real_specifier = "{$entity_type_id}_field__{$field_name}__{$specifier}";
782 $foreign_keys[$real_specifier]['table'] = $specification['table'];
783 foreach ($specification['columns'] as $column => $referenced) {
784 $foreign_keys[$real_specifier]['columns'][$column_mapping[$column]] = $referenced;
788 return $foreign_keys;
792 * Loads stored schema data for the given entity type definition.
794 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
795 * The entity type definition.
798 * The entity schema data array.
800 protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
801 return $this->installedStorageSchema()->get($entity_type->id() . '.entity_schema_data', []);
805 * Stores schema data for the given entity type definition.
807 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
808 * The entity type definition.
809 * @param array $schema
810 * The entity schema data array.
812 protected function saveEntitySchemaData(EntityTypeInterface $entity_type, $schema) {
813 $data = $this->getEntitySchemaData($entity_type, $schema);
814 $this->installedStorageSchema()->set($entity_type->id() . '.entity_schema_data', $data);
818 * Deletes schema data for the given entity type definition.
820 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
821 * The entity type definition.
823 protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
824 $this->installedStorageSchema()->delete($entity_type->id() . '.entity_schema_data');
828 * Loads stored schema data for the given field storage definition.
830 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
831 * The field storage definition.
834 * The field schema data array.
836 protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
837 return $this->installedStorageSchema()->get($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), []);
841 * Stores schema data for the given field storage definition.
843 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
844 * The field storage definition.
845 * @param array $schema
846 * The field schema data array.
848 protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) {
849 $this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema);
853 * Deletes schema data for the given field storage definition.
855 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
856 * The field storage definition.
858 protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
859 $this->installedStorageSchema()->delete($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName());
863 * Initializes common information for a base table.
865 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
869 * A partial schema array for the base table.
871 protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
872 $entity_type_id = $entity_type->id();
875 'description' => "The base table for $entity_type_id entities.",
876 'primary key' => [$entity_type->getKey('id')],
878 'foreign keys' => [],
881 if ($entity_type->hasKey('revision')) {
882 $revision_key = $entity_type->getKey('revision');
883 $key_name = $this->getEntityIndexName($entity_type, $revision_key);
884 $schema['unique keys'][$key_name] = [$revision_key];
885 $schema['foreign keys'][$entity_type_id . '__revision'] = [
886 'table' => $this->storage->getRevisionTable(),
887 'columns' => [$revision_key => $revision_key],
891 $this->addTableDefaults($schema);
897 * Initializes common information for a revision table.
899 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
903 * A partial schema array for the revision table.
905 protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) {
906 $entity_type_id = $entity_type->id();
907 $id_key = $entity_type->getKey('id');
908 $revision_key = $entity_type->getKey('revision');
911 'description' => "The revision table for $entity_type_id entities.",
912 'primary key' => [$revision_key],
915 $entity_type_id . '__revisioned' => [
916 'table' => $this->storage->getBaseTable(),
917 'columns' => [$id_key => $id_key],
922 $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = [$id_key];
924 $this->addTableDefaults($schema);
930 * Initializes common information for a data table.
932 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
936 * A partial schema array for the data table.
938 protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
939 $entity_type_id = $entity_type->id();
940 $id_key = $entity_type->getKey('id');
943 'description' => "The data table for $entity_type_id entities.",
944 'primary key' => [$id_key, $entity_type->getKey('langcode')],
946 $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
950 'table' => $this->storage->getBaseTable(),
951 'columns' => [$id_key => $id_key],
956 if ($entity_type->hasKey('revision')) {
957 $key = $entity_type->getKey('revision');
958 $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = [$key];
961 $this->addTableDefaults($schema);
967 * Initializes common information for a revision data table.
969 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
973 * A partial schema array for the revision data table.
975 protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) {
976 $entity_type_id = $entity_type->id();
977 $id_key = $entity_type->getKey('id');
978 $revision_key = $entity_type->getKey('revision');
981 'description' => "The revision data table for $entity_type_id entities.",
982 'primary key' => [$revision_key, $entity_type->getKey('langcode')],
984 $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
988 'table' => $this->storage->getBaseTable(),
989 'columns' => [$id_key => $id_key],
991 $entity_type_id . '__revision' => [
992 'table' => $this->storage->getRevisionTable(),
993 'columns' => [$revision_key => $revision_key],
998 $this->addTableDefaults($schema);
1004 * Adds defaults to a table schema definition.
1007 * The schema definition array for a single table, passed by reference.
1009 protected function addTableDefaults(&$schema) {
1012 'unique keys' => [],
1014 'foreign keys' => [],
1019 * Processes the gathered schema for a base table.
1021 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1023 * @param array $schema
1024 * The table schema, passed by reference.
1027 * A partial schema array for the base table.
1029 protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1030 $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
1034 * Processes the gathered schema for a base table.
1036 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1038 * @param array $schema
1039 * The table schema, passed by reference.
1042 * A partial schema array for the base table.
1044 protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1045 $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
1049 * Processes the gathered schema for a base table.
1051 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1053 * @param array $schema
1054 * The table schema, passed by reference.
1057 * A partial schema array for the base table.
1059 protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1060 // Marking the respective fields as NOT NULL makes the indexes more
1062 $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1066 * Processes the gathered schema for a base table.
1068 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1070 * @param array $schema
1071 * The table schema, passed by reference.
1074 * A partial schema array for the base table.
1076 protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1077 // Marking the respective fields as NOT NULL makes the indexes more
1079 $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1083 * Processes the specified entity key.
1085 * @param array $schema
1086 * The table schema, passed by reference.
1087 * @param string $key
1088 * The entity key name.
1090 protected function processIdentifierSchema(&$schema, $key) {
1091 if ($schema['fields'][$key]['type'] == 'int') {
1092 $schema['fields'][$key]['type'] = 'serial';
1094 $schema['fields'][$key]['not null'] = TRUE;
1095 unset($schema['fields'][$key]['default']);
1099 * Performs the specified operation on a field.
1101 * This figures out whether the field is stored in a dedicated or shared table
1102 * and forwards the call to the proper handler.
1104 * @param string $operation
1105 * The name of the operation to be performed.
1106 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1107 * The field storage definition.
1108 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1109 * (optional) The original field storage definition. This is relevant (and
1110 * required) only for updates. Defaults to NULL.
1112 protected function performFieldSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) {
1113 $table_mapping = $this->storage->getTableMapping();
1114 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1115 $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original);
1117 elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1118 $this->{$operation . 'SharedTableSchema'}($storage_definition, $original);
1123 * Creates the schema for a field stored in a dedicated table.
1125 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1126 * The storage definition of the field being created.
1128 protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1129 $schema = $this->getDedicatedTableSchema($storage_definition);
1130 foreach ($schema as $name => $table) {
1131 // Check if the table exists because it might already have been
1132 // created as part of the earlier entity type update event.
1133 if (!$this->database->schema()->tableExists($name)) {
1134 $this->database->schema()->createTable($name, $table);
1137 $this->saveFieldSchemaData($storage_definition, $schema);
1141 * Creates the schema for a field stored in a shared table.
1143 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1144 * The storage definition of the field being created.
1145 * @param bool $only_save
1146 * (optional) Whether to skip modification of database tables and only save
1147 * the schema data for future comparison. For internal use only. This is
1148 * used by onEntityTypeCreate() after it has already fully created the
1151 protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) {
1152 $created_field_name = $storage_definition->getName();
1153 $table_mapping = $this->storage->getTableMapping();
1154 $column_names = $table_mapping->getColumnNames($created_field_name);
1155 $schema_handler = $this->database->schema();
1156 $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
1158 // Iterate over the mapped table to find the ones that will host the created
1161 foreach ($shared_table_names as $table_name) {
1162 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1163 if ($field_name == $created_field_name) {
1164 // Create field columns.
1165 $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1167 foreach ($schema[$table_name]['fields'] as $name => $specifier) {
1168 // Check if the field exists because it might already have been
1169 // created as part of the earlier entity type update event.
1170 if (!$schema_handler->fieldExists($table_name, $name)) {
1171 $schema_handler->addField($table_name, $name, $specifier);
1174 if (!empty($schema[$table_name]['indexes'])) {
1175 foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
1176 // Check if the index exists because it might already have been
1177 // created as part of the earlier entity type update event.
1178 $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
1181 if (!empty($schema[$table_name]['unique keys'])) {
1182 foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
1183 $schema_handler->addUniqueKey($table_name, $name, $specifier);
1187 // After creating the field schema skip to the next table.
1193 $this->saveFieldSchemaData($storage_definition, $schema);
1196 // Make sure any entity index involving this field is re-created if
1198 $this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
1203 * Deletes the schema for a field stored in a dedicated table.
1205 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1206 * The storage definition of the field being deleted.
1208 protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1209 // When switching from dedicated to shared field table layout we need need
1210 // to delete the field tables with their regular names. When this happens
1211 // original definitions will be defined.
1212 $deleted = !$this->originalDefinitions;
1213 $table_mapping = $this->storage->getTableMapping();
1214 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted);
1215 $this->database->schema()->dropTable($table_name);
1216 if ($this->entityType->isRevisionable()) {
1217 $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted);
1218 $this->database->schema()->dropTable($revision_name);
1220 $this->deleteFieldSchemaData($storage_definition);
1224 * Deletes the schema for a field stored in a shared table.
1226 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1227 * The storage definition of the field being deleted.
1229 protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1230 // Make sure any entity index involving this field is dropped.
1231 $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
1233 $deleted_field_name = $storage_definition->getName();
1234 $table_mapping = $this->storage->getTableMapping(
1235 $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1237 $column_names = $table_mapping->getColumnNames($deleted_field_name);
1238 $schema_handler = $this->database->schema();
1239 $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
1241 // Iterate over the mapped table to find the ones that host the deleted
1243 foreach ($shared_table_names as $table_name) {
1244 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1245 if ($field_name == $deleted_field_name) {
1246 $schema = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1248 // Drop indexes and unique keys first.
1249 if (!empty($schema['indexes'])) {
1250 foreach ($schema['indexes'] as $name => $specifier) {
1251 $schema_handler->dropIndex($table_name, $name);
1254 if (!empty($schema['unique keys'])) {
1255 foreach ($schema['unique keys'] as $name => $specifier) {
1256 $schema_handler->dropUniqueKey($table_name, $name);
1260 foreach ($column_names as $column_name) {
1261 $schema_handler->dropField($table_name, $column_name);
1263 // After deleting the field schema skip to the next table.
1269 $this->deleteFieldSchemaData($storage_definition);
1273 * Updates the schema for a field stored in a shared table.
1275 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1276 * The storage definition of the field being updated.
1277 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1278 * The original storage definition; i.e., the definition before the update.
1280 * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
1281 * Thrown when the update to the field is forbidden.
1282 * @throws \Exception
1283 * Rethrown exception if the table recreation fails.
1285 protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1286 if (!$this->storage->countFieldData($original, TRUE)) {
1287 // There is no data. Re-create the tables completely.
1288 if ($this->database->supportsTransactionalDDL()) {
1289 // If the database supports transactional DDL, we can go ahead and rely
1290 // on it. If not, we will have to rollback manually if something fails.
1291 $transaction = $this->database->startTransaction();
1294 // Since there is no data we may be switching from a shared table schema
1295 // to a dedicated table schema, hence we should use the proper API.
1296 $this->performFieldSchemaOperation('delete', $original);
1297 $this->performFieldSchemaOperation('create', $storage_definition);
1299 catch (\Exception $e) {
1300 if ($this->database->supportsTransactionalDDL()) {
1301 $transaction->rollBack();
1305 $this->performFieldSchemaOperation('create', $original);
1311 if ($this->hasColumnChanges($storage_definition, $original)) {
1312 throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
1314 // There is data, so there are no column changes. Drop all the prior
1315 // indexes and create all the new ones, except for all the priors that
1317 $table_mapping = $this->storage->getTableMapping();
1318 $table = $table_mapping->getDedicatedDataTableName($original);
1319 $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
1321 // Get the field schemas.
1322 $schema = $storage_definition->getSchema();
1323 $original_schema = $original->getSchema();
1325 // Gets the SQL schema for a dedicated tables.
1326 $actual_schema = $this->getDedicatedTableSchema($storage_definition);
1328 foreach ($original_schema['indexes'] as $name => $columns) {
1329 if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
1330 $real_name = $this->getFieldIndexName($storage_definition, $name);
1331 $this->database->schema()->dropIndex($table, $real_name);
1332 $this->database->schema()->dropIndex($revision_table, $real_name);
1335 $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1336 $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1337 foreach ($schema['indexes'] as $name => $columns) {
1338 if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
1339 $real_name = $this->getFieldIndexName($storage_definition, $name);
1341 foreach ($columns as $column_name) {
1342 // Indexes can be specified as either a column name or an array with
1343 // column name and length. Allow for either case.
1344 if (is_array($column_name)) {
1346 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1351 $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1354 // Check if the index exists because it might already have been
1355 // created as part of the earlier entity type update event.
1356 $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
1357 $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
1360 $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
1365 * Updates the schema for a field stored in a shared table.
1367 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1368 * The storage definition of the field being updated.
1369 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1370 * The original storage definition; i.e., the definition before the update.
1372 * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
1373 * Thrown when the update to the field is forbidden.
1374 * @throws \Exception
1375 * Rethrown exception if the table recreation fails.
1377 protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1378 if (!$this->storage->countFieldData($original, TRUE)) {
1379 if ($this->database->supportsTransactionalDDL()) {
1380 // If the database supports transactional DDL, we can go ahead and rely
1381 // on it. If not, we will have to rollback manually if something fails.
1382 $transaction = $this->database->startTransaction();
1385 // Since there is no data we may be switching from a dedicated table
1386 // to a schema table schema, hence we should use the proper API.
1387 $this->performFieldSchemaOperation('delete', $original);
1388 $this->performFieldSchemaOperation('create', $storage_definition);
1390 catch (\Exception $e) {
1391 if ($this->database->supportsTransactionalDDL()) {
1392 $transaction->rollBack();
1395 // Recreate original schema.
1396 $this->createSharedTableSchema($original);
1402 if ($this->hasColumnChanges($storage_definition, $original)) {
1403 throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
1406 $updated_field_name = $storage_definition->getName();
1407 $table_mapping = $this->storage->getTableMapping();
1408 $column_names = $table_mapping->getColumnNames($updated_field_name);
1409 $schema_handler = $this->database->schema();
1411 // Iterate over the mapped table to find the ones that host the deleted
1413 $original_schema = $this->loadFieldSchemaData($original);
1415 foreach ($table_mapping->getTableNames() as $table_name) {
1416 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1417 if ($field_name == $updated_field_name) {
1418 $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1420 // Handle NOT NULL constraints.
1421 foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
1422 $not_null = !empty($specifier['not null']);
1423 $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
1424 if ($not_null !== $original_not_null) {
1425 if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
1426 throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
1428 $column_schema = $original_schema[$table_name]['fields'][$column_name];
1429 $column_schema['not null'] = $not_null;
1430 $schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
1434 // Drop original indexes and unique keys.
1435 if (!empty($original_schema[$table_name]['indexes'])) {
1436 foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
1437 $schema_handler->dropIndex($table_name, $name);
1440 if (!empty($original_schema[$table_name]['unique keys'])) {
1441 foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) {
1442 $schema_handler->dropUniqueKey($table_name, $name);
1445 // Create new indexes and unique keys.
1446 if (!empty($schema[$table_name]['indexes'])) {
1447 foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
1448 // Check if the index exists because it might already have been
1449 // created as part of the earlier entity type update event.
1450 $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
1454 if (!empty($schema[$table_name]['unique keys'])) {
1455 foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
1456 $schema_handler->addUniqueKey($table_name, $name, $specifier);
1459 // After deleting the field schema skip to the next table.
1464 $this->saveFieldSchemaData($storage_definition, $schema);
1469 * Creates the specified entity schema indexes and keys.
1471 * @param array $entity_schema
1472 * The entity schema definition.
1473 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
1474 * (optional) If a field storage definition is specified, only indexes and
1475 * keys involving its columns will be processed. Otherwise all defined
1476 * entity indexes and keys will be processed.
1478 protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
1479 $schema_handler = $this->database->schema();
1481 if ($storage_definition) {
1482 $table_mapping = $this->storage->getTableMapping();
1483 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1487 'indexes' => 'addIndex',
1488 'unique keys' => 'addUniqueKey',
1491 foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
1492 // Add fields schema because database driver may depend on this data to
1493 // perform index normalization.
1494 $schema['fields'] = $entity_schema[$table_name]['fields'];
1496 foreach ($index_keys as $key => $add_method) {
1497 if (!empty($schema[$key])) {
1498 foreach ($schema[$key] as $name => $specifier) {
1499 // If a set of field columns were specified we process only indexes
1500 // involving them. Only indexes for which all columns exist are
1501 // actually created.
1503 $specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
1504 if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1506 foreach ($specifier_columns as $specifier_column_name) {
1507 // This may happen when adding more than one field in the same
1508 // update run. Eventually when all field columns have been
1509 // created the index will be created too.
1510 if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
1517 $this->{$add_method}($table_name, $name, $specifier, $schema);
1526 * Deletes the specified entity schema indexes and keys.
1528 * @param array $entity_schema_data
1529 * The entity schema data definition.
1530 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
1531 * (optional) If a field storage definition is specified, only indexes and
1532 * keys involving its columns will be processed. Otherwise all defined
1533 * entity indexes and keys will be processed.
1535 protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
1536 $schema_handler = $this->database->schema();
1538 if ($storage_definition) {
1539 $table_mapping = $this->storage->getTableMapping();
1540 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1544 'indexes' => 'dropIndex',
1545 'unique keys' => 'dropUniqueKey',
1548 foreach ($entity_schema_data as $table_name => $schema) {
1549 foreach ($index_keys as $key => $drop_method) {
1550 if (!empty($schema[$key])) {
1551 foreach ($schema[$key] as $name => $specifier) {
1552 $specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
1553 if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1554 $schema_handler->{$drop_method}($table_name, $name);
1563 * Checks whether a field property has NULL values.
1565 * @param string $table_name
1566 * The name of the table to inspect.
1567 * @param string $column_name
1568 * The name of the column holding the field property data.
1571 * TRUE if NULL data is found, FALSE otherwise.
1573 protected function hasNullFieldPropertyData($table_name, $column_name) {
1574 $query = $this->database->select($table_name, 't')
1575 ->fields('t', [$column_name])
1577 $query->isNull('t.' . $column_name);
1578 $result = $query->execute()->fetchAssoc();
1579 return (bool) $result;
1583 * Gets the schema for a single field definition.
1585 * Entity types may override this method in order to optimize the generated
1586 * schema for given field. While all optimizations that apply to a single
1587 * field have to be added here, all cross-field optimizations should be via
1588 * SqlContentEntityStorageSchema::getEntitySchema() instead; e.g.,
1589 * an index spanning multiple fields.
1591 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1592 * The storage definition of the field whose schema has to be returned.
1593 * @param string $table_name
1594 * The name of the table columns will be added to.
1595 * @param string[] $column_mapping
1596 * A mapping of field column names to database column names.
1599 * The schema definition for the table with the following keys:
1600 * - fields: The schema definition for the each field columns.
1601 * - indexes: The schema definition for the indexes.
1602 * - unique keys: The schema definition for the unique keys.
1603 * - foreign keys: The schema definition for the foreign keys.
1605 * @throws \Drupal\Core\Field\FieldException
1606 * Exception thrown if the schema contains reserved column names.
1608 protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
1610 $field_schema = $storage_definition->getSchema();
1612 // Check that the schema does not include forbidden column names.
1613 if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) {
1614 throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
1617 $field_name = $storage_definition->getName();
1618 $base_table = $this->storage->getBaseTable();
1620 // A shared table contains rows for entities where the field is empty
1621 // (since other fields stored in the same table might not be empty), thus
1622 // the only columns that can be 'not null' are those for required
1623 // properties of required fields. However, even those would break in the
1624 // case where a new field is added to a table that contains existing rows.
1625 // For now, we only hardcode 'not null' to a couple "entity keys", in order
1626 // to keep their indexes optimized.
1627 // @todo Revisit once we have support for 'initial' in
1628 // https://www.drupal.org/node/2346019.
1629 $not_null_keys = $this->entityType->getKeys();
1630 // Label fields are not necessarily required.
1631 unset($not_null_keys['label']);
1632 // Because entity ID and revision ID are both serial fields in the base and
1633 // revision table respectively, the revision ID is not known yet, when
1634 // inserting data into the base table. Instead the revision ID in the base
1635 // table is updated after the data has been inserted into the revision
1636 // table. For this reason the revision ID field cannot be marked as NOT
1638 if ($table_name == $base_table) {
1639 unset($not_null_keys['revision']);
1642 foreach ($column_mapping as $field_column_name => $schema_field_name) {
1643 $column_schema = $field_schema['columns'][$field_column_name];
1645 $schema['fields'][$schema_field_name] = $column_schema;
1646 $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
1649 if (!empty($field_schema['indexes'])) {
1650 $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
1653 if (!empty($field_schema['unique keys'])) {
1654 $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
1657 if (!empty($field_schema['foreign keys'])) {
1658 $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
1665 * Adds an index for the specified field to the given schema definition.
1667 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1668 * The storage definition of the field for which an index should be added.
1669 * @param array $schema
1670 * A reference to the schema array to be updated.
1671 * @param bool $not_null
1672 * (optional) Whether to also add a 'not null' constraint to the column
1673 * being indexed. Doing so improves index performance. Defaults to FALSE,
1674 * in case the field needs to support NULL values.
1676 * (optional) The index size. Defaults to no limit.
1678 protected function addSharedTableFieldIndex(FieldStorageDefinitionInterface $storage_definition, &$schema, $not_null = FALSE, $size = NULL) {
1679 $name = $storage_definition->getName();
1680 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1681 $schema['indexes'][$real_key] = [$size ? [$name, $size] : $name];
1683 $schema['fields'][$name]['not null'] = TRUE;
1688 * Adds a unique key for the specified field to the given schema definition.
1690 * Also adds a 'not null' constraint, because many databases do not reliably
1691 * support unique keys on null columns.
1693 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1694 * The storage definition of the field to which to add a unique key.
1695 * @param array $schema
1696 * A reference to the schema array to be updated.
1698 protected function addSharedTableFieldUniqueKey(FieldStorageDefinitionInterface $storage_definition, &$schema) {
1699 $name = $storage_definition->getName();
1700 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1701 $schema['unique keys'][$real_key] = [$name];
1702 $schema['fields'][$name]['not null'] = TRUE;
1706 * Adds a foreign key for the specified field to the given schema definition.
1708 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1709 * The storage definition of the field to which to add a foreign key.
1710 * @param array $schema
1711 * A reference to the schema array to be updated.
1712 * @param string $foreign_table
1713 * The foreign table.
1714 * @param string $foreign_column
1715 * The foreign column.
1717 protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface $storage_definition, &$schema, $foreign_table, $foreign_column) {
1718 $name = $storage_definition->getName();
1719 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1720 $schema['foreign keys'][$real_key] = [
1721 'table' => $foreign_table,
1722 'columns' => [$name => $foreign_column],
1727 * Gets the SQL schema for a dedicated table.
1729 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1730 * The field storage definition.
1731 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1732 * (optional) The entity type definition. Defaults to the one returned by
1733 * the entity manager.
1736 * The schema definition for the table with the following keys:
1737 * - fields: The schema definition for the each field columns.
1738 * - indexes: The schema definition for the indexes.
1739 * - unique keys: The schema definition for the unique keys.
1740 * - foreign keys: The schema definition for the foreign keys.
1742 * @throws \Drupal\Core\Field\FieldException
1743 * Exception thrown if the schema contains reserved column names.
1745 * @see hook_schema()
1747 protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
1748 $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
1749 $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
1751 $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')];
1752 if ($id_definition->getType() == 'integer') {
1757 'description' => 'The entity id this data is attached to',
1762 'type' => 'varchar_ascii',
1765 'description' => 'The entity id this data is attached to',
1769 // Define the revision ID schema.
1770 if (!$this->entityType->isRevisionable()) {
1771 $revision_id_schema = $id_schema;
1772 $revision_id_schema['description'] = 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id';
1774 elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') {
1775 $revision_id_schema = [
1779 'description' => 'The entity revision id this data is attached to',
1783 $revision_id_schema = [
1784 'type' => 'varchar',
1787 'description' => 'The entity revision id this data is attached to',
1792 'description' => $description_current,
1795 'type' => 'varchar_ascii',
1799 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
1806 'description' => 'A boolean indicating whether this data item has been deleted'
1808 'entity_id' => $id_schema,
1809 'revision_id' => $revision_id_schema,
1811 'type' => 'varchar_ascii',
1815 'description' => 'The language code for this data item.',
1821 'description' => 'The sequence number for this data item, used for multi-value fields',
1824 'primary key' => ['entity_id', 'deleted', 'delta', 'langcode'],
1826 'bundle' => ['bundle'],
1827 'revision_id' => ['revision_id'],
1831 // Check that the schema does not include forbidden column names.
1832 $schema = $storage_definition->getSchema();
1833 $properties = $storage_definition->getPropertyDefinitions();
1834 $table_mapping = $this->storage->getTableMapping();
1835 if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
1836 throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
1839 // Add field columns.
1840 foreach ($schema['columns'] as $column_name => $attributes) {
1841 $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1842 $data_schema['fields'][$real_name] = $attributes;
1843 // A dedicated table only contain rows for actual field values, and no
1844 // rows for entities where the field is empty. Thus, we can safely
1845 // enforce 'not null' on the columns for the field's required properties.
1846 $data_schema['fields'][$real_name]['not null'] = $properties[$column_name]->isRequired();
1850 foreach ($schema['indexes'] as $index_name => $columns) {
1851 $real_name = $this->getFieldIndexName($storage_definition, $index_name);
1852 foreach ($columns as $column_name) {
1853 // Indexes can be specified as either a column name or an array with
1854 // column name and length. Allow for either case.
1855 if (is_array($column_name)) {
1856 $data_schema['indexes'][$real_name][] = [
1857 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1862 $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1868 foreach ($schema['unique keys'] as $index_name => $columns) {
1869 $real_name = $this->getFieldIndexName($storage_definition, $index_name);
1870 foreach ($columns as $column_name) {
1871 // Unique keys can be specified as either a column name or an array with
1872 // column name and length. Allow for either case.
1873 if (is_array($column_name)) {
1874 $data_schema['unique keys'][$real_name][] = [
1875 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1880 $data_schema['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1885 // Add foreign keys.
1886 foreach ($schema['foreign keys'] as $specifier => $specification) {
1887 $real_name = $this->getFieldIndexName($storage_definition, $specifier);
1888 $data_schema['foreign keys'][$real_name]['table'] = $specification['table'];
1889 foreach ($specification['columns'] as $column_name => $referenced) {
1890 $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1891 $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
1895 $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema];
1897 // If the entity type is revisionable, construct the revision table.
1898 $entity_type = $entity_type ?: $this->entityType;
1899 if ($entity_type->isRevisionable()) {
1900 $revision_schema = $data_schema;
1901 $revision_schema['description'] = $description_revision;
1902 $revision_schema['primary key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode'];
1903 $revision_schema['fields']['revision_id']['not null'] = TRUE;
1904 $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
1905 $dedicated_table_schema += [$table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema];
1908 return $dedicated_table_schema;
1912 * Gets the name to be used for the given entity index.
1914 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1916 * @param string $index
1917 * The index column name.
1922 protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
1923 return $entity_type->id() . '__' . $index;
1927 * Generates an index name for a field data table.
1929 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1930 * The field storage definition.
1931 * @param string $index
1932 * The name of the index.
1935 * A string containing a generated index name for a field data table that is
1936 * unique among all other fields.
1938 protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
1939 return $storage_definition->getName() . '_' . $index;
1943 * Checks whether a database table is non-existent or empty.
1945 * Empty tables can be dropped and recreated without data loss.
1947 * @param string $table_name
1948 * The database table to check.
1951 * TRUE if the table is empty, FALSE otherwise.
1953 protected function isTableEmpty($table_name) {
1954 return !$this->database->schema()->tableExists($table_name) ||
1955 !$this->database->select($table_name)
1963 * Compares schemas to check for changes in the column definitions.
1965 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1966 * Current field storage definition.
1967 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1968 * The original field storage definition.
1971 * Returns TRUE if there are schema changes in the column definitions.
1973 protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1974 if ($storage_definition->getColumns() != $original->getColumns()) {
1975 // Base field definitions have schema data stored in the original
1980 if (!$storage_definition->hasCustomStorage()) {
1981 $keys = array_flip($this->getColumnSchemaRelevantKeys());
1982 $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
1983 foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
1984 foreach ($table_schema['fields'] as $name => $spec) {
1985 $definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
1986 $stored_spec = array_intersect_key($spec, $keys);
1987 if ($definition_spec != $stored_spec) {
1998 * Returns a list of column schema keys affecting data storage.
2000 * When comparing schema definitions, only changes in certain properties
2001 * actually affect how data is stored and thus, if applied, may imply data
2005 * An array of key names.
2007 protected function getColumnSchemaRelevantKeys() {
2008 return ['type', 'size', 'length', 'unsigned'];
2012 * Creates an index, dropping it if already existing.
2014 * @param string $table
2016 * @param string $name
2018 * @param array $specifier
2019 * The fields to index.
2020 * @param array $schema
2021 * The table specification.
2023 * @see \Drupal\Core\Database\Schema::addIndex()
2025 protected function addIndex($table, $name, array $specifier, array $schema) {
2026 $schema_handler = $this->database->schema();
2027 $schema_handler->dropIndex($table, $name);
2028 $schema_handler->addIndex($table, $name, $specifier, $schema);
2032 * Creates a unique key, dropping it if already existing.
2034 * @param string $table
2036 * @param string $name
2038 * @param array $specifier
2039 * The unique fields.
2041 * @see \Drupal\Core\Database\Schema::addUniqueKey()
2043 protected function addUniqueKey($table, $name, array $specifier) {
2044 $schema_handler = $this->database->schema();
2045 $schema_handler->dropUniqueKey($table, $name);
2046 $schema_handler->addUniqueKey($table, $name, $specifier);