SqlContentEntityStorageSchema.php 107 KB

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