Problem/Motivation
When writing an entity query it is possible to write conditions against all fields within an entity, however if the field is computed it will throw an error.
Proposed resolution
- Add a check within the entity query if a field is computed, and throw a better error than the one below.
- Create a follow up issue to allow computed fields to add something within the base field definition to know what table to join, how to join it, and which database field is the one to return or base conditions against.
Remaining tasks
User interface changes
API changes
Data model changes
Drupal\\Core\\Entity\\Query\\QueryException: 'moderation_state' not found in /app/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php:348\nStack trace:\n#0 /app/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php(241): Drupal\\Core\\Entity\\Query\\Sql\\Tables->ensureEntityTable('', 'moderation_stat...', 'INNER', NULL, 'base_table', 'nid', Array)\n#1 /app/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php(44): Drupal\\Core\\Entity\\Query\\Sql\\Tables->addField('moderation_stat...', 'INNER', NULL)\n#2 /app/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php(39): Drupal\\Core\\Entity\\Query\\Sql\\Condition->compile(Object(Drupal\\Core\\Database\\Query\\Condition))\n#3 /app/core/lib/Drupal/Core/Entity/Query/Sql/Query.php(163): Drupal\\Core\\Entity\\Query\\Sql\\Condition->compile(Object(Drupal\\Core\\Database\\Driver\\mysql\\Select))\n#4 /app/core/lib/Drupal/Core/Entity/Query/Sql/Query.php(74): Drupal\\Core\\Entity\\Query\\Sql\\Query->compile()\n#5 /app/modules/jsonapi/src/Controller/EntityResource.php(326): Drupal\\Core\\Entity\\Query\\Sql\\Query->execute()\n#6 [internal function]: Drupal\\jsonapi\\Controller\\EntityResource->getCollection(Object(Symfony\\Component\\HttpFoundation\\Request))\n#7 /app/modules/jsonapi/src/Controller/RequestHandler.php(145): call_user_func_array(Array, Array)\n#8 /app/core/lib/Drupal/Core/Render/Renderer.php(582): Drupal\\jsonapi\\Controller\\RequestHandler->Drupal\\jsonapi\\Controller\\{closure}()\n#9 /app/modules/jsonapi/src/Controller/RequestHandler.php(146): Drupal\\Core\\Render\\Renderer->executeInRenderContext(Object(Drupal\\Core\\Render\\RenderContext), Object(Closure))\n#10 [internal function]: Drupal\\jsonapi\\Controller\\RequestHandler->handle(Object(Symfony\\Component\\HttpFoundation\\Request), Object(Drupal\\jsonapi_extras\\ResourceType\\ConfigurableResourceType))\n#11 /app/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array)\n#12 /app/core/lib/Drupal/Core/Render/Renderer.php(582): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->Drupal\\Core\\EventSubscriber\\{closure}()\n#13 /app/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(124): Drupal\\Core\\Render\\Renderer->executeInRenderContext(Object(Drupal\\Core\\Render\\RenderContext), Object(Closure))\n#14 /app/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array)\n#15 /app/vendor/symfony/http-kernel/HttpKernel.php(151): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->Drupal\\Core\\EventSubscriber\\{closure}()\n#16 /app/vendor/symfony/http-kernel/HttpKernel.php(68): Symfony\\Component\\HttpKernel\\HttpKernel->handleRaw(Object(Symfony\\Component\\HttpFoundation\\Request), 1)\n#17 /app/core/lib/Drupal/Core/StackMiddleware/Session.php(57): Symfony\\Component\\HttpKernel\\HttpKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#18 /app/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(47): Drupal\\Core\\StackMiddleware\\Session->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#19 /app/core/modules/page_cache/src/StackMiddleware/PageCache.php(99): Drupal\\Core\\StackMiddleware\\KernelPreHandle->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#20 /app/core/modules/page_cache/src/StackMiddleware/PageCache.php(78): Drupal\\page_cache\\StackMiddleware\\PageCache->pass(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#21 /app/modules/jsonapi/src/StackMiddleware/FormatSetter.php(40): Drupal\\page_cache\\StackMiddleware\\PageCache->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#22 /app/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(47): Drupal\\jsonapi\\StackMiddleware\\FormatSetter->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#23 /app/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(50): Drupal\\Core\\StackMiddleware\\ReverseProxyMiddleware->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#24 /app/vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Drupal\\Core\\StackMiddleware\\NegotiationMiddleware->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#25 /app/core/lib/Drupal/Core/DrupalKernel.php(664): Stack\\StackedHttpKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#26 /app/index.php(19): Drupal\\Core\\DrupalKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request))\n#27 {main}\n\nNext Symfony\\Component\\HttpKernel\\Exception\\HttpException: 'moderation_state' not found in /app/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php:43\nStack trace:\n#0 [internal function]: Drupal\\jsonapi\\EventSubscriber\\DefaultExceptionSubscriber->onException(Object(Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent), 'kernel.exceptio...', Object(Drupal\\Component\\EventDispatcher\\ContainerAwareEventDispatcher))\n#1 /app/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(111): call_user_func(Array, Object(Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent), 'kernel.exceptio...', Object(Drupal\\Component\\EventDispatcher\\ContainerAwareEventDispatcher))\n#2 /app/vendor/symfony/http-kernel/HttpKernel.php(228): Drupal\\Component\\EventDispatcher\\ContainerAwareEventDispatcher->dispatch('kernel.exceptio...', Object(Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent))\n#3 /app/vendor/symfony/http-kernel/HttpKernel.php(79): Symfony\\Component\\HttpKernel\\HttpKernel->handleException(Object(Drupal\\Core\\Entity\\Query\\QueryException), Object(Symfony\\Component\\HttpFoundation\\Request), 1)\n#4 /app/core/lib/Drupal/Core/StackMiddleware/Session.php(57): Symfony\\Component\\HttpKernel\\HttpKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#5 /app/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(47): Drupal\\Core\\StackMiddleware\\Session->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#6 /app/core/modules/page_cache/src/StackMiddleware/PageCache.php(99): Drupal\\Core\\StackMiddleware\\KernelPreHandle->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#7 /app/core/modules/page_cache/src/StackMiddleware/PageCache.php(78): Drupal\\page_cache\\StackMiddleware\\PageCache->pass(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#8 /app/modules/jsonapi/src/StackMiddleware/FormatSetter.php(40): Drupal\\page_cache\\StackMiddleware\\PageCache->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#9 /app/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(47): Drupal\\jsonapi\\StackMiddleware\\FormatSetter->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#10 /app/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(50): Drupal\\Core\\StackMiddleware\\ReverseProxyMiddleware->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#11 /app/vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Drupal\\Core\\StackMiddleware\\NegotiationMiddleware->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#12 /app/core/lib/Drupal/Core/DrupalKernel.php(664): Stack\\StackedHttpKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#13 /app/index.php(19): Drupal\\Core\\DrupalKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request))\n#14 {main}
Comments
Comment #2
wim leersThis is because the
moderation_statebase field that\Drupal\content_moderation\EntityTypeInfo::entityBaseFieldInfo()adds is computed and AFAIK hence not queryable in entity field queries.Comment #3
wim leersComment #4
wim leersPinged people in IRC who likely have pointers: timmillwood & catch.
Comment #5
wim leersComment #6
wim leersMaybe JSON API can special case this? It's really the entity query system's responsibility though…
Comment #7
wim leers#2958587: Unable to filter on columns of entity reference fields seems related too.
Comment #8
timmillwoodI'm not sure what to suggest here, I guess we need a more general way for entity queries to query computed fields.
Comment #9
timmillwoodOops I managed to revert some changes.
Comment #10
wim leersIndeed.
So how does Content Moderation support filtering by moderation state in views?
Comment #11
timmillwoodWith
\Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter. This adds the join to the revision data table for the content_moderation_state entity type and the related where queries.We also have similar joins within
\Drupal\content_moderation\ViewsData::getViewsData.My proposal would be two steps:
- First add a check within the entity query if a field is computed, and throw a better error than the one we see in the issue summary.
- Secondly, allow computed fields to add something within the base field definition to know what table to join, how to join it, and which database field is the one to return or base conditions against.
Should we move this issue to core? or mark as
Closed (won't fix)and open a core issue?Comment #12
wim leersLet's move this to core. Sounds like you know which component to move it to?
Comment #13
timmillwoodComment #14
gabesullice@timmillwood, why does this belong in the 9.x branch? AFAICT, there aren't any changes to the API or data model, just a better exception message as computed fields never worked to begin with. Perhaps you meant for the follow-up to be against 9.x?
Anyways, here's a rough first shot at it.
Comment #15
gabesulliceComment #16
timmillwoodSorry, it seemed to default to 9.x when I moved it from JSON API to Drupal Core.
Comment #17
gabesulliceThe one failure was in a test that was actually using an undocumented and incorrect way of writing an entity query. The interdiff shows that change.
However, in theory, others could also be using this mechanism and this patch would break their sites. Should we accept that it will break sites which have incorrect queries or should we translate queries to
field__columnintofield.columnas a BC layer?Comment #18
timmillwoodThat's a tricky one, I feel like we need to open a separate issue which this one is postponed on to investigate the origins of
field__column, then make an informed decision on how to, across the whole of core not sure just in core/lib/Drupal/Core/Entity/Query/Sql/Tables.php, throw an exception or support via a BC layer. It seems strange that that test was even passing in the first place!Comment #19
berdirYeah, also no idea how that ever worked, interesting :)
We could have a bit of BC that if the field does not exist, looks for __, replaces it with ., does a @trigger_error() and tries again, but not sure if that's worth it. Technically you can have a field with __ in the name I think, so it might be confusing for those cases?
I'd say using that was wrong usage and a bug, not a feature, and failing with a clear exception is OK.. but lets see what others think.
Comment #20
gabesulliceWhoops... logic didn't match correct comment. Obviously needs tests.
Comment #21
wim leersGlad to see some progress here :)
Comment #22
joachim commentedShould be a complete sentence, not running on from the exception class name above it.
Comment #24
jonathanshawSounds like we need a follow-up for #11:
Comment #25
wim leersComment #26
hchonovWhat is the point of this check?
From the documentation of
\Drupal\Core\Entity\EntityFieldManagerInterface::getFieldStorageDefinitions:So this means that if the method doesn't return a field storage definition, then the queried field is either computed or it doesn't exist, right?
Comment #27
joachim commentedI'm confused by that check too.
AFAIK, a field is either computed, or it isn't, and that's true for the whole field, that is, on all bundles.
Comment #30
wim leersThis still blocks #2916074: [PP-1] Test coverage: FieldResolver: path validation for computed fields.
Comment #34
bbrala@WimLeers any chance you can pick this up again?
Comment #36
joachim commentedNeeds work, based on #26.
Comment #40
eliaspapa commentedIs there any chance this issue gets traction again?
Trying to filter by moderation state using JSON:API and I would highly appreciate the proposal from #11.
Comment #41
owenbush commentedI'm pretty sure this is a similar issue that is plaguing the Recurring Events module.
In that case there's an entity type eventinstance which actually inherits it's title from a parent eventseries entity. So the title field is computed. This causes issues when using an entity reference field to reference an eventinstance, the entity query fails because the title field does not actually exist.
Drupal\Core\Entity\Query\QueryException: 'title' not found in Drupal\Core\Entity\Query\Sql\Tables->ensureEntityTable()
#3442751: Unable to reference event instances
Comment #42
marttir commentedEnded up in this issue while debugging another one in the Linkchecker module, which uses Dynamic Entity Reference.
It would seem the core lib/Drupal/Core/Entity/Query/Sql/Tables.php does not account for the existence of computed fields. However, does it really make sense for code in "Sql/Tables" to account for that? Where in the entity query interface or internals would it make sense to prevent computed fields from making it into entity queries at all?