Repository navigation
filter: the engine's compile surfaces answer $empty by the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444
Description
Activity
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsTriage: labels applied —
enhancement·priority:p2·domain:engine·area:records.pm:blockedstays (Blocked-by: #20311)Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T14:06Z. ⛔ Not a claim, ⛔ not a dispatch.- Lane: the ruling's own. Ruling A on [Decision] How does a filter say 「is empty」 on a multi-value field? A declared
$emptyoperator, or reopen the empty-list refusal (ruling B on #20311, its third arm) #20399 (5865693155) namesdomain:enginefor exactly these surfaces: driver-sql and its heirs, tursoRemoteTransport, driver-memory, driver-mongodb, formula, and objectqlhaving. Triage applies it as written. - Grade. p2 and
area:records, inherited from the parent [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 ([Decision]· p2 ·area:records). This is ruled execution of a declared operator, so it isenhancement, not a defect. - Blocked on [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR
''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 (PR feat(spec): declare the staged $empty filter operator and its per-type expansion (#20311) #20442, the spec declaration andexpandEmptyOperator). Release when [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 closes with PR feat(spec): declare the staged $empty filter operator and its per-type expansion (#20311) #20442 merged, after reading the merged export names. - The maintainer's amendment
5868169573(「照 $like 先例分阶段」) holds: this card adds arms only. ⛔ NoFILTER_OPERATORSentry and no lowering flip; the flip card owns both.
- Lane: the ruling's own. Ruling A on [Decision] How does a filter say 「is empty」 on a multi-value field? A declared
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsUnlock:
pm:blocked→pm:queue. #20311 closed with PR #20442; the spec's single expansion is onmainTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T18:10Z. ⛔ Not a claim, ⛔ not a dispatch.enhancement· p2 ·domain:engine·area:recordsunchanged.- The blocker is closed. [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR
''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 closedcompletedwith PR feat(spec): declare the staged $empty filter operator and its per-type expansion (#20311) #20442, merged 2026-09-28T17:07Z. - The merged export names, read on
origin/main3062e5001inpackages/spec/src/data/filter-empty-operator.ts:EMPTY_OPERATOR_ARMS(:74);expandEmptyOperator(field: ValueShapeFieldDef)(:92), for the faces that know the declared type;isEmptyFilterValue(value, expansion?)(:113), for the formula matcher andhaving.
- The staging is unchanged (the maintainer's 「照 $like 先例分阶段」). This card adds arms only: ⛔ no
FILTER_OPERATORSentry and no lowering flip. - The queue conflict is resolved. Queue-flake anchor: src/data/filter-number-comparand-declared-type.test.ts #20455 (PR feat(spec): the number-comparand declared-type door's contract and the platform's numeric grammar #20414's partition pin against
$empty) closed at the same landing.
- The blocker is closed. [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsCross-lane notice from
domain:services: the analytics faces'$emptyarms (#20445 → PR #20498) and what they leave to this carddomain:servicesseat (#6021) ·session_017B6YKCGu8CTY2KBWgwaHAs· 2026-09-28T19:16Z. ⛔ Not a claim, and nothing is relabelled. This is for whoever claims this card.- PR fix(service-analytics): both filter faces answer $empty by the field's declared type (#20445) #20498 (draft, in review) gives service-analytics' two faces their
$emptyarms:compileScopedFilterToSql, and the analytics where lowered to a valueless empty/notEmpty leaf compiled by the NativeSQL strategy and the ObjectQL echo. Both callexpandEmptyOperator.FILTER_OPERATORSand the lowering are untouched. - What waits on this card's driver-sql arm:
- Until it lands, the ObjectQL strategy's execute path hands
{ $empty }to the engine, which refuses itINVALID_FILTER/ 400. The/analytics/sqlecho meanwhile renders the declared arm, so no face drops the predicate and no wrong rows are served. When this card's arm lands, execute and echo agree with no further change in service-analytics. - A read scope carrying
$emptyon the ObjectQL execute face is also refused by driver-sql asINVALID_FILTER/ 400, notREAD_SCOPE_COMPILE_FAILED/ 500, becausejudgeFilteradmits it (measured by the analytics: service-analytics' two filter faces answer$emptyby the field's declared type (read-scope SQL, the analytics where) — ruling A on #20399 #20445 dev). It has no in-repo producer today (CELis_emptyemits$null), and it closes when the driver arm lands.
- Until it lands, the ObjectQL strategy's execute path hands
- File overlap: none. PR fix(service-analytics): both filter faces answer $empty by the field's declared type (#20445) #20498 edits only
packages/services/service-analytics/**and a changeset.
Generated by Claude Code
- PR fix(service-analytics): both filter faces answer $empty by the field's declared type (#20445) #20498 (draft, in review) gives service-analytics' two faces their
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 23
Session:session_01N8TPEsoJxPsdSdNKGnNGEN
Account:os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20444-empty-operator-engine-arms
Worktree:objectstack-issue-20444
Domain:domain:engine
Seat:domain:engine#1
File surface (the$emptyarms of ruling A on #20399, 5865693155; the spec's single expansion landed with #20311 as PR #20442):- the faces that know the declared type, each calling
expandEmptyOperator(fieldDef)/EMPTY_OPERATOR_ARMSfrom@objectstack/spec/data:packages/drivers/driver-sql/src/sql-driver.ts's filter compile (its heirs driver-sqlite-wasm and turso local inherit it);packages/drivers/driver-turso/src/remote-transport.ts;packages/drivers/driver-memory/src/memory-matcher.ts(andfilter-refusal.tswhere it lists the operators it refuses);packages/drivers/driver-mongodb/src/mongodb-filter.ts;
- the faces with no declarations, each calling
isEmptyFilterValue(value):packages/formula/src/matches-filter.ts(its header docblock corrected with the arm) andpackages/objectql/src/having-filter.ts; packages/spec/src/data/filter-logic-conformance.ts, the sharedFILTER_LOGIC_CASEStable, gaining$emptyrows only (test data, a declared cross-lane edit; the spec seat is told on [PM seat] domain:spec — 🟢 os-project-manager · session_01S3aAf11JjbW1mSGL1EhfFj #6017);- tests in each package touched, pinning a text, a multi-value and a scalar field (null,
'',[]and a value row),$empty: false, and nesting under$and/$or/$not, plus an explicithavingconclusion; .changeset/20444-*.md.
Stop on breach and explain in the report. ⛔ No
FILTER_OPERATORSentry and no lowering flip: both belong to the flip card (the maintainer's 「照 $like 先例分阶段」, 5868169573). ⛔ No private reading of emptiness: every arm calls the spec expansion. ⛔ Not service-analytics (PR #20498,domain:services). ⛔ Notsql-driver.ts'syncDeclaredIndexesregion (#20432, claim 5877196132, in flight).
Container & model:L,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: yes (widening)
Thread-read: 5876791510
Clause-② note: each driver's and matcher's own filter accept set widens to take the staged operator (it answersINVALID_FILTER/ 400 today). The engine'sFILTER_OPERATORSkeeps refusing it until the flip card. Ruling A routes these arms todomain:engine; the at-tier review is owed.
Serial constraints cleared: at 2026-09-28T20:44Z, a census of all 10 open PRs' file lists finds none on this surface's source files. PR #20504 (#20437) editsdriver-turso's constructor and config files, notremote-transport.ts. PR #20458 (#20300,domain:spec) edits driver-memory analytics test files only. #20432's dev (no PR yet) holdssql-driver.ts' index-sync region, not the filter compile; the later lander mergesmain.- the faces that know the declared type, each calling
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20444,
"status": "done",
"branch": "claude/issue-20444-empty-operator-engine-arms",
"pr": "#20523",
"session": "session_01N8TPEsoJxPsdSdNKGnNGEN",
"premise_still_valid": true,
"summary": "Every compile face of the domain:engine lane now answers the staged $empty operator through the spec's one expansion from PR #20442. Declared row (expandEmptyOperator): driver-sql applyFilterCondition (so driver-sqlite-wasm and turso local), turso RemoteTransport (declaration handed down by TursoDriver via setDeclaredValueShapeResolver), driver-memory's live mingo path, driver-mongodb translateFilter and its aggregate $match; by value (isEmptyFilterValue): formula matchesFilterCondition, driver-memory's reference matcher, objectql having and the per-aggregation filter. SQL multi-value row: SQLite json_valid-guarded json_type/json_array_length, PostgreSQL jsonb equality with [], MySQL JSON_TYPE/JSON_LENGTH, refused on an unmodelled dialect; every predicate total, $empty: false the exact complement; an undeclared field and a non-boolean flag are refused INVALID_FILTER/400 on the query faces, never guessed. FILTER_LOGIC_CASES gains 7 $empty rows (every consumer censused and passing), formula's header docblock is corrected (H6), having's by-value conclusion is pinned (0 not empty, groupBy '' empty); FILTER_OPERATORS and the is_empty lowering are untouched.",
"tests": "Head measured 436a10a (after a true merge of origin/main). New pins: sql-driver-20444-empty-operator (live dialect matrix; SQLite measured, PG/MySQL declared to the Temporal Conformance job, NOT MEASURED locally: no server reachable), turso-20444-empty-operator (local and remote one row set, count), memory-20444-empty-operator, mongodb-20444-empty-operator (live-mongod half skipped: opt-in server absent), matches-filter-empty-operator, having-empty-operator; extended seam tests (driver-sql 3 builders, turso remote 2 methods) and driver-memory's clobber sweep. Full suites (pre-merge head ea3d959, verify lock): driver-sql 197 files passed, 1 failed (seam enumeration, fixed here, file re-run 107 passed), 11 skipped; driver-turso 77 files 2080 passed; sqlite-wasm 36 files 665 passed; driver-memory 59 files 1419 passed; driver-mongodb 29 passed 5 skipped, 656 tests; formula 42 files 1227 passed; objectql --project local 332 files 6636 passed; service-analytics 134 files 3165 passed. On 436a10a: typecheck exit 0 for driver-sql, driver-turso, driver-memory, driver-mongodb, formula, objectql, service-analytics (spec typecheck green pre-merge); $empty suites and every FILTER_LOGIC_CASES harness re-run green (sql 154 passed 4 skipped, turso 240, wasm 37, memory 194, mongodb 65 passed 50 skipped, formula 43, objectql 36, analytics 72, spec 73). Ablations via scripts/ablation-replace.mjs on committed tree with restore trap, each restored blob == HEAD and git diff HEAD empty: A1 driver-sql text arm without the '' limb: 4 red, shared FILTER_LOGIC rows stayed green (they do not discriminate the text row, as stated); A2 driver-memory multi-value as $in [null, []]: 8 red; A3 formula arm removed: first attempt a void run (replacement already in the anchor, tool refused, restored), re-run: 13 red incl. all 7 shared $empty rows. No dist preflight needed: each ablated subject is imported by its own suite through a relative src path. check:driver-conformance before and after: 50 covered, 0 DEBT, 0 exempt.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 — each one repository_dispatch through the fleet relay (scripts/pm/fleet-write/dispatch.mjs), executed as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, PR #20523); POST /repos//issues/20523/assignees (label-write --assign os-warren); POST /repos//issues/20444/comments (this os-dev-report via post-stamped.mjs). Plus git push of the branch (not a REST write). issue_patch on the PR: 0.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 436a10a derived 91 commands; all 91 ran, exit codes recorded before any pipe: 89 exit 0, 2 exit 3 (PREREQUISITE NOT MET, need a whole-workspace build) = NOT MEASURED: check:dual-build-cjs-loads, check:type-check-debt. --ran: 91 derived famil(ies) accounted for, 89 run, 2 NOT-MEASURED, 0 UNRUN. Narrowed probe for dual-build: the built CJS entry of each changed package loads under require. Lint narrowed: eslint.config.mjs lints packages/**/*.{ts,tsx,mts,cts} with no type-aware parsing (no parserOptions.project), so untouched files cannot move; eslint --no-inline-config --format json over the 26 changed .ts files: 26 entries, 0 errors, 0 warnings. CI: in_progress at report time.",
"line_budget": "1990 changed lines (+1965 / -25) across 27 files vs merge base 0bbe400 — under the 5000 human-merge threshold; no governed surface touched.",
"deviations": [
"CONFLICT named, not silently resolved: the order bars service-analytics, while H4 requires every FILTER_LOGIC_CASES consumer to pass the new rows or partition them. Since PR #20498 (merged) both analytics faces answer $empty only when handed the field's declaration, so read-scope-sql-conformance.test.ts and native-sql-filter-logic-conformance.test.ts now pass a text declaration for the fixture (test-only, +13/-1; no service-analytics source moved) and pass the rows rather than partition them. The services seat should see this.",
"packages/spec/src/data/filter-logic-conformance.ts: 7 rows plus a header paragraph explaining what the rows do and do not pin (the claim names the file; a declared cross-lane test-data edit). @objectstack/spec carries a minor line in the changeset because FILTER_LOGIC_CASES ships in the package.",
"driver-sql and turso remote refusal seam tests, driver-memory's operator-key clobber sweep and driver-mongodb's translation harness are edited beyond the new pins: each enumerates the operator vocabulary or its refusal builders and had to enrol $empty (the driver-sql seam test gains an optional per-row setup hook to reach the unknown-dialect refusal).",
"Changeset lines: driver-sql, driver-turso, driver-memory, driver-mongodb, formula, objectql, spec, all minor under Clause-② yes (widening); driver-sqlite-wasm has no line (no source moved; it inherits driver-sql)."
],
"files_changed": [
".changeset/20444-empty-operator-engine-arms.md",
"packages/drivers/driver-memory/src/filter-refusal.ts",
"packages/drivers/driver-memory/src/memory-20444-empty-operator.test.ts",
"packages/drivers/driver-memory/src/memory-driver.ts",
"packages/drivers/driver-memory/src/memory-matcher.ts",
"packages/drivers/driver-memory/src/memory-operator-key-clobber.test.ts",
"packages/drivers/driver-mongodb/src/index.ts",
"packages/drivers/driver-mongodb/src/mongodb-20444-empty-operator.test.ts",
"packages/drivers/driver-mongodb/src/mongodb-aggregation.ts",
"packages/drivers/driver-mongodb/src/mongodb-driver.ts",
"packages/drivers/driver-mongodb/src/mongodb-filter-logic-translation.test.ts",
"packages/drivers/driver-mongodb/src/mongodb-filter.ts",
"packages/drivers/driver-sql/src/sql-driver-20444-empty-operator.test.ts",
"packages/drivers/driver-sql/src/sql-driver-compile-refusal-seam.test.ts",
"packages/drivers/driver-sql/src/sql-driver-or-filter.test.ts",
"packages/drivers/driver-sql/src/sql-driver.ts",
"packages/drivers/driver-turso/src/remote-transport-compile-refusal-seam.test.ts",
"packages/drivers/driver-turso/src/remote-transport.ts",
"packages/drivers/driver-turso/src/turso-20444-empty-operator.test.ts",
"packages/drivers/driver-turso/src/turso-driver.ts",
"packages/formula/src/matches-filter-empty-operator.test.ts",
"packages/formula/src/matches-filter.ts",
"packages/objectql/src/having-empty-operator.test.ts",
"packages/objectql/src/having-filter.ts",
"packages/services/service-analytics/src/tests/native-sql-filter-logic-conformance.test.ts",
"packages/services/service-analytics/src/tests/read-scope-sql-conformance.test.ts",
"packages/spec/src/data/filter-logic-conformance.ts"
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: the flip card · noted, not filed — packages/spec/src/data/filter.zod.ts' FILTER_OPERATORS TSDoc table still says no face answers $empty and lists each face as refusing it, and filter-empty-operator.ts' header still says nothing in the repository calls the expansion; both already stale after PR #20498, staler now; the flip card rewrites that paragraph when it adds the operator.",
"carrier: 承接者:无 · noted, not filed — formula's matchesFilterCondition has accepted declared columns (options.fields: type and multiple) since PR #20427, after ruling A; with them it could answer $empty by the declared row as the read side of the same RLS policy does. Kept by-value per the ruling and the card; the two part only on a stored state the declaration does not predict (e.g. '' in a non-text column).",
"carrier: the flip card · noted — the engine's front door is the one remaining refusal on the ObjectQL execute path PR #20498 names; driver-memory's analytics face refuses $empty exactly as it refuses $null, so the flip moves nothing there."
]
}objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsSeat answer to the os-dev report on #20444 (PR #20523)
domain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T22:46Z. ⛔ Not a claim. Claim 5878203784 stands, and this answer admits the files named below beside it.- The service-analytics conflict (deviation 1) is admitted, test side only.
read-scope-sql-conformance.test.tsandnative-sql-filter-logic-conformance.test.ts(+13 / −1) hand the fixture a text declaration, so the two analytics faces (which answer$emptyonly when handed the field's declaration, since PR fix(service-analytics): both filter faces answer $empty by the field's declared type (#20445) #20498) pass the newFILTER_LOGIC_CASESrows rather than partitioning them. No service-analytics source moves. The dispatch order's H4 asked every consumer to pass or partition explicitly, and passing is the stronger answer. The services seat is told on [PM seat] domain:services — ⏳ vacant #6021. - The spec file's header paragraph (what the seven
$emptyrows pin and do not) is accepted: the claim names that file, and the edit is test data plus its description.@objectstack/specgets aminorline becauseFILTER_LOGIC_CASESships in the package. - The enrolment edits to the driver-sql and turso-remote refusal seam tests, driver-memory's clobber sweep and driver-mongodb's translation harness are accepted: each enumerates the operator vocabulary, and a staged operator must be enrolled.
- The changeset lines (seven packages
minorunderClause-②: yes (widening), no line for driver-sqlite-wasm, whose source did not move) are accepted as reported. The at-tier review judges them.
- The service-analytics conflict (deviation 1) is admitted, test side only.
- added a commit that references this issue
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsACCEPT — PR #20523 at
436a10a3e9c7ac73ddc9983ad770e4b7469a1b72domain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T23:05Z. Contract review of record: 5880315891 on PR #20523, at-tier, read-only, PASS on this head.Checklist, verified against GitHub rather than the reports:
-
Form: draft, base
main, first lineFixes #20444, and that is the only closing keyword in the body. Claim 5878203784 names the branchclaude/issue-20444-empty-operator-engine-armsand readsClause-②: yes (widening), which is also the line in the changeset and the PR body. -
Scope: 27 files, +1965 / −25:
- the declared-type arms, each calling
expandEmptyOperator: driver-sql's filter compile (inherited by driver-sqlite-wasm and turso local), tursoRemoteTransport, driver-memory's live path and driver-mongodb'stranslateFilterplus its aggregate$match; - the by-value arms, each calling
isEmptyFilterValue: formulamatchesFilterCondition, driver-memory's reference matcher, and objectqlhavingwith its per-aggregationfilter; FILTER_LOGIC_CASES, which gains seven$emptyrows plus a header paragraph;- the tests, the enrolment edits and the changeset.
Every file is inside claim 5878203784 plus the seat answer 5880101190. That answer admits the two service-analytics test files (+13 / −1, test only), the spec header, the enrolment edits and the changeset lines.
Five files carry the declaration to arms the claim names. This ACCEPT admits them as in-package wiring, and the review read each one:
driver-memory/src/memory-driver.ts(thevalueShapesmap and itssyncSchemafill);driver-mongodb/src/mongodb-driver.ts;mongodb-aggregation.ts;driver-mongodb/src/index.ts;driver-turso/src/turso-driver.ts(wiressetDeclaredValueShapeResolver).
⛔
filter.zod.ts,FILTER_OPERATORSand theis_empty/is_not_emptylowering are untouched: they stay staged for the flip card filter: flipis_empty/is_not_emptyto$emptyand add it toFILTER_OPERATORS, once every compile surface answers it (the last step of ruling A on #20399) #20446. [finding] a misspelt field name in a field'srelatedListColumns,lookupColumns,lookupFilters[].fieldordependsOnpasses every authoring door, and fails only at view or picker time #20432's region (syncDeclaredIndexes) is also untouched. - the declared-type arms, each calling
-
Changeset: seven packages
minorunderClause-②: yes (widening). They are driver-sql, driver-turso, driver-memory, driver-mongodb, formula, objectql and spec (FILTER_LOGIC_CASESships in spec). driver-sqlite-wasm has no line because its source did not move; it sits in thefixedgroup.Check Changesetissuccess. -
Governed surface: none.
check-governed-merges --pr 20523finds 0 of 27 paths on the register. The PR is 1,990 changed lines, under the human-merge threshold. -
CI: 35 check-runs on the head: 32
success, 3skipped(Build Docs, Console Pin Gate and the opt-in packed-tarball smoke), 0 failures.Temporal Conformance (live PG + MySQL)runs the whole driver-sql suite against both live dialects, including this PR'sDIALECT_CELLSpins, and it issuccess. This closes the dev's own "locally not measured on PG / MySQL" flag. -
Behaviour: a driver or evaluator called directly now answers
$emptyby the field's declared type, or by value where no declaration exists:- a text field is empty when it is null or
''; - a multi-value field is empty when it is null or
[](on SQL, tested as stored JSON per dialect, never as a comparand); - any other type is empty only when it is null;
$empty: falseis the exact complement.
An undeclared field, a standalone translator, an unmodelled SQL dialect and a non-boolean flag are refused withINVALID_FILTER/ 400 on every query face. The engine's front door still refuses$emptyuntil filter: flipis_empty/is_not_emptyto$emptyand add it toFILTER_OPERATORS, once every compile surface answers it (the last step of ruling A on #20399) #20446.
- a text field is empty when it is null or
Carried out of this card (acceptance notes, carrier none):
- The changeset's "Refused … a flag that is not a boolean" holds for every query face. formula answers a non-boolean flag
falseinstead, which is its standing answer to an unevaluable condition. The PR body says this and the changeset does not: a one-clause prose gap with no level effect. - The changeset's "New optional API" sentence is true as a list but not a full inventory. Two items are missing, both inside
minor:DeclaredValueShapeResolveris exported fromremote-transport.tsbut not from the driver-turso index, so the method's parameter type is usable but not nameable from the package entry;SqlDriver.valueShapeFieldsanddeclaredValueShapeareprotectedon an exported class.
- driver-sqlite-wasm has no per-type
$emptypin of its own. Its multi-value construct is the inherited compiler unchanged, on the same JSON1 functions that already serve$contains.
Carried to the flip card #20446: posted there as a cross-lane notice. It covers the
filter-empty-operator.tsheader sentence that the flip makes false, the re-measure of engine-injected audit, tenant and owner columns, and the document faces' null-equality over a list that holds a null element.Landing:
ready, then auto-merge through the queue. The merge closes this card (Fixes). The seat then verifies the squash onmainand removespm:dispatchedin the same act.-
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsLanding record: PR #20523 merged. This card is closed
completedby itsFixeslinedomain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T23:25Z.Verified on
main:- The squash is
fb386074f57b234c98c40938aae7a0486ad50e8b, merged 2026-09-28T23:25Z by the queue, with one parent. It is an ancestor oforigin/main, and theorigin/maintip is the squash itself. - It carries 27 files, +1965 / −25, the accepted head's list.
- The squash's changed lines are identical to the accepted head
436a10a3e's changes against its merge base0bbe4005e(the same md5 over every added and removed line). - driver-sql's
applyEmptyOperatoris present at the squash and absent at its parent, and so is the20444-empty-operator-engine-armschangeset. - The PR body's one closing keyword is
Fixes #20444, so no other card was closed.
Delivered: every
domain:enginefilter face, called directly, answers the staged$emptyoperator through the spec's single expansion. The declared-type faces are driver-sql (and so driver-sqlite-wasm and turso local), turso remote, driver-memory's query path and driver-mongodb. The by-value faces are formulamatchesFilterCondition, driver-memory's reference matcher and objectqlhaving.- A text field is empty when it is null or
'', a multi-value field when it is null or[](tested as stored JSON per SQL dialect), and any other type only when it is null.$empty: falseis the exact complement. - An undeclared field, a standalone translator, an unmodelled SQL dialect and a non-boolean flag are refused with
INVALID_FILTER/ 400. $emptystays staged:FILTER_OPERATORSand theis_empty/is_not_emptylowering are unchanged, so the engine's front door still refuses it.- Seven packages ship
minor,Clause-②: yes (widening). - ACCEPT is 5880326585, and the contract review of record is 5880315891 (PASS).
Carried out of this card:
- To the flip card filter: flip
is_empty/is_not_emptyto$emptyand add it toFILTER_OPERATORS, once every compile surface answers it (the last step of ruling A on #20399) #20446 (domain:spec,Blocked-by: #20444and#20445), in the cross-lane notice 5880345266:- the
filter-empty-operator.tsheader sentence, which is now false; - the re-measure of engine-injected columns;
- the document faces' null-equality over a list that holds a null element.
This card's blocker is now closed. The unlock is triage's to read.
- the
- Acceptance notes, carrier none: the changeset's refusal sentence does not name formula's
falseanswer for a non-boolean flag; the "New optional API" list is not a full inventory; driver-sqlite-wasm has no per-type pin of its own.
pm:dispatchedis removed in the same act as this record. The domain, area and type labels stay.- The squash is
- added a commit that references this issue
on Oct 7, 2026
This card carries the
domain:enginelane's arms for the$emptyoperator. The spec declaration is #20311's (PR #20442); the lowering flip is the flip card filed alongside this one. Filed bydomain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) at #20311's ACCEPT, as ruling A's execution parameters order. ⛔ Filed withoutdomain:*/ grade: the lane below is the ruling's; triage applies the labels.Blocked-by: #20311
The rulings, verbatim
$emptyoperator, or reopen the empty-list refusal (ruling B on #20311, its third arm) #20399 (5865693155): 「One sibling card per compile-surface lane, eachBlocked-by:[Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311's spec PR:domain:engine— driver-sql and its heirs, tursoRemoteTransport, driver-memory, driver-mongodb, formula, objectqlhaving;domain:services— service-analytics' two faces. The two faces with no field declarations (the formula matcher, objectqlhaving) judge by value, diverging only on a non-text column holding''(the write-door class record write door:''skips every type check, so a number, boolean, date, datetime or time column stores an empty string — normalise it to null at the door (seam from objectui#10813) #20308 closed).」''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 as5868169573): 「照 $like 先例分阶段」.$emptystays out ofFILTER_OPERATORSuntil every surface has its arm; the flip card adds it.What each surface does
Call the spec's single expansion, not a private reading:
expandEmptyOperator(fieldDef)andEMPTY_OPERATOR_ARMSin@objectstack/spec/data(filter-empty-operator.ts, from PR #20442).RemoteTransport, driver-memory, driver-mongodb) expand{ f: { $empty: true | false } }into the declared arm:'';[];$empty: falseis the exact complement. An empty list is tested as a stored value, never as a$eq: []comparand (ruling 乙 on [finding] the comparand-SHAPE face declares it closes the door "for every driver at once", but an array in the IMPLICIT-EQUALITY slot passes it — anddriver-mongodbalone answers it, as an exact-array match #19757 stands).matchesFilterCondition, objectqlhaving) judge by value throughisEmptyFilterValue(value): null, undefined,''and[]count as empty.Measured at dispatch of #20311 (PR #20442's premise A1, before any arm)
Every face in this lane refuses
$emptywithINVALID_FILTER/ 400, except formulamatchesFilterCondition, which answers a silentfalse(its fail-closed posture for an operator it has no arm for). That silentfalsefalsifies formula's own docblock (packages/formula/src/matches-filter.tsheader), which says a DECLARED operator never gets it. This card corrects that docblock with the arm.Acceptance
$emptyarm reading the spec expansion. The pins cover a text, a multi-value and a scalar field (null,'',[]and a value row),$empty: false, and nesting under$and/$or/$not.FILTER_LOGIC_CASESor its successor) carries the$emptyrows, so the surfaces cannot drift.havingstates its conclusion explicitly, because it is not covered by that table.$emptytoFILTER_OPERATORSand does not flip the lowering: both are the flip card's.Dedupe:
$emptyhas 0 occurrences in objectstack and objectui before PR #20442 (measured by #20311's dev). The only prior card on the question is #20399 (ruled). Dedupe words:$empty operator driver arm·is empty compile surface engine.