Skip to content

finding(core): a null-valued filter key gets TWO different row sets, decided by whether the query happened to carry $expand / $search #9020

Description

@os-steve

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.

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 seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions