Repository navigation
driver-sql on PostgreSQL answers 500 for a boolean or Date compared against a number field (where { amount: { $gt: true } }), while memory answers no rows and SQLite every row: the non-string half #20336 / #20351 left out #20502
Description
Activity
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsPath: run it — one question, one answer on every driver | 缺项 (a boolean or
Datecomparand against a number field) | P2Triage: first grade —
bug·priority:p2·domain:spec·area:api·pm:queue. Direction: widen the published verdict, then the #20351 door consumes itTriage: the verdict lives in
packages/spec/src/data/filter-number-comparand-declared-type.ts(PR #20414, #20336 wasdomain:spec) ⇒domain:spec. The engine door that consumes it is the one PR #20501 (#20351) adds inpackages/objectql/src/number-comparand-declared-type-door.ts, so it goes in the same PR.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T20:02Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. One client mistake gets three answers, and PostgreSQL's is a 500
DATABASE_ERROR, a server fault for a client error. The query still runs on the other two drivers, just wrong, so this is runs-but-wrong. It is the same grade as its string sibling #20336 (p2).Direction (triage's call, as the card suggests): one verdict, widened.
- The published contract refuses any comparand against a declared
numberfield that is neither a number nor a string the numeric grammar admits: boolean,Date, object and array. The answer isINVALID_FILTER/ 400, naming the field. The string rule stays as PR feat(spec): the number-comparand declared-type door's contract and the platform's numeric grammar #20414 left it. - The engine door consumes that verdict and nothing else. ⛔ No second rule in the door, and ⛔ no driver-side coercion of
trueto1. - Pins: memory, SQLite and PostgreSQL at
where, at the per-aggregationfilterand athaving, fortrueand aDate, with a numeric control. - Serial after PR fix(objectql)!: refuse a non-numeric string compared against a number field at the engine's filter door, and narrow a numeric one (#20351) #20501 (objectql: refuse a non-numeric string compared against a number field at the engine's field-aware filter door (400, every driver and position) — the door half of #20336 #20351, draft). The door this consumes is new in that PR, in the same files.
- The published contract refuses any comparand against a declared
- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_014EJ1ED8X4MMrT18BhVx4tx
Account:os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-20502-number-comparand-non-string
Worktree:objectstack-issue-20502
Domain:domain:spec(the verdict; the engine door that consumes it is carried in this one PR, as triage5877498426routes it)
Seat:domain:spec#2(seat post #18549)
File surface, per triage5877498426:packages/spec/src/data/filter-number-comparand-declared-type.ts: the published verdict is widened to refuse any comparand that is neither a number nor a numeric-grammar string (boolean,Date, object, array) against a declarednumberfield, withINVALID_FILTER/ 400 naming the field. The string rule is unchanged.packages/objectql/src/number-comparand-declared-type-door.ts: consumes that verdict and nothing else. ⛔ No second rule in the door; ⛔ no driver-side coercion.- Their tests, pinned on memory, SQLite and PostgreSQL at
where, at the per-aggregationfilterand athaving, fortrueand aDate, each with a numeric control. - The ADR-0087 kit the gates ask for, and
.changeset/20502-*.md.
(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate). A published verdict narrows, so the at-tier contract review is owed before enqueue.
Clause-②: no (narrowing)
Thread-read: 5877498426
Serial constraints cleared: read at 2026-09-28T20:43Z againstorigin/main4a1df19656. - The triage-named predecessor PR fix(objectql)!: refuse a non-numeric string compared against a number field at the engine's filter door, and narrow a numeric one (#20351) #20501 (objectql: refuse a non-numeric string compared against a number field at the engine's field-aware filter door (400, every driver and position) — the door half of #20336 #20351) merged as
4a1df19656at 2026-09-28T20:26Z, so the door exists onmain. - No open PR touches either file (open-PR file lists read at this stamp).
- This seat's in-flight packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234 stage 3 edits comment lines under
packages/spec/src/data/**. That is ordinary concurrency, and the later lander merges.
Generated by Claude Code
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20502,
"status": "done",
"branch": "claude/issue-20502-number-comparand-non-string",
"pr": "#20545",
"session": "session_014EJ1ED8X4MMrT18BhVx4tx",
"premise_still_valid": true,
"summary": "The published verdict numberComparandDoorVerdict now answers door-refusal (INVALID_FILTER / 400) for a boolean, a Date and an array at a judged position on a declared numeric field, beside the unchanged string rule. It adds NON_NUMERIC_VALUE_FORMS and the NonNumericValueForm / NonNumericComparandForm types, three measured refusal clauses, a Date rendered as Date(ISO), and a value group in the derived case table. The objectql door changed only its refusal-site cast (value: comparand) and comments, so there is no second rule and no driver-side coercion; after the change, where (both spellings), the per-aggregation filter and having all answer 400 before any read on memory, SQLite and PostgreSQL 16, with the numeric controls unchanged. Before-state reproduced at 4a1df19: $gt true answered no rows / every row / 500, a Date no rows / no rows / 500, and the per-aggregation filter and having silently coerced true and [1] to 1 on all three. Objects are deliberately left to the comparand-type door, which already refuses them 400 at every position and on both spellings; the rationale is in deviations and open_questions. The producer census found no in-repo producer comparing a number field with a boolean, Date or array through the engine.",
"tests": "All at final head 829106f (origin/main 31d281d merged via os-regen-merge.sh; whole workspace built 72/72), each through os-verify-lock (VERDICT command-exit 0): spec door file 41/41, spec local 573 files 16842 passed 1 todo, spec repo 39 files 701 passed; objectql door file 34/34, objectql local 332 files 6641 passed, repo 1 file 5 passed; rest door file 8 passed 4 skipped (SQLite and live PostgreSQL 16 ran; MySQL named skip), rest local 222 files 4256 passed 22 skipped, repo 1 file 8 passed; driver-sql whole suite (TZ=America/New_York, live PG at Asia/Shanghai) 206 files 3997 passed 93 skipped; driver-memory 59 files 1419 passed; typecheck spec/objectql/rest exit 0 with test-typecheck ledgers held (53/251, 40/234, 0/0). Lint, proven narrowing: 5 touched lintable files all isPathIgnored=false in eslint.config.mjs, eslint --no-inline-config --format json = 5 files 0 errors 0 warnings, config has no parserOptions.project / projectService (type-aware linting off, so untouched files cannot move). Ablation from committed 829106f via ablation-replace wrap mode: the verdict's routing line gains an OR with globalThis.ABLATION_20502 === undefined (non-strings pass again); anchor 1 to 0, blob fdb42a2a to 55919fda, spec rebuilt, ablation-dist-preflight marker in 4 built files (exit 0); spec 5 failed / 36 passed, objectql 4 failed / 30 passed, rest 2 failed / 6 passed / 4 skipped (SQLite + PG cells), every failure a new non-string pin or the forms guard, all string pins and numeric controls green. Restore: blob back to fdb42a2a = HEAD blob, git diff HEAD empty, porcelain clean, rebuild, preflight --absent: absent from all 224 built files and tree clean, suites 41/41, 34/34, 8+4 skipped. A first attempt (constant OR 'ABLATION_20502') was void: preflight exit 1, because tsup treeshake folded the constant together with the marker; it was redone. Before/after table: scratch script against the built packages (before 4a1df19, after 0dc6901), in the PR body.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 829106f: 88 commands, all 88 run with exit captured before any pipe, 88 exit 0 (check:dual-build-cjs-loads and check:type-check-debt answered exit 3 PREREQUISITE NOT MET until the whole workspace was built, then 0). dispatch-gates --ran with command :: exit N: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero). check:adr-0087-registration: BREAKING+bang+clause-2-narrowing, not-required (no-migration-prescription). check-changeset-no-major: no major. CI convergence not awaited (in_progress at report time is PM's to read).",
"line_budget": "n/a: no skills/** and no governed surface in the diff. Branch delta vs origin/main (three-dot): 8 files, +544 / -47.",
"files_changed": [
"packages/spec/src/data/filter-number-comparand-declared-type.ts",
"packages/spec/src/data/filter-number-comparand-declared-type.test.ts",
"packages/objectql/src/number-comparand-declared-type-door.ts",
"packages/objectql/src/engine-number-comparand-declared-type-door.test.ts",
"packages/rest/src/data-number-comparand-door.test.ts",
"packages/spec/api-surface/data.json",
"packages/spec/export-origins/data.json",
".changeset/20502-number-comparand-non-string.md"
],
"deviations": [
"Objects: triage's list names object, but the widened verdict answers passes for a plain object (and for undefined, a Map, a symbol or a function) and leaves it to the comparand-type door. Measured on the base, that door already refuses these with INVALID_FILTER / 400, naming the path, on all three drivers and at all three positions (REST ingress: VALIDATION_FAILED). The engine runs the two doors in a different order per position (number door first on object-form where and the per-aggregation filter; comparand-type door first on FilterArray and having), so a second refusal would give one mistake two sets of words and change the comparand-type door's pinned words. It is pinned: the engine message equals normalizeFilterComparandTypes' own message, on both spellings. Raised in open_questions for the at-tier review.",
"Memory cell: pinned by construction in the objectql door suite (the recording driver asserts zero reads; the door refuses before any driver is resolved), as the predecessor PR did; the live InMemoryDriver before/after rows come from a scratch run. No permanent real-InMemoryDriver cell, because it would need a new dependency edge (rest or objectql on driver-memory).",
"Case-table fix inside the file surface: caseFor now answers passes at an unjudged slot (the flag operators' booleans $null/$exists/$empty), because the widened verdict refuses booleans; the engine never refused them.",
"Merges: the first merge of origin/main (0dc6901, before the branch touched any generated artifact) was a plain git merge. The second (829106f) went through os-regen-merge.sh; check:generated was 15/15 up to date at the final head and no regeneration commit was owed.",
"Coordinator's restart message: its premise did not hold in this container. uptime read 12h58m, my phase-2 script (PGID 10938), its locked workspace build and PostgreSQL on 54502 were all alive; only a foreground tail --pid call had been killed (exit 137). There was no stale lock holder, so PostgreSQL was not restarted; I terminated my own recorded process group 10938 (a pre-merge build) to merge main and rebuild once.",
"Teardown: private PostgreSQL 16 (pg_ctl stop, data dir removed) and all my process groups are gone; worktree node_modules removed; git worktree remove (no --force) follows this comment, with the tree clean and pushed at 829106f."
],
"mcp_calls": "0",
"api_writes": "3 (git push not counted), each a relay POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, giving draft PR 20545 (body read back byte-identical, 15091 bytes); (2) label-write assign, POST /repos//issues/20545/assignees os-tesla (read back: assignee os-tesla; labeler path labels present, not mine); (3) this os-dev-report comment via post-stamped, POST /repos//issues/20502/comments.",
"open_questions": [
{
"question": "Should the number verdict itself refuse a plain object (and undefined / a Map) against a numeric field, rather than leave those to the comparand-type door?",
"options": [
"A: as implemented. The verdict passes values outside the accepted comparand types; the comparand-type door refuses them 400 on every field, position and spelling, in one set of words. Cost: the verdict alone does not list object.",
"B: the verdict refuses them too. Cost: one mistake answered in two sets of words by position and spelling (number door first on object-form where and per-aggregation filter, comparand-type door first on FilterArray and having), and the comparand-type door's pinned words change for numeric fields, unless the engine's door order is also changed (engine.ts, outside this claim)."
],
"recommendation": "A. Actual business need: no producer, and the platform already refuses objects loudly everywhere. Long-term soundness: one owner per question (type membership versus numeric reading). AI-proofing: every object shape is already a loud 400 at every position. Startup focus: no new door order and no second dialect."
}
],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object/query and engine.find measured on base 4a1df19 (unchanged at head): where { amount: { "a": 1 } } (a plain object with no $ key, filter structure, on a scalar number field) answers 200 with no rows on InMemoryDriver but a driver 400 INVALID_FILTER on SqlDriver SQLite and PostgreSQL. One mistake, two answers; not specific to numeric fields · dedupe words: nested-relation deep-equality object scalar field, memory 200 sql 400 filter structure, no dollar key plain object comparand",
"carrier: none · the save-time half of the number verdict (no lint or save rule reads numberComparandDoorVerdict, so a stored filter comparing a number field with a string and now also a boolean or array saves clean and is refused at query time) is the family the predecessor dev report already carries; this change widens its population, no in-repo producer found · noted, not filed"
]
}objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsACCEPT — PR #20545 at head
829106fd06·domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T02:00ZThe seat reviewed the dev report
5882074829against GitHub and the diff.- PR shape: draft, base
main, first lineFixes #20502, assigneeos-tesla. 8 files (+544 / −47), all inside the claim. NOT governed. A driver-freemerge-treeontoorigin/mainexits 0. - Diff, read by the seat:
- The published verdict
numberComparandDoorVerdict(packages/spec/src/data/filter-number-comparand-declared-type.ts) now answersdoor-refusalfor a boolean, aDateand an array at a judged position on a declared numeric field or a numeric aggregated column. The string rule is unchanged. - The objectql door changed only its refusal-site cast and comments: one rule, no second predicate, no driver-side coercion.
- Three exports were added (
NON_NUMERIC_VALUE_FORMS,NonNumericValueForm,NonNumericComparandForm), andapi-surface/data.jsonandexport-origins/data.jsonare regenerated to match. - The changeset is
minorfor@objectstack/specand@objectstack/objectql,Clause-②: no (narrowing), ADR-0087not-required (no-migration-prescription).
- The published verdict
- Before and after, measured:
- Before:
$gt trueanswered no rows on memory, every row on SQLite, andDATABASE_ERROR/ 500 on PostgreSQL 16. The per-aggregationfilterandhavingsilently readtrueand[1]as 1 on all three. - After:
INVALID_FILTER/ 400 atwhere(both spellings), the per-aggregationfilterandhaving, on all three, before any read. The numeric controls are unchanged.
- Before:
- Evidence:
- spec local: 573 files / 16,842 tests. objectql: 332 files / 6,641. rest local: 222 files / 4,256. driver-sql: 206 files / 3,997, with live PostgreSQL 16. driver-memory: 59 files / 1,419. Typechecks are clean.
- The ablation of the verdict's routing line turns only the new non-string pins red, on spec, objectql and rest, and the restore is clean.
- At this head, 88 gates were derived and all 88 ran with exit 0.
- At-tier contract review:
5882199114on the PR, atCONTRACT_REVIEW_TIER, on this head — PASS. The seat checked its transcript: served at tier, read-only, one write (that comment). - The dev's open question: objects,
undefinedandMap. Seat answer: A, as implemented.- The record verified that the comparand-type door already refuses them with
INVALID_FILTER/ 400 at every literal position and on both spellings: object-formwhere, FilterArray, the per-aggregationfilterandhaving. Message equality pins that on both spellings. - So the answer to the author is the same 400 either way. Only which door's words answer differs, and one owner per question (type membership versus numeric reading) keeps one set of words.
- This is a technical ordering choice with no product-visible change, not a decision-box item.
- The record verified that the comparand-type door already refuses them with
- Deviations, adopted:
- The memory cell is pinned by construction: the recording driver asserts zero reads.
packages/restandpackages/objectqlhave nodriver-memoryedge. - The case-table fix:
caseForanswerspassesat the unjudged flag-operator slots. It is inside the claimed file and pinned three ways. - The PostgreSQL cell of the REST suite is a named skip in CI, because
OS_TEST_POSTGRES_URLis set only for driver-sql, metadata-protocol and runtime. The PostgreSQL row rests on the dev's local PostgreSQL 16 run, and MySQL is NOT MEASURED. The SQLite cell runs in Test Core.
- The memory cell is pinned by construction: the recording driver asserts zero reads.
- Findings:
where { amount: { "a": 1 } }, a no-operator object used as filter structure on a scalar field, answers 200 with no rows on memory and 400 on SQL: filed [finding] a plain object with no$key as a scalar field's filter value answers 200 with no rows on the memory driver andINVALID_FILTER400 on SQLite and PostgreSQL #20546.- The save-time half of the verdict: no lint or save rule reads it. This is the predecessor's known family, and the change widens its population. Acceptance notes.
- spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" at
havingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510 grows. This diff does not do what spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510 asks. Thehavingrefusal still says "compares a declared number field", pinned athaving.total.$gt. The two new clauses (boolean,date) carry "(PostgreSQL with a server error)" at every position, includinghavingand the per-aggregationfilter. spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510 proceeds after this landing, with two more clauses in scope. Its fix moves the objectqlhavingpin and the RESTwherephrase pin. The seat records this on spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510.
- Landing: when every check on this head is green or a roster skip, this seat runs the pre-landing checks, flips it ready and arms auto-merge.
Generated by Claude Code
- PR shape: draft, base
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsLanding record — PR #20545 MERGED ·
domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T02:25Z- Merged through the merge queue at 2026-09-29T02:24Z as
b05743433b, after the ACCEPT5882214533and the at-tier PASS5882199114. - Verified by content on
origin/main, not by the merged flag: all 8 files are blob-identical to the reviewed head829106fd06. That covers the published verdict, the objectql door, their tests on memory / SQLite / PostgreSQL, the REST suite, the regeneratedapi-surface/data.jsonandexport-origins/data.json, and the changeset. - What ships: a boolean, a
Dateor an array compared against a declared numeric field or a numeric aggregated column is refusedINVALID_FILTER/ 400 atwhere, the per-aggregationfilterandhaving, on every driver, before any read. Previously it answered no rows, every row, or a PostgreSQL 500. The string rule is unchanged. - Closing:
Fixes #20502closed this card ascompleted;pm:dispatchedis removed in this act. spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510, which was serial after this card and whose scope grew by the two new clauses (note5882219550), is now dispatchable. [finding] a plain object with no$key as a scalar field's filter value answers 200 with no rows on the memory driver andINVALID_FILTER400 on SQLite and PostgreSQL #20546 carries the plain-object filter-structure split.
Generated by Claude Code
- Merged through the merge queue at 2026-09-29T02:24Z as
- added a commit that references this issue
on Sep 29, 2026 - added a commit that references this issue
on Sep 29, 2026 - added a commit that references this issue
on Oct 7, 2026
Filing gate: ① a product defect with a measured
reach:. Finding class (a).reach:was measured at RESTPOST /api/v1/data/:object/queryand atengine.find, on a local PostgreSQL 16 server, onInMemoryDriverand onSqlDriver/ SQLite. Measured by the #20351 dev at base3062e5001and at PR #20501's head (identical).Filed by the
domain:engineexecution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN,os-warren) from the #20351 dev's report (os-dev-reporton #20351,out_of_scope_findings[0]). #20336's at-tier review (5868202485) asked #20351 to carry boolean andDatecells and to file only if they answer 500. They do. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens
A declared
numberfield,where { amount: { $gt: <comparand> } }:trueDATABASE_ERRORDate(engine.find)DATABASE_ERROR"abc"(after PR #20501)INVALID_FILTEROne client mistake gets three answers, and PostgreSQL's is a server fault. It is the same shape #20336 fixed for a non-numeric string.
Why
The number-comparand contract
#20336published (packages/spec/src/data/filter-number-comparand-declared-type.ts, PR #20414) judges strings only. The engine door PR #20501 adds consumes that verdict and nothing else, so a boolean orDatecomparand against a number field reaches the driver bind as written, and PostgreSQL refuses the bind.Suggested shape (⛔ not a ruling)
One answer for every dialect and position, decided once: refuse a non-numeric, non-string comparand (boolean,
Date, object) against a declared number field withINVALID_FILTER/ 400 naming the field. That is the spec verdict's scope widened (adomain:speccontract change) and the #20351 door consuming it. Pin it on memory, SQLite and PostgreSQL atwhere, the per-aggregationfilterandhaving.Dedupe
search_issues"boolean comparand number field postgres 500 Date comparand numeric column DATABASE_ERROR" inobjectstack-ai/objectstack, open and closed: 3 hits. #20351 is the string door (PR #20501), #20336 (closed) is the string contract, and #13382 (closed) is an OCCDatetoken. None is this.Dedupe words:
boolean comparand number field postgres 500·Date comparand numeric column database_error·non-string comparand declared number type