Repository navigation
Commit be531f6
feat(spec,core,objectql,driver-sql,formula,driver-mongodb,service-analytics): now-anchored filter comparand
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbFpVLroZXBfHvGDxkKmgWwk)_
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:
```ts
// opened more than review_sla_days_offset days ago — the column holds NEGATIVE days
{ opened_at: { $lt: { $now: true, addDays: { $field: 'review_sla_days_offset' } } } }
// closed on or after thirty days ago
{ closed_on: { $gte: { $now: true, addDays: -30 } } }
```
`{ $now: true, addDays: <integer | { $field }> }` is legal exactly where
a `{ $field }` reference is: the whole comparand of `$eq` / `$ne` /
`$gt` / `$gte` / `$lt` / `$lte`, on a `date` or `datetime` column.
`addDays` is REQUIRED (the unshifted instant stays the `{now}`
placeholder); its grammar is `FieldReferenceSchema.addDays`' — an
integer of any sign or a `{ $field }` to a numeric column of the same
record. There is no `subDays`: **"now minus the SLA" needs a column
holding negative days**, the sign rule the ruling names ("sign rules as
`addDays`").
## Work items 1–5 of the ruling
1. **Spec shape + authorable surface** — `NowAnchorSchema` in
`data/filter.zod.ts`, in the ordering and equality comparand unions
beside `FieldReferenceSchema`; refused by name as an `$in` / `$nin`
member and a `$between` endpoint; an equality triple with an anchored
comparand lowers to the explicit `$eq` (as #7597 did for a reference).
The `FilterCondition` door (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 and `content/docs/references/data/filter.mdx` regenerated.
2. **Engine resolution + class rule** — `resolveFilterTokens`
(`@objectstack/core`, where the resolver lives) pins `$now` to the
tree's one instant as an ISO string, so every executor sees `{ $now:
'<ISO>', addDays }`; an anchor without `addDays` or with a malformed
offset is `INVALID_FILTER` / 400 there. The temporal-comparand door
steps around the anchor as it steps around a reference.
`having-filter.ts` judges the anchor on `aggregations[i].filter`
(operands withheld, diagnostic to the log — `where`'s posture) and on
`having` (named — the author's own query): no base column, the target
`date` / `datetime`, the offset declared and numeric, never the tenant
column (on SQL), and compares it per row through the formula evaluator.
No `engine.ts` edit was needed: the judges it calls are the ones that
grew.
3. **driver-sql on all three dialects** — `applyNowAnchorComparison`:
the instant bound in the target column's storage form (`datetime` keeps
the time of day — canonical ISO, `mysqlDatetimeLiteral` on MySQL,
`cast(? as timestamptz)` / `cast(? as datetime(3))`; `date` meets the
instant's UTC calendar day, `cast(? as date)`; SQLite reads the
canonical text directly), shifted through the existing
`crossFieldOffsetExpr` — so **NULL offset = 0 days through the same
`coalesce`**, 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 `$not` takes 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 (#7929) and the diagnostic
in the server log.
4. **Formula + one driver-mongodb cell** — `matchesFilterCondition`
resolves the anchor in the form of the stored value it meets (a bare
calendar day stays a calendar day), `$now: true` reads 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.
5. **Conformance, lint, docs** — `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) and `CROSS_FIELD_NOW_REFUSALS`
(14) on the offset fixture, driven on both paths by driver-sql (sqlite +
live PostgreSQL locally; MySQL is the `Temporal Conformance` battery on
`main`), 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.mdx`
gains "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 (`findCrossFieldComparand` detects `$now` too), 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 `$eq` for an anchored comparand. A
dataset's authored `$now: true` is pinned by the service's own token
seam (`resolvedDatasetScopeGetter`).
## Acceptance notes
- **Sign caveat.** The ruling's title says "now minus an SLA"; the ruled
shape has no subtraction. A column that means "days before now" must
hold negative values (`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.
- **Related record.** The ruling names the SLA "held on a related
record"; SQL push-down compiles same-table columns only (2026-08-06), so
a dotted offset validates, resolves in memory and is refused by SQL
push-down by name — the same asymmetry the `addDays` pair has, pinned on
both faces. The shape that answers everywhere is the SLA propagated onto
the row.
- **driver-memory** (not on the claim's surface): measured with a probe
— it answers `[]` 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-report` comment on #25808 for the test and gate ledger.
_Generated by [Claude
Code](https://claude.ai/code/session_019YLF9PyrRqUX4N7C2pzSoH)_
Co-authored-by: Claude <noreply@anthropic.com>{ $now: true, addDays } shifted by days held on the record (#26261)1 parent 2409c9c commit be531f6
35 files changed
Lines changed: 2587 additions & 95 deletions
File tree
- .changeset
- content/docs
- protocol/objectql
- references
- data
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- core/src/utils
- drivers
- driver-mongodb/src
- driver-sqlite-wasm/src
- driver-sql/src
- formula/src
- lint/src
- objectql/src
- services/service-analytics/src
- __tests__
- strategies
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- src
- data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
526 | 526 | | |
527 | 527 | | |
528 | 528 | | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
529 | 590 | | |
530 | 591 | | |
531 | 592 | | |
| |||
0 commit comments