Skip to content

fix(service-analytics): the SQL echo prints a date bucket only in the driver's own expression, on every driver (#21647) - #21664

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21647-echo-never-in-memory-bucket
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21647-echo-never-in-memory-bucket

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

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 dateBucketSql hook), at a timezone of UTC or none. Everywhere else it answers the declared refusal (NOT_IMPLEMENTED / 501, refusal: true) that the non-UTC arm and SQLite already gave. The date_trunc('GRANULARITY', col) fallback is gone, and so is the docblock that declared it "representative", per triage's ruling 5974759561.

Reproduction, on main 1968d5e812, real driver-memory

A throwaway test (never committed, deleted) served analytics.query and analytics.generateSql, the seams POST /api/v1/analytics/query and POST /api/v1/analytics/sql forward, through the real AnalyticsServicePlugin and a real ObjectQL engine over InMemoryDriver, with the driver's aggregate and find spied.

timezone granularity driver asked face rows /analytics/query sql /analytics/sql
UTC month find only 2026-01 27, 2026-02 1 date_trunc('month', closed_at) date_trunc('month', closed_at)
UTC week find only 2026-W02 20, 2026-W05 7, 2026-W06 1 date_trunc('week', closed_at) date_trunc('week', closed_at)
unset month, week find only the same the same the same

After the fix (ee0abf936e), the same harness answered the same rows, with no sql on the query and NOT_IMPLEMENTED / 501 on the dry run, in all four cells.

The rule, and how it relates to the engine

dimExpr in ObjectQLStrategy.generateSql:

  1. a non-UTC timezone refuses (the analytics on PostgreSQL: a non-UTC timezone still echoes date_trunc(GRANULARITY, col), whose keys fall on the session zone's calendar and group rows differently from the face #21630 arm, unchanged);
  2. otherwise the hook's answer is printed;
  3. otherwise it refuses, on every dialect. The SQLite arm of analytics on SQLite: a week-bucketed (or non-UTC zone) dimension still echoes date_trunc, which SQLite refuses; driver-sql has no SQLite week expression #21595 is now just the first case of this.

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.aggregate decides pushdown inline: typeof driver.aggregate === 'function', every granularity in supports.queryDateGranularity, !tzRequiresInMemory, and no aggregations[].filter. No IDataEngine member exposes that decision, and this card allows no objectql edit. So the strategy reads the zone (the same test on the same value as tzRequiresInMemory) and the hook. Why the hook is the engine's capability signal: every driver-sql dialect's dateBucketSql answers null exactly where its supports.queryDateGranularity is false. Both read dateGranularityCapabilities, the first line of buildDateBucketExpr. 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:

    • a measure carrying its own 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 advertises queryDateGranularity: {}, 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) publishes queryDateGranularity for 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.ts

Driver 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:

  • pushed down to a driver that renders an expression for the bucket: that expression on both faces;
  • anything else: the refusal on /analytics/sql, and no sql on /analytics/query.

The single named exception is Turso's remote face.

  • Driver axis: BUILTIN_DRIVER_IDS from @objectstack/spec/data. DRIVER_ROWS satisfies a Record keyed by BuiltinDriverId, and a case asserts the key sets are equal, so a new builtin with no row fails both typecheck and the file.
    • Real: sqlite (better-sqlite3), sqlite-wasm (sql.js), and postgres / mysql (driver-sql, never connected).
    • By code path: memory (supports = {}, no dialectName, no dateBucketSql), mongodb, and turso's local and remote faces.
  • Timezone axis: 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/UTC is in the in-memory class because the engine treats only the literal UTC as UTC.
  • Granularity axis: TimeUpdateInterval.options, the enum timeDimensions[].granularity parses. 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:

face UTC / unset any other zone
memory (by code path) in memory, refusal in memory, refusal
mongodb (by code path) pushdown, refusal in memory, refusal
sqlite, sqlite-wasm, postgres, mysql, turso local pushdown, the driver's expression in memory, refusal
turso remote (by code path) in memory, the driver's expression (the named exception) in memory, refusal

A rows case serves real rows through the engine over driver-memory's surface. These are the rows measured above: 2026-01 27 and 2026-02 1; 2026-W02 20, 2026-W05 7 and 2026-W06 1. /analytics/query answers them with no sql, and the dry run refuses.

Pins turned (no assertion deleted)

pin asserted now why
objectql-echo-date-bucket.test.ts, FALLBACK block date_trunc('month', closed_on) on a host with no hook, at UTC the refusal, at UTC and unset the no-hook UTC arm is the case this card retires
dataset-selection-window.test.ts (was :350) date_trunc('month', created_at) with a hook: the hook's expression in SELECT and GROUP BY; with no hook: the rows and no sql its subject, "a bucket expression instead of the bare column", needs a host that has one
objectql-daterange.test.ts (was :423) date_trunc('month', close_date) beside the window the hook's expression beside the same window without a hook the dry run refuses, and the case's subject, the window, would go with it
cube-authored-format-granularity.test.ts (was :247) date_trunc('month' the hook is asked for placed_at:month and its expression groups; the no-hook host refuses; the undeclared dimension renders identically on both hosts its subject is the declared default granularity reaching the dry run

The 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 prints date_trunc) in place of the no-expression refusal should turn exactly 28 cases red:

  • enumeration: memory UTC and unset, 10; mongodb UTC and unset, 10; the rows case, 4;
  • echo pin: the no-hook UTC and unset cases, 2;
  • 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), through scripts/ablation-replace.mjs in wrap mode under the verify lock:

  • the anchor went from 1 hit to 0, and the replacement from 0 to 1; the blob went b6ff007d3371 to 895dda1491ae;
  • 28 failed, 275 passed, 9 skipped (312), exactly the predicted set.

Restore: the blob after restore is b6ff007d3371, equal to HEAD; git diff HEAD is empty; git status --porcelain is empty; there are 0 marker hits. The same result was observed earlier at d0875f3a82, when the memory row was the real driver. There is no dist leg: every subject is imported from src by relative path.

Tests, at 1ef5b5e198

  • Full suite: pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2: 177 files passed; 4403 tests passed, 262 skipped. The skips are the live-server cells.
  • Live PostgreSQL 16.14 (throwaway cluster, since stopped and deleted), OS_TEST_POSTGRES_URL set: the echo pin plus the enumeration, 260 of 260 passed, 0 skipped, the live postgres cell included.
  • Typecheck: pnpm --filter @objectstack/service-analytics typecheck exits 0, and tsc --listFiles includes all five touched test files.
  • Build: pnpm --filter @objectstack/service-analytics build exits 0. dist/index.d.ts carries the corrected AnalyticsServiceConfig.dateBucketSql TSDoc, and the old "representative date_trunc" text has 0 hits.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 64 commands. All 64 exited 0. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET) and exited 0 after a full turbo run build, which had 71 of 73 tasks cached. --ran with exit codes reports 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.
  • Lint, a declared narrowing:
    1. eslint's own config admits all 8 changed .ts files (--print-config answers for each; none is reported ignored);
    2. --no-inline-config --format json: 8 files, 0 errors, 0 warnings;
    3. eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rules), so this diff cannot move the verdict on an untouched file.

Docs

content/docs/** (outside releases/) has no sentence this makes false. data-api.mdx says the dry run's "support depends on the underlying driver/strategy". In skills/**, objectstack-ui/rules/dashboards.md:326 and objectstack-query/rules/aggregation.md:82 name date_trunc for the engine's pushdown, not for the echo. Both are carried by #21588, and neither is edited here.

Acceptance notes

  • The PM's assumption to pin /analytics/query on the real driver-memory was falsified by check: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; 1ef5b5e198 withdrew it, and package.json, vitest.config.ts and pnpm-lock.yaml are byte-identical to main.
  • native-sql-strategy.ts, around canHandle, says the engine buckets with "native date_trunc when UTC-safe". The drivers bucket with to_char, date_format and strftime. The comment predates this PR, this PR does not make it false, and nobody has been named to carry it.
  • main gained two commits after this branch merged 759dbe9ed: 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

claude added 5 commits October 4, 2026 00:36
…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>
…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>
@github-actions github-actions Bot added the size/l label Oct 4, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 6 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/plugins/packages.mdx (via AnalyticsServicePlugin (symbol, a top-level class))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17/17-0.mdx (via ObjectQLStrategy (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17/17-6.mdx (via AnalyticsServicePlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 55e6f14f8d87004c50b0f35eaafba36ac23cf32b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c73fc724bba06a870661face94ec589fa67246a0 — the merge of head 29c4a18396d48e5b340a33b73464009321241bb1 into base 55e6f14f8d87004c50b0f35eaafba36ac23cf32b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 55e6f14f8d87004c50b0f35eaafba36ac23cf32b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 4, 2026 01:59
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants