fix(service-analytics): the ObjectQL face echoes an offset with no limit as a statement the dialect runs - #21440
Conversation
…w the dialect runs generateSql's two window lines now call windowClauseSql (exported from native-sql-strategy.ts) with sqlDialectFor(ctx, tableName), the same dialect read the echo's read scope already makes. On SQLite an offset with no limit echoes LIMIT -1 OFFSET n, the statement the native face runs, instead of a bare OFFSET that SQLite refuses. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…atement's from ORDER BY on Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ow; add the changeset Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…jectql-echo-window
📓 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 3d428890a44add83b054db03b328c1d43525cf68 && git checkout 3d428890a44add83b054db03b328c1d43525cf68
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 b9456bff3716d8d5365ee9cf6202aa59bf09af89 && git checkout -B drift-repro 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 && git merge --no-ff b9456bff3716d8d5365ee9cf6202aa59bf09af89
node scripts/docs-audit/affected-docs.mjs --json 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2
|
Fixes #21365
Clause-②: no
What this changes
This is the remainder of #21365 after PR #21399 (
6d67ad5ec, which readPart of). That PR narrowed the window contract and made the native face render an offset-only window with the dialect's no-limit spelling. The ObjectQL face's echo still wrote its own window.ObjectQLStrategy.generateSql(packages/services/service-analytics/src/strategies/objectql-strategy.ts) wroteLIMIT nwhen a limit was set, thenOFFSET nwhen an offset was. Anoffsetwith nolimittherefore echoed a bareOFFSET, and SQLite's grammar has no bareOFFSET. Those two lines are now one call:windowClauseSql(query.limit, query.offset, sqlDialectFor(ctx, tableName))windowClauseSqlis the function the native face runs, already exported fromnative-sql-strategy.ts. That file is not edited, and there is no second spelling table.The ObjectQL face's echoed
sqland thePOST /api/v1/analytics/sqlbody (both aregenerateSql) now carry the same window clause the native face executes on the same driver.Measured at the real route
The harness is a scratch copy of
packages/runtime/src/analytics-query-window-validity.test.ts. It uses the realdispatcher-pluginmount overAnalyticsServicePluginand a realObjectQLengine withSqlDriver(better-sqlite3). It boots the default composition and one narrowed to the engine aggregate (the ObjectQL face). The query isorder: { note: 'asc' }, offset: 1, with no limit. Each echoed statement was then run on the same SQLite database through knex, to read SQLite's own diagnostic. The harness was deleted and never committed.mainbdd3654f2/queryrowssql=/sqlbody… ORDER BY "note" ASC OFFSET 1… ORDER BY "note" ASC LIMIT -1 OFFSET 1near "OFFSET": syntax error/sql… ASC LIMIT -1 OFFSET 1, x y zlimit: 2, offset: 1, ObjectQL echo… LIMIT 2 OFFSET 1, runsOn this query the ObjectQL echo now equals, byte for byte, the statement the native face runs on SQLite.
The dialect source (dispatch, Zone 2 item 2)
generateSqlalready has the strategy context in scope, and it already readssqlDialectFor(ctx, tableName)for its read-scope compile. That is the samesqlDialecthook, read the same way, thatNativeSQLStrategy.generateSqluses withsqlDialectFor(ctx, this.extractObjectName(cube)).tableNamehere isthis.extractObjectName(cube)too.AnalyticsServicePluginwires that hook from the data engine's driver whatever the query capabilities are, so the narrowed composition above readssqlite. No new hook.Pin
packages/services/service-analytics/src/__tests__/objectql-echo-offset-only-window.test.tshas three tests per cell:generateSql(the/analytics/sqlbody) equals the echo, with no params. FromORDER BYon, the echo equals the statement the native face executes on the same driver. Run through the engine's raw-SQL bridge, the echo answers the face's rows.unknownarm. AnObjectQLStrategywhose context wires nosqlDialecthook echoesLIMIT 9223372036854775807 OFFSET 1, and that statement runs.limit: 2, offset: 1keeps its bytes, and the echo runs.The cells:
OS_TEST_POSTGRES_URLis set, and a named skip otherwise. No CI step provisions that variable for this package, so CI runs only the SQLite cell. I ran it locally against PostgreSQL 16.14. There the echo keepsOFFSET 1alone, byte-identical to before. The comparison with the native statement starts atORDER BYbecause on PostgreSQL the native face also casts the summed column.Ablation at committed
2cfcc44ee, throughnode scripts/ablation-replace.mjs. The anchor hit once, and the blob changed9ed31b50c50btoe8b910c3c13f. The mutation restored the two old window lines. The pin readssrc/with nodist/leg (objectql is aliased to source, and the strategy is imported fromsrc). Result, with live PostgreSQL: 3 failed, 3 passed.OFFSET 1), and theunknownarm on both engines.I predicted that direction before the run. Restore was proven: the blob after restore equals HEAD
9ed31b50c50b, andgit diff HEADis empty.Tests and gates
All of these ran at head
b9456bff3, which is this branch withorigin/main8b123c0aemerged in. That merge brought PR #21424'snative-sql-strategy.tschange.windowClauseSqlis unchanged by it.pnpm --filter @objectstack/service-analytics test, withOS_TEST_POSTGRES_URLset to a local PostgreSQL 16.14: Test Files 170 passed (170), Tests 3957 passed (3957).os-verify-lockprintedVERDICT command-exit 0.pnpm --filter @objectstack/service-analytics typecheck:VERDICT command-exit 0.tsc --noEmit --listFileslists the new pin once. The packagetsconfigincludessrcand does not exclude tests.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) derived 63 commands. All 63 ran atb9456bff3. 62 exited 0 on the first run.pnpm check:dual-build-cjs-loadsfirst answeredPREREQUISITE NOT MET(exit 3), because the workspace was not fully built. After a fullturbo run build(72 tasks, exit 0), it exited 0.--ranreconciles 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN. The list includescheck:nul-bytes,check:changeset-gate-self-tests,check:adr-0087-registration,check:changeset-no-majorandcheck:test-source-alias. No gate touchedAGENTS.md, and the tree stayed clean.eslint --no-inline-config --format jsonon the 2 touched.tsfiles read 2 files, 0 errors, 0 warnings, none ignored.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so an untouched file's verdict cannot move. The repo-widepnpm lintis left to CI.Docs
I grepped
content/docs/**outsidereleases/for the analytics echo,/analytics/sqland window rendering.api/data-api.mdxdescribes/analytics/sqlas a dry run returning{ sql, params }, andapi/client-sdk.mdxshowsclient.analytics.explain. Neither says how a window renders. No sentence is made false, so there is no docs edit.Acceptance notes
date_trunc('month', closed_on). That holds on both faces, and in the default composition too, because the native face declines granularity. SQLite refuses it withno such function: date_trunc, measured on this branch after the window fix. Onmainthe same statement failed earlier, at the bareOFFSET. The driver runsstrftime('%Y-%m', …)there (driver-sqlsql-driver.ts). The comment overdimExprsays the echo renders "the SQL shape the driver's own bucketing implements", and on SQLite that is not so. This isdimExpr, not the two window lines, so it is reported to the seat rather than fixed here.mysql. The window cell iswindowClauseSql's own (LIMIT 18446744073709551615). It is NOT MEASURED on either face here, because no MySQL server is available. That is the same declared skip PR fix(analytics)!: a query window outside the non-negative integers is refused at the door, and an offset with no limit runs on SQLite #21399 carries.sqlDialecthook names no dialect now echoesLIMIT 9223372036854775807 OFFSET nwhere it echoedOFFSET n. The native face already runs that statement for such a host. It answers the same rows on PostgreSQL and SQLite, as the pin'sunknownarm measures.Generated by Claude Code