fix(driver-memory): a cube $lte on a bare day keeps the whole day on a declared datetime field (#20661) - #20714
Conversation
…ore storage form (#20661) The mingo and SQL-echo lte rows asked nextUtcCalendarDay about the comparand already in storage form, which on a declared datetime field is the midnight instant the helper refuses to widen. Both rows now render one lteUpperBound decision taken from the authored value, and only the widened bound is converted (ADR-0053 D-E3). Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…nd declared datetime (#20661) Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
#20661) Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 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 199fc9bab269f329a34294f9f11a6a1fb664700d && git checkout 199fc9bab269f329a34294f9f11a6a1fb664700d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57574637129bebb4a6868c465be7e1b80eed1954 8cae2ae3980972016b27ab33b3c6f59cf3197f8e && git checkout -B drift-repro 57574637129bebb4a6868c465be7e1b80eed1954 && git merge --no-ff 8cae2ae3980972016b27ab33b3c6f59cf3197f8e
node scripts/docs-audit/affected-docs.mjs --json 57574637129bebb4a6868c465be7e1b80eed1954
|
Contract reviewServed-tier: Inputs: card #20661 (body; triage 5893953164; claim 5897541483; os-dev-report 5898224404), PR #20714 (body, file list, net diff against ① Derived judgmentsFiles:
② Semver level
Clause-②: Shipped prose, sentence by sentence against the diff and
③ Boundary flags
Check-runs on Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20661
Clause-②: no
The memory analytics (cube) face now applies the whole-day rule for a bare-day
$lteBEFORE it converts the bound to the field's storage form, as ADR-0053 D-E3 orders. On a declareddatetimefield,where { created_at: { $lte: '2026-07-28' } }now answers the same rows asfind(), and the SQL echo shows the bound that runs.The change
packages/drivers/driver-memory/src/memory-analytics.ts:lteUpperBound(authored, comparand, storageForm). It asksnextUtcCalendarDayabout the AUTHORED value and returns one of three decisions:before(a bare day: the next day's midnight, converted to storage form, exclusive),unbounded(9999-12-31, which has no next day), orthrough(anything else: the authored comparand in storage form, inclusive).lterows, the mingo$matchrow (CUBE_OPERATOR_TO_MONGO_PREDICATE) and the SQL echo row (CUBE_OPERATOR_TO_SQL_PREDICATE), now only render that one decision. They used to callnextUtcCalendarDay(comparands[0]), andcomparandsis already in storage form. On a declareddatetimefield that value is the instant2026-07-28T00:00:00.000Z, which the helper correctly refuses to widen.comparandsForbecamestorageFormFor(cube, member), which returns the per-member conversion (stilldriver.filterComparandStorageForm). Each exit maps its comparands through it and passes the same function to its builder asstorageForm, for the one bound a builder derives instead of receiving.comparandsis still computed for every operator, so every conversion (and any refusal it raises) happens exactly as before.nextUtcCalendarDay(packages/speccalendar-day.ts) is untouched. The driver's own filter path (memory-driver.ts) was already in this order (nextUtcCalendarDay(val), thenstore(nextDay)) and is untouched.The landing site is the one the claim expected. The producer is the emitter, not
filterComparandStorageForm: ADR-0053 D-D1 keeps the storage-form conversion operator-blind and puts the operator-sensitive rewrite on the emitters.Before and after, measured at the published export
Built
@objectstack/driver-memorydist/index.mjs(MemoryAnalyticsService,InMemoryDriver). Before =origin/mainatfa0a4b6610(this branch's base;e4e5222b7plus one commit that does not touch this package). After = this branch. Rowsr28created_at: 2026-07-28T10:00:00.000Zandr272026-07-27T10:00:00.000Z. "Declared" meanssyncSchemawithcreated_at: datetimeandmade_on: date.wherefind()query()beforequery()aftercreated_atundeclared$lte '2026-07-28'created_at < '2026-07-29'created_atdeclareddatetime$lte '2026-07-28'created_at <= '2026-07-28T00:00:00.000Z'created_at < '2026-07-29T00:00:00.000Z'made_ondeclareddate(control)$lte '2026-07-28'made_on < '2026-07-29'created_atdeclareddatetime$lteaDateat2026-07-28T00:00Z(control)created_at <= '2026-07-28T00:00:00.000Z'created_atdeclareddatetime$lte '9999-12-31', rowsc262026-07-15T14:00Z,mid9999-12-31T10:00Z,nonecreated_at <= '9999-12-31T00:00:00.000Z'created_at IS NOT NULLcreated_atundeclared$lte '9999-12-31', same rowscreated_at IS NOT NULLIn the last-day rows,
nonewas written withcreated_at: null, and the driver'screatestamps itscreated_atdefault in its place, so that row has a value. The pinned test uses no such row.Dispatch hypotheses
fa0a4b6610, bothlterows callednextUtcCalendarDay(comparands[0])(:273,:468).comparandscame fromcomparandsFor(:1555, called at:886and:1367), and the builder context also carriedraw. The fix widens fromraw[0]and converts only the widened bound, through the same conversion the comparands take. It is one function that both rows render, so the two exits cannot drift.$betweenis not inMONGO_TO_CUBE_OPERATOR. Measured on both exits with a declareddatetimefield and a bare-day maximum:INVALID_FILTER/ 400 ("declared by the Filter Protocol but cannot be compiled by driver-memory's analytics (cube) face"). No row added. The new test pins that refusal on the date shape, so widening the table to take$betweenturns it red and points atlteUpperBound.timeDimensions[].dateRangeend widens the rawendstring before it builds the string andDatebounds (:1005), so it already follows D-E3. On a declareddatetimefield,['2026-07-01', '2026-07-28']answers r27, r28 before and after, the same asfind()with$gte/$lte. Pinned, not changed.datefield the storage form of a bare day is that bare day, and the answer and echo are byte-identical before and after (table above). Pinned.isUnboundedAbovepath still answers "has a value". Its existing pins inmemory-driver-calendar-day-upper-bound.test.ts(undeclared field) are green. On a DECLAREDdatetimefield the last day was also broken by this card's order (<= '9999-12-31T00:00:00.000Z', droppingmid). It now answersIS NOT NULL/$ne: nulland is pinned.Verification
Tests: new
packages/drivers/driver-memory/src/memory-analytics-20661-lte-whole-day-first.test.ts, 7 cases. Every case compares the cube's rows withfind()and with a literal id list, and checks the echo'sWHERE:the card's pair, undeclared and declared
datetime;a full-timestamp
$lte(stays inclusive, in storage form);the declared
datecontrol;the declared-
datetimelast day and the day before;the
dateRangesibling;the
$betweenrefusal on both exits, asserting the envelopecodeandstatus.pnpm --filter @objectstack/driver-memory test: 63 files, 1456 tests passed.pnpm --filter @objectstack/driver-memory typecheck(tsc --noEmit && tsc --noEmit -p tsconfig.typecheck.json): exit 0.--listFileson the first program lists the new test file.Reverse verification. The fix was committed first (HEAD
cb155edf6c). The subject is loaded fromsrcthrough the test's relative import, so nodistis involved. The mutation went throughscripts/ablation-replace.mjsunder atraprestoringHEAD: the anchornextUtcCalendarDay(authored)was replaced withnextUtcCalendarDay(comparand), which is the old order. On disk the anchor went 1 to 0, the replacement 0 to 1, and the blob014497594cc6becamea41c37fac411. Run over the new file andmemory-driver-calendar-day-upper-bound.test.ts: 2 failed, 15 passed. The two failures were the declared-datetimepair (expected [ 'r27' ] to deeply equal [ 'r27', 'r28' ]) and the declared last day (expected [ 'c26', 'prev' ] …). The undeclared,date, instant,dateRange,$betweenand #20600 cases stayed green, which is the expected direction. Restore: the blob equals theHEADblob014497594cc6andgit diff HEADis empty.Gates, at
8cae2ae398:node scripts/pm/dispatch-gates.mjs --commands: 60 commands, all exit 0.check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debtfirst exited 3 (PREREQUISITE NOT MET, no built closure). They were re-run green afterpnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'(71 of 71 tasks).--ranreconciliation: "60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN".Lint, narrowed and not the repo-wide run:
eslint --no-inline-config --format jsonover the 2 touched.tsfiles: 2 files, 0 errors, 0 warnings.--print-configresolves a config for each, with 6 and 5 rules.eslint.config.mjsenables no type-aware linting (noparserOptions.project; its own header says so at:327), so this diff cannot move the verdict for any untouched file.The
Clause-②: noline above is the claim's.Acceptance notes
MemoryAnalyticsService:git grep 'new MemoryAnalyticsService('finds 0 non-test hits against 31 in test files (the control).service-analyticsplugin.ts:245mentions it in a comment as a registered fallback "from dev-plugin", but no code in this repo registers it.service-analyticsnative-sql-strategy.ts:1244widensvalues[0]and then coercesnextDay.objectql-strategy.ts:548widens the raw$ltebound.lteBoundis type-blind and converts nothing.driver-mongodbmongodb-filter.ts:1165widensvalueand then storesnextDay.origin/mainby 2 commits. They were9b384f63ae(service-storage docs) and5757463712(platform-objects translations), with no path indriver-memory,specorcore. The branch was not merged forward; CI's merge ref covers them.Generated by Claude Code