Skip to content

ObjectQLStrategy.execute cannot see a cross-object filter nested in a conjunct, so it accepts a set /analytics/sql rejects #10759

Description

@os-warren

Found while implementing #10413 phase 1 (PR #10758). Filed rather than widened into that PR: different defect, different remedy.

What the code says

ObjectQLStrategy runs its cross-object envelope check from two call sites, and they are handed different views of the filter:

  • execute()this.planCrossObject(cube, query, filter), where filter is the built engine-filter record.
  • generateSql()this.planCrossObject(cube, query, Object.fromEntries(collectFilterLeaves(normalizeAnalyticsFilterTree(query)).map(...))), i.e. every member the filter touches, flattened.

planCrossObject inspects only Object.keys(filter):

...Object.keys(filter).map((f)=>({where: 'filter',member: f,field: f})),

An AND-ed leaf lands at the top level of the record, so it is seen. Anything structural — an $or, a $not, a nested $and whose operands cannot merge — is pushed onto conjuncts and folded into filter.$and, so the only key the check sees for it is the literal $and, which is never a cross-object field name.

So a query whose cross-object reference sits inside a combinator (where: { $or: [{ 'account.industry': 'x' }, ...] }) is not refused by execute() and reaches engine.aggregate, which cannot join — while the same query on /analytics/sqlis refused, because the echo's call site flattens the leaves. That contradicts the invariant the file states for itself:

generateSql() calls this too, so the preview accepts/rejects the same set.

and the reason the refusal exists at all:

the envelope check rejects cross-object filters, so a member it cannot see is a filter it cannot reject

A loud 400 was chosen over a silent mis-bucket; for this shape neither happens on the execution door.

Scope note

This predates #10413 — the caller's own $or has always taken this route. PR #10758 adds one more producer of the same shape (a dataset's definition-level filter travels as an $and conjunct), so a cross-object dataset filter is now in the same blind spot; it does not create the blind spot.

Confidence

Read from the code, in packages/services/service-analytics/src/strategies/objectql-strategy.ts (the two call sites and planCrossObject's member collection). Not executed — no runtime reproduction was run, and the triage should reproduce before sizing. The fix shape is plausibly one line (hand execute()'s call site the same collectFilterLeaves view the echo uses), but whether the refusal set should widen for real callers today is a judgement, not a mechanical fix.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions