From 56fc0e77e355d1d44e187d0507251378131ebaa5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 01:29:42 +0000 Subject: [PATCH 1/2] docs(changeset): scope two pending analytics BREAKING banners to the SQL echo on either strategy, and anchor the nested-relation note's base The field-level gate and the relationship-path admission both run in the call context generateSql() shares, ahead of the strategy choice, and the ObjectQL strategy's echo never reached the engine's guards: the echo moved from a printed statement to 403 on both strategies, not only on a SQL deployment. The nested-relation note's Why paragraph describes the base measured before those two gates landed; it now says so. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude --- .changeset/20887-analytics-nested-relation-engine-answer.md | 2 +- .changeset/20917-analytics-field-permission-gate.md | 2 +- .changeset/20933-analytics-relationship-path-admission.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/20887-analytics-nested-relation-engine-answer.md b/.changeset/20887-analytics-nested-relation-engine-answer.md index 5cb7f21ae00..ede9946ef36 100644 --- a/.changeset/20887-analytics-nested-relation-engine-answer.md +++ b/.changeset/20887-analytics-nested-relation-engine-answer.md @@ -17,7 +17,7 @@ Clause-②: yes (narrowing) - At a measure's own `filter` the form is refused with `400 INVALID_FILTER`, as the engine refuses it at an aggregation's own `filter`: put the condition in the query's `where`. - `POST /api/v1/analytics/sql` refuses a `where` carrying the form with `400 INVALID_FILTER`: no statement it could print reproduces a read of the related object as the caller. The query itself is answered by `POST /api/v1/analytics/query`. -**Why.** Measured on the base over one fixture with the real security layer (a related field the caller may not read, a related row scope, 1,001 matching related records). The native-SQL strategy flattened the form to a dotted member and joined the related table: through a dataset that `include`d the relationship it answered rows for a condition on a field the caller cannot read, answered a match past the engine's cap, and counted a measure filter carrying the form; without the declared join it named a table that does not exist (500), and a multi-valued relation was refused. The engine-aggregate strategy refused the form as a cross-object filter (400). The engine serves the form since the nested-relation filter landed in `where`. +**Why.** Measured on the base before the field-level gate (#20917) and the relationship-path admission (#20933) landed, over one fixture with the real security layer (a related field the caller may not read, a related row scope, 1,001 matching related records). The native-SQL strategy flattened the form to a dotted member and joined the related table: through a dataset that `include`d the relationship it answered rows for a condition on a field the caller cannot read, answered a match past the engine's cap, and counted a measure filter carrying the form; without the declared join it named a table that does not exist (500), and a multi-valued relation was refused. The engine-aggregate strategy refused the form as a cross-object filter (400). The engine serves the form since the nested-relation filter landed in `where`. **How.** The native-SQL strategy declines a query in which the form appears in the `where`, the dataset's own `filter` or a requested measure's `filter`, so the query runs on the engine-aggregate path, which hands the form to the engine as written. diff --git a/.changeset/20917-analytics-field-permission-gate.md b/.changeset/20917-analytics-field-permission-gate.md index 8b099e60cde..e6f16918ca7 100644 --- a/.changeset/20917-analytics-field-permission-gate.md +++ b/.changeset/20917-analytics-field-permission-gate.md @@ -8,7 +8,7 @@ Clause-②: yes (narrowing) -**BREAKING for analytics queries on a SQL deployment that read a field the caller may not read.** +**BREAKING for analytics queries that read a field the caller may not read: on a SQL deployment, and on `POST /api/v1/analytics/sql` whichever strategy serves the cube.** **What changed.** `POST /api/v1/analytics/query`, `POST /api/v1/analytics/sql` and `POST /api/v1/analytics/dataset/query` now judge every field a query reads diff --git a/.changeset/20933-analytics-relationship-path-admission.md b/.changeset/20933-analytics-relationship-path-admission.md index 7b1ecf4d4c1..e6de8a89e8d 100644 --- a/.changeset/20933-analytics-relationship-path-admission.md +++ b/.changeset/20933-analytics-relationship-path-admission.md @@ -8,7 +8,7 @@ Clause-②: no (narrowing) -**BREAKING for analytics queries on a SQL deployment that read a related object through a relationship path the cube does not declare.** +**BREAKING for analytics queries that read a related object through a relationship path the cube does not declare: on a SQL deployment, and on `POST /api/v1/analytics/sql` whichever strategy serves the cube.** **What changed.** The analytics door admits and row-scopes one object set before either strategy runs. It held the cube's base object and the joins the From db64c37690a68e42a0855c658261fcfc9e198507 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 01:43:28 +0000 Subject: [PATCH 2/2] docs(changeset): qualify the ObjectQL "already refused" sentences of two pending analytics notes to the doors they hold for The ObjectQL strategy already refused these queries on the query and the dataset doors, through the engine; on the SQL echo it printed the statement, because its generateSql renders without an engine call. Each sentence now names the two doors and says the echo printed. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude --- .changeset/20917-analytics-field-permission-gate.md | 4 +++- .changeset/20933-analytics-relationship-path-admission.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.changeset/20917-analytics-field-permission-gate.md b/.changeset/20917-analytics-field-permission-gate.md index e6f16918ca7..8ec24209245 100644 --- a/.changeset/20917-analytics-field-permission-gate.md +++ b/.changeset/20917-analytics-field-permission-gate.md @@ -19,7 +19,9 @@ filters. A member of an authored cube is judged by the field it resolves to, not by its name in the cube. A field the caller may not read answers `403 PERMISSION_DENIED`, in the words the engine uses for the same field. The native-SQL strategy, the one a SQL driver serves first, answered such queries; -the ObjectQL strategy and the data API already refused them. +the ObjectQL strategy already refused them on `POST /api/v1/analytics/query` +and `POST /api/v1/analytics/dataset/query`, as the data API did, but printed +the statement on `POST /api/v1/analytics/sql`. **What is not affected.** A query that reads only fields the caller may read answers as before. A system context, and a caller with no permission sets, are diff --git a/.changeset/20933-analytics-relationship-path-admission.md b/.changeset/20933-analytics-relationship-path-admission.md index e6de8a89e8d..a737f05eea8 100644 --- a/.changeset/20933-analytics-relationship-path-admission.md +++ b/.changeset/20933-analytics-relationship-path-admission.md @@ -40,7 +40,9 @@ deployment with no security service applies no object-level check, as on the data API. **Refusals that change form.** On the ObjectQL strategy a related object the -caller may not read was already refused; it now answers the analytics door's +caller may not read was already refused on `POST /api/v1/analytics/query` and +`POST /api/v1/analytics/dataset/query`, though `POST /api/v1/analytics/sql` +printed the statement; on those two doors it now answers the analytics door's refusal rather than the engine's, the same one a declared join gets. A filter, a time window or a two-hop path through such an object moves from `400 INVALID_FIELD` to that `403`. A relationship path whose relationship name