Updated: Comment #203

This is being coded in the D8MI - Entity Translation sandbox.

Problem/Motivation

  • An entity type's base field definitions are to a large part duplicated in the providing module's hook_schema() implementation which is needed to provide the database schema for the entity type. Especially since field items provide their own schema, this has become completely unnecessary
  • Because the field definitions and the schema are completely separate systems changes to field definitions are not reflected in the schema and thus are only supported if the storage is cared for separately
  • As this has storage implications it is not easily possible to change entity types from translatable to non-translatable or vice-versa and from revisionable to non-revisionable or vice-verca
  • Even in Drupal 7 it is very cumbersome to properly declare the schema for revisionable entities which is why few contrib entity types are properly revisionable. With Drupal 8 and the notion of entity translation in core, the burden for declaring the schema for revisionable and translatable entities is even higher.

Proposed resolution

  • Introduce a SqlStorageInterface for SQL-based entity storages. It contains a getTableMapping() method which returns a TableMappingInterface value object which contains a list of database fields per table for the entity type. It also stores the relationship of the database fields to the entity field definition. Both entity field definitions which provide multiple database fields (i.e. multi-column fields) and database fields that do not correspond to entity field definitions (i.e. denormalized database fields such as 'default_langcode') are supported. Because of this complexity a value object with utility methods is chosen rather than a simple array. As the table mapping is needed during querying and saving the respective code is put directly on the storage class. The table mapping has a switch statement that returns a different table structure, depending on whether the revisionable or not, translatable or not, or any combination of that. This will allow for easier refactoring once we split ContentEntityDatabaseStorage into different classes for the different types.
  • Introduce a ContentEntitySchemaHandlerInterface that uses the entity type, the entity type's field definitions (and their respective schema arrays) and the table mapping (see above) to generate the full schema array for all of the entity type's schema in a getSchema() method. As this code is only used during module install (currently) this code lives in a separate class, the ContentEntitySchemaHandler. To hide this code organization detail from the outside ContentEntityDatabaseStorage implements ContentEntitySchemaHandlerInterface. The getSchema() method of ContentEntityDatabaseStorage simply forwards the call to ContentEntitySchemaHandler, which it lazy-instantiates directly (i.e. using the class name directly). While using direct instantiation in a class is generally discouraged neither of the 3 methods we have for swappability in core are applicable here:
    • We cannot make it a service as the schema handler needs the entity type injected. Therefore we would need per-(content)-entity-type services, but this is not possible as you cannot call EntityManagerInterface::getDefinitions() during container building
    • We could make it a top-level entity controller/handler but the whole concept of schema is bound to the specific implementation of the storage. I.e. there might be storages which do not need a schema at all and, thus, it would be semantically incorrect to expose this as part of the API
    • The entity query factory factory pattern was invented to allow for this sort of easy swapping per entity type. Following that pattern we could introduce a getSchemaHandlerServiceName() method on SqlStorageInterface. Then we could use the storage to figure out which schema handler to use. But since the storage needs the schema handler (constructor) injected there is a chicken and egg problem: Between the storage and the schema handler each one needs the other to be instantiated. This could be solved by using setter injection but we don't really have a way to use setter injection for entity controllers/handlers.

    The fact that we have this separation is quite neat as entity types do not actually have to override the schema handler but can just provide additional schema tables that are not (in part not yet) auto-generated in the storage class.

  • Entity queries are updated to use the table mapping instead of drupal_get_schema()
  • Entity saves and loads are updated to use the table mapping instead of drupal_get_schema() and drupal_write_record()
  • Because entity type tables are no longer registered in hook_schema() DrupalUnitTestBase::installSchema() fails for entity type tables. Therefore a DrupalUnitTestBase::installEntitySchema() is introduced.

Remaining tasks

  • More reviews?

User interface changes

None.

API changes

  • Modules should/can no longer declare their entity type schema in hook_schema(). Instead the schema is generated automatically for them. In case they want to specify additional schema information (denormalization tables, indexes, ...) they need to provide a custom storage class and override getSchema(). If they do want to avoid this behavior for some reason they can provide a storage controller which does not implement ContentEntitySchemaHandlerInterface
  • DrupalUnitTestBase::installSchema() no longer works for entity tables.

API additions

  • Introduce a TableMappingInterface
  • Introduce a SqlStorageInterface
  • Introduce a ContentEntitySchemaHandlerInterface
  • Introduce a DrupalUnitTestBase::installEntitySchema()

Follow-ups

Follow-ups needed if/when this is committed:

Blockers of this issue

-

Spin off to separate issues

Good ideas of separate parts discussed or found during this issue, but not blocking this, and could be done without committing this (not follow-ups)

CommentFileSizeAuthor
#424 postresql-schema.patch1.32 KBtstoeckler
#415 d8_entity_schema.patch301.55 KBfago
#415 d8_entity_schema.interdiff.txt1.33 KBfago
#412 d8_entity_schema.interdiff.txt21.51 KBfago
#412 d8_entity_schema.patch321.57 KBfago
#411 d8_entity_schema.interdiff.txt4.25 KBfago
#411 d8_entity_schema.patch301.56 KBfago
#411 d8_entity_schema.patch301.71 KBfago
#411 d8_entity_schema.interdiff.txt4.12 KBfago
#409 d8_entity_schema.interdiff.txt1.12 KBfago
#409 d8_entity_schema.patch299.11 KBfago
#402 entity-schema-2183231-402.patch297.99 KBxjm
entity-schema-2183231-400.patch7.62 KBxjm
#392 entity-schema-auto-2183231-392.patch297.96 KBplach
#391 entity-schema-auto-2183231-390.patch303.26 KBplach
#390 entity-schema-auto-2183231-390.patch303.26 KBplach
#390 entity-schema-auto-2183231-390.interdiff.txt5.16 KBplach
#389 entity-schema-auto-2183231-389-psr4.patch305.95 KBjessebeach
#386 dumpfile-PATCH.sql_.txt2.9 MBjessebeach
#386 dumpfile-HEAD.sql_.txt2.91 MBjessebeach
#384 interdiff-internally.txt1.2 KBxjm
#384 entity-2183231-384.patch302.48 KBxjm
#378 d8_entity_schema.interdiff.txt3.28 KBfago
#378 d8_entity_schema.patch303.53 KBfago
#377 d8_entity_schema.patch303.5 KBfago
#377 d8_entity_schema.interdiff.txt10.82 KBfago
#371 entity-schema-auto-2183231-371.interdiff.txt15.7 KBplach
#371 entity-schema-auto-2183231-371.patch303.21 KBplach
#370 entity-schema-auto-2183231-369.interdiff.txt688 bytesplach
#369 entity-schema-auto-2183231-369.patch302.96 KBplach
#362 interdiff.txt2.71 KBjessebeach
#362 entity-schema-auto-2183231-362.patch302.64 KBjessebeach
#358 d8_entity_schema_interdiff.txt2.42 KBfago
#358 d8_entity_schema.patch300.76 KBfago
#353 shortcut.png203.53 KBjessebeach
#347 entity-schema-auto-2183231-345.patch300.21 KBjessebeach
#347 interdiff.txt4.64 KBjessebeach
#343 interdiff.txt6.28 KBjessebeach
#343 entity-schema-auto-2183231-343.patch297.46 KBjessebeach
#336 interdiff.txt633 bytesjessebeach
#336 entity-schema-auto-2183231-336.patch302.23 KBjessebeach
#334 2183231-234-entity-schema-auto.patch302.29 KBtstoeckler
#334 2183231-333-334-interdiff.txt10.76 KBtstoeckler
#333 entity-schema-auto-2183231-333.patch300.59 KBplach
#333 entity-schema-auto-2183231-333.interdiff.txt1.53 KBplach
#330 interdiff.txt635 bytesjessebeach
#330 entity-schema-auto-2183231-330.patch300.59 KBjessebeach
#328 entity-schema-auto-2183231-328.patch300.49 KBjessebeach
#328 interdiff.txt1.77 KBjessebeach
#323 entity-schema-auto-2183231-323.patch299.79 KBBerdir
#320 interdiff.txt5.53 KBjessebeach
#320 entity-schema-auto-2183231-320.patch304.67 KBjessebeach
#319 bigint.png319.37 KBjessebeach
#317 dumpfile-PATCH.sql_.txt142.2 KBjessebeach
#317 dumpfile-HEAD.sql_.txt143.81 KBjessebeach
#308 interdiff.txt911 bytesjessebeach
#308 interdiff-isRequired.txt669 bytesjessebeach
#308 entity-schema-auto-2183231-308-isRequired-do-not-test.patch304.86 KBjessebeach
#308 entity-schema-auto-2183231-308.patch304.21 KBjessebeach
#300 interdiff.txt4.25 KBjessebeach
#300 entity-schema-auto-2183231-300.patch304.05 KBjessebeach
#298 entity-schema-auto-2183231-296.patch302.92 KBjessebeach
#298 interdiff.txt1.21 KBjessebeach
#294 interdiff.txt5.68 KBjessebeach
#294 entity-schema-auto-2183231-293.patch302.88 KBjessebeach
#292 entity-schema-auto-2183231-292.patch302.66 KBjessebeach
#292 interdiff.txt1.28 KBjessebeach
#290 2183231-290-entity-schema-auto.patch302.26 KBtstoeckler
#290 2183231-288-290-interdiff.txt684 byteststoeckler
#288 2183231-288-entity-schema-auto.patch301.93 KBtstoeckler
#288 2183231-286-288-interdiff.txt655 byteststoeckler
#286 2183231-286-entity-schema-auto.patch301.98 KBtstoeckler
#283 interdiff.txt3.74 KBjessebeach
#283 entity-schema-auto-2183231-282.patch302.79 KBjessebeach
#277 dumpfile-PATCH.sql_.txt143.37 KBjessebeach
#277 dumpfile-HEAD.sql_.txt145.08 KBjessebeach
#273 2183231-273-entity-schema-auto.patch299.41 KBtstoeckler
#271 2183231-271-entity-schema-auto.patch299.73 KBtstoeckler
#271 2183231-269-271-interdiff.txt18.18 KBtstoeckler
#269 2183231-269-entity-schema-auto.patch285.41 KBtstoeckler
#269 2183231-266-269-interdiff.txt21.64 KBtstoeckler
#266 2183231-266-entity-schema-auto.patch276.88 KBtstoeckler
#266 2183231-260-266-interdiff.txt39.31 KBtstoeckler
#260 2183231-260-entity-schema-auto-interdiff.txt2.54 KBBerdir
#260 2183231-260-entity-schema-auto.patch263.2 KBBerdir
#257 2183231-257-entity-schema-auto.patch262.05 KBtstoeckler
#257 2183231-255-257-interdiff.txt1.29 KBtstoeckler
#255 2183231-255-entity-schema-auto.patch261.94 KBtstoeckler
#255 2183231-254-255-interdiff.txt6.76 KBtstoeckler
#254 2183231-254-entity-schema-auto.patch261.56 KBtstoeckler
#254 2183231-252-254-interdiff.txt5.85 KBtstoeckler
#252 2183231-252-entity-schema-auto.patch261.54 KBtstoeckler
#252 2183231-250-252-interdiff.txt4.97 KBtstoeckler
#250 2183231-249-entity-schema-auto.patch261 KBtstoeckler
#250 2183231-244-248-interdiff-4.txt27.76 KBtstoeckler
#250 2183231-244-248-interdiff-3.txt660 byteststoeckler
#250 2183231-244-248-interdiff-2.txt12.96 KBtstoeckler
#250 2183231-244-248-interdiff-1.txt6.1 KBtstoeckler
#244 2183231-244-entity-schema-auto.patch250.8 KBtstoeckler
#244 2183231-243-244-interdiff.txt28.66 KBtstoeckler
#243 2183231-243-entity-schema-auto.patch236.83 KBtstoeckler
#243 2183231-239-243-interdiff.txt822 byteststoeckler
#239 2183231-239-entity-schema-auto.patch236.86 KBtstoeckler
#239 2183231-238-239-interdiff.txt26.73 KBtstoeckler
#238 2183231-238-entity-schema-auto.patch243.26 KBtstoeckler
#238 2183231-234-238-interdiff.txt3.9 KBtstoeckler
#234 2183231-234-entity-schema-auto.patch242.33 KBtstoeckler
#234 2183231-232-234-interdiff.txt2.33 KBtstoeckler
#232 2183231-232-entity-schema-auto.patch242.14 KBtstoeckler
#232 2183231-231-232-interdiff.txt22.55 KBtstoeckler
#231 2183231-230-entity-schema-auto.patch237.16 KBtstoeckler
#231 2183231-228-230-interdiff.txt944 byteststoeckler
#228 2183231-228-entity-schema-auto.patch236.24 KBtstoeckler
#228 2183231-226-228-interdiff.txt1.99 KBtstoeckler
#226 entity-schema_auto-2183231-226.patch236.19 KBplach
#226 entity-schema_auto-2183231-226.interdiff.txt1.34 KBplach
#222 2183231-222-entity-schema-auto.patch236.04 KBtstoeckler
#222 2183231-218-222-interdiff.txt506 byteststoeckler
#218 2183231-218-entity-schema-auto.patch236.35 KBtstoeckler
#218 2183231-212-218-interdiff.txt35.91 KBtstoeckler
#212 2183231-212-entity-schema-auto.patch245.49 KBtstoeckler
#211 2183231-211-entity-schema-auto.patch338.05 KBtstoeckler
#211 2183231-210-211-interdiff--remove-alter-hook.patch6.09 KBtstoeckler
#211 2183231-210-211-interdiff--try-catch.patch17.57 KBtstoeckler
#210 2183231-210-entity-schema-auto.patch244.08 KBtstoeckler
#210 2183231-208-210-interdiff.txt748 byteststoeckler
#208 2183231-208-entity-schema-auto.patch243.35 KBtstoeckler
#208 2183231-206-208-interdiff-2.txt3.38 KBtstoeckler
#208 2183231-206-208-interdiff.txt7.81 KBtstoeckler
#206 entity-schema_auto-2183231-206.interdiff.txt6.05 KBplach
#206 entity-schema_auto-2183231-206.patch235.35 KBplach
#202 2183231-200-entity-schema-auto.patch237.55 KBtstoeckler
#202 2183231-196-200-interdiff-2.txt7.08 KBtstoeckler
#202 2183231-196-200-interdiff-1.txt54.75 KBtstoeckler
#196 entity-schema_auto-2183231-196.patch233.75 KBplach
#196 entity-schema_auto-2183231-196.interdiff.txt7.46 KBplach
#192 entity-schema_auto-2183231-191.patch232.6 KBplach
#191 Entity_Schema_getSchema.png116.48 KBjessebeach
#190 Entity_Schema_2183231_1.png55.62 KBjessebeach
#186 Entity_Schema_2183231.png53.52 KBjessebeach
#186 Entity_Schema_HEAD.png22.28 KBjessebeach
#185 Entity Schema - HEAD.png22.28 KBjessebeach
#185 Entity Schema - 2183231 (1).png53.52 KBjessebeach
#176 2183231-176-entity-schema-auto.patch230.43 KBtstoeckler
#174 2183231-174-entity-schema-auto.patch236.34 KBtstoeckler
#174 2183231-171-174-interdiff.txt1.9 KBtstoeckler
#171 2183231-167-entity-schema-auto.patch237.24 KBtstoeckler
#171 2183231-165-167-interdiff.txt7.54 KBtstoeckler
#165 entity-schema_auto-2183231-165.patch229.27 KBplach
#165 entity-schema_auto-2183231-165.interdiff.txt22.8 KBplach
#163 entity-schema_auto-2183231-163.patch244.97 KBplach
#163 entity-schema_auto-2183231-163.interdiff.txt928 bytesplach
#161 entity-schema_auto-2183231-161.patch244.98 KBplach
#159 entity-schema_auto-2183231-159.patch244.24 KBplach
#159 entity-schema_auto-2183231-159.interdiff.txt737 bytesplach
#157 entity-schema_auto-2183231-157.patch244.04 KBplach
#157 entity-schema_auto-2183231-157.interdiff.txt4.21 KBplach
#153 entity-schema-auto-2183231-153.patch243.05 KBplach
#151 entity-schema_auto-2183231-151.patch242.81 KBplach
#151 entity-schema_auto-2183231-151.interdiff.txt1.08 KBplach
#149 entity-schema_auto-2183231-149.patch242.74 KBplach
#149 entity-schema_auto-2183231-149.interdiff.txt1.56 KBplach
#147 entity-schema_auto-2183231-147.patch240.59 KBplach
#147 entity-schema_auto-2183231-147.interdiff.txt2.97 KBplach
#146 entity-schema_auto-2183231-146.patch244.43 KBplach
#89 2183231-87-entity-schema-auto.patch203.35 KBtstoeckler
#87 2183231-87-entity-schema-auto.patch203.35 KBtstoeckler
#87 2183231-85-87-interdiff.txt7 KBtstoeckler
#85 2183231-85-entity-schema-auto.patch198.13 KBtstoeckler
#85 2183231-79-85-interdiff.txt29.3 KBtstoeckler
#84 2183231-interdiff.txt9.75 KBtstoeckler
#79 2183231-79-entity-schema-auto.patch183.97 KBtstoeckler
#79 2183231-72-79-interdiff.txt18.58 KBtstoeckler
#72 entity-schema-auto-2183231-72.patch168.12 KBplach
#72 entity-schema-auto-2183231-72.interdiff.txt84.79 KBplach
#70 entity-schema-auto-2183231-70.patch127.82 KBplach
#68 entity-schema-auto-2183231-68.patch127.78 KBplach
#68 entity-schema-auto-2183231-68.interdiff.txt36.35 KBplach
#67 entity-schema-auto-2183231-67.patch98.53 KBplach
#67 entity-schema-auto-2183231-67.interdiff.txt765 bytesplach
#65 entity-schema-auto-2183231-65.interdiff.txt12.92 KBplach
#65 entity-schema-auto-2183231-65.patch98.52 KBplach
#65 entity-schema-auto-2183231-65.fix_.patch94.87 KBplach
#63 entity-schema-auto-2183231-63.interdiff.txt905 bytesplach
#63 entity-schema-auto-2183231-63.patch94.8 KBplach
#61 entity-schema-auto-2183231-61.interdiff.txt2.15 KBplach
#61 entity-schema-auto-2183231-61.patch94.74 KBplach
#59 entity-schema-auto-2183231-59.interdiff.txt2.48 KBplach
#59 entity-schema-auto-2183231-59.patch94.43 KBplach
#54 entity-schema-auto-2183231-54.patch94.63 KBplach
#54 entity-schema-auto-2183231-54.interdiff.txt11.35 KBplach
#50 entity-schema-auto-2183231-50.patch96.84 KBplach
#50 entity-schema-auto-2183231-50.interdiff.txt4.28 KBplach
#49 entity-schema-auto-2183231-49.patch100.78 KBplach
#49 entity-schema-auto-2183231-49.interdiff.txt23.74 KBplach
#46 entity-schema-auto-2183231-46.patch92.06 KBplach
#45 entity-schema-auto-2183231-45.patch92.13 KBplach
#45 entity-schema-auto-2183231-45.interdiff.txt32.49 KBplach
#44 entity-schema-auto-2183231-44.patch94.07 KBplach
#44 entity-schema-auto-2183231-44.interdiff.txt1.87 KBplach
#42 entity-schema-auto-2183231-42.patch92.78 KBplach
#42 entity-schema-auto-2183231-42.interdiff.txt19.09 KBplach
#39 entity-schema-auto-2183231-39.patch92.85 KBplach
#39 entity-schema-auto-2183231-39.interdiff.txt11.5 KBplach
#37 2183231-36-entity-schema-auto-INTERDIFF.txt5.46 KBmauzeh
#36 2183231-36-entity-schema-auto.patch81.98 KBmauzeh
#34 2183231-34-entity-schema-auto.patch78.61 KBplach
#27 2183231-27-entity-schema-auto.patch80.18 KBtstoeckler
#27 2183231-25-27-interdiff.txt4.25 KBtstoeckler
#25 2183231-25-entity-schema-auto.patch81.24 KBtstoeckler
#25 2183231-23-25-interdiff.txt6.42 KBtstoeckler
#23 2183231-23-entity-schema-auto.patch83.26 KBtstoeckler
#21 2183231-21-entity-schema-auto.patch75.82 KBtstoeckler
#21 2183231-17-21-interdiff.txt1.96 KBtstoeckler
#17 2183231-16-entity-schema-auto.patch75.35 KBtstoeckler
#17 2183231-13-16-interdiff-2.txt5.24 KBtstoeckler
#17 2183231-13-16-interdiff-1.txt23.27 KBtstoeckler
#13 2183231-12-entity-schema-auto.patch57.39 KBtstoeckler
#13 2183231-10-12-interdiff-2.txt32.88 KBtstoeckler
#13 2183231-10-12-interdiff-1.txt41.87 KBtstoeckler
#10 2183231-10-entity-schema-auto.patch37.6 KBtstoeckler
#8 2183231-8-entity-schema-auto.patch37.64 KBtstoeckler
#8 2183231-6-8-interdiff.txt895 byteststoeckler
#6 2183231-6-entity-schema-auto.patch37.58 KBtstoeckler
#6 2183231-4-6-interdiff.txt15.16 KBtstoeckler
#4 2183231-4-entity-schema-auto.patch39.8 KBtstoeckler
#4 2183231-3-4-interdiff.txt42.47 KBtstoeckler
#3 2183231-3-entity-schema-auto.patch13.71 KBtstoeckler
#3 schema.diff.txt8.4 KBtstoeckler
#3 diff-script.php_.txt2.21 KBtstoeckler
#1 2183231-1-schema.diff.txt0 byteststoeckler
#1 2183231-1-schema-changes.patch.txt27.66 KBtstoeckler
#1 2183231-1-diff-script.php_.txt1.64 KBtstoeckler
#1 2183231-1-get-schema.patch3.51 KBtstoeckler
#94 2183231-94-entity-schema-auto.patch205.94 KBBerdir
#94 2183231-94-entity-schema-auto-interdiff.txt12.9 KBBerdir
#96 2183231-96-entity-schema-auto.patch214.24 KBBerdir
#96 2183231-96-entity-schema-auto-interdiff.txt8.9 KBBerdir
#99 entity-schema-auto-2183231-99.patch0 bytesplach
#101 entity-schema-auto-2183231-101.patch214.5 KBplach
#103 entity-schema-auto-2183231-103.interdiff.txt1.3 KBplach
#103 entity-schema-auto-2183231-103.patch215.55 KBplach
#107 entity-schema-auto-2183231-107.interdiff.txt5.11 KBplach
#107 entity-schema-auto-2183231-107.patch215.97 KBplach
#109 2183231-107-109-interdiff-1.txt6.95 KBtstoeckler
#109 2183231-107-109-interdiff-2.txt2.68 KBtstoeckler
#109 2183231-109-entity-schema-auto.patch224.04 KBtstoeckler
#112 2183231-109-112-interdiff.txt31.34 KBtstoeckler
#112 2183231-112-entity-schema-auto.patch244.88 KBtstoeckler
#118 2183231-112-118-interdiff-1.txt21.43 KBtstoeckler
#118 2183231-112-118-interdiff-2.txt822 byteststoeckler
#118 2183231-118-entity-schema-auto.patch263.66 KBtstoeckler
#120 2183231-118-120-interdiff.txt2.09 KBtstoeckler
#120 2183231-120-entity-schema-auto.patch264.13 KBtstoeckler
#124 2183231-120-124-interdiff.txt650 byteststoeckler
#124 2183231-124-entity-schema-auto.patch264.76 KBtstoeckler
#127 2183231-124-127-interdiff.txt531 byteststoeckler
#127 2183231-124-127-interdiff-2.txt2.46 KBtstoeckler
#127 2183231-127-entity-schema-auto.patch241.07 KBtstoeckler
#128 2183231-128-entity-schema-auto.patch240.99 KBtstoeckler
#129 2183231-127-129-interdiff.txt836 byteststoeckler
#129 2183231-129-entity-schema-auto.patch240.91 KBtstoeckler
#132 2183231-132-entity-schema-auto.patch240.92 KBtstoeckler
#133 entity-schema_auto-2183231-133.patch241.11 KBplach
#136 entity-schema_auto-2183231-136.interdiff.txt13.68 KBplach
#136 entity-schema_auto-2183231-136.patch240.39 KBplach
#138 entity-schema_auto-2183231-138.interdiff.txt1.35 KBplach
#138 entity-schema_auto-2183231-138.patch240.39 KBplach
#139 entity-schema_auto-2183231-139.interdiff.txt6.11 KBplach
#139 entity-schema_auto-2183231-139.patch244.67 KBplach
#144 entity-schema_auto-2183231-144.patch244.28 KBplach
#144 entity-schema_auto-2183231-144.interdiff.txt400 bytesplach
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tstoeckler’s picture

So I started some work on this recently.

I used the following approach: I added a getSchema() method to FieldableDatabaseStorageController. I started by simply looping over baseFieldDefinitions() of the entity type and putting that together. I then diffed the generated schema with the current schema in hook_schema(). That made me notice a number of missing pieces in the entity field API which is why I opened the following issues:

The current status is a WIP and more of an initial stab at this. @plach's comment in #1498720-34: [meta] Make the entity storage system handle changes in the entity and field schema definitions lead to post this now, though, rather than later in order not to duplicate any efforts.

I'm providing the following files:

  • the *-gets-schema.patch is the actual getSchema() method in FieldableDatabaseStorageController
  • the *-diff-script.php is the script I used to diff the two schemas
  • the *-schema.diff is the output of the above script
  • the *-schema-changes.patch is a bunch of minor changes to the existing hook_schema() implementations to avoid inconsistencies and reduce the size of the diff generated by the script

Note that I'm not at all bent on following this approach any further it just proved useful to me in finding out how our schemas are currently structured.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
2.21 KB
8.4 KB
13.71 KB

Here's a new patch. With this patch I am able to properly generate the entire schema of the various entity_test_* entity types. I chose entity_test specifically so we can proceed quicker on this, as there are some minor issues that remain: So far I have ignored a bunch of non-critical schema keys completely: 'description', 'default', 'not null', 'size', 'unsigned'. The first ones are actually not very hard to fix but the first ones are very inconsistent between our current hook_schema() and our field definitions. So we should figure that out in separate issues, but hopefully proceed here nonetheless.

Again, attaching the (updated) diff script that I used to diff the old and the new schema and the actual diff output.
A couple of notes for that diff output:
1. While a bunch of things are being added in the diff, they all make sense and show incompleteness and deficiencies in the current entity_test schema.

2. In order to get that diff output I had to cheat a bit and apply the following patches:
* #2209071: StringItem should cast the 'length' schema value to (int)
* #2209049: Allow field schemas to contain unique keys

3. Do not turn on entity_reference module when testing this, as that will mess with the schema of entity reference fields. Will open an issue for that as well.

The patch (mostly) contains the FieldableDatabaseStorageController::getSchema() method.

The next step would be actually using that to install the schema. That plays into #1498720: [meta] Make the entity storage system handle changes in the entity and field schema definitions.

But this could now seriously use some reviews!

tstoeckler’s picture

FileSize
42.47 KB
39.8 KB

Opened #2209981: ConfigurableEntityReferenceItem should not mess with the field schema for the problem mentioned above.

New patch, which actually *uses* the automated schema for the entity_test entity types. It contains a couple hacks that we'll eventually want to remove, notably:
* Manually installing the schema in hook_install()
* Manually uninstalling the schema in hook_uninstall()
* Allow to pass in the schema into drupal_write_record().
But this gets us going.

This also reveals a problem, that I've noticed before but ignored so far: Schema defaults. Those are currently almost consistently missing from the field item schemas. Will open an issue to discuss that.

Let's see how this goes.

Because I intended the whole getSchema() function due to the static caching, I used -w for the interdiff, so that won't apply.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
15.16 KB
37.58 KB

So I apparently my logic which field gets saved into which table was wrong: Only translatable fields end up in the data table(s). I was also trying to save the values of computed fields so far, which wasn't working out very well. The former issue led to rewrite most of the getSchema() method, but that makes it much more readable now. It still needs inline docs (lacking almost entirely now), but it looks OK, to my mind. I can install with this at least.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
895 bytes
37.64 KB

Crossing my fingers that that was the only problem...

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
37.6 KB

Hmm... must have been a context change, no merge conflicts.

xjm’s picture

tstoeckler’s picture

Status: Needs work » Needs review
Issue tags: +drupaldevdays
FileSize
41.87 KB
32.88 KB
57.39 KB

OK, discussed this with @plach, @fago and @Berdir today at Drupal Dev Days. Here's a new patch that implements the EntitySchemaBuilder that @plach proposed in #1498720: [meta] Make the entity storage system handle changes in the entity and field schema definitions. As discussed I added this as a separate service.

We also discussed making it a top-level controller but decided against this because this would leak the concept of 'schema' into the top-level API, while it is really a very storage-related concepts that might or might not apply depending on the storage controller. The fact that we have to pass around $entity_type, however, into every single method of that class, really feels like we should have per-entity-type instances for this. That would allow to pass in the entity type in the constructor and then just reference it everywhere instead of passing it around. Not sure.

This also includes a temporary shiv to fall back to hook_schema() implementations if they still exist. That's because e.g. Node's schema does not at all comply with its field definitions. That depends at least on #2111887: Regression: Only title (of base fields) on nodes is marked as translatable but there might be similar things for other entity types. Anyway, getting this patch green would allow us to progress quicker here, and then figuring out the rest of the entity types in parallel.

So this patch (just like the previous ones) just removes entity_test_schema(). Let's see. I can save nodes successfully at least...

Providing two (consecutive) interdiffs, but it's pretty much a full rewrite, so it might make more sense to review the patch in its entirety.

tstoeckler’s picture

The reason this fails install is that by ditching drupal_write_record() we lost the auto-serializing support for fields that have 'serialize' => TRUE in their schema. Shortcut entities have the 'route_parameters' field which is an array, so when saving that things blow up. So we somehow need to bring that back.

Also I forgot the actual installation of the dynamic schema.... *slapsforehead* thanks @plach for pointing that out.

plach’s picture

This is already looking very good, +1 for per-entity-type services. Aside from that:

  1. +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php
    @@ -113,6 +145,11 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
    +    if ($base_table = $this->entityType->getBaseTable()) {
    
    @@ -120,12 +157,14 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
    +      if ($this->revisionTable) {
    

    I think we should remove tables from the entity type definition: they don't belong there, they are a detail that matters only for the storage controller. The table name logic should be hard-coded (aside from allowing temporary table names). We may want to introduce a different key to specify the entity is not stored, but I guess it wouldn't specify a storage controller then.

  2. +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php
    @@ -120,12 +157,14 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
    +    // @todo Replace this with a check for the 'language' entity key when
    +    //   https://drupal.org/node/2143729 has landed.
    

    Ok, but see above.

  3. +++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaBuilder.php
    @@ -0,0 +1,383 @@
    +          if (FALSE !== $key = array_search($field_name, $entity_keys)) {
    

    Please no, it took me 30 seconds to parse this :)

  4. +++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaBuilder.php
    @@ -0,0 +1,383 @@
    +  protected function getTables(ContentEntityType $entity_type) {
    

    As I was saying above I think we should hard-code these.

  5. +++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaBuilderInterface.php
    @@ -0,0 +1,47 @@
    +/**
    + * Created by PhpStorm.
    
    +++ b/core/lib/Drupal/Core/Entity/SchemaStorageControllerInterface.php
    @@ -0,0 +1,22 @@
    + * Created by PhpStorm.
    

    PhpStuff :)

  6. +++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaBuilderInterface.php
    @@ -0,0 +1,47 @@
    +  public function getAllSchema(ContentEntityType $entity_type, array $field_definitions);
    +
    ...
    +  public function getTableSchema(ContentEntityType $entity_type, array $field_definitions, $table_key);
    
    +++ b/core/lib/Drupal/Core/Entity/SchemaStorageControllerInterface.php
    @@ -0,0 +1,22 @@
    +  *   A schema array for all of the entity type's tables.
    

    Since this is a generic API would it make sense to avoid the "table" word? I guess the returned array shouldn't be required to be a valid Schema API array. This would be the case only for controllers implementing the SqlStorageInterface. Maybe just implementing on getSchema() and just returning the full array?

  7. +++ b/core/modules/system/tests/modules/entity_test/entity_test.install
    @@ -9,433 +9,34 @@
    -function entity_test_schema() {
    

    Recreating the schema would help making entity tests pass :)

  8. +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php
    @@ -134,7 +134,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setSetting('max_length', 32);
    

    Why is this needed?

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
23.27 KB
5.24 KB
75.35 KB

Following changes:

- Fixed the serialization problem:
- This forced me to remove one very strange line from Shortcut::getRouteParams(), which I do not understand how it currently works anyway, but...

- Added automatic installation and uninstallation to ModuleHandler::instal()/uninstall()

- In order to fix some of the tests (or attempt that, at least) added a DrupalUnitTestBase::installEntitySchema() as DUTB::installSchema() doesn't work with auto-generated entity tables anymore.

- Fix some weirdnesses in the entity API tests. Most notably they are working with a field that's completely undefined, i.e. no base field definition?! I must be missing something...

- Renamed SchemaStorageControllerInterface to SqlStorageControllerInterface per #2079019: Make Views use SqlEntityStorageInterface. We should postpone and repurpose that issue, I think.

Regarding the review above:
- Fixed 3. I changed it to a in_array() + array_search() combination which is one function call more, but should be more readable I think.
- Fixed 5.
- 7. should be fixed per the above.
- 8. is "needed" to make the auto-generated schema match the current one. We can of course remove that, if wanted.

Still @todo.
- Fix the 1000s of tests that will probably fail. (But hopefully this will install now...)
- Investigate using per entity type services.
- Investigate hardcoding the entity tables in the storage controller. (1., 2., 4., of the review above). I agree with this point but I'm not yet sure how big the scope of that change will be.

plach’s picture

If it turns out that removing table keys from entity type annotation is too big of a change, probably it make sense to defer that to a follow-up.

plach’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php
    @@ -305,12 +309,16 @@ protected function attachPropertyData(array &$entities) {
    +        $data_column_names = array_flip(array_diff(array_keys($data_revision_schema['fields']), array_keys($base_schema['fields'])));
    

    Just a reminder: we should investigate whether we need an actual schema array or a lighter data-structure is enough. Maybe we can add a @todo?

  2. +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php
    @@ -442,11 +450,14 @@ protected function buildQuery($ids, $revision_id = FALSE) {
    +    $base_schema = $this->schemaBuilder->getTableSchema($this->entityType, $field_definitions, 'base_table');
    

    I guess per-entity-type services is the way: we should pass this information just once. @Crell was pointing out we should not require storage controller to have a schem builder definied, though.

  3. +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php
    @@ -726,7 +737,12 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_key
    +      $serialize = !empty($definitions[$name]->getColumns()[$definitions[$name]->getMainPropertyName()]['serialize']);
    

    Should we consider a follow-up to add a serialize() method (actual name TBD) to the field item class? Regular items would just return their value while valuess needing to be serialized would get an automatic serialize() call.

  4. +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
    @@ -662,6 +665,25 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
    +        // Install entity type tables.
    

    I don't think we should bake this logic into the module handler, it's a new dependency I don't think we *need* to introduce. What about implementing entity_modules_installed()?

  5. +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityApiTest.php
    @@ -25,16 +25,10 @@ public static function getInfo() {
    +    $this->installEntitySchema('entity_test_mul');
    +    $this->installEntitySchema('entity_test_mulrev');
    

    This is really a cool change :)

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
1.96 KB
75.82 KB

Ohh, I will (not) miss you drupal_write_record(). You had some nifty features, however...

Let's see if we can get some useful test output this time.

This does not address #21 yet. Thanks for the review, though! Very much appreciated.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
83.26 KB

This fixes 1.,2. and 4. from above. This now does not actually use per-entity-type services, as you cannot call EntityManager::getDefinitions() during container building as that will blow up. I then explored implementing a factory-factory logic similar to entity query, so that each storage controller can specify the service name of its schema builder. That however did not work because it introduced a circular dependency issue:
- In order to instantiate the schema builder you need to call the storage to call the getSchemaBuilderServiceName() method.
- In order to instantiate the storage controller, you need the schema builder, as that gets injected.

So this now just introduces a schemaBuilder() method which manually instantiates the schema builder on demand. It's not as nice architecturally, but it's overridable and its lazy-load and it works :-)
Injecting the entity type greatly simplifies the logic in EntitySchemaBuilder. @plach you were right from the beginning that it's better this way, I just needed to see this for myself :-)

I like 3. But let's do that in a follow-up.

Sorry, no interdiff as I fucked my branch up locally. It's pretty much a rewrite anyway, though.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
6.42 KB
81.24 KB

Ahh, the revision issue conflicted with this. HEAD moves fast this week.... :-)

Also removed some bogus field definitions that I had added. @plach said (correctly) that those don't make sense.

And I changed EntitySchemaBuilder to account for $field_definition->hasCustomStorage().

Let's see.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
4.25 KB
80.18 KB

So in my many refactorings I seemingly removed a necesseray check which was causing the fatal.

Also removes a bunch of leftover stuff that I saw in self-reviewing the patch above.

*crossingfingers*

jessebeach’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilderInterface.php
    @@ -0,0 +1,41 @@
    +/**
    + * Created by PhpStorm.
    + * User: tobias
    + * Date: 24.03.14
    + * Time: 14:23
    + */
    

    Some boilerplate generation spillover.

  2. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilderInterface.php
    @@ -0,0 +1,41 @@
    +  /**
    +   * Gets the full schema array for a given entity type.
    +   *
    +   * @param \Drupal\Core\Entity\ContentEntityType $entity_type
    +   *   The entity type to return the schema for.
    +   * @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
    +   *   The field definitions of the entity type, keyed by field name.
    +   *
    +   * @return array
    +   *   A schema array for the entity type's tables.
    +   */
    +  public function getSchema();
    

    Params need to be included in the function signature and given default values. Objects should be identified by Interface. There's a use statement missing for \Drupal\Core\Field\FieldDefinitionInterface

  3. +++ b/core/lib/Drupal/Core/Entity/SqlStorageControllerInterface.php
    @@ -0,0 +1,15 @@
    +/**
    + * @file
    + * Contains \Drupal\Core\Entity\SchemaStorageInterface.
    + */
    +
    +namespace Drupal\Core\Entity;
    +
    +use Drupal\Core\Entity\Schema\SchemaBuilderInterface;
    +
    +/**
    + * A common interface for SQL-based storage controllers.
    + */
    +interface SqlStorageControllerInterface extends EntityStorageControllerInterface, SchemaBuilderInterface {
    +}
    

    The @file info is wrong here.

Going to do some manual testing now.

plach’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php
    @@ -8,7 +8,10 @@
    +use Drupal\Core\Entity\Schema\SchemaBuilder;
    +use Drupal\Core\Entity\Schema\SchemaBuilderInterface;
     use Drupal\Core\Language\Language;
    

    Can we rename those ContentEntitySchemaHandler(Interface)? I just realized that schema builder is the name I wanted to give to the service actually triggering (re)builds (see #1498720-48: [meta] Make the entity storage system handle changes in the entity and field schema definitions). The prefix Content is to communicate the fact we are dealing with content entities.

  2. +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php
    @@ -247,6 +297,8 @@ protected function mapFromStorageRecords(array $records) {
    +      $table_mapping = $this->getTableMapping();
    
    @@ -391,6 +444,8 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
    +    $table_mapping = $this->getTableMapping();
    

    Can we move these close to the lines where we are actually using it?

  3. +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php
    @@ -657,41 +723,64 @@ protected function savePropertyData(EntityInterface $entity, $table_key = 'data_
    +        // Since this is not a multi-column field we can assume there is only one
    

    80 chars

  4. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +
    

    bogus blank line

  5. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +   * Constructs an SchemaBuilder.
    

    "a SchemaBuilder"

    I think :)

  6. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +      elseif ($base_schema = drupal_get_schema($tables['base_table'])) {
    

    Please let's add a comment here: every time I see drupal_get_schema() I think "WTF?!" :)

  7. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +        // @todo This will become unnecessary once https://drupal.org/node/2143729
    ...
    +        // $tables and $entity_keys instead of building the array manually avoids
    ...
    +          // 'default_language' on the data table and the revision data table, if
    

    80 chars

  8. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +        $other_definitions = array_diff_key($storable_definitions, array_flip($entity_keys) + array('langcode' => TRUE));
    +        $other_fields = array_keys($other_definitions);
    

    $other_ is a really poor variable name prefix :)
    I had to read the code multiple times to understand what was going on, what about $data_?

  9. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +        $table_mapping = array_fill_keys(array_keys($tables), array_values($entity_keys));
    

    I don't get what this is doing: shouldn't we use array_combine here?

  10. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +          $table_mapping = array_merge_recursive($table_mapping, array_fill_keys(array_keys($tables), array('language')));
    

    Actually the base table does not hold the language column.

  11. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +
    ...
    +
    
    +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilderInterface.php
    @@ -0,0 +1,41 @@
    +use Drupal\Core\Entity\ContentEntityType;
    +
    

    bogus blank line

  12. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +    $tables = $this->getTables($this->entityType);
    

    bogus parameter

  13. +++ b/core/lib/Drupal/Core/Entity/Schema/SchemaBuilder.php
    @@ -0,0 +1,402 @@
    +      // @todo Descriptions, unique keys, indexes and foreign keys.
    

    We need at very least indexes/unique keys before this can be committed. We probably need a spearat method like addTableIndexes() to make it overridable per-entity-type. Or maybe those can go into the processTable* methods?

  14. +++ b/core/modules/entity/entity.module
    @@ -129,16 +130,55 @@ function entity_entity_bundle_delete($entity_type_id, $bundle) {
    +function entity_module_preinstall($module) {
    

    This looks very neat now :)

  15. +++ b/core/modules/system/tests/modules/entity_test/entity_test.install
    @@ -9,433 +9,36 @@
    +  foreach ($entity_type_ids as $entity_type_id) {
    +    $storage_controller = $entity_manager->getStorageController($entity_type_id);
    +
         // Auto-create fields for testing.
    -    entity_create('field_config', array(
    +    $storage_controller->create(array(
           'name' => 'field_test_text',
    -      'entity_type' => $entity_type,
    +      'entity_type' => $entity_type_id,
    +      'user_id' => 1,
           'type' => 'text',
           'cardinality' => 1,
           'translatable' => FALSE,
         ))->save();
    -    entity_create('field_instance_config', array(
    -      'entity_type' => $entity_type,
    +    $storage_controller->create(array(
    +      'entity_type' => $entity_type_id,
    +      'user_id' => 1,
           'field_name' => 'field_test_text',
    -      'bundle' => $entity_type,
    +      'bundle' => $entity_type_id,
           'label' => 'Test text-field',
         ))->save();
     
    -    entity_get_form_display($entity_type, $entity_type, 'default')
    +    entity_get_form_display($entity_type_id, $entity_type_id, 'default')
    

    This change really does not look right to me: you are using the test entity storage controller to create field/instance entities...

  16. +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php
    @@ -94,6 +95,10 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    $fields['field_test_text'] = FieldDefinition::create('text')
    

    This does not look right either: the definition will be provided by the Field module once we create the related field_config/field_config_instance entities.

plach’s picture

I like 3. But let's do that in a follow-up.

Yep, it's totally follow-up material :)

plach’s picture

Assigned: Unassigned » plach

Working a bit on this.

tstoeckler’s picture

plach’s picture

Assigned: plach » Unassigned
Status: Needs work » Needs review
FileSize
78.61 KB

Let's see how many failures we have now...

mauzeh’s picture

Status: Needs work » Needs review
FileSize
81.98 KB

Fixed some more tests

mauzeh’s picture

posting interdiff

plach’s picture

Status: Needs work » Needs review
FileSize
11.5 KB
92.85 KB

More fixes from my and @mauzeh's branches.

plach’s picture

@mauzeh:

Reviewing your code, a couple of notes:

  1. +++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/DrupalUnitTestBaseTest.php
    @@ -19,7 +19,7 @@ class DrupalUnitTestBaseTest extends DrupalUnitTestBase {
    +  public static $modules = array('entity', 'entity_test', 'entity_install_schema_test');
    
    @@ -33,8 +33,8 @@ public static function getInfo() {
    +    $modules = array('entity', 'entity_test', 'entity_install_schema_test');
    +    $tables = array('entity_test', 'entity_install_schema_test');
    
    @@ -43,7 +43,9 @@ function testSetUp() {
    +    foreach($tables as $table){
    +      $this->assertFalse(db_table_exists($table), "'$table' database table not found.");
    +    }
    

    Do we need still need entity/entity_test modules here?

  2. +++ b/core/modules/simpletest/tests/modules/entity_install_schema_test/entity_install_schema_test.install
    @@ -0,0 +1,50 @@
    +function entity_install_schema_test_schema(){
    +  $schema['entity_install_schema_test'] = array(
    ...
    +      'entity_install_schema_test_index' => array('sid', 'dummy_integer'),
    

    I think we should call the module "simpletest_test" or something. It has nothing to do with entities so the current name is a bit misleading. By calling this way we allow it to be used also for other purposes.

  3. +++ b/core/modules/simpletest/tests/modules/entity_install_schema_test/entity_install_schema_test.module
    @@ -0,0 +1,25 @@
    +function entity_install_schema_test_permission() {
    

    Why do we need these permissions?

plach’s picture

Status: Needs work » Needs review
FileSize
19.09 KB
92.78 KB

This might be green finally.
(crossing-fingers)

plach’s picture

Status: Needs work » Needs review
FileSize
1.87 KB
94.07 KB

That was unfair, marvin :(

plach’s picture

This refactors the whole stuff a bit: as discussed yesterday with Tobias, we are moving the getTableMapping() method on the storage, so we can properly lazy load the schema builder (which will grow bigger in the follow-ups). Interfaces have been refactored accordingy.

plach’s picture

FileSize
92.06 KB

Sorry, wrong patch. The interdiff in #45 is good.

Berdir’s picture

Quick review...

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -133,6 +220,173 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
    +
    +        $this->getLayoutType();
    +        switch (TRUE) {
    +          // The base layout stores all the base field values in the base table.
    +          case $this->layoutType == static::LAYOUT_BASE:
    +            $table_mapping[$this->baseTable] = $this->processFields(array_merge($key_fields, array_diff($storable_fields, $key_fields)));
    +            break;
    

    I don't even...

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -582,7 +845,17 @@ public function save(EntityInterface $entity) {
    +        // Even if this is a new entity, the ID key might have been set in which
    +        // case we should not override the provided ID. An empty value for the
    +        // id is interpreted as NULL and thus overriden.
    +        if (empty($record->{$this->idKey})) {
    +          $record->{$this->idKey} = $insert_id;
    +        }
    +        $return = SAVED_NEW;
    

    There's special cases like uid 0, wondering if that could be a problem. You don't really override that, though.

  3. +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
    @@ -0,0 +1,345 @@
    +
    +    // @todo Descriptions, unique keys, indexes and foreign keys.
    +  }
    

    How are multi-field indexes going to work?

  4. +++ b/core/modules/entity/entity.module
    @@ -99,6 +100,36 @@ function entity_entity_bundle_delete($entity_type_id, $bundle) {
    +function entity_modules_installed($modules) {
    

    Are you sure this is not too late? modules might already want to create entities in hook_install()?

  5. +++ b/core/modules/serialization/lib/Drupal/serialization/Tests/EntitySerializationTest.php
    @@ -101,6 +95,18 @@ public function testNormalize() {
    +      ),
    +      'revision_uid' => array(
    +        array('target_id' => NULL),
    +      ),
    +      'log' => array(
    +        array('value' => NULL),
    +      ),
    

    So we add those by default now?

  6. +++ b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.install
    index 0000000..b3d9bbc
    --- /dev/null
    
    --- /dev/null
    +++ b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.module
    
    +++ b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.module
    +++ b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.module
    @@ -0,0 +1 @@
    
    @@ -0,0 +1 @@
    +<?php
    

    No longer needed, .module files are optional.

  7. +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php
    @@ -86,7 +86,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
           ->setDescription(t('The bundle of the test entity.'))
    -      ->setRequired(TRUE);
    +      ->setRequired(TRUE)
    +      ->setSetting('max_length', 32);
     
    

    FYI: #1709960: declare a maximum length for entity and bundle machine names adds a constant for 32.

  8. +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestRev.php
    @@ -60,6 +60,25 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    $fields['revision_uid'] = FieldDefinition::create('entity_reference')
    +      ->setLabel(t('Revision user ID'))
    +      ->setDescription(t('The user ID of the author of the current revision.'))
    +      ->setSettings(array('target_type' => 'user'))
    +      ->setQueryable(FALSE)
    +      ->setRevisionable(TRUE);
    +
    +    $fields['log'] = FieldDefinition::create('string')
    +      ->setLabel(t('Log'))
    +      ->setDescription(t('The log entry explaining the changes in this revision.'))
    +      ->setRevisionable(TRUE)
    +      ->setTranslatable(TRUE);
    

    Ah, I see, this is manually added now, I guess to test how this will work for nodes?

    Those two fields only exist on the revision tables now, do we keep that? We don't want to see those values if we don't load a revision I think, or we risk duplicating it on save/showing it by default.

    Also, log shouldn't be Translatable, not in EntityTestRev.

plach’s picture

Sorry, just saw #48, I will address it in the next iteration.

  1. I know, I know :)
  2. Yep, in that case it's not a real override
  3. We are going to provide sensible defaults in the process methods that can be overriden by entity-type-specific schema builders (going to work on this now).
  4. Getting field definitions before installing the schema introduces a nasty bug when enabling the Locale module, which then tries to lookup its tables to translate annotations, but they are not there yet. We will introduce a new hook to let modules act when entity schema is installed.
  5. I would aim to do that, but it needs its own discussion. Now it just defaults to that.
  6. Cool :)
  7. Ok, we'll just skip that then
  8. Yep, but I think it's no longer needed with this patch
  9. I (mostly) agree but I think this needs a separate discussion
plach’s picture

FileSize
4.28 KB
96.84 KB

Reverted some probably unneeded changes after addressing #48.

plach’s picture

The content translation test failures are expected because we are currently disabling translatability for entity types that do have translation support, as their schema has not been converted yet. We probably need some kind of BC layer until we are able to switch table layout, which probably needs to wait for #2068325: [META] Convert entity SQL queries to the Entity Query API.

plach’s picture

FileSize
11.35 KB
94.63 KB
3ca5a9b DEV 2183231: Renamed ::schemaBuilder() to ::schemaHandler().
9c1527d DEV 2183231: Restored shortcut translatability.
e531b32 DEV 2183231: Refactored entity save to avoid instantiating the schema handler.
92dfff1 DEV 2183231: Fixed entity serialization test.
969becf DEV 2183231: Reintroduced BC layer to support content translation without the data table.

This might be green again.

plach’s picture

Status: Needs work » Needs review
Related issues:
FileSize
94.43 KB
2.48 KB

This should fix installation.

plach’s picture

Status: Needs work » Needs review
Related issues:
FileSize
94.74 KB
2.15 KB

Let's see whether this is better:

fda182d DEV 2183231: Fixed BC layer comment.
32ad26d DEV 2183231: Fixed entity save.
plach’s picture

Status: Needs work » Needs review
Related issues:
FileSize
94.8 KB
905 bytes

More fixes

plach’s picture

Status: Needs work » Needs review
Related issues:
FileSize
94.87 KB
98.52 KB
12.92 KB

This includes the previous fixes plus support for index and keys generation. We should be ready to remove the BC layer now.

plach’s picture

Status: Needs work » Needs review
FileSize
765 bytes
98.53 KB

Fixed unique keys generation.

plach’s picture

FileSize
36.35 KB
127.78 KB

Just a quick and dirty attempt to remove all entity schemas. I was able to complete astandard installation, let's how this performs.

plach’s picture

Status: Needs work » Needs review
FileSize
127.82 KB
plach’s picture

This one should be cleaner:

f634ebb DEV 2183231: Fixed UUID unique keys.
edd5fd6 DEV 2183231: Completed removal of content entity schemas.
c0110bf DEV 2183231: Filtered out multiple base fields from base table mapping.
86b0f73 DEV 2183231: Moved additional entity tables to the related storage classes.
99120f8 DEV 2183231: Fixed some tests.
dfe9810 DEV 2183231: Fixed entity reference hack.
d2a41b9 DEV 2183231: Fixed entity query.

Still missing:

  • some details in the key generation to match exactly the original schema (unsigned int for id/revision_id), int length for id/revision_id (11 vs 10)
  • entity-type specific indexes (to be added in the various buildSchema() methods)
  • removing the drupal_get_schema() BC layer
  • documentation for the new hooks
  • fixing test failures
  • improving test coverage

We should be ready for some serious review though.

tstoeckler’s picture

Fixing some of the test fails now. Starting from the bottom of the list in case anyone is already working from the top.

tstoeckler’s picture

Found two issues:
- As entity query now uses getTableMapping() it fails to find the 'default_langcode' field on data tables, as we currently don't expose that as it's an implementation detail of the storage controller.
- Shortcut currently doesn't have stuff like shortcut_set on the data table denormalized (like node does) which means you need to reinstall after applying this patch, because after the patch it does. Or you can just use minimal profile.

tstoeckler’s picture

Also:
- Forum module is currently broken due to the issue mentioned in #48.4: Forum module tries to save a taxonomy term in forum_install(). This no longer works. :-/

tstoeckler’s picture

Also:
- Node module should have a separate storage controller which adds {node_access} to the schema.

(Sorry for the noise, just going through the tests one by one and don't want to lose this information)

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
18.58 KB
183.97 KB

One last problem:
- tracker_install() queries {node}

This one fixes all those fails that are related to legacy usage of installSchema() in DUTB tests. Let's see.

tstoeckler’s picture

Some of the aggregator fails are due to #2227367: StringLong's schema is broken, so marking that as critical. Applying that patch doesn't actually fix aggregator because of the entity key defaults stuff. Aggregator items don't have a UUID (see also #2149851: Remove todo about GUID field on the 'aggregator_item' entity and add UUID field), but we're trying to add a unique key for the 'uuid' column nevertheless. I would suggest to simply roll the defaults stuff back. Also #2209049: Allow field schemas to contain unique keys would allow us to remove that hardcoded unique key from the schema handler in the first place.

plach’s picture

@tstockler:

Thanks for keeping the work going!

- As entity query now uses getTableMapping() it fails to find the 'default_langcode' field on data tables, as we currently don't expose that as it's an implementation detail of the storage controller.

I think this is not correct, we should definitely have default_langcode in the table mapping: it's true it's a table to field representation, but I think all actual table columns should be included.

- Forum module is currently broken due to the issue mentioned in #48.4: Forum module tries to save a taxonomy term in forum_install(). This no longer works. :-/

We can move the term installation to hook_entity_schema_installed() (see user.install) to fix that.

- Node module should have a separate storage controller which adds {node_access} to the schema.

There is the node access storage that should be the perfect candidate to do that, but I think this is follow-up material.

- tracker_install() queries {node}

Can we do a quick entity query conversion or it's too much work?

I would suggest to simply roll the defaults stuff back.

I think it's fine: now that stuff is properly refactored to leverage those properties the right way, removing entity key defaults and add some @todo should not be problematic.

Also #2209049: Allow field schemas to contain unique keys would allow us to remove that hardcoded unique key from the schema handler in the first place.

I don't think that's a big deal: if that gets in before this we can adjust our code, otherwise it's a just few lines that will need to be refactored.

YesCT’s picture

Issue summary: View changes
Issue tags: +Needs issue summary update

#20 3. (follow-up needed still?)

Also, tagging for issue summary update as a lot seems to have been sorted out since this issue was created.

Let's get a section for follow-ups and blockers.

tstoeckler’s picture

FileSize
9.75 KB

Re #82: Thanks for the feedback. Agreed on all accounts.

Re #83: Yes, that follow-up still needs to be created. I will hopefully get to that tonight.

...Along with another re-roll which should then seriously bring down the failure count

If I don't get to that, or life happens..., here's my interdiff for what I have so far. I will assign tonight if I get to it, so as long as this is unassigned, feel free to work on it, but then don't forget to assign yourself.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
29.3 KB
198.13 KB

Oops, I did forget to assign in the end. Oh well.

Here we go: This should have a lot less fails. At least all of the problems mentioned above.

The fact that we now have non-entity table fields in getTableMapping() forced me to introduce a weirdness: There really was no sensible array key to use (as the key is the field definition name, but we don't have a field definition here), so I used '' as a key. To clean that up I would like to introduce a small value(ish) object for the table mapping á la Url which has some methods for building and returning the various parts of the current multi-dimensional array. Let's first get this to green, though.

I also introduced a hack in the form of db_table_exists() in tracker_install(). Will have to think more about how to avoid that, couldn't come up with anything so far.

I also removed the drupal_get_schema() backwards-compatibility layer as that should no longer be needed.

I'm pretty tired so won't update the issue summary and open follow-ups just yet, but hopefully tomorrow.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
7 KB
203.35 KB

Here's some more trivial test fixes. The rest are actual failures. I really thought there would be less of those :-( Some of those might be trivial to fix as well, but we'll have to look into them one by one to figure them out. Before working on those I'm going to a) update the issue summary and open a bunch of follow-ups and b) complete the unit test that I have locally, at least with some basic coverage. Maybe that will turn out something.

Maybe I can summon @mauzeh to help with some of the test fails?! (Or anyone else of course!)

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
203.35 KB

'PDOException' with message 'SQLSTATE[08004] [1040] Too many connections'

Assuming that's a testbot problem. Re-uploading patch.

tstoeckler’s picture

Rewrote the issue summary and opened a bunch of separate issues. There's only one that's truely a follow-up, though. And none of them are critical or major, so don't panic! :-)

Berdir’s picture

Status: Needs work » Needs review
FileSize
205.94 KB
12.9 KB

This should fix a lot of tests...

Berdir’s picture

Status: Needs work » Needs review
FileSize
214.24 KB
8.9 KB

More test fixes, done for the moment.

plach’s picture

Priority: Critical » Normal
Status: Needs work » Needs review
FileSize
0 bytes

Just a reroll before starting work

plach’s picture

FileSize
214.5 KB

Now for reals

plach’s picture

Assigned: plach » Unassigned
Status: Needs work » Needs review
FileSize
1.3 KB
215.55 KB

A couple of fixes, I hoped to be able to work on this a bit more...

e70f72c DEV 2183231: Fixed book comment test.
0953b48 DEV 2183231: Fixed revision_uid default.
Berdir’s picture

  1. +++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentBookTest.php
    @@ -45,6 +45,7 @@ public function testBookCommentPrint() {
           'title' => 'Book title',
           'body' => 'Book body',
    +      'status' => NODE_PUBLISHED,
         ));
         $book_node->book['bid'] = 'new';
    

    This seems like the wrong fix? It's the same problem as feed.queued had, which had no default value. Instead, we should set the default_value on the status field...

  2. +++ b/core/modules/node/lib/Drupal/node/Entity/Node.php
    @@ -94,6 +94,10 @@ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $reco
    +      // Ensure we have a valid value for the revision author.
    +      if (!isset($record->revision_uid)) {
    +        $record->revision_uid = 0;
    +      }
    

    Same here I guess?

plach’s picture

Assigned: plach » Unassigned
Status: Needs work » Needs review
FileSize
5.11 KB
215.97 KB

Not many fixes again...

tstoeckler’s picture

Assigned: Unassigned » tstoeckler
Status: Needs work » Needs review
FileSize
6.95 KB
2.68 KB
224.04 KB

OK, I'll take a stab at this.

This one already updates the patch to use the new EntityTypeInterface::BUNDLE_MAX_LENGTH and ContentEntityTypeInterface. Additionally it fixes ViewEntityDependencyTest.

It seems we need to open a follow-up to auto-generate views data from the auto-schema. I.e. that that would no longer be a nice-to-have but at least a major bug. See the @todo in the interdiff. Of course there might be a way to just temporarily fix the problem in some way that I haven't thought of, which would mitigate this.

plach’s picture

@tstoeckler:

It seems we need to open a follow-up to auto-generate views data from the auto-schema. I.e. that that would no longer be a nice-to-have but at least a major bug. See the @todo in the interdiff.

We have #1740492: Implement a default entity views data handler postponed on this work. I already linked it in a @todo or two in a previous patch, I think we can do the same with the @todo you added in the last one.

+++ b/core/lib/Drupal/Core/Entity/SqlStorageInterface.php
@@ -12,6 +12,80 @@
+   * The base table layout: no revision or multilingual support for base fields.
+   *
+   * @var int
+   */
+  const LAYOUT_BASE = 0;
+
+  /**
+   * The revision table layout: provides revision support for base fields.
+   *
+   * @var int
+   */
+  const LAYOUT_REVISION = 1;
+
+  /**
+   * The multilingual table layout: provides multilingual support for base
+   * fields.
+   *
+   * @var int
+   */
+  const LAYOUT_MULTILINGUAL = 2;
+
+  /**
+   * The multilingual revision table layout: provides revision and multilingual
+   * support for base fields.
+   *
+   * @var int
+   */
+  const LAYOUT_MULTILINGUAL_REVISION = 3;

I disagree with this change, sorry: those constants (and the methods below) are all details of our core storage implementation. Other SQL-based implementations might want to store data in a completely different way. Those constants/methods would make no sense in that context. I originally thought about SqlStorageInterface as a marker interface, then I realized that we would need the ::getTableMapping() method to let View generate its data automatically, but I think there's not much more we can abstract. I think it's fair to put ContentEntityDatabaseStorage in our type-hints as in those cases we are explicitly referring to our well-known core implementation.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
31.34 KB
244.88 KB

Ok here we go. Will try to tackle some more stuff later today, but unassigning for now.

Re #111: As I really like type-hinting interfaces I did the following as a compromise: I introduced a new ContentEntityDatabaseStorageInterface extends SqlStorageInterface where I moved the constants and the get*Table() methods. I don't feel *super* strongly about this, so if this doesn't work for you @plach, then I'll give up :-) and type-hint ContentEntityDatabaseStorage again.

- This also reverts some of the changes to the node defaults that were problematic:
- The $node->status field default, for instance, depends on the node type configuration. So I added that default in bundleFieldDefinitions(). I also found that the field definition cache is not cleared when a bundle entity is updated, so I fixes that. Should probably be moved to a separate issue.
- The uid and revision_uid should not default to 0, IMO, but uid should be set to the current user by default and revision_uid should be set to the same value as uid if it is not provided. This is not yet implemented, so a couple tests that passed above will probably fail. I got a little confused however, which tests pass and which fail, so uploading this to get a status quo from the bot.

This also includes a baseline unit test. For now only the LAYOUT_BASE schema is tested without a UUID key, so there's a lot of more coverage to provide but it's a start.

tstoeckler’s picture

Hmm... damn. Will look into this later today.

Also forgot to mention: The interdiff in #112 includes the one from #107, at least those parts which I don't change further. I had forgot to apply that to my branch and noticed in the middle of fixing stuff.

Sylvain Lecoy’s picture

You might be interested in looking at the reverse procedure: e.g. building an entity object from Schema API: http://drupalcode.org/project/doctrine.git/blob/48b52566638b3e31e3e0ebff...

If you have any questions i'll be glad to help :)

Sylvain Lecoy’s picture

andypost’s picture

tstoeckler’s picture

Here we go.

This fixes all failures from below, except for ConfigImportAllTest, which I did not run locally (yet). The EntityReferenceAdminUiTest depends on #2225739: Remove usage of field_info_instances(), use EntityManager::getFieldDefinitions() instead in order to pass, so this is now blocked on that. Therefore the patch also includes that for now. (I left that out of the interdiff(s), though.)

I also fixed a few things here and there that I noticed reading through the patch, so even though I tried to be very careful I might have broken this or that. I really hope this gets us into the single digits, though, in terms of fails.

Other than that I added the hook documentation for hook_entity_schema_installed() (and for the corresponding hook_entity_schema_uninstalled() that I introduced for consistency). It's not the coolest hook in the world and we might be able to remove it at some point but for now this gets us closer to commit.

Also added test coverage for an entity with a UUID field. Adding test coverage for the other layout types (i.e. the actual nitty gritty) is up next.

Unassigning for now, though. I'll try to work a little bit more on test coverage but that shouldn't block anyone else taking a stab.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
2.09 KB
264.13 KB

One last try for today. This should fix all but one of the tests that previously failed. Given how things have gone in this issue I expect a whole bunch of new tests to fail now... Let's see.

martin107’s picture

It looks like there has been a change in policy

from Node.php

    // If no revision author has been set explicitly, make the node owner the
    // revision author.
    if (!$this->getRevisionAuthor()) {
      $this->setRevisionAuthorId($this->getOwnerId());
    }

So anonymous user changes will be marked as owner changes

and from UserCancelTest.php

    // Confirm that user's content has been attributed to anonymous user.
    $test_node = node_load($node->id(), TRUE);
    $this->assertTrue(($test_node->getOwnerId() == 0 && $test_node->isPublished()), 'Node of the user has been attributed to anonymous user.');
    $test_node = node_revision_load($revision, TRUE);
*    $this->assertTrue(($test_node->getRevisionAuthor()->id() == 0 && $test_node->isPublished()), 'Node revision of the user has been attributed to anonymous user.');
    $test_node = node_load($revision_node->id(), TRUE);
    $this->assertTrue(($test_node->getOwnerId() != 0 && $test_node->isPublished()), "Current revision of the user's node was not attributed to anonymous user.");

The test marked with a star (line 294) is expecting attribution by anonymous user.

There is more than this going on as the compiler error is

( ! ) Fatal error: Call to a member function id() on a non-object in XXXXX/sites/drupal/core/modules/user/lib/Drupal/user/Tests/UserCancelTest.php on line 294
tstoeckler’s picture

Assigned: Unassigned » tstoeckler

Re #122: Good catch! That check should do a strict check for === FALSE.

Regarding the UserCancelTest failure that's a different problem. I already have that fixed locally, will upload patch tomorrow.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
650 bytes
264.76 KB

Actually, reading #122 again, the conclusion is incorrect. $node->getRevisionAuthor() will actually return the user entity, not the ID, so even for anonymous users the current check is sufficient.

Also, for anyone wondering: In core there is no UI for setting the revision_uid column. The current behavior of always assigning the ID of the current user is not mitigated by the referenced code. I verified that.

Here is the patch with the fix to UserCancelTest. That test actually fails due to a pre-existing bug: When a user account gets cancelled and the nodes get reassigned node.module fails to re-assign the node_revision.revision_uid database field. There is actually test coverage for this (which failed with the above patch), the reason this is not failing in 8.x must be some weirdness in EntityReferenceItem. I will look into that.

Anyway, this one should ***finally*** be green. Let's see.

tstoeckler’s picture

So in terms of bringing this along additional test coverage would be awesome, if anyone wants to work on that.

I also still want to work on converting the table mapping to a value object before this lands, as the current empty string hack is really not so nice. My thoughts on that were something like the following:

TableMappingInterface {
  /**
   * Returns a list of tables.
   */
  public function getTables();

  /**
   * Returns a mapping of entity field names to the respective database columns for a specific table.
   */
  public function getFieldColumns($table)
  // Or alternatively?!
  public function getFieldColumns($table, $field_name);

  /**
   * Adds a list of database columns for a specific entity field.
   */
  public function addFieldColumns($table, $field_name, $columns);

  /**
   * Gets a list of database columns that do not belong to entity fields (e.g. for denormalization purposes).
   *
   * Please do find a better name for this method :-)
   */
  public function getOtherColumns($table);

  /**
   * Adds a database column that does not belong to an entity field.
   */
  public function addOtherColumn($table, $column_name);

}

Of course naming, etc. should be improved. I think those methods should cover the current usage of the table mapping, though. I might be wrong, however.

plach’s picture

Awesome work! I will try to review the patch ASAP.

About #125 it might be a good idea but we should try to get feedback from at least @fago and @berdir, before going on with coding.

tstoeckler’s picture

FileSize
531 bytes
2.46 KB
241.07 KB

This needed a re-roll after #2225739: Remove usage of field_info_instances(), use EntityManager::getFieldDefinitions() instead.

I removed the Entity Reference hacks, as those should no longer be necessary. Let's see.

I also fixed a small oversight that SqlStorageInterface wasn't extending EntityStorageInterface.

tstoeckler’s picture

FileSize
240.99 KB
tstoeckler’s picture

FileSize
836 bytes
240.91 KB

Git doesn't like me today....

Let's try this again. Also with an interdiff, as I removed a check that should not be necessary.

tstoeckler’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
240.92 KB
plach’s picture

Status: Needs review » Needs work
FileSize
241.11 KB

Just a reroll to merge/update my dev branch. Tomorrow I will post my review.

fago’s picture

Amazing to see this one being green, great work!

ad #125, TableMappingInterface: Yeah, I really like this idea. It's way cleaner and easier to use than an array with some documented keys. Even further, in future it might be possible to do a DatabaseStorage class that works independently from the table mapping? E.g., it just has save() and saveRevision() methods that work through all respective tables according the mapping. We could prepare for that by making sure TableMappingInterface already provides all the information needed for doing that.

Regarding uid default values, there is #1979260: Automatically populate the author default value with the current user which is working on that already.

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -80,12 +121,27 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase {
    +   * The entity schema builder.
    +   *
    +   * @var \Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface
    ...
    +  protected $schemaHandler;
    

    Is it a builder or handler? :-)

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +  public function getLayoutType() {
    +    if (!isset($this->layoutType)) {
    +      $this->layoutType = static::LAYOUT_BASE;
    

    This and the fuddling with constants does not seem too nice. Also, what does it add compared to checking whether an entity type is revisionable or translatable, or both?

    The check for the data table seems unnecessary for me. If it is translatable it *has* to be there (given this storage class is used), i.e. we can throw an exception e.g. during construct if it's not defined.

    Once that is done, couldn't the schema handler go and do just $entity_type->isTranslatable() / $entity_type->isRevisionable() / $entity_type->isTranslatable() && $entity_type->isRevisionable() to do the checks?

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +  protected function processFields($field_names) {
    

    getColumnMapping() then maybe?

  4. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -663,53 +942,53 @@ protected function savePropertyData(EntityInterface $entity, $table_key = 'data_
    +          // If we are creating a new entity, we must not populate the record
    +          // with NULL values otherwise defaults would not be applied.
    

    Imo that's deprecated. We've default values in field definitions which have been applied to $entity - those should be saved, nothing else.

  5. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorageInterface.php
    @@ -0,0 +1,103 @@
    +interface ContentEntityDatabaseStorageInterface extends SqlStorageInterface {
    

    what about ContentEntityDefaultDatabaseStorageInterface ?
    -> this would reflect that any new tandem of storage + schema classes should add their own interface variants.

    Also, documentation should clarify this is the interface of the default database storage, and contrib is free to add/use their own.

  6. +++ b/core/modules/node/lib/Drupal/node/Entity/Node.php
    @@ -449,10 +463,30 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    // @todo Fix this!
    +    if (!$node_type) {
    +      return $fields;
    

    Is there already an issue for that?

  7. +++ b/core/modules/node/lib/Drupal/node/NodeStorage.php
    @@ -0,0 +1,100 @@
    +    // @todo Theoretically this table belongs to NodeGrantDatabaseStorage
    +    //   instead of NodeStorage. Add a schema creation facility to the former
    +    //   similar to this one.
    

    So as it doesn't belong in here, shouldn't it better stay in node_schema() for now?

  8. +++ b/core/modules/user/lib/Drupal/user/UserStorage.php
    @@ -154,4 +159,96 @@ public function updateLastLoginTimestamp(UserInterface $account) {
    +    // @todo Theoretically this table belongs to UserData instead of
    +    //   UserStorage. Add a schema creation facility to the former similar to
    +    //   this one.
    +    $schema['users_data'] = array(
    

    Again, should stay in user_schema() then?

plach’s picture

FileSize
13.68 KB
240.39 KB

Fixed 1 and 3 from #135 + some other minor clean-up.

@fago:

2. Actually those constants aren't as useful as I had imagined, however we might keep them for now, as for BC reasons we need to keep those checks around and we cannot simply rely on the entity type keys.

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +          $table_mapping[$this->dataTable][''] = array('default_langcode');
    ...
    +          $table_mapping[$this->dataTable][''] = array('default_langcode');
    ...
    +          $table_mapping[$this->revisionDataTable][''] = array('default_langcode');
    

    These don't look very clean, I am wondering whether we can come up with something better.

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -663,53 +942,53 @@ protected function savePropertyData(EntityInterface $entity, $table_key = 'data_
    +          // If we are creating a new entity, we must not populate the record
    +          // with NULL values otherwise defaults would not be applied.
    +          if (isset($value) || !$is_new) {
    

    We can probably remove this check, as we no longer apply defaults at SQL level. This might imply some tricky test failure though, if that's the case we can defer this to a follow-up.

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorageInterface.php
    @@ -0,0 +1,103 @@
    +interface ContentEntityDatabaseStorageInterface extends SqlStorageInterface {
    

    I am really not feeling the need for this interface, I cannot imagine why one would need to provide an alternative implementation of the very same concept without subclassing ContentEntityDatabaseStorage. Anyway, I am not feeling strongly about this either, if we go this way I agree with @fago that we need a more specific name.

  4. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorageInterface.php
    @@ -0,0 +1,103 @@
    +  public function getLayoutType();
    

    If we don't drop this, I guess getTableLayout() would be a better name, now that I think about it :)

  5. +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
    @@ -0,0 +1,464 @@
    +  public function getFieldColumnName(FieldDefinitionInterface $definition, $column) {
    

    This should move onto the storage class: we are once again instantiating the schema handler at every request atm.

  6. +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
    @@ -0,0 +1,464 @@
    +    if ($this->storage->getLayoutType() & ContentEntityDatabaseStorage::LAYOUT_REVISION) {
    

    This should be referring to the interface (if we don't drop constants).

  7. +++ b/core/lib/Drupal/Core/Entity/SqlStorageInterface.php
    @@ -0,0 +1,24 @@
    +interface SqlStorageInterface extends EntityStorageInterface {
    

    Nice catch :)

  8. +++ b/core/modules/entity/entity.api.php
    @@ -0,0 +1,49 @@
    +    \Drupal::database()->schema()->dropTable('mymodule_node_data');
    

    Not sure this is the best example we can provide: we want to encourage people to use fields for this kind of additions :)

  9. +++ b/core/modules/node/lib/Drupal/node/NodeStorage.php
    @@ -0,0 +1,100 @@
    +  public function getSchema() {
    +    $schema = parent::getSchema();
    

    These should be buildSchema() otherwise we would bypass the alter hook, but...

  10. +++ b/core/modules/node/lib/Drupal/node/NodeStorage.php
    @@ -0,0 +1,100 @@
    +    // @todo Theoretically this table belongs to NodeGrantDatabaseStorage
    +    //   instead of NodeStorage. Add a schema creation facility to the former
    +    //   similar to this one.
    +    $schema['node_access'] = array(
    

    ... I agree with @fago that these shuld be left in the various hook_schema() implementations :)

  11. +++ b/core/modules/node/node.install
    @@ -434,18 +50,25 @@ function node_install() {
    +function node_entity_schema_installed(array $storages) {
    

    We can get rid of this change then.

  12. +++ b/core/modules/node/node.module
    @@ -836,6 +836,10 @@ function node_user_cancel($edit, $account, $method) {
    +      db_update('node_revision')
    +        ->fields(array('revision_uid' => 0))
    +        ->condition('revision_uid', $account->id())
    +        ->execute();
    

    Ouch, luckily we are addressing this in #2068333: Convert node SQL queries to the Entity Query API.

  13. +++ b/core/modules/user/user.install
    @@ -5,243 +5,36 @@
    +    db_insert('users')
    ...
    +    db_insert('users')
    

    We could try and just do a couple of entity saves.

  14. +++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
    @@ -0,0 +1,312 @@
    +  protected $sqlStorage;
    

    I guess we could call this $dbStorage or just $storage. The current name might be ambigous wrt SqlStorageInterface.

plach’s picture

Status: Needs work » Needs review
FileSize
1.35 KB
240.39 KB

Better one

plach’s picture

FileSize
6.11 KB
244.67 KB

Fixed aggregator, block and node indexes. I will finish them tomorrow.

@tstoeckler:

Do you want to take care of the reviews above or do you want me to go ahead with them?

plach’s picture

+++ b/core/modules/system/system.module
@@ -1698,6 +1698,8 @@ function system_page_alter(&$page) {
+  $schema = \Drupal::entityManager()->getStorage('custom_block')->getSchema();

Mmmh, I will get rid of this in the next reroll :)

plach’s picture

About #125: what about getExtraColumns() instead of getOtherColumns()?

sun’s picture

Hm. I noticed a level of excitement about this issue/patch slowly getting ready

I want to encourage you to keep on working on this, but I hope that you're aware that there are some critical problems in the currently proposed code...? Sorry for being snarky, but it's my honest first time impression after glancing over this patch :)

LOLWUT @ http://ibin.co/1JAhv20hGyHa

+        case static::LAYOUT_MULTILINGUAL_REVISION:
+          $table_mapping[$this->baseTable] = $this->getColumnMapping(array_diff($key_fields, array($this->langcodeKey)));
+          $data_key_fields = array_diff($key_fields, array($this->uuidKey));
+          $data_fields = array_diff($storable_fields, $key_fields, $revision_metadata_fields);
+          $table_mapping[$this->dataTable] = $this->getColumnMapping(array_merge($data_key_fields, $data_fields));
+          // Add the denormalized 'default_langcode' field to the mapping. As it
+          // does not correspond to a field definition we add it with an empty
+          // key.
+          $table_mapping[$this->dataTable][''] = array('default_langcode');
+          $table_mapping[$this->revisionTable] = $this->getColumnMapping(array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields));
+          $revision_data_key_fields = array_diff($key_fields, array($this->bundleKey, $this->uuidKey));
+          $revisionable_fields = array_keys(array_filter($storable_definitions, $revisionable_filter_callback));
+          $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, $revision_data_key_fields);
+          $table_mapping[$this->revisionDataTable] = $this->getColumnMapping(array_merge($revision_data_key_fields, $revision_data_fields));
+          // See above.
+          $table_mapping[$this->revisionDataTable][''] = array('default_langcode');
+          break;

There are so many array_ functions involved, I... am not even remotely able to decipher what this code tries to do, and thus cannot suggest a more appropriate solution...

However, in case I get the gist right, then that crazy code cries for a dedicated domain data object with proper methods...?

plach’s picture

Status: Needs work » Needs review
FileSize
400 bytes
244.28 KB

Hm. I noticed a level of excitement about this issue/patch slowly getting ready

I AM excited, honestly :)

I want to encourage you to keep on working on this, but I hope that you're aware that there are some critical problems in the currently proposed code...? Sorry for being snarky, but it's my honest first time impression after glancing over this patch :)

Well, I guess it depends on the definition of "critical": we are aware there's something we didn't address properly yet. Hopefully we are not missing anything that would require to rewrite the whole thing :)
Did you express all your main concerns in #143 or there's something more you did not have to the time to write down?

LOLWUT @ http://ibin.co/1JAhv20hGyHa

I know I know :)
(see #136.1)

There are so many array_ functions involved, I... am not even remotely able to decipher what this code tries to do, and thus cannot suggest a more appropriate solution...

Basically it's a way to ensure well-known columns come before arbitrary field columns. I guess there are ways to clean-up that code, but I really didn't have the time/motivation to do that yet.

However, in case I get the gist right, then that crazy code cries for a dedicated domain data object with proper methods...?

If I get you right, we were discussing about that in #125 and following.

plach’s picture

Rerolled

plach’s picture

FileSize
2.97 KB
240.59 KB

This fixes a problem in the node indexes and entity schema uninstallation.

plach’s picture

Status: Needs work » Needs review
FileSize
1.56 KB
242.74 KB

Just an attempt to fix schema uninstallation (not really final, sun, all that code should be OOPified ;) + some test fixes. Partial interdiff, sorry I messed it up.

plach’s picture

Status: Needs work » Needs review
FileSize
1.08 KB
242.81 KB

This should get rid of notices.

Berdir’s picture

Went through the module/test changes bottom to top.

  1. +++ b/core/modules/comment/lib/Drupal/comment/CommentStorage.php
    @@ -127,4 +131,86 @@ public function getChildCids(array $comments) {
    +
    +    $schema['comment_entity_statistics'] = array(
    +      'description' => 'Maintains statistics of entity and comments posts to show "new" and "updated" flags.',
    

    Same as file_usage.

  2. +++ b/core/modules/file/lib/Drupal/file/FileStorage.php
    @@ -38,4 +38,60 @@ public function retrieveTemporaryFiles() {
    +
    +    $schema['file_usage'] = array(
    +      'description' => 'Track where a file is used.',
    

    This needs the same @todo as similar tables, or we'd simply keep it in hook_schema(). Like we for example do for shortcut.module. I don't really understand why we have to do this? And in some cases don't...

  3. +++ b/core/modules/forum/forum.install
    @@ -89,6 +78,22 @@ function forum_install() {
     /**
    + * Implements hook_entity_schema_installed().
    + */
    +function forum_entity_schema_installed(array $storages) {
    +  if (isset($storages['taxonomy_term'])) {
    +    // Create a default forum so forum posts can be created.
    

    Are we sure this hook is called when forum and taxonomy_term is not enabled at the same time? Again, another problem that we could avoid if we install the schema earlier I think.

  4. +++ b/core/modules/simpletest/tests/modules/simpletest_test/simpletest_test.install
    @@ -0,0 +1,50 @@
    +function simpletest_test_schema(){
    +  $schema['simpletest_test'] = array(
    +    'description' => 'Stores simpltest_test data.',
    

    typo.

    Can we maybe use the existing test module that we use for database tests instead of adding a new table for this?

  5. +++ b/core/modules/tracker/tracker.install
    @@ -16,12 +16,18 @@ function tracker_uninstall() {
      */
     function tracker_install() {
    -  $max_nid = db_query('SELECT MAX(nid) FROM {node}')->fetchField();
    -  if ($max_nid != 0) {
    -    \Drupal::state()->set('tracker.index_nid', $max_nid);
    -    // To avoid timing out while attempting to do a complete indexing, we
    -    // simply call our cron job to remove stale records and begin the process.
    -    tracker_cron();
    +  // @todo Is there some way to avoid this check?
    +  if (\Drupal::database()->schema()->tableExists('node')) {
    

    Can't we install the entity schema before calling hook_install(), like we do normal schema? Yes, it has to be hardcoded in ModuleHandler, but so what? Or we could maybe use hook_preinstall?

  6. +++ b/core/modules/user/lib/Drupal/user/AccountFormController.php
    @@ -366,9 +366,12 @@ public function validate(array $form, array &$form_state) {
    +      // @todo Make the user signature field use a widget to benefit from
    +      //   automatic typed data validation.
    +      $field_definitions = $this->entityManager->getFieldDefinitions('user', $this->getEntity()->bundle());
    +      $max_length = $field_definitions['signature']->getSetting('max_length');
    +      if (drupal_strlen($form_state['values']['signature']) > $max_length) {
    

    #2227381: Apply formatters and widgets to User base fields 'name' and 'email' is the issue for this.

  7. +++ b/core/modules/user/lib/Drupal/user/Entity/User.php
    @@ -500,8 +500,9 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setDescription(t('Whether the user is active or blocked.'))
    +      // @todo This should be FALSE.
    +      ->setSetting('default_value', TRUE);
    

    Why? (@todo should say). Temporary todo or do we need a follow-up for this?

  8. +++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php
    @@ -157,7 +157,7 @@ public function testCreate() {
         $field_info = $this->getMockBuilder('\Drupal\field\FieldInfo')
           ->disableOriginalConstructor()
           ->getMock();
    -    $entity_storage = new ContentEntityDatabaseStorage($entity_type, $connection, $field_info);
    +    $entity_storage = new ContentEntityDatabaseStorage($entity_type, $connection, $entity_manager, $field_info);
     
    

    I'm removing $field_info and injecting $entity_manager in #2116363: Unified repository of field definitions (cache + API), if we can get that in first then there's a bunch of stuff that won't be needed anymore.

plach’s picture

FileSize
243.05 KB

Just a reroll. New patch following...

plach’s picture

FileSize
4.21 KB
244.04 KB

This completes the entity-type-specifc index definitions. I will start addressing the reviews above soon.

plach’s picture

Status: Needs work » Needs review
FileSize
737 bytes
244.24 KB

@Berdir:

We have a tricky issue with the UriItem whose schema defines a text column having 2048 as maxlength, while the current {file}.uri column is a varchar(255). Can you have a look to the interdiff?

plach’s picture

Status: Needs work » Needs review
FileSize
244.98 KB

Rerolled

plach’s picture

Status: Needs work » Needs review
FileSize
928 bytes
244.97 KB

Another try...

plach’s picture

Status: Needs work » Needs review
FileSize
22.8 KB
229.27 KB

This reverts the changes to hook_schema() implementations not related to entity storage classes plus some other schema fix:

72b246e DEV 2183231: Fixed comment indexes + reverted unrelated schema changes.
d2bd54e DEV 2183231: Reverted unrelated schema changes.
09748a5 DEV 2183231: Fixed term schema.

Some DUBT test will probably fail due to these changes, but posting anyway to identify them all.

After fixing test failures I will start addressing the reviews above if @tstoeckler does not beat me to it.

jessebeach’s picture

Issue summary: View changes

Massive patch! Awesome work! I'm 50% through reviewing. I think I got through the meat of it and the rest seems to be conversions, but I'll finish up tomorrow to be sure.

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +
    +    // @todo Table names do not belong to the entity type definition, they are
    +    //   storage implementation details. Rip them out.
    +    $this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
    +
    

    This @todo needs an isuse logged for it.

    What should get removed from what? The recommendation is ambiguous.

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +      if ($this->entityType->hasKey('revision')) {
    +        $this->layoutType |= static::LAYOUT_REVISION;
    +      }
    

    This could do with a comment to explain what this bitwise operation is doing.

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +      $data_table = $this->entityType->getDataTable();
    +      if ($data_table && $this->entityType->isTranslatable()) {
    +        $this->layoutType |= static::LAYOUT_MULTILINGUAL;
    +      }
    +    }
    

    Again, a comment would be most helpful to explain what this bitwise operation is doing.

  4. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +   */
    +  public function getSchema() {
    +    $schema = $this->buildSchema();
    +    $context = array('entity_type' => $this->entityType);
    +    // TODO Document this hook.
    +    $this->moduleHandler()->alter('entity_schema', $schema, $context);
    +    return $schema;
    +  }
    

    Secret hooks :)

    I added this to the issue summary.

  5. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +  public function getTableMapping() {
    +    if (!isset($this->tableMapping)) {
    +      $table_mapping = array();
    +
    +      $key_fields = array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey));
    +
    +      // Storable fields are single-value base fields that are not defined as
    +      // computed and that do not specify a custom storage.
    +      // @todo Add support for multiple-value base fields.
    +      $storable_definitions = array_filter($this->fieldDefinitions, function (FieldDefinitionInterface $field_definition) {
    +        return !$field_definition->isComputed() && !$field_definition->hasCustomStorage() && !$field_definition->isMultiple();
    +      });
    +      $storable_fields = array_keys($storable_definitions);
    

    This @todo needs an issue.

  6. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +
    +      // @todo Provide automatic definitions for revision metadata fields.
    +      //   Rename 'log' to 'revision_log'.
    +      $revision_metadata_fields = array_intersect(array('revision_timestamp', 'revision_uid', 'log'), $storable_fields);
    +      $revisionable_filter_callback = function (FieldDefinitionInterface $definition) { return $definition->isRevisionable(); };
    +
    

    This @todo needs an issue.

  7. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +          $table_mapping[$this->dataTable][''] = array('default_langcode');
    +          $table_mapping[$this->revisionTable] = $this->getColumnMapping(array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields));
    +          $revision_data_key_fields = array_diff($key_fields, array($this->bundleKey, $this->uuidKey));
    +          $revisionable_fields = array_keys(array_filter($storable_definitions, $revisionable_filter_callback));
    +          $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, $revision_data_key_fields);
    +          $table_mapping[$this->revisionDataTable] = $this->getColumnMapping(array_merge($revision_data_key_fields, $revision_data_fields));
    +          // See above.
    +          $table_mapping[$this->revisionDataTable][''] = array('default_langcode');
    +          break;
    +      }
    

    Is there no default? Perhaps an error?

  8. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -742,12 +1013,20 @@ protected function mapToDataStorageRecord(EntityInterface $entity, $table_key =
               ->fields(array($this->revisionKey => $record->{$this->revisionKey}))
    @@ -756,7 +1035,11 @@ protected function saveRevision(EntityInterface $entity) {
    
    @@ -756,7 +1035,11 @@ protected function saveRevision(EntityInterface $entity) {
           }
         }
         else {
    -      drupal_write_record($this->revisionTable, $record, $this->revisionKey);
    +      $this->database
    +        ->update($this->revisionTable)
    +        ->fields((array) $record)
    +        ->condition($this->revisionKey, $record->{$this->revisionKey})
    +        ->execute();
         }
    

    Is drupal_write_record removed or deprecated in this patch?

  9. +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php
    @@ -0,0 +1,456 @@
    +   */
    +  protected function getFieldSchemaData(FieldDefinitionInterface $definition, $key) {
    +    $data = array();
    +    $schema = $definition->getSchema();
    +
    +    foreach ($schema[$key] as $key => $columns) {
    

    Is $schema just an array at this point?

  10. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
    @@ -7,6 +7,7 @@
     use Drupal\Core\Field\FieldItemBase;
    @@ -58,6 +59,10 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    
    @@ -58,6 +59,10 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
         $settings = $field_definition->getSettings();
         $target_type_info = \Drupal::entityManager()->getDefinition($settings['target_type']);
     
    +    if (!$target_type_info) {
    +      throw new \Exception(String::format('Target type @entity_type does not exist.', array('@target_type' => $settings['target_type'])));
    +    }
    +
         if ($target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) {
    

    Perhaps a specific Exception here and not the general one.

plach’s picture

Assigned: tstoeckler » plach

Thanks Jesse! I am going to fix those failure and then I will address your review and the ones above :)

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
7.54 KB
237.24 KB

Sorry @everyone, the last week was a bit crazy for me. I really wanted to push this along, but never got around to it. Thanks for keeping this going, especially @plach.

This should fix tests. Will work on addressing the reviews now.

Also noticed this is still assigned to me, sorry for that.

Edit: Crosspost. Damn, hopefully I wasn't to late.

plach’s picture

Assigned: plach » Unassigned

Ouch, I was about to post more or less the same patch :(

+++ b/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php
@@ -20,6 +20,15 @@ public static function getInfo() {
+    $this->installSchema('user', array('users_data'));

I think this can be fixed in the parent class.

Some answers:

@Berdir (#152):

Can't we install the entity schema before calling hook_install(), like we do normal schema? Yes, it has to be hardcoded in ModuleHandler, but so what? Or we could maybe use hook_preinstall?

Honestly I didn't think about harcoding it in the module handler: one reason we might want to avoid that and leave everything under the Entity module's control is that, when switching to dynamic schema handling, we will have a class (probably a service) handling schema installation/uninstallation/reinstallation/manipulation, well, you get me :)

The reason why hook_preinstall() does not work is that it's too early. Originally we were doing it there, but we found a nasty bug: when installing the Locale module, entity type definitions were parsed before Locale schema was installed, thus causing the string translator service, which was already aware of the Locale backend, to break as it tried to query (yet) unexisting tables. We might try to work around this, but actually this is just an example of a situation that theoretically any contrib module might trigger, so I thought it would be better to just define a dedicated hook, also because more of them will be added or renamed when unifying field storage and implementing dynamic schema.

@jessebeach:

I didn't qualify the @todos yet as I wanted to have a final patch, with some +1 before :)

1. The base|data|revision|revision_data table entity type keys
2. Ok, anyway @fago suggested to remove that code so we might go that way instead
7. Not sure what you mean :)
8. Nope, we are just no longer using it in the storage classes
9. Yep, it should always be a Schema API array

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
1.9 KB
236.34 KB

Let's try that again.

tstoeckler’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
230.43 KB

Oops, accidentally included some local, non-comitted changes and also forgot to merge 8.x for the 1000th time...
This should be better. Interdiff was correct.

I agree with putting the schema creation back into ModuleHandler. Once #2206347: Use event system in ModuleHandler is in, we can have the entity system register an event subscriber to clean that up.

plach’s picture

I agree with putting the schema creation back into ModuleHandler. #2206347: Use event system in ModuleHandler is in, we can have the entity system register an event subscriber to clean that up.

I am not clear why the event system should let us fix something hooks are not a good fit for: we'd still have to find the proper point in the execution flow: a new module-system hook or a new event, what would be the actual difference?

tstoeckler’s picture

Re @plach the difference is that the entity *system* - i.e. \Drupal\Core\Entity - cannot implement hooks. Hooks can only implemented by modules. That's why we had to put the stuff in entity.module for now. With the event system we can register arbitrary subscribers in core.services.yml. So the entity system can totally provide an event listener in \Drupal\Core\Entity\EventSubscriber to react to the module handler to install/uninstall the schema.

tstoeckler’s picture

Ahh, sorry I misunderstood your comment. Yes, you are of course correct and I am wrong.

Unlike I tried to allude to in #178 the problem is *not* that the code isn't being called, but the problem is *when* it gets called. Thanks for pointing that out! I will think about this more, maybe we need a new event over there or a new hook without that issue.

Berdir’s picture

The new event will have to be called before hook_install(), so the tables will be available there. The event is not a replacement for hook_install(), it's a replacement for the 10 one-off calls that are currently in ModuleHandler::install() to clear various caches, rebuild stuff and so on.

That's also what we did with the plugin cache clearer service, added a hardcoded call there and will then move to an event in that issue.

plach’s picture

Before hook_install() sounds much like hook_module_preinstall(), which would re-introduce the bug described above. From a theoretical POV #180 works for me, but we will have to be very careful with the actual implementation :)

jessebeach’s picture

+++ b/core/modules/comment/comment.install
@@ -28,157 +28,10 @@ function comment_install() {
-      ),
-      'name' => array(
-        'type' => 'varchar',
-        'length' => 60,
-        'not null' => FALSE,
-        'description' => "The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form.",
-      ),
-      'mail' => array(
-        'type' => 'varchar',
-        'length' => 64,
-        'not null' => FALSE,
-        'description' => "The comment author's e-mail address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.",
-      ),
-      'homepage' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-        'description' => "The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.",
-      ),

Where do these db fields get added back? I'm looking in \Drupal\comment\CommentStorage::buildSchema(), but I don't see them declared.

Berdir’s picture

They are not re-added. They're generated based on Comment::baseFieldDefinitions() and the schema() method of the corresponding field type/field item classes. that's the main thing this issue is doing, generating the schema for the entity tables. buildSchema() overrides are only necessary for things that can't be generated automatically like additional indexes and tables like user roles or taxonomy term hierarchy stuff.

jessebeach’s picture

UML: http://www.lucidchart.com/invitations/accept/5356c34a-9470-474a-8732-696...

The UML is open for editing.

UML diagram of the database storage classes. ContentEntityDatabaseStorage now contains a reference to an instance of ContentEntitySchemaHandler

\Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface is not doing much for us yet.

Also, both \Drupal\Core\Entity\ContentEntityDatabaseStorage and \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler implement this interface, even though ContentEntityDatabaseStorage contains an instance of ContentEntitySchemaHandler. We end up with a circuitious route to getting a schema:

ContentEntityDatabaseStorage->getSchema()

which calls

ContentEntityDatabaseStorage->buildSchema() 

which calls

ContentEntityDatabaseStorage->schemaHandler->getSchema()

It seems improper to share an interface like this. Does ContentEntitySchemaHandler really need to implement ContentEntitySchemaHandlerInterface?

All but one method of ContentEntitySchemaHandler is defined outside an interface as well.

This is the current HEAD class/interface UML for reference.

UML diagram of the database storage classes.

jessebeach’s picture

Adding the images with different names. Grumble, process, grumble.

plach’s picture

It seems improper to share an interface like this. Does ContentEntitySchemaHandler really need to implement ContentEntitySchemaHandlerInterface?

I don't see any problem in having an object referencing another object and proxying some calls to it. It's a technique used in several common patterns. For a detailed explanation of why we went this way see the issue summary (bullet 2 of the proposed resolution).

Berdir’s picture

file.uri is special because it's quite a different thing than a usual (http) URI, it's something like public://file_path, and that coudn't be much longer than 255 anyway, so I'd suggest to simply set the max_length of that to 255 instead of doing it on the index only.

+++ b/core/modules/file/lib/Drupal/file/FileStorage.php
@@ -45,6 +45,8 @@ public function retrieveTemporaryFiles() {
 
+    // @todo There should be a 'binary' field type or setting.
+    $schema['file_managed']['fields']['uri']['binary'] = TRUE;

We do have an issue for this I think.

The reason why hook_preinstall() does not work is that it's too early. Originally we were doing it there, but we found a nasty bug: when installing the Locale module, entity type definitions were parsed before Locale schema was installed, thus causing the string translator service, which was already aware of the Locale backend, to break as it tried to query (yet) unexisting tables. We might try to work around this, but actually this is just an example of a situation that theoretically any contrib module might trigger, so I thought it would be better to just define a dedicated hook, also because more of them will be added or renamed when unifying field storage and implementing dynamic schema

I had the same problem with the plugin cache clearer, the answer was simply that the plugin cache clear needs to happen after the module schema is installed, I think it's the same here, first install tables from hook_schema(), then entity tables, then hook_install(). The order of calls *is* very fragile there, but I think it should be possible to find something that works.

I don't see another way, default stuff needs to work in hook_install(), I'm pretty sure that forum only works in the tests because it's always enabled at the same time as taxonomy.

+++ b/core/modules/comment/comment.install
@@ -28,3 +28,82 @@ function comment_install() {
+      'entity_type' => array(
+        'type' => 'varchar',
+        'not null' => TRUE,
+        'default' => 'node',
+        'length' => 255,
+        'description' => 'The entity_type of the entity to which this comment is a reply.',
+      ),
...
+    'primary key' => array('entity_id', array('entity_type', 32), array('field_id', 32)),

Oh, I missed this in the issue that limited entity_type to 32. Because this is broken, if I would really store a longer entity_type in there, then things could break if the only difference is the longer entity_type. Not related to this issue, but I did notice that you also have a limit on the entity_type index for comment to 32, that should also no longer be necessary because the field length should now already be limited to 32 (if it is not, then we need to adjust it accordingly). We should open a separate issue for this, though.

jessebeach’s picture

FileSize
55.62 KB

I don't see any problem in having an object referencing another object and proxying some calls to it. It's a technique used in several common patterns. For a detailed explanation of why we went this way see the issue summary (bullet 2 of the proposed resolution).

Fair point. I concede it's not the pattern that concerns me, just some of the details. So if we can work through the details, I'll be assuaged :)

Firstly, if the intent is to make it possible to swap out the schema handling (this is what I understand the intent to be), then we have a method in ContentEntitySchemaHandler that isn't declared in ContentEntitySchemaHandlerInterface, but that ContentEntityDatabaseStorage assumes exists.

public getFieldColumnName()

as in

protected function getColumnMapping($field_names) {
  $mapping = array();
  foreach ($field_names as $field_name) {
    $columns = isset($this->fieldDefinitions[$field_name]) ? array_keys($this->fieldDefinitions[$field_name]->getColumns()) : array();
    foreach ($columns as $index => $column) {
      $mapping[$field_name][] = $this->schemaHandler()->getFieldColumnName($this->fieldDefinitions[$field_name], $column);
    }
  }
  return $mapping;
}

Any schema handler would need to implement this function outside of the ContentEntitySchemaHandlerInterface, no?

Also, in bullet 2 of the issue summary, the following statement is made:

To hide this code organization detail from the outside SqlStorageInterface extends ContentEntitySchemaHandlerInterface.

I hadn't recalled this relationship so I added SqlStorageInterface to my map and discovered that the implementation has deviated from this description.

UML diagram of the Entity Storage and Database storage classes and interfaces.

SqlStorageInterface extends EntityStorageInterface. Maybe we just need to update the description in the issue summary?

And the last observation. The docblock of ContentEntitySchemaHandlerInterface::getSchema() indicates that the method "Gets the full schema array for a given entity type.". This is true for ContentEntitySchemaHandler. For ContentEntityDatabaseStorage, the getSchema method implementation also invokes the entity_schema alter hook. So in this case, the returned array is the full schema plus anything else modules alter into it. Perhaps that should be noted in the docblock for ContentEntityDataStorage::getSchema() implementation in addition to the @inheritdoc statement.

jessebeach’s picture

FileSize
116.48 KB

So, in the spirit of "putting it all together", I've mapped out the landscape and a little context for the getSchema method in its various incarnations.

In the graph, the blue-background boxes are "field" class or interfaces. The pink boxes are notes indicating what exactly getSchema is doing on this class.

UML diagram of the getSchema method in its various incarnations on interfaces.

For fields, getSchema is defined in \Drupal\Core\Field\FieldStorageDefinitionInterface. Both FieldInstanceConfig and FieldConfig implement this interface. FieldInstanceConfig delegates its getSchema method to its stored instance of FieldConfig.

For entity storage, getSchema is defined in \Drupal\Core\Entity\Schema\ContentEntitySchemaHandler. Both ContentEntityDatabaseStorage and ContentEntitySchemaHandler implement this interface. ContentEntityDatabaseStorage delegates its getSchema method to its stored instance of ContentEntitySchemaHandler.

The getSchema method ContentEntitySchemaHandler loops through its list of FieldItemInterface[] instances and calls their getSchema methods, aggregating the returned arrays by field name.

plach’s picture

Here is reroll after #2225955: Improve the DX of writing entity storage classes, I hope I didn't break anything.

I am going to work a bit on making the schema match (almost) exactly the current HEAD one (I'm focusing on FKs now), hopefully my changes won't conflict with Tobias' ones.

@jessebeach:

Firstly, if the intent is to make it possible to swap out the schema handling (this is what I understand the intent to be)

Yep, that's the main goal :)

then we have a method in ContentEntitySchemaHandler that isn't declared in ContentEntitySchemaHandlerInterface, but that ContentEntityDatabaseStorage assumes exists.

Good catch, actually I was planning to move it on the storage class for two reasons:

  • since it is needed to build the table mapping, the dependency flow suggests that it belongs to the storage class and that it should be called by the schema handler;
  • ContentEntitySchemaHandlerInterface is storage-agnostic: it does not know (nor it needs to know) anything about tables and columns, in fact a MongoDatabaseStorage class could use it to return a schema definition array that makes sense for its implementation, possibly not even a Schema API array. By the way this is the reason why we decoupled it from the SqlStorageInterface :)

SqlStorageInterface extends EntityStorageInterface. Maybe we just need to update the description in the issue summary?

Yep, we definitely do. Moreover your nice diagram made me notice that ContentEntityDatabaseStorageInterface (which does not make too much sense to me, btw) should probably extend also FieldableStorageInterface.

[...] So in this case, the returned array is the full schema plus anything else modules alter into it.

I think the description is still valid as what's returned is always the entity schema: it's up to modules altering it in a sensible way. Adding a note the method implementation might make sense, but I think the hook documentation could be enough.

plach’s picture

Status: Needs work » Needs review
FileSize
7.46 KB
233.75 KB

This should fix foreign keys.

tstoeckler’s picture

OK, sorry it took me so long (again...) :-(

This fixes all reviews above, except:

  1. #136.13:
    +++ b/core/modules/user/user.install
    @@ -5,243 +5,36 @@
    +    db_insert('users')
    ...
    +    db_insert('users')
    

    We could try and just do a couple of entity saves.

    The code there is just being moved, I'd hate to introduce failures or anything due to an unrelated change. Opened #2249113: Use an entity save instead of db_insert() in user_install() for that.

  2. #152.3:
    +++ b/core/modules/forum/forum.install
    @@ -89,6 +78,22 @@ function forum_install() {
    /**
    + * Implements hook_entity_schema_installed().
    + */
    +function forum_entity_schema_installed(array $storages) {
    +  if (isset($storages['taxonomy_term'])) {
    +    // Create a default forum so forum posts can be created.
    

    Are we sure this hook is called when forum and taxonomy_term is not enabled at the same time? Again, another problem that we could avoid if we install the schema earlier I think.

    No we're not. In fact I'm pretty sure that it won't be. See below for my thoughts on the schema installation

  3. #168.2/3:
    +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +      if ($this->entityType->hasKey('revision')) {
    +        $this->layoutType |= static::LAYOUT_REVISION;
    +      }
    

    This could do with a comment to explain what this bitwise operation is doing.

    +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +      $data_table = $this->entityType->getDataTable();
    +      if ($data_table && $this->entityType->isTranslatable()) {
    +        $this->layoutType |= static::LAYOUT_MULTILINGUAL;
    +      }
    +    }
    

    Again, a comment would be most helpful to explain what this bitwise operation is doing.

    I simply forgot those. Although see below, for my thoughts on the layout type.

  4. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -97,38 +153,241 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    +   */
    +  public function getSchema() {
    +    $schema = $this->buildSchema();
    +    $context = array('entity_type' => $this->entityType);
    +    // TODO Document this hook.
    +    $this->moduleHandler()->alter('entity_schema', $schema, $context);
    +    return $schema;
    +  }
    

    Secret hooks :)

    I added this to the issue summary.

    See below for my thoughts on hook_entity_schema_alter()

OK, so there is a couple of things I would like to do:

  1. Regarding schema installation I think Berdir's points above are proof enough that the current approach does not fly. Before hacking this into the ModuleHandler, though, I'd like to try the following: Use the approach used in the cache system of putting all the queries in a try {} block and then trying to create the schema and re-running the query in a catch. If that does work it would solve a lot of our problems magically.
  2. Regarding hook_entity_schema_alter() I feel pretty strongly that we shouldn't introduce such a hook. hook_schema_alter() has always been pretty weird, and here it's just as weird, but IMO also completely unnecessary. You can swap out the storage controller, alter the field definitions, .... There really shouldn't be any need to alter the schema.
  3. Regarding the layout type I agree with @fago (and @plach?) that the constants don't provide much benefit. I'll look into ripping that out, but I don't think it's a priority.
plach’s picture

Thanks! Some answers to #202:

  1. Yep, it seems the current approach for schema installation is borked. As I said above, I didn't think about fiddling with the module handler directly, that's probably our better option. If then we are able to move things back to a hook implementation or an event, that's even better :)
  2. I am bit scared by hook_schema_alter() too, but when talking about this stuff with @Berdir on our way back from Szeged, he pointed out that it's not our job to prevent people from doing stupid things and that an alter hook could be useful. At least this is what I remember of our conversation :)
    I am more than open to reconsidering it, if @Berdir does not feel strongly about it. We could even remove it for now and add it back later if needed, it would just be an API addition. If we go this way we can get rid of the ::buildSchema() methods and just go with the ::getSchema() ones.
  3. As I said, initially I thought they would be way more useful, looking again to them now they look pretty redundant. The real reason beyond them is I never get a chance to use bitwise operators these days :D

Going to review the code now.

plach’s picture

Status: Needs work » Needs review
FileSize
235.35 KB
6.05 KB

This should fix many tests.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
7.81 KB
3.38 KB
243.35 KB

This should be green again.

Some explanations:
- DrupalUnitTestBaseTest:
- Some assertions are removed, but no actual test coverage is removed: The test previously did the following:
- First test that installing a table from an enabled module works
- Second test that installing a table from a non-enabled module does not work
- Third enable the module from the previous step and test that installing the table now works.
- As you can see, the third step and the first provide identical coverage. That is way some assertions are removed and a little code is moved around.

- MigrateTaxonomyTermTest:
- Taxonomy terms are incredibly weird right now, as:
- They have a 'parent' field item which is defined as a single-value integer field in the field definitions, with a default value of 0.
- It is in fact a multi-value field that is stored in taxonomy_term_hierarchy table. This is fixed in the attached patch. Previously we were generating a schema field for the parent due to this, which this also fixes.
- When it doesn't have an actual parent it gets a single value of 0.
- The test was asserting that the field value is actually NULL, where it is 0 due to the above. I changed it to assert the correct (?) behavior.
- I have no idea why this patch changes that behavior.

Also looked into the taxonomy_term.vid max_length override and - instead of hardcoding the max length in the schema - I added 'max_length' support to EntityReferenceItem. The field definition already declares a max_length, so we just have to use it. Checked that the schema is generated correctly.

tstoeckler’s picture

Assigned: tstoeckler » Unassigned
Status: Needs work » Needs review
FileSize
748 bytes
244.08 KB

Here we go. Should be green for realz.

Also unassigning. I do plan to work on this and finish off the few remaining todos but don't want to hold anyone back in case I don't get to it.

tstoeckler’s picture

Here we go. This drops hook_entity_schema_alter(). Discussed this briefly in IRC with @Berdir and he didn't have a concrete use-case in mind either. Surely people will find some crazy way of using but I'd rather have them alter the field definitions or swap out the schema handler if they really need it. Since we want to support schema updates when field definitions change having altered schemas is really, really weird. I personally see this similar to an alter hook for config data, which we also don't have.

I also tried to employ the try-catch mechanism for creating schemas that the cache system uses. I literally copied a lot of code from there. Because the storage controllers already have a save->doSave separation I couldn't use that. I am currently using anonymous functions for that but would appreciate better suggestions.

This allowed me to remove hook_entity_schema_(un)installed(). Which should make this patch at least a tad smaller. This also makes #2249113: Use an entity save instead of db_insert() in user_install() not only a random nice-to-have issue but actually a (soft) blocker of this. I put a workaround in user_install() for now.

I suppose we could also remove DrupalUnitTestBase::installEntitySchema() but I didn't do that for now, it should work as is.

This will make #2068325: [META] Convert entity SQL queries to the Entity Query API a hard blocker of this, unfortunately. I hacked around one query in comment_get_thread() so I could at least save and display a node.

I also had to introduce 2 hacks into Views module, which I don't really know what to do with. We're trying very hard to discourage people from querying the DB directly but Views is built on the opposite. At the very least we should have a EntityQuery extends Query (well not *that* EntityQuery, but...) so that we don't have to put the hack in the generic Views Query class, but have a dedicated Entity one. Similarly, we should have a EntityPager class.

Let's see what else breaks :-/

Feel free to suggest to revert the try-catch changes. I too really wanted to avoid the entity query issue as a blocker. I *think* hacking the schema installation into the module handler - for now - would sufficiently solve the problem discussed above as well, but @plach was fairly adamently against it. So not really sure how to proceed.

tstoeckler’s picture

FileSize
245.49 KB

Doh, forgot to merge 8.x. Sorry for the noise. The interdiffs are correct, though.

tstoeckler’s picture

So this hunk is of course not intentional, will remove on next re-roll:

diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index d48ae83..a151c9c 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -439,7 +439,7 @@ function install_begin_request(&$install_state) {
     $config = BootstrapConfigStorageFactory::get()->listAll();
     if (!empty($config)) {
       $task = NULL;
-      throw new AlreadyInstalledException($container->get('string_translation'));
+      //throw new AlreadyInstalledException($container->get('string_translation'));
     }
   }
 
plach’s picture

Assigned: Unassigned » tstoeckler
Status: Needs work » Needs review

Feel free to suggest to revert the try-catch changes. I too really wanted to avoid the entity query issue as a blocker. I *think* hacking the schema installation into the module handler - for now - would sufficiently solve the problem discussed above as well, but @plach was fairly adamently against it. So not really sure how to proceed.

I am definitely not against hard-coding the entity schema installation in the module-handler (see #204.1): please (please!), let's revert the try/catch change, it feels very fragile and awkward :(

I reviewed all the lastest interdiffs, the only stuff that is bothering me (aside from the try/catch change) is from #202:

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -30,7 +30,7 @@
    +class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements ContentEntityDefaultDatabaseStorageInterface, ContentEntitySchemaHandlerInterface {
    

    This shows exactly what bothers me of this interface: interfaces are used to hide implementation details, this one mandates or at least strongly suggests an exact implementation. Any storage class implementing it and not respecting some strict assumptions will probably end-up breaking stuff. This interface is a lie IMHO :)

  2. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -525,27 +525,32 @@ public function getFieldStorageDefinitions($entity_type_id, $include_custom_stor
    +   * @param bool $include_custom_storage
    

    This is overly verbose, would just $custom work?

  3. +++ b/core/modules/field/lib/Drupal/field/Entity/FieldConfig.php
    @@ -476,7 +476,9 @@ public function getSchema() {
    +    return TRUE;
    

    I am not sure about this change: this means that the storage class will no longer take care of configurable fields. Even if it's just temporary, this may negatively affect the parent issue, so I'd like to find another solution if possible.

  4. +++ b/core/modules/node/lib/Drupal/node/Entity/Node.php
    @@ -361,8 +361,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    -      ->setRevisionable(TRUE);
    

    Why we removed this? Language is revisionable and storage classes need to now it.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
35.91 KB
236.35 KB

Thanks for the review!

1. I removed ContentEntityDefaultDatabaseStorageInterface now. I did like it, but I'm fine with removing it.

2. We have a few other $include_foo parameters in EntityManagerInterface so I went with $include_custom as a compromise.

3. I reverted this and added a hack with a lengthy comment in ContentEntityDatabaseStorage. Note that currently using hasCustomStorage() would not be a problem but with #2144263: Decouple entity field storage from configurable fields or a follow-up thereof it would eventually become problematic.

4. I personally think isRevisionable() and isTranslatable() does not make sense at all for entity key fields as it's not at all defined what that actually means, but I reverted that change for now.

plach’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -156,10 +184,18 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
    +      return !$definition->isMultiple() && !($definition instanceof FieldConfigInterface);
    

    The idea is that we use "base entity schema" only for base fields, configurable fields should be bundle fields if I am not mistaken.

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -345,17 +374,17 @@ public function getTableMapping() {
    +        // data field values for all the reivisionable fields and the
    

    typo

tstoeckler’s picture

Re #220.1: Well field instances (FieldInstanceConfig) are bundle field definitions, but fields themselves (FieldConfig) are field storage definitions. Which makes sense, that is what they are conceptually. They are added in hook_entity_field_storage_info(). Bundle fields themselves cannot have any storage, as we don't have per bundle storage.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
506 bytes
236.04 KB

Sorry, should have tested this. This one installs at least and I can add a node. Let's see if I broken anything inadvertantly.

tstoeckler’s picture

plach’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -156,10 +184,18 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa
     $this->storageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id(), FALSE);
-    // @todo Remove this when multiple-value base fields are supported in
+    // @todo Remove the check for FieldDefinitionInterface::isMultiple() when
+    //   multiple-value base fields are supported in
     //   https://drupal.org/node/2248977.
+    // @todo Configurable fields currently have a storage that supports multiple
+    //   values even when their cardinality is set to 1. Therefore the
+    //   FieldDefinitionInterface::isMultiple() check is not sufficient to
+    //   filter them out. From the perspective of the storage
+    //   FieldConfig::isMultiple() should always return TRUE. This cannot be
+    //   implemented that way, however, as the same method is used to decide
+    //   whether or not to display a multiple widget on forms.
     $this->storageDefinitions = array_filter($this->storageDefinitions, function (FieldStorageDefinitionInterface $definition) {
-      return !$definition->isMultiple();
+      return !$definition->isMultiple() && !($definition instanceof FieldConfigInterface);
     });
 

@221: We are having troubles because we are retrieving field storage definitions, then we need to filter out field config storage definitions obviously. If we just get base field definitions (which ARE field storage definitions), through EntityManagerInterface::getBaseFieldDefinitions(), we don't need such a check. This was one of the main points we discussed in Szeged: we decide whether a field is stored in the base tables or gets CCK schema based on it being "optional", i.e. belonging only to certain entity type bundles.

plach’s picture

Status: Needs work » Needs review
FileSize
1.34 KB
236.19 KB

Let's try this

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
1.99 KB
236.24 KB

Let's see if this is already enough. It fixes VocabularyUnitTest at least and ContentEntitySchemaHandlerTest.

tstoeckler’s picture

Status: Needs work » Needs review

Ahh, so the fails from above were actually fixed, the new fails are due to a bug that was introduced by #2201051: Convert path.module form alters to a field widget. PathItem does not declare any 'columns' in its schema (because the values are stored externally) But getSchema() calls $schema['columns'] unconditionally to see if the field item declares columns with reserved words. It's just that PathItem::getSchema() is never called in 8.x

tstoeckler’s picture

FileSize
944 bytes
237.16 KB

Oops forgot the patch+interdiff.

tstoeckler’s picture

FileSize
22.55 KB
242.14 KB

This introduces TableMappingInterface.

I think I've found an API that is reasonably nice for both using the mapping as well as building it. I'd love your thoughts on it, though!

Let's see what this breaks.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
2.33 KB
242.33 KB

Yeah, late night patch rolling is not always a good idea.

plach’s picture

Status: Needs work » Needs review

Overall, changes look good to me, thanks! I still think in the stroage class we should be using base field definitions instead of storage definitions (see #224). Storage field deifnitions make sense in the table mapping stuff though, as we should probably return also cck tables in the getTables() method (to be done in #2144263: Decouple entity field storage from configurable fields, probably). We can differentate between the base vs bundle fields by checking whether the storage definition implements FieldDefinitionInterface.

  1. +++ b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php
    @@ -0,0 +1,87 @@
    +   *       'format' => 'description_format',
    ...
    +   * 'description_format' in the example above) depend on the table mapping
    

    Missing double underscore.

  2. +++ b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php
    @@ -0,0 +1,87 @@
    +   * The values of the inner array ('title', 'description__value', and
    

    I think 'title' should be replaced with 'value' if I am understanding this correctly.

  3. +++ b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php
    @@ -0,0 +1,87 @@
    +   *   An array where the keys are the names of the entity fields and the values
    +   *   are in turn arrays whose keys are the names of the columns as specified
    +   *   in the field item's schema() method and the values are the respective
    +   *   database column names for the respective entity fields.
    

    I am wondering whether this description is too implementation-specifc. Would it make sense to write something more generic?

plach’s picture

Status: Needs review » Needs work
tstoeckler’s picture

Status: Needs work » Needs review
FileSize
3.9 KB
243.26 KB

Re 2.: No in fact 'title' is correct there. The point of this paragraph is to try to explain which parts are implementation specific and which parts are generic. In this case naming the database column after the field name (== 'title') is the implementation detail. We could just as well choose to always use the property name as well even for single-column fields - i.e. 'title__value'. Please do provide suggestions on how to document this better.

I tried to expand the documentation a little bit in this patch.

Re 3.: The problem is that for the mapping to be truely generically usable as is, i.e. without further looking into field definitions, etc. We need all three pieces of the following pieces information and we need the relationship between them:
- the field name
- the column name as defined in getSchema()
- the actual db column name
Again, I am more than open to improvement suggestions, but I didn't really see what structure would be better for this.
Hmm... thinking about this I have the following idea: Instead of getFieldColumns() returning the nested structure as above. We have a getFieldNames($table_name) that simply returns a list of field names that are stored per entity type, and then we have a getFieldColumnMapping($field_name) which returns the array('value' => 'title') or array('value' => 'description__value', 'format' => 'description__format') structure, respectively. Thoughts?

According to the previous test result this should be green. Let's see.

Edit: Forgot the most important part: Thanks a lot (!) for the review. This feels close..

tstoeckler’s picture

FileSize
26.73 KB
236.86 KB

This implements the idea from #238. It forced me to add a line of code in a few places but it allowed to remove the lengthy documentation on the array structure, which IMO is a sign of a more sane API.

This also changes back to using baseFieldDefinitions() instead of fieldStorageDefinitions(). I still think the latter is more correct, but we can re-hash that debate when we add support for multi-value fields at which point there will actually be a difference. For now it's just a semantic debate.

I will work on adding test coverage next.

Btw: I have one question, that I forgot to ask the whole time. Why are we using 'field__' for field-level unique keys/indexes/foreign keys and not the entity type ID? I mean they are still within the realm of an entity type?!

jessebeach’s picture

The test fail in #239 is due to $entity_manager->getBaseFieldDefinitions($entity_type->id()), where $entity_type->id() equals shortcut, is returning NULL. This is happening in the __construct method of ContentEntityDatabaseStorage.

jessebeach’s picture

The test fail in #239 is due to $entity_manager->getBaseFieldDefinitions($entity_type->id()), where $entity_type->id() equals shortcut, is returning NULL. This is happening in the __construct method of ContentEntityDatabaseStorage.

Derp, my bad. base fields are returning just fine for shortcut. Still looking :)

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
822 bytes
236.83 KB

Sorry, I should really know to run unit tests locally by now, before submitting patches.

Here we go.

Working in better test coverage now.

tstoeckler’s picture

FileSize
28.66 KB
250.8 KB

Here we go. This brings 100% coverage for DefaultTableMapping and greatly expands the test coverage of ContentEntitySchemaHandler. Still no revisionable / translatable layouts, but full test coverage of index / foreign key generation.

Speaking of which, I think our naming standard for foreign keys is a little bit strange: for single-column fields it is "field__$field_name" and for multi-column fields it is "field__$field_name_$specifier" where $specifier is the original name of the foreign key. I don't really see a need for this differentiation but I'll leave that up to @plach to decide.

Writing the test coverage made be refactor a few internal things minorly. I.e. before we were calling getSchema() and getDescription() multiple times, so I optimized that. I also noticed that for multi-column fields we duplicate the schema description currently, i.e. all columns for a field simply get the field description as schema description. Let's sort that out in #2232471: Discuss and standardize field definition descriptions == schema descriptions, though. I think I might have an idea for that...

yched’s picture

From #239 :

Why are we using 'field__' for field-level unique keys/indexes/foreign keys and not the entity type ID? I mean they are still within the realm of an entity type?!

Yeah, that sounds wrong. Probably an overlook from when we moved storage from per-field to "per [entity type, field name]" in #1497374: Switch from Field-based storage to Entity-based storage ?

think our naming standard for foreign keys is a little bit strange: for single-column fields it is "field__$field_name" and for multi-column fields it is "field__$field_name_$specifier" where $specifier is the original name of the foreign key. I don't really see a need for this differentiation but I'll leave that up to @plach to decide

Agreed that the difference is not ideal. Then again, doesn't it (kind of) reflect the current column names for base fields in base tables ? (just $field_name for single-column field, $field_name__$column for multi column). I guess it's better to keep the two in sync.

plach’s picture

I quickly skimmed through the changes and they (again) look good to me thanks! I will do a deeper review later today. I think we still owe @fago the removal of the layout type constants, but I can try to work on that myself if you wish to work on improving test-coverage further.

Why are we using 'field__' for field-level unique keys/indexes/foreign keys and not the entity type ID? I mean they are still within the realm of an entity type?!

The original idea was reducing the risk of clashes between stuff automatically generated from field definitions and things manually added by the storage class. If not prefixing field stuff with the entity type id feels wrong I think we can safely add such a prefix:

node_field__stuff
node__stuff
I think our naming standard for foreign keys is a little bit strange: for single-column fields it is "field__$field_name" and for multi-column fields it is "field__$field_name_$specifier" where $specifier is the original name of the foreign key.

I don't remember whether this was intentional, but I don't see the need for such a difference either. Probably it's just a failed attempt to implement what @yched says in #245.2 :)

+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php
@@ -0,0 +1,267 @@
\ No newline at end of file

Oops :)

plach’s picture

Assigned: tstoeckler » Unassigned
Status: Needs review » Needs work

Since we are getting close to being ready here I performed a diff between the entity schema tables before/after the patch. These are the relevant differences:

  • Dropped SQL defaults / NOT NULL constraints (aside from mandatory field values), since defaults should be provided at data structure level.
  • Added some additional indexes, as specified by field schema or generated schema.
  • In some places we have different int lengths for IDs (serial and entity references) - int(11) vs int(10) unsigned.

These are the table-specifc differences:

  • aggregator_feed: hash - varchar(64) vs varchar(255), image - longtext vs text
  • aggregator_item: guid - text vs longtext
  • comment: status - tinyint(3) unsigned vs tinyint(4), mail - varchar(64) vs varchar(254), homepage - varchar(255) vs text
  • custom_block: info - varchar(128) vs varchar(255)
  • custom_block_revision: info - varchar(128) vs varchar(255), log - longtext vs varchar(255)
  • file_managed: uri - varchar(255) vs text, filesize - bigint(20) unsigned vs int(10) unsigned, status - tinyint(4) vs int(11), created/changed - int(10) unsigned vs int(11)
  • node: type - varchar(32) vs varchar(255)
  • node_field_data: type - varchar(32) vs varchar(255), status/promote/sticky: int(11) vs tinyint(4)
  • node_field_revision: type - varchar(32) vs varchar(255), status/promote/sticky: int(11) vs tinyint(4)
  • node_revision: log - longtext vs varchar(255),
  • shortcut: shortcut_set - varchar(32) vs varchar(255), weight/route_name/route_parameters moved to shortcut_field_data
  • shortcut_field_data: title - varchar(32) vs varchar(255)
  • users: name - varchar(60) vs varchar(255), pass - varchar(128) vs varchar(255)

Some of the above can be fixed by changing field definitions but I think most of them is ok. I will post a detailed plan tomorrow.

plach’s picture

Assigned: Unassigned » tstoeckler
Status: Needs work » Needs review
FileSize
660 bytes
246.21 KB

Minor adjustment

tstoeckler’s picture

Here we go. This fixes #245/#246 and completes the test coverage for ContentEntitySchemaHandler. The only thing left to test is ContentEntityDatabaseStorage::getTableMapping().

Discussed the index and foreign key naming with @plach and we agreed on the following pattern: ENTITY_TYPE_ID_field__FIELD_NAME__KEY where KEY is the key used for the index/unique key/foreign key in the field schema declaration.

This also removes the table layout constants per the above.

I had to produce several interdiffs to due a bunch of merges.

Marking "needs review" for the bot, but this is still needs work per #248.

Edit: Crosspost with #249, but included that as well (including interdiff :-P).

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
4.97 KB
261.54 KB

A) I accidentally removed more code that I wanted to.

B) I fixed the NOT NULL => TRUE for entity keys above, but that apparently causes other problems. I will have to investigate that.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
5.85 KB
261.56 KB

tstoeckler--

tstoeckler’s picture

FileSize
6.76 KB
261.94 KB

This should work. Added a lengthy comment on why revision_id cannot be NOT NULL.

tstoeckler’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
1.29 KB
262.05 KB

Ahh, that 'label' is also an entity key always throws me off. That should not be required either.

Also updating the issue summary a bit.

Berdir’s picture

Status: Needs work » Needs review
FileSize
263.2 KB
2.54 KB

Crazy stuff, as usual with rest.module ;)

Shortcut is also tricky, the existing code there was broken, WimLeers recently noticed that too in #2241235: Shortcut/ShortcutSet entity types should use cache tags, this is a slightly different fix without the weird isOriginalId() condition. Also added a not-syncing check there.

Based on a discussion with @alexpott, this code should probably live in the UI and not in postSave(), it depends on the user creating the shorcut to check which one is the default, so that's pretty weird to live there.. but that goes beyond what we have to fix here.

fago’s picture

Great to see the TableMapping class + interface, that's much better than the arrays. Even more awesome you've been able to do away with the table layout constants already!

   * Adds field columns for a table to the table mapping.
   *
   * @param string $table_name
   *   The name of the table to add the field column for.
   * @param string[] $field_names
   *   A list of field names to add the columns for.
   *
   * @return $this
   */
  public function addFieldColumns($table_name, array $field_names) {
    $this->fieldNames[$table_name] = $field_names;

I think this should be called setFieldNames as it overwrites existing ones. Maybe a better name would be setTableFields() as it defines which fields go into that table.

  /**
   * Returns a list of all database columns for a given table.
   *
....
   */
  public function getAllColumns($table_name);

I think this can be called just getColumns($table_name) - as there are no different ways between getting all or not-all columns.

  /**
   * {@inheritdoc}
   */
  public function getColumnMapping($field_name) {

I must say this is a bit confusing as it's a mapping part of another mapping (table mapping). Could we just call this getTableColumnNames() maybe, i.e. always differentiate between a "field column" and a "table column" ?

protected function processIdentifierSchema(&$schema, $key) {
hm, could we solve that by adding a separate "identifier" field type maybe? That said, we should probably mark the "revision id" as revisionable as it values change by revision also.

Awesome to see the table mappings being applied in mapToStorageRecord(), #2232427: Allow field types to control how properties are mapped to and from storage is a great follow-up here.

@ContentEntityDataBaseStorage

  /**
   * {@inheritdoc}
   */
  public function getTableMapping() {
    if (!isset($this->tableMapping)) {
      $this->tableMapping = new DefaultTableMapping($this->storageDefinitions);
....

As I'd see it, all that table mapping customizations do not fit in the storage class as the one controls the table mapping should be solely the schema handler. The schema handler tells us how the table mapping looks like, so it should be created/configured there. So could we move that code over there and just forward getTableMapping() to the schema handler?
Ideally, I think the storage class itself would not know about the table mapping at all and just handle all tables in a generic fashion, similar as attachPropertyData() already does. That's something we could look at in a follow-up though.

Found a typo:

@@ -135,7 +135,7 @@ protected function getTables() {
    *   The table schema to add the field schema to, passed by reference.
    * @param string $field_name
    *   The name of the field.
-   * @param string[] $column_mappi
+   * @param string[] $column_mapping
plach’s picture

As I'd see it, all that table mapping customizations do not fit in the storage class as the one controls the table mapping should be solely the schema handler. The schema handler tells us how the table mapping looks like, so it should be created/configured there. So could we move that code over there and just forward getTableMapping() to the schema handler?

This would imply loading the schema handler at each request, which we are trying to avoid as it will grow bigger. I think it's ok for the storage class to know how fields are mapped to tables. Note that this information does not prevent us from cleaning-up all the current assumptions and just relying on the mapping in the load/save code.

plach’s picture

+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
@@ -133,6 +133,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
+        // It is not possible to set the language to NULL as it is automatically
+        // re-initialized. As it must not be empty, skip it if it is.

I am wondering whther it would make sense to open a follow-up to allow the langcode to be actually NULL. I am not sure why we should special case language, since the other entity keys can be NULL.

Berdir’s picture

The problem with langcode is that we re-initialize it automatically in onChange(), so it's set to an empty value instead of NULL.

Wasn't sure if ContentEntityBase would be able to deal with that being NULL but maybe we can add a check to only update it if it's not NULL? somehow... +1 to try and improve that in a separate issue, but I still think that the crazy stuff that rest.module there is doing is well... crazy. There should be a better way than having to differentiate between setting a list to NULL and array().

tstoeckler’s picture

Here we go.

This adds some test coverage for the methods in ContentEntityDatabaseStorage (except for getTableMapping(), didn't get to that one yet) and fixes the above review, except for:

I think this can be called just getColumns($table_name) - as there are no different ways between getting all or not-all columns.

Well, there's still getExtraColumns(), so I think having just getColumns() in addition would be confusing.

As I'd see it, all that table mapping customizations do not fit in the storage class as the one controls the table mapping should be solely the schema handler. The schema handler tells us how the table mapping looks like, so it should be created/configured there. So could we move that code over there and just forward getTableMapping() to the schema handler?

I don't really care at this point, but as @plach above disagrees, I'll let you guys fight this out before changing anything. :-)

protected function processIdentifierSchema(&$schema, $key) {
hm, could we solve that by adding a separate "identifier" field type maybe? That said, we should probably mark the "revision id" as revisionable as it values change by revision also.

Let's please handle those in a follow-up, as both them are non-trivial/controversial IMO.

plach’s picture

If the end goal is having both a storage class and a schema handler class that are decoupled from the actual table layout, what about having a separate factory encapsulating the table-layout logic to instantiate the table mapping? We could exploit per-entity-type services to make the factory swappable.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
21.64 KB
285.41 KB

PhpStorm-- I don't know why it didn't rename the actual usages of the method. I should have checked, though, so tstoeckler-- also.

This should fix that.

Also added test coverage for getTableMapping() for simple and revisionable entity types. Translatable and "both" are up next.

I like #268. If noone beats me to it, I will look into that after completing test coverage. However, if I'm not mistaken

We could exploit per-entity-type services to make the factory swappable.

will not work for the same reason it didn't work for the schema handler.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
18.18 KB
299.73 KB

Fix new test introduced in #2257421: Convert CommentDefaultFormatterCacheTagsTest from web test to entity unit test.

Also completes test coverage. We now have complete test coverage for the introduced code.

tstoeckler’s picture

Assigned: tstoeckler » Unassigned
Issue summary: View changes
Status: Needs work » Needs review
FileSize
299.41 KB

The drop is moving fast today... This time it was #2224549: Simplify checking whether an entity type is revisionable. That's a good thing, though. Because we can use the method that that introduced here.

jessebeach’s picture

If the end goal is having both a storage class and a schema handler class that are decoupled from the actual table layout, what about having a separate factory encapsulating the table-layout logic to instantiate the table mapping? We could exploit per-entity-type services to make the factory swappable.

A fine goal. Two questions:

  1. Can we do this as a followup?
  2. This strikes me as non-API breaking, just code refactoring. Is that true?
jessebeach’s picture

Thank you for the test coverage for DefaultTableMappingTest.

This patch has well passed that size where one can comfortably review it in a day :) So, we need to satisfy the following steps, I believe, to feel confident that it's ready.

1. Testbost is green ('natch).
2. New test coverage is sufficient
3. Generated schemas match the schemas in 8.x HEAD

tstoeckler added test coverage for DefaultTableMapping. As far as I can tell the rest of the test coverage looks sufficient.

So, let's establish that the auto-generated schemas are the same as the declared schemas in HEAD and then move on this patch. I'm going to attempt to do this.

jessebeach’s picture

FileSize
145.08 KB
143.37 KB

Alight, I've created SQL dumps for an 8.x HEAD install and a site install with this patch. I installed every module and applied config for each core field. If you'd like to install this testing site, I've pushed it up to a sandbox. The dump files are attached.

https://drupal.org/project/bloat/git-instructions

I diffed them and noticed the following inconsistencies:

  1. In comment.install, we had the following entry for the Comment entity's cid field.
    'cid' => array(
      'type' => 'serial',
      'not null' => TRUE,
      'description' => 'Primary Key: Unique comment ID.',
    ),
    

    which ends up having length 11.

    In the patch, we declare this field as an int, which ends up having length 10. Many incrementing fields were 11 and now are 10 in length.

  2. The 'file_managed' schema has numerous inconsistencies in field types: filesize was a bigint, now it's an int.
  3. The shortcut table is missing the following properties: weight, route_name, route_parameters

    Before

    CREATE TABLE `shortcut` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Primary Key: Unique shortcut ID.',
      `uuid` varchar(128) DEFAULT NULL COMMENT 'Unique Key: Universally unique identifier for this shortcut.',
      `shortcut_set` varchar(32) NOT NULL DEFAULT '' COMMENT 'The bundle of the shortcut.',
      `langcode` varchar(12) NOT NULL DEFAULT '' COMMENT 'The language.langcode of the original variant of this shortcut.',
      `weight` int(11) NOT NULL DEFAULT '0' COMMENT 'Weight among shortcuts in the same shortcut set.',
      `route_name` varchar(255) DEFAULT NULL COMMENT 'The machine name of a defined Symfony Route this menu item represents.',
      `route_parameters` longblob COMMENT 'Serialized array of route parameters of this shortcut.',
      PRIMARY KEY (`id`),
      UNIQUE KEY `uuid` (`uuid`)
    )
    

    After

    CREATE TABLE `shortcut` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'The ID of the shortcut.',
      `shortcut_set` varchar(255) NOT NULL COMMENT 'The bundle of the shortcut.',
      `uuid` varchar(128) NOT NULL COMMENT 'The UUID of the shortcut.',
      `langcode` varchar(12) NOT NULL COMMENT 'The language code of the shortcut.',
      PRIMARY KEY (`id`),
      UNIQUE KEY `shortcut__uuid` (`uuid`),
      KEY `shortcut_field__shortcut_set__target_id` (`shortcut_set`)
    )
    

    The missing properties are now found in the shortcut_field_data table. I'm not sure if this done intentionally or not.

    Before

    CREATE TABLE `shortcut_field_data` (
      `id` int(10) unsigned NOT NULL COMMENT 'The shortcut.id of the shortcut.',
      `langcode` varchar(12) NOT NULL DEFAULT '' COMMENT 'The language.langcode of this variant of this shortcut.',
      `default_langcode` int(11) NOT NULL DEFAULT '1' COMMENT 'Boolean indicating whether the current variant is in the original entity language.',
      `title` varchar(32) DEFAULT NULL COMMENT 'The title of the shortcut.',
      PRIMARY KEY (`id`,`langcode`)
    )
    

    After

    CREATE TABLE `shortcut_field_data` (
      `id` int(10) unsigned NOT NULL COMMENT 'The ID of the shortcut.',
      `shortcut_set` varchar(255) NOT NULL COMMENT 'The bundle of the shortcut.',
      `langcode` varchar(12) NOT NULL COMMENT 'The language code of the shortcut.',
      `title` varchar(255) DEFAULT NULL COMMENT 'The name of the shortcut.',
      `weight` int(11) DEFAULT NULL COMMENT 'Weight among shortcuts in the same shortcut set.',
      `route_name` varchar(255) DEFAULT NULL COMMENT 'The machine name of a defined Route this shortcut represents.',
      `route_parameters` longblob COMMENT 'A serialized array of route parameters of this shortcut.',
      `default_langcode` tinyint(4) NOT NULL DEFAULT '1' COMMENT 'Boolean indicating whether field values are in the default entity language.',
      PRIMARY KEY (`id`,`langcode`),
      KEY `shortcut_field__shortcut_set__target_id` (`shortcut_set`)
    )
    
tstoeckler’s picture

In comment.install, we had the following entry for the Comment entity's cid field.

'cid' => array(
  'type' => 'serial',
  'not null' => TRUE,
  'description' => 'Primary Key: Unique comment ID.',
),

which ends up having length 11.

In the patch, we declare this field as an int, which ends up having length 10. Many incrementing fields were 11 and now are 10 in length.

Hmm... actually the ID field of an entity is declared as serial. See ContentEntitySchemaHandler::processBaseTable(). I don't know where the different lengths come from.

The shortcut table is missing the following properties: weight, route_name, route_parameters

...

The missing properties are now found in the shortcut_field_data table. I'm not sure if this done intentionally or not.

Yes, this is intentional. Those properties were probably not put on the base table because they are not translatable. However, we put all fields on the data table - translatable or not - so that we don't have to join on the base table on normal load queries. We should check the actual queries, however, whether this actually works currently. But, yes, that is how it should be. This issue has just revealed an inconsistency where Shortcut module is not following best practices.

plach’s picture

@269:

I probably did not explain myself well: when I say per-entity-type services I just mean that every storage class can implement its own ::createInstance() method and define its own dependencies and services. As long as we return an implementation of TableMappingFactoryInterface (I guess), there is really no point in requiring the service string identifier to be fixed. For instance:

class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlStorageInterface, ContentEntitySchemaHandlerInterface {

  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, FieldInfo $field_info, TableMappingFactoryInterface $table_mapping_factory) {
    // ...
  }

}

class NodeStorage extends ContentEntityDatabaseStorage {

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    return new static(
      $entity_type,
      $container->get('database'),
      $container->get('entity.manager'),
      $container->get('field.info'),
      $container->get('node.table_mapping_factory')
    );
  }

}

class CommentStorage extends ContentEntityDatabaseStorage {

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    return new static(
      $entity_type,
      $container->get('database'),
      $container->get('entity.manager'),
      $container->get('field.info'),
      $container->get('comment.table_mapping_factory')
    );
  }

}

@275:

We could do this as a follow-up, but I think it would imply a small (probably irrelevant) API change: we would probably end-up changing the services passed to ContentEntityDatabaseStorage::__construct(). I think it would be perfectly fine, though, this is getting huge.

plach’s picture

Wrt the schema differences outlined in #248 and #277, here are my thoughts:

  1. For serial id columns the int display length difference is due to the fact that we are now declaring those as unsigned, so they require one-less character to be displayed, in fact both int(11) and int(10) unsigned would display 10 digits (as they both have a 4 byte storage capacity -> 2^32), but the former would optionally display also the minus sign. From my POV the patch is actually fixing things by declaring ids with the unsigned attribute, but we can remove it otherwise.
  2. For aggregator stuff: if making the {aggregator_feed}.image column smaller is a problem (IMHO the current limit of 65535 bytes is ok), we should change the field definition to string_long.
  3. For comment stuff: the only concerning change is the homepage column, in fact by changing it from varchar to text we might make certain type of queries slower:

    Instances of BLOB or TEXT columns in the result of a query that is processed using a temporary table causes the server to use a table on disk rather than in memory because the MEMORY storage engine does not support those data types (see Section 8.8.5, “How MySQL Uses Internal Temporary Tables”). Use of disk incurs a performance penalty, so include BLOB or TEXT columns in the query result only if they are really needed. For example, avoid using SELECT *, which selects all columns.

    (from the MySQL reference manual)
    To address this we should probably change the UriItem schema definition to varchar(2048). This size is supported since MySQL 5.0.3 so we should be fine as our minimum requirement is MySQL 5.0.15.

  4. For custom block stuff: we are changing log from longtext to varchar(255), which is going to severely reduce the revision log maximum size. We probably need to change the log definition type to string_long.
  5. For file stuff: the uri column would be fixed by the UriItem change suggested in #3; the filesize column probably requires a new big_int field item, à la string_long; the status column needs to become a boolean item.
  6. For node stuff: the type column needs to be limited to a max of 32 as the other bundle names; status/promote/sticky definitions should become boolean; the log column should become a string_long.
  7. For shortcut stuff: the shortcut_set column needs to be limited to a max of 32 as the other bundle names.
  8. The other differences in varchar lengths should be fixed by specifying the original lenghts as max_length in the related field definitions, the schema handler should use that information to set the max size of varchar columns.

I think we can fix most differences here but the ones requiring bigger changes (if any) could be deferred to a follow-up (discussed this with @catch in Szeged).

jessebeach’s picture

We could do this as a follow-up, but I think it would imply a small (probably irrelevant) API change: we would probably end-up changing the services passed to ContentEntityDatabaseStorage::__construct(). I think it would be perfectly fine, though, this is getting huge.

I doubt anyone will be this deep into entities and storage during the beta phase. I'd venture we'll be well within the limits of acceptable churn to do this as a followup rather than further complicate an already massive patch.

I logged the issue #2265779: Allow storage classes to define their own dependencies and services.

Berdir’s picture

Nobody is quite sure on how to deal with the dependency injection for base/subclasses and if it's an API change or not I think. We'd like say it's not one but the problem is that as soon as you have a storage that needs something else, you need to duplicate the parent create() and __construct() methods, so changing it *will* break those subclasses.

We also do that for content entity storage, see comment and user implementations. Those would have to change too.

But agreed that we can still do that later.

jessebeach’s picture

#280:2 "For aggregator stuff: if making the {aggregator_feed}.image column smaller is a problem (IMHO the current limit of 65535 bytes is ok), we should change the field definition to string_long."

This gives us, conservatively, about 16K characters. I think this length will be plenty.

#280:3, updated UriItem to varchar(2048)

#280:4, the log field is now string_long

#280:5.1, fixed with the change in #280:3

#280:5.2, introduced integer_big type. For the life of me, I can't find tests for the data types, so I don't know how to add coverage for this.

#280:5.3, changed integer to boolean.

#280:6-8, Haven't gotten to them yet.

tstoeckler’s picture

  1. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerBigItem.php
    @@ -0,0 +1,42 @@
    +class IntegerBigItem extends IntegerItem {
    

    I think we should rather introduce a new setting on the existing Integer class. Even though we have String and StringLong, etc. we did the same thing for the 'unsigned' setting instead of introducing an UnsignedInteger class.

  2. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php
    @@ -52,7 +52,8 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
    +          'length' => (int) $field_definition->getSetting('max_length'),
    

    Yay, this is an oversight. Nice catch!

  3. +++ b/core/modules/file/lib/Drupal/file/Entity/File.php
    @@ -255,12 +255,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    $fields['status'] = FieldDefinition::create('boolean')
           ->setLabel(t('Status'))
           ->setDescription(t('The status of the file, temporary (0) and permanent (1).'));
    

    It seems we should update the description also?

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
301.98 KB
tstoeckler’s picture

Status: Needs work » Needs review
FileSize
655 bytes
301.93 KB

Reverting the UriItem schema change from #283 for now, apparently SQL doesn't like that for some reason.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
684 bytes
302.26 KB

That was my fault. Incorrect merge above...

plach’s picture

+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php
@@ -52,7 +52,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
+          'type' => 'text',

This was addressing #280, we should fix the test instead.

jessebeach’s picture

Thanks for the reroll on those 8.x changes. That was a nasty merge indeed.

Reverting the UriItem schema change from #283 for now, apparently SQL doesn't like that for some reason.

The Feed entity sets the max_length setting to NULL, causing the length of our varchar to be (int) NULL...BOOM. I've removed the settings override in the Feed Entity so it uses the 2048 default.

I'm working on the remaining comments in #280 now.

jessebeach’s picture

Status: Needs work » Needs review
FileSize
302.88 KB
5.68 KB
  1. 280:6.1, set the length of the 'type' field value to BUNDLE_MAX_LENGTH
    $fields['type'] = FieldDefinition::create('entity_reference')
     ->setLabel(t('Type'))
      ->setDescription(t('The node type.'))
      ->setSettings(array(
        'target_type' => 'node_type',
        'max_length' => BUNDLE_MAX_LENGTH,
      ))
      ->setReadOnly(TRUE);
    
  2. 280:6.2,

    status/promote/sticky definitions should become boolean

    They are already. Maybe tstoeckler updated these?

  3. 280:6.3

    the log column should become a string_long

    Updated.

  4. 280:7

    Set the max_length field setting to BUNDLE_MAX_LENGTH.

  5. 280:8, the rest of the varchar definitions look fine. The title of the shortcut_set went from 32 to 255. That seems fine.
  6. 285:1
    +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerBigItem.php
    @@ -0,0 +1,42 @@
    +class IntegerBigItem extends IntegerItem {
    

    I think we should rather introduce a new setting on the existing Integer class. Even though we have String and StringLong, etc. we did the same thing for the 'unsigned' setting instead of introducing an UnsignedInteger class.

    I just had the File entity set the size of the filesize field to big.

    $fields['filesize'] = FieldDefinition::create('integer')
        ->setLabel(t('File size'))
        ->setDescription(t('The size of the file in bytes.'))
        ->setSettings(array(
          'unsigned' => TRUE,
          'size' => 'big',
        ));
    
  7. 285:3
    +++ b/core/modules/file/lib/Drupal/file/Entity/File.php
    @@ -255,12 +255,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    $fields['status'] = FieldDefinition::create('boolean')
           ->setLabel(t('Status'))
           ->setDescription(t('The status of the file, temporary (0) and permanent (1).'));
    

    It seems we should update the description also?

    Indeed, updated to: "'The status of the file, temporary (FALSE) and permanent (TRUE).'".

The interdiff includes the UriItem change from #292. I neglected to push that commit to my diffing repo for piling on a few more commits, so...it's there.

tstoeckler’s picture

Without having diffed the schema myself (in a while, at least) the changes look good to me.

Except for one little thing:

+++ b/core/modules/file/lib/Drupal/file/Entity/File.php
@@ -255,14 +255,17 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
-      ->setSetting('unsigned', TRUE);
+      ->setSettings(array(
+        'unsigned' => TRUE,
+        'size' => 'big',
+      ));

Here and elsewhere:

Minor, but please use setSetting() multiple times instead of setSettings(). See #2236903: setSettings() on field definitions can unintentionally clear default values for the reasoning. (Only fix this when re-rolling anyway: again, it's minor! :-))

tstoeckler’s picture

+++ b/core/modules/node/lib/Drupal/node/Entity/Node.php
@@ -356,7 +356,10 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+        'max_length' => BUNDLE_MAX_LENGTH,

+++ b/core/modules/shortcut/src/Entity/Shortcut.php
@@ -167,7 +167,10 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+        'max_length' => BUNDLE_MAX_LENGTH,

Oops, forgot this part: This is now EntityTypeInterface::BUNDLE_MAX_LENGTH

That's why the install is failing.

jessebeach’s picture

Status: Needs work » Needs review
FileSize
1.21 KB
302.92 KB

Namespace the constants!

jessebeach’s picture

Status: Needs work » Needs review
FileSize
304.05 KB
4.25 KB

setSettings() to setSetting().

yched’s picture

I don't fully get the introduction of the 'max_length' setting for EntityRef fields.
- It's a bit awkward as it's meaningless for content entities.
- Why would it be the responsibility of the code defining the field to know the length of IDs used by the referenced (config) entity type ?
- Also, unless I'm mistaken, ER fields on config entities store the full config ID, right ? So why would some fields use a value different than 255 ?

Also, FWIW, I'm not too fond of switching to individual setSetting() calls - left a note in #2236903: setSettings() on field definitions can unintentionally clear default values.

fago’s picture

oh no, more than 300 comments! :D

I think we should rather introduce a new setting on the existing Integer class. Even though we have String and StringLong, etc. we did the same thing for the 'unsigned' setting instead of introducing an UnsignedInteger class.

Agreed. I'm wondering whether those could be constraints only even, but we have already max_length as setting so let's continue like that for now.

jessebeach’s picture

I don't fully get the introduction of the 'max_length' setting for EntityRef fields.
- It's a bit awkward as it's meaningless for content entities.
- Why would it be the responsibility of the code defining the field to know the length of IDs used by the referenced (config) entity type ?
- Also, unless I'm mistaken, ER fields on config entities store the full config ID, right ? So why would some fields use a value different than 255 ?

An excellent question. It took me a while to tease this apart.

Firstly, the EntityReference field makes a distinction between content and config entities in terms of the field type:

 public static function schema(FieldStorageDefinitionInterface $field_definition) {
    $target_type = $field_definition->getSetting('target_type');
    $target_type_info = \Drupal::entityManager()->getDefinition($target_type);

    if ($target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) {
      $columns = array(
        'target_id' => array(
          'description' => 'The ID of the target entity.',
          'type' => 'int',
          'unsigned' => TRUE,
          'not null' => TRUE,
        ),
      );
    }
    else {
      $columns = array(
        'target_id' => array(
          'description' => 'The ID of the target entity.',
          'type' => 'varchar',
          'length' => $field_definition->getSetting('max_length'),
        ),
      );
    }
...

For content entities, the field type is int(10). For config entities, we use varchar(255).

To test this, I created a node called: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.
I created a field called: field_bbbbbbbbbbbbbbbbbbbbbbbbbb

I then referenced this field instance from an entity reference field on an article. The target_id saved to the DB is: node.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.field_bbbbbbbbbbbbbbbbbbbbbbbbbb

This is a varchar(255) field.

The Node entity defines a base field type and the Shortcut entity defines a base field shortcut_set that reference a NodeType config entity. Without overriding the max_length property, this would be a varchar(255), like any other config entity target_id. But the length of the ID of a NodeType (bundle) can never be more than 32, so setting the max_length to 32 on these entities' entity reference base fields -- creating a varchar(32) db field -- is an optimization from the default db field length.

jessebeach’s picture

#302,

oh no, more than 300 comments! :D

I think we should rather introduce a new setting on the existing Integer class. Even though we have String and StringLong, etc. we did the same thing for the 'unsigned' setting instead of introducing an UnsignedInteger class.

Agreed. I'm wondering whether those could be constraints only even, but we have already max_length as setting so let's continue like that for now.

I did this is the latest patch. IntegerBigItem is removed. The filesize base field is now just an int with an overridden size property:

$fields['filesize'] = FieldDefinition::create('integer')
   ->setLabel(t('File size'))
   ->setDescription(t('The size of the file in bytes.'))
   ->setSetting('unsigned', TRUE)
   ->setSetting('size', 'big');
jessebeach’s picture

On comparing the generated table schema again, I noticed numerous inconsistencies where a db field had been NOT NULL and after this patch, became NULL.

I think I've tracked down why in Drupal\Core\Entity\Schema\ContentEntitySchemaHandler::addFieldSchema(). There's a loop in this method that sets the not null property of a fields schema to FALSE if the field name isn't in the keys defined for the table. See the last line of the code below.

foreach ($column_mapping as $field_column_name => $schema_field_name) {
    $column_schema = $field_schema['columns'][$field_column_name];

    $schema['fields'][$schema_field_name] = $column_schema;
    $schema['fields'][$schema_field_name]['description'] = $field_description;
    // Only entity keys are required.
    $keys = $this->entityType->getKeys() + array('langcode' => 'langcode');
    // The label is an entity key, but label fields are not necessarily
    // required.
    // Because entity ID and revision ID are both serial fields in the base
    // and revision table respectively, the revision ID is not known yet, when
    // inserting data into the base table. Instead the revision ID in the base
    // table is updated after the data has been inserted into the revision
    // table. For this reason the revision ID field cannot be marked as NOT
    // NULL.
    unset($keys['label'], $keys['revision']);
    $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $keys);
}

So any non-key field that invoked setRequired(TRUE) on the field definition would have this required property reversed here.

Which means fields that had been marked 'not null' => TRUE become 'not null' => FALSE.

I updated the code as follows, only setting the 'not null' to true if the field is one of the keys:

foreach ($column_mapping as $field_column_name => $schema_field_name) {
    $column_schema = $field_schema['columns'][$field_column_name];

    $schema['fields'][$schema_field_name] = $column_schema;
    $schema['fields'][$schema_field_name]['description'] = $field_description;
    // Only entity keys are required.
    $keys = $this->entityType->getKeys() + array('langcode' => 'langcode');
    // The label is an entity key, but label fields are not necessarily
    // required.
    // Because entity ID and revision ID are both serial fields in the base
    // and revision table respectively, the revision ID is not known yet, when
    // inserting data into the base table. Instead the revision ID in the base
    // table is updated after the data has been inserted into the revision
    // table. For this reason the revision ID field cannot be marked as NOT
    // NULL.
    unset($keys['label'], $keys['revision']);
    // Key fields may not be NULL.
    $is_required = in_array($field_name, $keys);
    if ($is_required) {
      $schema['fields'][$schema_field_name]['not null'] = $is_required;
    }
}

Which brought up some errors on install with required fields that don't have default values.

SQLSTATE[HY000]: General error: 1364 Field 'status' doesn't have a default value: INSERT INTO {users} (uid, uuid, name, mail, langcode) VALUES (:db_insert_placeholder_0,     [error]
:db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4); Array
(
    [:db_insert_placeholder_0] => 0
    [:db_insert_placeholder_1] => 152e32be-1cad-4de9-824f-79a82b17e0a6
    [:db_insert_placeholder_2] => 
    [:db_insert_placeholder_3] => 
    [:db_insert_placeholder_4] => en
)

I'm fixing these now.

yched’s picture

so setting the max_length to 32 on these
entities' entity reference base fields -- creating a varchar(32) db field --
is an optimization from the default db field length

Yes, I'm not sure it's worth the extra setting, and the added onus on the "field definer" to kow which max length makes sense depending on which config entity type is referenced. I'd tend to think that just using 'int' for content entities and varchar(255) for content entities would be good enough - but that's just my 2 cts :-)

Berdir’s picture

The problem with that is that those fields are often used in indexes for multiple fields, sometimes even as part of a primary key, and if the field is set to 255, then you need to manually limit the size of the field in the index, that's what we cleaned up in the issue that enforced that bundles must not be longer than 32 characters.

jessebeach’s picture

Committing #2249113: Use an entity save instead of db_insert() in user_install() fixed the user status field empty problems noted in #305.

This patch update fixes the problem that all non-key db fields were set to 'not null' => FALSE (meaning NULL is allowed) regarding of their DataType definition settings.

Now, having resolved that, I surfaced another issue. The method \Drupal\Core\TypedData\DataDefinition::setRequired(), which we see on the Node title field definition here:

$fields['title'] = FieldDefinition::create('string')
  ->setLabel(t('Title'))
  ->setDescription(t('The title of this node, always treated as non-markup plain text.'))
  ->setRequired(TRUE)
  ->setTranslatable(TRUE)
  ->setRevisionable(TRUE)
  ->setSetting('default_value', '')
  ->setSetting('max_length', 255)
  ->setDisplayOptions('view', array(
    'label' => 'hidden',
    'type' => 'string',
    'weight' => -5,
  ))
  ->setDisplayOptions('form', array(
    'type' => 'string',
    'weight' => -5,
  ))
  ->setDisplayConfigurable('form', TRUE);

Does not end up changing the value of the not null db field property; the default for the DataType, in this case string with a default 'not null' => FALSE, is always used. We can override a setting like 'length' with the setSetting() method because the static method StringItem::schema() calls up the field definition setting, which is directly manipulated in the title field declaration above.

public static function schema(FieldStorageDefinitionInterface $field_definition) {
  return array(
    'columns' => array(
      'value' => array(
        'type' => 'varchar',
        'length' => (int) $field_definition->getSetting('max_length'),
        'not null' => FALSE,
      ),
    ),
  );
}

So, this brings up the issue that settings in schema() are essentially hard-coded unless the value is pulled from the field definition settings with a default supplied.

I'd love some help with the setRequired() problem. I'm not sure where this should be handled. I fixed this for StringItem, but this approach isn't great since we'll need to change each of the other DataType items to allow 'not null' to be manipulated with setRequired().

yched’s picture

@Berdir : hm, indexes... yeah, makes sense I guess. I still find the "max_length setting" approach less than ideal, but I'll have to live with it.
Any chance config entity type could publicize somewhow the corect "length" to use when referencing them ? That way the 'length' could be silently set to the correct value by ERItem::schema() ?

yched’s picture

Regarding NULL / NOT NULL / isRequired() :

Yeah, historically in CCK, NOT NULL in the schema has never been tied to whether the field is required.

IIRC, handling of NULL was a nasty pain back in CCK with the "mixed / dynamic table layout" ("per-bundle" tables & "per-field" tables, and the fact that a field could move from one to the other).

It's possible that those reasons are mostly gone with the current "always store configurable fields in per-field tables" layout for configurable fields". Since we don't write any record in the per-field table when a field is empty, NULL or NOT NULL is not too relevant.

But then again we're now back to generating schemas for tables that hold several fields in base tables...
And the fact that a field is required doesn't necessarily mean you have valid, non NULL values to put in the tables :
- imagine a field that's "not required", and is stored in a table along with other fields : then, in the row for a given entity (revision) ID, you have columns for every field stored in the table, and the columns for that non-required field will have NULL values when the field is empty.
- now switch the field to "required" : this change only applies to the subsequent entity submits, existing entities still have the field empty, and those NULL values are still there in the table, so you can't suddenly switch the schema to NOT NULL = TRUE

Also, depending on the field type, a field value can store several "columns", and it's not obvious to determine for which columns the "required" property should translate to a NOT NULL. A "secondary" column can totally be NULL while the field itself is not empty.

Long story short : back then, we gave up on trying to closely assign NOT NULL in field schemas, and went with 'not null' => FALSE for all field column schemas...

plach’s picture

I didn't read the last comments with the attention they deserve, but I am not sure we need all this NOT NULL futzing: before coding we agreed that all schema columns will be nullable except for entity keys as defaults should be enforced at data structure level.

tstoeckler’s picture

Also, I already opened #2232477: Fatal when adding new fields with NOT NULL constraints in a base table that contains existing entities a while back to discuss that and that is still waiting for review. (And yes, it's in the issue summary... :-)) So can we move that discussion there?!

jessebeach’s picture

#311,

Yeah, historically in CCK, NOT NULL in the schema has never been tied to whether the field is required.

That makes sense. I was erroneously equating the two.

#312,

I didn't read the last comments with the attention they deserve, but I am not sure we need all this NOT NULL futzing: before coding we agreed that all schema columns will be nullable except for entity keys as defaults should be enforced at data structure level.

My only concern here is that many fields are explicitly marked as "NOT NULL" in the HEAD hook_install schemas. After the conversion in this patch, they've lost that value and are now simply NULL. The title field of nodes is canonical example. By not exposing the 'non null' property of column items for manipulation through setSettings(), all fields based on the core data types will be locked into the default value of 'not null' for that type.

jessebeach’s picture

Folks can also extend the basic data types in order to alter the column item values.

sun’s picture

Re: To NULL or to NOT NULL:

As long as the columns are not part of an index, the change doesn't make a difference. However, for columns that are part of an index, allowing NULL values should be used wisely, because affected indexes require an additional internal flag to track emptiness. That makes the index larger, and a larger index negatively affects filter/group/sort performance.

This is part of the one-pager about most basic database performance optimization rules, which everyone should know inside-out and follow by heart:

http://dev.mysql.com/doc/refman/5.6/en/data-size.html

jessebeach’s picture

The latest schema dumps.

jessebeach’s picture

We can address the NULL/NOT NULL differences in #2232477: Fatal when adding new fields with NOT NULL constraints in a base table that contains existing entities, a much smaller and focused patch. At 300KB, this issue's patch is already unwieldy. Consensus is that the schema differences introduced by this patch with NULL/NOT NULL values are not regressing current behavior.

jessebeach’s picture

FileSize
319.37 KB

The 'bigint' for file size isn't being plumbed through:

Screenshot of a diff comparing db schema in HEAD to the schema produced by this patch. The filesize db field is indicated. The values between schemas changed from bigint to int erroneously.

jessebeach’s picture

Status: Needs work » Needs review
FileSize
304.67 KB
5.53 KB

With the change to ContentEntitySchemaHandler:: addFieldSchema() in #308, non null is no longer explicitly set on all db fields. The existing schema verification tests assumed that not null is always present, so I removed the values from the items that no longer explicitly set it.

The isRequired() experiment code in #308 is NOT included in this patch.

I also fixed the bigint problem noted in #319.

Those are the last of my concerns. I'm ready to move on this patch.

jessebeach’s picture

These merge conflicts are blowing my mind. I can't tell where to take out the field info and where to keep it. I'm going to have to start this again tomorrow morning.

Anyone else is more than welcome to tackle this mess before then :)

Berdir’s picture

Status: Needs work » Needs review
FileSize
299.79 KB

Yeah, sorry, that was to be expected, plus side is, you don't need to worry about the field.info service anymore :)

Re-roll is based on a rebase, so no useful conflict diff or something, as that worked best..

plach’s picture

@sun:

Thanks for pointing that out, in IRC we agreed to address indexes + NULLable stuff in #2232477: Fatal when adding new fields with NOT NULL constraints in a base table that contains existing entities. Basically the idea is the we make NOT NULL every field for which we add an index automatically, fields adding indexes in their definition are in charge to evaluate whether it's the case to make their columns NULLable or not.

tstoeckler’s picture

  1. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php
    @@ -41,6 +41,7 @@ public static function defaultInstanceSettings() {
    +      'size' => NULL,
    
    @@ -91,6 +92,9 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
    +          // Expose the 'size' setting in the field item schema. For instance,
    +          // supply 'big' as a value to produce a 'bigint' type.
    +          'size' => $field_definition->getSetting('size'),
    

    Checking the schema generation code (\Drupal\Core\Database\Driver\mysql\Schema::getFIeldTypeMap()), possible values are 'tiny', 'small', 'medium', 'big', and 'normal', where 'normal' is the 'default'. So I think we should use 'normal' as the default value here. Also, since the defaultInstanceSettings() is the one and only source for information on the possible settings right now it would be extra awesome to add a comment listing the allowed values.

  2. +++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php
    @@ -17,6 +17,7 @@
    + * @group ponies
    

    You don't seriously think you could geth this into core unnoticed?! :-P Leave the ponies out of my otherwise beautiful unit test!

jessebeach’s picture

Drat! You and your attention to detail!

Honestly though, I realized I'd let that slip in. I was going to pull it out in a reroll. Honestly. I swear.

jessebeach’s picture

Goodbye my ponies.

The two test fails arose because the IntegerItem now has a default size property with the value normal. The expected return value of this field type in two migration tests just needed this property added to them.

jessebeach’s picture

Oops, forgot the comment requested by tstoeckler in #326.

pwolanin’s picture

the MapItem field is currently getting saved incorrectly. There is a patch to fix it, but berdir thought the change might have some impact on or overlap with this patch.

see: #2267753: ContentEntityDatabaseStorage::mapToStorageRecord hard-codes $entity->$name->value

tstoeckler’s picture

Thanks @pwolanin for the pointer. We're aware of the problem space and that issue is one step in the right direction. We still need #2232427: Allow field types to control how properties are mapped to and from storage however, to properly "fix" this and remove the 'serialize' hack here.

plach’s picture

I just completed a full review of the code and I think it looks quite good. We still have lots of @todos, but I think we should be ready to go. The amount of test coverage we are introducing is impressive, btw :)

Final reviews would be welcome now, as we should not be too distant from RTBC.

+++ b/core/modules/tracker/tracker.install
@@ -16,12 +16,18 @@ function tracker_uninstall() {
+  // @todo Is there some way to avoid this check?
+  if (\Drupal::database()->schema()->tableExists('node')) {

Do we still need this?

tstoeckler’s picture

FileSize
10.76 KB
302.29 KB

Thanks @jessebeach for keeping the ball rolling. Yay!

1. Simplified the 'not null' adding for entity keys.
2. Explicitly added 'not null' => TRUE for fields used in indexes to avoid the problem mentioned by @sun in #316
3. Removed the 'max_length' "feature" from EntityReferenceItem in favor of an explicit check for $entity_type->getBundleOf() to resolve @yched's concern from #310.
4. Removed unnecessary change per #333 that was still leftover from the lazy-schema creation.

I also think this is ready now. Everything from above has been resolved and we discussed this with @plach, @fago, @Berdir, and @jessebeach in IRC last week and also nothing came up there that hasn't been resolved now. So...

...who will dare to RTBC?! :-)

jessebeach’s picture

Status: Needs work » Needs review
FileSize
302.23 KB
633 bytes

The mail base field of the User entity cannot be set to 'not null' == TRUE to optimize performance; the field might be empty (an empty string for example) because a user does not have an email address. This is the case for the anonymous user. Install failed on this SQL exception.

I propose we set a 24 hour auto-RTBC on this issue since any qualified to RTBC has touched this issue in a significant way.

tstoeckler’s picture

Yay, @jessebeach, you rock! Makes a lot of sense. Thanks for always cleaning up after me... :-) !

plach’s picture

+++ b/core/modules/tracker/tracker.install
@@ -16,18 +16,12 @@ function tracker_uninstall() {
+  $max_nid = db_query('SELECT MAX(nid) FROM {node}')->fetchField();

Why are we reintroducing this SQL query? The entity query was fine, my only doubt was about doing it only after doing a SQL-specific check...

About RTBC, I think @fago didn't write code for this, so he could be a good candidate to RTBC it. It would also be good to get a +1 from @yched, if possible.

jessebeach’s picture

+++ b/core/modules/tracker/tracker.install
@@ -16,18 +16,12 @@ function tracker_uninstall() {
+  $max_nid = db_query('SELECT MAX(nid) FROM {node}')->fetchField();

Why are we reintroducing this SQL query? The entity query was fine, my only doubt was about doing it only after doing a SQL-specific check...

plach, I don't see this code in the patch in #336. Maybe you were reviewing an early version?

jessebeach’s picture

Status: Needs work » Needs review
FileSize
297.46 KB
6.28 KB

The patch in #336 no longer applies. Rerolled

yched’s picture

Didn't delve in the meaty parts yet (ContentEntityDatabaseStorage, Core\Entity\Schema, Core\Entity\Sql),
posting what I have on "the rest" - mostly minor stuff.

  1. +++ b/core/modules/comment/comment.install
    @@ -28,157 +28,10 @@ function comment_install() {
     }
    -
     /**
    

    Unintended empty line removal

  2. +++ b/core/modules/node/lib/Drupal/node/Entity/Node.php
    @@ -65,6 +65,23 @@ class Node extends ContentEntityBase implements NodeInterface {
    +  public function preSave(EntityStorageInterface $storage) {
    +    parent::preSave($storage);
    +
    +    // If no owner has been set explicitly, make the current user the owner.
    +    if (!$this->getOwner()) {
    +      $this->setOwnerId(\Drupal::currentUser()->id());
    +    }
    +    // If no revision author has been set explicitly, make the node owner the
    +    // revision author.
    +    if (!$this->getRevisionAuthor()) {
    +      $this->setRevisionAuthorId($this->getOwnerId());
    +    }
    +  }
    

    I can't seem to find where this behavior lived previously, and thus don't fully get how moving it is related to that patch. No biggie, just wondering.

  3. +++ b/core/modules/node/lib/Drupal/node/Entity/NodeType.php
    @@ -209,4 +209,14 @@ public static function preCreate(EntityStorageInterface $storage, array &$values
    +  protected function onUpdateBundleEntity() {
    +    // The bundle field definitions of nodes depend on the node type settings.
    +    // @see \Drupal\node\Entity\Node::bundleFieldDefinitions()
    +    $this->entityManager()->clearCachedFieldDefinitions();
    +    parent::onUpdateBundleEntity();
    +  }
    

    Does this only affect nodes / nodeTypes ?
    Isn't that potentially true for any (config) entity that is a bundle_of a (content) entity ?

  4. +++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
    @@ -133,6 +133,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +        // @todo: Use the langcode entity key when available.
    

    @todo needs an issue link ?

    (+ minor : the code would be slightly easier on quick visual parse if the $field_name == 'language' condition came first)

  5. +++ b/core/modules/views/lib/Drupal/views/Entity/View.php
    @@ -275,7 +275,10 @@ public function calculateDependencies() {
    +    // @todo Entity base tables are no longer registered in hook_schema(). Once
    

    @todo needs an issue link ?

  6. +++ b/core/modules/shortcut/src/Entity/Shortcut.php
    @@ -167,6 +167,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH)
    

    Not relevant anymore, according to @tstoeckler's #334 ?

    Also
    - one in Node.php
    - MigrateFieldInstanceTest::testFieldInstanceSettings() adds a 'max_length' to the expected settings of a file field
    - CustomBlock::baseFieldDefinitions() still has a 'max_length' for the 'type' field
    - Term::baseFieldDefinitions(), same for 'vid' field.

    (What I don't get is that those last two are already in current HEAD - got added in #1709960: declare a maximum length for entity and bundle machine names. Yet AFAICT EntityRefItem defines no 'max_length' setting ?)

  7. +++ b/core/modules/views/lib/Drupal/views/Plugin/views/query/Sql.php
    @@ -1425,7 +1425,6 @@ function execute(ViewExecutable $view) {
             $result = $query->execute();
             $result->setFetchMode(\PDO::FETCH_CLASS, 'Drupal\views\ResultRow');
    -
             $view->result = iterator_to_array($result);
    

    minor - needless hunk, we could avoid touching that file completely (well, not that patch size matters at this point :-p)

jessebeach’s picture

Assigned: jessebeach » Unassigned
Status: Needs work » Needs review
FileSize
4.64 KB
300.21 KB

Fixing failures.

jessebeach’s picture

Huh, so $entity->getRouteParams() returns array() in 8.x and NULL with the patch. I can find no obvious reason for this so far. Passing NULL as the $parameters value to UrlGenerator::doGenerate() results in a PHP warning because that NULL gets passed to array_replace.

jessebeach’s picture

The fields on the Shortcut entity are causing the problem and it seems to be because we switch the shortcut and shortcut_field_data tables. I'm getting closer to the issue...I'm currently looking at how the entities are constructed that get passed to ContentEntityDatabaseStorage::attachPropertyData().

In HEAD and the patch, the following query is run:

$query = $this->database->select($table, 'data', array('fetch' => \PDO::FETCH_ASSOC))
        ->fields('data')
        ->condition($this->idKey, array_keys($entities))
        ->orderBy('data.' . $this->idKey);

In HEAD, this simply returns four fields in the table shortcut_field_data: id, langcode, default_langcode and title.

In the patch, it returns the base fields, which includes the field route_parameters, which allows NULL and has no default, so it gets the value NULL.

route_parameters is a LONGBLOB and a map field item, so maybe route_parameters just needs to set a default of array()?

jessebeach’s picture

FileSize
203.53 KB

Alright, these Shortcut field values are simply being stored differently.

jessebeach’s picture

Status: Needs work » Postponed

So, the problem is the use of the MapItem. berdir notes that #2235457: Use link field for shortcut entity will change the route_parameters field from a MapItem to Link type field.

jessebeach’s picture

I'm trying to get around the shortcut route_parameters issue just to keep us moving here with a simple workaround by somehow setting a default value of array() on the field, but nothing seems to stick. I've tried setting the value with FieldDefinition::setSetting('default_value', array());; I've tried setting a default value in Shortcut::preCreate() and Shortcut::preSave(). Nothing wants to work to get an empty array in place.

jessebeach’s picture

Status: Postponed » Needs work

Actually, I'm unpostponing this. I think we can find a simple solution to move this issue along. It might not be the most elegant or perfect solution, but it just needs to enable us to make progress. Postponing this on an issue that is itself postponed is just going to grind progress into the dirt.

Berdir’s picture

This was not properly merged with #2267753: ContentEntityDatabaseStorage::mapToStorageRecord hard-codes $entity->$name->value. That issue adds a check for the main property in mapToStorageRecord() and if that is NULL, it uses all values of the field and stores those. That check got lost.

I'm not sure if that logic should happen inside the table mapping somehow or outside, probably inside (make column name NULL or something to indicate that?), but I don't know this stuff well enough to provide a fix.

Might also be worth to keep some of the documentation/logic of the changes that that method from the other issue, check how it looks right now with always working on the first field item explicitly.

fago’s picture

Status: Needs work » Needs review
FileSize
300.76 KB
2.42 KB

I think the table mapping is fine as it is, i.e. value => $field_name. So attached patch re-implements the logic of #2267753: ContentEntityDatabaseStorage::mapToStorageRecord hard-codes $entity->$name->value and re-adds + updates the comment.

yched’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
@@ -825,16 +826,29 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_nam
+      foreach ($columns as $column_name => $schema_name) {
+        if (!$definition->getMainPropertyName() && count($columns) == 1) {

Putting the case for "count($columns) == 1" within the loop on $columns looks a bit weird :-)

Also, can we preserve the "start by unconditionally doing $items = $entity->get($name)->first(), and then handle the various cases" shape that was added in #2267753: ContentEntityDatabaseStorage::mapToStorageRecord hard-codes $entity->$name->value ?
The mapToStorageRecord() method only knows how to store single-values fields, let's make this explicit upfront. The current patch has one "if" branch that does an explicit $field_name->first(), and the other that does an implicit $field_name->$column_name.

fago’s picture

PThe mapToStorageRecord() method only knows how to store single-values fields, let's make this explicit upfront.

Makes sense, ok.

Putting the case for "count($columns) == 1" within the loop on $columns looks a bit weird :-)

yep, that's a little bit confusing but I was not able to model the logic simply in another way. Howsoever this code is only temporarily until we get to #2232427: Allow field types to control how properties are mapped to and from storage anyway.

jessebeach’s picture

Status: Needs work » Needs review
FileSize
302.64 KB
2.71 KB

This should resolve the test failures. There are two exceptions in the KernelTestBaseTest that I'm seeing on HEAD as well.

yched’s picture

Making my way in the EntityStorage / tableMapping / SchemaHandler stack.

A couple high-level remarks for now :

  1. naming : SqlStorageInterface is about "Entity storage"

    --> rename to SqlEntityStorageInterface ?

  2. naming : ContentEntityDatabaseStorage implements SqlStorageInterface, and is about a storage with an SQL schema.
    But Mongo is a database too.

    --> rename to ContentEntitySqlStorage ?
    makes it clear that core stores in SQL by default, not just "in a database".

  3. naming : ContentEntitySchemaHandlerInterface's only method is getSchema()
    The only thing such an object does is generate a schema

    --> we could be more specific than a vague "handler" (basically = "doer of some stuff") and rename to ContentEntitySchemaGeneratorInterface ?

  4. ContentEntityDatabaseStorage extends ContentEntitySchemaHandlerInterface
    OK, ContentEntityDatabaseStorage has getSchema(), which proxies to $this->schemaHandler->getSchema()
    Not sure that should mean ContentEntityDatabaseStorage implements ContentEntitySchemaHandlerInterface.
    Not clear what this brings, and muddies the waters IMO : a ContentEntityDatabaseStorage is not a SchemaHandler / Generator, and I don't think we have a use case for a ContentEntityDatabaseStorage facading as one ?

    --> The getSchema() method could simply added to SqlStorageInterface ? (which would make sense anyway IMO - an SqlStorage needs to have a schema)
    --> Or at least do "SqlStorageInterface extends ContentEntitySchemaHandlerInterface", and then ContentEntityDatabaseStorage only implements SqlStorageInterface ?
    (I'd still vote for the former though)

  5. TableMappingInterface::getFieldNames($table) : "Returns a list of entity field names for the given table"
    nitpick : "entity fields for a table" isn't too clear. List of fields stored in the table ?

    + : why return just names, and not definitions directly ?
    - DefaultTableMapping can do it since it stores $storageDefinitions it receives from the EntityStorage.
    - It would simplify ContentEntitySchemaHandler, which then doesn't need to store storageDefinitions itself

  6. Related to the previous : currently all 3 of ContentEntityDatabaseStorage, DefaultTableMapping and ContentEntitySchemaHandler keep their onw $storageDefinitions,
    --> it seems we could at least remove it from ContentEntitySchemaHandler with the previous point
    --> we could also remove it from DefaultTableMapping if its construct received the ContentEntityDatabaseStorage ?

    Basically : you are a TableMapping or a SchemaHandler *for a given EntityStorage*. If you need the list of field definitions, ask it to your EntityStorage.

  7. Naming nitpick : the $storageDefinitions property currently used in those 3 classes (and possibly in just ContentEntityDatabaseStorage in the future) is a bit vague.
    ContentEntityDatabaseStorage::$storageDefinitions = not clear which "storage definitions" we're talking about :-)

    --> we should go with the exact name IMO and rename to $fieldStorageDefinitions

yched’s picture

Also :

- DefaultTableMapping.php
Why "Default" ? There's no real logic/behavior in there, it's basically a glorified array - the mapping is designed from the outside by EntityStorage, in a series of if(revisionable/translatable) in getTableMapping(). So, not sure why we have a "Default" TableMapping class that you might want to override - direclty naming it TableMapping would fly ?

Alternatively, we could move "design the mapping for the various cases" logic in the Mapping class itself, and have different classes for "revisionable", "translatable", "revisionable + translatable".
EntityStorage::getTableMapping() would just instanciate the right class in its if(revisionable/translatable) checks ?
No need for public setFieldNames() / setExtraColumns() methods anymore, this is done by each class internally according to the case it handles. They can probably all inherit from a TableMappingBase that is basically the current DefaultTableMapping, with the set*() methods moving to protected ?

Not sure which is preferable, just putting the idea out there. I must say delegating some complexity out of ContentEntityDatabaseStorage kind of appeals to me.

fago’s picture

Issue summary: View changes

Added my thoughts regarding #365 and #366:

--> rename to SqlEntityStorageInterface ?
--> rename to ContentEntitySqlStorage ?
makes it clear that core stores in SQL by default, not just "in a database".

True. But what about SqlEntityStorageInterface and SqlContentEntityStorage ? We should be consistent where to put the SQL.

naming : ContentEntitySchemaHandlerInterface's only method is getSchema()
The only thing such an object does is generate a schema

--> we could be more specific than a vague "handler" (basically = "doer of some stuff") and rename to ContentEntitySchemaGeneratorInterface ?

Seems reasonable. Also the interface is totally unrelated to content entities - so I was wondering whether it should just be EntitySchemaHandlerInterface/EntitySchemaGeneratorInterface

Not clear what this brings, and muddies the waters IMO : a ContentEntityDatabaseStorage is not a SchemaHandler / Generator, and I don't think we have a use case for a ContentEntityDatabaseStorage facading as one ?

Good question. However, once you can get the table mapping from the storage, it would make sense to be able to get the schema also. Moreover it should be required during installation right now. Instead we could just move getSchema() to the SqlEntityStorageInterfae - if you implement that you should provide a schema as well right? Then, EntitySchemaHandlerInterface could be used only internally and is not required to be known outside the entity system.

--> The getSchema() method could simply added to SqlStorageInterface ? (which would make sense anyway IMO - an SqlStorage needs to have a schema)

Yes that! ;)

Basically : you are a TableMapping or a SchemaHandler *for a given EntityStorage*. If you need the list of field definitions, ask it to your EntityStorage.

Not sure how it would get the field definitions from the storage?

--> we should go with the exact name IMO and rename to $fieldStorageDefinitions

Seems reasonable.

Alternatively, we could move "design the mapping for the various cases" logic in the Mapping class itself, and have different classes for "revisionable", "translatable", "revisionable + translatable".

Interesting idea, I could see this help to better organize and separate things - thus I like it. What do others think?
Imo, this would be something we could take care of together with the discussed follow-up of improving the separation of the main storage and the table mapping. I was not able to find an issue for that though, so I created one: #2274017: Make SqlContentEntityStorage table mapping agnostic .

plach’s picture

Assigned: Unassigned » plach

@365:

1: Makes sense (with @fago's correction :)
2: Makes sense too, but I am not sure this is the right place to make it happen. The patch here is already huge and this change would increase it unnecessarily.
3: My plan is adding more methods and functionalities to this class in #1498720: [meta] Make the entity storage system handle changes in the entity and field schema definitions to handle changes in entity/field definitions, so it will become a full-fledged schema handler. Thus I think the name is fine as-is.
4: Given 3, I think your proposal makes sense: we would be forced to add more methods to the storage class that would make no sense for it, so just adding an "unrelated" getSchema() method to the storage would make sense.
5: No strong opinion about this. I guess we should try and be consistent with the rest of core code: if usually we return a list of definitions, let's do it here too.
6: Not sure about this: it might complicate the inter-dependencies between the three classes, which are already a bit unclear. What about addressing this in #2274017: Make SqlContentEntityStorage table mapping agnostic ?
7: Agreed

I like 366 too: I spent quite some time trying to figure out how to isolate that logic and the table mapping seems to be a good choice. I agree we could discuss this in #2274017: Make SqlContentEntityStorage table mapping agnostic where more clean-up should happen.

Working on those failures now.

plach’s picture

plach’s picture

Status: Needs work » Needs review
FileSize
688 bytes

Here is the interdiff

plach’s picture

Assigned: plach » Unassigned
FileSize
303.21 KB
15.7 KB

Addressed 365, except 5 (it was too late):

373ec80 DEV 2183231: Fixed KernelTestBaseTest.
1e90cb0 DEV 2183231: Renamed SqlStorageInterface.
39ea9e2 DEV 2183231: Renamed ContentEntitySchemaHandlerInterface.
3dde40a DEV 2183231: Introduced the EntitySchemaProviderInterface.
c00ba61 DEV 2183231: Renamed storageDefinitions.

The most relevant change is the introduction of EntitySchemaProviderInterface, which exposes only the getSchema() method and is implemented by the storage class. EntitySchemaHandlerInterface extends it and will expose additional methods after #1498720: [meta] Make the entity storage system handle changes in the entity and field schema definitions.

@Jessebeach:

If you are going to post other patches for this, please use the sandbox and pick a topic branch named after you, so managing interdiffs will be easier, thanks!

xjm’s picture

+++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaProviderInterface.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface.
+ */
+namespace Drupal\Core\Entity\Schema;
+
+/**
+ * A common interface to return the storage schema for entities.
+ */
+interface EntitySchemaProviderInterface {
+
+  /**
+   * Gets the full schema array for a given entity type.
+   *
+   * @return array
+   *   A schema array for the entity type's tables.
+   */
+  public function getSchema();
+
+}

This seems like good DX. However, the documentation here doesn't explain how I'd want to use this method (e.g., overriding it to add the {taxonomy_index} table).

xjm’s picture

I also added this information to the change record:
https://drupal.org/node/2259243/revisions

fago’s picture

I reviewed the whole patch again and fixed small things while going through it. Attached is a patch which does various small documentation fixes/improvements. Besides that, SqlEnttiyStorageInterface should extend from EntitySchemaProviderInterface, thus I've done so also.
Interdiff & patch attached, I've pushed it to 8.x-et-entity_schema_auto-2183231-fago also.

This seems like good DX. However, the documentation here doesn't explain how I'd want to use this method (e.g., overriding it to add the {taxonomy_index} table).

I do not think this matters to someone *using* the interface, but it matters for someone defining the storage of an entity type. Thus, I added respective documentation to ContentEntityDatabaseStorage - please see the interdiff attached.

Further issue I noted while going through the patch:

  • ContentEntityDatabaseStorage::$fieldStorageDefinitions are not all field storage definitions, but one would expect that.
  • ContentEntityDatabaseStorage::getBaseTable() and other getTable methods are not part of the interface but needed by the schema handler. Not a big issue as it should go with #2274017: Make SqlContentEntityStorage table mapping agnostic anyway imho.

I think we should rather introduce a new setting on the existing Integer class. Even though we have String and StringLong, etc. we did the same thing for the 'unsigned' setting instead of introducing an UnsignedInteger class.

Having "string_long" + integer and a size option is a DX wtf. Thus, I think string should have a respective size option as well.
Update: That's pre-existing though, i.e. integer and strings do it already differently. Thus, it should be a separate issue.

Then I figured we have quite some "Not null" fields which are not marked as required, e.g. comment pid. That's not related to this issue though, so I created #2274597: Not all required fields are marked as required

fago’s picture

ContentEntityDatabaseStorage::$fieldStorageDefinitions are not all field storage definitions, but one would expect that.

Attached patch/interdiff addresses that, other remarks of #377 are left for follow-ups as said.

plach’s picture

Changes look good to me, I hope they won't prevent @fago from RTBC'ing this ;)

ContentEntityDatabaseStorage::getBaseTable() and other getTable methods are not part of the interface but needed by the schema handler. Not a big issue as it should go with #2274017: Make SqlContentEntityStorage table mapping agnostic table mapping agnostic anyway imho.

That was intentional as the schema handler knows it deals with the ContentEntityDatabaseStorage and I didn't want to pollute the public API with those methods that are implementation-specific. Anyway, I agree those are probably going given the latest discussions, so no need to worry about them.

Tomorrow I will hopefully finish to address @yched's reviews (I think also #345 is still pending), and then I guess we should be done, at least with this first step.

aspilicious’s picture

In the change record I'm missing some info about "changing the schema". Everyone knows how to alter a database schema in an update function for the regular hook_schema. How are schema changes handled now?

Or are the schema's of content entities fixed by default.

fago’s picture

ad #380: That's not implemented yet, but stuff left for [#2259243].

I've worked over the change record draft, to explain that + improve it in general. I've removed the class diagram from there as it was hard to understand and I don't think internals need to be in the change record, but feel free to add it back if you think it should be there. Changes: https://drupal.org/node/2259243/revisions/view/7280867/7282547

plach’s picture

@aspilicious:

One of the goals of this issue is reducing the need of manually altering the entity schema, as this is (mainly) derived from entity/field definitions. In most cases you would change the related definitions instead of actually touching the schema definition (this will be handled in #1498720: [meta] Make the entity storage system handle changes in the entity and field schema definitions). Whether allowing direct manipulation of the generated schema is going to be discussed in #2258347: Consider adding hook_entity_schema_alter(), but personally I'd hope for a won't fix, although concrete use cases might be presented that would make me change my mind :)

xjm’s picture

Agreed, the class diagram might be relevant handbook documentation on the Entity API's architecture (or even linked from the Entity API @defgroup) but it doesn't really belong in a change record.

xjm’s picture

Rerolled for #2167167: Remove field_info_*() -- merge conflict was the removal of core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php. And a small doc fix. (I don't have access to the sandbox.)

jessebeach’s picture

Attached are the two latest SQL dumps for diffing. I don't see any major inconsistencies.

Let's get this in!

jessebeach’s picture

I'm pre-emptively rerolling this on #2247991-57: [May 27] Move all module code from …/lib/Drupal/… to …/src/… for PSR-4, which is scheduled to land today.

jessebeach’s picture

To psr4-ify this patch, grep for the following pattern in the patch:

\/lib\/Drupal\/(?!Core)[a-z_]*\/

and replace the pattern with this:

/src/

I ran this in Sublime Text 2. In the past, my regexes have not reliably run in other REGEX flavors, so buyer beware :)

jessebeach’s picture

Status: Needs review » Needs work
FileSize
305.95 KB

I'll set back to Needs Review when #2247991: [May 27] Move all module code from …/lib/Drupal/… to …/src/… for PSR-4 is committed, which should be shortly.

plach’s picture

@jessebeach:

I had to ignore your latest patch to be able to work in the sandbox (please push your next changes to it in topic branch named after you). Would you mind to do another conversion to PSR-4 to this one? I'd like the bot to have a pass to it.

@yched:

why return just names, and not definitions directly ?

I tried to implement this, but it made lots of parts of code more complicated as we actually need field names in many places, so I don't think it's worth.

This should address also #345:

93fdb07 DEV 2183231: Fixed PHP docs.
e5a6c12 DEV 2183231: Moved NodeType fix to the parent class.

2: I think previously we were just allowing for NULL values to be stored but now we don't.
6: My understanding was that we would fix just ER items, not that we would be reverting #1709960: declare a maximum length for entity and bundle machine names.

plach’s picture

Status: Needs work » Needs review
FileSize
303.26 KB

Re-uploading just in case...

plach’s picture

FileSize
297.96 KB

Rerolled after #2247991: [May 27] Move all module code from …/lib/Drupal/… to …/src/… for PSR-4, git did all the work, let's see whether this is still green.

fago’s picture

Status: Needs review » Reviewed & tested by the community

Changes look good, except for:

+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -555,14 +555,18 @@ protected static function invalidateTagsOnDelete(array $entities) {
+      // If this entity is a bundle entity type of another entity type, and we're

Comment exceeds 80chars.

Anyway, there is nothing left which should this one up - we can do address improvements and field definition vs names questions in follow-ups. Let's unblock the remaining blockers and get this in! As I only did some small comment fixes I take the liberty to mark this RTBC anyway.

yched’s picture

- why return just names, and not definitions directly ?
- I tried to implement this, but it made lots of parts of code more complicated as we actually need field names in many places, so I don't think it's worth.

I don't understand this. If it returns an array of Definitions keyed by field name, then both cases ("caller needs definitions" and "caller just needs names") are covered ?

Re: 'max_length' on ER field definitions : well, that setting currently has no effect whatsoever. Maybe we don't want to remove the existing ones from core in this patch here, but at any rate we shouldn't be adding new ones ?

catch’s picture

Read through the whole patch except I ran out of steam at the test changes.

I couldn't find anything to complain about, this looks great. So many hook_schema() gone and the code that replaces hook_schema() is very light. Thanks for all the work comparing the generated schema, that was my main worry with this patch.

Do we need a follow-up for getFieldNames()?

I'm confused by this comment:

+    $schema['file_managed']['unique keys'] += array(
+      // FIXME We have an index size of 255, but the max URI length is 2048 so
+      // this might now always work. Should we replace this with a regular
+      // index?
+      'file__uri' => array(array('uri', 255)),

Isn't this still a varchar(255)?

Not committing just yet but I don't have any objections if someone else does.

jessebeach’s picture

fago’s picture

xjm’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
297.99 KB

Not just the --stat. ;)

alexpott’s picture

+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -241,4 +248,27 @@ protected function addJoin($type, $table, $join_condition, $langcode) {
+  protected function getTableMapping($table, $entity_type_id) {
+    $storage = $this->entityManager->getStorage($entity_type_id);
+    // @todo Stop calling drupal_get_schema() once menu links are converted
+    //   to the Entity Field API. See https://drupal.org/node/1842858.
+    $schema = drupal_get_schema($table);
+    if (!$schema && $storage instanceof SqlEntityStorageInterface) {
+      $mapping = $storage->getTableMapping()->getAllColumns($table);
+    }
+    else {
+      $mapping = array_keys($schema['fields']);
+    }
+    return array_flip($mapping);
+  }

The logic here is curious - shouldn't we be prioritising getting the table mapping from the storage over using drupal_get_schema

fago’s picture

The logic here is curious - shouldn't we be prioritising getting the table mapping from the storage over using drupal_get_schema

Problem is that menu links are not covered by the table mapping yet, that's content entity only and menu links are no content entities. However, even with custom-tailored schema it should be possible to implement the respective interfaces to provide a tablemapping. @plach, @tstoeckler, what do you think?

fago’s picture

Status: Needs work » Needs review
FileSize
299.11 KB
1.12 KB

This should fix the test fail.

fago’s picture

Assigned: Unassigned » fago
Status: Needs work » Needs review
FileSize
4.12 KB
301.71 KB
301.56 KB
4.25 KB

Besides that the test now requires existing files, it was actually broken although green else. The test needs a custom changed date, but that's forced to REQUEST_TIME when doing an entity save. Turned out retrieveTemporaryFiles() is broken also, as it says to return array but returns a database result object. Here is a proper fix, fixing the implementation of retrieveTemporaryFiles() by making it return a list of file identifiers.

Looking at the menu-link issue now.

Update:
grml, I had the first two files deleted :/ The right ones are the last two, please look at those only.

fago’s picture

So here is an updated patch + interdiff that moves the menu link schema to its storage controller.

Problem is that it uses drupal_write_record() which we cannot easily replace as it applies defaults and takes care of some serialization also. I introduced an optional $schema parameter which allows using drupal_write_record() with passed in schema also.

fago’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
1.33 KB
301.55 KB

#412 will need some more review and changing db_write_record() is out of scope for this issue, so let's better move this to a follow-up -> created #2277979: Menu link storage does not implement SqlEntityStorageInterface.

Attached patch reverts #412 and adds the following interdiff for making the fallback to db schema nicer.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @fago. I agree that it makes sense to explore the menu link issue in a follow-up. The interdiffs look good and address @alexpott's only concern. Back to RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 1ab2651 and pushed to 8.x. Thanks!

  • Commit 1ab2651 on 8.x by alexpott:
    Issue #2183231 by tstoeckler, plach, jessebeach, fago, Berdir, xjm,...

  • Commit 01185d2 on 8.x by alexpott:
    Issue #2183231 followup: Convert PSR-0 code to PSR-4 in custom_block...
yched’s picture

tstoeckler’s picture

Yay, thanks so much everyone for keeping this going in the last couple of weeks. AWESOME!!!! :-)

See you in the various related / follow-up issues.

plach’s picture

A W E S O M E work guys!

jaredsmith’s picture

This commit breaks installation of Drupal 8 HEAD on PostgreSQL.

During the installation, it tries to create a database table with illegal syntax:

An AJAX HTTP error occurred.
HTTP Result Code: 200
Debugging information follows.
Path: http://ocho/core/install.php?langcode=en&profile=standard&id=1&op=do_nojs&op=do
StatusText: OK
ResponseText: PDOException: SQLSTATE[42601]: Syntax error: 7 ERROR:  syntax error at or near "Array"
LINE 15:  CONSTRAINT file_managed_file__uri_key UNIQUE (Array)
^: CREATE TABLE {file_managed} (
fid serial CHECK (fid >= 0),
uuid varchar(128) NOT NULL,
langcode varchar(12) NOT NULL,
uid bigint CHECK (uid >= 0) NOT NULL,
filename varchar(255) NULL,
uri varchar(2048) NOT NULL,
filemime varchar(255) NULL,
filesize bigint CHECK (filesize >= 0) NULL,
status smallint NOT NULL,
created int NULL,
changed int NOT NULL,
PRIMARY KEY ("fid"),
CONSTRAINT file_managed_file__uuid_key UNIQUE (uuid),
CONSTRAINT file_managed_file__uri_key UNIQUE (Array)
); Array
(
)
in Drupal\Core\Extension\ModuleHandler->install() (line 809 of /var/www/html/drupal/core/lib/Drupal/Core/Extension/ModuleHandler.php).

Please notice the Array in the second unique key.

tstoeckler’s picture

Issue tags: +PostgreSQL
FileSize
1.32 KB

Found the following issue: #2278493: Drop support for unique keys specified as an array of column name and length (breaks PG installation)

The attached patch avoids that error by not sepcifying a unique key as an array of column name and length in the first place.

This still does not get through the installer. The next fail is:

Drupal\Core\Entity\EntityStorageException: SQLSTATE[22003]: Numeric value out of range: 7 ERROR: setval: value 0 is out of bounds for sequence "shortcut_id_seq" (1..9223372036854775807) in Drupal\Core\Entity\ContentEntityDatabaseStorage->save() (line 677 of /core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php)

Haven't looked into that yet.

jessebeach’s picture

@tstoeckler, great catch. We should open this as its own bug given we're at comment #425 here and this issue might need further discussion.

Crell’s picture

Issue tags: -PostgreSQL

The Postgres driver didn't work right even before this patch went in; See #2160433: [policy, no patch] Move PostgreSQL driver support into contrib and #2157455: [Meta] Make Drupal 8 work with PostgreSQL or remove support from core before release.

Please file a new issue for the postgres driver with that meta as a parent. Thanks.

Xano’s picture

This issue conflicts with my content entity type, whose fields do not match the table schema. This has the following consequences:

  1. Saving entities is no longer done with drupal_write_record(), which means data is no longer automatically serialized based on the schema. Even though this is easily dealt with, it is something to take into account.
  2. The query for loading entities is built using field definitions. As not all field names match column names and there are columns that don't have entity fields, loading fails entirely.
  3. One of the fields on my entity type is an entity reference to a config entity type of which the ID always consists of three letters. The new storage controller automatically creates a VARCHAR(255) instead of the VARCHAR(3) I used to use. While this is not a bug, this does take up more space.

I'm mostly interested in solutions to my query problems, as I want to disable the field > column matching completely.

tstoeckler’s picture

Re #425: I agree that that should be a separate issue, but I never really understood why we did the file.uri unique key thing, so @plach would have to open that issue. I wouldn't know what to call the issue! :-)

Re #426: #2278493: Drop support for unique keys specified as an array of column name and length (breaks PG installation) is that issue for a concrete problem found already. Marked that as a child of #2157455: [Meta] Make Drupal 8 work with PostgreSQL or remove support from core before release. Will open an issue for the shortcut problem and any other ones, as soon as I figure out what the problem is.

Re #427:

  1. Saving entities is no longer done with drupal_write_record(), which means data is no longer automatically serialized based on the schema. Even though this is easily dealt with, it is something to take into account.

    So I'm not sure whether this is true, depending on what specifically you mean. How serialization works currently is that it checks the serialize key in the field item schema. See MapItem for example.

  2. The query for loading entities is built using field definitions. As not all field names match column names and there are columns that don't have entity fields, loading fails entirely.
    • I don't understand why you would want column names that don't match the field names, that is in fact not supported out of the box. Unless you are talking about multi-column fields, which work absolutely fine. If you do want to have a column that is not named after a field you can override SqlStorageInterface::getTableMapping() in your storage.
    • For columns that do not correspond to fields you are hopefully talking about denormalization columns. All columns that store actual, normalized data should be handled by an entity field. That's the whole premise of content entities. To add non-standard denormalization columns you can override SqlStorageInterface::getTableMapping() and use $table_mapping->setExtraColumns().
  3. One of the fields on my entity type is an entity reference to a config entity type of which the ID always consists of three letters. The new storage controller automatically creates a VARCHAR(255) instead of the VARCHAR(3) I used to use. While this is not a bug, this does take up more space.

    It would be pretty cool if we could support that optimization. But while I'm not yet 100% sure we can do that in a clean way let's discuss that in #2107249: Don't assume that content entities have numeric IDs in EntityReferenceItem.

Soul88’s picture

One of the fields on my entity type is an entity reference to a config entity type of which the ID always consists of three letters. The new storage controller automatically creates a VARCHAR(255) instead of the VARCHAR(3) I used to use. While this is not a bug, this does take up more space.

It doesn't.
http://dev.mysql.com/doc/refman/5.7/en/char.html

In contrast to CHAR, VARCHAR values are stored as a 1-byte or 2-byte length prefix plus data. The length prefix indicates the number of bytes in the value. A column uses one length byte if values require no more than 255 bytes, two length bytes if values may require more than 255 bytes.

Berdir’s picture

3. Thanks for the clarification there, @Soul88. What the table schema supports automatically is limiting the size of the bundle config entity reference to 32, because it's often used in combined keys. Everything else can be done by altering the generated schema as pointed out by @tstoeckler for the other parts.

I'm also confused by your other tasks, content entities are essentially nothing but a container for a set of fields, they shouldn't consist of anything that's not a field and therefore, I'm also not sure why you need those requirements with differently named columns and additional columns. Entity query was already strictly based on the field definitions before this issue.

Xano’s picture

Some of the 'problems' I am experiencing are because my content entities contain plugins. The entity types are quite a bit older than much code in Drupal 8, so maybe there is a solution for this that was added after creating the entity types and that I am not aware of yet.

The thing with plugins is that plugin IDs and plugin configurations must be stored, but when entities are loaded, this data must not be available as fields, but as plugin instances.

I also just published the change record for this issue. I did not see that last night, because it was still unpublished, so I may have missed some important information. I will check it today and report back later.

yched’s picture

Wah, plugins in a content entity :-). Not sure what your use case is, but that sounds like it would require a custom "plugin id + config array" field type.

D8 is "If it needs to be in a content entity, it has to be a field".

jaredsmith’s picture

Re: 426, PostgreSQL may not have been working perfectly before this patch was committed, but at least we could complete the installation process and add basic content, etc. Once this patch was committed, we lost the ability to even complete a Drupal 8 installation on PostgreSQL. I'm fine with moving this to a new issue, but I think saying "PostgreSQL was already broken, so it's not a big deal" is a bit cavalier.

tstoeckler’s picture

Related issues: +#2279363: file_managed.uri should not specify a prefixed unique key

So took the patch from #424 to its own issue: #2279363: file_managed.uri should not specify a prefixed unique key. @Berdir's comment in #2278493: Drop support for unique keys specified as an array of column name and length (breaks PG installation) convinced that it's an actual bug.

Will now look into the shortcut problem and link it from here, as soon as I find anything.

@Xano: Can we discuss your use-case in a separate issue? It seems that is a sufficiently complex problem space that that would deserve it. I'm happy to help/discuss there, though!

tstoeckler’s picture

Opened #2279395: The PostgreSQL backend does not handle NULL values for serial fields gracefully and #2279405: ContentEntityDatabaseStorage inserts bogus NULL values for new entities. Applying the patch from the latter together with the one from #2279363: file_managed.uri should not specify a prefixed unique key lets me install without errors on PostgreSQL again. Phew! And sorry for the trouble everyone.

I think this is now finally finished.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

plach’s picture

david_garcia’s picture

Possibly related issue:

#2342699 ContentEntityDatabaseStorage tries to update primary key values by default

As for now, statements against the database are issued with UPDATES to PRIMARY KEY fields. Some strict engines, such as SQL Server, complain because of this. A PRIMARY KEY cannot be updated.

yched’s picture

The people in this issue will probably be interested in #2371709-22: Move the on-demand-table creation into the database API...

yched’s picture

This added a 'size' setting to IntegerItem, as a *field* setting rather than a storage setting. Yet it's used in the schema.
#2376689: IntegerItem 'size' setting should be a storage setting

effulgentsia’s picture

+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -133,6 +133,13 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
+        // It is not possible to set the language to NULL as it is automatically
+        // re-initialized. As it must not be empty, skip it if it is.
+        // @todo: Use the langcode entity key when available. See
+        //   https://drupal.org/node/2143729.
+        if ($field_name == 'langcode' && $field->isEmpty()) {
+          continue;
+        }

Anyone here remember why this was added? It's being targeted for removal in #2933408: Remove broken allowance of empty langcode in a PATCH request, so please comment there if you have any concerns about that.