SqlContentEntityStorageSchema.php 91 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276
  1. <?php
  2. namespace Drupal\Core\Entity\Sql;
  3. use Drupal\Core\Database\Connection;
  4. use Drupal\Core\Database\DatabaseExceptionWrapper;
  5. use Drupal\Core\DependencyInjection\DependencySerializationTrait;
  6. use Drupal\Core\Entity\ContentEntityTypeInterface;
  7. use Drupal\Core\Entity\EntityManagerInterface;
  8. use Drupal\Core\Entity\EntityPublishedInterface;
  9. use Drupal\Core\Entity\EntityStorageException;
  10. use Drupal\Core\Entity\EntityTypeInterface;
  11. use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
  12. use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
  13. use Drupal\Core\Field\BaseFieldDefinition;
  14. use Drupal\Core\Field\FieldException;
  15. use Drupal\Core\Field\FieldStorageDefinitionInterface;
  16. use Drupal\Core\Language\LanguageInterface;
  17. /**
  18. * Defines a schema handler that supports revisionable, translatable entities.
  19. *
  20. * Entity types may extend this class and optimize the generated schema for all
  21. * entity base tables by overriding getEntitySchema() for cross-field
  22. * optimizations and getSharedTableFieldSchema() for optimizations applying to
  23. * a single field.
  24. */
  25. class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorageSchemaInterface {
  26. use DependencySerializationTrait;
  27. /**
  28. * The entity manager.
  29. *
  30. * @var \Drupal\Core\Entity\EntityManagerInterface
  31. */
  32. protected $entityManager;
  33. /**
  34. * The entity type this schema builder is responsible for.
  35. *
  36. * @var \Drupal\Core\Entity\ContentEntityTypeInterface
  37. */
  38. protected $entityType;
  39. /**
  40. * The storage field definitions for this entity type.
  41. *
  42. * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
  43. */
  44. protected $fieldStorageDefinitions;
  45. /**
  46. * The original storage field definitions for this entity type. Used during
  47. * field schema updates.
  48. *
  49. * @var \Drupal\Core\Field\FieldDefinitionInterface[]
  50. */
  51. protected $originalDefinitions;
  52. /**
  53. * The storage object for the given entity type.
  54. *
  55. * @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage
  56. */
  57. protected $storage;
  58. /**
  59. * A static cache of the generated schema array.
  60. *
  61. * @var array
  62. */
  63. protected $schema;
  64. /**
  65. * The database connection to be used.
  66. *
  67. * @var \Drupal\Core\Database\Connection
  68. */
  69. protected $database;
  70. /**
  71. * The key-value collection for tracking installed storage schema.
  72. *
  73. * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
  74. */
  75. protected $installedStorageSchema;
  76. /**
  77. * The deleted fields repository.
  78. *
  79. * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
  80. */
  81. protected $deletedFieldsRepository;
  82. /**
  83. * Constructs a SqlContentEntityStorageSchema.
  84. *
  85. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
  86. * The entity manager.
  87. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  88. * The entity type.
  89. * @param \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage
  90. * The storage of the entity type. This must be an SQL-based storage.
  91. * @param \Drupal\Core\Database\Connection $database
  92. * The database connection to be used.
  93. */
  94. public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, SqlContentEntityStorage $storage, Connection $database) {
  95. $this->entityManager = $entity_manager;
  96. $this->entityType = $entity_type;
  97. $this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
  98. $this->storage = $storage;
  99. $this->database = $database;
  100. }
  101. /**
  102. * Gets the keyvalue collection for tracking the installed schema.
  103. *
  104. * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
  105. *
  106. * @todo Inject this dependency in the constructor once this class can be
  107. * instantiated as a regular entity handler:
  108. * https://www.drupal.org/node/2332857.
  109. */
  110. protected function installedStorageSchema() {
  111. if (!isset($this->installedStorageSchema)) {
  112. $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
  113. }
  114. return $this->installedStorageSchema;
  115. }
  116. /**
  117. * Gets the deleted fields repository.
  118. *
  119. * @return \Drupal\Core\Field\DeletedFieldsRepositoryInterface
  120. * The deleted fields repository.
  121. *
  122. * @todo Inject this dependency in the constructor once this class can be
  123. * instantiated as a regular entity handler:
  124. * https://www.drupal.org/node/2332857.
  125. */
  126. protected function deletedFieldsRepository() {
  127. if (!isset($this->deletedFieldsRepository)) {
  128. $this->deletedFieldsRepository = \Drupal::service('entity_field.deleted_fields_repository');
  129. }
  130. return $this->deletedFieldsRepository;
  131. }
  132. /**
  133. * {@inheritdoc}
  134. */
  135. public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
  136. return
  137. $this->hasSharedTableStructureChange($entity_type, $original) ||
  138. // Detect changes in key or index definitions.
  139. $this->getEntitySchemaData($entity_type, $this->getEntitySchema($entity_type, TRUE)) != $this->loadEntitySchemaData($original);
  140. }
  141. /**
  142. * Detects whether there is a change in the shared table structure.
  143. *
  144. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  145. * The new entity type.
  146. * @param \Drupal\Core\Entity\EntityTypeInterface $original
  147. * The origin entity type.
  148. *
  149. * @return bool
  150. * Returns TRUE if either the revisionable or translatable flag changes or
  151. * a table has been renamed.
  152. */
  153. protected function hasSharedTableStructureChange(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
  154. return
  155. $entity_type->isRevisionable() != $original->isRevisionable() ||
  156. $entity_type->isTranslatable() != $original->isTranslatable() ||
  157. $this->hasSharedTableNameChanges($entity_type, $original);
  158. }
  159. /**
  160. * Detects whether any table name got renamed in an entity type update.
  161. *
  162. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  163. * The new entity type.
  164. * @param \Drupal\Core\Entity\EntityTypeInterface $original
  165. * The origin entity type.
  166. *
  167. * @return bool
  168. * Returns TRUE if there have been changes.
  169. */
  170. protected function hasSharedTableNameChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
  171. $base_table = $this->database->schema()->tableExists($entity_type->getBaseTable());
  172. $data_table = $this->database->schema()->tableExists($entity_type->getDataTable());
  173. $revision_table = $this->database->schema()->tableExists($entity_type->getRevisionTable());
  174. $revision_data_table = $this->database->schema()->tableExists($entity_type->getRevisionDataTable());
  175. // We first check if the new table already exists because the storage might
  176. // have created it even though it wasn't specified in the entity type
  177. // definition.
  178. return
  179. (!$base_table && $entity_type->getBaseTable() != $original->getBaseTable()) ||
  180. (!$data_table && $entity_type->getDataTable() != $original->getDataTable()) ||
  181. (!$revision_table && $entity_type->getRevisionTable() != $original->getRevisionTable()) ||
  182. (!$revision_data_table && $entity_type->getRevisionDataTable() != $original->getRevisionDataTable());
  183. }
  184. /**
  185. * {@inheritdoc}
  186. */
  187. public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
  188. $table_mapping = $this->storage->getTableMapping();
  189. if (
  190. $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
  191. $storage_definition->getSchema() != $original->getSchema() ||
  192. $storage_definition->isRevisionable() != $original->isRevisionable() ||
  193. $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) ||
  194. $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original)
  195. ) {
  196. return TRUE;
  197. }
  198. if ($storage_definition->hasCustomStorage()) {
  199. // The field has custom storage, so we don't know if a schema change is
  200. // needed or not, but since per the initial checks earlier in this
  201. // function, nothing about the definition changed that we manage, we
  202. // return FALSE.
  203. return FALSE;
  204. }
  205. $current_schema = $this->getSchemaFromStorageDefinition($storage_definition);
  206. $this->processFieldStorageSchema($current_schema);
  207. $installed_schema = $this->loadFieldSchemaData($original);
  208. $this->processFieldStorageSchema($installed_schema);
  209. return $current_schema != $installed_schema;
  210. }
  211. /**
  212. * Gets the schema data for the given field storage definition.
  213. *
  214. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  215. * The field storage definition. The field that must not have custom
  216. * storage, i.e. the storage must take care of storing the field.
  217. *
  218. * @return array
  219. * The schema data.
  220. */
  221. protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
  222. assert(!$storage_definition->hasCustomStorage());
  223. $table_mapping = $this->storage->getTableMapping();
  224. $schema = [];
  225. if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
  226. $schema = $this->getDedicatedTableSchema($storage_definition);
  227. }
  228. elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
  229. $field_name = $storage_definition->getName();
  230. foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
  231. if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
  232. $column_names = $table_mapping->getColumnNames($storage_definition->getName());
  233. $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
  234. }
  235. }
  236. }
  237. return $schema;
  238. }
  239. /**
  240. * {@inheritdoc}
  241. */
  242. public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
  243. // Check if the entity type specifies that data migration is being handled
  244. // elsewhere.
  245. if ($entity_type->get('requires_data_migration') === FALSE) {
  246. return FALSE;
  247. }
  248. // If the original storage has existing entities, or it is impossible to
  249. // determine if that is the case, require entity data to be migrated.
  250. $original_storage_class = $original->getStorageClass();
  251. if (!class_exists($original_storage_class)) {
  252. return TRUE;
  253. }
  254. // Data migration is not needed when only indexes changed, as they can be
  255. // applied if there is data.
  256. if (!$this->hasSharedTableStructureChange($entity_type, $original)) {
  257. return FALSE;
  258. }
  259. // Use the original entity type since the storage has not been updated.
  260. $original_storage = $this->entityManager->createHandlerInstance($original_storage_class, $original);
  261. return $original_storage->hasData();
  262. }
  263. /**
  264. * {@inheritdoc}
  265. */
  266. public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
  267. return !$this->storage->countFieldData($original, TRUE);
  268. }
  269. /**
  270. * {@inheritdoc}
  271. */
  272. public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
  273. $this->checkEntityType($entity_type);
  274. $schema_handler = $this->database->schema();
  275. // Create entity tables.
  276. $schema = $this->getEntitySchema($entity_type, TRUE);
  277. foreach ($schema as $table_name => $table_schema) {
  278. if (!$schema_handler->tableExists($table_name)) {
  279. $schema_handler->createTable($table_name, $table_schema);
  280. }
  281. }
  282. // Create dedicated field tables.
  283. $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
  284. foreach ($this->fieldStorageDefinitions as $field_storage_definition) {
  285. if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
  286. $this->createDedicatedTableSchema($field_storage_definition);
  287. }
  288. elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
  289. // The shared tables are already fully created, but we need to save the
  290. // per-field schema definitions for later use.
  291. $this->createSharedTableSchema($field_storage_definition, TRUE);
  292. }
  293. }
  294. // Save data about entity indexes and keys.
  295. $this->saveEntitySchemaData($entity_type, $schema);
  296. }
  297. /**
  298. * {@inheritdoc}
  299. */
  300. public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
  301. $this->checkEntityType($entity_type);
  302. $this->checkEntityType($original);
  303. // If no schema changes are needed, we don't need to do anything.
  304. if (!$this->requiresEntityStorageSchemaChanges($entity_type, $original)) {
  305. return;
  306. }
  307. // If a migration is required, we can't proceed.
  308. if ($this->requiresEntityDataMigration($entity_type, $original)) {
  309. throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type->id() . ') with data.');
  310. }
  311. // If we have no data just recreate the entity schema from scratch.
  312. if ($this->isTableEmpty($this->storage->getBaseTable())) {
  313. if ($this->database->supportsTransactionalDDL()) {
  314. // If the database supports transactional DDL, we can go ahead and rely
  315. // on it. If not, we will have to rollback manually if something fails.
  316. $transaction = $this->database->startTransaction();
  317. }
  318. try {
  319. $this->onEntityTypeDelete($original);
  320. $this->onEntityTypeCreate($entity_type);
  321. }
  322. catch (\Exception $e) {
  323. if ($this->database->supportsTransactionalDDL()) {
  324. $transaction->rollBack();
  325. }
  326. else {
  327. // Recreate original schema.
  328. $this->onEntityTypeCreate($original);
  329. }
  330. throw $e;
  331. }
  332. }
  333. else {
  334. // Drop original indexes and unique keys.
  335. $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
  336. // Create new indexes and unique keys.
  337. $entity_schema = $this->getEntitySchema($entity_type, TRUE);
  338. $this->createEntitySchemaIndexes($entity_schema);
  339. // Store the updated entity schema.
  340. $this->saveEntitySchemaData($entity_type, $entity_schema);
  341. }
  342. }
  343. /**
  344. * {@inheritdoc}
  345. */
  346. public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
  347. $this->checkEntityType($entity_type);
  348. $schema_handler = $this->database->schema();
  349. $field_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type->id());
  350. $table_mapping = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions);
  351. // Delete entity and field tables.
  352. foreach ($table_mapping->getTableNames() as $table_name) {
  353. if ($schema_handler->tableExists($table_name)) {
  354. $schema_handler->dropTable($table_name);
  355. }
  356. }
  357. // Delete the field schema data.
  358. foreach ($field_storage_definitions as $field_storage_definition) {
  359. $this->deleteFieldSchemaData($field_storage_definition);
  360. }
  361. // Delete the entity schema.
  362. $this->deleteEntitySchemaData($entity_type);
  363. }
  364. /**
  365. * {@inheritdoc}
  366. */
  367. public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
  368. $this->performFieldSchemaOperation('create', $storage_definition);
  369. }
  370. /**
  371. * {@inheritdoc}
  372. */
  373. public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
  374. // Store original definitions so that switching between shared and dedicated
  375. // field table layout works.
  376. $this->performFieldSchemaOperation('update', $storage_definition, $original);
  377. }
  378. /**
  379. * {@inheritdoc}
  380. */
  381. public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
  382. try {
  383. $has_data = $this->storage->countFieldData($storage_definition, TRUE);
  384. }
  385. catch (DatabaseExceptionWrapper $e) {
  386. // This may happen when changing field storage schema, since we are not
  387. // able to use a table mapping matching the passed storage definition.
  388. // @todo Revisit this once we are able to instantiate the table mapping
  389. // properly. See https://www.drupal.org/node/2274017.
  390. return;
  391. }
  392. // If the field storage does not have any data, we can safely delete its
  393. // schema.
  394. if (!$has_data) {
  395. $this->performFieldSchemaOperation('delete', $storage_definition);
  396. return;
  397. }
  398. // There's nothing else we can do if the field storage has a custom storage.
  399. if ($storage_definition->hasCustomStorage()) {
  400. return;
  401. }
  402. // Retrieve a table mapping which contains the deleted field still.
  403. $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
  404. $table_mapping = $this->storage->getTableMapping($storage_definitions);
  405. $field_table_name = $table_mapping->getFieldTableName($storage_definition->getName());
  406. if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
  407. // Move the table to a unique name while the table contents are being
  408. // deleted.
  409. $table = $table_mapping->getDedicatedDataTableName($storage_definition);
  410. $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
  411. $this->database->schema()->renameTable($table, $new_table);
  412. if ($this->entityType->isRevisionable()) {
  413. $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
  414. $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE);
  415. $this->database->schema()->renameTable($revision_table, $revision_new_table);
  416. }
  417. }
  418. else {
  419. // Move the field data from the shared table to a dedicated one in order
  420. // to allow it to be purged like any other field.
  421. $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName());
  422. // Refresh the table mapping to use the deleted storage definition.
  423. $deleted_storage_definition = $this->deletedFieldsRepository()->getFieldStorageDefinitions()[$storage_definition->getUniqueStorageIdentifier()];
  424. $original_storage_definitions = [$storage_definition->getName() => $deleted_storage_definition] + $storage_definitions;
  425. $table_mapping = $this->storage->getTableMapping($original_storage_definitions);
  426. $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition);
  427. $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName());
  428. $dedicated_table_name = $table_mapping->getDedicatedDataTableName($deleted_storage_definition, TRUE);
  429. $dedicated_table_name_mapping[$table_mapping->getDedicatedDataTableName($deleted_storage_definition)] = $dedicated_table_name;
  430. if ($this->entityType->isRevisionable()) {
  431. $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_storage_definition, TRUE);
  432. $dedicated_table_name_mapping[$table_mapping->getDedicatedRevisionTableName($deleted_storage_definition)] = $dedicated_revision_table_name;
  433. }
  434. // Create the dedicated field tables using "deleted" table names.
  435. foreach ($dedicated_table_field_schema as $name => $table) {
  436. if (!$this->database->schema()->tableExists($dedicated_table_name_mapping[$name])) {
  437. $this->database->schema()->createTable($dedicated_table_name_mapping[$name], $table);
  438. }
  439. else {
  440. throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.');
  441. }
  442. }
  443. if ($this->database->supportsTransactionalDDL()) {
  444. // If the database supports transactional DDL, we can go ahead and rely
  445. // on it. If not, we will have to rollback manually if something fails.
  446. $transaction = $this->database->startTransaction();
  447. }
  448. try {
  449. // Copy the data from the base table.
  450. $this->database->insert($dedicated_table_name)
  451. ->from($this->getSelectQueryForFieldStorageDeletion($field_table_name, $shared_table_field_columns, $dedicated_table_field_columns))
  452. ->execute();
  453. // Copy the data from the revision table.
  454. if (isset($dedicated_revision_table_name)) {
  455. if ($this->entityType->isTranslatable()) {
  456. $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionDataTable() : $this->storage->getDataTable();
  457. }
  458. else {
  459. $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionTable() : $this->storage->getBaseTable();
  460. }
  461. $this->database->insert($dedicated_revision_table_name)
  462. ->from($this->getSelectQueryForFieldStorageDeletion($revision_table, $shared_table_field_columns, $dedicated_table_field_columns, $field_table_name))
  463. ->execute();
  464. }
  465. }
  466. catch (\Exception $e) {
  467. if (isset($transaction)) {
  468. $transaction->rollBack();
  469. }
  470. else {
  471. // Delete the dedicated tables.
  472. foreach ($dedicated_table_field_schema as $name => $table) {
  473. $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]);
  474. }
  475. }
  476. throw $e;
  477. }
  478. // Delete the field from the shared tables.
  479. $this->deleteSharedTableSchema($storage_definition);
  480. }
  481. }
  482. /**
  483. * {@inheritdoc}
  484. */
  485. public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
  486. $this->performFieldSchemaOperation('delete', $storage_definition);
  487. }
  488. /**
  489. * Returns a SELECT query suitable for inserting data into a dedicated table.
  490. *
  491. * @param string $table_name
  492. * The entity table name to select from.
  493. * @param array $shared_table_field_columns
  494. * An array of field column names for a shared table schema.
  495. * @param array $dedicated_table_field_columns
  496. * An array of field column names for a dedicated table schema.
  497. * @param string $base_table
  498. * (optional) The name of the base entity table. Defaults to NULL.
  499. *
  500. * @return \Drupal\Core\Database\Query\SelectInterface
  501. * A database select query.
  502. */
  503. protected function getSelectQueryForFieldStorageDeletion($table_name, array $shared_table_field_columns, array $dedicated_table_field_columns, $base_table = NULL) {
  504. // Create a SELECT query that generates a result suitable for writing into
  505. // a dedicated field table.
  506. $select = $this->database->select($table_name, 'entity_table');
  507. // Add the bundle column.
  508. if ($bundle = $this->entityType->getKey('bundle')) {
  509. if ($base_table) {
  510. $select->join($base_table, 'base_table', "entity_table.{$this->entityType->getKey('id')} = %alias.{$this->entityType->getKey('id')}");
  511. $select->addField('base_table', $bundle, 'bundle');
  512. }
  513. else {
  514. $select->addField('entity_table', $bundle, 'bundle');
  515. }
  516. }
  517. else {
  518. $select->addExpression(':bundle', 'bundle', [':bundle' => $this->entityType->id()]);
  519. }
  520. // Add the deleted column.
  521. $select->addExpression(':deleted', 'deleted', [':deleted' => 1]);
  522. // Add the entity_id column.
  523. $select->addField('entity_table', $this->entityType->getKey('id'), 'entity_id');
  524. // Add the revision_id column.
  525. if ($this->entityType->isRevisionable()) {
  526. $select->addField('entity_table', $this->entityType->getKey('revision'), 'revision_id');
  527. }
  528. else {
  529. $select->addField('entity_table', $this->entityType->getKey('id'), 'revision_id');
  530. }
  531. // Add the langcode column.
  532. if ($langcode = $this->entityType->getKey('langcode')) {
  533. $select->addField('entity_table', $langcode, 'langcode');
  534. }
  535. else {
  536. $select->addExpression(':langcode', 'langcode', [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]);
  537. }
  538. // Add the delta column and set it to 0 because we are only dealing with
  539. // single cardinality fields.
  540. $select->addExpression(':delta', 'delta', [':delta' => 0]);
  541. // Add all the dynamic field columns.
  542. $or = $select->orConditionGroup();
  543. foreach ($shared_table_field_columns as $field_column_name => $schema_column_name) {
  544. $select->addField('entity_table', $schema_column_name, $dedicated_table_field_columns[$field_column_name]);
  545. $or->isNotNull('entity_table.' . $schema_column_name);
  546. }
  547. $select->condition($or);
  548. // Lock the table rows.
  549. $select->forUpdate(TRUE);
  550. return $select;
  551. }
  552. /**
  553. * Checks that we are dealing with the correct entity type.
  554. *
  555. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  556. * The entity type to be checked.
  557. *
  558. * @return bool
  559. * TRUE if the entity type matches the current one.
  560. *
  561. * @throws \Drupal\Core\Entity\EntityStorageException
  562. */
  563. protected function checkEntityType(EntityTypeInterface $entity_type) {
  564. if ($entity_type->id() != $this->entityType->id()) {
  565. throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
  566. }
  567. return TRUE;
  568. }
  569. /**
  570. * Gets the entity schema for the specified entity type.
  571. *
  572. * Entity types may override this method in order to optimize the generated
  573. * schema of the entity tables. However, only cross-field optimizations should
  574. * be added here; e.g., an index spanning multiple fields. Optimizations that
  575. * apply to a single field have to be added via
  576. * SqlContentEntityStorageSchema::getSharedTableFieldSchema() instead.
  577. *
  578. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  579. * The entity type definition.
  580. * @param bool $reset
  581. * (optional) If set to TRUE static cache will be ignored and a new schema
  582. * array generation will be performed. Defaults to FALSE.
  583. *
  584. * @return array
  585. * A Schema API array describing the entity schema, excluding dedicated
  586. * field tables.
  587. *
  588. * @throws \Drupal\Core\Field\FieldException
  589. */
  590. protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
  591. $this->checkEntityType($entity_type);
  592. $entity_type_id = $entity_type->id();
  593. if (!isset($this->schema[$entity_type_id]) || $reset) {
  594. // Prepare basic information about the entity type.
  595. $tables = $this->getEntitySchemaTables();
  596. // Initialize the table schema.
  597. $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type);
  598. if (isset($tables['revision_table'])) {
  599. $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type);
  600. }
  601. if (isset($tables['data_table'])) {
  602. $schema[$tables['data_table']] = $this->initializeDataTable($entity_type);
  603. }
  604. if (isset($tables['revision_data_table'])) {
  605. $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type);
  606. }
  607. // We need to act only on shared entity schema tables.
  608. $table_mapping = $this->storage->getCustomTableMapping($entity_type, $this->fieldStorageDefinitions);
  609. $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
  610. foreach ($table_names as $table_name) {
  611. if (!isset($schema[$table_name])) {
  612. $schema[$table_name] = [];
  613. }
  614. foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
  615. if (!isset($this->fieldStorageDefinitions[$field_name])) {
  616. throw new FieldException("Field storage definition for '$field_name' could not be found.");
  617. }
  618. // Add the schema for base field definitions.
  619. elseif ($table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) {
  620. $column_names = $table_mapping->getColumnNames($field_name);
  621. $storage_definition = $this->fieldStorageDefinitions[$field_name];
  622. $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
  623. }
  624. }
  625. }
  626. // Process tables after having gathered field information.
  627. $this->processBaseTable($entity_type, $schema[$tables['base_table']]);
  628. if (isset($tables['revision_table'])) {
  629. $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
  630. }
  631. if (isset($tables['data_table'])) {
  632. $this->processDataTable($entity_type, $schema[$tables['data_table']]);
  633. }
  634. if (isset($tables['revision_data_table'])) {
  635. $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
  636. }
  637. // Add an index for the 'published' entity key.
  638. if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) {
  639. $published_key = $entity_type->getKey('published');
  640. if ($published_key && !$this->fieldStorageDefinitions[$published_key]->hasCustomStorage()) {
  641. $published_field_table = $table_mapping->getFieldTableName($published_key);
  642. $id_key = $entity_type->getKey('id');
  643. if ($bundle_key = $entity_type->getKey('bundle')) {
  644. $key = "{$published_key}_{$bundle_key}";
  645. $columns = [$published_key, $bundle_key, $id_key];
  646. }
  647. else {
  648. $key = $published_key;
  649. $columns = [$published_key, $id_key];
  650. }
  651. $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns;
  652. }
  653. }
  654. $this->schema[$entity_type_id] = $schema;
  655. }
  656. return $this->schema[$entity_type_id];
  657. }
  658. /**
  659. * Gets a list of entity type tables.
  660. *
  661. * @return array
  662. * A list of entity type tables, keyed by table key.
  663. */
  664. protected function getEntitySchemaTables() {
  665. return array_filter([
  666. 'base_table' => $this->storage->getBaseTable(),
  667. 'revision_table' => $this->storage->getRevisionTable(),
  668. 'data_table' => $this->storage->getDataTable(),
  669. 'revision_data_table' => $this->storage->getRevisionDataTable(),
  670. ]);
  671. }
  672. /**
  673. * Gets entity schema definitions for index and key definitions.
  674. *
  675. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  676. * The entity type definition.
  677. * @param array $schema
  678. * The entity schema array.
  679. *
  680. * @return array
  681. * A stripped down version of the $schema Schema API array containing, for
  682. * each table, only the key and index definitions not derived from field
  683. * storage definitions.
  684. */
  685. protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
  686. $entity_type_id = $entity_type->id();
  687. // Collect all possible field schema identifiers for shared table fields.
  688. // These will be used to detect entity schema data in the subsequent loop.
  689. $field_schema_identifiers = [];
  690. $table_mapping = $this->storage->getTableMapping($this->fieldStorageDefinitions);
  691. foreach ($this->fieldStorageDefinitions as $field_name => $storage_definition) {
  692. if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
  693. // Make sure both base identifier names and suffixed names are listed.
  694. $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name);
  695. $field_schema_identifiers[$name] = $name;
  696. foreach ($storage_definition->getColumns() as $key => $columns) {
  697. $name = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
  698. $field_schema_identifiers[$name] = $name;
  699. }
  700. }
  701. }
  702. // Extract entity schema data from the Schema API definition.
  703. $schema_data = [];
  704. $keys = ['indexes', 'unique keys'];
  705. $unused_keys = array_flip(['description', 'fields', 'foreign keys']);
  706. foreach ($schema as $table_name => $table_schema) {
  707. $table_schema = array_diff_key($table_schema, $unused_keys);
  708. foreach ($keys as $key) {
  709. // Exclude data generated from field storage definitions, we will check
  710. // that separately.
  711. if ($field_schema_identifiers && !empty($table_schema[$key])) {
  712. $table_schema[$key] = array_diff_key($table_schema[$key], $field_schema_identifiers);
  713. }
  714. }
  715. $schema_data[$table_name] = array_filter($table_schema);
  716. }
  717. return $schema_data;
  718. }
  719. /**
  720. * Gets an index schema array for a given field.
  721. *
  722. * @param string $field_name
  723. * The name of the field.
  724. * @param array $field_schema
  725. * The schema of the field.
  726. * @param string[] $column_mapping
  727. * A mapping of field column names to database column names.
  728. *
  729. * @return array
  730. * The schema definition for the indexes.
  731. */
  732. protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) {
  733. return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes');
  734. }
  735. /**
  736. * Gets a unique key schema array for a given field.
  737. *
  738. * @param string $field_name
  739. * The name of the field.
  740. * @param array $field_schema
  741. * The schema of the field.
  742. * @param string[] $column_mapping
  743. * A mapping of field column names to database column names.
  744. *
  745. * @return array
  746. * The schema definition for the unique keys.
  747. */
  748. protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) {
  749. return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys');
  750. }
  751. /**
  752. * Gets field schema data for the given key.
  753. *
  754. * @param string $field_name
  755. * The name of the field.
  756. * @param array $field_schema
  757. * The schema of the field.
  758. * @param string[] $column_mapping
  759. * A mapping of field column names to database column names.
  760. * @param string $schema_key
  761. * The type of schema data. Either 'indexes' or 'unique keys'.
  762. *
  763. * @return array
  764. * The schema definition for the specified key.
  765. */
  766. protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) {
  767. $data = [];
  768. $entity_type_id = $this->entityType->id();
  769. foreach ($field_schema[$schema_key] as $key => $columns) {
  770. // To avoid clashes with entity-level indexes or unique keys we use
  771. // "{$entity_type_id}_field__" as a prefix instead of just
  772. // "{$entity_type_id}__". We additionally namespace the specifier by the
  773. // field name to avoid clashes when multiple fields of the same type are
  774. // added to an entity type.
  775. $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
  776. foreach ($columns as $column) {
  777. // Allow for indexes and unique keys to specified as an array of column
  778. // name and length.
  779. if (is_array($column)) {
  780. list($column_name, $length) = $column;
  781. $data[$real_key][] = [$column_mapping[$column_name], $length];
  782. }
  783. else {
  784. $data[$real_key][] = $column_mapping[$column];
  785. }
  786. }
  787. }
  788. return $data;
  789. }
  790. /**
  791. * Generates a safe schema identifier (name of an index, column name etc.).
  792. *
  793. * @param string $entity_type_id
  794. * The ID of the entity type.
  795. * @param string $field_name
  796. * The name of the field.
  797. * @param string|null $key
  798. * (optional) A further key to append to the name.
  799. *
  800. * @return string
  801. * The field identifier name.
  802. */
  803. protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key = NULL) {
  804. $real_key = isset($key) ? "{$entity_type_id}_field__{$field_name}__{$key}" : "{$entity_type_id}_field__{$field_name}";
  805. // Limit the string to 48 characters, keeping a 16 characters margin for db
  806. // prefixes.
  807. if (strlen($real_key) > 48) {
  808. // Use a shorter separator, a truncated entity_type, and a hash of the
  809. // field name.
  810. // Truncate to the same length for the current and revision tables.
  811. $entity_type = substr($entity_type_id, 0, 36);
  812. $field_hash = substr(hash('sha256', $real_key), 0, 10);
  813. $real_key = $entity_type . '__' . $field_hash;
  814. }
  815. return $real_key;
  816. }
  817. /**
  818. * Gets field foreign keys.
  819. *
  820. * @param string $field_name
  821. * The name of the field.
  822. * @param array $field_schema
  823. * The schema of the field.
  824. * @param string[] $column_mapping
  825. * A mapping of field column names to database column names.
  826. *
  827. * @return array
  828. * The schema definition for the foreign keys.
  829. */
  830. protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) {
  831. $foreign_keys = [];
  832. foreach ($field_schema['foreign keys'] as $specifier => $specification) {
  833. // To avoid clashes with entity-level foreign keys we use
  834. // "{$entity_type_id}_field__" as a prefix instead of just
  835. // "{$entity_type_id}__". We additionally namespace the specifier by the
  836. // field name to avoid clashes when multiple fields of the same type are
  837. // added to an entity type.
  838. $entity_type_id = $this->entityType->id();
  839. $real_specifier = "{$entity_type_id}_field__{$field_name}__{$specifier}";
  840. $foreign_keys[$real_specifier]['table'] = $specification['table'];
  841. foreach ($specification['columns'] as $column => $referenced) {
  842. $foreign_keys[$real_specifier]['columns'][$column_mapping[$column]] = $referenced;
  843. }
  844. }
  845. return $foreign_keys;
  846. }
  847. /**
  848. * Loads stored schema data for the given entity type definition.
  849. *
  850. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  851. * The entity type definition.
  852. *
  853. * @return array
  854. * The entity schema data array.
  855. */
  856. protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
  857. return $this->installedStorageSchema()->get($entity_type->id() . '.entity_schema_data', []);
  858. }
  859. /**
  860. * Stores schema data for the given entity type definition.
  861. *
  862. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  863. * The entity type definition.
  864. * @param array $schema
  865. * The entity schema data array.
  866. */
  867. protected function saveEntitySchemaData(EntityTypeInterface $entity_type, $schema) {
  868. $data = $this->getEntitySchemaData($entity_type, $schema);
  869. $this->installedStorageSchema()->set($entity_type->id() . '.entity_schema_data', $data);
  870. }
  871. /**
  872. * Deletes schema data for the given entity type definition.
  873. *
  874. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  875. * The entity type definition.
  876. */
  877. protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
  878. $this->installedStorageSchema()->delete($entity_type->id() . '.entity_schema_data');
  879. }
  880. /**
  881. * Loads stored schema data for the given field storage definition.
  882. *
  883. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  884. * The field storage definition.
  885. *
  886. * @return array
  887. * The field schema data array.
  888. */
  889. protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
  890. return $this->installedStorageSchema()->get($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), []);
  891. }
  892. /**
  893. * Stores schema data for the given field storage definition.
  894. *
  895. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  896. * The field storage definition.
  897. * @param array $schema
  898. * The field schema data array.
  899. */
  900. protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) {
  901. $this->processFieldStorageSchema($schema);
  902. $this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema);
  903. }
  904. /**
  905. * Deletes schema data for the given field storage definition.
  906. *
  907. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  908. * The field storage definition.
  909. */
  910. protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
  911. $this->installedStorageSchema()->delete($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName());
  912. }
  913. /**
  914. * Initializes common information for a base table.
  915. *
  916. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  917. * The entity type.
  918. *
  919. * @return array
  920. * A partial schema array for the base table.
  921. */
  922. protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
  923. $entity_type_id = $entity_type->id();
  924. $schema = [
  925. 'description' => "The base table for $entity_type_id entities.",
  926. 'primary key' => [$entity_type->getKey('id')],
  927. 'indexes' => [],
  928. 'foreign keys' => [],
  929. ];
  930. if ($entity_type->hasKey('revision')) {
  931. $revision_key = $entity_type->getKey('revision');
  932. $key_name = $this->getEntityIndexName($entity_type, $revision_key);
  933. $schema['unique keys'][$key_name] = [$revision_key];
  934. $schema['foreign keys'][$entity_type_id . '__revision'] = [
  935. 'table' => $this->storage->getRevisionTable(),
  936. 'columns' => [$revision_key => $revision_key],
  937. ];
  938. }
  939. $this->addTableDefaults($schema);
  940. return $schema;
  941. }
  942. /**
  943. * Initializes common information for a revision table.
  944. *
  945. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  946. * The entity type.
  947. *
  948. * @return array
  949. * A partial schema array for the revision table.
  950. */
  951. protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) {
  952. $entity_type_id = $entity_type->id();
  953. $id_key = $entity_type->getKey('id');
  954. $revision_key = $entity_type->getKey('revision');
  955. $schema = [
  956. 'description' => "The revision table for $entity_type_id entities.",
  957. 'primary key' => [$revision_key],
  958. 'indexes' => [],
  959. 'foreign keys' => [
  960. $entity_type_id . '__revisioned' => [
  961. 'table' => $this->storage->getBaseTable(),
  962. 'columns' => [$id_key => $id_key],
  963. ],
  964. ],
  965. ];
  966. $schema['indexes'][$this->getEntityIndexName($entity_type, $id_key)] = [$id_key];
  967. $this->addTableDefaults($schema);
  968. return $schema;
  969. }
  970. /**
  971. * Initializes common information for a data table.
  972. *
  973. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  974. * The entity type.
  975. *
  976. * @return array
  977. * A partial schema array for the data table.
  978. */
  979. protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
  980. $entity_type_id = $entity_type->id();
  981. $id_key = $entity_type->getKey('id');
  982. $schema = [
  983. 'description' => "The data table for $entity_type_id entities.",
  984. 'primary key' => [$id_key, $entity_type->getKey('langcode')],
  985. 'indexes' => [
  986. $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
  987. ],
  988. 'foreign keys' => [
  989. $entity_type_id => [
  990. 'table' => $this->storage->getBaseTable(),
  991. 'columns' => [$id_key => $id_key],
  992. ],
  993. ],
  994. ];
  995. if ($entity_type->hasKey('revision')) {
  996. $key = $entity_type->getKey('revision');
  997. $schema['indexes'][$this->getEntityIndexName($entity_type, $key)] = [$key];
  998. }
  999. $this->addTableDefaults($schema);
  1000. return $schema;
  1001. }
  1002. /**
  1003. * Initializes common information for a revision data table.
  1004. *
  1005. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  1006. * The entity type.
  1007. *
  1008. * @return array
  1009. * A partial schema array for the revision data table.
  1010. */
  1011. protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) {
  1012. $entity_type_id = $entity_type->id();
  1013. $id_key = $entity_type->getKey('id');
  1014. $revision_key = $entity_type->getKey('revision');
  1015. $schema = [
  1016. 'description' => "The revision data table for $entity_type_id entities.",
  1017. 'primary key' => [$revision_key, $entity_type->getKey('langcode')],
  1018. 'indexes' => [
  1019. $entity_type_id . '__id__default_langcode__langcode' => [$id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')],
  1020. ],
  1021. 'foreign keys' => [
  1022. $entity_type_id => [
  1023. 'table' => $this->storage->getBaseTable(),
  1024. 'columns' => [$id_key => $id_key],
  1025. ],
  1026. $entity_type_id . '__revision' => [
  1027. 'table' => $this->storage->getRevisionTable(),
  1028. 'columns' => [$revision_key => $revision_key],
  1029. ],
  1030. ],
  1031. ];
  1032. $this->addTableDefaults($schema);
  1033. return $schema;
  1034. }
  1035. /**
  1036. * Adds defaults to a table schema definition.
  1037. *
  1038. * @param $schema
  1039. * The schema definition array for a single table, passed by reference.
  1040. */
  1041. protected function addTableDefaults(&$schema) {
  1042. $schema += [
  1043. 'fields' => [],
  1044. 'unique keys' => [],
  1045. 'indexes' => [],
  1046. 'foreign keys' => [],
  1047. ];
  1048. }
  1049. /**
  1050. * Processes the gathered schema for a base table.
  1051. *
  1052. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  1053. * The entity type.
  1054. * @param array $schema
  1055. * The table schema, passed by reference.
  1056. */
  1057. protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
  1058. // Process the schema for the 'id' entity key only if it exists.
  1059. if ($entity_type->hasKey('id')) {
  1060. $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
  1061. }
  1062. }
  1063. /**
  1064. * Processes the gathered schema for a base table.
  1065. *
  1066. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  1067. * The entity type.
  1068. * @param array $schema
  1069. * The table schema, passed by reference.
  1070. */
  1071. protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
  1072. // Process the schema for the 'revision' entity key only if it exists.
  1073. if ($entity_type->hasKey('revision')) {
  1074. $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
  1075. }
  1076. }
  1077. /**
  1078. * Processes the gathered schema for a base table.
  1079. *
  1080. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  1081. * The entity type.
  1082. * @param array $schema
  1083. * The table schema, passed by reference.
  1084. *
  1085. * @return array
  1086. * A partial schema array for the base table.
  1087. */
  1088. protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
  1089. // Marking the respective fields as NOT NULL makes the indexes more
  1090. // performant.
  1091. $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
  1092. }
  1093. /**
  1094. * Processes the gathered schema for a base table.
  1095. *
  1096. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  1097. * The entity type.
  1098. * @param array $schema
  1099. * The table schema, passed by reference.
  1100. *
  1101. * @return array
  1102. * A partial schema array for the base table.
  1103. */
  1104. protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
  1105. // Marking the respective fields as NOT NULL makes the indexes more
  1106. // performant.
  1107. $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
  1108. }
  1109. /**
  1110. * Processes the specified entity key.
  1111. *
  1112. * @param array $schema
  1113. * The table schema, passed by reference.
  1114. * @param string $key
  1115. * The entity key name.
  1116. */
  1117. protected function processIdentifierSchema(&$schema, $key) {
  1118. if ($schema['fields'][$key]['type'] == 'int') {
  1119. $schema['fields'][$key]['type'] = 'serial';
  1120. }
  1121. $schema['fields'][$key]['not null'] = TRUE;
  1122. unset($schema['fields'][$key]['default']);
  1123. }
  1124. /**
  1125. * Processes the schema for a field storage definition.
  1126. *
  1127. * @param array &$field_storage_schema
  1128. * An array that contains the schema data for a field storage definition.
  1129. */
  1130. protected function processFieldStorageSchema(array &$field_storage_schema) {
  1131. // Clean up some schema properties that should not be taken into account
  1132. // after a field storage has been created.
  1133. foreach ($field_storage_schema as $table_name => $table_schema) {
  1134. foreach ($table_schema['fields'] as $key => $schema) {
  1135. unset($field_storage_schema[$table_name]['fields'][$key]['initial']);
  1136. unset($field_storage_schema[$table_name]['fields'][$key]['initial_from_field']);
  1137. }
  1138. }
  1139. }
  1140. /**
  1141. * Performs the specified operation on a field.
  1142. *
  1143. * This figures out whether the field is stored in a dedicated or shared table
  1144. * and forwards the call to the proper handler.
  1145. *
  1146. * @param string $operation
  1147. * The name of the operation to be performed.
  1148. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1149. * The field storage definition.
  1150. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
  1151. * (optional) The original field storage definition. This is relevant (and
  1152. * required) only for updates. Defaults to NULL.
  1153. */
  1154. protected function performFieldSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) {
  1155. $table_mapping = $this->storage->getTableMapping();
  1156. if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
  1157. $this->{$operation . 'DedicatedTableSchema'}($storage_definition, $original);
  1158. }
  1159. elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
  1160. $this->{$operation . 'SharedTableSchema'}($storage_definition, $original);
  1161. }
  1162. }
  1163. /**
  1164. * Creates the schema for a field stored in a dedicated table.
  1165. *
  1166. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1167. * The storage definition of the field being created.
  1168. */
  1169. protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
  1170. $schema = $this->getDedicatedTableSchema($storage_definition);
  1171. foreach ($schema as $name => $table) {
  1172. // Check if the table exists because it might already have been
  1173. // created as part of the earlier entity type update event.
  1174. if (!$this->database->schema()->tableExists($name)) {
  1175. $this->database->schema()->createTable($name, $table);
  1176. }
  1177. }
  1178. $this->saveFieldSchemaData($storage_definition, $schema);
  1179. }
  1180. /**
  1181. * Creates the schema for a field stored in a shared table.
  1182. *
  1183. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1184. * The storage definition of the field being created.
  1185. * @param bool $only_save
  1186. * (optional) Whether to skip modification of database tables and only save
  1187. * the schema data for future comparison. For internal use only. This is
  1188. * used by onEntityTypeCreate() after it has already fully created the
  1189. * shared tables.
  1190. */
  1191. protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) {
  1192. $created_field_name = $storage_definition->getName();
  1193. $table_mapping = $this->storage->getTableMapping();
  1194. $column_names = $table_mapping->getColumnNames($created_field_name);
  1195. $schema_handler = $this->database->schema();
  1196. $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
  1197. // Iterate over the mapped table to find the ones that will host the created
  1198. // field schema.
  1199. $schema = [];
  1200. foreach ($shared_table_names as $table_name) {
  1201. foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
  1202. if ($field_name == $created_field_name) {
  1203. // Create field columns.
  1204. $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
  1205. if (!$only_save) {
  1206. // The entity schema needs to be checked because the field schema is
  1207. // potentially incomplete.
  1208. // @todo Fix this in https://www.drupal.org/node/2929120.
  1209. $entity_schema = $this->getEntitySchema($this->entityType);
  1210. foreach ($schema[$table_name]['fields'] as $name => $specifier) {
  1211. // Check if the field is part of the primary keys and pass along
  1212. // this information when adding the field.
  1213. // @see \Drupal\Core\Database\Schema::addField()
  1214. $new_keys = [];
  1215. if (isset($entity_schema[$table_name]['primary key']) && array_intersect($column_names, $entity_schema[$table_name]['primary key'])) {
  1216. $new_keys = ['primary key' => $entity_schema[$table_name]['primary key']];
  1217. }
  1218. // Check if the field exists because it might already have been
  1219. // created as part of the earlier entity type update event.
  1220. if (!$schema_handler->fieldExists($table_name, $name)) {
  1221. $schema_handler->addField($table_name, $name, $specifier, $new_keys);
  1222. }
  1223. }
  1224. if (!empty($schema[$table_name]['indexes'])) {
  1225. foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
  1226. // Check if the index exists because it might already have been
  1227. // created as part of the earlier entity type update event.
  1228. $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
  1229. }
  1230. }
  1231. if (!empty($schema[$table_name]['unique keys'])) {
  1232. foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
  1233. $schema_handler->addUniqueKey($table_name, $name, $specifier);
  1234. }
  1235. }
  1236. }
  1237. // After creating the field schema skip to the next table.
  1238. break;
  1239. }
  1240. }
  1241. }
  1242. $this->saveFieldSchemaData($storage_definition, $schema);
  1243. if (!$only_save) {
  1244. // Make sure any entity index involving this field is re-created if
  1245. // needed.
  1246. $this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
  1247. }
  1248. }
  1249. /**
  1250. * Deletes the schema for a field stored in a dedicated table.
  1251. *
  1252. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1253. * The storage definition of the field being deleted.
  1254. */
  1255. protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
  1256. $table_mapping = $this->storage->getTableMapping();
  1257. $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
  1258. if ($this->database->schema()->tableExists($table_name)) {
  1259. $this->database->schema()->dropTable($table_name);
  1260. }
  1261. if ($this->entityType->isRevisionable()) {
  1262. $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $storage_definition->isDeleted());
  1263. if ($this->database->schema()->tableExists($revision_table_name)) {
  1264. $this->database->schema()->dropTable($revision_table_name);
  1265. }
  1266. }
  1267. $this->deleteFieldSchemaData($storage_definition);
  1268. }
  1269. /**
  1270. * Deletes the schema for a field stored in a shared table.
  1271. *
  1272. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1273. * The storage definition of the field being deleted.
  1274. */
  1275. protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
  1276. // Make sure any entity index involving this field is dropped.
  1277. $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
  1278. $deleted_field_name = $storage_definition->getName();
  1279. $table_mapping = $this->storage->getTableMapping(
  1280. $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
  1281. );
  1282. $column_names = $table_mapping->getColumnNames($deleted_field_name);
  1283. $schema_handler = $this->database->schema();
  1284. $shared_table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames());
  1285. // Iterate over the mapped table to find the ones that host the deleted
  1286. // field schema.
  1287. foreach ($shared_table_names as $table_name) {
  1288. foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
  1289. if ($field_name == $deleted_field_name) {
  1290. $schema = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
  1291. // Drop indexes and unique keys first.
  1292. if (!empty($schema['indexes'])) {
  1293. foreach ($schema['indexes'] as $name => $specifier) {
  1294. $schema_handler->dropIndex($table_name, $name);
  1295. }
  1296. }
  1297. if (!empty($schema['unique keys'])) {
  1298. foreach ($schema['unique keys'] as $name => $specifier) {
  1299. $schema_handler->dropUniqueKey($table_name, $name);
  1300. }
  1301. }
  1302. // Drop columns.
  1303. foreach ($column_names as $column_name) {
  1304. $schema_handler->dropField($table_name, $column_name);
  1305. }
  1306. // After deleting the field schema skip to the next table.
  1307. break;
  1308. }
  1309. }
  1310. }
  1311. $this->deleteFieldSchemaData($storage_definition);
  1312. }
  1313. /**
  1314. * Updates the schema for a field stored in a shared table.
  1315. *
  1316. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1317. * The storage definition of the field being updated.
  1318. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
  1319. * The original storage definition; i.e., the definition before the update.
  1320. *
  1321. * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
  1322. * Thrown when the update to the field is forbidden.
  1323. * @throws \Exception
  1324. * Rethrown exception if the table recreation fails.
  1325. */
  1326. protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
  1327. if (!$this->storage->countFieldData($original, TRUE)) {
  1328. // There is no data. Re-create the tables completely.
  1329. if ($this->database->supportsTransactionalDDL()) {
  1330. // If the database supports transactional DDL, we can go ahead and rely
  1331. // on it. If not, we will have to rollback manually if something fails.
  1332. $transaction = $this->database->startTransaction();
  1333. }
  1334. try {
  1335. // Since there is no data we may be switching from a shared table schema
  1336. // to a dedicated table schema, hence we should use the proper API.
  1337. $this->performFieldSchemaOperation('delete', $original);
  1338. $this->performFieldSchemaOperation('create', $storage_definition);
  1339. }
  1340. catch (\Exception $e) {
  1341. if ($this->database->supportsTransactionalDDL()) {
  1342. $transaction->rollBack();
  1343. }
  1344. else {
  1345. // Recreate tables.
  1346. $this->performFieldSchemaOperation('create', $original);
  1347. }
  1348. throw $e;
  1349. }
  1350. }
  1351. else {
  1352. if ($this->hasColumnChanges($storage_definition, $original)) {
  1353. throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
  1354. }
  1355. // There is data, so there are no column changes. Drop all the prior
  1356. // indexes and create all the new ones, except for all the priors that
  1357. // exist unchanged.
  1358. $table_mapping = $this->storage->getTableMapping();
  1359. $table = $table_mapping->getDedicatedDataTableName($original);
  1360. $revision_table = $table_mapping->getDedicatedRevisionTableName($original);
  1361. // Get the field schemas.
  1362. $schema = $storage_definition->getSchema();
  1363. $original_schema = $original->getSchema();
  1364. // Gets the SQL schema for a dedicated tables.
  1365. $actual_schema = $this->getDedicatedTableSchema($storage_definition);
  1366. foreach ($original_schema['indexes'] as $name => $columns) {
  1367. if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
  1368. $real_name = $this->getFieldIndexName($storage_definition, $name);
  1369. $this->database->schema()->dropIndex($table, $real_name);
  1370. $this->database->schema()->dropIndex($revision_table, $real_name);
  1371. }
  1372. }
  1373. $table = $table_mapping->getDedicatedDataTableName($storage_definition);
  1374. $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
  1375. foreach ($schema['indexes'] as $name => $columns) {
  1376. if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
  1377. $real_name = $this->getFieldIndexName($storage_definition, $name);
  1378. $real_columns = [];
  1379. foreach ($columns as $column_name) {
  1380. // Indexes can be specified as either a column name or an array with
  1381. // column name and length. Allow for either case.
  1382. if (is_array($column_name)) {
  1383. $real_columns[] = [
  1384. $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
  1385. $column_name[1],
  1386. ];
  1387. }
  1388. else {
  1389. $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
  1390. }
  1391. }
  1392. // Check if the index exists because it might already have been
  1393. // created as part of the earlier entity type update event.
  1394. $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
  1395. $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
  1396. }
  1397. }
  1398. $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
  1399. }
  1400. }
  1401. /**
  1402. * Updates the schema for a field stored in a shared table.
  1403. *
  1404. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1405. * The storage definition of the field being updated.
  1406. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
  1407. * The original storage definition; i.e., the definition before the update.
  1408. *
  1409. * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
  1410. * Thrown when the update to the field is forbidden.
  1411. * @throws \Exception
  1412. * Rethrown exception if the table recreation fails.
  1413. */
  1414. protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
  1415. if (!$this->storage->countFieldData($original, TRUE)) {
  1416. if ($this->database->supportsTransactionalDDL()) {
  1417. // If the database supports transactional DDL, we can go ahead and rely
  1418. // on it. If not, we will have to rollback manually if something fails.
  1419. $transaction = $this->database->startTransaction();
  1420. }
  1421. try {
  1422. // Since there is no data we may be switching from a dedicated table
  1423. // to a schema table schema, hence we should use the proper API.
  1424. $this->performFieldSchemaOperation('delete', $original);
  1425. $this->performFieldSchemaOperation('create', $storage_definition);
  1426. }
  1427. catch (\Exception $e) {
  1428. if ($this->database->supportsTransactionalDDL()) {
  1429. $transaction->rollBack();
  1430. }
  1431. else {
  1432. // Recreate original schema.
  1433. $this->createSharedTableSchema($original);
  1434. }
  1435. throw $e;
  1436. }
  1437. }
  1438. else {
  1439. if ($this->hasColumnChanges($storage_definition, $original)) {
  1440. throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
  1441. }
  1442. $updated_field_name = $storage_definition->getName();
  1443. $table_mapping = $this->storage->getTableMapping();
  1444. $column_names = $table_mapping->getColumnNames($updated_field_name);
  1445. $schema_handler = $this->database->schema();
  1446. // Iterate over the mapped table to find the ones that host the deleted
  1447. // field schema.
  1448. $original_schema = $this->loadFieldSchemaData($original);
  1449. $schema = [];
  1450. foreach ($table_mapping->getTableNames() as $table_name) {
  1451. foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
  1452. if ($field_name == $updated_field_name) {
  1453. $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
  1454. // Handle NOT NULL constraints.
  1455. foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
  1456. $not_null = !empty($specifier['not null']);
  1457. $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
  1458. if ($not_null !== $original_not_null) {
  1459. if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
  1460. throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
  1461. }
  1462. $column_schema = $original_schema[$table_name]['fields'][$column_name];
  1463. $column_schema['not null'] = $not_null;
  1464. $schema_handler->changeField($table_name, $column_name, $column_name, $column_schema);
  1465. }
  1466. }
  1467. // Drop original indexes and unique keys.
  1468. if (!empty($original_schema[$table_name]['indexes'])) {
  1469. foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
  1470. $schema_handler->dropIndex($table_name, $name);
  1471. }
  1472. }
  1473. if (!empty($original_schema[$table_name]['unique keys'])) {
  1474. foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) {
  1475. $schema_handler->dropUniqueKey($table_name, $name);
  1476. }
  1477. }
  1478. // Create new indexes and unique keys.
  1479. if (!empty($schema[$table_name]['indexes'])) {
  1480. foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
  1481. // Check if the index exists because it might already have been
  1482. // created as part of the earlier entity type update event.
  1483. $this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
  1484. }
  1485. }
  1486. if (!empty($schema[$table_name]['unique keys'])) {
  1487. foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
  1488. $schema_handler->addUniqueKey($table_name, $name, $specifier);
  1489. }
  1490. }
  1491. // After deleting the field schema skip to the next table.
  1492. break;
  1493. }
  1494. }
  1495. }
  1496. $this->saveFieldSchemaData($storage_definition, $schema);
  1497. }
  1498. }
  1499. /**
  1500. * Creates the specified entity schema indexes and keys.
  1501. *
  1502. * @param array $entity_schema
  1503. * The entity schema definition.
  1504. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
  1505. * (optional) If a field storage definition is specified, only indexes and
  1506. * keys involving its columns will be processed. Otherwise all defined
  1507. * entity indexes and keys will be processed.
  1508. */
  1509. protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
  1510. $schema_handler = $this->database->schema();
  1511. if ($storage_definition) {
  1512. $table_mapping = $this->storage->getTableMapping();
  1513. $column_names = $table_mapping->getColumnNames($storage_definition->getName());
  1514. }
  1515. $index_keys = [
  1516. 'indexes' => 'addIndex',
  1517. 'unique keys' => 'addUniqueKey',
  1518. ];
  1519. foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
  1520. // Add fields schema because database driver may depend on this data to
  1521. // perform index normalization.
  1522. $schema['fields'] = $entity_schema[$table_name]['fields'];
  1523. foreach ($index_keys as $key => $add_method) {
  1524. if (!empty($schema[$key])) {
  1525. foreach ($schema[$key] as $name => $specifier) {
  1526. // If a set of field columns were specified we process only indexes
  1527. // involving them. Only indexes for which all columns exist are
  1528. // actually created.
  1529. $create = FALSE;
  1530. $specifier_columns = array_map(function ($item) {
  1531. return is_string($item) ? $item : reset($item);
  1532. }, $specifier);
  1533. if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
  1534. $create = TRUE;
  1535. foreach ($specifier_columns as $specifier_column_name) {
  1536. // This may happen when adding more than one field in the same
  1537. // update run. Eventually when all field columns have been
  1538. // created the index will be created too.
  1539. if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
  1540. $create = FALSE;
  1541. break;
  1542. }
  1543. }
  1544. }
  1545. if ($create) {
  1546. $this->{$add_method}($table_name, $name, $specifier, $schema);
  1547. }
  1548. }
  1549. }
  1550. }
  1551. }
  1552. }
  1553. /**
  1554. * Deletes the specified entity schema indexes and keys.
  1555. *
  1556. * @param array $entity_schema_data
  1557. * The entity schema data definition.
  1558. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|null $storage_definition
  1559. * (optional) If a field storage definition is specified, only indexes and
  1560. * keys involving its columns will be processed. Otherwise all defined
  1561. * entity indexes and keys will be processed.
  1562. */
  1563. protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
  1564. $schema_handler = $this->database->schema();
  1565. if ($storage_definition) {
  1566. $table_mapping = $this->storage->getTableMapping();
  1567. $column_names = $table_mapping->getColumnNames($storage_definition->getName());
  1568. }
  1569. $index_keys = [
  1570. 'indexes' => 'dropIndex',
  1571. 'unique keys' => 'dropUniqueKey',
  1572. ];
  1573. foreach ($entity_schema_data as $table_name => $schema) {
  1574. foreach ($index_keys as $key => $drop_method) {
  1575. if (!empty($schema[$key])) {
  1576. foreach ($schema[$key] as $name => $specifier) {
  1577. $specifier_columns = array_map(function ($item) {
  1578. return is_string($item) ? $item : reset($item);
  1579. }, $specifier);
  1580. if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
  1581. $schema_handler->{$drop_method}($table_name, $name);
  1582. }
  1583. }
  1584. }
  1585. }
  1586. }
  1587. }
  1588. /**
  1589. * Checks whether a field property has NULL values.
  1590. *
  1591. * @param string $table_name
  1592. * The name of the table to inspect.
  1593. * @param string $column_name
  1594. * The name of the column holding the field property data.
  1595. *
  1596. * @return bool
  1597. * TRUE if NULL data is found, FALSE otherwise.
  1598. */
  1599. protected function hasNullFieldPropertyData($table_name, $column_name) {
  1600. $query = $this->database->select($table_name, 't')
  1601. ->fields('t', [$column_name])
  1602. ->range(0, 1);
  1603. $query->isNull('t.' . $column_name);
  1604. $result = $query->execute()->fetchAssoc();
  1605. return (bool) $result;
  1606. }
  1607. /**
  1608. * Gets the schema for a single field definition.
  1609. *
  1610. * Entity types may override this method in order to optimize the generated
  1611. * schema for given field. While all optimizations that apply to a single
  1612. * field have to be added here, all cross-field optimizations should be via
  1613. * SqlContentEntityStorageSchema::getEntitySchema() instead; e.g.,
  1614. * an index spanning multiple fields.
  1615. *
  1616. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1617. * The storage definition of the field whose schema has to be returned.
  1618. * @param string $table_name
  1619. * The name of the table columns will be added to.
  1620. * @param string[] $column_mapping
  1621. * A mapping of field column names to database column names.
  1622. *
  1623. * @return array
  1624. * The schema definition for the table with the following keys:
  1625. * - fields: The schema definition for the each field columns.
  1626. * - indexes: The schema definition for the indexes.
  1627. * - unique keys: The schema definition for the unique keys.
  1628. * - foreign keys: The schema definition for the foreign keys.
  1629. *
  1630. * @throws \Drupal\Core\Field\FieldException
  1631. * Exception thrown if the schema contains reserved column names or if the
  1632. * initial values definition is invalid.
  1633. */
  1634. protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
  1635. $schema = [];
  1636. $table_mapping = $this->storage->getTableMapping();
  1637. $field_schema = $storage_definition->getSchema();
  1638. // Check that the schema does not include forbidden column names.
  1639. if (array_intersect(array_keys($field_schema['columns']), $table_mapping->getReservedColumns())) {
  1640. throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
  1641. }
  1642. $field_name = $storage_definition->getName();
  1643. $base_table = $this->storage->getBaseTable();
  1644. // Define the initial values, if any.
  1645. $initial_value = $initial_value_from_field = [];
  1646. $storage_definition_is_new = empty($this->loadFieldSchemaData($storage_definition));
  1647. if ($storage_definition_is_new && $storage_definition instanceof BaseFieldDefinition && $table_mapping->allowsSharedTableStorage($storage_definition)) {
  1648. if (($initial_storage_value = $storage_definition->getInitialValue()) && !empty($initial_storage_value)) {
  1649. // We only support initial values for fields that are stored in shared
  1650. // tables (i.e. single-value fields).
  1651. // @todo Implement initial value support for multi-value fields in
  1652. // https://www.drupal.org/node/2883851.
  1653. $initial_value = reset($initial_storage_value);
  1654. }
  1655. if ($initial_value_field_name = $storage_definition->getInitialValueFromField()) {
  1656. // Check that the field used for populating initial values is valid. We
  1657. // must use the last installed version of that, as the new field might
  1658. // be created in an update function and the storage definition of the
  1659. // "from" field might get changed later.
  1660. $last_installed_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
  1661. if (!isset($last_installed_storage_definitions[$initial_value_field_name])) {
  1662. throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field $initial_value_field_name does not exist.");
  1663. }
  1664. if ($storage_definition->getType() !== $last_installed_storage_definitions[$initial_value_field_name]->getType()) {
  1665. throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field types do not match.");
  1666. }
  1667. if (!$table_mapping->allowsSharedTableStorage($last_installed_storage_definitions[$initial_value_field_name])) {
  1668. throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: Both fields have to be stored in the shared entity tables.");
  1669. }
  1670. $initial_value_from_field = $table_mapping->getColumnNames($initial_value_field_name);
  1671. }
  1672. }
  1673. // A shared table contains rows for entities where the field is empty
  1674. // (since other fields stored in the same table might not be empty), thus
  1675. // the only columns that can be 'not null' are those for required
  1676. // properties of required fields. For now, we only hardcode 'not null' to a
  1677. // few "entity keys", in order to keep their indexes optimized.
  1678. // @todo Fix this in https://www.drupal.org/node/2841291.
  1679. $not_null_keys = $this->entityType->getKeys();
  1680. // Label and the 'revision_translation_affected' fields are not necessarily
  1681. // required.
  1682. unset($not_null_keys['label'], $not_null_keys['revision_translation_affected']);
  1683. // Because entity ID and revision ID are both serial fields in the base and
  1684. // revision table respectively, the revision ID is not known yet, when
  1685. // inserting data into the base table. Instead the revision ID in the base
  1686. // table is updated after the data has been inserted into the revision
  1687. // table. For this reason the revision ID field cannot be marked as NOT
  1688. // NULL.
  1689. if ($table_name == $base_table) {
  1690. unset($not_null_keys['revision']);
  1691. }
  1692. foreach ($column_mapping as $field_column_name => $schema_field_name) {
  1693. $column_schema = $field_schema['columns'][$field_column_name];
  1694. $schema['fields'][$schema_field_name] = $column_schema;
  1695. $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
  1696. // Use the initial value of the field storage, if available.
  1697. if ($initial_value && isset($initial_value[$field_column_name])) {
  1698. $schema['fields'][$schema_field_name]['initial'] = drupal_schema_get_field_value($column_schema, $initial_value[$field_column_name]);
  1699. }
  1700. if (!empty($initial_value_from_field)) {
  1701. $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name];
  1702. }
  1703. }
  1704. if (!empty($field_schema['indexes'])) {
  1705. $schema['indexes'] = $this->getFieldIndexes($field_name, $field_schema, $column_mapping);
  1706. }
  1707. if (!empty($field_schema['unique keys'])) {
  1708. $schema['unique keys'] = $this->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
  1709. }
  1710. if (!empty($field_schema['foreign keys'])) {
  1711. $schema['foreign keys'] = $this->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
  1712. }
  1713. return $schema;
  1714. }
  1715. /**
  1716. * Adds an index for the specified field to the given schema definition.
  1717. *
  1718. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1719. * The storage definition of the field for which an index should be added.
  1720. * @param array $schema
  1721. * A reference to the schema array to be updated.
  1722. * @param bool $not_null
  1723. * (optional) Whether to also add a 'not null' constraint to the column
  1724. * being indexed. Doing so improves index performance. Defaults to FALSE,
  1725. * in case the field needs to support NULL values.
  1726. * @param int $size
  1727. * (optional) The index size. Defaults to no limit.
  1728. */
  1729. protected function addSharedTableFieldIndex(FieldStorageDefinitionInterface $storage_definition, &$schema, $not_null = FALSE, $size = NULL) {
  1730. $name = $storage_definition->getName();
  1731. $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
  1732. $schema['indexes'][$real_key] = [$size ? [$name, $size] : $name];
  1733. if ($not_null) {
  1734. $schema['fields'][$name]['not null'] = TRUE;
  1735. }
  1736. }
  1737. /**
  1738. * Adds a unique key for the specified field to the given schema definition.
  1739. *
  1740. * Also adds a 'not null' constraint, because many databases do not reliably
  1741. * support unique keys on null columns.
  1742. *
  1743. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1744. * The storage definition of the field to which to add a unique key.
  1745. * @param array $schema
  1746. * A reference to the schema array to be updated.
  1747. */
  1748. protected function addSharedTableFieldUniqueKey(FieldStorageDefinitionInterface $storage_definition, &$schema) {
  1749. $name = $storage_definition->getName();
  1750. $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
  1751. $schema['unique keys'][$real_key] = [$name];
  1752. $schema['fields'][$name]['not null'] = TRUE;
  1753. }
  1754. /**
  1755. * Adds a foreign key for the specified field to the given schema definition.
  1756. *
  1757. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1758. * The storage definition of the field to which to add a foreign key.
  1759. * @param array $schema
  1760. * A reference to the schema array to be updated.
  1761. * @param string $foreign_table
  1762. * The foreign table.
  1763. * @param string $foreign_column
  1764. * The foreign column.
  1765. */
  1766. protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface $storage_definition, &$schema, $foreign_table, $foreign_column) {
  1767. $name = $storage_definition->getName();
  1768. $real_key = $this->getFieldSchemaIdentifierName($storage_definition->getTargetEntityTypeId(), $name);
  1769. $schema['foreign keys'][$real_key] = [
  1770. 'table' => $foreign_table,
  1771. 'columns' => [$name => $foreign_column],
  1772. ];
  1773. }
  1774. /**
  1775. * Gets the SQL schema for a dedicated table.
  1776. *
  1777. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1778. * The field storage definition.
  1779. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  1780. * (optional) The entity type definition. Defaults to the one returned by
  1781. * the entity manager.
  1782. *
  1783. * @return array
  1784. * The schema definition for the table with the following keys:
  1785. * - fields: The schema definition for the each field columns.
  1786. * - indexes: The schema definition for the indexes.
  1787. * - unique keys: The schema definition for the unique keys.
  1788. * - foreign keys: The schema definition for the foreign keys.
  1789. *
  1790. * @throws \Drupal\Core\Field\FieldException
  1791. * Exception thrown if the schema contains reserved column names.
  1792. *
  1793. * @see hook_schema()
  1794. */
  1795. protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
  1796. $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
  1797. $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
  1798. $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')];
  1799. if ($id_definition->getType() == 'integer') {
  1800. $id_schema = [
  1801. 'type' => 'int',
  1802. 'unsigned' => TRUE,
  1803. 'not null' => TRUE,
  1804. 'description' => 'The entity id this data is attached to',
  1805. ];
  1806. }
  1807. else {
  1808. $id_schema = [
  1809. 'type' => 'varchar_ascii',
  1810. 'length' => 128,
  1811. 'not null' => TRUE,
  1812. 'description' => 'The entity id this data is attached to',
  1813. ];
  1814. }
  1815. // Define the revision ID schema.
  1816. if (!$this->entityType->isRevisionable()) {
  1817. $revision_id_schema = $id_schema;
  1818. $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';
  1819. }
  1820. elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') {
  1821. $revision_id_schema = [
  1822. 'type' => 'int',
  1823. 'unsigned' => TRUE,
  1824. 'not null' => TRUE,
  1825. 'description' => 'The entity revision id this data is attached to',
  1826. ];
  1827. }
  1828. else {
  1829. $revision_id_schema = [
  1830. 'type' => 'varchar',
  1831. 'length' => 128,
  1832. 'not null' => TRUE,
  1833. 'description' => 'The entity revision id this data is attached to',
  1834. ];
  1835. }
  1836. $data_schema = [
  1837. 'description' => $description_current,
  1838. 'fields' => [
  1839. 'bundle' => [
  1840. 'type' => 'varchar_ascii',
  1841. 'length' => 128,
  1842. 'not null' => TRUE,
  1843. 'default' => '',
  1844. 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
  1845. ],
  1846. 'deleted' => [
  1847. 'type' => 'int',
  1848. 'size' => 'tiny',
  1849. 'not null' => TRUE,
  1850. 'default' => 0,
  1851. 'description' => 'A boolean indicating whether this data item has been deleted',
  1852. ],
  1853. 'entity_id' => $id_schema,
  1854. 'revision_id' => $revision_id_schema,
  1855. 'langcode' => [
  1856. 'type' => 'varchar_ascii',
  1857. 'length' => 32,
  1858. 'not null' => TRUE,
  1859. 'default' => '',
  1860. 'description' => 'The language code for this data item.',
  1861. ],
  1862. 'delta' => [
  1863. 'type' => 'int',
  1864. 'unsigned' => TRUE,
  1865. 'not null' => TRUE,
  1866. 'description' => 'The sequence number for this data item, used for multi-value fields',
  1867. ],
  1868. ],
  1869. 'primary key' => ['entity_id', 'deleted', 'delta', 'langcode'],
  1870. 'indexes' => [
  1871. 'bundle' => ['bundle'],
  1872. 'revision_id' => ['revision_id'],
  1873. ],
  1874. ];
  1875. // Check that the schema does not include forbidden column names.
  1876. $schema = $storage_definition->getSchema();
  1877. $properties = $storage_definition->getPropertyDefinitions();
  1878. $table_mapping = $this->storage->getTableMapping();
  1879. if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
  1880. throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
  1881. }
  1882. // Add field columns.
  1883. foreach ($schema['columns'] as $column_name => $attributes) {
  1884. $real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
  1885. $data_schema['fields'][$real_name] = $attributes;
  1886. // A dedicated table only contain rows for actual field values, and no
  1887. // rows for entities where the field is empty. Thus, we can safely
  1888. // enforce 'not null' on the columns for the field's required properties.
  1889. $data_schema['fields'][$real_name]['not null'] = $properties[$column_name]->isRequired();
  1890. }
  1891. // Add indexes.
  1892. foreach ($schema['indexes'] as $index_name => $columns) {
  1893. $real_name = $this->getFieldIndexName($storage_definition, $index_name);
  1894. foreach ($columns as $column_name) {
  1895. // Indexes can be specified as either a column name or an array with
  1896. // column name and length. Allow for either case.
  1897. if (is_array($column_name)) {
  1898. $data_schema['indexes'][$real_name][] = [
  1899. $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
  1900. $column_name[1],
  1901. ];
  1902. }
  1903. else {
  1904. $data_schema['indexes'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
  1905. }
  1906. }
  1907. }
  1908. // Add unique keys.
  1909. foreach ($schema['unique keys'] as $index_name => $columns) {
  1910. $real_name = $this->getFieldIndexName($storage_definition, $index_name);
  1911. foreach ($columns as $column_name) {
  1912. // Unique keys can be specified as either a column name or an array with
  1913. // column name and length. Allow for either case.
  1914. if (is_array($column_name)) {
  1915. $data_schema['unique keys'][$real_name][] = [
  1916. $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
  1917. $column_name[1],
  1918. ];
  1919. }
  1920. else {
  1921. $data_schema['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
  1922. }
  1923. }
  1924. }
  1925. // Add foreign keys.
  1926. foreach ($schema['foreign keys'] as $specifier => $specification) {
  1927. $real_name = $this->getFieldIndexName($storage_definition, $specifier);
  1928. $data_schema['foreign keys'][$real_name]['table'] = $specification['table'];
  1929. foreach ($specification['columns'] as $column_name => $referenced) {
  1930. $sql_storage_column = $table_mapping->getFieldColumnName($storage_definition, $column_name);
  1931. $data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
  1932. }
  1933. }
  1934. $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema];
  1935. // If the entity type is revisionable, construct the revision table.
  1936. $entity_type = $entity_type ?: $this->entityType;
  1937. if ($entity_type->isRevisionable()) {
  1938. $revision_schema = $data_schema;
  1939. $revision_schema['description'] = $description_revision;
  1940. $revision_schema['primary key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode'];
  1941. $revision_schema['fields']['revision_id']['not null'] = TRUE;
  1942. $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
  1943. $dedicated_table_schema += [$table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema];
  1944. }
  1945. return $dedicated_table_schema;
  1946. }
  1947. /**
  1948. * Gets the name to be used for the given entity index.
  1949. *
  1950. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
  1951. * The entity type.
  1952. * @param string $index
  1953. * The index column name.
  1954. *
  1955. * @return string
  1956. * The index name.
  1957. */
  1958. protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
  1959. return $entity_type->id() . '__' . $index;
  1960. }
  1961. /**
  1962. * Generates an index name for a field data table.
  1963. *
  1964. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1965. * The field storage definition.
  1966. * @param string $index
  1967. * The name of the index.
  1968. *
  1969. * @return string
  1970. * A string containing a generated index name for a field data table that is
  1971. * unique among all other fields.
  1972. */
  1973. protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
  1974. return $storage_definition->getName() . '_' . $index;
  1975. }
  1976. /**
  1977. * Checks whether a database table is non-existent or empty.
  1978. *
  1979. * Empty tables can be dropped and recreated without data loss.
  1980. *
  1981. * @param string $table_name
  1982. * The database table to check.
  1983. *
  1984. * @return bool
  1985. * TRUE if the table is empty, FALSE otherwise.
  1986. */
  1987. protected function isTableEmpty($table_name) {
  1988. return !$this->database->schema()->tableExists($table_name) ||
  1989. !$this->database->select($table_name)
  1990. ->countQuery()
  1991. ->range(0, 1)
  1992. ->execute()
  1993. ->fetchField();
  1994. }
  1995. /**
  1996. * Compares schemas to check for changes in the column definitions.
  1997. *
  1998. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
  1999. * Current field storage definition.
  2000. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
  2001. * The original field storage definition.
  2002. *
  2003. * @return bool
  2004. * Returns TRUE if there are schema changes in the column definitions.
  2005. */
  2006. protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
  2007. if ($storage_definition->getColumns() != $original->getColumns()) {
  2008. // Base field definitions have schema data stored in the original
  2009. // definition.
  2010. return TRUE;
  2011. }
  2012. if (!$storage_definition->hasCustomStorage()) {
  2013. $keys = array_flip($this->getColumnSchemaRelevantKeys());
  2014. $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
  2015. foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
  2016. foreach ($table_schema['fields'] as $name => $spec) {
  2017. $definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
  2018. $stored_spec = array_intersect_key($spec, $keys);
  2019. if ($definition_spec != $stored_spec) {
  2020. return TRUE;
  2021. }
  2022. }
  2023. }
  2024. }
  2025. return FALSE;
  2026. }
  2027. /**
  2028. * Returns a list of column schema keys affecting data storage.
  2029. *
  2030. * When comparing schema definitions, only changes in certain properties
  2031. * actually affect how data is stored and thus, if applied, may imply data
  2032. * manipulation.
  2033. *
  2034. * @return string[]
  2035. * An array of key names.
  2036. */
  2037. protected function getColumnSchemaRelevantKeys() {
  2038. return ['type', 'size', 'length', 'unsigned'];
  2039. }
  2040. /**
  2041. * Creates an index, dropping it if already existing.
  2042. *
  2043. * @param string $table
  2044. * The table name.
  2045. * @param string $name
  2046. * The index name.
  2047. * @param array $specifier
  2048. * The fields to index.
  2049. * @param array $schema
  2050. * The table specification.
  2051. *
  2052. * @see \Drupal\Core\Database\Schema::addIndex()
  2053. */
  2054. protected function addIndex($table, $name, array $specifier, array $schema) {
  2055. $schema_handler = $this->database->schema();
  2056. $schema_handler->dropIndex($table, $name);
  2057. $schema_handler->addIndex($table, $name, $specifier, $schema);
  2058. }
  2059. /**
  2060. * Creates a unique key, dropping it if already existing.
  2061. *
  2062. * @param string $table
  2063. * The table name.
  2064. * @param string $name
  2065. * The index name.
  2066. * @param array $specifier
  2067. * The unique fields.
  2068. *
  2069. * @see \Drupal\Core\Database\Schema::addUniqueKey()
  2070. */
  2071. protected function addUniqueKey($table, $name, array $specifier) {
  2072. $schema_handler = $this->database->schema();
  2073. $schema_handler->dropUniqueKey($table, $name);
  2074. $schema_handler->addUniqueKey($table, $name, $specifier);
  2075. }
  2076. }