Skip to content

Commit 0a8cf07

Browse files
fix(schema-compiler): Allow escaping of curly braces in yaml models (#9469)
* fix(schema-compiler): Allow escaping of curly braces in yaml models * add tests * remove obsolete * Add docs --------- Co-authored-by: Igor Lukanin <[email protected]>
1 parent e31640d commit 0a8cf07

File tree

4 files changed

+303
-94
lines changed

4 files changed

+303
-94
lines changed

docs/pages/product/data-modeling/syntax.mdx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,73 @@ the [template context](/reference/python/cube#templatecontext-class).
602602

603603
</ReferenceBox>
604604

605+
### Curly braces and escaping
606+
607+
As you can see in the examples above, within [SQL expressions][self-sql-expressions],
608+
curly braces are used to reference cubes and members.
609+
610+
In YAML data models, use `{reference}`:
611+
612+
```yaml
613+
cubes:
614+
- name: orders
615+
sql: >
616+
SELECT id, created_at
617+
FROM {other_cube.sql()}
618+
619+
dimensions:
620+
- name: status
621+
sql: status
622+
type: string
623+
624+
- name: status_x2
625+
sql: "{status} || ' ' || {status}"
626+
type: string
627+
```
628+
629+
In JavaScript data models, use `${reference}` in [JavaScript template
630+
literals][link-js-template-literals] (mind the dollar sign):
631+
632+
```javascript
633+
cube(`orders`, {
634+
sql: `
635+
SELECT id, created_at
636+
FROM ${other_cube.sql()}
637+
`,
638+
639+
dimensions: {
640+
status: {
641+
sql: `status`,
642+
type: `string`
643+
},
644+
645+
status_x2: {
646+
sql: `${status} || ' ' || ${status}`,
647+
type: `string`
648+
}
649+
}
650+
})
651+
```
652+
653+
If you need to use literal, non-referential curly braces in YAML, e.g.,
654+
to define a JSON object, you can escape them with a backslash:
655+
656+
```yaml
657+
cubes:
658+
- name: json_object_in_postgres
659+
sql: SELECT CAST('\{"key":"value"\}'::JSON AS TEXT) AS json_column
660+
661+
- name: csv_from_s3_in_duckdb
662+
sql: >
663+
SELECT *
664+
FROM read_csv(
665+
's3://bbb/aaa.csv',
666+
delim = ',',
667+
header = true,
668+
columns=\{'time':'DATE','count':'NUMERIC'\}
669+
)
670+
```
671+
605672
### Non-SQL references
606673
607674
Outside [SQL expressions][self-sql-expressions], `column` is not recognized
@@ -693,4 +760,5 @@ defining dynamic data models.
693760
[ref-custom-granularities]: /reference/data-model/dimensions#granularities
694761
[ref-style-guide]: /guides/style-guide
695762
[ref-polymorphism]: /product/data-modeling/concepts/polymorphic-cubes
696-
[ref-data-blending]: /product/data-modeling/concepts/data-blending
763+
[ref-data-blending]: /product/data-modeling/concepts/data-blending
764+
[link-js-template-literals]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Strings#embedding_javascript

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,11 @@ export class YamlCompiler {
230230
} else if (str[i] === '`' && peek().inStr) {
231231
result.push(str[i]);
232232
stateStack.pop();
233-
} else if (str[i] === '{' && str[i + 1] === '{' && peek()?.inFormattedStr) {
234-
result.push('{{');
233+
} else if (str[i] === '\\' && str[i + 1] === '{' && stateStack.length === 0) {
234+
result.push('\\{');
235235
i += 1;
236-
} else if (str[i] === '}' && str[i + 1] === '}' && peek()?.inFormattedStr) {
237-
result.push('}}');
236+
} else if (str[i] === '\\' && str[i + 1] === '}' && stateStack.length === 0) {
237+
result.push('\\}');
238238
i += 1;
239239
} else if (str[i] === '{' && peek()?.inFormattedStr) {
240240
result.push(str[i]);

packages/cubejs-schema-compiler/test/integration/postgres/yaml-compiler.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,40 @@ cubes:
5757
);
5858
});
5959

60+
it('simple with json/curly in sql', async () => {
61+
const { compiler, joinGraph, cubeEvaluator } = prepareYamlCompiler(`
62+
cubes:
63+
- name: ActiveUsers
64+
sql: SELECT 1 as user_id, '2022-01-01'::TIMESTAMP as timestamp, CAST('\\{"key":"value"\\}'::JSON AS TEXT) AS json_col
65+
66+
dimensions:
67+
- name: time
68+
sql: "{CUBE}.timestamp"
69+
type: time
70+
- name: json_col
71+
sql: json_col
72+
type: string
73+
`);
74+
await compiler.compile();
75+
76+
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
77+
dimensions: ['ActiveUsers.time', 'ActiveUsers.json_col'],
78+
timezone: 'UTC'
79+
});
80+
81+
console.log(query.buildSqlAndParams());
82+
83+
const res = await dbRunner.testQuery(query.buildSqlAndParams());
84+
console.log(JSON.stringify(res));
85+
86+
expect(res).toEqual(
87+
[{
88+
active_users__time: '2022-01-01T00:00:00.000Z',
89+
active_users__json_col: '{"key":"value"}',
90+
}]
91+
);
92+
});
93+
6094
it('missed sql', async () => {
6195
const { compiler } = prepareYamlCompiler(`
6296
cubes:

0 commit comments

Comments
 (0)