Repository navigation
Description
Measured while implementing #5222 (cross-field push-down). Filing unassigned — recording, not claiming.
The fact
parseFilterAST lowers an authored triple differently depending on the operator spelling. Measured on current main:
| Authored | Lowered to |
|---|---|
[['amount', '>', { $field: 'budget' }]] |
{"amount":{"$gt":{"$field":"budget"}}} |
[['amount', 'gt', { $field: 'budget' }]] |
{"amount":{"$gt":{"$field":"budget"}}} |
[['amount', '=', { $field: 'budget' }]] |
{"amount":{"$field":"budget"}} ← bare |
[['amount', 'equals', { $field: 'budget' }]] |
{"amount":{"$field":"budget"}} ← bare |
The equality spellings drop the operator, because a literal comparand's implicit-equality form is { field: value } — correct for a literal, but for a field reference it produces a field spec whose only key is $field.
Nothing reads that shape as an equality. In matches-filter.ts, evalField sees an all-$ key set and dispatches $field to evalOp, which has no arm for it and returns its fail-closed false. So the filter silently matches no record. On the SQL side the same shape arrives as an OPERATOR named $field.
Why it matters
Two authored spellings of one intent, two fates: ['amount', '>', ref] works (post-#5222 on SQL, and always in memory), ['amount', '=', ref] silently matches nothing. The failure is invisible on the path that produces it.
#5222 narrowed the SQL side to give this shape an actionable refusal naming $eq rather than a generic operator list — but that is a driver-side mitigation of a lowering defect, and it deliberately did NOT compile the shape, because compiling it would make SQL answer rows for a filter the memory evaluator answers false for (a new divergence in the change that closed one).
The decision this needs
Most likely: parseFilterAST should lower an equality triple whose comparand is a FieldReferenceSchema to { field: { $eq: ref } } rather than the bare form — the spelling that IS implemented on both paths. That is a one-sink change (parseFilterAST is the single lowering sink per #5158), but it is a semantics change at the authoring face, so it wants a ruling rather than a drive-by fix.
Alternatively, matches-filter.ts could read a bare { $field } field spec as an implicit equality — but that changes the evaluator's unknown-operator posture, which #6520 examined and deliberately KEPT.
Refs
- [spec] SqlDriver 将
$field编译为列对列比较(cross-field comparison push-down) #5222 (the driver-side refusal + its wording) - [engine] driver-sql 编译 spec 未声明的「数组 where 方言」—— 与 Turso remote 的拒收分叉,需一次定调(接纳进 spec 或响亮弃用) #5158 (
FilterArrayis input-only sugar;parseFilterASTis the single sink) - JS 求值面全体拒收
$icontains(driver-memory 两面 / driver-mongodb / objectqlhaving/ formula)—— SQL 族已实现,同一 filter 在内存 double 上抛错 #6520 (the evaluator's unknown-operator posture, kept deliberately)
Activity
os-zhuang commentedon Aug 11, 2026
Triage:
needs-user-decisionappended;domain:spec(filer-applied) confirmed correct.FieldReferenceSchemacomparand to{ field: { $eq: ref } }"is a semantics change at the authoring face, so it wants a ruling rather than a drive-by fix" — and the alternative (reading bare{ $field }as implicit equality) reverses the evaluator posture JS 求值面全体拒收$icontains(driver-memory 两面 / driver-mongodb / objectqlhaving/ formula)—— SQL 族已实现,同一 filter 在内存 double 上抛错 #6520 deliberately kept. Both branches move the authoring-face semantics ofpackages/spec, sodomain:specper standing instruction ③'s reverse red line.origin/main@744b8f5:parseFilterASTlives atpackages/spec/src/data/filter.zod.ts:1205(single lowering sink per [engine] driver-sql 编译 spec 未声明的「数组 where 方言」—— 与 Turso remote 的拒收分叉,需一次定调(接纳进 spec 或响亮弃用) #5158 holds — no second definition found).FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 (declared-but-unresolved list positions) and [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 (analytics faces) — this one is the lowering sink itself. No existing card names the bare-$fieldequality lowering; no open PR touches it.$fieldcapability whose SQL half ([spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222) is still in flight and now refuses this shape loudly with an actionable message; not judged RC-blocking on its own — the ruling can ride the [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 follow-up window.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Maintainer ruling recorded 2026-08-11 (PM session, executing the maintainer's direct instruction in chat — verbatim: 「接受你的全部建议,请更新 issue 的状态和标签」, accepting the four-lens decision-inbox review in full).
Ruling: fix the lowering.
parseFilterASTlowers an equality triple whose comparand is aFieldReferenceSchemato{ field: { $eq: ref } }— the spelling already implemented on both evaluation paths. ⛔ Do NOT change the memory evaluator's unknown-operator posture; #6520's decision stands. Single-sink change per #5158.Rationale (four-lens): silent zero-row matching is the worst failure shape; the fix is one sink, no new capability surface, and removes a trap AI-authored filters walk straight into.
State:
needs-user-decision→pm:queue(domain:specseat).Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Round-1 selection note (spec lane, session
session_01JY2Q5Xto1u8YHADgrZDTnk): deferred to the NEXT round by the batch cap — round 1 took #7495/#7496/#7513 (fan-out-ordered). Not shelved; this card rides first in round 2, ahead of #7596 (same-file serialization: both editpackages/spec/src/data/filter.zod.ts, and this ruled silent-zero-rows fix outranks the removal).Facts recorded for the dispatching round (all re-verified 09:18Z on
origin/main):parseFilterASTis now at:1415— the triage comment's:1205is stale.fieldsstops prescribing a dotted path no driver resolves (#7601) #7617 (docs prose, in flight, sibling sub-lane) declares a different file face; re-verify at dispatch time.FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596's removal scope (re-pricing rule — both cards share the filter contract face).Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Claim: PM loop round 1 backfill (spec lane — slot freed by #7495's voiding; maintainer instructed 加速派发)
Session:
session_01JY2Q5Xto1u8YHADgrZDTnkBranch:
claude/issue-7597-equality-field-ref-loweringWorktree: cloud dev session (own container)
Domain:
domain:specFile surface:
packages/spec/src/data/filter.zod.ts(parseFilterAST, ~:1415) + its tests + the cross-field conformance corpus (cross-field-conformance-cases.tsregion, additive). ⛔ Not the$between/$in/$ninunions — that region is #7596's, serialized behind this card. Stop on breach; explain in the report.Container & model: M (ruled authoring-face semantics change with cross-path conformance obligations),
mode:cloud, model: opusSerial constraints cleared: #7596 (same file) is explicitly queued BEHIND this card (hot-file serial queue, seat post #6017; defer note on #7596 09:27Z). No in-flight PR touches
filter.zod.ts(checked 09:20Z; #7593 merged this morning — anchors re-verified post-merge). #7617 (in flight, sibling sub-lane) declares a docs-prose face.Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
OS-DEV-REPORT — #7597
os-dev-report
Draft PR: #7672 · Branch:
claude/issue-7597-equality-field-ref-lowering· Base:origin/main@cc3555ePremise verified, then fixed. Measured on
origin/mainbefore the first edit: all four$eqspellings (=,==,equals,eq) lowered['amount', '<op>', { $field: 'budget' }]to the bare{"amount":{"$field":"budget"}}and the in-memory evaluator returned[], while>/gtlowered to{"$gt":ref}and returned["1"]. After the fix all four lower to{"amount":{"$eq":{"$field":"budget"}}}and return["3","6"]— the conformance corpus's declared$eqrow set.parseFilterASTis atfilter.zod.ts:1415as the dispatch said (the triage comment's:1205was stale); confirmed single sink — no second definition.Full spelling set measured, not assumed.
AST_OPERATOR_MAPfolds exactly those four onto$eq, andconvertComparison's implicit-equality branch intercepts exactly those four, so the two lists coincide. A new test sweeps the whole exportedVALID_AST_OPERATORSvocabulary and fails if any spelling lowers a reference comparand to a bare field spec.Compile-surface enumeration (full evidence in the PR body): face 1
driver-sqlalready conformant and now exercised by the new authoring arm; face 2 turso RemoteTransport already conformant (loudINVALID_FILTERbefore and after); face 3read-scope-sqlnot reachable (read scopes are policy-compiled, never through this sink) and owned by #7598; face 5formulais the reference implementation; half-face objectqlhavingout of reach (havingis aFilterConditionobject, never AST-lowered);driver-memory/driver-mongodbout of scope per the #5499 freeze.service-analyticsfilter-normalizer. The equality triple moves there from a loudINVALID_FILTER/400("Unsupported filter operator$field") into the same SILENT bucket the ordering spellings have occupied since #3948:assertCompilableComparandgates only the LIKE family and$in/$ninmembers, so a scalar operator's object comparand falls through totoSqlBindValueand is bound as JSON text. Not a new defect class — one more spelling entering an existing one — and the same shift lands on the frozendriver-memory. Left unfixed deliberately, on the precedent this exact surface set: #7604's triage records that #5222's dev seat declined to widen analytics as a side effect and the PM called that right; refusing only$eqthere would create a fresh asymmetry inside the face, and refusing all six is #7598's open question 1. Measurement posted to #7598.Re-pricing #7596: untouched. The list positions are unchanged by this PR (
in/nin/betweenlowerings pass their comparand through as before), and the two diff hunks start at:1350/:1376, so #7596's unions at:319-320and:819-820are byte-identical and unshifted. It gets marginally cleaner to argue — after this fix the remaining declared-but-unexecutable$fieldpositions are exactly the ones #7596 removes — but neither easier nor harder to do.Region boundary respected: the
$between/$in/$ninunions were not touched.{ "issue": 7597, "status": "done", "branch": "claude/issue-7597-equality-field-ref-lowering", "pr": "https://github.com/objectstack-ai/objectstack/pull/7672", "premise_still_valid": true, "summary": "parseFilterAST's implicit-equality shorthand now branches on the comparand: an equality triple whose comparand is a FieldReferenceSchema lowers to { field: { $eq: ref } } instead of the bare { field: { $field } } that no backend reads as an equality, so ['amount','=',ref] finally behaves like ['amount','>',ref] on both evaluation paths. All four $eq spellings are covered and a vocabulary sweep test keeps a fifth from re-opening it; literal comparands, non-string $field carriers, and the evaluator's #6520 unknown-operator posture are untouched — a hand-authored bare field spec keeps today's fate on every backend. The cross-field conformance corpus gains an AUTHORING arm (CROSS_FIELD_AUTHORED_CASES) that enters through the sink and holds the lowered filter to the same both-paths-same-rows rule, run by driver-sql per dialect cell and by driver-sqlite-wasm. One consequence to weigh: at service-analytics' filter-normalizer (and the frozen driver-memory) the equality spelling shifts from a loud refusal into the silent JSON-bind bucket the ordering spellings already sit in — measured, reported, and left to #7598 rather than widening analytics as a side effect.", "tests": "pnpm --filter @objectstack/spec test => 377 files / 9880 tests passed (includes the new src/data/filter-field-reference-lowering.test.ts). pnpm --filter @objectstack/driver-sql test => 86 passed | 4 skipped (90) files, 1399 passed | 50 skipped tests; the 15 new authoring-arm cases confirmed running by name under -t '#7597'. pnpm --filter @objectstack/driver-sqlite-wasm test => 24 files / 374 tests passed. pnpm --filter @objectstack/formula test => 25 files / 631 tests passed. typecheck (spec, driver-sql, driver-sqlite-wasm) clean. Gates: check:driver-conformance OK (37 covered cells, 3 DEBT, 0 exempt); check:merge-driver, check:adr-anchors, check:spec-parsed-alias OK; check:authorable-surface OK (1276 defaults unchanged, no anchor diff); scripts/check-nul-bytes.mjs OK (7069 files). REVERSE VERIFICATION, direction predicted first: removing the fix via a patch file must turn the AUTHORING arm red and leave the hand-authored bare-form refusal green. Measured exactly that -- 14 authoring cases plus 'the equality TRIPLE no longer lowers to that bare spelling' failed with \"expected { amount: { '$field': 'budget' } } to deeply equal { amount: { '$eq': ... } }\", while the '>' control and the bare-form refusal pin stayed green; restored with git apply. CI status at report time: in_progress (draft PR just opened; PM owns convergence).", "open_questions": [ { "question": "service-analytics' filter-normalizer (face 4) and the frozen driver-memory now answer the equality-with-$field shape SILENTLY (JSON-bound comparand, zero rows) where they previously refused loudly. Fix inside #7598's scope, or does the PM want a narrow refusal landed sooner?", "options": [ "A - leave it to #7598 (queued, domain:services, owns both analytics emitters; its open question 1 is implement-vs-refuse for exactly these faces)", "B - land a narrow $field refusal at the analytics scalar-comparand door in a follow-up card, ahead of #7598's larger decision, so the face stays loud for all six operators" ], "recommendation": "A. Refusing only $eq at that door would create a fresh asymmetry inside the analytics face -- the exact defect class this card closes -- and refusing all six pre-empts #7598's ruling on whether these emitters implement $field instead. The shift is not a new defect class: the ordering spellings have been silent there since #3948, so this is one more spelling joining an existing, already-filed hole. The measurement has been posted to #7598 so its executor inherits it rather than re-deriving it." } ], "out_of_scope_findings": [ "not filed (existing card): the analytics silent-bind residue above belongs to #7598 (#7604 was closed as its duplicate) - measurement posted there as a comment rather than a twin issue", "FieldReferenceSchema's execution-support docblock (packages/spec/src/data/filter.zod.ts, ~:36-64) still says SQL push-down refuses a field reference and that compiling it 'is tracked as its own capability in #5222' - stale since #7582 landed that compilation. Doc-only, adjacent to but not part of this card's change; left untouched, flagged here for the PM to route (a docs-accuracy pass on the same file would also catch it)." ] }Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Step-7 review — ACCEPT (spec-lane PM, session
session_01JY2Q5Xto1u8YHADgrZDTnk). PR: #7672 (draft,Fixes #7597).Verified against GitHub, not the report's claims:
convertComparison) — aFieldReferenceSchemacomparand in the equality shorthand lowers to{ $eq: ref }; literals keep implicit equality (pinned); a non-string$fieldis not a reference (predicate mirrorsdriver-sql'sfieldReferenceOf— good cross-layer consistency); JS 求值面全体拒收$icontains(driver-memory 两面 / driver-mongodb / objectqlhaving/ formula)—— SQL 族已实现,同一 filter 在内存 double 上抛错 #6520's evaluator posture untouched and pinned on the hand-authored bare form.$eqspellings fixed, case folding covered, and a vocabulary sweep overVALID_AST_OPERATORSpins that NO spelling can lower a reference comparand to a bare field spec — a fifth spelling entering the map cannot silently re-open this.havingnever passes the sink — stated with the metadata-protocol quote) and the frozen faces (measured anyway, for the record).service-analyticslowerAnalyticsWhere) shifts loud→silent for the equality spelling — it joins the same silent bucket the ORDERING spellings have always occupied there. Measured before/after, precedent cited ([spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222's decline-to-widen, PM-endorsed), and the measurement posted to [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 whose card owns the analytics$fieldposture wholesale. This is the right scoping call; flagged in the round report so the maintainer sees the trade.FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 answered: untouched — list positions pass through unchanged, the:319-320/:819-820unions are byte-identical and unshifted, and the removal boundary it draws gets cleaner. [spec]FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 dispatches after this merges, per the hot-file queue.>control and the re-authored bare-form refusal stayed green.check:driver-conformance37 cells OK,check:authorable-surfaceno anchor diff, nul-bytes clean. Changeset spec minor + driver-sql minor, justified with the Filter AST:likeis folded to$containsat the wire — wildcards bind as literals and driver-sql'slike/ilikearm is unreachable #7536 precedent.Landing plan: flip to ready + auto-merge on gate-job conclusions (flip check armed); the merge queue serializes it against #7655/#7657 mechanically.
Generated by Claude Code
fix(spec): lower equality triples with a $field comparand to {$eq: re…
feat(service-analytics): serve a `$field` RLS rule by declining nativ…
test(spec): the third data/ file group's test titles state each cited…