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

Snugug created an issue. See original summary.

wim leers’s picture

This is because the moderation_state base field that \Drupal\content_moderation\EntityTypeInfo::entityBaseFieldInfo() adds is computed and AFAIK hence not queryable in entity field queries.

wim leers’s picture

Title: Filter on Moderation State » 500 error when trying to filter on moderation_state, because it is a computed field, which cannot be queried
Issue tags: +content moderation, +Workflow Initiative
wim leers’s picture

Title: 500 error when trying to filter on moderation_state, because it is a computed field, which cannot be queried » 500 error when trying to filter on moderation_state in JSON API, because it is a computed field, which cannot be queried

Pinged people in IRC who likely have pointers: timmillwood & catch.

wim leers’s picture

wim leers’s picture

Maybe JSON API can special case this? It's really the entity query system's responsibility though…

wim leers’s picture

timmillwood’s picture

I'm not sure what to suggest here, I guess we need a more general way for entity queries to query computed fields.

timmillwood’s picture

wim leers’s picture

I guess we need a more general way for entity queries to query computed fields.

Indeed.

So how does Content Moderation support filtering by moderation state in views?

timmillwood’s picture

So how does Content Moderation support filtering by moderation state in views?

With \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?

wim leers’s picture

Let's move this to core. Sounds like you know which component to move it to?

timmillwood’s picture

Title: 500 error when trying to filter on moderation_state in JSON API, because it is a computed field, which cannot be queried » Handle computed fields in entity queries
Project: JSON:API » Drupal core
Version: 8.x-1.15 » 9.x-dev
Component: Code » entity system
Issue summary: View changes
gabesullice’s picture

StatusFileSize
new2.19 KB

@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.

gabesullice’s picture

Issue tags: +Needs tests
timmillwood’s picture

Version: 9.x-dev » 8.6.x-dev

Sorry, it seemed to default to 9.x when I moved it from JSON API to Drupal Core.

gabesullice’s picture

Status: Active » Needs review
StatusFileSize
new2.89 KB
new711 bytes

The 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__column into field.column as a BC layer?

timmillwood’s picture

That'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!

berdir’s picture

Yeah, 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.

gabesullice’s picture

StatusFileSize
new2.89 KB
new790 bytes

Whoops... logic didn't match correct comment. Obviously needs tests.

wim leers’s picture

Title: Handle computed fields in entity queries » Handle computed fields in entity queries: throwing a helpful exception is better than a PHP fatal error

Glad to see some progress here :)

joachim’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -450,4 +452,39 @@ protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column
+   *   Thrown if the field specifier

Should be a complete sentence, not running on from the exception class name above it.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

jonathanshaw’s picture

Sounds like we need a follow-up for #11:

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.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.78 KB
new3.16 KB
hchonov’s picture

+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -452,4 +454,40 @@ protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column
+    // Assume the specifier is computed, if any bundle has a non-computed field
+    // this will become FALSE.

What is the point of this check?

From the documentation of \Drupal\Core\Entity\EntityFieldManagerInterface::getFieldStorageDefinitions :

   * This returns all field storage definitions for base fields and bundle
   * fields of an entity type. Note that field storage definitions of a base
   * field equal the full base field definition (i.e. they implement
   * FieldDefinitionInterface), while the storage definitions for bundle fields
   * may implement FieldStorageDefinitionInterface only.

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?

joachim’s picture

I'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.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bbrala’s picture

@WimLeers any chance you can pick this up again?

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

joachim’s picture

Status: Needs review » Needs work

Needs work, based on #26.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

eliaspapa’s picture

Is 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.

owenbush’s picture

I'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

marttir’s picture

Ended 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?

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.