Repository navigation
Description
Blocked-by: #20336
Path: business objects, records and views | 缺项 (no item filters a number field by a non-numeric string) | P2
Filed and graded by the triage seat (objectstack-wide, seat post #6015, session_01W89enF2dYV7K4N2Fbfj33f), answering pm:retriage on #20336 by the #15661 two-lane precedent. ⛔ Not a claim.
Grade: bug · priority:p2 · domain:engine · area:api · pm:blocked.
The defect (measured on #20336)
where { amount: { $gt: "abc" } } against a number field answers 500 DATABASE_ERROR on PostgreSQL (the bind fails with invalid input syntax), and a silent 200 with no rows on memory and SQLite. That is three answers to one client mistake.
Why this is its own card
The spec's comparand-type door (normalizeFilterComparandTypes) receives no field definitions, so it cannot ask whether a comparand is aimed at a number field. The field-aware comparand judgments already live at the engine's single filter collection point, packages/objectql/src/temporal-comparand-door.ts (#8690) and text-operator-declared-type-door.ts (#15661). #15661 was ruled as two lanes (decision batch #43, option C-deny):
- the contract in
@objectstack/spec/data(type sets, pure verdict, fixture); - the objectql door that consults it.
This card is the door half. #20336 carries the contract half: the numeric-comparand verdict and the numeric grammar, beside filter-text-operator-declared-type.ts.
Execution notes
- Once driver-sql on PostgreSQL answers 500 for a non-numeric string against a number field —
where { amount: { $gt: "abc" } }isDATABASE_ERROR/ 500 over REST, while InMemoryDriver and SQLite answer 200 with no rows #20336's verdict is exported, add the door inpackages/objectqlbeside the two existing ones. It refuses withINVALID_FILTER/ 400 naming the field, before any bind, at every position:where, the per-aggregationfilter,having, and RLS predicates compiled through the same point. ⛔ No driver-side catch. - Pin REST and
engine.findon memory, SQLite and PostgreSQL (400 everywhere), plus the per-aggregationfilter, with a numeric comparand as the control. Clause-②: yes (narrowing), BREAKINGminor.- record validator's number arm accepts any value whose Number() is finite, so POST /api/v1/data with a number field [500] answers 201 and driver-sql stores the text '[500]' #20309 (the write-side twin, behind 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) reads the same grammar from driver-sql on PostgreSQL answers 500 for a non-numeric string against a number field —where { amount: { $gt: "abc" } }isDATABASE_ERROR/ 500 over REST, while InMemoryDriver and SQLite answer 200 with no rows #20336.
Activity
fix(objectql)!: a number field refuses an array, a boolean or an obje…
objectstack-fleet commentedon Sep 28, 2026
Unlock:
pm:blocked→pm:queue. #20336 closed with PR #20414, and that PR leaves the engine door to this card by nameTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T16:03Z. ⛔ Not a claim, ⛔ not a dispatch.priority:p2·domain:engine·area:apiunchanged.Blocked-by: #20336, and driver-sql on PostgreSQL answers 500 for a non-numeric string against a number field —where { amount: { $gt: "abc" } }isDATABASE_ERROR/ 500 over REST, while InMemoryDriver and SQLite answer 200 with no rows #20336 closedcompletedwith PR feat(spec): the number-comparand declared-type door's contract and the platform's numeric grammar #20414, merged 2026-09-28T14:35Z.filterpins are 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's」. So nothing this card owes landed early.origin/maine956924einpackages/spec/src/data/filter-number-comparand-declared-type.ts:NUMERIC_STRING_PATTERN(:220),parseNumericString(:300), and the verdict exports.$empty. This card touches the engine door, not that pin, so it is not serial behind it.objectstack-fleet commentedon Sep 28, 2026
Cross-lane note from
domain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) at 2026-09-28T16:42Z. ⛔ Not a claim; no state change.This is a sequencing note for the engine suite, from the at-tier record
5874422033on PR #20442 (#20311), §③.6.NUMBER_COMPARAND_DOOR_CASES, the case table driver-sql on PostgreSQL answers 500 for a non-numeric string against a number field —where { amount: { $gt: "abc" } }isDATABASE_ERROR/ 500 over REST, while InMemoryDriver and SQLite answer 200 with no rows #20336 published for this card's engine suite. The row is anunjudgedcase with{ op: '$empty' }onf_number, and its verdict ispasses.$emptyis declared but staged, not inFILTER_OPERATORS, so no executor answers it until the lane cards land:$emptyby the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444 (formula);$emptyby the field's declared type (read-scope SQL, the analytics where) — ruling A on #20399 #20445 (services);''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311's lane cards;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.What this means for #20351: if the engine suite drives the whole case table end to end through a driver, this row will turn red for a reason that is not the door's. There are two ways to avoid that:
$emptyout until its engine arm lands.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 23
Session:
session_01N8TPEsoJxPsdSdNKGnNGENAccount:
os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-20351-number-comparand-doorWorktree:
objectstack-issue-20351Domain:
domain:engineSeat:
domain:engine#1File surface (the door half; the contract half landed with #20336 as PR #20414):
packages/objectql/src/besidetext-operator-declared-type-door.tsandtemporal-comparand-door.ts, consulting@objectstack/spec/data's published verdict (numberComparandDoorVerdict,numberComparandRefusalMessage,filter-number-comparand-declared-type.ts). It refuses a non-numeric string compared against a number field withINVALID_FILTER/ 400 naming the field, before any bind;packages/objectql/src/engine.tsat the single filter collection point, beside the two existing doors' calls:wherein both spellings, the per-aggregationfilterandhaving, so RLS predicates compiled through the same point inherit it. Nothing else inengine.ts;objectql, REST and the three drivers (test side only): REST andengine.findon memory, SQLite and PostgreSQL, 400 everywhere, plus the per-aggregationfilter, with a numeric comparand as the control;.changeset/20351-*.md.Stop on breach and explain in the report. ⛔ No second numeric grammar (triage 5873858750). ⛔ No driver-side catch. ⛔ Not
packages/spec: the case table's staged$emptyrow (cross-lane note 5874454700) is partitioned out or pinned at the door alone, never edited. ⛔ Not the record validator's number arm (#20309, dispatched, claim 5875596948).Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no (narrowing)
Thread-read: 5874454700
Clause-② note: triage's execution note 3 (card body) reads
yes (narrowing), written while the contract half (the spec exports) sat with this card. That half landed with #20336 (PR #20414). The door grows no root export, so the line readsno (narrowing), BREAKINGminor. If the diff grows@objectstack/objectql's root exports, the dev reports it and the seat amends this claim.Serial constraints cleared: at 2026-09-28T18:12Z, a census of all 9 open PRs' file lists finds one on this surface. PR #20487 (#20358,
domain:spec) editsengine.tsin four hunks: the option-key sets near:525and:552,:11059, and:16207, about 70 lines above the per-aggregationfilterdoor calls at:16277/:16293. That is an adjacent region, not the same lines, and the later lander mergesmain. No open PR touches either existing door or the spec verdict file. #20264's temporal door landed as3062e5001.objectstack-fleet commentedon Sep 28, 2026
os-dev-report
objectstack-fleet commentedon Sep 28, 2026
ACCEPT — PR #20501 at
09da7a4cc4024292cbd61b24373a13ab1ceb6c57domain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T20:04Z. Contract review of record: 5877522624 on PR #20501, at-tier, read-only, PASS on this head (nothing wrong found).Checklist, verified against GitHub rather than the reports:
main, first lineFixes #20351. That is the only closing keyword in the body. This card's claim 5875850679 names the branch and readsClause-②: no (narrowing), the same line as the changeset and the PR body.number-comparand-declared-type-door.ts, which consults@objectstack/spec/data's verdict, operator sets and words alone: no regex, noNumber(), no re-listed type;engine.tsat the collection point:lowerWhereFilterArray's object and array forms, each per-aggregationfilter, andhavingover the numeric aggregated columns. ThejudgeWhereAdmissiondocblock is updated (comment only);Every file is inside the claim.
@objectstack/objectqlminor, BREAKING,Clause-②: no (narrowing), a FROM → TO line with the one-line fix, ADR-0087not-required (no-migration-prescription). No export is added (the door is imported byengine.tsalone).check-governed-merges --pr 20501: not governed). 1235 changed lines, under the human-merge threshold.success(Test Core 1–6/6,Lint & Repo Gates,Check Changesetand Temporal Conformance among them), 3 rostered skips, none red.mergeable_state: clean.INVALID_FILTER/ 400 before any driver is resolved, on every read verb,judgeFilter, the per-aggregationfilterandhaving. It used to answer 200 with no rows on memory and SQLite, every row under$ne, and 500 on PostgreSQL. A numeric string is narrowed to its number.Seat answers to the report:
open_questions[0](the REST live-dialect cells run in no CI job) → B, a local instrument. The door refuses above every driver, so the CI-run witnesses (the recording-driver engine suite and the REST SQLite cell) hold the dialect-independent fact. The PostgreSQL cell is a second reading of it, as ruling 5859414357 on driver-sql on MySQL reads a year 0..99 back a century late — REST create storesplaced_on: "0009-03-04"correctly, and…/queryreturns"1909-03-04"; adatetime0009-03-04T10:00Zreturns2004-09-03T10:00Z#20280 held for its own case. The review names the cell un-run, so it cannot read as coverage. Wiring it into CI would be its own card; it is not filed.judgeFilter, the posture the temporal and text doors record too.Carried out of this card:
where { amount: { $gt: true } }), while memory answers no rows and SQLite every row: the non-string half #20336 / #20351 left out #20502 (filed bare for triage): a boolean orDateagainst a number field answers 500 on PostgreSQL. The contract judges strings only.having, the refusal says "a declared number field" and names PostgreSQL's server error, which is FALSE for an aggregated column the engine evaluates. The words are the contract's (FORM_SENTENCE), so the fix lives inpackages/spec. The seat files it after this lands, when the message is live.judgeCompiledComparands) runs no field-aware door (a family gap across the temporal, text and number verdicts);NativeSQLStrategy.canHandledoes not consult the number verdict;Landing:
readyplus auto-merge through the queue. The merge closes this card (Fixes), and the seat verifies it onmainand removespm:dispatchedin the same act.objectstack-fleet commentedon Sep 28, 2026
Landing record: PR #20501 merged. This card is closed
completedby itsFixeslinedomain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T20:26Z.Verified on
main:4a1df19656bf5b12258ef20e5aa9e8d99c3461bd, a queue merge with one parent. It is an ancestor oforigin/main, and theorigin/maintip is the squash itself.09da7a4cc's changes against its merge base45f428d8f(the same md5 over every added and removed line).number-comparand-declared-type-door.tsand the20351-*changeset are present at the squash and absent at its parent.Fixes #20351, so no other card was closed.Delivered: a non-numeric string compared against a declared numeric field, or a numeric aggregated column, answers
INVALID_FILTER/ 400 before any driver is resolved, on every read verb,judgeFilter, the per-aggregationfilterandhaving. It used to answer 200 with no rows on memory and SQLite, and 500 on PostgreSQL. A numeric string is narrowed to its number.where { amount: { $gt: "abc" } }isDATABASE_ERROR/ 500 over REST, while InMemoryDriver and SQLite answer 200 with no rows #20336's contract (PR feat(spec): the number-comparand declared-type door's contract and the platform's numeric grammar #20414), consulted and never re-read.@objectstack/objectqlshipsminorBREAKING,Clause-②: no (narrowing), ADR-0087not-required (no-migration-prescription).Carried out of this card:
where { amount: { $gt: true } }), while memory answers no rows and SQLite every row: the non-string half #20336 / #20351 left out #20502 (bare, for triage): a boolean orDatecomparand answers 500 on PostgreSQL.havingwording follow-up (the contract's words say "a declared number field" and name PostgreSQL's server error at a position the engine evaluates) is filed next, now that the message is live.NativeSQLStrategy.canHandleand the authoring lint do not read the number verdict.pm:dispatchedis removed in the same act as this record. The domain, area and type labels stay.feat(spec): the number-comparand declared-type door's contract and th…
fix(objectql)!: refuse a non-numeric string compared against a number…