Skip to content

fix(service-analytics): the SQL echo refuses a date bucket the engine buckets in memory, on every dialect (#21630) - #21645

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21630-echo-refuses-in-memory-bucket
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21630-echo-refuses-in-memory-bucket

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21630
Clause-②: no

What changes

One rule for the ObjectQL face's SQL echo (ObjectQLStrategy.generateSql), on every dialect, as triage ruled on the card (5973378431): whenever the engine buckets a date dimension in memory because the query carries a non-UTC timezone (ADR-0053 Phase 2, D2; tzRequiresInMemory in objectql's engine.ts), the echo answers the refusal PR #21629 introduced on SQLite, NOT_IMPLEMENTED / 501 with refusal: true. No date_trunc text for that case on any dialect.

  • The check runs in dimExpr before the dateBucketSql hook is asked, on the test the engine makes (timezone set and not 'UTC'). So it holds on PostgreSQL, MySQL and SQLite, and also on a host that wires no hook and on a non-SQL driver: the engine buckets a non-UTC zone in memory whatever the driver.
  • sqliteBucketEchoRefused becomes bucketEchoRefused, with two arms that share one envelope: the in-memory zone (every dialect, new message), and SQLite where no driver expression answers at UTC (the earlier no-hook arm, message unchanged).
  • UTC and an unset timezone are untouched: each driver's own expression (to_char / date_format / strftime).
  • AnalyticsServiceConfig.dateBucketSql's TSDoc said "a host that wires nothing keeps the echo's representative date_trunc". It ships in dist/index.d.ts (measured, line 1250 after build), and this change makes it false for a non-UTC zone, so it now says when the hook is asked. Comment only. It sits outside the claim's listed file surface; see the scope note below.
  • Changeset: @objectstack/service-analytics patch, Clause-②: no.

No driver change, no packages/objectql edit, no packages/spec edit, no zone-aware expression ("Not this card" in the ruling).

Reproduced first, on main

PostgreSQL 16.14, a throwaway server with TimeZone = Asia/Shanghai, process TZ=America/New_York, the pin's five rows, at main 7b07749f05. The echo text is the same in the /analytics/query answer's sql and in the /analytics/sql dry run (analytics.generateSql, the body that route answers with).

query timezone face rows echo printed that echo, run on PostgreSQL
Asia/Shanghai, month 2026-01 20, 2026-02 8, 2026-03 10, 2026-04 5 date_trunc('month', closed_at) 2025-12-31T16:00:00.000Z 20, 2026-01-31T16:00:00.000Z 8, 2026-02-28T16:00:00.000Z 10, 2026-03-31T16:00:00.000Z 5
America/New_York, month 2026-01 27, 2026-02 1, 2026-03 10, 2026-04 5 the same text 20 / 8 / 10 / 5, on the session zone's calendar: other groupings than the face
UTC or unset, month 2026-01 27, 2026-02 1, 2026-03 10, 2026-04 5 to_char(("closed_at")::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM') identical to the face

week gave the same shape (non-UTC echoed date_trunc('week', closed_at)).

Both faces after the change

Measured on the same server at ff399dae2d:

  • /analytics/sql refuses with NOT_IMPLEMENTED / 501 and refusal: true, and declaredRefusalMessage(err) returns err.message. That is the same envelope the SQLite arm answers, raised through the same error shape.
  • /analytics/query returns the same rows as before for both zones (table above, unchanged), and its answer has no sql key, because execute() already swallows an echo refusal.
  • Not re-measured: the HTTP envelope through the dispatcher. The route forwards the error object the SQLite arm already raises, so it was not measured again.

Pins

objectql-echo-date-bucket.test.ts:

  • Turned around. The PostgreSQL cell's FALLBACK: a non-UTC timezone ... keeps date_trunc asserted date_trunc('month', closed_at), a statement the engine did not run. That case and the SQLite REFUSAL case are now one case that runs on every live cell, for Asia/Shanghai and America/New_York. It asserts:
    • the rows, per zone;
    • that res.sql is undefined;
    • the dry run's code, status and refusal;
    • a UTC control that now equals driver.dateBucketSql(...) (it was not.toContain('date_trunc')).
  • New: a dialect matrix. SqlDriver is built for better-sqlite3, pg and mysql2 and never connected; its dialectName and dateBucketSql answer from the config alone. It is wired the way the plugin wires its two hooks. On each dialect, non-UTC × {month, week} refuses, and UTC or unset × {month, week} equals that driver's own expression. This pins MySQL without a server.
  • New: a host that wires no hook refuses a non-UTC zone.
  • Kept: the no-hook UTC date_trunc case and the no-hook SQLite refusal.

Other service-analytics tests that assert the fallback text, classified. All three stay: none sets a timezone and none wires the hook, so each asserts the UTC path with no hook.

file assertion disposition
dataset-selection-window.test.ts:357 date_trunc('month', created_at) stays: the dataset path resolves selection.timezone ?? context.timezone ?? 'UTC' to UTC, and no hook is wired
objectql-daterange.test.ts:423 date_trunc('month', close_date) stays: no timezone, no hook
cube-authored-format-granularity.test.ts:247 date_trunc('month' regex stays: no timezone, no hook

No assertion was deleted.

Ablation

Predicted first. Revert the in-memory refusal to main's fallback (date_trunc unless SQLite). Then 11 cases turn red:

  • the live PostgreSQL REFUSAL cases (2);
  • the matrix's postgres and mysql non-UTC cases (8);
  • the no-hook non-UTC case (1).

The SQLite cases and every UTC case stay green.

Observed. At ff399dae2d (the fix committed first), through scripts/ablation-replace.mjs in wrap mode, with live PostgreSQL:

  • the anchor went from 1 hit to 0, and the blob from 1ebcf8942983 to b4081144edee;
  • the run answered 11 failed, 37 passed (48): exactly the predicted set.

Restore proven:

  • the blob is back to 1ebcf8942983, which is the HEAD blob;
  • git diff HEAD is empty, and git status --porcelain is empty.

There is no build leg: the pin imports the strategy from src by relative path.

Tests and gates, at head 267d2202ae (after merging origin/main)

  • Full suite. pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2, with OS_TEST_POSTGRES_URL set (PostgreSQL 16.14) and TZ=America/New_York: 176 files passed; 4451 tests passed, 2 skipped.
  • The pin, verbose: 48 passed. The live PostgreSQL cell ran.
  • Typecheck. pnpm --filter @objectstack/service-analytics typecheck is clean. tsc --listFiles includes the pin.
  • Build. The dependency closure (--filter '@objectstack/service-analytics^...') and the package itself.
  • Derived gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 64 commands, and all 64 exited 0. --ran reconciled: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. The dist-reading gates (check:dts-closure, check:dual-build-cjs-loads, check:sourcemap-no-sources-content, check:published-files, check:lean-entry-closure) were re-run after the package's own build, and all exited 0.
  • Lint, a declared narrowing. These three facts together make it a measurement:
    1. eslint's own config admitted the 3 changed .ts files, with none ignored, under --no-inline-config (as pnpm lint runs).
    2. --format json counted 3 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 a file it does not touch.
  • NOT MEASURED: MySQL live rows, because there is no MySQL server in the container. The matrix pins the echo there. The rows of a non-UTC query are bucketed by the engine in memory on every dialect.

Overlap with #21577

Open draft PR #21577 edits this pin's header docblock (the CI-provisioning sentence about OS_TEST_POSTGRES_URL). This PR's docblock edits start one line below that sentence and leave it untouched. A local git merge-tree of this head with #21577's head 6e12d6934a exits 0, and the merged docblock keeps both intents. Once #21577 lands, CI runs the live PostgreSQL REFUSAL case.

Docs and skills

Acceptance notes

  • Scope note. The AnalyticsServiceConfig.dateBucketSql TSDoc fix in analytics-service.ts is outside the claim's listed file surface. It is the published sentence this change made false, in the same package and the same defect class, and the edit is mechanical. No open PR touches the file. The seat records the surface addition.
  • date_trunc is still reachable at a UTC or unset timezone, where the hook answers nothing and the dialect is not SQLite. That arm is outside the ruling's "the engine buckets in memory (a non-UTC timezone)", so it is not widened here:
    • On a driver-memory datasource it was measured at the service seam: the engine buckets in memory (the driver advertises no queryDateGranularity) and answers 2026-01 keys, while both faces echo date_trunc('month', closed_at), a statement nothing ran.
    • MongoDB takes the same code path, with no dateBucketSql and an unknown dialect. Not measured: there is no server.
    • The no-hook pin keeps this arm on purpose.
    • Reported to the seat as a finding.
  • plugin.ts comment. The dateBucketSql bridge comment in plugin.ts ("undefined keeps the echo's representative date_trunc(…)") has been inexact on SQLite since PR fix(driver-sql,service-analytics): bucket the ISO week natively on SQLite, and the SQL echo refuses a bucket SQLite cannot run #21629. It is an internal comment, not in the published types, and this change does not touch it. Carrier: none.

Generated by Claude Code

claude added 3 commits October 3, 2026 22:54
… buckets in memory, on every dialect

With a non-UTC timezone the engine buckets a date dimension in memory on
that zone's calendar (ADR-0053 Phase 2, D2), on every driver. The ObjectQL
echo kept printing date_trunc on PostgreSQL and MySQL, a statement the
engine never ran; on PostgreSQL it grouped on the session zone's calendar.
It now answers the refusal SQLite already answered (NOT_IMPLEMENTED / 501,
refusal: true), raised before the dateBucketSql hook is asked. UTC and an
unset timezone keep each driver's own expression.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
…en the hook is asked

Its TSDoc said a host that wires no hook keeps the echo's representative
date_trunc. The echo now refuses a non-UTC timezone whatever is wired, and
SQLite already refused; the sentence names both.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

⛔ 3 release-owned page(s) 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/ 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
  • 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 0c50b5dfe723cb5c4733a1ac37508504a0ef1870 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a88097b39240e1f3fbeef280076aca73d499f072 — the merge of head 267d2202ae2d25650ed6a44955701fe8ec0343b5 into base 0c50b5dfe723cb5c4733a1ac37508504a0ef1870, 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 a88097b39240e1f3fbeef280076aca73d499f072 && git checkout a88097b39240e1f3fbeef280076aca73d499f072
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0c50b5dfe723cb5c4733a1ac37508504a0ef1870 267d2202ae2d25650ed6a44955701fe8ec0343b5 && git checkout -B drift-repro 0c50b5dfe723cb5c4733a1ac37508504a0ef1870 && git merge --no-ff 267d2202ae2d25650ed6a44955701fe8ec0343b5

node scripts/docs-audit/affected-docs.mjs --json 0c50b5dfe723cb5c4733a1ac37508504a0ef1870

⚠️ 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 0c50b5dfe723cb5c4733a1ac37508504a0ef1870 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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/m tests tooling

Projects

None yet

2 participants