12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276 |
- <?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();
- $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
- $table_mapping = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions);
- // Delete entity and field tables.
- foreach ($table_mapping->getTableNames() as $table_name) {
- if ($schema_handler->tableExists($table_name)) {
- $schema_handler->dropTable($table_name);
- }
- }
- // Delete the field schema data.
- foreach ($field_storage_definitions as $field_storage_definition) {
- $this->deleteFieldSchemaData($field_storage_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) {
- // 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->getCustomTableMapping($entity_type, $this->fieldStorageDefinitions);
- $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;
- }
- 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.
- */
- protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
- // Process the schema for the 'id' entity key only if it exists.
- if ($entity_type->hasKey('id')) {
- $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.
- */
- protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
- // Process the schema for the 'revision' entity key only if it exists.
- if ($entity_type->hasKey('revision')) {
- $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) {
- // The entity schema needs to be checked because the field schema is
- // potentially incomplete.
- // @todo Fix this in https://www.drupal.org/node/2929120.
- $entity_schema = $this->getEntitySchema($this->entityType);
- foreach ($schema[$table_name]['fields'] as $name => $specifier) {
- // Check if the field is part of the primary keys and pass along
- // this information when adding the field.
- // @see \Drupal\Core\Database\Schema::addField()
- $new_keys = [];
- if (isset($entity_schema[$table_name]['primary key']) && array_intersect($column_names, $entity_schema[$table_name]['primary key'])) {
- $new_keys = ['primary key' => $entity_schema[$table_name]['primary key']];
- }
- // Check if the field exists because it might already have been
- // created as part of the earlier entity type update event.
- if (!$schema_handler->fieldExists($table_name, $name)) {
- $schema_handler->addField($table_name, $name, $specifier, $new_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 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, $column_name, $column_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]);
- }
- if (!empty($initial_value_from_field)) {
- $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name];
- }
- }
- if (!empty($field_schema['indexes'])) {
- $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);
- }
- }
|