Skip to content

Commit 8e7c5c7

Browse files
authored
fix(schema-compiler): fix Maximum call stack size exceeded if FILTER_PARAMS are used inside dimensions/measures (#8867)
* fix(schema-compiler): fix Maximum call stack size exceeded if FILTER_PARAMS are used inside dimensions/measures * add tests * move guard to the filter_params proxy
1 parent 211d1c1 commit 8e7c5c7

File tree

2 files changed

+91
-12
lines changed

2 files changed

+91
-12
lines changed

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2240,6 +2240,7 @@ export class BaseQuery {
22402240
this.safeEvaluateSymbolContext().memberChildren[parentMember].push(memberPath);
22412241
}
22422242
}
2243+
22432244
this.safeEvaluateSymbolContext().currentMember = memberPath;
22442245
try {
22452246
if (type === 'measure') {
@@ -3882,9 +3883,16 @@ export class BaseQuery {
38823883
// collectFrom() -> traverseSymbol() -> evaluateSymbolSql() ->
38833884
// evaluateSql() -> resolveSymbolsCall() -> cubeReferenceProxy->toString() ->
38843885
// evaluateSymbolSql() -> evaluateSql()... -> and got here again
3886+
//
3887+
// When FILTER_PARAMS is used in dimension/measure SQL - we also hit recursive loop:
3888+
// allBackAliasMembersExceptSegments() -> collectFrom() -> traverseSymbol() -> evaluateSymbolSql() ->
3889+
// autoPrefixAndEvaluateSql() -> evaluateSql() -> filterProxyFromAllFilters->Proxy->toString()
3890+
// and so on...
3891+
// For this case aliasGathering flag is added to the context in first iteration and
3892+
// is checked below to prevent looping.
38853893
const aliases = allFilters ?
38863894
allFilters
3887-
.map(v => (v.query ? v.query.allBackAliasMembersExceptSegments() : {}))
3895+
.map(v => (v.query && !v.query.safeEvaluateSymbolContext().aliasGathering ? v.query.allBackAliasMembersExceptSegments() : {}))
38883896
.reduce((a, b) => ({ ...a, ...b }), {})
38893897
: {};
38903898
// Filtering aliases that somehow relate to this group member
@@ -3932,8 +3940,10 @@ export class BaseQuery {
39323940
const query = this;
39333941
return members.map(
39343942
member => {
3935-
const collectedMembers = query
3936-
.collectFrom([member], query.collectMemberNamesFor.bind(query), 'collectMemberNamesFor');
3943+
const collectedMembers = query.evaluateSymbolSqlWithContext(
3944+
() => query.collectFrom([member], query.collectMemberNamesFor.bind(query), 'collectMemberNamesFor'),
3945+
{ aliasGathering: true }
3946+
);
39373947
const memberPath = member.expressionPath();
39383948
let nonAliasSeen = false;
39393949
return collectedMembers

packages/cubejs-schema-compiler/test/unit/base-query.test.ts

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,15 +1586,37 @@ describe('SQL Generation', () => {
15861586
cubes: [{
15871587
name: 'Order',
15881588
sql: 'select * from order where {FILTER_PARAMS.Order.type.filter(\'type\')}',
1589-
measures: [{
1590-
name: 'count',
1591-
type: 'count',
1592-
}],
1593-
dimensions: [{
1594-
name: 'type',
1595-
sql: 'type',
1596-
type: 'string'
1597-
}]
1589+
measures: [
1590+
{
1591+
name: 'count',
1592+
type: 'count',
1593+
},
1594+
{
1595+
name: 'avg_filtered',
1596+
sql: 'product_id',
1597+
type: 'avg',
1598+
filters: [
1599+
{ sql: `{FILTER_PARAMS.Order.category.filter('category')}` }
1600+
]
1601+
}
1602+
],
1603+
dimensions: [
1604+
{
1605+
name: 'type',
1606+
sql: 'type',
1607+
type: 'string'
1608+
},
1609+
{
1610+
name: 'category',
1611+
sql: 'category',
1612+
type: 'string'
1613+
},
1614+
{
1615+
name: 'proxied',
1616+
sql: `{FILTER_PARAMS.Order.type.filter("x => type = 'online'")}`,
1617+
type: 'boolean',
1618+
}
1619+
]
15981620
}],
15991621
views: [{
16001622
name: 'orders_view',
@@ -1829,6 +1851,53 @@ describe('SQL Generation', () => {
18291851
const queryString = queryAndParams[0];
18301852
expect(/select\s+\*\s+from\s+order\s+where\s+\(\(type\s=\s\?\)\)/.test(queryString)).toBeTruthy();
18311853
});
1854+
1855+
it('correctly substitute filter params in cube\'s query dimension used in filter', async () => {
1856+
await compilers.compiler.compile();
1857+
const query = new BaseQuery(compilers, {
1858+
measures: ['Order.count'],
1859+
dimensions: ['Order.proxied'],
1860+
filters: [
1861+
{
1862+
member: 'Order.proxied',
1863+
operator: 'equals',
1864+
values: [true],
1865+
},
1866+
],
1867+
});
1868+
const queryAndParams = query.buildSqlAndParams();
1869+
const queryString = queryAndParams[0];
1870+
expect(queryString).toContain(`SELECT
1871+
(1 = 1) "order__proxied", count(*) "order__count"
1872+
FROM
1873+
(select * from order where (1 = 1)) AS "order" WHERE ((1 = 1) = ?)`);
1874+
});
1875+
1876+
it('correctly substitute filter params in cube\'s query measure used in filter', async () => {
1877+
await compilers.compiler.compile();
1878+
const query = new BaseQuery(compilers, {
1879+
measures: ['Order.avg_filtered'],
1880+
dimensions: ['Order.type'],
1881+
filters: [
1882+
{
1883+
member: 'Order.type',
1884+
operator: 'equals',
1885+
values: ['online'],
1886+
},
1887+
{
1888+
member: 'Order.category',
1889+
operator: 'equals',
1890+
values: ['category'],
1891+
},
1892+
],
1893+
});
1894+
const queryAndParams = query.buildSqlAndParams();
1895+
const queryString = queryAndParams[0];
1896+
expect(queryString).toContain(`SELECT
1897+
"order".type "order__type", avg(CASE WHEN ((category = ?)) THEN "order".product_id END) "order__avg_filtered"
1898+
FROM
1899+
(select * from order where (type = ?)) AS "order" WHERE ("order".type = ?) AND ("order".category = ?)`);
1900+
});
18321901
});
18331902

18341903
describe('FILTER_GROUP', () => {

0 commit comments

Comments
 (0)