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\BaseFieldDefinition;
16 use Drupal\Core\Field\FieldException;
17 use Drupal\Core\Field\FieldStorageDefinitionInterface;
18 use Drupal\field\FieldStorageConfigInterface;
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 * Constructs a SqlContentEntityStorageSchema.
92 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
94 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
96 * @param \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage
97 * The storage of the entity type. This must be an SQL-based storage.
98 * @param \Drupal\Core\Database\Connection $database
99 * The database connection to be used.
101 public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, SqlContentEntityStorage $storage, Connection $database) {
102 $this->entityManager = $entity_manager;
103 $this->entityType = $entity_type;
104 $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
105 $this->storage = $storage;
106 $this->database = $database;
110 * Gets the keyvalue collection for tracking the installed schema.
112 * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
114 * @todo Inject this dependency in the constructor once this class can be
115 * instantiated as a regular entity handler:
116 * https://www.drupal.org/node/2332857.
118 protected function installedStorageSchema() {
119 if (!isset($this->installedStorageSchema)) {
120 $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
122 return $this->installedStorageSchema;
128 public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
130 $this->hasSharedTableStructureChange($entity_type, $original) ||
131 // Detect changes in key or index definitions.
132 $this->getEntitySchemaData($entity_type, $this->getEntitySchema($entity_type, TRUE)) != $this->loadEntitySchemaData($original);
136 * Detects whether there is a change in the shared table structure.
138 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
139 * The new entity type.
140 * @param \Drupal\Core\Entity\EntityTypeInterface $original
141 * The origin entity type.
144 * Returns TRUE if either the revisionable or translatable flag changes or
145 * a table has been renamed.
147 protected function hasSharedTableStructureChange(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
149 $entity_type->isRevisionable() != $original->isRevisionable() ||
150 $entity_type->isTranslatable() != $original->isTranslatable() ||
151 $this->hasSharedTableNameChanges($entity_type, $original);
155 * Detects whether any table name got renamed in an entity type update.
157 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
158 * The new entity type.
159 * @param \Drupal\Core\Entity\EntityTypeInterface $original
160 * The origin entity type.
163 * Returns TRUE if there have been changes.
165 protected function hasSharedTableNameChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
166 $base_table = $this->database->schema()->tableExists($entity_type->getBaseTable());
167 $data_table = $this->database->schema()->tableExists($entity_type->getDataTable());
168 $revision_table = $this->database->schema()->tableExists($entity_type->getRevisionTable());
169 $revision_data_table = $this->database->schema()->tableExists($entity_type->getRevisionDataTable());
171 // We first check if the new table already exists because the storage might
172 // have created it even though it wasn't specified in the entity type
175 (!$base_table && $entity_type->getBaseTable() != $original->getBaseTable()) ||
176 (!$data_table && $entity_type->getDataTable() != $original->getDataTable()) ||
177 (!$revision_table && $entity_type->getRevisionTable() != $original->getRevisionTable()) ||
178 (!$revision_data_table && $entity_type->getRevisionDataTable() != $original->getRevisionDataTable());
184 public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
185 $table_mapping = $this->storage->getTableMapping();
188 $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
189 $storage_definition->getSchema() != $original->getSchema() ||
190 $storage_definition->isRevisionable() != $original->isRevisionable() ||
191 $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) ||
192 $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original)
197 if ($storage_definition->hasCustomStorage()) {
198 // The field has custom storage, so we don't know if a schema change is
199 // needed or not, but since per the initial checks earlier in this
200 // function, nothing about the definition changed that we manage, we
205 $current_schema = $this->getSchemaFromStorageDefinition($storage_definition);
206 $this->processFieldStorageSchema($current_schema);
207 $installed_schema = $this->loadFieldSchemaData($original);
208 $this->processFieldStorageSchema($installed_schema);
210 return $current_schema != $installed_schema;
214 * Gets the schema data for the given field storage definition.
216 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
217 * The field storage definition. The field that must not have custom
218 * storage, i.e. the storage must take care of storing the field.
223 protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
224 assert('!$storage_definition->hasCustomStorage();');
225 $table_mapping = $this->storage->getTableMapping();
227 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
228 $schema = $this->getDedicatedTableSchema($storage_definition);
230 elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
231 $field_name = $storage_definition->getName();
232 foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
233 if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
234 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
235 $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
245 public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
246 // Check if the entity type specifies that data migration is being handled
248 if ($entity_type->get('requires_data_migration') === FALSE) {
252 // If the original storage has existing entities, or it is impossible to
253 // determine if that is the case, require entity data to be migrated.
254 $original_storage_class = $original->getStorageClass();
255 if (!class_exists($original_storage_class)) {
259 // Data migration is not needed when only indexes changed, as they can be
260 // applied if there is data.
261 if (!$this->hasSharedTableStructureChange($entity_type, $original)) {
265 // Use the original entity type since the storage has not been updated.
266 $original_storage = $this->entityManager->createHandlerInstance($original_storage_class, $original);
267 return $original_storage->hasData();
273 public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
274 return !$this->storage->countFieldData($original, TRUE);
280 public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
281 $this->checkEntityType($entity_type);
282 $schema_handler = $this->database->schema();
284 // Create entity tables.
285 $schema = $this->getEntitySchema($entity_type, TRUE);
286 foreach ($schema as $table_name => $table_schema) {
287 if (!$schema_handler->tableExists($table_name)) {
288 $schema_handler->createTable($table_name, $table_schema);
292 // Create dedicated field tables.
293 $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
294 foreach ($this->fieldStorageDefinitions as $field_storage_definition) {
295 if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
296 $this->createDedicatedTableSchema($field_storage_definition);
298 elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
299 // The shared tables are already fully created, but we need to save the
300 // per-field schema definitions for later use.
301 $this->createSharedTableSchema($field_storage_definition, TRUE);
305 // Save data about entity indexes and keys.
306 $this->saveEntitySchemaData($entity_type, $schema);
312 public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
313 $this->checkEntityType($entity_type);
314 $this->checkEntityType($original);
316 // If no schema changes are needed, we don't need to do anything.
317 if (!$this->requiresEntityStorageSchemaChanges($entity_type, $original)) {
321 // If a migration is required, we can't proceed.
322 if ($this->requiresEntityDataMigration($entity_type, $original)) {
323 throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type->id() . ') with data.');
326 // If we have no data just recreate the entity schema from scratch.
327 if ($this->isTableEmpty($this->storage->getBaseTable())) {
328 if ($this->database->supportsTransactionalDDL()) {
329 // If the database supports transactional DDL, we can go ahead and rely
330 // on it. If not, we will have to rollback manually if something fails.
331 $transaction = $this->database->startTransaction();
334 $this->onEntityTypeDelete($original);
335 $this->onEntityTypeCreate($entity_type);
337 catch (\Exception $e) {
338 if ($this->database->supportsTransactionalDDL()) {
339 $transaction->rollBack();
342 // Recreate original schema.
343 $this->onEntityTypeCreate($original);
349 // Drop original indexes and unique keys.
350 $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
352 // Create new indexes and unique keys.
353 $entity_schema = $this->getEntitySchema($entity_type, TRUE);
354 $this->createEntitySchemaIndexes($entity_schema);
356 // Store the updated entity schema.
357 $this->saveEntitySchemaData($entity_type, $entity_schema);
364 public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
365 $this->checkEntityType($entity_type);
366 $schema_handler = $this->database->schema();
367 $actual_definition = $this->entityManager->getDefinition($entity_type->id());
368 // @todo Instead of switching the wrapped entity type, we should be able to
369 // instantiate a new table mapping for each entity type definition. See
370 // https://www.drupal.org/node/2274017.
371 $this->storage->setEntityType($entity_type);
373 // Delete entity tables.
374 foreach ($this->getEntitySchemaTables() as $table_name) {
375 if ($schema_handler->tableExists($table_name)) {
376 $schema_handler->dropTable($table_name);
380 // Delete dedicated field tables.
381 $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
382 $this->originalDefinitions = $field_storage_definitions;
383 $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
384 foreach ($field_storage_definitions as $field_storage_definition) {
385 // If we have a field having dedicated storage we need to drop it,
386 // otherwise we just remove the related schema data.
387 if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
388 $this->deleteDedicatedTableSchema($field_storage_definition);
390 elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
391 $this->deleteFieldSchemaData($field_storage_definition);
394 $this->originalDefinitions = NULL;
396 $this->storage->setEntityType($actual_definition);
398 // Delete the entity schema.
399 $this->deleteEntitySchemaData($entity_type);
405 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
406 $this->performFieldSchemaOperation('create', $storage_definition);
412 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
413 // Store original definitions so that switching between shared and dedicated
414 // field table layout works.
415 $this->originalDefinitions = $this->fieldStorageDefinitions;
416 $this->originalDefinitions[$original->getName()] = $original;
417 $this->performFieldSchemaOperation('update', $storage_definition, $original);
418 $this->originalDefinitions = NULL;
424 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
425 // Only configurable fields currently support purging, so prevent deletion
426 // of ones we can't purge if they have existing data.
427 // @todo Add purging to all fields: https://www.drupal.org/node/2282119.
429 if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
430 throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
433 catch (DatabaseException $e) {
434 // This may happen when changing field storage schema, since we are not
435 // able to use a table mapping matching the passed storage definition.
436 // @todo Revisit this once we are able to instantiate the table mapping
437 // properly. See https://www.drupal.org/node/2274017.
441 // Retrieve a table mapping which contains the deleted field still.
442 $table_mapping = $this->storage->getTableMapping(
443 $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
445 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
446 // Move the table to a unique name while the table contents are being
448 $table = $table_mapping->getDedicatedDataTableName($storage_definition);
449 $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
450 $this->database->schema()->renameTable($table, $new_table);
451 if ($this->entityType->isRevisionable()) {
452 $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
453 $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
454 $this->database->schema()->renameTable($revision_table, $revision_new_table);
458 // @todo Remove when finalizePurge() is invoked from the outside for all
459 // fields: https://www.drupal.org/node/2282119.
460 if (!($storage_definition instanceof FieldStorageConfigInterface)) {
461 $this->performFieldSchemaOperation('delete', $storage_definition);
468 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
469 $this->performFieldSchemaOperation('delete', $storage_definition);
473 * Checks that we are dealing with the correct entity type.
475 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
476 * The entity type to be checked.
479 * TRUE if the entity type matches the current one.
481 * @throws \Drupal\Core\Entity\EntityStorageException
483 protected function checkEntityType(EntityTypeInterface $entity_type) {
484 if ($entity_type->id() != $this->entityType->id()) {
485 throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
491 * Gets the entity schema for the specified entity type.
493 * Entity types may override this method in order to optimize the generated
494 * schema of the entity tables. However, only cross-field optimizations should
495 * be added here; e.g., an index spanning multiple fields. Optimizations that
496 * apply to a single field have to be added via
497 * SqlContentEntityStorageSchema::getSharedTableFieldSchema() instead.
499 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
500 * The entity type definition.
502 * (optional) If set to TRUE static cache will be ignored and a new schema
503 * array generation will be performed. Defaults to FALSE.
506 * A Schema API array describing the entity schema, excluding dedicated
509 * @throws \Drupal\Core\Field\FieldException
511 protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
512 $this->checkEntityType($entity_type);
513 $entity_type_id = $entity_type->id();
515 if (!isset($this->schema[$entity_type_id]) || $reset) {
516 // Back up the storage definition and replace it with the passed one.
517 // @todo Instead of switching the wrapped entity type, we should be able
518 // to instantiate a new table mapping for each entity type definition.
519 // See https://www.drupal.org/node/2274017.
520 $actual_definition = $this->entityManager->getDefinition($entity_type_id);
521 $this->storage->setEntityType($entity_type);
523 // Prepare basic information about the entity type.
524 $tables = $this->getEntitySchemaTables();
526 // Initialize the table schema.
527 $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type);
528 if (isset($tables['revision_table'])) {
529 $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type);
531 if (isset($tables['data_table'])) {
532 $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
534 if (isset($tables['revision_data_table'])) {
535 $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
538 // We need to act only on shared entity schema tables.
539 $table_mapping = $this->storage->getTableMapping();
540 $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
541 foreach ($table_names as $table_name) {
542 if (!isset($schema[$table_name])) {
543 $schema[$table_name] = [];
545 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
546 if (!isset($this->fieldStorageDefinitions[$field_name])) {
547 throw new FieldException("Field storage definition for '$field_name' could not be found.");
549 // Add the schema for base field definitions.
550 elseif ($table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) {
551 $column_names = $table_mapping->getColumnNames($field_name);
552 $storage_definition = $this->fieldStorageDefinitions[$field_name];
553 $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
558 // Process tables after having gathered field information.
559 $this->processBaseTable($entity_type, $schema[$tables['base_table']]);
560 if (isset($tables['revision_table'])) {
561 $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
563 if (isset($tables['data_table'])) {
564 $this->processDataTable($entity_type, $schema[$tables['data_table']]);
566 if (isset($tables['revision_data_table'])) {
567 $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
570 // Add an index for the 'published' entity key.
571 if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) {
572 $published_key = $entity_type->getKey('published');
573 if ($published_key && !$this->fieldStorageDefinitions[$published_key]->hasCustomStorage()) {
574 $published_field_table = $table_mapping->getFieldTableName($published_key);
575 $id_key = $entity_type->getKey('id');
576 if ($bundle_key = $entity_type->getKey('bundle')) {
577 $key = "{$published_key}_{$bundle_key}";
578 $columns = [$published_key, $bundle_key, $id_key];
581 $key = $published_key;
582 $columns = [$published_key, $id_key];
584 $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns;
588 $this->schema[$entity_type_id] = $schema;
590 // Restore the actual definition.
591 $this->storage->setEntityType($actual_definition);
594 return $this->schema[$entity_type_id];
598 * Gets a list of entity type tables.
601 * A list of entity type tables, keyed by table key.
603 protected function getEntitySchemaTables() {
604 return array_filter([
605 'base_table' => $this->storage->getBaseTable(),
606 'revision_table' => $this->storage->getRevisionTable(),
607 'data_table' => $this->storage->getDataTable(),
608 'revision_data_table' => $this->storage->getRevisionDataTable(),
613 * Gets entity schema definitions for index and key definitions.
615 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
616 * The entity type definition.
617 * @param array $schema
618 * The entity schema array.
621 * A stripped down version of the $schema Schema API array containing, for
622 * each table, only the key and index definitions not derived from field
623 * storage definitions.
625 protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
626 $entity_type_id = $entity_type->id();
628 // Collect all possible field schema identifiers for shared table fields.
629 // These will be used to detect entity schema data in the subsequent loop.
630 $field_schema_identifiers = [];
631 $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
632 foreach ($this->fieldStorageDefinitions as $field_name => $storage_definition) {
633 if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
634 // Make sure both base identifier names and suffixed names are listed.
635 $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name);
636 $field_schema_identifiers[$name] = $name;
637 foreach ($storage_definition->getColumns() as $key => $columns) {
638 $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
639 $field_schema_identifiers[$name] = $name;
644 // Extract entity schema data from the Schema API definition.
646 $keys = ['indexes', 'unique keys'];
647 $unused_keys = array_flip(['description', 'fields', 'foreign keys']);
648 foreach ($schema as $table_name => $table_schema) {
649 $table_schema = array_diff_key($table_schema, $unused_keys);
650 foreach ($keys as $key) {
651 // Exclude data generated from field storage definitions, we will check
653 if ($field_schema_identifiers && !empty($table_schema[$key])) {
654 $table_schema[$key] = array_diff_key($table_schema[$key], $field_schema_identifiers);
657 $schema_data[$table_name] = array_filter($table_schema);
664 * Gets an index schema array for a given field.
666 * @param string $field_name
667 * The name of the field.
668 * @param array $field_schema
669 * The schema of the field.
670 * @param string[] $column_mapping
671 * A mapping of field column names to database column names.
674 * The schema definition for the indexes.
676 protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) {
677 return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes');
681 * Gets a unique key schema array for a given field.
683 * @param string $field_name
684 * The name of the field.
685 * @param array $field_schema
686 * The schema of the field.
687 * @param string[] $column_mapping
688 * A mapping of field column names to database column names.
691 * The schema definition for the unique keys.
693 protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) {
694 return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys');
698 * Gets field schema data for the given key.
700 * @param string $field_name
701 * The name of the field.
702 * @param array $field_schema
703 * The schema of the field.
704 * @param string[] $column_mapping
705 * A mapping of field column names to database column names.
706 * @param string $schema_key
707 * The type of schema data. Either 'indexes' or 'unique keys'.
710 * The schema definition for the specified key.
712 protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) {
715 $entity_type_id = $this->entityType->id();
716 foreach ($field_schema[$schema_key] as $key => $columns) {
717 // To avoid clashes with entity-level indexes or unique keys we use
718 // "{$entity_type_id}_field__" as a prefix instead of just
719 // "{$entity_type_id}__". We additionally namespace the specifier by the
720 // field name to avoid clashes when multiple fields of the same type are
721 // added to an entity type.
722 $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
723 foreach ($columns as $column) {
724 // Allow for indexes and unique keys to specified as an array of column
726 if (is_array($column)) {
727 list($column_name, $length) = $column;
728 $data[$real_key][] = [$column_mapping[$column_name], $length];
731 $data[$real_key][] = $column_mapping[$column];
740 * Generates a safe schema identifier (name of an index, column name etc.).
742 * @param string $entity_type_id
743 * The ID of the entity type.
744 * @param string $field_name
745 * The name of the field.
746 * @param string|null $key
747 * (optional) A further key to append to the name.
750 * The field identifier name.
752 protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key = NULL) {
753 $real_key = isset($key) ? "{$entity_type_id}_field__{$field_name}__{$key}" : "{$entity_type_id}_field__{$field_name}";
754 // Limit the string to 48 characters, keeping a 16 characters margin for db
756 if (strlen($real_key) > 48) {
757 // Use a shorter separator, a truncated entity_type, and a hash of the
759 // Truncate to the same length for the current and revision tables.
760 $entity_type = substr($entity_type_id, 0, 36);
761 $field_hash = substr(hash('sha256', $real_key), 0, 10);
762 $real_key = $entity_type . '__' . $field_hash;
768 * Gets field foreign keys.
770 * @param string $field_name
771 * The name of the field.
772 * @param array $field_schema
773 * The schema of the field.
774 * @param string[] $column_mapping
775 * A mapping of field column names to database column names.
778 * The schema definition for the foreign keys.
780 protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) {
783 foreach ($field_schema['foreign keys'] as $specifier => $specification) {
784 // To avoid clashes with entity-level foreign keys we use
785 // "{$entity_type_id}_field__" as a prefix instead of just
786 // "{$entity_type_id}__". We additionally namespace the specifier by the
787 // field name to avoid clashes when multiple fields of the same type are
788 // added to an entity type.
789 $entity_type_id = $this->entityType->id();
790 $real_specifier = "{$entity_type_id}_field__{$field_name}__{$specifier}";
791 $foreign_keys[$real_specifier]['table'] = $specification['table'];
792 foreach ($specification['columns'] as $column => $referenced) {
793 $foreign_keys[$real_specifier]['columns'][$column_mapping[$column]] = $referenced;
797 return $foreign_keys;
801 * Loads stored schema data for the given entity type definition.
803 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
804 * The entity type definition.
807 * The entity schema data array.
809 protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
810 return $this->installedStorageSchema()->get($entity_type->id() . '.entity_schema_data', []);
814 * Stores schema data for the given entity type definition.
816 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
817 * The entity type definition.
818 * @param array $schema
819 * The entity schema data array.
821 protected function saveEntitySchemaData(EntityTypeInterface $entity_type, $schema) {
822 $data = $this->getEntitySchemaData($entity_type, $schema);
823 $this->installedStorageSchema()->set($entity_type->id() . '.entity_schema_data', $data);
827 * Deletes schema data for the given entity type definition.
829 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
830 * The entity type definition.
832 protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
833 $this->installedStorageSchema()->delete($entity_type->id() . '.entity_schema_data');
837 * Loads stored schema data for the given field storage definition.
839 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
840 * The field storage definition.
843 * The field schema data array.
845 protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
846 return $this->installedStorageSchema()->get($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), []);
850 * Stores schema data for the given field storage definition.
852 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
853 * The field storage definition.
854 * @param array $schema
855 * The field schema data array.
857 protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) {
858 $this->processFieldStorageSchema($schema);
859 $this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema);
863 * Deletes schema data for the given field storage definition.
865 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
866 * The field storage definition.
868 protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
869 $this->installedStorageSchema()->delete($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName());
873 * Initializes common information for a base table.
875 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
879 * A partial schema array for the base table.
881 protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
882 $entity_type_id = $entity_type->id();
885 'description' => "The base table for $entity_type_id entities.",
886 'primary key' => [$entity_type->getKey('id')],
888 'foreign keys' => [],
891 if ($entity_type->hasKey('revision')) {
892 $revision_key = $entity_type->getKey('revision');
893 $key_name = $this->getEntityIndexName($entity_type, $revision_key);
894 $schema['unique keys'][$key_name] = [$revision_key];
895 $schema['foreign keys'][$entity_type_id . '__revision'] = [
896 'table' => $this->storage->getRevisionTable(),
897 'columns' => [$revision_key => $revision_key],
901 $this->addTableDefaults($schema);
907 * Initializes common information for a revision table.
909 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
913 * A partial schema array for the revision table.
915 protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) {
916 $entity_type_id = $entity_type->id();
917 $id_key = $entity_type->getKey('id');
918 $revision_key = $entity_type->getKey('revision');
921 'description' => "The revision table for $entity_type_id entities.",
922 'primary key' => [$revision_key],
925 $entity_type_id . '__revisioned' => [
926 'table' => $this->storage->getBaseTable(),
927 'columns' => [$id_key => $id_key],
932 $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = [$id_key];
934 $this->addTableDefaults($schema);
940 * Initializes common information for a data table.
942 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
946 * A partial schema array for the data table.
948 protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
949 $entity_type_id = $entity_type->id();
950 $id_key = $entity_type->getKey('id');
953 'description' => "The data table for $entity_type_id entities.",
954 'primary key' => [$id_key, $entity_type->getKey('langcode')],
956 $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
960 'table' => $this->storage->getBaseTable(),
961 'columns' => [$id_key => $id_key],
966 if ($entity_type->hasKey('revision')) {
967 $key = $entity_type->getKey('revision');
968 $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = [$key];
971 $this->addTableDefaults($schema);
977 * Initializes common information for a revision data table.
979 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
983 * A partial schema array for the revision data table.
985 protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) {
986 $entity_type_id = $entity_type->id();
987 $id_key = $entity_type->getKey('id');
988 $revision_key = $entity_type->getKey('revision');
991 'description' => "The revision data table for $entity_type_id entities.",
992 'primary key' => [$revision_key, $entity_type->getKey('langcode')],
994 $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
998 'table' => $this->storage->getBaseTable(),
999 'columns' => [$id_key => $id_key],
1001 $entity_type_id . '__revision' => [
1002 'table' => $this->storage->getRevisionTable(),
1003 'columns' => [$revision_key => $revision_key],
1008 $this->addTableDefaults($schema);
1014 * Adds defaults to a table schema definition.
1017 * The schema definition array for a single table, passed by reference.
1019 protected function addTableDefaults(&$schema) {
1022 'unique keys' => [],
1024 'foreign keys' => [],
1029 * Processes the gathered schema for a base table.
1031 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1033 * @param array $schema
1034 * The table schema, passed by reference.
1037 * A partial schema array for the base table.
1039 protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1040 $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
1044 * Processes the gathered schema for a base table.
1046 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1048 * @param array $schema
1049 * The table schema, passed by reference.
1052 * A partial schema array for the base table.
1054 protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1055 $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
1059 * Processes the gathered schema for a base table.
1061 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1063 * @param array $schema
1064 * The table schema, passed by reference.
1067 * A partial schema array for the base table.
1069 protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1070 // Marking the respective fields as NOT NULL makes the indexes more
1072 $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1076 * Processes the gathered schema for a base table.
1078 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1080 * @param array $schema
1081 * The table schema, passed by reference.
1084 * A partial schema array for the base table.
1086 protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
1087 // Marking the respective fields as NOT NULL makes the indexes more
1089 $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
1093 * Processes the specified entity key.
1095 * @param array $schema
1096 * The table schema, passed by reference.
1097 * @param string $key
1098 * The entity key name.
1100 protected function processIdentifierSchema(&$schema, $key) {
1101 if ($schema['fields'][$key]['type'] == 'int') {
1102 $schema['fields'][$key]['type'] = 'serial';
1104 $schema['fields'][$key]['not null'] = TRUE;
1105 unset($schema['fields'][$key]['default']);
1109 * Processes the schema for a field storage definition.
1111 * @param array &$field_storage_schema
1112 * An array that contains the schema data for a field storage definition.
1114 protected function processFieldStorageSchema(array &$field_storage_schema) {
1115 // Clean up some schema properties that should not be taken into account
1116 // after a field storage has been created.
1117 foreach ($field_storage_schema as $table_name => $table_schema) {
1118 foreach ($table_schema['fields'] as $key => $schema) {
1119 unset($field_storage_schema[$table_name]['fields'][$key]['initial']);
1120 unset($field_storage_schema[$table_name]['fields'][$key]['initial_from_field']);
1126 * Performs the specified operation on a field.
1128 * This figures out whether the field is stored in a dedicated or shared table
1129 * and forwards the call to the proper handler.
1131 * @param string $operation
1132 * The name of the operation to be performed.
1133 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1134 * The field storage definition.
1135 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1136 * (optional) The original field storage definition. This is relevant (and
1137 * required) only for updates. Defaults to NULL.
1139 protected function performFieldSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) {
1140 $table_mapping = $this->storage->getTableMapping();
1141 if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1142 $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original);
1144 elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1145 $this->{$operation . 'SharedTableSchema'}($storage_definition, $original);
1150 * Creates the schema for a field stored in a dedicated table.
1152 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1153 * The storage definition of the field being created.
1155 protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1156 $schema = $this->getDedicatedTableSchema($storage_definition);
1157 foreach ($schema as $name => $table) {
1158 // Check if the table exists because it might already have been
1159 // created as part of the earlier entity type update event.
1160 if (!$this->database->schema()->tableExists($name)) {
1161 $this->database->schema()->createTable($name, $table);
1164 $this->saveFieldSchemaData($storage_definition, $schema);
1168 * Creates the schema for a field stored in a shared table.
1170 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1171 * The storage definition of the field being created.
1172 * @param bool $only_save
1173 * (optional) Whether to skip modification of database tables and only save
1174 * the schema data for future comparison. For internal use only. This is
1175 * used by onEntityTypeCreate() after it has already fully created the
1178 protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) {
1179 $created_field_name = $storage_definition->getName();
1180 $table_mapping = $this->storage->getTableMapping();
1181 $column_names = $table_mapping->getColumnNames($created_field_name);
1182 $schema_handler = $this->database->schema();
1183 $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
1185 // Iterate over the mapped table to find the ones that will host the created
1188 foreach ($shared_table_names as $table_name) {
1189 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1190 if ($field_name == $created_field_name) {
1191 // Create field columns.
1192 $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1194 foreach ($schema[$table_name]['fields'] as $name => $specifier) {
1195 // Check if the field exists because it might already have been
1196 // created as part of the earlier entity type update event.
1197 if (!$schema_handler->fieldExists($table_name, $name)) {
1198 $schema_handler->addField($table_name, $name, $specifier);
1201 if (!empty($schema[$table_name]['indexes'])) {
1202 foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
1203 // Check if the index exists because it might already have been
1204 // created as part of the earlier entity type update event.
1205 $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
1208 if (!empty($schema[$table_name]['unique keys'])) {
1209 foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
1210 $schema_handler->addUniqueKey($table_name, $name, $specifier);
1214 // After creating the field schema skip to the next table.
1220 $this->saveFieldSchemaData($storage_definition, $schema);
1223 // Make sure any entity index involving this field is re-created if
1225 $this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
1230 * Deletes the schema for a field stored in a dedicated table.
1232 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1233 * The storage definition of the field being deleted.
1235 protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1236 // When switching from dedicated to shared field table layout we need need
1237 // to delete the field tables with their regular names. When this happens
1238 // original definitions will be defined.
1239 $deleted = !$this->originalDefinitions;
1240 $table_mapping = $this->storage->getTableMapping();
1241 $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted);
1242 if ($this->database->schema()->tableExists($table_name)) {
1243 $this->database->schema()->dropTable($table_name);
1245 if ($this->entityType->isRevisionable()) {
1246 $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted);
1247 if ($this->database->schema()->tableExists($revision_table_name)) {
1248 $this->database->schema()->dropTable($revision_table_name);
1251 $this->deleteFieldSchemaData($storage_definition);
1255 * Deletes the schema for a field stored in a shared table.
1257 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1258 * The storage definition of the field being deleted.
1260 protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
1261 // Make sure any entity index involving this field is dropped.
1262 $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
1264 $deleted_field_name = $storage_definition->getName();
1265 $table_mapping = $this->storage->getTableMapping(
1266 $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1268 $column_names = $table_mapping->getColumnNames($deleted_field_name);
1269 $schema_handler = $this->database->schema();
1270 $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
1272 // Iterate over the mapped table to find the ones that host the deleted
1274 foreach ($shared_table_names as $table_name) {
1275 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1276 if ($field_name == $deleted_field_name) {
1277 $schema = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1279 // Drop indexes and unique keys first.
1280 if (!empty($schema['indexes'])) {
1281 foreach ($schema['indexes'] as $name => $specifier) {
1282 $schema_handler->dropIndex($table_name, $name);
1285 if (!empty($schema['unique keys'])) {
1286 foreach ($schema['unique keys'] as $name => $specifier) {
1287 $schema_handler->dropUniqueKey($table_name, $name);
1291 foreach ($column_names as $column_name) {
1292 $schema_handler->dropField($table_name, $column_name);
1294 // After deleting the field schema skip to the next table.
1300 $this->deleteFieldSchemaData($storage_definition);
1304 * Updates the schema for a field stored in a shared table.
1306 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1307 * The storage definition of the field being updated.
1308 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1309 * The original storage definition; i.e., the definition before the update.
1311 * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
1312 * Thrown when the update to the field is forbidden.
1313 * @throws \Exception
1314 * Rethrown exception if the table recreation fails.
1316 protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1317 if (!$this->storage->countFieldData($original, TRUE)) {
1318 // There is no data. Re-create the tables completely.
1319 if ($this->database->supportsTransactionalDDL()) {
1320 // If the database supports transactional DDL, we can go ahead and rely
1321 // on it. If not, we will have to rollback manually if something fails.
1322 $transaction = $this->database->startTransaction();
1325 // Since there is no data we may be switching from a shared table schema
1326 // to a dedicated table schema, hence we should use the proper API.
1327 $this->performFieldSchemaOperation('delete', $original);
1328 $this->performFieldSchemaOperation('create', $storage_definition);
1330 catch (\Exception $e) {
1331 if ($this->database->supportsTransactionalDDL()) {
1332 $transaction->rollBack();
1336 $this->performFieldSchemaOperation('create', $original);
1342 if ($this->hasColumnChanges($storage_definition, $original)) {
1343 throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
1345 // There is data, so there are no column changes. Drop all the prior
1346 // indexes and create all the new ones, except for all the priors that
1348 $table_mapping = $this->storage->getTableMapping();
1349 $table = $table_mapping->getDedicatedDataTableName($original);
1350 $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
1352 // Get the field schemas.
1353 $schema = $storage_definition->getSchema();
1354 $original_schema = $original->getSchema();
1356 // Gets the SQL schema for a dedicated tables.
1357 $actual_schema = $this->getDedicatedTableSchema($storage_definition);
1359 foreach ($original_schema['indexes'] as $name => $columns) {
1360 if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
1361 $real_name = $this->getFieldIndexName($storage_definition, $name);
1362 $this->database->schema()->dropIndex($table, $real_name);
1363 $this->database->schema()->dropIndex($revision_table, $real_name);
1366 $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1367 $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1368 foreach ($schema['indexes'] as $name => $columns) {
1369 if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
1370 $real_name = $this->getFieldIndexName($storage_definition, $name);
1372 foreach ($columns as $column_name) {
1373 // Indexes can be specified as either a column name or an array with
1374 // column name and length. Allow for either case.
1375 if (is_array($column_name)) {
1377 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1382 $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1385 // Check if the index exists because it might already have been
1386 // created as part of the earlier entity type update event.
1387 $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
1388 $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
1391 $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
1396 * Updates the schema for a field stored in a shared table.
1398 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1399 * The storage definition of the field being updated.
1400 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
1401 * The original storage definition; i.e., the definition before the update.
1403 * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
1404 * Thrown when the update to the field is forbidden.
1405 * @throws \Exception
1406 * Rethrown exception if the table recreation fails.
1408 protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1409 if (!$this->storage->countFieldData($original, TRUE)) {
1410 if ($this->database->supportsTransactionalDDL()) {
1411 // If the database supports transactional DDL, we can go ahead and rely
1412 // on it. If not, we will have to rollback manually if something fails.
1413 $transaction = $this->database->startTransaction();
1416 // Since there is no data we may be switching from a dedicated table
1417 // to a schema table schema, hence we should use the proper API.
1418 $this->performFieldSchemaOperation('delete', $original);
1419 $this->performFieldSchemaOperation('create', $storage_definition);
1421 catch (\Exception $e) {
1422 if ($this->database->supportsTransactionalDDL()) {
1423 $transaction->rollBack();
1426 // Recreate original schema.
1427 $this->createSharedTableSchema($original);
1433 if ($this->hasColumnChanges($storage_definition, $original)) {
1434 throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
1437 $updated_field_name = $storage_definition->getName();
1438 $table_mapping = $this->storage->getTableMapping();
1439 $column_names = $table_mapping->getColumnNames($updated_field_name);
1440 $schema_handler = $this->database->schema();
1442 // Iterate over the mapped table to find the ones that host the deleted
1444 $original_schema = $this->loadFieldSchemaData($original);
1446 foreach ($table_mapping->getTableNames() as $table_name) {
1447 foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
1448 if ($field_name == $updated_field_name) {
1449 $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
1451 // Handle NOT NULL constraints.
1452 foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
1453 $not_null = !empty($specifier['not null']);
1454 $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
1455 if ($not_null !== $original_not_null) {
1456 if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
1457 throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
1459 $column_schema = $original_schema[$table_name]['fields'][$column_name];
1460 $column_schema['not null'] = $not_null;
1461 $schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
1465 // Drop original indexes and unique keys.
1466 if (!empty($original_schema[$table_name]['indexes'])) {
1467 foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
1468 $schema_handler->dropIndex($table_name, $name);
1471 if (!empty($original_schema[$table_name]['unique keys'])) {
1472 foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) {
1473 $schema_handler->dropUniqueKey($table_name, $name);
1476 // Create new indexes and unique keys.
1477 if (!empty($schema[$table_name]['indexes'])) {
1478 foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
1479 // Check if the index exists because it might already have been
1480 // created as part of the earlier entity type update event.
1481 $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
1485 if (!empty($schema[$table_name]['unique keys'])) {
1486 foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
1487 $schema_handler->addUniqueKey($table_name, $name, $specifier);
1490 // After deleting the field schema skip to the next table.
1495 $this->saveFieldSchemaData($storage_definition, $schema);
1500 * Creates the specified entity schema indexes and keys.
1502 * @param array $entity_schema
1503 * The entity schema definition.
1504 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
1505 * (optional) If a field storage definition is specified, only indexes and
1506 * keys involving its columns will be processed. Otherwise all defined
1507 * entity indexes and keys will be processed.
1509 protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
1510 $schema_handler = $this->database->schema();
1512 if ($storage_definition) {
1513 $table_mapping = $this->storage->getTableMapping();
1514 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1518 'indexes' => 'addIndex',
1519 'unique keys' => 'addUniqueKey',
1522 foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
1523 // Add fields schema because database driver may depend on this data to
1524 // perform index normalization.
1525 $schema['fields'] = $entity_schema[$table_name]['fields'];
1527 foreach ($index_keys as $key => $add_method) {
1528 if (!empty($schema[$key])) {
1529 foreach ($schema[$key] as $name => $specifier) {
1530 // If a set of field columns were specified we process only indexes
1531 // involving them. Only indexes for which all columns exist are
1532 // actually created.
1534 $specifier_columns = array_map(function ($item) {
1535 return is_string($item) ? $item : reset($item);
1537 if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1539 foreach ($specifier_columns as $specifier_column_name) {
1540 // This may happen when adding more than one field in the same
1541 // update run. Eventually when all field columns have been
1542 // created the index will be created too.
1543 if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
1550 $this->{$add_method}($table_name, $name, $specifier, $schema);
1559 * Deletes the specified entity schema indexes and keys.
1561 * @param array $entity_schema_data
1562 * The entity schema data definition.
1563 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
1564 * (optional) If a field storage definition is specified, only indexes and
1565 * keys involving its columns will be processed. Otherwise all defined
1566 * entity indexes and keys will be processed.
1568 protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
1569 $schema_handler = $this->database->schema();
1571 if ($storage_definition) {
1572 $table_mapping = $this->storage->getTableMapping();
1573 $column_names = $table_mapping->getColumnNames($storage_definition->getName());
1577 'indexes' => 'dropIndex',
1578 'unique keys' => 'dropUniqueKey',
1581 foreach ($entity_schema_data as $table_name => $schema) {
1582 foreach ($index_keys as $key => $drop_method) {
1583 if (!empty($schema[$key])) {
1584 foreach ($schema[$key] as $name => $specifier) {
1585 $specifier_columns = array_map(function ($item) {
1586 return is_string($item) ? $item : reset($item);
1588 if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
1589 $schema_handler->{$drop_method}($table_name, $name);
1598 * Checks whether a field property has NULL values.
1600 * @param string $table_name
1601 * The name of the table to inspect.
1602 * @param string $column_name
1603 * The name of the column holding the field property data.
1606 * TRUE if NULL data is found, FALSE otherwise.
1608 protected function hasNullFieldPropertyData($table_name, $column_name) {
1609 $query = $this->database->select($table_name, 't')
1610 ->fields('t', [$column_name])
1612 $query->isNull('t.' . $column_name);
1613 $result = $query->execute()->fetchAssoc();
1614 return (bool) $result;
1618 * Gets the schema for a single field definition.
1620 * Entity types may override this method in order to optimize the generated
1621 * schema for given field. While all optimizations that apply to a single
1622 * field have to be added here, all cross-field optimizations should be via
1623 * SqlContentEntityStorageSchema::getEntitySchema() instead; e.g.,
1624 * an index spanning multiple fields.
1626 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1627 * The storage definition of the field whose schema has to be returned.
1628 * @param string $table_name
1629 * The name of the table columns will be added to.
1630 * @param string[] $column_mapping
1631 * A mapping of field column names to database column names.
1634 * The schema definition for the table with the following keys:
1635 * - fields: The schema definition for the each field columns.
1636 * - indexes: The schema definition for the indexes.
1637 * - unique keys: The schema definition for the unique keys.
1638 * - foreign keys: The schema definition for the foreign keys.
1640 * @throws \Drupal\Core\Field\FieldException
1641 * Exception thrown if the schema contains reserved column names or if the
1642 * initial values definition is invalid.
1644 protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
1646 $table_mapping = $this->storage->getTableMapping();
1647 $field_schema = $storage_definition->getSchema();
1649 // Check that the schema does not include forbidden column names.
1650 if (array_intersect(array_keys($field_schema['columns']), $table_mapping->getReservedColumns())) {
1651 throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
1654 $field_name = $storage_definition->getName();
1655 $base_table = $this->storage->getBaseTable();
1657 // Define the initial values, if any.
1658 $initial_value = $initial_value_from_field = [];
1659 $storage_definition_is_new = empty($this->loadFieldSchemaData($storage_definition));
1660 if ($storage_definition_is_new && $storage_definition instanceof BaseFieldDefinition && $table_mapping->allowsSharedTableStorage($storage_definition)) {
1661 if (($initial_storage_value = $storage_definition->getInitialValue()) && !empty($initial_storage_value)) {
1662 // We only support initial values for fields that are stored in shared
1663 // tables (i.e. single-value fields).
1664 // @todo Implement initial value support for multi-value fields in
1665 // https://www.drupal.org/node/2883851.
1666 $initial_value = reset($initial_storage_value);
1669 if ($initial_value_field_name = $storage_definition->getInitialValueFromField()) {
1670 // Check that the field used for populating initial values is valid. We
1671 // must use the last installed version of that, as the new field might
1672 // be created in an update function and the storage definition of the
1673 // "from" field might get changed later.
1674 $last_installed_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
1675 if (!isset($last_installed_storage_definitions[$initial_value_field_name])) {
1676 throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field $initial_value_field_name does not exist.");
1679 if ($storage_definition->getType() !== $last_installed_storage_definitions[$initial_value_field_name]->getType()) {
1680 throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field types do not match.");
1683 if (!$table_mapping->allowsSharedTableStorage($last_installed_storage_definitions[$initial_value_field_name])) {
1684 throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: Both fields have to be stored in the shared entity tables.");
1687 $initial_value_from_field = $table_mapping->getColumnNames($initial_value_field_name);
1691 // A shared table contains rows for entities where the field is empty
1692 // (since other fields stored in the same table might not be empty), thus
1693 // the only columns that can be 'not null' are those for required
1694 // properties of required fields. For now, we only hardcode 'not null' to a
1695 // few "entity keys", in order to keep their indexes optimized.
1696 // @todo Fix this in https://www.drupal.org/node/2841291.
1697 $not_null_keys = $this->entityType->getKeys();
1698 // Label and the 'revision_translation_affected' fields are not necessarily
1700 unset($not_null_keys['label'], $not_null_keys['revision_translation_affected']);
1701 // Because entity ID and revision ID are both serial fields in the base and
1702 // revision table respectively, the revision ID is not known yet, when
1703 // inserting data into the base table. Instead the revision ID in the base
1704 // table is updated after the data has been inserted into the revision
1705 // table. For this reason the revision ID field cannot be marked as NOT
1707 if ($table_name == $base_table) {
1708 unset($not_null_keys['revision']);
1711 foreach ($column_mapping as $field_column_name => $schema_field_name) {
1712 $column_schema = $field_schema['columns'][$field_column_name];
1714 $schema['fields'][$schema_field_name] = $column_schema;
1715 $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
1717 // Use the initial value of the field storage, if available.
1718 if ($initial_value && isset($initial_value[$field_column_name])) {
1719 $schema['fields'][$schema_field_name]['initial'] = drupal_schema_get_field_value($column_schema, $initial_value[$field_column_name]);
1721 elseif (!empty($initial_value_from_field)) {
1722 $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name];
1726 if (!empty($field_schema['indexes'])) {
1727 $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
1730 if (!empty($field_schema['unique keys'])) {
1731 $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
1734 if (!empty($field_schema['foreign keys'])) {
1735 $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
1742 * Adds an index for the specified field to the given schema definition.
1744 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1745 * The storage definition of the field for which an index should be added.
1746 * @param array $schema
1747 * A reference to the schema array to be updated.
1748 * @param bool $not_null
1749 * (optional) Whether to also add a 'not null' constraint to the column
1750 * being indexed. Doing so improves index performance. Defaults to FALSE,
1751 * in case the field needs to support NULL values.
1753 * (optional) The index size. Defaults to no limit.
1755 protected function addSharedTableFieldIndex(FieldStorageDefinitionInterface $storage_definition, &$schema, $not_null = FALSE, $size = NULL) {
1756 $name = $storage_definition->getName();
1757 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1758 $schema['indexes'][$real_key] = [$size ? [$name, $size] : $name];
1760 $schema['fields'][$name]['not null'] = TRUE;
1765 * Adds a unique key for the specified field to the given schema definition.
1767 * Also adds a 'not null' constraint, because many databases do not reliably
1768 * support unique keys on null columns.
1770 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1771 * The storage definition of the field to which to add a unique key.
1772 * @param array $schema
1773 * A reference to the schema array to be updated.
1775 protected function addSharedTableFieldUniqueKey(FieldStorageDefinitionInterface $storage_definition, &$schema) {
1776 $name = $storage_definition->getName();
1777 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1778 $schema['unique keys'][$real_key] = [$name];
1779 $schema['fields'][$name]['not null'] = TRUE;
1783 * Adds a foreign key for the specified field to the given schema definition.
1785 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1786 * The storage definition of the field to which to add a foreign key.
1787 * @param array $schema
1788 * A reference to the schema array to be updated.
1789 * @param string $foreign_table
1790 * The foreign table.
1791 * @param string $foreign_column
1792 * The foreign column.
1794 protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface $storage_definition, &$schema, $foreign_table, $foreign_column) {
1795 $name = $storage_definition->getName();
1796 $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
1797 $schema['foreign keys'][$real_key] = [
1798 'table' => $foreign_table,
1799 'columns' => [$name => $foreign_column],
1804 * Gets the SQL schema for a dedicated table.
1806 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1807 * The field storage definition.
1808 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1809 * (optional) The entity type definition. Defaults to the one returned by
1810 * the entity manager.
1813 * The schema definition for the table with the following keys:
1814 * - fields: The schema definition for the each field columns.
1815 * - indexes: The schema definition for the indexes.
1816 * - unique keys: The schema definition for the unique keys.
1817 * - foreign keys: The schema definition for the foreign keys.
1819 * @throws \Drupal\Core\Field\FieldException
1820 * Exception thrown if the schema contains reserved column names.
1822 * @see hook_schema()
1824 protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
1825 $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
1826 $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
1828 $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')];
1829 if ($id_definition->getType() == 'integer') {
1834 'description' => 'The entity id this data is attached to',
1839 'type' => 'varchar_ascii',
1842 'description' => 'The entity id this data is attached to',
1846 // Define the revision ID schema.
1847 if (!$this->entityType->isRevisionable()) {
1848 $revision_id_schema = $id_schema;
1849 $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';
1851 elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') {
1852 $revision_id_schema = [
1856 'description' => 'The entity revision id this data is attached to',
1860 $revision_id_schema = [
1861 'type' => 'varchar',
1864 'description' => 'The entity revision id this data is attached to',
1869 'description' => $description_current,
1872 'type' => 'varchar_ascii',
1876 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
1883 'description' => 'A boolean indicating whether this data item has been deleted'
1885 'entity_id' => $id_schema,
1886 'revision_id' => $revision_id_schema,
1888 'type' => 'varchar_ascii',
1892 'description' => 'The language code for this data item.',
1898 'description' => 'The sequence number for this data item, used for multi-value fields',
1901 'primary key' => ['entity_id', 'deleted', 'delta', 'langcode'],
1903 'bundle' => ['bundle'],
1904 'revision_id' => ['revision_id'],
1908 // Check that the schema does not include forbidden column names.
1909 $schema = $storage_definition->getSchema();
1910 $properties = $storage_definition->getPropertyDefinitions();
1911 $table_mapping = $this->storage->getTableMapping();
1912 if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
1913 throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
1916 // Add field columns.
1917 foreach ($schema['columns'] as $column_name => $attributes) {
1918 $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1919 $data_schema['fields'][$real_name] = $attributes;
1920 // A dedicated table only contain rows for actual field values, and no
1921 // rows for entities where the field is empty. Thus, we can safely
1922 // enforce 'not null' on the columns for the field's required properties.
1923 $data_schema['fields'][$real_name]['not null'] = $properties[$column_name]->isRequired();
1927 foreach ($schema['indexes'] as $index_name => $columns) {
1928 $real_name = $this->getFieldIndexName($storage_definition, $index_name);
1929 foreach ($columns as $column_name) {
1930 // Indexes can be specified as either a column name or an array with
1931 // column name and length. Allow for either case.
1932 if (is_array($column_name)) {
1933 $data_schema['indexes'][$real_name][] = [
1934 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1939 $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1945 foreach ($schema['unique keys'] as $index_name => $columns) {
1946 $real_name = $this->getFieldIndexName($storage_definition, $index_name);
1947 foreach ($columns as $column_name) {
1948 // Unique keys can be specified as either a column name or an array with
1949 // column name and length. Allow for either case.
1950 if (is_array($column_name)) {
1951 $data_schema['unique keys'][$real_name][] = [
1952 $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
1957 $data_schema['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1962 // Add foreign keys.
1963 foreach ($schema['foreign keys'] as $specifier => $specification) {
1964 $real_name = $this->getFieldIndexName($storage_definition, $specifier);
1965 $data_schema['foreign keys'][$real_name]['table'] = $specification['table'];
1966 foreach ($specification['columns'] as $column_name => $referenced) {
1967 $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name);
1968 $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
1972 $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema];
1974 // If the entity type is revisionable, construct the revision table.
1975 $entity_type = $entity_type ?: $this->entityType;
1976 if ($entity_type->isRevisionable()) {
1977 $revision_schema = $data_schema;
1978 $revision_schema['description'] = $description_revision;
1979 $revision_schema['primary key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode'];
1980 $revision_schema['fields']['revision_id']['not null'] = TRUE;
1981 $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
1982 $dedicated_table_schema += [$table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema];
1985 return $dedicated_table_schema;
1989 * Gets the name to be used for the given entity index.
1991 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
1993 * @param string $index
1994 * The index column name.
1999 protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
2000 return $entity_type->id() . '__' . $index;
2004 * Generates an index name for a field data table.
2006 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
2007 * The field storage definition.
2008 * @param string $index
2009 * The name of the index.
2012 * A string containing a generated index name for a field data table that is
2013 * unique among all other fields.
2015 protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
2016 return $storage_definition->getName() . '_' . $index;
2020 * Checks whether a database table is non-existent or empty.
2022 * Empty tables can be dropped and recreated without data loss.
2024 * @param string $table_name
2025 * The database table to check.
2028 * TRUE if the table is empty, FALSE otherwise.
2030 protected function isTableEmpty($table_name) {
2031 return !$this->database->schema()->tableExists($table_name) ||
2032 !$this->database->select($table_name)
2040 * Compares schemas to check for changes in the column definitions.
2042 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
2043 * Current field storage definition.
2044 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
2045 * The original field storage definition.
2048 * Returns TRUE if there are schema changes in the column definitions.
2050 protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
2051 if ($storage_definition->getColumns() != $original->getColumns()) {
2052 // Base field definitions have schema data stored in the original
2057 if (!$storage_definition->hasCustomStorage()) {
2058 $keys = array_flip($this->getColumnSchemaRelevantKeys());
2059 $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
2060 foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
2061 foreach ($table_schema['fields'] as $name => $spec) {
2062 $definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
2063 $stored_spec = array_intersect_key($spec, $keys);
2064 if ($definition_spec != $stored_spec) {
2075 * Returns a list of column schema keys affecting data storage.
2077 * When comparing schema definitions, only changes in certain properties
2078 * actually affect how data is stored and thus, if applied, may imply data
2082 * An array of key names.
2084 protected function getColumnSchemaRelevantKeys() {
2085 return ['type', 'size', 'length', 'unsigned'];
2089 * Creates an index, dropping it if already existing.
2091 * @param string $table
2093 * @param string $name
2095 * @param array $specifier
2096 * The fields to index.
2097 * @param array $schema
2098 * The table specification.
2100 * @see \Drupal\Core\Database\Schema::addIndex()
2102 protected function addIndex($table, $name, array $specifier, array $schema) {
2103 $schema_handler = $this->database->schema();
2104 $schema_handler->dropIndex($table, $name);
2105 $schema_handler->addIndex($table, $name, $specifier, $schema);
2109 * Creates a unique key, dropping it if already existing.
2111 * @param string $table
2113 * @param string $name
2115 * @param array $specifier
2116 * The unique fields.
2118 * @see \Drupal\Core\Database\Schema::addUniqueKey()
2120 protected function addUniqueKey($table, $name, array $specifier) {
2121 $schema_handler = $this->database->schema();
2122 $schema_handler->dropUniqueKey($table, $name);
2123 $schema_handler->addUniqueKey($table, $name, $specifier);