Problem/Motivation
SegmentResolver::buildConditionGroup() builds a nested AND/OR condition structure and applies it to an entity query. Entity API's QueryAccess\ConditionGroup is the same structure — nested conjunctions, addCondition($field, $value, $operator) — with one capability this module lacks: ViewsQueryAlter applies the same tree directly to a Views query.
That capability is the real motivation. Plugin/views/filter/SegmentAudience currently resolves a segment to a full ID list and filters on it, so filtering a View by a large segment materializes every member ID in PHP first. A tree both backends can consume removes that step.
Proposed resolution
Have queryable plugins contribute to an Entity API ConditionGroup rather than directly to a ConditionInterface, then hand the finished group to EntityQueryAlter for resolve() and to ViewsQueryAlter for the audience filter.
QueryableSegmentPluginInterface::applyToQuery() changes shape. SegmentResolverInterface does not.
Remaining tasks
- Confirm
ConditionGroupsupports the reference traversal the entity query gets fromfield.entity.subfieldpaths. - Benchmark the Views audience filter before and after on a large audience.
- Decide the fate of the set-based fallback path, which cannot be expressed as a query group and would still materialize IDs.
User interface changes
None.
API changes
drupal/entity becomes a dependency of the base engine; it is currently present only transitively via group and crm. QueryableSegmentPluginInterface changes, breaking third-party queryable plugins. The module is alpha with no backwards-compatibility promise, but this belongs in the release notes.
Data model changes
None. The stored condition tree format is unchanged.
Issue fork entity_segment-3612428
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
jdleonardInvestigated against
drupal/entity8.x-1.6 (HEAD of8.x-1.x, the only active branch — there is no 2.x). The answer to remaining task 1 is no, and it turns out to be the smaller of two blockers. Closing this as won't fix.1. ConditionGroup cannot express reference traversal (remaining task #1)
ViewsQueryAlter::mapConditions()parses a field path like this:explode()is unbounded andlist()takes the first two elements, so every segment past the second is silently discarded:entityis then handed toDefaultTableMapping::getFieldColumnName(), which does not validate the property name. Kernel-test output onentity_testwith a configurable reference fieldorgand the base reference fielduser_id:So a traversal path fails in one of two ways depending on how the reference field is stored:
user_id.entity.name = 'Acme'compiles tobase_table.user_id = 'Acme', comparing an entity ID against a name. Wrong rows, silently.For comparison, the entity query gets the same path right, because it goes through core's
Drupal\Core\Entity\Query\Sql\Tables:EntityQueryAlterusesTablesand would be fine;ViewsQueryAlterhas no join-through-a-reference logic anywhere in the class. Traversal is architecturally absent there, not merely buggy — which defeats the whole premise, since the premise is one tree, two backends.Worth flagging separately: that
continueon an unrecognised field meansViewsQueryAltersilently drops conditions it cannot map. For access filtering, dropping a condition fails open on a query that was already permission-scoped. For a segment audience filter it silently widens the audience — andSegmentAudiencehas a privilegedrawmode. That is not a failure mode we want on this path.2. Condition rejects three operators we already ship
Drupal\entity\QueryAccess\Condition::$supportedOperatorsis a closed list, and the constructor throws\InvalidArgumentExceptionfor anything outside it:FieldValue::operatorLabels()offers all three for the string category. So this is not a Views-only regression: any stored, currently valid segment using "contains", "starts with" or "ends with" would throw insideresolve()— the primary path. Excluding those operators from push-down instead would mean forcing the set-based fallback exactly where push-down matters most (string matching over a large base).3. Neither Alter class is reusable as an applier
The proposed resolution is to "hand the finished group to
EntityQueryAlter… and toViewsQueryAlter". There is no public API for that:@internal.EntityQueryAlter::alter(SelectInterface $query, EntityTypeInterface $entity_type)fetches its own conditions from the entity type'squery_accesshandler. It cannot be handed a caller-supplied group.ViewsQueryAlter::alter(Sql $query, ViewExecutable $view)does the same, and returns early unless the entity typehasHandlerClass('query_access').mapConditions()isprotectedin both.The only ways through are to register a
query_accesshandler on every segment target type — which hijacks a global access mechanism to smuggle in a per-View filter, and would apply to every query of that type rather than this one View — or to copy both mappers into this module. The second means taking a hard dependency ondrupal/entityfor a data structure, while writing ourselves the only part that actually works.4. ConditionGroup::addCondition() rewrites the tree
count() === 0is silently dropped.count() === 1is flattened into its parent.We define an empty group as selecting no entities; dropping it from an
ANDparent makes it select every entity — an exact inversion. TodaySegmentResolver::analyze()marks empty groups non-queryable so they never reach a builder, so this is latent rather than live. But the stored tree is this module's data model, and the flattening also breaks the "the tree you see is the tree that runs" property thatConditionTreeRendererand the diff builder lean on.5. The motivation is weaker than the summary states
SegmentAudiencedefaults tomode: filtered, which routes toAudienceAccess::filtered()→SegmentResolver::resolveAccessible()— and that loads every audience entity and calls->access('view')on each. A condition tree pushed into the Views query removes the ID materialization only inrawmode. In the default mode the per-entity access loop remains, cannot be expressed as a query condition under any design, and dominates the cost. So even a working version of this change would leave the default configuration exactly as expensive as it is now.Remaining task 3 — the set-based fallback
Independent of this proposal, and unchanged by it: a set-based plugin's
resolveToIds()is arbitrary PHP, so it can never become a query group. It has to stay. Any future push-down has to accept a mixed tree withIN (ids)leaves where a set-based child sits.Remaining task 2 — benchmark
Not run, deliberately. It was scoped as before/after on this change, and there is no "after" to measure. It moves to the follow-up below, where it can gate a real alternative.
Closing this issue
Marking this won't fix.
QueryableSegmentPluginInterfacedoes not change, the stored tree format does not change, and there are no release notes to write.The motivation is still worth pursuing, but with core primitives rather than Entity API's QueryAccess structures.
Drupal\Core\Entity\Query\Sql\Tables— the classEntityQueryAltergets right andViewsQueryAlterlacks — is public API, is instantiable against anySelectInterface, and already implementsentity.traversal and the full operator set. A follow-up can build aSELECT idsubquery of the target type from the wholly-queryable part of the tree and hand it to Views asIN (subquery), keepingIN (ids)leaves for set-based children. That keeps traversal, keeps every operator, adds no dependency, and does not rewrite the stored tree. It would still only helprawmode, per point 5, so the benchmark comes first and decides whether it is worth building at all.