You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e1383be
Browse filesBrowse the repository at this point in the historyBrowse files
**BREAKING**: this narrows what `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` answer for one class of request. When an authored cube's time dimension declares exactly one granularity, a query that groups by that dimension without stating a granularity is now bucketed at the declared one — and a bucketed query is served by the engine aggregate path, which refuses a custom-SQL measure (a measure of type `number`, `string` or `boolean` whose `sql` is an expression) with `400 INVALID_FIELD`. Such a query was answered before this change, with one group per distinct timestamp; it is now refused, exactly as it already was when the caller stated that granularity by hand. A host that overrides `queryCapabilities` to offer raw SQL with no engine aggregate bridge (the plugin's default wires both) now answers such a query "No strategy can handle query" instead of grouping raw timestamps. The remedy: query the custom-SQL measure without grouping by that dimension, or, if the dimension is not meant to have one default bucket, declare the granularities it offers as a list of two or more (or omit the key). It ships as `minor` under the launch-window convention; the widening half is two authored keys taking effect.
12
+
**BREAKING**: this narrows what `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` answer for one class of request. When an authored cube's time dimension declares exactly one granularity, a query that groups by that dimension without stating a granularity is now bucketed at the declared one. The raw-SQL path declines every bucketed query, so such a query now runs on the engine aggregate path, which answers `400 INVALID_FIELD` for every member it cannot evaluate: a custom-SQL measure (a measure of type `number`, `string` or `boolean` whose `sql` is an expression); and, on a cube whose members resolve through its `joins`, a measure or a `where` field over a joined object, a `timeDimensions` entry over a joined object (bucketed or a `dateRange` window, so grouping by a one-granularity time dimension over a joined object is refused too), a dimension that traverses more than one relationship, and an `avg` or `count_distinct` measure beside any dimension over a joined object. The raw-SQL path answers every one of these, with one group per distinct timestamp; each is now refused, exactly as it already was when the caller stated that granularity by hand. On a host that overrides `queryCapabilities` to offer raw SQL with no engine aggregate bridge (the plugin's default wires both), no strategy remains for a bucketed query, so every newly bucketed query, a plain `count` included, now answers "No strategy can handle query" instead of grouping raw timestamps. The remedy: run such a query without grouping by that dimension, or, if the dimension is not meant to have one default bucket, declare the granularities it offers as a list of two or more (or omit the key); on a raw-SQL-only host, add the engine aggregate bridge. It ships as `minor` under the launch-window convention; the widening half is two authored keys taking effect.
13
13
14
14
Until this change both keys were read on the compiled-dataset path only. One cube shape has three producers — cubes authored with `defineCube()` / `defineStack({ analyticsCubes })`, cubes the dataset compiler mints, and cubes inferred for an ad-hoc query — and only a compiled dataset's cube reached the two readers:
it('DECLARED NARROWING, joined cube: a cross-object measure grouped by a declared-default dimension gets the refusal a stated granularity gets',async()=>{
326
+
// The engine path's other refusals are reached by the same re-route: on a
327
+
// cube that declares `joins`, a member resolving through one (here a
328
+
// measure over `account.balance`) is refused by
329
+
// `ObjectQLStrategy.planCrossObject`, where the raw-SQL path joins it.
Copy file name to clipboardExpand all lines: packages/spec/liveness/analytics_cube.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -108,7 +108,7 @@
108
108
"verifiedAt": "2026-09-29",
109
109
"evidence": "packages/services/service-analytics/src/analytics-service.ts#withDeclaredGranularityDefaults — on `query()` (`POST /api/v1/analytics/query`) and `generateSql()` (`POST /api/v1/analytics/sql`), before the source-field gates and strategy selection run, a time dimension the query GROUPS BY with no stated granularity is bucketed at the default its cube dimension declares; packages/services/service-analytics/src/dataset-executor.ts#declaredDefaultGranularity is that default — the one reading of this key, a single-entry list — shared with the compiled-dataset path's `granularityOf`, so both producers' cubes are read by one rule. Pinned against a compiled-dataset control in packages/services/service-analytics/src/__tests__/cube-authored-format-granularity.test.ts, and over both routes in packages/runtime/src/analytics-authored-cube-format-granularity.test.ts.",
110
110
"producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
111
-
"note": "Dead until 2026-09-29: the one read (`granularityOf`) took a `CompiledDataset`, which an authored cube never becomes, so grouping by an authored time dimension grouped raw timestamps whatever it declared. What the key does now is exactly the compiled-dataset path's reading: a SINGLE-entry list is the dimension's default bucket; a stated `timeDimensions[].granularity` always wins; a multi-entry list (the compiler's five-entry 'all intervals' shape included) states no default; an entry carrying only a `dateRange` for a dimension the query does not group stays a filter. ⚠️ What it does NOT do: refuse a requested granularity the list does not name — the dataset path compares against no list, so neither does this — and whether a multi-entry list should narrow the accept set (the 'Supported Granularities' reading of the declaration) is an open question on #20282, not decided by this row. A declared default routes a grouped query to the engine path, because NativeSQL declines any bucketed query, and the engine path refuses a custom-SQL measure — the same answer a caller who states that granularity by hand gets. The dataset-side authoring key is still `dataset.dimensions[].dateGranularity` (dataset.json), which the compiler lowers into this slot. ⛔ Retirement was never the remedy."
111
+
"note": "Dead until 2026-09-29: the one read (`granularityOf`) took a `CompiledDataset`, which an authored cube never becomes, so grouping by an authored time dimension grouped raw timestamps whatever it declared. What the key does now is exactly the compiled-dataset path's reading: a SINGLE-entry list is the dimension's default bucket; a stated `timeDimensions[].granularity` always wins; a multi-entry list (the compiler's five-entry 'all intervals' shape included) states no default; an entry carrying only a `dateRange` for a dimension the query does not group stays a filter. ⚠️ What it does NOT do: refuse a requested granularity the list does not name — the dataset path compares against no list, so neither does this — and whether a multi-entry list should narrow the accept set (the 'Supported Granularities' reading of the declaration) is an open question on #20282, not decided by this row. A declared default routes a grouped query to the engine path, because NativeSQL declines any bucketed query, and the engine path answers 400 `INVALID_FIELD` — the same answer a caller who states that granularity by hand gets — for every member it cannot evaluate: a custom-SQL measure (`ObjectQLStrategy.resolveMeasureAggregation`), and, from `ObjectQLStrategy.planCrossObject`, on a cube whose members resolve through its `joins`: a measure or `where` field over a joined object, a `timeDimensions` entry over a joined object (bucketed or a window), a dimension that traverses more than one relationship, and an `avg` / `count_distinct` measure beside a dimension over a joined object (planCrossObject's two dataset-definition arms cannot fire on an authored cube). NativeSQL serves all of them, so each goes from answered to refused when grouped by a one-interval dimension. On a host whose `queryCapabilities` offers raw SQL with no engine aggregate bridge (a hand override), no strategy remains, so every newly bucketed query answers \"No strategy can handle query\". The declared narrowing and its D3 entry `analytics-cube-single-granularity-default-enforced` name this class. The dataset-side authoring key is still `dataset.dimensions[].dateGranularity` (dataset.json), which the compiler lowers into this slot. ⛔ Retirement was never the remedy."
0 commit comments