diff --git a/docs-mintlify/docs/data-modeling/measures.mdx b/docs-mintlify/docs/data-modeling/measures.mdx
index 11d1b15416888..8d3fccf679181 100644
--- a/docs-mintlify/docs/data-modeling/measures.mdx
+++ b/docs-mintlify/docs/data-modeling/measures.mdx
@@ -361,13 +361,6 @@ measures:
format: percent
```
-
-
-The `filter` parameter requires the [Tesseract SQL planner][ref-tesseract-env]
-(`CUBEJS_TESSERACT_SQL_PLANNER=true`).
-
-
-
### Nested aggregates
Use the [`grain`][ref-grain] parameter with `include` to compute an aggregate
@@ -477,7 +470,6 @@ measures:
[ref-filter]: /reference/data-modeling/measures#filter
[ref-case]: /reference/data-modeling/measures#case
[ref-switch-dim]: /reference/data-modeling/dimensions#type
-[ref-tesseract-env]: /reference/configuration/environment-variables#cubejs_tesseract_sql_planner
[ref-calendar-cubes]: /docs/data-modeling/concepts/calendar-cubes
[ref-pop-recipe]: /recipes/data-modeling/period-over-period
[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
diff --git a/docs-mintlify/docs/data-modeling/multi-fact-views.mdx b/docs-mintlify/docs/data-modeling/multi-fact-views.mdx
index 102d5f91b1948..4b7d5a6fd19f5 100644
--- a/docs-mintlify/docs/data-modeling/multi-fact-views.mdx
+++ b/docs-mintlify/docs/data-modeling/multi-fact-views.mdx
@@ -336,8 +336,6 @@ separate-subquery-then-join strategy described
This rewrite applies only when:
-- The Tesseract SQL planner is enabled via
- [`CUBEJS_TESSERACT_SQL_PLANNER`][ref-tesseract-env].
- Both sides of the join condition resolve to the **same underlying cube
member** (a shared dimension), and the join key is composed only of
dimensions.
@@ -540,5 +538,4 @@ to that fact's subquery.
[ref-view-ref]: /reference/data-modeling/view
[ref-segments]: /reference/data-modeling/segments
[ref-sql-api]: /reference/core-data-apis/sql-api
-[ref-tesseract-env]: /reference/configuration/environment-variables#cubejs_tesseract_sql_planner
[link-tesseract]: https://cube.dev/blog/introducing-tesseract
diff --git a/docs-mintlify/docs/pre-aggregations/matching-pre-aggregations.mdx b/docs-mintlify/docs/pre-aggregations/matching-pre-aggregations.mdx
index e96b03c3b6d68..e56af663a419d 100644
--- a/docs-mintlify/docs/pre-aggregations/matching-pre-aggregations.mdx
+++ b/docs-mintlify/docs/pre-aggregations/matching-pre-aggregations.mdx
@@ -159,5 +159,4 @@ configuration option.
[ref-views]: /docs/data-modeling/views
[ref-multi-fact-views]: /docs/data-modeling/multi-fact-views
[ref-multi-stage]: /docs/data-modeling/measures#multi_stage
-[ref-tesseract-env]: /reference/configuration/environment-variables#cubejs_tesseract_sql_planner
-[link-tesseract]: https://cube.dev/blog/introducing-tesseract
\ No newline at end of file
+[link-tesseract]: https://cube.dev/blog/introducing-tesseract
diff --git a/docs-mintlify/reference/core-data-apis/sql-api/joins.mdx b/docs-mintlify/reference/core-data-apis/sql-api/joins.mdx
index 48ae8a7b4935e..563e9d692d584 100644
--- a/docs-mintlify/reference/core-data-apis/sql-api/joins.mdx
+++ b/docs-mintlify/reference/core-data-apis/sql-api/joins.mdx
@@ -227,8 +227,7 @@ GROUP BY 1
```
The `JOIN` type (`INNER`, `LEFT`, `RIGHT`, `FULL`) controls which keys are
-kept. This requires the [Tesseract SQL planner][ref-tesseract-env] and only
-applies to grouped queries whose `GROUP BY` is the join key. See
+kept. This only applies to grouped queries whose `GROUP BY` is the join key. See
[multi-fact views][ref-multi-fact-views] for the full explanation.
@@ -236,4 +235,3 @@ applies to grouped queries whose `GROUP BY` is the join key. See
[ref-join-paths]: /docs/data-modeling/joins#join-paths
[ref-join-hints]: /docs/data-modeling/joins#join-hints
[ref-multi-fact-views]: /docs/data-modeling/multi-fact-views
-[ref-tesseract-env]: /reference/configuration/environment-variables#cubejs_tesseract_sql_planner
\ No newline at end of file
diff --git a/docs-mintlify/reference/data-modeling/measures.mdx b/docs-mintlify/reference/data-modeling/measures.mdx
index 3c5da051b5e57..686f4d50e23f3 100644
--- a/docs-mintlify/reference/data-modeling/measures.mdx
+++ b/docs-mintlify/reference/data-modeling/measures.mdx
@@ -722,13 +722,6 @@ This is commonly used for "share of total" calculations where the denominator
must ignore a filter applied by the query — for example, computing each status's
share of the per-category total while the query is filtered to a single status.
-
-
-The `filter` parameter requires the [Tesseract SQL planner][ref-tesseract-env]
-(`CUBEJS_TESSERACT_SQL_PLANNER=true`).
-
-
-
```yaml title="YAML"
@@ -1498,7 +1491,6 @@ cube(`orders`, {
[ref-nested-aggregate]: /docs/data-modeling/measures#nested-aggregates
[ref-calendar-cubes]: /docs/data-modeling/concepts/calendar-cubes
[ref-switch-dimensions]: /reference/data-modeling/dimensions#type
-[ref-tesseract-env]: /reference/configuration/environment-variables#cubejs_tesseract_sql_planner
[ref-filters-query]: /reference/core-data-apis/rest-api/query-format#filters-format
[ref-data-masking]: /docs/data-modeling/data-access-policies#data-masking
[link-d3-format]: https://d3js.org/d3-format
diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
index 7f97165002ea6..fbc4aae41d0f9 100644
--- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
+++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
@@ -1016,6 +1016,7 @@ export class BaseQuery {
preAggregationsMatchOnly: true,
preAggregationId: this.options.preAggregationId || null,
securityContext: this.contextSymbols.securityContext,
+ joinHints: this.options.joinHints,
cubestoreSupportMultistage: this.options.cubestoreSupportMultistage ?? getEnv('cubeStoreRollingWindowJoin'),
disableExternalPreAggregations: !!this.options.disableExternalPreAggregations,
subqueryJoins: this.options.subqueryJoins,
@@ -3366,7 +3367,12 @@ export class BaseQuery {
}
}
const primaryKeys = this.cubeEvaluator.primaryKeys[cubeName];
- const orderBySql = (symbol.orderBy || []).map(o => ({ sql: this.evaluateSql(cubeName, o.sql), dir: o.dir }));
+ // order_by templates reference members of the measure's owning cube. When
+ // the measure is exposed through a view, those members may be absent or
+ // exposed only under an alias, so resolve them against the owning cube
+ // (from aliasMember, the underlying reference) instead of the view.
+ const orderByCubeName = symbol.aliasMember ? symbol.aliasMember.split('.')[0] : cubeName;
+ const orderBySql = (symbol.orderBy || []).map(o => ({ sql: this.evaluateSql(orderByCubeName, o.sql), dir: o.dir }));
let sql;
let patchedSymbol = symbol;
if (symbol.type !== 'rank') {
diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/multi-stage-order-by-view.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/multi-stage-order-by-view.test.ts
index cd9eba2e817c3..242e98185e602 100644
--- a/packages/cubejs-schema-compiler/test/integration/postgres/multi-stage-order-by-view.test.ts
+++ b/packages/cubejs-schema-compiler/test/integration/postgres/multi-stage-order-by-view.test.ts
@@ -73,6 +73,17 @@ views:
- created_at
- volume_by_day_rank
# num_parcels deliberately NOT exposed
+
+ - name: orders_view_alias
+ cubes:
+ - join_path: orders
+ includes:
+ - created_at
+ - volume_by_day_rank
+ # num_parcels exposed, but only under an alias, so the order_by
+ # template's bare {num_parcels} still does not resolve in the view.
+ - name: num_parcels
+ alias: total_parcels
`);
if (getEnv('nativeSqlPlanner')) {
@@ -102,9 +113,24 @@ views:
{ orders_view__created_at_day: '2024-01-01T00:00:00.000Z', orders_view__volume_by_day_rank: '2' },
{ orders_view__created_at_day: '2024-01-02T00:00:00.000Z', orders_view__volume_by_day_rank: '1' },
], { joinGraph, cubeEvaluator, compiler }));
+
+ // Repro: the same measure through a view that exposes num_parcels only under
+ // an alias (the order_by template still references it by its bare name).
+ it('view: rank order_by references a member exposed only under an alias', async () => dbRunner.runQueryTest({
+ measures: ['orders_view_alias.volume_by_day_rank'],
+ timeDimensions: [
+ { dimension: 'orders_view_alias.created_at', granularity: 'day' }
+ ],
+ order: [{ id: 'orders_view_alias.created_at' }],
+ timezone: 'UTC',
+ }, [
+ { orders_view_alias__created_at_day: '2024-01-01T00:00:00.000Z', orders_view_alias__volume_by_day_rank: '2' },
+ { orders_view_alias__created_at_day: '2024-01-02T00:00:00.000Z', orders_view_alias__volume_by_day_rank: '1' },
+ ], { joinGraph, cubeEvaluator, compiler }));
} else {
// Multi-stage rank is Tesseract-only.
test.skip('base cube: rank order_by references a non-selected member', () => { expect(1).toBe(1); });
test.skip('view: rank order_by references a member not exposed by the view', () => { expect(1).toBe(1); });
+ test.skip('view: rank order_by references a member exposed only under an alias', () => { expect(1).toBe(1); });
}
});
diff --git a/packages/cubejs-schema-compiler/test/unit/multi-stage-order-by-view.test.ts b/packages/cubejs-schema-compiler/test/unit/multi-stage-order-by-view.test.ts
new file mode 100644
index 0000000000000..42cd7a9350322
--- /dev/null
+++ b/packages/cubejs-schema-compiler/test/unit/multi-stage-order-by-view.test.ts
@@ -0,0 +1,77 @@
+import { PostgresQuery } from '../../src/adapter/PostgresQuery';
+import { prepareYamlCompiler } from './PrepareCompiler';
+
+// A `rank` multi-stage measure whose `order_by` template references another
+// measure by its bare name (`{num_parcels}`) must resolve that reference against
+// the measure's owning cube, not against a view exposing the rank. Otherwise the
+// order_by template is evaluated in the view's context during the shared JS
+// member-collection pass and crashes with
+// `Cannot read properties of undefined (reading '_objectWithResolvedProperties')`
+// (the referenced member is absent from the view namespace, or present only under
+// an alias).
+describe('Multi-stage measure order_by through a view', () => {
+ const model = (viewIncludes: string) => `
+cubes:
+ - name: orders
+ sql: "SELECT 1 AS id, 10 AS parcels, '2024-01-01'::timestamptz AS created_at"
+ dimensions:
+ - name: id
+ sql: id
+ type: number
+ primary_key: true
+ - name: created_at
+ sql: created_at
+ type: time
+ measures:
+ - name: num_parcels
+ type: sum
+ sql: parcels
+ - name: volume_by_day_rank
+ type: rank
+ multi_stage: true
+ reduce_by:
+ - created_at
+ order_by:
+ - sql: "{num_parcels}"
+ dir: desc
+
+views:
+ - name: orders_view
+ cubes:
+ - join_path: orders
+ includes:
+${viewIncludes}
+ `;
+
+ const buildViewRankQuery = async (viewIncludes: string) => {
+ const { compiler, joinGraph, cubeEvaluator } = prepareYamlCompiler(model(viewIncludes));
+ await compiler.compile();
+ const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
+ measures: ['orders_view.volume_by_day_rank'],
+ timeDimensions: [{ dimension: 'orders_view.created_at', granularity: 'day' }],
+ order: [{ id: 'orders_view.created_at' }],
+ timezone: 'UTC',
+ // The crash is in the shared JS member-collection pass (query construction),
+ // which runs under both planners; pin the planner so the asserted SQL is
+ // deterministic and does not require the native addon to be built.
+ useNativeSqlPlanner: false,
+ });
+ return query.buildSqlAndParams();
+ };
+
+ it('resolves order_by against the owning cube when the member is not exposed by the view', async () => {
+ const [sql] = await buildViewRankQuery(
+ ' - created_at\n - volume_by_day_rank'
+ );
+ expect(sql).toContain('rank() OVER (');
+ expect(sql).toContain('"orders__num_parcels" desc');
+ });
+
+ it('resolves order_by against the owning cube when the member is exposed under an alias', async () => {
+ const [sql] = await buildViewRankQuery(
+ ' - created_at\n - volume_by_day_rank\n - name: num_parcels\n alias: total_parcels'
+ );
+ expect(sql).toContain('rank() OVER (');
+ expect(sql).toContain('"orders__num_parcels" desc');
+ });
+});
diff --git a/packages/cubejs-schema-compiler/test/unit/pre-aggregations.test.ts b/packages/cubejs-schema-compiler/test/unit/pre-aggregations.test.ts
index eb60b07785c2b..25d3a02d8f00b 100644
--- a/packages/cubejs-schema-compiler/test/unit/pre-aggregations.test.ts
+++ b/packages/cubejs-schema-compiler/test/unit/pre-aggregations.test.ts
@@ -1058,4 +1058,101 @@ describe('pre-aggregations', () => {
});
});
});
+
+ // Regression: the SQL API sends query-level joinHints when the queried members alone
+ // don't determine the join root (e.g. dimensions from `products` and a filter on
+ // `customers`, both reachable only through `orders`). Native pre-aggregation matching
+ // re-plans the query and must receive those hints too — without them it fails with
+ // "Can't find join path to join 'products', 'customers'".
+ describe('pre-aggregation matching with query-level join hints', () => {
+ const { compiler, joinGraph, cubeEvaluator } = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`SELECT * FROM orders\`,
+
+ joins: {
+ customers: {
+ relationship: \`many_to_one\`,
+ sql: \`\${CUBE.customerId} = \${customers.id}\`,
+ },
+ products: {
+ relationship: \`many_to_one\`,
+ sql: \`\${CUBE.productId} = \${products.id}\`,
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: \`id\`,
+ type: \`number\`,
+ primaryKey: true,
+ },
+ customerId: {
+ sql: \`customer_id\`,
+ type: \`number\`,
+ },
+ productId: {
+ sql: \`product_id\`,
+ type: \`number\`,
+ },
+ },
+ });
+
+ cube(\`customers\`, {
+ sql: \`SELECT * FROM customers\`,
+
+ dimensions: {
+ id: {
+ sql: \`id\`,
+ type: \`number\`,
+ primaryKey: true,
+ },
+ state: {
+ sql: \`state\`,
+ type: \`string\`,
+ },
+ },
+ });
+
+ cube(\`products\`, {
+ sql: \`SELECT * FROM products\`,
+
+ dimensions: {
+ id: {
+ sql: \`id\`,
+ type: \`number\`,
+ primaryKey: true,
+ },
+ name: {
+ sql: \`name\`,
+ type: \`string\`,
+ },
+ },
+ });
+ `);
+
+ beforeAll(async () => {
+ await compiler.compile();
+ });
+
+ it('matching does not lose query-level join hints', async () => {
+ const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
+ measures: [],
+ dimensions: ['products.name'],
+ filters: [{
+ member: 'customers.state',
+ operator: 'set',
+ }],
+ joinHints: [
+ ['orders', 'products'],
+ ['orders', 'customers'],
+ ],
+ timezone: 'UTC',
+ preAggregationsSchema: '',
+ });
+
+ // Must not throw while determining the matching pre-aggregation.
+ expect(query.preAggregations?.preAggregationsDescription()).toEqual([]);
+ expect(query.buildSqlAndParams()[0]).toMatch(/orders/);
+ });
+ });
});
diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/params_allocator.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/params_allocator.rs
index bccd0f50439fc..5401a4c0f7ead 100644
--- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/params_allocator.rs
+++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/params_allocator.rs
@@ -116,12 +116,19 @@ impl ParamsAllocator {
Ok((sql.to_string(), self.params.clone()))
} else {
let mut result_params = self.params.clone();
+ // Occurrences of the same native param index must map to the same
+ // internal param: databases like Postgres require expressions in
+ // GROUP BY and window PARTITION BY/ORDER BY to be strictly equal,
+ // and that includes param placeholders
+ let mut native_index_map: HashMap = HashMap::new();
let sql = NATIVE_PARAMS_MATCH_RE
.replace_all(sql, |caps: &Captures| {
let ind: usize = caps[1].to_string().parse().unwrap();
- let param = native_allocated_params[ind].clone();
- result_params.push(param);
- self.make_placeholder(result_params.len() - 1)
+ let internal_ind = *native_index_map.entry(ind).or_insert_with(|| {
+ result_params.push(native_allocated_params[ind].clone());
+ result_params.len() - 1
+ });
+ self.make_placeholder(internal_ind)
})
.to_string();
Ok((sql, result_params))
@@ -184,4 +191,35 @@ mod tests {
]
);
}
+
+ #[test]
+ fn repeated_native_param_maps_to_single_internal_param() {
+ let allocator = ParamsAllocator::new(false);
+
+ // The same native param referenced from several member expressions
+ // (e.g. a dimension and window function PARTITION BY inside a measure)
+ // must stay a single param, otherwise Postgres rejects the query with
+ // "must appear in the GROUP BY clause" as the expressions stop being equal.
+ let sql = "SELECT TO_CHAR(d, $0$) x, LAG(m) OVER (PARTITION BY TO_CHAR(d, $0$)), $1$";
+ let native = vec![
+ FilterValue::Str("month".to_string()),
+ FilterValue::Str("other".to_string()),
+ ];
+
+ let (rewritten_sql, params) = allocator
+ .add_native_allocated_params(sql, &native)
+ .expect("merge should succeed");
+
+ assert_eq!(
+ rewritten_sql,
+ "SELECT TO_CHAR(d, $_0_$) x, LAG(m) OVER (PARTITION BY TO_CHAR(d, $_0_$)), $_1_$"
+ );
+ assert_eq!(
+ params,
+ vec![
+ FilterValue::Str("month".to_string()),
+ FilterValue::Str("other".to_string()),
+ ]
+ );
+ }
}