namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Database\Connection;
-use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldException;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Language\LanguageInterface;
/**
* Defines a schema handler that supports revisionable, translatable entities.
*/
protected $installedStorageSchema;
+ /**
+ * The deleted fields repository.
+ *
+ * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
+ */
+ protected $deletedFieldsRepository;
+
/**
* Constructs a SqlContentEntityStorageSchema.
*
return $this->installedStorageSchema;
}
+ /**
+ * Gets the deleted fields repository.
+ *
+ * @return \Drupal\Core\Field\DeletedFieldsRepositoryInterface
+ * The deleted fields repository.
+ *
+ * @todo Inject this dependency in the constructor once this class can be
+ * instantiated as a regular entity handler:
+ * https://www.drupal.org/node/2332857.
+ */
+ protected function deletedFieldsRepository() {
+ if (!isset($this->deletedFieldsRepository)) {
+ $this->deletedFieldsRepository = \Drupal::service('entity_field.deleted_fields_repository');
+ }
+ return $this->deletedFieldsRepository;
+ }
+
/**
* {@inheritdoc}
*/
return FALSE;
}
- return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original);
+ $current_schema = $this->getSchemaFromStorageDefinition($storage_definition);
+ $this->processFieldStorageSchema($current_schema);
+ $installed_schema = $this->loadFieldSchemaData($original);
+ $this->processFieldStorageSchema($installed_schema);
+
+ return $current_schema != $installed_schema;
}
/**
* The schema data.
*/
protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
- assert('!$storage_definition->hasCustomStorage();');
+ assert(!$storage_definition->hasCustomStorage());
$table_mapping = $this->storage->getTableMapping();
$schema = [];
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
* {@inheritdoc}
*/
public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+ // Check if the entity type specifies that data migration is being handled
+ // elsewhere.
+ if ($entity_type->get('requires_data_migration') === FALSE) {
+ return FALSE;
+ }
+
// If the original storage has existing entities, or it is impossible to
// determine if that is the case, require entity data to be migrated.
$original_storage_class = $original->getStorageClass();
}
// Create dedicated field tables.
- $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type->id());
- $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
- foreach ($field_storage_definitions as $field_storage_definition) {
+ $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
+ foreach ($this->fieldStorageDefinitions as $field_storage_definition) {
if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
$this->createDedicatedTableSchema($field_storage_definition);
}
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this->checkEntityType($entity_type);
$schema_handler = $this->database->schema();
- $actual_definition = $this->entityManager->getDefinition($entity_type->id());
- // @todo Instead of switching the wrapped entity type, we should be able to
- // instantiate a new table mapping for each entity type definition. See
- // https://www.drupal.org/node/2274017.
- $this->storage->setEntityType($entity_type);
-
- // Delete entity tables.
- foreach ($this->getEntitySchemaTables() as $table_name) {
+
+ $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
+ $table_mapping = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions);
+
+ // Delete entity and field tables.
+ foreach ($table_mapping->getTableNames() as $table_name) {
if ($schema_handler->tableExists($table_name)) {
$schema_handler->dropTable($table_name);
}
}
- // Delete dedicated field tables.
- $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
- $this->originalDefinitions = $field_storage_definitions;
- $table_mapping = $this->storage->getTableMapping($field_storage_definitions);
+ // Delete the field schema data.
foreach ($field_storage_definitions as $field_storage_definition) {
- // If we have a field having dedicated storage we need to drop it,
- // otherwise we just remove the related schema data.
- if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
- $this->deleteDedicatedTableSchema($field_storage_definition);
- }
- elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
- $this->deleteFieldSchemaData($field_storage_definition);
- }
+ $this->deleteFieldSchemaData($field_storage_definition);
}
- $this->originalDefinitions = NULL;
-
- $this->storage->setEntityType($actual_definition);
// Delete the entity schema.
$this->deleteEntitySchemaData($entity_type);
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
// Store original definitions so that switching between shared and dedicated
// field table layout works.
- $this->originalDefinitions = $this->fieldStorageDefinitions;
- $this->originalDefinitions[$original->getName()] = $original;
$this->performFieldSchemaOperation('update', $storage_definition, $original);
- $this->originalDefinitions = NULL;
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
- // Only configurable fields currently support purging, so prevent deletion
- // of ones we can't purge if they have existing data.
- // @todo Add purging to all fields: https://www.drupal.org/node/2282119.
try {
- if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
- throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
- }
+ $has_data = $this->storage->countFieldData($storage_definition, TRUE);
}
- catch (DatabaseException $e) {
+ catch (DatabaseExceptionWrapper $e) {
// This may happen when changing field storage schema, since we are not
// able to use a table mapping matching the passed storage definition.
// @todo Revisit this once we are able to instantiate the table mapping
return;
}
+ // If the field storage does not have any data, we can safely delete its
+ // schema.
+ if (!$has_data) {
+ $this->performFieldSchemaOperation('delete', $storage_definition);
+ return;
+ }
+
+ // There's nothing else we can do if the field storage has a custom storage.
+ if ($storage_definition->hasCustomStorage()) {
+ return;
+ }
+
// Retrieve a table mapping which contains the deleted field still.
- $table_mapping = $this->storage->getTableMapping(
- $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
- );
+ $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
+ $table_mapping = $this->storage->getTableMapping($storage_definitions);
+ $field_table_name = $table_mapping->getFieldTableName($storage_definition->getName());
+
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
// Move the table to a unique name while the table contents are being
// deleted.
$this->database->schema()->renameTable($revision_table, $revision_new_table);
}
}
+ else {
+ // Move the field data from the shared table to a dedicated one in order
+ // to allow it to be purged like any other field.
+ $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName());
- // @todo Remove when finalizePurge() is invoked from the outside for all
- // fields: https://www.drupal.org/node/2282119.
- if (!($storage_definition instanceof FieldStorageConfigInterface)) {
- $this->performFieldSchemaOperation('delete', $storage_definition);
+ // Refresh the table mapping to use the deleted storage definition.
+ $deleted_storage_definition = $this->deletedFieldsRepository()->getFieldStorageDefinitions()[$storage_definition->getUniqueStorageIdentifier()];
+ $original_storage_definitions = [$storage_definition->getName() => $deleted_storage_definition] + $storage_definitions;
+ $table_mapping = $this->storage->getTableMapping($original_storage_definitions);
+
+ $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition);
+ $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName());
+
+ $dedicated_table_name = $table_mapping->getDedicatedDataTableName($deleted_storage_definition, TRUE);
+ $dedicated_table_name_mapping[$table_mapping->getDedicatedDataTableName($deleted_storage_definition)] = $dedicated_table_name;
+ if ($this->entityType->isRevisionable()) {
+ $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_storage_definition, TRUE);
+ $dedicated_table_name_mapping[$table_mapping->getDedicatedRevisionTableName($deleted_storage_definition)] = $dedicated_revision_table_name;
+ }
+
+ // Create the dedicated field tables using "deleted" table names.
+ foreach ($dedicated_table_field_schema as $name => $table) {
+ if (!$this->database->schema()->tableExists($dedicated_table_name_mapping[$name])) {
+ $this->database->schema()->createTable($dedicated_table_name_mapping[$name], $table);
+ }
+ else {
+ throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.');
+ }
+ }
+
+ if ($this->database->supportsTransactionalDDL()) {
+ // If the database supports transactional DDL, we can go ahead and rely
+ // on it. If not, we will have to rollback manually if something fails.
+ $transaction = $this->database->startTransaction();
+ }
+ try {
+ // Copy the data from the base table.
+ $this->database->insert($dedicated_table_name)
+ ->from($this->getSelectQueryForFieldStorageDeletion($field_table_name, $shared_table_field_columns, $dedicated_table_field_columns))
+ ->execute();
+
+ // Copy the data from the revision table.
+ if (isset($dedicated_revision_table_name)) {
+ if ($this->entityType->isTranslatable()) {
+ $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionDataTable() : $this->storage->getDataTable();
+ }
+ else {
+ $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionTable() : $this->storage->getBaseTable();
+ }
+ $this->database->insert($dedicated_revision_table_name)
+ ->from($this->getSelectQueryForFieldStorageDeletion($revision_table, $shared_table_field_columns, $dedicated_table_field_columns, $field_table_name))
+ ->execute();
+ }
+ }
+ catch (\Exception $e) {
+ if (isset($transaction)) {
+ $transaction->rollBack();
+ }
+ else {
+ // Delete the dedicated tables.
+ foreach ($dedicated_table_field_schema as $name => $table) {
+ $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]);
+ }
+ }
+ throw $e;
+ }
+
+ // Delete the field from the shared tables.
+ $this->deleteSharedTableSchema($storage_definition);
}
}
$this->performFieldSchemaOperation('delete', $storage_definition);
}
+ /**
+ * Returns a SELECT query suitable for inserting data into a dedicated table.
+ *
+ * @param string $table_name
+ * The entity table name to select from.
+ * @param array $shared_table_field_columns
+ * An array of field column names for a shared table schema.
+ * @param array $dedicated_table_field_columns
+ * An array of field column names for a dedicated table schema.
+ * @param string $base_table
+ * (optional) The name of the base entity table. Defaults to NULL.
+ *
+ * @return \Drupal\Core\Database\Query\SelectInterface
+ * A database select query.
+ */
+ protected function getSelectQueryForFieldStorageDeletion($table_name, array $shared_table_field_columns, array $dedicated_table_field_columns, $base_table = NULL) {
+ // Create a SELECT query that generates a result suitable for writing into
+ // a dedicated field table.
+ $select = $this->database->select($table_name, 'entity_table');
+
+ // Add the bundle column.
+ if ($bundle = $this->entityType->getKey('bundle')) {
+ if ($base_table) {
+ $select->join($base_table, 'base_table', "entity_table.{$this->entityType->getKey('id')} = %alias.{$this->entityType->getKey('id')}");
+ $select->addField('base_table', $bundle, 'bundle');
+ }
+ else {
+ $select->addField('entity_table', $bundle, 'bundle');
+ }
+ }
+ else {
+ $select->addExpression(':bundle', 'bundle', [':bundle' => $this->entityType->id()]);
+ }
+
+ // Add the deleted column.
+ $select->addExpression(':deleted', 'deleted', [':deleted' => 1]);
+
+ // Add the entity_id column.
+ $select->addField('entity_table', $this->entityType->getKey('id'), 'entity_id');
+
+ // Add the revision_id column.
+ if ($this->entityType->isRevisionable()) {
+ $select->addField('entity_table', $this->entityType->getKey('revision'), 'revision_id');
+ }
+ else {
+ $select->addField('entity_table', $this->entityType->getKey('id'), 'revision_id');
+ }
+
+ // Add the langcode column.
+ if ($langcode = $this->entityType->getKey('langcode')) {
+ $select->addField('entity_table', $langcode, 'langcode');
+ }
+ else {
+ $select->addExpression(':langcode', 'langcode', [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]);
+ }
+
+ // Add the delta column and set it to 0 because we are only dealing with
+ // single cardinality fields.
+ $select->addExpression(':delta', 'delta', [':delta' => 0]);
+
+ // Add all the dynamic field columns.
+ $or = $select->orConditionGroup();
+ foreach ($shared_table_field_columns as $field_column_name => $schema_column_name) {
+ $select->addField('entity_table', $schema_column_name, $dedicated_table_field_columns[$field_column_name]);
+ $or->isNotNull('entity_table.' . $schema_column_name);
+ }
+ $select->condition($or);
+
+ // Lock the table rows.
+ $select->forUpdate(TRUE);
+
+ return $select;
+ }
+
/**
* Checks that we are dealing with the correct entity type.
*
$entity_type_id = $entity_type->id();
if (!isset($this->schema[$entity_type_id]) || $reset) {
- // Back up the storage definition and replace it with the passed one.
- // @todo Instead of switching the wrapped entity type, we should be able
- // to instantiate a new table mapping for each entity type definition.
- // See https://www.drupal.org/node/2274017.
- $actual_definition = $this->entityManager->getDefinition($entity_type_id);
- $this->storage->setEntityType($entity_type);
-
// Prepare basic information about the entity type.
$tables = $this->getEntitySchemaTables();
}
// We need to act only on shared entity schema tables.
- $table_mapping = $this->storage->getTableMapping();
+ $table_mapping = $this->storage->getCustomTableMapping($entity_type, $this->fieldStorageDefinitions);
$table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
- $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
foreach ($table_names as $table_name) {
if (!isset($schema[$table_name])) {
$schema[$table_name] = [];
}
foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
- if (!isset($storage_definitions[$field_name])) {
+ if (!isset($this->fieldStorageDefinitions[$field_name])) {
throw new FieldException("Field storage definition for '$field_name' could not be found.");
}
// Add the schema for base field definitions.
- elseif ($table_mapping->allowsSharedTableStorage($storage_definitions[$field_name])) {
+ elseif ($table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) {
$column_names = $table_mapping->getColumnNames($field_name);
- $storage_definition = $storage_definitions[$field_name];
+ $storage_definition = $this->fieldStorageDefinitions[$field_name];
$schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
}
}
// Add an index for the 'published' entity key.
if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) {
$published_key = $entity_type->getKey('published');
- if ($published_key && !$storage_definitions[$published_key]->hasCustomStorage()) {
+ if ($published_key && !$this->fieldStorageDefinitions[$published_key]->hasCustomStorage()) {
$published_field_table = $table_mapping->getFieldTableName($published_key);
$id_key = $entity_type->getKey('id');
if ($bundle_key = $entity_type->getKey('bundle')) {
}
$this->schema[$entity_type_id] = $schema;
-
- // Restore the actual definition.
- $this->storage->setEntityType($actual_definition);
}
return $this->schema[$entity_type_id];
// Collect all possible field schema identifiers for shared table fields.
// These will be used to detect entity schema data in the subsequent loop.
$field_schema_identifiers = [];
- $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
- $table_mapping = $this->storage->getTableMapping($storage_definitions);
- foreach ($storage_definitions as $field_name => $storage_definition) {
+ $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
+ foreach ($this->fieldStorageDefinitions as $field_name => $storage_definition) {
if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
// Make sure both base identifier names and suffixed names are listed.
$name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name);
* The field schema data array.
*/
protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) {
+ $this->processFieldStorageSchema($schema);
$this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema);
}
$entity_type_id . '__revision' => [
'table' => $this->storage->getRevisionTable(),
'columns' => [$revision_key => $revision_key],
- ]
+ ],
],
];
* The entity type.
* @param array $schema
* The table schema, passed by reference.
- *
- * @return array
- * A partial schema array for the base table.
*/
protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
- $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
+ // Process the schema for the 'id' entity key only if it exists.
+ if ($entity_type->hasKey('id')) {
+ $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
+ }
}
/**
* The entity type.
* @param array $schema
* The table schema, passed by reference.
- *
- * @return array
- * A partial schema array for the base table.
*/
protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
- $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
+ // Process the schema for the 'revision' entity key only if it exists.
+ if ($entity_type->hasKey('revision')) {
+ $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
+ }
}
/**
unset($schema['fields'][$key]['default']);
}
+ /**
+ * Processes the schema for a field storage definition.
+ *
+ * @param array &$field_storage_schema
+ * An array that contains the schema data for a field storage definition.
+ */
+ protected function processFieldStorageSchema(array &$field_storage_schema) {
+ // Clean up some schema properties that should not be taken into account
+ // after a field storage has been created.
+ foreach ($field_storage_schema as $table_name => $table_schema) {
+ foreach ($table_schema['fields'] as $key => $schema) {
+ unset($field_storage_schema[$table_name]['fields'][$key]['initial']);
+ unset($field_storage_schema[$table_name]['fields'][$key]['initial_from_field']);
+ }
+ }
+ }
+
/**
* Performs the specified operation on a field.
*
// Create field columns.
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
if (!$only_save) {
+ // The entity schema needs to be checked because the field schema is
+ // potentially incomplete.
+ // @todo Fix this in https://www.drupal.org/node/2929120.
+ $entity_schema = $this->getEntitySchema($this->entityType);
foreach ($schema[$table_name]['fields'] as $name => $specifier) {
+ // Check if the field is part of the primary keys and pass along
+ // this information when adding the field.
+ // @see \Drupal\Core\Database\Schema::addField()
+ $new_keys = [];
+ if (isset($entity_schema[$table_name]['primary key']) && array_intersect($column_names, $entity_schema[$table_name]['primary key'])) {
+ $new_keys = ['primary key' => $entity_schema[$table_name]['primary key']];
+ }
+
// Check if the field exists because it might already have been
// created as part of the earlier entity type update event.
if (!$schema_handler->fieldExists($table_name, $name)) {
- $schema_handler->addField($table_name, $name, $specifier);
+ $schema_handler->addField($table_name, $name, $specifier, $new_keys);
}
}
if (!empty($schema[$table_name]['indexes'])) {
* The storage definition of the field being deleted.
*/
protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
- // When switching from dedicated to shared field table layout we need need
- // to delete the field tables with their regular names. When this happens
- // original definitions will be defined.
- $deleted = !$this->originalDefinitions;
$table_mapping = $this->storage->getTableMapping();
- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted);
- $this->database->schema()->dropTable($table_name);
+ $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
+ if ($this->database->schema()->tableExists($table_name)) {
+ $this->database->schema()->dropTable($table_name);
+ }
if ($this->entityType->isRevisionable()) {
- $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted);
- $this->database->schema()->dropTable($revision_name);
+ $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $storage_definition->isDeleted());
+ if ($this->database->schema()->tableExists($revision_table_name)) {
+ $this->database->schema()->dropTable($revision_table_name);
+ }
}
$this->deleteFieldSchemaData($storage_definition);
}
}
$column_schema = $original_schema[$table_name]['fields'][$column_name];
$column_schema['not null'] = $not_null;
- $schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
+ $schema_handler->changeField($table_name, $column_name, $column_name, $column_schema);
}
}
// involving them. Only indexes for which all columns exist are
// actually created.
$create = FALSE;
- $specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
+ $specifier_columns = array_map(function ($item) {
+ return is_string($item) ? $item : reset($item);
+ }, $specifier);
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
$create = TRUE;
foreach ($specifier_columns as $specifier_column_name) {
foreach ($index_keys as $key => $drop_method) {
if (!empty($schema[$key])) {
foreach ($schema[$key] as $name => $specifier) {
- $specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
+ $specifier_columns = array_map(function ($item) {
+ return is_string($item) ? $item : reset($item);
+ }, $specifier);
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
$schema_handler->{$drop_method}($table_name, $name);
}
* - foreign keys: The schema definition for the foreign keys.
*
* @throws \Drupal\Core\Field\FieldException
- * Exception thrown if the schema contains reserved column names.
+ * Exception thrown if the schema contains reserved column names or if the
+ * initial values definition is invalid.
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = [];
+ $table_mapping = $this->storage->getTableMapping();
$field_schema = $storage_definition->getSchema();
// Check that the schema does not include forbidden column names.
- if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) {
+ if (array_intersect(array_keys($field_schema['columns']), $table_mapping->getReservedColumns())) {
throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
}
$field_name = $storage_definition->getName();
$base_table = $this->storage->getBaseTable();
+ // Define the initial values, if any.
+ $initial_value = $initial_value_from_field = [];
+ $storage_definition_is_new = empty($this->loadFieldSchemaData($storage_definition));
+ if ($storage_definition_is_new && $storage_definition instanceof BaseFieldDefinition && $table_mapping->allowsSharedTableStorage($storage_definition)) {
+ if (($initial_storage_value = $storage_definition->getInitialValue()) && !empty($initial_storage_value)) {
+ // We only support initial values for fields that are stored in shared
+ // tables (i.e. single-value fields).
+ // @todo Implement initial value support for multi-value fields in
+ // https://www.drupal.org/node/2883851.
+ $initial_value = reset($initial_storage_value);
+ }
+
+ if ($initial_value_field_name = $storage_definition->getInitialValueFromField()) {
+ // Check that the field used for populating initial values is valid. We
+ // must use the last installed version of that, as the new field might
+ // be created in an update function and the storage definition of the
+ // "from" field might get changed later.
+ $last_installed_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
+ if (!isset($last_installed_storage_definitions[$initial_value_field_name])) {
+ throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field $initial_value_field_name does not exist.");
+ }
+
+ if ($storage_definition->getType() !== $last_installed_storage_definitions[$initial_value_field_name]->getType()) {
+ throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field types do not match.");
+ }
+
+ if (!$table_mapping->allowsSharedTableStorage($last_installed_storage_definitions[$initial_value_field_name])) {
+ throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: Both fields have to be stored in the shared entity tables.");
+ }
+
+ $initial_value_from_field = $table_mapping->getColumnNames($initial_value_field_name);
+ }
+ }
+
// A shared table contains rows for entities where the field is empty
// (since other fields stored in the same table might not be empty), thus
// the only columns that can be 'not null' are those for required
- // properties of required fields. However, even those would break in the
- // case where a new field is added to a table that contains existing rows.
- // For now, we only hardcode 'not null' to a couple "entity keys", in order
- // to keep their indexes optimized.
- // @todo Revisit once we have support for 'initial' in
- // https://www.drupal.org/node/2346019.
+ // properties of required fields. For now, we only hardcode 'not null' to a
+ // few "entity keys", in order to keep their indexes optimized.
+ // @todo Fix this in https://www.drupal.org/node/2841291.
$not_null_keys = $this->entityType->getKeys();
- // Label fields are not necessarily required.
- unset($not_null_keys['label']);
+ // Label and the 'revision_translation_affected' fields are not necessarily
+ // required.
+ unset($not_null_keys['label'], $not_null_keys['revision_translation_affected']);
// Because entity ID and revision ID are both serial fields in the base and
// revision table respectively, the revision ID is not known yet, when
// inserting data into the base table. Instead the revision ID in the base
$schema['fields'][$schema_field_name] = $column_schema;
$schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
+
+ // Use the initial value of the field storage, if available.
+ if ($initial_value && isset($initial_value[$field_column_name])) {
+ $schema['fields'][$schema_field_name]['initial'] = drupal_schema_get_field_value($column_schema, $initial_value[$field_column_name]);
+ }
+ if (!empty($initial_value_from_field)) {
+ $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name];
+ }
}
if (!empty($field_schema['indexes'])) {
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
- 'description' => 'A boolean indicating whether this data item has been deleted'
+ 'description' => 'A boolean indicating whether this data item has been deleted',
],
'entity_id' => $id_schema,
'revision_id' => $revision_id_schema,