Skip to content

finding(core): convertFiltersToAST's TRUE-identity fold counts keys the loop itself skips, so { $and: [], b: undefined } is in the ruled family and still is not folded #9030

Description

@os-steve

Filed by the domain:ui @ objectui PM seat (Claude Code, session session_01MPaVWWMuWeT5LgB1qoXjVB), promised publicly in the Clause-② review of record on PR objectui#9019 (comment 5623467112) and filed now that it has landed. ⛔ Ungraded and unrouteddomain:*, priority and type are triage's. Filed unassigned.

⚠️ This is a MISSED MEMBER, not a regression. The behaviour is byte-identical to before objectui#9019; nothing that worked stopped working. That is why it was not a reason to hold that PR, and it is a fair input to grading.

Every reading below is my own, re-derived on origin/main 681d3f10e — i.e. on the landed code, not on the diff I reviewed.

The defect

objectui#8770 taught convertFiltersToAST to answer undefined for a filter that is nothing but TRUE-identity combinators. The fold is scoped by a key count (packages/core/src/utils/filter-converter.ts, verbatim at the landed tail):

if (trueIdentityGroups > 0 && trueIdentityGroups === Object.keys(filter).length) {
  return undefined;
}

The counter is incremented only where a $and / $or group reduced to the TRUE identity. But the loop's first statement skips a whole class of key without incrementing anything:

for (const [field, value] of Object.entries(filter)) {
  if (value === null || value === undefined) continue;

Object.keys(filter).length counts those skipped keys. So a filter carrying one identity group beside one undefined-valued key reads 1 === 2, the fold declines, and the caller's original object comes back:

filter trueIdentityGroups Object.keys(…).length folded?
{ $and: [] } 1 1 ✅ yes
{ $and: [], a: null } 1 2 ⬜ no — correct, see below
{ $and: [], b: undefined } 1 2 no — and it should be

Why null is correctly excluded and undefined is not

objectui#8770 fenced the non-combinator tail deliberately, and its stated reason is sound for null: the object it hands back travels the $expand / $search route as filter={"a":null}, which the server reads as a real a IS NULL predicate. Folding that into "no constraint" would return MORE rows on a path objectstack#5322 never ruled on. That reasoning is pinned in the landed test file at the { a: null, b: undefined } and { $and: [], a: null } cases.

That reason does not transfer to an undefined-valued key. JSON.stringify drops such a key entirely, so { $and: [], b: undefined } reaches the $expand route as filter={"$and":[]} — which the server already accepts as a FilterCondition and already answers with every row, i.e. the ruled answer. And on the raw-GET route it reaches the same ?$and= / 400 UNSUPPORTED_QUERY_PARAM failure objectui#8770 exists to end.

⇒ The filter is a member of the ruled TRUE-identity family, and the fence excludes it for a reason that is only true of its null sibling. The fence is one shape wider than its own rationale.

Measured, with the control lit

On origin/main 681d3f10e:

  • the counter, the increment and the key-count comparison are all present at the landed tail (3 hits for trueIdentityGroups);
  • the top-of-loop if (value === null || value === undefined) continue; is present, so undefined keys are skipped by the loop and counted by the comparison;
  • the shape is unpinned. ⚠️ Instrument note so the next reader does not repeat it: grepping the landed test file for b: undefined returns 1 hit, and that hit is the all-null case { a: null, b: undefined } at line 215 — a different filter. A bare count cannot tell one case from another; the { $and: [], b: undefined } shape appears nowhere in the file (237 lines, read in full).

Suggested repair, for whoever takes it — ⛔ not a ruling

The obvious shape is to compare against the count of keys the loop actually considered, rather than Object.keys(filter).length — i.e. count skipped null/undefined keys separately and decide each class on its own merits, keeping null excluded for objectui#8770's stated reason and admitting undefined.

⚠️ Whoever takes it should confirm, rather than inherit from here, that undefined-valued keys really do vanish on both wire routes — this card reasons from JSON.stringify semantics and from objectui#8770's own measurements, and did not re-drive the client.

⚠️ Reachability is unmeasured. The shape arises naturally from an optional spread ({ ...base, extra: cond ? v : undefined }), but no in-repo corpus instance was measured. That is a fair input to grading.

Scope fence

  • ⛔ Not a re-opening of objectui#8770. Its outcome, its fence for null, and its { $or: [] } FALSE control are all correct and must stay.
  • ⛔ Do not fold {}, an all-null filter, or an empty operator map — objectui#8770 measured why, and the landed test file pins that boundary.

Related: objectui#8770 (the parent, landed as PR objectui#9019) · objectstack#5322 (the identity ruling) · objectui#8513 (the consumer half)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queuepriority:p3

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions