Measured while landing objectui#8770 (PR #9019). Not that card's defect and deliberately not folded into it — different shape, different acceptance probe. Not claimed.
Measured
convertFiltersToAST (packages/core/src/utils/filter-converter.ts) SKIPS a key whose value is null / undefined — long-standing and pinned (filter-converter.test.ts, "should skip null and undefined values"). But when every key is skipped, the general tail returns the CALLER'S ORIGINAL OBJECT, and that object then means different things on the two find() routes of @object-ui/data-objectstack.
Measured against @objectstack/spec 17.4.0 and @objectstack/client 17.4.0, on 2596b1b85:
convertFiltersToAST({ a: null }) => { a: null } (same object back)
convertFiltersToAST({ a: null, s: 1 }) => ['s', '=', 1] (the null key is dropped)
Route A, the client SDK route (convertQueryParams then client.data.find) — the client's non-AST branch spreads a plain object's entries as query parameters and its spread SKIPS null values, so nothing at all is appended:
GET /data/acct (no filter parameter) => EVERY row
Route B, the raw GET route (rawFindWithPopulate, taken when $expand or $search is present; the export route builds the same way) — the object is JSON-serialised into filter=:
GET /data/acct?filter={"a":null} => the server's normalizer accepts a plain
object as a FilterCondition, so this is a
REAL `a IS NULL` predicate => SOME rows
Why this is a defect and not a curiosity
One authored filter, two row sets, and the deciding input is whether the query happened to want a lookup expanded or a search term — the exact split objectui#6948 recorded on this same file ("decided, as ever, by whether the query happened to expand a lookup") and that the shared translateFilterToAST helper was written to close. It is also self-inconsistent within one route: { a: null } means "no constraint" the moment ANY sibling key produces a condition, and means a IS NULL when it is alone — behaviour that depends on siblings, the hazard objectui#8555 named on this file.
Neither answer is obviously the right one, which is why this is filed rather than repaired in passing:
- if a null-valued key is a REAL
a IS NULL predicate, then the continue that skips it is dropping a predicate and widening the result set on every filter that has siblings;
- if it is "no constraint", then route B is applying a predicate the author's own converter says is not there.
@objectstack/spec's FilterCondition should be the tiebreaker; this card has not read it for that answer.
Explicitly NOT objectui#8770
objectui#8770 folds the TRUE-identity COMBINATORS ({ $and: [] } and friends) to "no constraint". Its fix is scoped, on purpose, to a filter whose every key is such a group precisely SO THAT it does not decide this question: { $and: [], a: null } keeps the object it always returned, and PR #9019 pins that boundary. So this stays open and independent after that lands.
Dedup
search_issues (repo-scoped, semantic) for this shape returned zero, and the instrument was validated in the same session with a known-hit control (a near-verbatim objectui#8770 title, which returned 9 results including it). Closed cards were in range.
Filed by the developer seat that implemented objectui#8770, session session_01MPaVWWMuWeT5LgB1qoXjVB, with Claude Code.
Measured while landing objectui#8770 (PR #9019). Not that card's defect and deliberately not folded into it — different shape, different acceptance probe. Not claimed.
Measured
convertFiltersToAST(packages/core/src/utils/filter-converter.ts) SKIPS a key whose value isnull/undefined— long-standing and pinned (filter-converter.test.ts, "should skip null and undefined values"). But when every key is skipped, the general tail returns the CALLER'S ORIGINAL OBJECT, and that object then means different things on the twofind()routes of@object-ui/data-objectstack.Measured against
@objectstack/spec17.4.0 and@objectstack/client17.4.0, on2596b1b85:Route A, the client SDK route (
convertQueryParamsthenclient.data.find) — the client's non-AST branch spreads a plain object's entries as query parameters and its spread SKIPS null values, so nothing at all is appended:Route B, the raw GET route (
rawFindWithPopulate, taken when$expandor$searchis present; the export route builds the same way) — the object is JSON-serialised intofilter=:Why this is a defect and not a curiosity
One authored filter, two row sets, and the deciding input is whether the query happened to want a lookup expanded or a search term — the exact split objectui#6948 recorded on this same file ("decided, as ever, by whether the query happened to expand a lookup") and that the shared
translateFilterToASThelper was written to close. It is also self-inconsistent within one route:{ a: null }means "no constraint" the moment ANY sibling key produces a condition, and meansa IS NULLwhen it is alone — behaviour that depends on siblings, the hazard objectui#8555 named on this file.Neither answer is obviously the right one, which is why this is filed rather than repaired in passing:
a IS NULLpredicate, then thecontinuethat skips it is dropping a predicate and widening the result set on every filter that has siblings;@objectstack/spec'sFilterConditionshould be the tiebreaker; this card has not read it for that answer.Explicitly NOT objectui#8770
objectui#8770 folds the TRUE-identity COMBINATORS (
{ $and: [] }and friends) to "no constraint". Its fix is scoped, on purpose, to a filter whose every key is such a group precisely SO THAT it does not decide this question:{ $and: [], a: null }keeps the object it always returned, and PR #9019 pins that boundary. So this stays open and independent after that lands.Dedup
search_issues(repo-scoped, semantic) for this shape returned zero, and the instrument was validated in the same session with a known-hit control (a near-verbatim objectui#8770 title, which returned 9 results including it). Closed cards were in range.Filed by the developer seat that implemented objectui#8770, session
session_01MPaVWWMuWeT5LgB1qoXjVB, with Claude Code.