From c3995deb6f906466fe8577d14636d3ed3c531329 Mon Sep 17 00:00:00 2001
From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
Date: Fri, 15 May 2026 20:40:05 +0400
Subject: [PATCH 1/2] docs: Document DAX cross-view filtering (#10891)
Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
---
.../core-data-apis/dax-api/_meta.js | 1 +
.../dax-api/cross-view-filter.mdx | 85 +++++++++++++++++++
.../core-data-apis/dax-api/index.mdx | 12 +++
3 files changed, 98 insertions(+)
create mode 100644 docs/content/product/apis-integrations/core-data-apis/dax-api/cross-view-filter.mdx
diff --git a/docs/content/product/apis-integrations/core-data-apis/dax-api/_meta.js b/docs/content/product/apis-integrations/core-data-apis/dax-api/_meta.js
index c37c3be470df6..7b63b67855604 100644
--- a/docs/content/product/apis-integrations/core-data-apis/dax-api/_meta.js
+++ b/docs/content/product/apis-integrations/core-data-apis/dax-api/_meta.js
@@ -1,3 +1,4 @@
export default {
+ "cross-view-filter": "Cross-view filtering",
"reference": "Reference"
}
\ No newline at end of file
diff --git a/docs/content/product/apis-integrations/core-data-apis/dax-api/cross-view-filter.mdx b/docs/content/product/apis-integrations/core-data-apis/dax-api/cross-view-filter.mdx
new file mode 100644
index 0000000000000..bb32d4398b0ab
--- /dev/null
+++ b/docs/content/product/apis-integrations/core-data-apis/dax-api/cross-view-filter.mdx
@@ -0,0 +1,85 @@
+# Cross-view filtering in the DAX API
+
+By default, the [DAX API][ref-dax-api] exposes each [view][ref-views] as a
+separate perspective in [Power BI][ref-powerbi]. Visualizations from different
+views can't share the same filters, and a single report can't combine measures
+and dimensions across views.
+
+Cross-view filtering removes this limitation, letting you build dashboards
+that span multiple views and apply filters consistently across visualizations.
+It is supported in both [Live connection](#live-connection-mode) and
+[DirectQuery](#directquery-mode) modes.
+
+To use cross-view filters such as slicers, you must enable single-perspective mode
+in the DAX API. To enable single-perspective mode in the DAX API,
+set the CUBEJS_DAX_SINGLE_PERSPECTIVE environment variable to `true`.
+
+In single-perspective mode, all views are exposed as part of a single perspective.
+This allows you to use a single connection and show visualizations from different
+views on the same dashboard.
+
+
+
+Single-perspective mode changes the way views are exposed by the DAX API.
+Because of that, it is not backwards-compatible with dashboards created before
+enabling this environment variable.
+
+
+
+## Live connection mode
+
+In Live connection mode, cross-view filters work automatically, provided that
+the following two conditions are met:
+
+- The column used in the filter must be present in every view that the
+cross-filter has to be applied to, and named exactly the same in each view.
+- The column in all such views must resolve to the same cube member.
+
+For example, if views `orders_view` and `users_view` both expose a column named
+`address_country` pointing to the `country.country` cube member, cross-filtering
+will be applied across these views.
+
+If the `address_country` column in `orders_view` points to `orders.country`
+while the `address_country` column in `users_view` points to `users.country`,
+cross-filtering will not be applied, because the columns resolve to different
+cube members.
+
+Likewise, if `orders_view` exposes an `address_country` column pointing to
+`country.country` and `users_view` exposes a `country` column also pointing to
+`country.country`, cross-filtering will not be applied, because the column
+names differ between the views.
+
+## DirectQuery mode
+
+In DirectQuery mode, cross-view filters require the same two conditions as in
+[Live connection mode](#live-connection-mode): the filter column must be
+present in every view with the exact same name, and it must resolve to the
+same cube member across those views.
+
+In addition, you must manually configure relationships between tables in Power
+BI so that filters propagate across views:
+
+- Open Model view in the left sidebar.
+- On the right, open the Data sidebar, then the Model tab.
+- Right-click Relationships and choose New relationship.
+- In the Properties sidebar, select a table and a column.
+- For cardinality, choose Many to many (*:*). Ignore the warning.
+- Select another table with the same column.
+- For cross-filter direction, select Both.
+- Click Apply changes.
+
+Once the relationship is configured correctly, cross-view filters will work in
+DirectQuery mode.
+
+The steps above describe configuring a relationship between two tables. When
+many views need to be cross-filtered, the best practice is to create a single
+view in the Cube data model that includes all the columns from the views that
+need to be cross-filtered, and then create relationships between this large
+view and each of the other views. This keeps the relationship graph in Power
+BI simple and avoids configuring pairwise relationships between every
+combination of views.
+
+
+[ref-dax-api]: /product/apis-integrations/dax-api
+[ref-views]: /product/data-modeling/concepts#views
+[ref-powerbi]: /product/configuration/visualization-tools/powerbi
diff --git a/docs/content/product/apis-integrations/core-data-apis/dax-api/index.mdx b/docs/content/product/apis-integrations/core-data-apis/dax-api/index.mdx
index 4a965ac625bec..366bd1fe94ff1 100644
--- a/docs/content/product/apis-integrations/core-data-apis/dax-api/index.mdx
+++ b/docs/content/product/apis-integrations/core-data-apis/dax-api/index.mdx
@@ -68,8 +68,20 @@ The DAX API only exposes [views][ref-views], not cubes.
+## Cross-view filtering
+
+By default, each view is exposed as a separate perspective in Power BI, so
+visualizations from different views can't share the same filters. Cross-view
+filtering lets you build dashboards that span multiple views and apply filters
+consistently across visualizations, in both Live connection and DirectQuery
+modes.
+
+See [Cross-view filtering][ref-cross-view-filter] for details on how to enable
+and use it.
+
[ref-powerbi]: /product/configuration/visualization-tools/powerbi
+[ref-cross-view-filter]: /product/apis-integrations/dax-api/cross-view-filter
[link-dax]: https://learn.microsoft.com/en-us/dax/
[ref-sql-api]: /product/apis-integrations/sql-api
[ref-ref-dax-api]: /product/apis-integrations/dax-api/reference
From 233e86fba2d2b769deedcde54d0aaec3a1cb5936 Mon Sep 17 00:00:00 2001
From: Igor Lukanin
Date: Fri, 15 May 2026 21:15:08 +0200
Subject: [PATCH 2/2] fix(athena-driver): propagate query cancellation to
Athena via StopQueryExecution (#10861)
* fix(athena-driver): propagate query cancellation to Athena via StopQueryExecution
* refactor(athena-driver): reuse decorateWithCancel, guard redundant StopQueryExecution, use QueryExecutionState enum
* fix(athena-driver): drop redundant stream release, route queryColumnTypes through outer saveCancelFn
* refactor(athena-driver): cancel from the driver on pollTimeout, drop cancelCombinator wiring
* refactor(athena-driver): use pollTimeout constructor option in test, drop log prefix
* chore: trigger CI
* fix(athena-driver): restore stream release no-op, use this.logger for cancel failure
---
.../cubejs-athena-driver/src/AthenaDriver.ts | 14 +++++
.../test/AthenaDriver.test.ts | 61 ++++++++++++++++++-
2 files changed, 73 insertions(+), 2 deletions(-)
diff --git a/packages/cubejs-athena-driver/src/AthenaDriver.ts b/packages/cubejs-athena-driver/src/AthenaDriver.ts
index cc3ad6f00bdca..b89dd5302688f 100644
--- a/packages/cubejs-athena-driver/src/AthenaDriver.ts
+++ b/packages/cubejs-athena-driver/src/AthenaDriver.ts
@@ -553,11 +553,25 @@ export class AthenaDriver extends BaseDriver implements DriverInterface {
Math.min(this.config.pollMaxInterval, 500 * i)
);
}
+ await this.stopQuery(qid);
throw new Error(
`Athena job timeout reached ${this.config.pollTimeout}ms`
);
}
+ // Best-effort: a failure to stop must never bubble up to the caller,
+ // which has already abandoned the query.
+ protected async stopQuery(qid: AthenaQueryId): Promise {
+ try {
+ await this.athena.stopQueryExecution({ QueryExecutionId: qid.QueryExecutionId });
+ } catch (e) {
+ this.logger?.('Failed to stop Athena query', {
+ queryExecutionId: qid.QueryExecutionId,
+ error: (e as Error).message ?? String(e),
+ });
+ }
+ }
+
protected async viewsSchema(tablesSchema: DatabaseStructure): Promise {
const isView = (table: AthenaTable) => !tablesSchema[table.schema]
|| !tablesSchema[table.schema][table.name];
diff --git a/packages/cubejs-athena-driver/test/AthenaDriver.test.ts b/packages/cubejs-athena-driver/test/AthenaDriver.test.ts
index 11c3f22532231..1ac567dc5ac02 100644
--- a/packages/cubejs-athena-driver/test/AthenaDriver.test.ts
+++ b/packages/cubejs-athena-driver/test/AthenaDriver.test.ts
@@ -1,5 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { DriverTests, smartStringTrim } from '@cubejs-backend/testing-shared';
+import { pausePromise } from '@cubejs-backend/shared';
+import { QueryExecutionState } from '@aws-sdk/client-athena';
import { AthenaDriver } from '../src';
@@ -15,14 +17,32 @@ class AthenaDriverTest extends DriverTests {
}
}
+// A row-by-row regex+concat over a 49999×49999 cross-join forces full
+// materialization (no aggregate pushdown), so Athena cannot finish
+// before the driver's pollTimeout fires.
+const SLOW_QUERY = `
+ SELECT count(*) AS c
+ FROM (
+ SELECT length(regexp_replace(
+ CONCAT(CAST(a.i * b.j + 7919 AS VARCHAR), '-', CAST(a.i AS VARCHAR)),
+ '[0-9]', 'd'
+ )) AS n
+ FROM unnest(sequence(1, 49999)) AS a(i)
+ CROSS JOIN unnest(sequence(1, 49999)) AS b(j)
+ )
+ WHERE n > 0
+`;
+
describe('AthenaDriver', () => {
let tests: AthenaDriverTest;
+ let driver: AthenaDriver;
- jest.setTimeout(2 * 60 * 1000);
+ jest.setTimeout(3 * 60 * 1000);
beforeAll(async () => {
+ driver = new AthenaDriver({});
tests = new AthenaDriverTest(
- new AthenaDriver({}),
+ driver,
{
expectStringFields: true,
csvNoHeader: true,
@@ -53,4 +73,41 @@ describe('AthenaDriver', () => {
test('unload empty', async () => {
await tests.testUnloadEmpty();
});
+
+ test('pollTimeout cancels the in-flight Athena query', async () => {
+ // Aggressive pollTimeout (5s) so the test doesn't depend on the
+ // ambient CUBEJS_DB_QUERY_TIMEOUT. Constructor multiplies by 1000.
+ const cancelDriver = new AthenaDriver({ pollTimeout: 5 });
+ const athena = (cancelDriver as any).athena;
+
+ const startOriginal = athena.startQueryExecution.bind(athena);
+ let queryExecutionId = '';
+ athena.startQueryExecution = async (input: any) => {
+ const result = await startOriginal(input);
+ queryExecutionId = result.QueryExecutionId;
+ return result;
+ };
+
+ try {
+ await expect(cancelDriver.query(SLOW_QUERY, [])).rejects.toThrow(/Athena job timeout/);
+ expect(queryExecutionId).toBeTruthy();
+
+ // Verify Athena's own view of the query: must be CANCELLED (or
+ // FAILED, if a cancel raced with completion) — never SUCCEEDED.
+ for (let i = 0; i < 30; i++) {
+ const exec = await athena.getQueryExecution({ QueryExecutionId: queryExecutionId });
+ const state = exec.QueryExecution?.Status?.State;
+ if (state === QueryExecutionState.CANCELLED || state === QueryExecutionState.FAILED) {
+ return;
+ }
+ if (state === QueryExecutionState.SUCCEEDED) {
+ throw new Error(`Athena query ${queryExecutionId} succeeded before cancel took effect`);
+ }
+ await pausePromise(500);
+ }
+ throw new Error(`Athena query ${queryExecutionId} did not reach a terminal state within 15s of cancel`);
+ } finally {
+ await cancelDriver.release();
+ }
+ });
});