Repository navigation
feat(spec,core,objectql,driver-sql,formula,driver-mongodb,service-analytics): now-anchored filter comparand { $now: true, addDays } shifted by days held on the record - #26261
Conversation
…lytics): now-anchored filter comparand shifted by days held on the record
A filter comparand `{ $now: true, addDays: <integer | { $field }> }`, the
maintainer's letter A on #25808 (ruling comment 6415710193): legal exactly
where a `{ $field }` reference is, as the whole comparand of $eq / $ne /
$gt / $gte / $lt / $lte on a date or datetime column. `addDays` is required
and shares `FieldReferenceSchema`'s grammar (there is no subDays; "now minus
the SLA" is a column holding negative days). The unshifted instant stays
the `{now}` placeholder.
- spec: `NowAnchorSchema` in the ordering and equality comparand unions;
refused by name as a set member or `$between` endpoint; the comparand
type door and the lowering face step around a well-formed anchor and
refuse a malformed one in its own words; authorable surface, json-schema
manifest, api surface and the filter reference page regenerated.
- core: `resolveFilterTokens` pins `$now` to the tree's one instant as an
ISO string; a missing or malformed addDays is INVALID_FILTER / 400.
- objectql: the temporal door skips the anchor; `aggregations[i].filter`
and `having` judge it by `where`'s rules and compare per row through the
formula evaluator.
- driver-sql: same-table push-down on SQLite, PostgreSQL and MySQL through
`crossFieldOffsetExpr` (NULL offset = 0 days), the instant bound in the
target column's storage form; NULL-total predicates so `$not` is the
exact complement; every other position refused with operands withheld.
- formula: `matchesFilterCondition` resolves the anchor in the stored
value's form, so the memory path answers the SQL path's rows.
- driver-mongodb: refuses the anchor in every position (INVALID_FILTER).
- service-analytics: NativeSQLStrategy declines the anchor as it declines a
`{ $field }` comparand, so dataset filters route to the engine path.
Pinned on the drivers' cross-field conformance corpus (CROSS_FIELD_NOW_CASES
/ CROSS_FIELD_NOW_REFUSALS) on both execution paths and on the analytics
dataset shape; changeset minor on @objectstack/spec.
Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6415710193
Claude-Session: https://claude.ai/code/session_019YLF9PyrRqUX4N7C2pzSoH
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 7 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 152 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b4750ab39c8350d740d2456bbd3563246258d0ed && git checkout b4750ab39c8350d740d2456bbd3563246258d0ed
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3d50a43bcb55053e0501d939b304242b41cfde19 9594f3737bf2cf0f4e2dd815372fe13d85e724bd && git checkout -B drift-repro 3d50a43bcb55053e0501d939b304242b41cfde19 && git merge --no-ff 9594f3737bf2cf0f4e2dd815372fe13d85e724bd
node scripts/docs-audit/affected-docs.mjs --json 3d50a43bcb55053e0501d939b304242b41cfde19
|
Contract reviewServed-tier: Inputs read: card #25808 (body and all eight comments, ruling 6415710193 letter A, claim 6420309633, premise check 6420234055, os-dev-report 6423037096), PR #26261 (body, 35-file list, net diff against main at merge base ① Derived judgments
② Semver level
Clause-②: yes (widening) ③ Boundary flagsThe dev's os-dev-report (6423037096) lists four deviations, two out-of-scope findings and an empty
Gate verdicts on the head (48 check-runs, all completed): every one of the eight required contexts is Implemented-by: VERDICT: PASS |
席位核对ACCEPT · domain:spec seat 2 · 2026-10-26T18:08Z · head
|
Requested by Steedos · project thread
Fixes #25808
Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6415710193
What
A filter comparand anchored at the current instant and shifted by whole days read off the record — the maintainer's letter A on #25808, comment 6415710193:
{ $now: true, addDays: <integer | { $field }> }is legal exactly where a{ $field }reference is: the whole comparand of$eq/$ne/$gt/$gte/$lt/$lte, on adateordatetimecolumn.addDaysis REQUIRED (the unshifted instant stays the{now}placeholder); its grammar isFieldReferenceSchema.addDays' — an integer of any sign or a{ $field }to a numeric column of the same record. There is nosubDays: "now minus the SLA" needs a column holding negative days, the sign rule the ruling names ("sign rules asaddDays").Work items 1–5 of the ruling
NowAnchorSchemaindata/filter.zod.ts, in the ordering and equality comparand unions besideFieldReferenceSchema; refused by name as an$in/$ninmember and a$betweenendpoint; an equality triple with an anchored comparand lowers to the explicit$eq(as [spec]parseFilterASTlowers['a', '=', { $field: 'b' }]to a bare field spec no backend reads as equality #7597 did for a reference). TheFilterConditiondoor (comparand-TYPE walk,filter-comparand-type.ts) steps around a well-formed anchor and refuses a malformed one in the anchor's words; the lowering face (filter-lowering.ts) treats an anchored comparison as NULL-total.authorable-surface/data.json,json-schema.manifest/data.json, the api surface andcontent/docs/references/data/filter.mdxregenerated.resolveFilterTokens(@objectstack/core, where the resolver lives) pins$nowto the tree's one instant as an ISO string, so every executor sees{ $now: '<ISO>', addDays }; an anchor withoutaddDaysor with a malformed offset isINVALID_FILTER/ 400 there. The temporal-comparand door steps around the anchor as it steps around a reference.having-filter.tsjudges the anchor onaggregations[i].filter(operands withheld, diagnostic to the log —where's posture) and onhaving(named — the author's own query): no base column, the targetdate/datetime, the offset declared and numeric, never the tenant column (on SQL), and compares it per row through the formula evaluator. Noengine.tsedit was needed: the judges it calls are the ones that grew.applyNowAnchorComparison: the instant bound in the target column's storage form (datetimekeeps the time of day — canonical ISO,mysqlDatetimeLiteralon MySQL,cast(? as timestamptz)/cast(? as datetime(3));datemeets the instant's UTC calendar day,cast(? as date); SQLite reads the canonical text directly), shifted through the existingcrossFieldOffsetExpr— so NULL offset = 0 days through the samecoalesce, pinned and documented. Predicate total:(A is not null and A op Bx)for the orderings and$eq,(A is null or A <> Bx)for$ne, so$nottakes exactly the complement. Offset column under the 2026-08-06 rules; non-temporal target, malformed anchor and every other position refused with the operands withheld (finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929) and the diagnostic in the server log.matchesFilterConditionresolves the anchor in the form of the stored value it meets (a bare calendar day stays a calendar day),$now: truereads the evaluator's clock for a caller with no resolver seam; MongoDB refuses the anchor in every position (INVALID_FILTER/ 400), one pinned cell per position.CROSS_FIELD_NOW_CASES(12 expectations × both temporal pairs + 4 per-class cells with a NOW off the fixture's time of day — 28 cases) andCROSS_FIELD_NOW_REFUSALS(14) on the offset fixture, driven on both paths by driver-sql (sqlite + live PostgreSQL locally; MySQL is theTemporal Conformancebattery onmain), driver-sqlite-wasm, service-analytics (where + RLS read scope + dataset measure filter + dataset filter, authored$now: true), plus formula, objectql engine (where / aggregation filter / having), spec, core and lint cells.content/docs/protocol/objectql/query-syntax.mdxgains "Comparing Against Now, Shifted by Days Held on the Record" after "Comparing Two Fields".Item 6 (hotcrm's dataset authoring) is a hotcrm card, not this PR.
NativeSQLStrategy
Declines a query carrying an anchor as it declines a
{ $field }comparand (findCrossFieldComparanddetects$nowtoo), so a dataset filter or measure filter routes to the engine path where the driver compiles it under the 2026-08-12 ruling; the ObjectQL strategy's equality lowering emits the explicit$eqfor an anchored comparand. A dataset's authored$now: trueis pinned by the service's own token seam (resolvedDatasetScopeGetter).Acceptance notes
review_sla_days_offset: -14). The docs section and the schema description say so; a positive SLA column needs a negated sibling column or a formula field.addDayspair has, pinned on both faces. The shape that answers everywhere is the SLA propagated onto the row.[]for a{ $field }comparand today and answers the same[]for an anchor; that silent hole is pre-existing and reported as out of scope.Verification
See the
os-dev-reportcomment on #25808 for the test and gate ledger.Generated by Claude Code