123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289 |
- <?php
- namespace Drupal\Core\Entity\Sql;
- use Drupal\Core\Database\Connection;
- use Drupal\Core\Database\DatabaseExceptionWrapper;
- use Drupal\Core\DependencyInjection\DependencySerializationTrait;
- use Drupal\Core\Entity\ContentEntityTypeInterface;
- use Drupal\Core\Entity\EntityManagerInterface;
- use Drupal\Core\Entity\EntityPublishedInterface;
- use Drupal\Core\Entity\EntityStorageException;
- 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\Core\Language\LanguageInterface;
- /**
- * Defines a schema handler that supports revisionable, translatable entities.
- *
- * Entity types may extend this class and optimize the generated schema for all
- * entity base tables by overriding getEntitySchema() for cross-field
- * optimizations and getSharedTableFieldSchema() for optimizations applying to
- * a single field.
- */
- class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorageSchemaInterface {
- use DependencySerializationTrait;
- /**
- * The entity manager.
- *
- * @var \Drupal\Core\Entity\EntityManagerInterface
- */
- protected $entityManager;
- /**
- * The entity type this schema builder is responsible for.
- *
- * @var \Drupal\Core\Entity\ContentEntityTypeInterface
- */
- protected $entityType;
- /**
- * The storage field definitions for this entity type.
- *
- * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
- */
- protected $fieldStorageDefinitions;
- /**
- * The original storage field definitions for this entity type. Used during
- * field schema updates.
- *
- * @var \Drupal\Core\Field\FieldDefinitionInterface[]
- */
- protected $originalDefinitions;
- /**
- * The storage object for the given entity type.
- *
- * @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage
- */
- protected $storage;
- /**
- * A static cache of the generated schema array.
- *
- * @var array
- */
- protected $schema;
- /**
- * The database connection to be used.
- *
- * @var \Drupal\Core\Database\Connection
- */
- protected $database;
- /**
- * The key-value collection for tracking installed storage schema.
- *
- * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
- */
- protected $installedStorageSchema;
- /**
- * The deleted fields repository.
- *
- * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
- */
- protected $deletedFieldsRepository;
- /**
- * Constructs a SqlContentEntityStorageSchema.
- *
- * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
- * The entity manager.
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type.
- * @param \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage
- * The storage of the entity type. This must be an SQL-based storage.
- * @param \Drupal\Core\Database\Connection $database
- * The database connection to be used.
- */
- public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, SqlContentEntityStorage $storage, Connection $database) {
- $this->entityManager = $entity_manager;
- $this->entityType = $entity_type;
- $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
- $this->storage = $storage;
- $this->database = $database;
- }
- /**
- * Gets the keyvalue collection for tracking the installed schema.
- *
- * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
- *
- * @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 installedStorageSchema() {
- if (!isset($this->installedStorageSchema)) {
- $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
- }
- 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}
- */
- public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
- return
- $this->hasSharedTableStructureChange($entity_type, $original) ||
- // Detect changes in key or index definitions.
- $this->getEntitySchemaData($entity_type, $this->getEntitySchema($entity_type, TRUE)) != $this->loadEntitySchemaData($original);
- }
- /**
- * Detects whether there is a change in the shared table structure.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The new entity type.
- * @param \Drupal\Core\Entity\EntityTypeInterface $original
- * The origin entity type.
- *
- * @return bool
- * Returns TRUE if either the revisionable or translatable flag changes or
- * a table has been renamed.
- */
- protected function hasSharedTableStructureChange(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
- return
- $entity_type->isRevisionable() != $original->isRevisionable() ||
- $entity_type->isTranslatable() != $original->isTranslatable() ||
- $this->hasSharedTableNameChanges($entity_type, $original);
- }
- /**
- * Detects whether any table name got renamed in an entity type update.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The new entity type.
- * @param \Drupal\Core\Entity\EntityTypeInterface $original
- * The origin entity type.
- *
- * @return bool
- * Returns TRUE if there have been changes.
- */
- protected function hasSharedTableNameChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
- $base_table = $this->database->schema()->tableExists($entity_type->getBaseTable());
- $data_table = $this->database->schema()->tableExists($entity_type->getDataTable());
- $revision_table = $this->database->schema()->tableExists($entity_type->getRevisionTable());
- $revision_data_table = $this->database->schema()->tableExists($entity_type->getRevisionDataTable());
- // We first check if the new table already exists because the storage might
- // have created it even though it wasn't specified in the entity type
- // definition.
- return
- (!$base_table && $entity_type->getBaseTable() != $original->getBaseTable()) ||
- (!$data_table && $entity_type->getDataTable() != $original->getDataTable()) ||
- (!$revision_table && $entity_type->getRevisionTable() != $original->getRevisionTable()) ||
- (!$revision_data_table && $entity_type->getRevisionDataTable() != $original->getRevisionDataTable());
- }
- /**
- * {@inheritdoc}
- */
- public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- $table_mapping = $this->storage->getTableMapping();
- if (
- $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
- $storage_definition->getSchema() != $original->getSchema() ||
- $storage_definition->isRevisionable() != $original->isRevisionable() ||
- $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) ||
- $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original)
- ) {
- return TRUE;
- }
- if ($storage_definition->hasCustomStorage()) {
- // The field has custom storage, so we don't know if a schema change is
- // needed or not, but since per the initial checks earlier in this
- // function, nothing about the definition changed that we manage, we
- // return FALSE.
- return FALSE;
- }
- $current_schema = $this->getSchemaFromStorageDefinition($storage_definition);
- $this->processFieldStorageSchema($current_schema);
- $installed_schema = $this->loadFieldSchemaData($original);
- $this->processFieldStorageSchema($installed_schema);
- return $current_schema != $installed_schema;
- }
- /**
- * Gets the schema data for the given field storage definition.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field storage definition. The field that must not have custom
- * storage, i.e. the storage must take care of storing the field.
- *
- * @return array
- * The schema data.
- */
- protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
- assert(!$storage_definition->hasCustomStorage());
- $table_mapping = $this->storage->getTableMapping();
- $schema = [];
- if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- $schema = $this->getDedicatedTableSchema($storage_definition);
- }
- elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
- $field_name = $storage_definition->getName();
- foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
- if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
- $column_names = $table_mapping->getColumnNames($storage_definition->getName());
- $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
- }
- }
- }
- return $schema;
- }
- /**
- * {@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();
- if (!class_exists($original_storage_class)) {
- return TRUE;
- }
- // Data migration is not needed when only indexes changed, as they can be
- // applied if there is data.
- if (!$this->hasSharedTableStructureChange($entity_type, $original)) {
- return FALSE;
- }
- // Use the original entity type since the storage has not been updated.
- $original_storage = $this->entityManager->createHandlerInstance($original_storage_class, $original);
- return $original_storage->hasData();
- }
- /**
- * {@inheritdoc}
- */
- public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- return !$this->storage->countFieldData($original, TRUE);
- }
- /**
- * {@inheritdoc}
- */
- public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
- $this->checkEntityType($entity_type);
- $schema_handler = $this->database->schema();
- // Create entity tables.
- $schema = $this->getEntitySchema($entity_type, TRUE);
- foreach ($schema as $table_name => $table_schema) {
- if (!$schema_handler->tableExists($table_name)) {
- $schema_handler->createTable($table_name, $table_schema);
- }
- }
- // Create dedicated field tables.
- $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);
- }
- elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
- // The shared tables are already fully created, but we need to save the
- // per-field schema definitions for later use.
- $this->createSharedTableSchema($field_storage_definition, TRUE);
- }
- }
- // Save data about entity indexes and keys.
- $this->saveEntitySchemaData($entity_type, $schema);
- }
- /**
- * {@inheritdoc}
- */
- public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
- $this->checkEntityType($entity_type);
- $this->checkEntityType($original);
- // If no schema changes are needed, we don't need to do anything.
- if (!$this->requiresEntityStorageSchemaChanges($entity_type, $original)) {
- return;
- }
- // If a migration is required, we can't proceed.
- if ($this->requiresEntityDataMigration($entity_type, $original)) {
- throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type->id() . ') with data.');
- }
- // If we have no data just recreate the entity schema from scratch.
- if ($this->isTableEmpty($this->storage->getBaseTable())) {
- 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 {
- $this->onEntityTypeDelete($original);
- $this->onEntityTypeCreate($entity_type);
- }
- catch (\Exception $e) {
- if ($this->database->supportsTransactionalDDL()) {
- $transaction->rollBack();
- }
- else {
- // Recreate original schema.
- $this->onEntityTypeCreate($original);
- }
- throw $e;
- }
- }
- else {
- // Drop original indexes and unique keys.
- $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
- // Create new indexes and unique keys.
- $entity_schema = $this->getEntitySchema($entity_type, TRUE);
- $this->createEntitySchemaIndexes($entity_schema);
- // Store the updated entity schema.
- $this->saveEntitySchemaData($entity_type, $entity_schema);
- }
- }
- /**
- * {@inheritdoc}
- */
- 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) {
- 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);
- 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->originalDefinitions = NULL;
- $this->storage->setEntityType($actual_definition);
- // Delete the entity schema.
- $this->deleteEntitySchemaData($entity_type);
- }
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
- $this->performFieldSchemaOperation('create', $storage_definition);
- }
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- // Store original definitions so that switching between shared and dedicated
- // field table layout works.
- $this->performFieldSchemaOperation('update', $storage_definition, $original);
- }
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
- try {
- $has_data = $this->storage->countFieldData($storage_definition, TRUE);
- }
- 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
- // properly. See https://www.drupal.org/node/2274017.
- 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.
- $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.
- $table = $table_mapping->getDedicatedDataTableName($storage_definition);
- $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
- $this->database->schema()->renameTable($table, $new_table);
- if ($this->entityType->isRevisionable()) {
- $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
- $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
- $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());
- // 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);
- }
- }
- /**
- * {@inheritdoc}
- */
- public function finalizePurge(FieldStorageDefinitionInterface $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.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The entity type to be checked.
- *
- * @return bool
- * TRUE if the entity type matches the current one.
- *
- * @throws \Drupal\Core\Entity\EntityStorageException
- */
- protected function checkEntityType(EntityTypeInterface $entity_type) {
- if ($entity_type->id() != $this->entityType->id()) {
- throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
- }
- return TRUE;
- }
- /**
- * Gets the entity schema for the specified entity type.
- *
- * Entity types may override this method in order to optimize the generated
- * schema of the entity tables. However, only cross-field optimizations should
- * be added here; e.g., an index spanning multiple fields. Optimizations that
- * apply to a single field have to be added via
- * SqlContentEntityStorageSchema::getSharedTableFieldSchema() instead.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type definition.
- * @param bool $reset
- * (optional) If set to TRUE static cache will be ignored and a new schema
- * array generation will be performed. Defaults to FALSE.
- *
- * @return array
- * A Schema API array describing the entity schema, excluding dedicated
- * field tables.
- *
- * @throws \Drupal\Core\Field\FieldException
- */
- protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
- $this->checkEntityType($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();
- // Initialize the table schema.
- $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type);
- if (isset($tables['revision_table'])) {
- $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type);
- }
- if (isset($tables['data_table'])) {
- $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
- }
- if (isset($tables['revision_data_table'])) {
- $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
- }
- // We need to act only on shared entity schema tables.
- $table_mapping = $this->storage->getTableMapping();
- $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
- 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($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($this->fieldStorageDefinitions[$field_name])) {
- $column_names = $table_mapping->getColumnNames($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));
- }
- }
- }
- // Process tables after having gathered field information.
- $this->processBaseTable($entity_type, $schema[$tables['base_table']]);
- if (isset($tables['revision_table'])) {
- $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
- }
- if (isset($tables['data_table'])) {
- $this->processDataTable($entity_type, $schema[$tables['data_table']]);
- }
- if (isset($tables['revision_data_table'])) {
- $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
- }
- // 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 && !$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')) {
- $key = "{$published_key}_{$bundle_key}";
- $columns = [$published_key, $bundle_key, $id_key];
- }
- else {
- $key = $published_key;
- $columns = [$published_key, $id_key];
- }
- $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns;
- }
- }
- $this->schema[$entity_type_id] = $schema;
- // Restore the actual definition.
- $this->storage->setEntityType($actual_definition);
- }
- return $this->schema[$entity_type_id];
- }
- /**
- * Gets a list of entity type tables.
- *
- * @return array
- * A list of entity type tables, keyed by table key.
- */
- protected function getEntitySchemaTables() {
- return array_filter([
- 'base_table' => $this->storage->getBaseTable(),
- 'revision_table' => $this->storage->getRevisionTable(),
- 'data_table' => $this->storage->getDataTable(),
- 'revision_data_table' => $this->storage->getRevisionDataTable(),
- ]);
- }
- /**
- * Gets entity schema definitions for index and key definitions.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type definition.
- * @param array $schema
- * The entity schema array.
- *
- * @return array
- * A stripped down version of the $schema Schema API array containing, for
- * each table, only the key and index definitions not derived from field
- * storage definitions.
- */
- protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
- $entity_type_id = $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 = [];
- $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);
- $field_schema_identifiers[$name] = $name;
- foreach ($storage_definition->getColumns() as $key => $columns) {
- $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
- $field_schema_identifiers[$name] = $name;
- }
- }
- }
- // Extract entity schema data from the Schema API definition.
- $schema_data = [];
- $keys = ['indexes', 'unique keys'];
- $unused_keys = array_flip(['description', 'fields', 'foreign keys']);
- foreach ($schema as $table_name => $table_schema) {
- $table_schema = array_diff_key($table_schema, $unused_keys);
- foreach ($keys as $key) {
- // Exclude data generated from field storage definitions, we will check
- // that separately.
- if ($field_schema_identifiers && !empty($table_schema[$key])) {
- $table_schema[$key] = array_diff_key($table_schema[$key], $field_schema_identifiers);
- }
- }
- $schema_data[$table_name] = array_filter($table_schema);
- }
- return $schema_data;
- }
- /**
- * Gets an index schema array for a given field.
- *
- * @param string $field_name
- * The name of the field.
- * @param array $field_schema
- * The schema of the field.
- * @param string[] $column_mapping
- * A mapping of field column names to database column names.
- *
- * @return array
- * The schema definition for the indexes.
- */
- protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) {
- return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes');
- }
- /**
- * Gets a unique key schema array for a given field.
- *
- * @param string $field_name
- * The name of the field.
- * @param array $field_schema
- * The schema of the field.
- * @param string[] $column_mapping
- * A mapping of field column names to database column names.
- *
- * @return array
- * The schema definition for the unique keys.
- */
- protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) {
- return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys');
- }
- /**
- * Gets field schema data for the given key.
- *
- * @param string $field_name
- * The name of the field.
- * @param array $field_schema
- * The schema of the field.
- * @param string[] $column_mapping
- * A mapping of field column names to database column names.
- * @param string $schema_key
- * The type of schema data. Either 'indexes' or 'unique keys'.
- *
- * @return array
- * The schema definition for the specified key.
- */
- protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) {
- $data = [];
- $entity_type_id = $this->entityType->id();
- foreach ($field_schema[$schema_key] as $key => $columns) {
- // To avoid clashes with entity-level indexes or unique keys we use
- // "{$entity_type_id}_field__" as a prefix instead of just
- // "{$entity_type_id}__". We additionally namespace the specifier by the
- // field name to avoid clashes when multiple fields of the same type are
- // added to an entity type.
- $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
- foreach ($columns as $column) {
- // Allow for indexes and unique keys to specified as an array of column
- // name and length.
- if (is_array($column)) {
- list($column_name, $length) = $column;
- $data[$real_key][] = [$column_mapping[$column_name], $length];
- }
- else {
- $data[$real_key][] = $column_mapping[$column];
- }
- }
- }
- return $data;
- }
- /**
- * Generates a safe schema identifier (name of an index, column name etc.).
- *
- * @param string $entity_type_id
- * The ID of the entity type.
- * @param string $field_name
- * The name of the field.
- * @param string|null $key
- * (optional) A further key to append to the name.
- *
- * @return string
- * The field identifier name.
- */
- protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key = NULL) {
- $real_key = isset($key) ? "{$entity_type_id}_field__{$field_name}__{$key}" : "{$entity_type_id}_field__{$field_name}";
- // Limit the string to 48 characters, keeping a 16 characters margin for db
- // prefixes.
- if (strlen($real_key) > 48) {
- // Use a shorter separator, a truncated entity_type, and a hash of the
- // field name.
- // Truncate to the same length for the current and revision tables.
- $entity_type = substr($entity_type_id, 0, 36);
- $field_hash = substr(hash('sha256', $real_key), 0, 10);
- $real_key = $entity_type . '__' . $field_hash;
- }
- return $real_key;
- }
- /**
- * Gets field foreign keys.
- *
- * @param string $field_name
- * The name of the field.
- * @param array $field_schema
- * The schema of the field.
- * @param string[] $column_mapping
- * A mapping of field column names to database column names.
- *
- * @return array
- * The schema definition for the foreign keys.
- */
- protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) {
- $foreign_keys = [];
- foreach ($field_schema['foreign keys'] as $specifier => $specification) {
- // To avoid clashes with entity-level foreign keys we use
- // "{$entity_type_id}_field__" as a prefix instead of just
- // "{$entity_type_id}__". We additionally namespace the specifier by the
- // field name to avoid clashes when multiple fields of the same type are
- // added to an entity type.
- $entity_type_id = $this->entityType->id();
- $real_specifier = "{$entity_type_id}_field__{$field_name}__{$specifier}";
- $foreign_keys[$real_specifier]['table'] = $specification['table'];
- foreach ($specification['columns'] as $column => $referenced) {
- $foreign_keys[$real_specifier]['columns'][$column_mapping[$column]] = $referenced;
- }
- }
- return $foreign_keys;
- }
- /**
- * Loads stored schema data for the given entity type definition.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The entity type definition.
- *
- * @return array
- * The entity schema data array.
- */
- protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
- return $this->installedStorageSchema()->get($entity_type->id() . '.entity_schema_data', []);
- }
- /**
- * Stores schema data for the given entity type definition.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The entity type definition.
- * @param array $schema
- * The entity schema data array.
- */
- protected function saveEntitySchemaData(EntityTypeInterface $entity_type, $schema) {
- $data = $this->getEntitySchemaData($entity_type, $schema);
- $this->installedStorageSchema()->set($entity_type->id() . '.entity_schema_data', $data);
- }
- /**
- * Deletes schema data for the given entity type definition.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The entity type definition.
- */
- protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
- $this->installedStorageSchema()->delete($entity_type->id() . '.entity_schema_data');
- }
- /**
- * Loads stored schema data for the given field storage definition.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field storage definition.
- *
- * @return array
- * The field schema data array.
- */
- protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
- return $this->installedStorageSchema()->get($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), []);
- }
- /**
- * Stores schema data for the given field storage definition.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field storage definition.
- * @param array $schema
- * 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);
- }
- /**
- * Deletes schema data for the given field storage definition.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field storage definition.
- */
- protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
- $this->installedStorageSchema()->delete($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName());
- }
- /**
- * Initializes common information for a base table.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type.
- *
- * @return array
- * A partial schema array for the base table.
- */
- protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
- $entity_type_id = $entity_type->id();
- $schema = [
- 'description' => "The base table for $entity_type_id entities.",
- 'primary key' => [$entity_type->getKey('id')],
- 'indexes' => [],
- 'foreign keys' => [],
- ];
- if ($entity_type->hasKey('revision')) {
- $revision_key = $entity_type->getKey('revision');
- $key_name = $this->getEntityIndexName($entity_type, $revision_key);
- $schema['unique keys'][$key_name] = [$revision_key];
- $schema['foreign keys'][$entity_type_id . '__revision'] = [
- 'table' => $this->storage->getRevisionTable(),
- 'columns' => [$revision_key => $revision_key],
- ];
- }
- $this->addTableDefaults($schema);
- return $schema;
- }
- /**
- * Initializes common information for a revision table.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type.
- *
- * @return array
- * A partial schema array for the revision table.
- */
- protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) {
- $entity_type_id = $entity_type->id();
- $id_key = $entity_type->getKey('id');
- $revision_key = $entity_type->getKey('revision');
- $schema = [
- 'description' => "The revision table for $entity_type_id entities.",
- 'primary key' => [$revision_key],
- 'indexes' => [],
- 'foreign keys' => [
- $entity_type_id . '__revisioned' => [
- 'table' => $this->storage->getBaseTable(),
- 'columns' => [$id_key => $id_key],
- ],
- ],
- ];
- $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = [$id_key];
- $this->addTableDefaults($schema);
- return $schema;
- }
- /**
- * Initializes common information for a data table.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type.
- *
- * @return array
- * A partial schema array for the data table.
- */
- protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
- $entity_type_id = $entity_type->id();
- $id_key = $entity_type->getKey('id');
- $schema = [
- 'description' => "The data table for $entity_type_id entities.",
- 'primary key' => [$id_key, $entity_type->getKey('langcode')],
- 'indexes' => [
- $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
- ],
- 'foreign keys' => [
- $entity_type_id => [
- 'table' => $this->storage->getBaseTable(),
- 'columns' => [$id_key => $id_key],
- ],
- ],
- ];
- if ($entity_type->hasKey('revision')) {
- $key = $entity_type->getKey('revision');
- $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = [$key];
- }
- $this->addTableDefaults($schema);
- return $schema;
- }
- /**
- * Initializes common information for a revision data table.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type.
- *
- * @return array
- * A partial schema array for the revision data table.
- */
- protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) {
- $entity_type_id = $entity_type->id();
- $id_key = $entity_type->getKey('id');
- $revision_key = $entity_type->getKey('revision');
- $schema = [
- 'description' => "The revision data table for $entity_type_id entities.",
- 'primary key' => [$revision_key, $entity_type->getKey('langcode')],
- 'indexes' => [
- $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
- ],
- 'foreign keys' => [
- $entity_type_id => [
- 'table' => $this->storage->getBaseTable(),
- 'columns' => [$id_key => $id_key],
- ],
- $entity_type_id . '__revision' => [
- 'table' => $this->storage->getRevisionTable(),
- 'columns' => [$revision_key => $revision_key],
- ]
- ],
- ];
- $this->addTableDefaults($schema);
- return $schema;
- }
- /**
- * Adds defaults to a table schema definition.
- *
- * @param $schema
- * The schema definition array for a single table, passed by reference.
- */
- protected function addTableDefaults(&$schema) {
- $schema += [
- 'fields' => [],
- 'unique keys' => [],
- 'indexes' => [],
- 'foreign keys' => [],
- ];
- }
- /**
- * Processes the gathered schema for a base table.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * 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'));
- }
- /**
- * Processes the gathered schema for a base table.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * 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'));
- }
- /**
- * Processes the gathered schema for a base table.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type.
- * @param array $schema
- * The table schema, passed by reference.
- *
- * @return array
- * A partial schema array for the base table.
- */
- protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
- // Marking the respective fields as NOT NULL makes the indexes more
- // performant.
- $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
- }
- /**
- * Processes the gathered schema for a base table.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type.
- * @param array $schema
- * The table schema, passed by reference.
- *
- * @return array
- * A partial schema array for the base table.
- */
- protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
- // Marking the respective fields as NOT NULL makes the indexes more
- // performant.
- $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
- }
- /**
- * Processes the specified entity key.
- *
- * @param array $schema
- * The table schema, passed by reference.
- * @param string $key
- * The entity key name.
- */
- protected function processIdentifierSchema(&$schema, $key) {
- if ($schema['fields'][$key]['type'] == 'int') {
- $schema['fields'][$key]['type'] = 'serial';
- }
- $schema['fields'][$key]['not null'] = TRUE;
- 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.
- *
- * This figures out whether the field is stored in a dedicated or shared table
- * and forwards the call to the proper handler.
- *
- * @param string $operation
- * The name of the operation to be performed.
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field storage definition.
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
- * (optional) The original field storage definition. This is relevant (and
- * required) only for updates. Defaults to NULL.
- */
- protected function performFieldSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) {
- $table_mapping = $this->storage->getTableMapping();
- if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
- $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original);
- }
- elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
- $this->{$operation . 'SharedTableSchema'}($storage_definition, $original);
- }
- }
- /**
- * Creates the schema for a field stored in a dedicated table.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field being created.
- */
- protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
- $schema = $this->getDedicatedTableSchema($storage_definition);
- foreach ($schema as $name => $table) {
- // Check if the table exists because it might already have been
- // created as part of the earlier entity type update event.
- if (!$this->database->schema()->tableExists($name)) {
- $this->database->schema()->createTable($name, $table);
- }
- }
- $this->saveFieldSchemaData($storage_definition, $schema);
- }
- /**
- * Creates the schema for a field stored in a shared table.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field being created.
- * @param bool $only_save
- * (optional) Whether to skip modification of database tables and only save
- * the schema data for future comparison. For internal use only. This is
- * used by onEntityTypeCreate() after it has already fully created the
- * shared tables.
- */
- protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) {
- $created_field_name = $storage_definition->getName();
- $table_mapping = $this->storage->getTableMapping();
- $column_names = $table_mapping->getColumnNames($created_field_name);
- $schema_handler = $this->database->schema();
- $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
- // Iterate over the mapped table to find the ones that will host the created
- // field schema.
- $schema = [];
- foreach ($shared_table_names as $table_name) {
- foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
- if ($field_name == $created_field_name) {
- // Create field columns.
- $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
- if (!$only_save) {
- foreach ($schema[$table_name]['fields'] as $name => $specifier) {
- // 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);
- }
- }
- if (!empty($schema[$table_name]['indexes'])) {
- foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
- // Check if the index exists because it might already have been
- // created as part of the earlier entity type update event.
- $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
- }
- }
- if (!empty($schema[$table_name]['unique keys'])) {
- foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
- $schema_handler->addUniqueKey($table_name, $name, $specifier);
- }
- }
- }
- // After creating the field schema skip to the next table.
- break;
- }
- }
- }
- $this->saveFieldSchemaData($storage_definition, $schema);
- if (!$only_save) {
- // Make sure any entity index involving this field is re-created if
- // needed.
- $this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
- }
- }
- /**
- * Deletes the schema for a field stored in a dedicated table.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field being deleted.
- */
- protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
- $table_mapping = $this->storage->getTableMapping();
- $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_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);
- }
- /**
- * Deletes the schema for a field stored in a shared table.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field being deleted.
- */
- protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
- // Make sure any entity index involving this field is dropped.
- $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
- $deleted_field_name = $storage_definition->getName();
- $table_mapping = $this->storage->getTableMapping(
- $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
- );
- $column_names = $table_mapping->getColumnNames($deleted_field_name);
- $schema_handler = $this->database->schema();
- $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
- // Iterate over the mapped table to find the ones that host the deleted
- // field schema.
- foreach ($shared_table_names as $table_name) {
- foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
- if ($field_name == $deleted_field_name) {
- $schema = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
- // Drop indexes and unique keys first.
- if (!empty($schema['indexes'])) {
- foreach ($schema['indexes'] as $name => $specifier) {
- $schema_handler->dropIndex($table_name, $name);
- }
- }
- if (!empty($schema['unique keys'])) {
- foreach ($schema['unique keys'] as $name => $specifier) {
- $schema_handler->dropUniqueKey($table_name, $name);
- }
- }
- // Drop columns.
- foreach ($column_names as $column_name) {
- $schema_handler->dropField($table_name, $column_name);
- }
- // After deleting the field schema skip to the next table.
- break;
- }
- }
- }
- $this->deleteFieldSchemaData($storage_definition);
- }
- /**
- * Updates the schema for a field stored in a shared table.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field being updated.
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
- * The original storage definition; i.e., the definition before the update.
- *
- * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
- * Thrown when the update to the field is forbidden.
- * @throws \Exception
- * Rethrown exception if the table recreation fails.
- */
- protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- if (!$this->storage->countFieldData($original, TRUE)) {
- // There is no data. Re-create the tables completely.
- 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 {
- // Since there is no data we may be switching from a shared table schema
- // to a dedicated table schema, hence we should use the proper API.
- $this->performFieldSchemaOperation('delete', $original);
- $this->performFieldSchemaOperation('create', $storage_definition);
- }
- catch (\Exception $e) {
- if ($this->database->supportsTransactionalDDL()) {
- $transaction->rollBack();
- }
- else {
- // Recreate tables.
- $this->performFieldSchemaOperation('create', $original);
- }
- throw $e;
- }
- }
- else {
- if ($this->hasColumnChanges($storage_definition, $original)) {
- throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
- }
- // There is data, so there are no column changes. Drop all the prior
- // indexes and create all the new ones, except for all the priors that
- // exist unchanged.
- $table_mapping = $this->storage->getTableMapping();
- $table = $table_mapping->getDedicatedDataTableName($original);
- $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
- // Get the field schemas.
- $schema = $storage_definition->getSchema();
- $original_schema = $original->getSchema();
- // Gets the SQL schema for a dedicated tables.
- $actual_schema = $this->getDedicatedTableSchema($storage_definition);
- foreach ($original_schema['indexes'] as $name => $columns) {
- if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
- $real_name = $this->getFieldIndexName($storage_definition, $name);
- $this->database->schema()->dropIndex($table, $real_name);
- $this->database->schema()->dropIndex($revision_table, $real_name);
- }
- }
- $table = $table_mapping->getDedicatedDataTableName($storage_definition);
- $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
- foreach ($schema['indexes'] as $name => $columns) {
- if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
- $real_name = $this->getFieldIndexName($storage_definition, $name);
- $real_columns = [];
- foreach ($columns as $column_name) {
- // Indexes can be specified as either a column name or an array with
- // column name and length. Allow for either case.
- if (is_array($column_name)) {
- $real_columns[] = [
- $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
- $column_name[1],
- ];
- }
- else {
- $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
- }
- }
- // Check if the index exists because it might already have been
- // created as part of the earlier entity type update event.
- $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
- $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
- }
- }
- $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
- }
- }
- /**
- * Updates the schema for a field stored in a shared table.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field being updated.
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
- * The original storage definition; i.e., the definition before the update.
- *
- * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
- * Thrown when the update to the field is forbidden.
- * @throws \Exception
- * Rethrown exception if the table recreation fails.
- */
- protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- if (!$this->storage->countFieldData($original, TRUE)) {
- 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 {
- // Since there is no data we may be switching from a dedicated table
- // to a schema table schema, hence we should use the proper API.
- $this->performFieldSchemaOperation('delete', $original);
- $this->performFieldSchemaOperation('create', $storage_definition);
- }
- catch (\Exception $e) {
- if ($this->database->supportsTransactionalDDL()) {
- $transaction->rollBack();
- }
- else {
- // Recreate original schema.
- $this->createSharedTableSchema($original);
- }
- throw $e;
- }
- }
- else {
- if ($this->hasColumnChanges($storage_definition, $original)) {
- throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
- }
- $updated_field_name = $storage_definition->getName();
- $table_mapping = $this->storage->getTableMapping();
- $column_names = $table_mapping->getColumnNames($updated_field_name);
- $schema_handler = $this->database->schema();
- // Iterate over the mapped table to find the ones that host the deleted
- // field schema.
- $original_schema = $this->loadFieldSchemaData($original);
- $schema = [];
- foreach ($table_mapping->getTableNames() as $table_name) {
- foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
- if ($field_name == $updated_field_name) {
- $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
- // Handle NOT NULL constraints.
- foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
- $not_null = !empty($specifier['not null']);
- $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
- if ($not_null !== $original_not_null) {
- if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
- throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
- }
- $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);
- }
- }
- // Drop original indexes and unique keys.
- if (!empty($original_schema[$table_name]['indexes'])) {
- foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
- $schema_handler->dropIndex($table_name, $name);
- }
- }
- if (!empty($original_schema[$table_name]['unique keys'])) {
- foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) {
- $schema_handler->dropUniqueKey($table_name, $name);
- }
- }
- // Create new indexes and unique keys.
- if (!empty($schema[$table_name]['indexes'])) {
- foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
- // Check if the index exists because it might already have been
- // created as part of the earlier entity type update event.
- $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
- }
- }
- if (!empty($schema[$table_name]['unique keys'])) {
- foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
- $schema_handler->addUniqueKey($table_name, $name, $specifier);
- }
- }
- // After deleting the field schema skip to the next table.
- break;
- }
- }
- }
- $this->saveFieldSchemaData($storage_definition, $schema);
- }
- }
- /**
- * Creates the specified entity schema indexes and keys.
- *
- * @param array $entity_schema
- * The entity schema definition.
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
- * (optional) If a field storage definition is specified, only indexes and
- * keys involving its columns will be processed. Otherwise all defined
- * entity indexes and keys will be processed.
- */
- protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
- $schema_handler = $this->database->schema();
- if ($storage_definition) {
- $table_mapping = $this->storage->getTableMapping();
- $column_names = $table_mapping->getColumnNames($storage_definition->getName());
- }
- $index_keys = [
- 'indexes' => 'addIndex',
- 'unique keys' => 'addUniqueKey',
- ];
- foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
- // Add fields schema because database driver may depend on this data to
- // perform index normalization.
- $schema['fields'] = $entity_schema[$table_name]['fields'];
- foreach ($index_keys as $key => $add_method) {
- if (!empty($schema[$key])) {
- foreach ($schema[$key] as $name => $specifier) {
- // If a set of field columns were specified we process only indexes
- // 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);
- if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
- $create = TRUE;
- foreach ($specifier_columns as $specifier_column_name) {
- // This may happen when adding more than one field in the same
- // update run. Eventually when all field columns have been
- // created the index will be created too.
- if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
- $create = FALSE;
- break;
- }
- }
- }
- if ($create) {
- $this->{$add_method}($table_name, $name, $specifier, $schema);
- }
- }
- }
- }
- }
- }
- /**
- * Deletes the specified entity schema indexes and keys.
- *
- * @param array $entity_schema_data
- * The entity schema data definition.
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
- * (optional) If a field storage definition is specified, only indexes and
- * keys involving its columns will be processed. Otherwise all defined
- * entity indexes and keys will be processed.
- */
- protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
- $schema_handler = $this->database->schema();
- if ($storage_definition) {
- $table_mapping = $this->storage->getTableMapping();
- $column_names = $table_mapping->getColumnNames($storage_definition->getName());
- }
- $index_keys = [
- 'indexes' => 'dropIndex',
- 'unique keys' => 'dropUniqueKey',
- ];
- foreach ($entity_schema_data as $table_name => $schema) {
- 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);
- if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
- $schema_handler->{$drop_method}($table_name, $name);
- }
- }
- }
- }
- }
- }
- /**
- * Checks whether a field property has NULL values.
- *
- * @param string $table_name
- * The name of the table to inspect.
- * @param string $column_name
- * The name of the column holding the field property data.
- *
- * @return bool
- * TRUE if NULL data is found, FALSE otherwise.
- */
- protected function hasNullFieldPropertyData($table_name, $column_name) {
- $query = $this->database->select($table_name, 't')
- ->fields('t', [$column_name])
- ->range(0, 1);
- $query->isNull('t.' . $column_name);
- $result = $query->execute()->fetchAssoc();
- return (bool) $result;
- }
- /**
- * Gets the schema for a single field definition.
- *
- * Entity types may override this method in order to optimize the generated
- * schema for given field. While all optimizations that apply to a single
- * field have to be added here, all cross-field optimizations should be via
- * SqlContentEntityStorageSchema::getEntitySchema() instead; e.g.,
- * an index spanning multiple fields.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field whose schema has to be returned.
- * @param string $table_name
- * The name of the table columns will be added to.
- * @param string[] $column_mapping
- * A mapping of field column names to database column names.
- *
- * @return array
- * The schema definition for the table with the following keys:
- * - fields: The schema definition for the each field columns.
- * - indexes: The schema definition for the indexes.
- * - unique keys: The schema definition for the unique keys.
- * - foreign keys: The schema definition for the foreign keys.
- *
- * @throws \Drupal\Core\Field\FieldException
- * 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']), $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. 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 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
- // table is updated after the data has been inserted into the revision
- // table. For this reason the revision ID field cannot be marked as NOT
- // NULL.
- if ($table_name == $base_table) {
- unset($not_null_keys['revision']);
- }
- foreach ($column_mapping as $field_column_name => $schema_field_name) {
- $column_schema = $field_schema['columns'][$field_column_name];
- $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]);
- }
- elseif (!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'])) {
- $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
- }
- if (!empty($field_schema['unique keys'])) {
- $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
- }
- if (!empty($field_schema['foreign keys'])) {
- $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
- }
- return $schema;
- }
- /**
- * Adds an index for the specified field to the given schema definition.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field for which an index should be added.
- * @param array $schema
- * A reference to the schema array to be updated.
- * @param bool $not_null
- * (optional) Whether to also add a 'not null' constraint to the column
- * being indexed. Doing so improves index performance. Defaults to FALSE,
- * in case the field needs to support NULL values.
- * @param int $size
- * (optional) The index size. Defaults to no limit.
- */
- protected function addSharedTableFieldIndex(FieldStorageDefinitionInterface $storage_definition, &$schema, $not_null = FALSE, $size = NULL) {
- $name = $storage_definition->getName();
- $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
- $schema['indexes'][$real_key] = [$size ? [$name, $size] : $name];
- if ($not_null) {
- $schema['fields'][$name]['not null'] = TRUE;
- }
- }
- /**
- * Adds a unique key for the specified field to the given schema definition.
- *
- * Also adds a 'not null' constraint, because many databases do not reliably
- * support unique keys on null columns.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field to which to add a unique key.
- * @param array $schema
- * A reference to the schema array to be updated.
- */
- protected function addSharedTableFieldUniqueKey(FieldStorageDefinitionInterface $storage_definition, &$schema) {
- $name = $storage_definition->getName();
- $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
- $schema['unique keys'][$real_key] = [$name];
- $schema['fields'][$name]['not null'] = TRUE;
- }
- /**
- * Adds a foreign key for the specified field to the given schema definition.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The storage definition of the field to which to add a foreign key.
- * @param array $schema
- * A reference to the schema array to be updated.
- * @param string $foreign_table
- * The foreign table.
- * @param string $foreign_column
- * The foreign column.
- */
- protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface $storage_definition, &$schema, $foreign_table, $foreign_column) {
- $name = $storage_definition->getName();
- $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
- $schema['foreign keys'][$real_key] = [
- 'table' => $foreign_table,
- 'columns' => [$name => $foreign_column],
- ];
- }
- /**
- * Gets the SQL schema for a dedicated table.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field storage definition.
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * (optional) The entity type definition. Defaults to the one returned by
- * the entity manager.
- *
- * @return array
- * The schema definition for the table with the following keys:
- * - fields: The schema definition for the each field columns.
- * - indexes: The schema definition for the indexes.
- * - unique keys: The schema definition for the unique keys.
- * - foreign keys: The schema definition for the foreign keys.
- *
- * @throws \Drupal\Core\Field\FieldException
- * Exception thrown if the schema contains reserved column names.
- *
- * @see hook_schema()
- */
- protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
- $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
- $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
- $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')];
- if ($id_definition->getType() == 'integer') {
- $id_schema = [
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'description' => 'The entity id this data is attached to',
- ];
- }
- else {
- $id_schema = [
- 'type' => 'varchar_ascii',
- 'length' => 128,
- 'not null' => TRUE,
- 'description' => 'The entity id this data is attached to',
- ];
- }
- // Define the revision ID schema.
- if (!$this->entityType->isRevisionable()) {
- $revision_id_schema = $id_schema;
- $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';
- }
- elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') {
- $revision_id_schema = [
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'description' => 'The entity revision id this data is attached to',
- ];
- }
- else {
- $revision_id_schema = [
- 'type' => 'varchar',
- 'length' => 128,
- 'not null' => TRUE,
- 'description' => 'The entity revision id this data is attached to',
- ];
- }
- $data_schema = [
- 'description' => $description_current,
- 'fields' => [
- 'bundle' => [
- 'type' => 'varchar_ascii',
- 'length' => 128,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
- ],
- 'deleted' => [
- 'type' => 'int',
- 'size' => 'tiny',
- 'not null' => TRUE,
- 'default' => 0,
- 'description' => 'A boolean indicating whether this data item has been deleted'
- ],
- 'entity_id' => $id_schema,
- 'revision_id' => $revision_id_schema,
- 'langcode' => [
- 'type' => 'varchar_ascii',
- 'length' => 32,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'The language code for this data item.',
- ],
- 'delta' => [
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'description' => 'The sequence number for this data item, used for multi-value fields',
- ],
- ],
- 'primary key' => ['entity_id', 'deleted', 'delta', 'langcode'],
- 'indexes' => [
- 'bundle' => ['bundle'],
- 'revision_id' => ['revision_id'],
- ],
- ];
- // Check that the schema does not include forbidden column names.
- $schema = $storage_definition->getSchema();
- $properties = $storage_definition->getPropertyDefinitions();
- $table_mapping = $this->storage->getTableMapping();
- if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
- throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
- }
- // Add field columns.
- foreach ($schema['columns'] as $column_name => $attributes) {
- $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
- $data_schema['fields'][$real_name] = $attributes;
- // A dedicated table only contain rows for actual field values, and no
- // rows for entities where the field is empty. Thus, we can safely
- // enforce 'not null' on the columns for the field's required properties.
- $data_schema['fields'][$real_name]['not null'] = $properties[$column_name]->isRequired();
- }
- // Add indexes.
- foreach ($schema['indexes'] as $index_name => $columns) {
- $real_name = $this->getFieldIndexName($storage_definition, $index_name);
- foreach ($columns as $column_name) {
- // Indexes can be specified as either a column name or an array with
- // column name and length. Allow for either case.
- if (is_array($column_name)) {
- $data_schema['indexes'][$real_name][] = [
- $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
- $column_name[1],
- ];
- }
- else {
- $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
- }
- }
- }
- // Add unique keys.
- foreach ($schema['unique keys'] as $index_name => $columns) {
- $real_name = $this->getFieldIndexName($storage_definition, $index_name);
- foreach ($columns as $column_name) {
- // Unique keys can be specified as either a column name or an array with
- // column name and length. Allow for either case.
- if (is_array($column_name)) {
- $data_schema['unique keys'][$real_name][] = [
- $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
- $column_name[1],
- ];
- }
- else {
- $data_schema['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
- }
- }
- }
- // Add foreign keys.
- foreach ($schema['foreign keys'] as $specifier => $specification) {
- $real_name = $this->getFieldIndexName($storage_definition, $specifier);
- $data_schema['foreign keys'][$real_name]['table'] = $specification['table'];
- foreach ($specification['columns'] as $column_name => $referenced) {
- $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name);
- $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
- }
- }
- $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema];
- // If the entity type is revisionable, construct the revision table.
- $entity_type = $entity_type ?: $this->entityType;
- if ($entity_type->isRevisionable()) {
- $revision_schema = $data_schema;
- $revision_schema['description'] = $description_revision;
- $revision_schema['primary key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode'];
- $revision_schema['fields']['revision_id']['not null'] = TRUE;
- $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
- $dedicated_table_schema += [$table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema];
- }
- return $dedicated_table_schema;
- }
- /**
- * Gets the name to be used for the given entity index.
- *
- * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
- * The entity type.
- * @param string $index
- * The index column name.
- *
- * @return string
- * The index name.
- */
- protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
- return $entity_type->id() . '__' . $index;
- }
- /**
- * Generates an index name for a field data table.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * The field storage definition.
- * @param string $index
- * The name of the index.
- *
- * @return string
- * A string containing a generated index name for a field data table that is
- * unique among all other fields.
- */
- protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
- return $storage_definition->getName() . '_' . $index;
- }
- /**
- * Checks whether a database table is non-existent or empty.
- *
- * Empty tables can be dropped and recreated without data loss.
- *
- * @param string $table_name
- * The database table to check.
- *
- * @return bool
- * TRUE if the table is empty, FALSE otherwise.
- */
- protected function isTableEmpty($table_name) {
- return !$this->database->schema()->tableExists($table_name) ||
- !$this->database->select($table_name)
- ->countQuery()
- ->range(0, 1)
- ->execute()
- ->fetchField();
- }
- /**
- * Compares schemas to check for changes in the column definitions.
- *
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
- * Current field storage definition.
- * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
- * The original field storage definition.
- *
- * @return bool
- * Returns TRUE if there are schema changes in the column definitions.
- */
- protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
- if ($storage_definition->getColumns() != $original->getColumns()) {
- // Base field definitions have schema data stored in the original
- // definition.
- return TRUE;
- }
- if (!$storage_definition->hasCustomStorage()) {
- $keys = array_flip($this->getColumnSchemaRelevantKeys());
- $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
- foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
- foreach ($table_schema['fields'] as $name => $spec) {
- $definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
- $stored_spec = array_intersect_key($spec, $keys);
- if ($definition_spec != $stored_spec) {
- return TRUE;
- }
- }
- }
- }
- return FALSE;
- }
- /**
- * Returns a list of column schema keys affecting data storage.
- *
- * When comparing schema definitions, only changes in certain properties
- * actually affect how data is stored and thus, if applied, may imply data
- * manipulation.
- *
- * @return string[]
- * An array of key names.
- */
- protected function getColumnSchemaRelevantKeys() {
- return ['type', 'size', 'length', 'unsigned'];
- }
- /**
- * Creates an index, dropping it if already existing.
- *
- * @param string $table
- * The table name.
- * @param string $name
- * The index name.
- * @param array $specifier
- * The fields to index.
- * @param array $schema
- * The table specification.
- *
- * @see \Drupal\Core\Database\Schema::addIndex()
- */
- protected function addIndex($table, $name, array $specifier, array $schema) {
- $schema_handler = $this->database->schema();
- $schema_handler->dropIndex($table, $name);
- $schema_handler->addIndex($table, $name, $specifier, $schema);
- }
- /**
- * Creates a unique key, dropping it if already existing.
- *
- * @param string $table
- * The table name.
- * @param string $name
- * The index name.
- * @param array $specifier
- * The unique fields.
- *
- * @see \Drupal\Core\Database\Schema::addUniqueKey()
- */
- protected function addUniqueKey($table, $name, array $specifier) {
- $schema_handler = $this->database->schema();
- $schema_handler->dropUniqueKey($table, $name);
- $schema_handler->addUniqueKey($table, $name, $specifier);
- }
- }
|