Skip to content

Commit 3671fb6

Browse files
committed
test(tesseract): e2e Postgres tests for view default value filters
New file scaffolded on calendars.test.ts: a single inline YAML model with both a real string dimension (`country`) and a virtual `type: switch` dimension (`currency`), each exposed through two views — `_unconditional` and `_with_unless`. Tests run through the full JS pipeline (Joi schema, YAML transpiler, CubeEvaluator.prepareViewFilters, BaseQuery, Tesseract planner) against testcontainers Postgres, gated on `nativeSqlPlanner`. Six cases — three per filter flavour: default applies without `unless`, projection alone does not release the default, an explicit filter on the unless-member overrides the default. YamlCompiler fix in the same commit: `filters.[N].member` (string) and `filters.[N].unless` (array) are now wrapped as f-string literals, like `values`. They are member references in the view's own namespace, not Python expressions — and the view's `includedMembers` are not resolvable at transpile time, so the previous default path treated names like `country` as undefined identifiers and blew up at runtime with `country is not defined`.
1 parent 25b16fa commit 3671fb6

2 files changed

Lines changed: 248 additions & 1 deletion

File tree

packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,27 @@ export class YamlCompiler {
187187
for (const p of transpiledFieldsPatterns) {
188188
const fullPath = propertyPath.join('.');
189189
if (fullPath.match(p)) {
190+
// View default filter `member` / `unless` are member references in
191+
// the view's own namespace — not Python expressions — so they go
192+
// through the same f-string path as `values`. The view's
193+
// `includedMembers` are not resolvable at transpile time, so
194+
// running them through the Python parser would treat the name
195+
// as an undefined identifier.
196+
const isViewFilterMember = /^filters\.\d+\.member$/.test(fullPath);
197+
const isViewFilterUnless = /^filters\.\d+\.unless$/.test(fullPath);
190198
if (typeof obj === 'string' && ['sql', 'sqlTable'].includes(propertyPath[propertyPath.length - 1])) {
191199
return this.parsePythonIntoArrowFunction(`f"${this.escapeDoubleQuotes(obj)}"`, cubeName, obj, errorsReport);
200+
} else if (typeof obj === 'string' && isViewFilterMember) {
201+
return this.parsePythonIntoArrowFunction(`f"${this.escapeDoubleQuotes(obj)}"`, cubeName, obj, errorsReport);
192202
} else if (typeof obj === 'string') {
193203
return this.parsePythonIntoArrowFunction(obj, cubeName, obj, errorsReport);
194204
} else if (Array.isArray(obj)) {
205+
const treatAsLiteral =
206+
propertyPath[propertyPath.length - 1] === 'values' || isViewFilterUnless;
195207
const resultAst = t.program([t.expressionStatement(t.arrayExpression(obj.map(code => {
196208
let ast: t.Program | t.NullLiteral | t.BooleanLiteral | t.NumericLiteral | null = null;
197209
// Special case for accessPolicy.rowLevel.filter.values and other values-like fields
198-
if (propertyPath[propertyPath.length - 1] === 'values') {
210+
if (treatAsLiteral) {
199211
if (typeof code === 'string') {
200212
ast = this.parsePythonAndTranspileToJs(`f"${this.escapeDoubleQuotes(code)}"`, errorsReport);
201213
} else if (typeof code === 'boolean') {
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { getEnv } from '@cubejs-backend/shared';
2+
import { PostgresQuery } from '../../../src/adapter';
3+
import { prepareYamlCompiler } from '../../unit/PrepareCompiler';
4+
import { dbRunner } from './PostgresDBRunner';
5+
6+
describe('View default value filters', () => {
7+
jest.setTimeout(200000);
8+
9+
// Two flavours of default filter live side-by-side:
10+
//
11+
// * `orders_view_*_real` — `country` is a real string dimension, the
12+
// default filter rewrites the WHERE clause.
13+
// * `orders_view_*_switch` — `currency` is a virtual `type: switch`
14+
// dimension; the default filter pins the switch union to one branch.
15+
//
16+
// Each cube exposes both an `_unconditional` view (no `unless`) and a
17+
// `_with_unless` view (`unless: [<member>]`). The seed has five rows with
18+
// mixed `country` so we can spot bugs by row count alone.
19+
//
20+
// language=YAML
21+
const { compiler, joinGraph, cubeEvaluator } = prepareYamlCompiler(`
22+
cubes:
23+
- name: orders
24+
sql: >
25+
SELECT * FROM (VALUES
26+
(1, 'US', 100, 92),
27+
(2, 'CA', 50, 46),
28+
(3, 'DE', 80, 75),
29+
(4, 'FR', 30, 28),
30+
(5, 'GB', 60, 56)
31+
) AS t(id, country, amount_usd, amount_eur)
32+
33+
dimensions:
34+
- name: id
35+
sql: id
36+
type: number
37+
primary_key: true
38+
public: true
39+
40+
- name: country
41+
sql: country
42+
type: string
43+
44+
- name: currency
45+
type: switch
46+
values:
47+
- USD
48+
- EUR
49+
- GBP
50+
51+
measures:
52+
- name: count
53+
type: count
54+
55+
- name: total_amount_usd
56+
type: sum
57+
sql: amount_usd
58+
59+
- name: total_amount_eur
60+
type: sum
61+
sql: amount_eur
62+
63+
views:
64+
- name: orders_view_real_unconditional
65+
cubes:
66+
- join_path: orders
67+
includes: "*"
68+
filters:
69+
- member: country
70+
operator: equals
71+
values:
72+
- US
73+
74+
- name: orders_view_real_with_unless
75+
cubes:
76+
- join_path: orders
77+
includes: "*"
78+
filters:
79+
- member: country
80+
operator: equals
81+
values:
82+
- US
83+
unless:
84+
- country
85+
86+
- name: orders_view_switch_unconditional
87+
cubes:
88+
- join_path: orders
89+
includes: "*"
90+
filters:
91+
- member: currency
92+
operator: equals
93+
values:
94+
- USD
95+
96+
- name: orders_view_switch_with_unless
97+
cubes:
98+
- join_path: orders
99+
includes: "*"
100+
filters:
101+
- member: currency
102+
operator: equals
103+
values:
104+
- USD
105+
unless:
106+
- currency
107+
`);
108+
109+
async function runQueryTest(q: any, expectedResult: any) {
110+
// Default value filters are wired only through the Tesseract planner.
111+
if (!getEnv('nativeSqlPlanner')) {
112+
return;
113+
}
114+
115+
await compiler.compile();
116+
const query = new PostgresQuery(
117+
{ joinGraph, cubeEvaluator, compiler },
118+
{ ...q, timezone: 'UTC', preAggregationsSchema: '' }
119+
);
120+
121+
const qp = query.buildSqlAndParams();
122+
const res = await dbRunner.testQuery(qp);
123+
124+
expect(res).toEqual(expectedResult);
125+
}
126+
127+
describe('Real dimension default filter', () => {
128+
// Default filter pins `country = US`. Five rows in the cube, one with
129+
// country='US' — `count` must be 1.
130+
it('applies when no `unless` and no relevant projection', async () => runQueryTest(
131+
{
132+
measures: ['orders_view_real_unconditional.count'],
133+
},
134+
[
135+
{
136+
orders_view_real_unconditional__count: '1',
137+
},
138+
]
139+
));
140+
141+
// Projection adds `country` to the SELECT but does NOT release the
142+
// default — only the US row survives.
143+
it('keeps applying when `unless: [country]` and country is only in projection', async () => runQueryTest(
144+
{
145+
measures: ['orders_view_real_with_unless.count'],
146+
dimensions: ['orders_view_real_with_unless.country'],
147+
order: [{ id: 'orders_view_real_with_unless.country' }],
148+
},
149+
[
150+
{
151+
orders_view_real_with_unless__country: 'US',
152+
orders_view_real_with_unless__count: '1',
153+
},
154+
]
155+
));
156+
157+
// Explicit filter on `country` releases the default — only the user's
158+
// FR row remains.
159+
it('is released when `unless: [country]` and explicit filter on country', async () => runQueryTest(
160+
{
161+
measures: ['orders_view_real_with_unless.count'],
162+
filters: [
163+
{
164+
member: 'orders_view_real_with_unless.country',
165+
operator: 'equals',
166+
values: ['FR'],
167+
},
168+
],
169+
},
170+
[
171+
{
172+
orders_view_real_with_unless__count: '1',
173+
},
174+
]
175+
));
176+
});
177+
178+
describe('Virtual switch dimension default filter', () => {
179+
// No filter at all would unfold five base rows × three switch values
180+
// = 15 cells. The default `currency = USD` pins the union to the USD
181+
// branch, leaving five rows-as-cells, so count=5.
182+
it('collapses the switch union when no `unless`', async () => runQueryTest(
183+
{
184+
measures: ['orders_view_switch_unconditional.count'],
185+
dimensions: ['orders_view_switch_unconditional.currency'],
186+
order: [{ id: 'orders_view_switch_unconditional.currency' }],
187+
},
188+
[
189+
{
190+
orders_view_switch_unconditional__currency: 'USD',
191+
orders_view_switch_unconditional__count: '5',
192+
},
193+
]
194+
));
195+
196+
// Projection of `currency` does not release the default with
197+
// `unless: [currency]`: union is still pinned to USD only.
198+
it('keeps the union pinned when `unless: [currency]` and currency is only in projection', async () => runQueryTest(
199+
{
200+
measures: ['orders_view_switch_with_unless.count'],
201+
dimensions: ['orders_view_switch_with_unless.currency'],
202+
order: [{ id: 'orders_view_switch_with_unless.currency' }],
203+
},
204+
[
205+
{
206+
orders_view_switch_with_unless__currency: 'USD',
207+
orders_view_switch_with_unless__count: '5',
208+
},
209+
]
210+
));
211+
212+
// Explicit filter `currency = EUR` releases the default and replaces
213+
// it: only the EUR branch survives.
214+
it('is released when `unless: [currency]` and explicit filter on currency', async () => runQueryTest(
215+
{
216+
measures: ['orders_view_switch_with_unless.count'],
217+
dimensions: ['orders_view_switch_with_unless.currency'],
218+
filters: [
219+
{
220+
member: 'orders_view_switch_with_unless.currency',
221+
operator: 'equals',
222+
values: ['EUR'],
223+
},
224+
],
225+
order: [{ id: 'orders_view_switch_with_unless.currency' }],
226+
},
227+
[
228+
{
229+
orders_view_switch_with_unless__currency: 'EUR',
230+
orders_view_switch_with_unless__count: '5',
231+
},
232+
]
233+
));
234+
});
235+
});

0 commit comments

Comments
 (0)