fix(service-analytics): the SQL echo prints a date bucket only in the driver's own expression, on every driver (#21647) - #21664
Conversation
…ver's own expression ObjectQLStrategy.generateSql no longer prints a representative date_trunc for a bucket no driver expression stands for. At a UTC or unset timezone, a dateBucketSql hook that answers nothing (driver-memory, driver-mongodb, a granularity the driver leaves to the engine, a host that wires no hook) now refuses with the NOT_IMPLEMENTED / 501 declared refusal the non-UTC arm already answers, on every dialect. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…'s own bucketing A new pin runs driver x timezone class x granularity through the real plugin and engine, reading the axes off BUILTIN_DRIVER_IDS and TimeUpdateInterval, and judges each cell by where the engine actually put the bucket. The three pins that asserted the representative date_trunc of a host with no dateBucketSql hook now assert the hook's expression and the refusal. driver-memory joins the package's devDependencies, aliased to source, for the live memory cell. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…river-memory consumer
check:driver-memory-census ledgers every consumer of
@objectstack/driver-memory, and a new one is a maintainer ruling rather
than a test's choice. The memory row and the rows case now carry
InMemoryDriver's declared surface (supports {}, no dialectName, no
dateBucketSql) under the real engine, as the mongodb row does, and the
devDependency, its source alias and its lockfile entry are withdrawn.
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also 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 c73fc724bba06a870661face94ec589fa67246a0 && git checkout c73fc724bba06a870661face94ec589fa67246a0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55e6f14f8d87004c50b0f35eaafba36ac23cf32b 29c4a18396d48e5b340a33b73464009321241bb1 && git checkout -B drift-repro 55e6f14f8d87004c50b0f35eaafba36ac23cf32b && git merge --no-ff 29c4a18396d48e5b340a33b73464009321241bb1
node scripts/docs-audit/affected-docs.mjs --json 55e6f14f8d87004c50b0f35eaafba36ac23cf32b
|
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…e runtime dry-run pin The client envelope-caller census gains a NOT_SDK row for the two analytics.query sites in service-analytics' new bucket echo enumeration pin (section 2: 9 to 11 service sites; section 3: 9 to 11 not-SDK), and the file is declared by name in @objectstack/client#test's cross-package test inputs, beside the date-bucket echo pin. runtime's authored-cube pin asserted 200 and date_trunc from POST /analytics/sql on a host that wires no dateBucketSql hook. That dry run now answers the declared refusal, NOT_IMPLEMENTED / 501, whose kept message names the bucket (the declared month, the stated year) and the cause. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Part of #21647
Clause-②: no
The ObjectQL face's SQL echo now prints a date-bucketed dimension in one thing only: the expression the driver itself renders for it (the
dateBucketSqlhook), at atimezoneofUTCor none. Everywhere else it answers the declared refusal (NOT_IMPLEMENTED/ 501,refusal: true) that the non-UTC arm and SQLite already gave. Thedate_trunc('GRANULARITY', col)fallback is gone, and so is the docblock that declared it "representative", per triage's ruling5974759561.Reproduction, on
main1968d5e812, realdriver-memoryA throwaway test (never committed, deleted) served
analytics.queryandanalytics.generateSql, the seamsPOST /api/v1/analytics/queryandPOST /api/v1/analytics/sqlforward, through the realAnalyticsServicePluginand a realObjectQLengine overInMemoryDriver, with the driver'saggregateandfindspied./analytics/querysql/analytics/sqlfindonly2026-0127,2026-021date_trunc('month', closed_at)date_trunc('month', closed_at)findonly2026-W0220,2026-W057,2026-W061date_trunc('week', closed_at)date_trunc('week', closed_at)findonlyAfter the fix (
ee0abf936e), the same harness answered the same rows, with nosqlon the query andNOT_IMPLEMENTED/ 501 on the dry run, in all four cells.The rule, and how it relates to the engine
dimExprinObjectQLStrategy.generateSql:timezonerefuses (the analytics on PostgreSQL: a non-UTCtimezonestill echoesdate_trunc(GRANULARITY, col), whose keys fall on the session zone's calendar and group rows differently from the face #21630 arm, unchanged);The refusal names its cause. The zone arm keeps its message. The no-expression arm says either that the driver names no SQL dialect (the in-memory and MongoDB drivers, or a host with no hook), or that no expression is known on this dialect (the engine buckets it in memory, or the host wires no hook).
The engine's own predicate is not reachable from this package.
engine.aggregatedecides pushdown inline:typeof driver.aggregate === 'function', every granularity insupports.queryDateGranularity,!tzRequiresInMemory, and noaggregations[].filter. NoIDataEnginemember exposes that decision, and this card allows noobjectqledit. So the strategy reads the zone (the same test on the same value astzRequiresInMemory) and the hook. Why the hook is the engine's capability signal: everydriver-sqldialect'sdateBucketSqlanswersnullexactly where itssupports.queryDateGranularityis false. Both readdateGranularityCapabilities, the first line ofbuildDateBucketExpr. Where this reading and the engine's predicate differ, the cases are named, not hidden:The engine buckets in memory, and the echo still prints, because the hook answers and the expression, run on that dialect, answers the face's keys:
filter: the engine aggregates every such query in memory. The existing echo pin measures that running the echo reproduces the face's rows, live on SQLite and PostgreSQL;driver-turso's remote face: it advertisesqueryDateGranularity: {}, inherits the SQLite expression (REMOTE_FACE_ANSWERS.dateBucketSql: 'inherited'), and libSQL runs it.See the open question in the report: the literal headline of triage's ruling covers both.
The engine pushes down, and the echo refuses:
driver-mongodb(by code path) publishesqueryDateGranularityfor all five granularities (MONGODB_DATE_GRANULARITIES), so its own pipeline groups the bucket. This corrects the card's premise that MongoDB buckets in memory. MongoDB runs no SQL, and triage's ruling names the refusal as the honest answer on a non-SQL driver. A SQL host that wires no hook refuses too: it cannot name the expression.The enumeration pin:
objectql-echo-bucket-enumeration.test.tsDriver x timezone class x granularity, 200 cells. Every cell runs through the real plugin bridges and a real engine with the driver's two data doors spied. Each cell therefore observes where the engine put the bucket, and its expected echo is derived from that observation:
/analytics/sql, and nosqlon/analytics/query.The single named exception is Turso's remote face.
BUILTIN_DRIVER_IDSfrom@objectstack/spec/data.DRIVER_ROWSsatisfies aRecordkeyed byBuiltinDriverId, and a case asserts the key sets are equal, so a new builtin with no row fails both typecheck and the file.sqlite(better-sqlite3),sqlite-wasm(sql.js), andpostgres/mysql(driver-sql, never connected).memory(supports = {}, nodialectName, nodateBucketSql),mongodb, andturso's local and remote faces.UTC, unset, and any other zone (Asia/Shanghai,America/New_York,Etc/UTC). No enum declares these classes; the engine's inline test does. So a case asserts each probe's tier on the engine, and a zone that moves class turns red.Etc/UTCis in the in-memory class because the engine treats only the literalUTCas UTC.TimeUpdateInterval.options, the enumtimeDimensions[].granularityparses. The cells are generated from it, so a new granularity has its cells the moment it is declared.Observed matrix at
1ef5b5e198, with a temporary log line, since removed:A rows case serves real rows through the engine over
driver-memory's surface. These are the rows measured above:2026-0127 and2026-021;2026-W0220,2026-W057 and2026-W061./analytics/queryanswers them with nosql, and the dry run refuses.Pins turned (no assertion deleted)
objectql-echo-date-bucket.test.ts, FALLBACK blockdate_trunc('month', closed_on)on a host with no hook, at UTCdataset-selection-window.test.ts(was :350)date_trunc('month', created_at)SELECTandGROUP BY; with no hook: the rows and nosqlobjectql-daterange.test.ts(was :423)date_trunc('month', close_date)beside the windowcube-authored-format-granularity.test.ts(was :247)date_trunc('month'placed_at:monthand its expression groups; the no-hook host refuses; the undeclared dimension renders identically on both hostsThe measure-filter case in the echo pin keeps its assertion, with a comment naming it as one of the two in-memory cells that print.
Ablation
The prediction was written before running. Restoring
main's fallback (SQLite refuses, any other dialect printsdate_trunc) in place of the no-expression refusal should turn exactly 28 cases red:dataset-selection-window: 1;cube-authored-format-granularity: 1.Everything else should stay green: every SQL-driver UTC/unset cell, every non-UTC cell, the SQLite no-hook case, the Turso exception and
objectql-daterange.Observed at
1ef5b5e198(fix committed first), throughscripts/ablation-replace.mjsin wrap mode under the verify lock:b6ff007d3371to895dda1491ae;Restore: the blob after restore is
b6ff007d3371, equal to HEAD;git diff HEADis empty;git status --porcelainis empty; there are 0 marker hits. The same result was observed earlier atd0875f3a82, when the memory row was the real driver. There is no dist leg: every subject is imported fromsrcby relative path.Tests, at
1ef5b5e198pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2: 177 files passed; 4403 tests passed, 262 skipped. The skips are the live-server cells.OS_TEST_POSTGRES_URLset: the echo pin plus the enumeration, 260 of 260 passed, 0 skipped, the live postgres cell included.pnpm --filter @objectstack/service-analytics typecheckexits 0, andtsc --listFilesincludes all five touched test files.pnpm --filter @objectstack/service-analytics buildexits 0.dist/index.d.tscarries the correctedAnalyticsServiceConfig.dateBucketSqlTSDoc, and the old "representativedate_trunc" text has 0 hits.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 64 commands. All 64 exited 0.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET) and exited 0 after a fullturbo run build, which had 71 of 73 tasks cached.--ranwith exit codes reports 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN..tsfiles (--print-configanswers for each; none is reported ignored);--no-inline-config --format json: 8 files, 0 errors, 0 warnings;eslint.config.mjsenables no type-aware linting (noparserOptions.project, no typed rules), so this diff cannot move the verdict on an untouched file.Docs
content/docs/**(outsidereleases/) has no sentence this makes false.data-api.mdxsays the dry run's "support depends on the underlying driver/strategy". Inskills/**,objectstack-ui/rules/dashboards.md:326andobjectstack-query/rules/aggregation.md:82namedate_truncfor the engine's pushdown, not for the echo. Both are carried by #21588, and neither is edited here.Acceptance notes
/analytics/queryon the realdriver-memorywas falsified bycheck:driver-memory-census. Every consumer of that package is ledgered, and a new one is a maintainer ruling, not a ledger edit. The memory row and the rows case are pinned by code path under the real engine, and the real driver was measured before and after the change in an uncommitted repro. An intermediate commit (d0875f3a82) added the devDependency;1ef5b5e198withdrew it, andpackage.json,vitest.config.tsandpnpm-lock.yamlare byte-identical tomain.native-sql-strategy.ts, aroundcanHandle, says the engine buckets with "native date_trunc when UTC-safe". The drivers bucket withto_char,date_formatandstrftime. The comment predates this PR, this PR does not make it false, and nobody has been named to carry it.maingained two commits after this branch merged759dbe9ed: a spec inline-grid retirement and a service-automation fix. Both are disjoint from this package. The PR's CI runs on the merge ref.Generated by Claude Code