Repository navigation
fix(service-analytics)!: the read scope and the draft preview compare a temporal comparand in the column storage form (ADR-0053 D-A1 / D-A2) - #21562
Conversation
…heir whole-day and NULL-polarity copies are deleted #5930 step 4 (domain:services), faces F9 and F10. The shared lowering (lowerFilterCondition) is now the one source of the whole-day bound, the $between split and the NULL-polarity guards on the analytics read scope and the where tree: - native-sql-strategy: buildFilterClause's bare-day lte arm is deleted; the dateRange window is the { $gte, $lte } pair, lowered by the same reader as the where (ADR-0053 D-D1 item 8). The reader reads a column the host cannot name type-blind (item 7). - objectql-strategy: the /analytics/sql echo renders the window through the same lowering; the reader leaves an undeclared column as written, for the engine seam to read. - filter-normalizer: the $not-operand rewrite and the #5298 leaf wrap, with their polarity tables, are deleted. - read-scope-sql: the $not-operand rewrite, its three tables and the IS NULL OR wrap are deleted. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…d lowering; the echo window renders the declared column's bound #5930 step 4. Every pin that recorded a face's own copy of the NULL guard stacked inside the shared lowering's (the step-3 rows marked "until the copy's deletion card") now reads the single guard. No row answer moved: every id-set assertion in these files is unchanged. The /analytics/sql window pins wire the declared type the plugin relays (sourceFieldMeta, close_date a datetime), and two controls pin the render on a declared date and where the host names no type (the bound execute() hands the engine, as written), and a preset that stops before its end. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…LL rule's source #5930 step 4: the $empty and non-text-column notes named the deleted $not rewrite and its operatorIsNullTotal table. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
… F9 and F10, and the bare day on every face #5930 step 4. analytics-faces-one-lowering.test.ts holds: - the enumeration: no analytics source file holds a whole-day helper but the draft preview (F11, pending its reader), and none holds a NULL-polarity copy; a positive control proves the scan reads the faces; - one source: the native compiler and the echo emit the bound the lowering hands them (a function of the declared type alone), and every null predicate F9 and F10 emit is one the lowering wrote; - the typed drivers' answer on every face over a real engine (SQLite, and PostgreSQL where OS_TEST_POSTGRES_URL is set): datetime, date and text columns, $lte, $between, $not and dateRange windows, the carrier-note text cell included; - a host with no typed reader: the native face reads type-blind, and the ObjectQL face hands the engine the bound as written; - TEMPORAL_CASES on the native and ObjectQL faces of the plugin's composition and through the read scope. native-sql-temporal-conformance.test.ts runs its matrix with and without the declared-type hook. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…e native face's bare-day bound on a non-temporal column #5930 step 4. The measured answer move, the reader-less hosts, the /analytics/sql echo changes and the ADR-0087 disposition. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…alytics-faces-delete-copies
…in as a query The dateRange pair is a tuple in AnalyticsQuery; tsc refused the widened string[]. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…with no private comment stripper
check:comment-mask-adoption refused the pin's regex comment stripper.
A file holds a helper when it imports, declares or calls it; a prose
mention (a backticked name, a {@link}) is none of those, so no comment
stripping is needed.
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
…ough the driver's coercion pair compileScopedFilterToSql takes two optional members, coerceTemporalFilterValue and coerceTemporalFilterColumn (the driver's ADR-0053 D-A2 pair, bound to the object), and applies them after the shared lowering to every value comparison's comparand and column. Absent is identity. The native read-scope merge and the ObjectQL echo pass the context's pair. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…st the engine on SQLite and PostgreSQL Sixteen cells (twelve datetime, four date as the control) answer the rows engine.find answers, compiled with the driver's pair and end to end through the native face with a host getReadScope; the ObjectQL echo prints the coerced comparand; an uncertified SQLite datetime column pins the column half. The F9 read-scope test in analytics-faces-one-lowering now runs on PostgreSQL too. Changeset: minor, Clause-② yes (widening). Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Brings in #21417 as landed (squash 81e69ca). Conflicts resolved by keeping both sides' meaning: read-scope-sql.ts keeps main's text plus this card's coercion arms; the #21417 changeset and analytics-faces-one-lowering.test.ts take main's final version, with this card's F9 PostgreSQL edit re-applied. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…n its storage form on both sides The preview has no driver, so its counterpart of the engine door is the rule that door applies: @objectstack/core's temporalStorageForm, for the kind temporalComparandKind gives the column's declared type. Each value comparison's comparand and every declared temporal field of a drafted row take that form before the match, as driver-memory reads them. A column the host names no type for stays as written. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…m against the engine The sixteen cells and two window ends written shorter than the stored instant answer engine.find's rows through queryDataset previewDrafts, over drafted rows in the canonical spelling and over the same instants respelled (a Date, no milliseconds, zone-naive, an offset). Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…he read scope and the draft preview answer the engine's rows The answer moves in both directions onto engine.find's rows, so the changeset carries a BREAKING banner and an ADR-0087 not-required (no-migration-prescription) disposition, as #21417 declared for its answer change. It names the two new optional members, their identity default, and the preview's storage form on both sides. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ 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 — 10 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 2911ef0ed4ce171d71964f2a02e6b462d61db368 && git checkout 2911ef0ed4ce171d71964f2a02e6b462d61db368
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 10454b3afa94d49e6e424cc16fbbff3a898f3ad8 f8113c0a093dbd9ccc77c9346dabf65ba53d7690 && git checkout -B drift-repro 10454b3afa94d49e6e424cc16fbbff3a898f3ad8 && git merge --no-ff f8113c0a093dbd9ccc77c9346dabf65ba53d7690
node scripts/docs-audit/affected-docs.mjs --json 10454b3afa94d49e6e424cc16fbbff3a898f3ad8
|
Fixes #21505
Clause-②: yes (narrowing)
ADR-0053 D-A1 binds every surface that puts a filter comparand into raw SQL to the driver's own temporal coercion, and D-A2 makes the comparand coercion and its column companion a pair. The analytics read scope bound a temporal comparand as written, and the draft preview compared temporal values as text. On a declared
datetimecolumn both faces then selected different rows fromengine.findfor the same filter, in both directions. Triage ruling 5964364198 (one coercion, the engine door's, no second copy) and claim revision 5964626028 set the shape built here.What changed
The read scope (
read-scope-sql.ts).ReadScopeCompileOptionsgains two optional members,coerceTemporalFilterValue(field, value)andcoerceTemporalFilterColumn(field, columnSql). They mirrorStrategyContext.coerceTemporalFilterValue/coerceTemporalFilterColumn, bound to the object the scope reads, and reach the driver'stemporalFilterValue/temporalFilterColumnSql. After the shared lowering, every value comparison (implicit equality,$eq,$ne, the four orderings,$in,$nin,$between) binds its comparand through the first and reads its column through the second. Null tests,$emptyand the text arms read the column as stored, as the nativewhereface does. An absent member is identity. The order is ADR-0053 D-E3's by construction: the lowering widens a bare day first, then the arm converts the bound.The two callers.
NativeSQLStrategy.applyReadScopebinds the context's pair to the OBJECT, never the join alias. TheObjectQLStrategyread-scope echo binds it to its table. One call site each.The draft preview (
preview-evaluator.ts). It has no driver, so its counterpart of the engine door is the rule that door applies.@objectstack/core'stemporalStorageForm, for the kindtemporalComparandKindgives the declared type, is applied to both sides of each value comparison: the comparand, and the drafted row's value, asdriver-memoryreads them. It covers thewhereand the window. A column the host names no type for stays as written. The declared type is the reader #21417 already hands the evaluator, soanalytics-service.tsis untouched.No new export (the package index is unchanged), no
@objectstack/specedit, and no new dependency edge.Changeset:
@objectstack/service-analyticsminor, BREAKING, with an ADR-0087not-required (no-migration-prescription)disposition. The answer moves in both directions onto the engine's rows: on some filters fewer rows than before, on others more.Measured before the change (at d2f452b; classes only, the cells live in the pins)
engine.find, over 16 cells (12datetime, 4date): 7 of 12datetimecells differed on SQLite, and 9 of 12 on PostgreSQL 16.14 under a non-UTC server (America/New_York). Both dialects included the admitting direction. Thedatecells differed on 0 of 4.getReadScope, the native face differed on the same cells; the ObjectQL face (the engine path) differed on none.datetimecells and 0 of 4datecells. A window end written shorter than the stored instant left out the row at that instant.temporalStorageFormequals the driver's coercion on SQLite and PostgreSQL. On MySQL the driver adds its own physical spelling, which is why the read scope takes the driver's pair rather than the core rule.Pins
read-scope-temporal-coercion.test.ts(new):$emptyand text arms are untouched; absent members equal identity members;engine.find;getReadScopeequals the engine on all 16;datetimecolumn (an external object, which the driver never backfills), against the driver's ownfind;queryDatasetpreviewDrafts: the 16 cells and two window ends written shorter than the stored instant equalengine.find, over drafted rows in the canonical spelling and over the same instants respelled.analytics-faces-one-lowering.test.ts: the read-scope (F9) test's PostgreSQL skip is gone; it passes the driver pair and runs on both databases.Ablations (predicted first; mutated with
scripts/ablation-replace.mjs, each restore verified blob == HEAD andgit diff HEADempty)The read-scope pair was ablated at
508b0bea9dand again at this head, with identical results.Tests and gates (at f8113c0)
pnpm --filter @objectstack/service-analytics test, with live PostgreSQL set: 175 files passed, 4403 tests passed, 2 skipped.pnpm --filter @objectstack/service-analytics typecheck: green.dispatch-gates --commands: 64 families derived, 64 run, every one exit 0. The--ranreconciliation reads 0 NOT-MEASURED (derived from recorded exit codes).pnpm lintis CI's.Acceptance notes
OS_TEST_POSTGRES_URLfor it, so the PostgreSQL cells here (and #5930 step 4 (domain:services): the analytics faces delete their hand-copied filter meaning — the read scope (F9), thewheretree and its compilers (F10), the draft preview (F11) — each naming its typed column reader #21417's in the same file) are named skips in CI. Each asserts a non-UTC server, so wiring the URL there would not pass vacuously. Noted, not filed.origin/maintook #5930 step 4 (domain:services): the analytics faces delete their hand-copied filter meaning — the read scope (F9), thewheretree and its compilers (F10), the draft preview (F11) — each naming its typed column reader #21417 as a squash (81e69ca), so this branch's commit list still shows #5930 step 4 (domain:services): the analytics faces delete their hand-copied filter meaning — the read scope (F9), thewheretree and its compilers (F10), the draft preview (F11) — each naming its typed column reader #21417's pre-squash commits beneath this card's. The file diff is this card's 7 files only.compileScopedFilterToSqldirectly keeps the as-written bind until it passes the pair from its driver: absent is identity.Generated by Claude Code