Skip to content

Commit 159c302

Browse files
Expand Cube adapter coverage: extends, views, hierarchies, meta, export fixes (#110)
* Expand Cube adapter coverage: extends, views, hierarchies, meta, and export fixes Wire up existing core model fields (extends, meta, title, shown, drill_members) that were already defined but not read during Cube YAML parsing. Add case dimension to SQL CASE WHEN conversion, hierarchy to Dimension.parent chain parsing, rolling_window.type:to_date support, time_shift to time_comparison mapping, and type:rank handling. Parse views section into composite Models with join_path/includes/excludes/prefix/alias resolution. Fix export: proper ${CUBE} join SQL format, all relationship types, pre-aggregation export, and meta/title/ shown roundtrip. Add public field to Dimension and Metric, fix inheritance merge to include pre_aggregations. * Auto-update JSON schema * Fix join export to use resolved models and escape CASE labels Use inheritance-resolved models for join target lookup during export so inherited primary keys are picked up correctly. Escape single quotes in CASE dimension labels to prevent SQL injection/syntax errors. * Fix join export SQL format and rank measure self-reference Export join SQL using ${CUBE}.col syntax (not ${model_name}.col) so the importer's FK extraction regex can re-parse it on roundtrip. Remove self-referential SQL fallback for rank measures: store rank metadata only without fabricating a derived type that points to a nonexistent column. * Fix inherited primary key and time_shift base_metric guard Only include primary_key in Model kwargs when explicitly declared via primary_key:true on a dimension, so child cubes in an extends chain inherit the parent's key instead of silently defaulting to "id". Guard time_shift conversion: only set type=time_comparison when the base_metric reference can be parsed from sql. If sql is not a bare {metric_name} token, fall through to normal derived handling instead of raising a validation error. * Defer hierarchy application and fix time_shift export roundtrip Apply hierarchy parent chains after inheritance resolution so child cubes that reference parent-inherited dimensions get correct parent links. Previously hierarchies were applied during _parse_cube before inheritance, so inherited dimensions were invisible. Fix time_comparison export to emit ${metric} Cube references instead of qualified cube.metric names, preserving roundtrip fidelity. * Fix rank queryability, meta inheritance, and view prefix refs Give rank measures agg=count as executable fallback while preserving rank metadata (order_by, reduce_by) in meta for special handling. This prevents NotImplementedError when rank metrics are selected in queries. Add meta to the scalar override list in merge_model so child cube metadata is preserved through inheritance resolution. Update view prefix logic to rewrite dependent references (parent on dimensions, drill_fields on metrics) when prefixing member names. * Propagate parent hierarchies to extended cubes and fix alias refs Collect extends relationships during parse and use them to propagate parent cube hierarchies to child cubes after inheritance resolution. Previously, inherited dimensions from parent cubes had no parent links if the child didn't redeclare the hierarchy. Build alias map when processing view includes with {name, alias} and apply it to dependent references (parent on dimensions, drill_fields on metrics) so aliased members maintain valid internal references. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 9776a34 commit 159c302

8 files changed

Lines changed: 780 additions & 150 deletions

File tree

docs/compatibility/cube.md

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ The adapter only reads YAML. Cube's original JavaScript schema format (`cube.js`
2828
| `sql_table` | Supported (stored as `Model.table`) |
2929
| `sql` (inline query) | Supported (stored as `Model.sql`, `Model.table = None`) |
3030
| `description` | Supported |
31-
| `extends` | Partial support: the extending cube is parsed with only its own fields. Inherited fields from the base cube are not merged. Each cube in an extends chain is independent. |
32-
| `public` / `shown` | Unsupported |
31+
| `extends` | Supported (full inheritance resolution: child inherits all dimensions, metrics, relationships, segments, and pre-aggregations from parent, with child values taking precedence) |
32+
| `meta` | Supported (stored on `Model.meta`) |
33+
| `public` / `shown` | Unsupported (cube-level visibility) |
3334
| `data_source` | Unsupported |
3435
| `sql_alias` | Unsupported |
3536
| `refresh_key` (cube-level) | Unsupported |
@@ -55,15 +56,17 @@ Not mapped: `title`, `rewrite_queries`, `context_members`.
5556
| `sql: ${cube_name}.column` | Supported (cube-name references replaced with `{model}`) |
5657
| `primary_key: true` | Supported |
5758
| `description` | Supported |
59+
| `title` | Supported (stored as `Dimension.label`) |
5860
| `format` | Supported (stored on `Dimension.format`) |
61+
| `shown` / `public` | Supported (stored as `Dimension.public`) |
62+
| `meta` | Supported (stored on `Dimension.meta`) |
5963
| Cross-cube dimension references (`${other_cube.field}`) | Supported (preserved as-is in SQL) |
6064
| Path-qualified references (`{b.d.id}`) | Supported (preserved as-is in SQL) |
61-
| `case: { when: [...] }` (case dimensions) | Partial support: the `case` block is parsed by YAML without error but the case/when/else structure is not evaluated or stored. The dimension has no SQL expression. |
65+
| `case: { when: [...] }` (case dimensions) | Supported (converted to SQL `CASE WHEN ... THEN ... ELSE ... END` expression) |
66+
| Custom `granularities` on time dimensions | Supported (stored in `Dimension.supported_granularities`) |
6267
| `sub_query: true` | Unsupported (flag ignored, dimension SQL preserved verbatim) |
63-
| Custom `granularities` on time dimensions | Unsupported (parsed by YAML but not stored) |
64-
| `meta` | Unsupported |
6568

66-
Not mapped: `shown`, `title`, `propagate_filters_to_sub_query`, `case` labels.
69+
Not mapped: `propagate_filters_to_sub_query`.
6770

6871
---
6972

@@ -82,13 +85,17 @@ Not mapped: `shown`, `title`, `propagate_filters_to_sub_query`, `case` labels.
8285
| `sql: ${CUBE}.column` | Supported (normalized to `{model}` placeholder) |
8386
| `sql: ${dimension_ref}` (referencing a dimension) | Supported (preserved for resolution) |
8487
| `description` | Supported |
88+
| `title` | Supported (stored as `Metric.label`) |
8589
| `format` | Supported (stored on `Metric.format`) |
86-
| `type: rank` | Partial support: parses without error but rank semantics (`order_by`, `reduce_by`) are not stored. Becomes a regular measure with `agg_type=count` (default fallback). |
90+
| `shown` / `public` | Supported (stored as `Metric.public`) |
91+
| `meta` | Supported (stored on `Metric.meta`) |
92+
| `drill_members` | Supported (stored as `Metric.drill_fields`) |
93+
| `type: rank` | Partial support: stored as `type=derived` with rank semantics (`order_by`, `reduce_by`) preserved in `Metric.meta["cube_type"]`. Does not execute rank window function. |
8794
| `type: string` | Unsupported (no mapping; falls back to `count`) |
8895
| `type: boolean` | Unsupported (no mapping; falls back to `count`) |
8996
| `type: running_total` | Unsupported (no mapping) |
9097

91-
Not mapped: `shown`, `title`, `drill_members` (on import; used on export), `meta`, `drill_filters`.
98+
Not mapped: `drill_filters`.
9299

93100
---
94101

@@ -125,7 +132,7 @@ Derived measures are detected by `type: number` and handled with several strateg
125132
| `rolling_window: { trailing: "1 month" }` | Supported (trailing value stored in `Metric.window`) |
126133
| `rolling_window: { offset: end }` | Partial support: the `offset` value is not stored. Only `trailing` is captured. |
127134
| `rolling_window: { leading: "-1 month" }` | Partial support: the `leading` value is not stored. |
128-
| `rolling_window: { type: to_date, granularity: year }` | Partial support: treated as cumulative. The `type` and `granularity` sub-fields are not stored. |
135+
| `rolling_window: { type: to_date, granularity: year }` | Supported (maps to `Metric.grain_to_date`, e.g., `grain_to_date="year"` for YTD) |
129136

130137
---
131138

@@ -137,6 +144,7 @@ Derived measures are detected by `type: number` and handled with several strateg
137144
| `${CUBE}` / `{CUBE}` replacement in segment SQL | Supported |
138145
| `${cube_name}` replacement in segment SQL | Supported |
139146
| `description` | Supported |
147+
| `shown` / `public` | Supported (stored as `Segment.public`) |
140148
| Query-time segment application | Supported |
141149
| Segments without `sql:` | Supported (correctly skipped, not added) |
142150

@@ -191,24 +199,38 @@ Pre-aggregations are fully mapped to Sidemantic's `PreAggregation` model, includ
191199

192200
## Views
193201

194-
Unsupported. The `views:` top-level section in Cube YAML is silently ignored during parsing. Files that contain only views (no `cubes:` section) parse without error and produce an empty graph.
202+
Supported. The `views:` top-level section in Cube YAML is parsed after all cubes are loaded. Each view is resolved into a composite Model by projecting members from source cubes.
195203

196-
Cube views are a composition layer that project and rename members from cubes via `join_path`, `includes`, `excludes`, `prefix`, and `alias`. None of these concepts are mapped:
197-
198-
- `join_path` traversal
199-
- `includes: "*"` wildcard and selective includes
200-
- `excludes` list
201-
- `prefix: true` namespacing
202-
- `alias` renaming
203-
- View-level `access_policy`
204-
- `folders` (grouping members in views)
205-
- `extends` on views
204+
| Feature | Status |
205+
|---------|--------|
206+
| `join_path: cube_name` (single cube) | Supported (resolves to target cube by last segment of path) |
207+
| `join_path: cube_a.cube_b` (multi-level) | Supported (resolves to the last cube in the path) |
208+
| `includes: "*"` (wildcard) | Supported (imports all dimensions and metrics from source cube) |
209+
| `includes:` (selective list) | Supported (imports named dimensions and metrics) |
210+
| `includes:` with `{ name, alias }` | Supported (renames members via `alias`) |
211+
| `excludes:` list | Supported (removes named members from includes) |
212+
| `prefix: true` | Supported (prefixes member names with `{cube_name}_`) |
213+
| `alias` on cube entry | Supported (used as prefix when `prefix: true`) |
214+
| View-only files (no `cubes:` section) | Supported (views that reference cubes from other files resolve correctly when parsed from a directory) |
215+
| Empty view (no resolvable cubes) | Supported (silently skipped, no model created) |
216+
| `extends` on views | Unsupported |
217+
| `folders` | Unsupported (UI grouping concept) |
218+
| View-level `access_policy` | Unsupported |
219+
220+
View models are marked with `meta={"cube_type": "view"}` and are excluded from Cube export.
206221

207222
---
208223

209224
## Hierarchies
210225

211-
Unsupported. `hierarchies:` blocks on cubes are parsed by YAML without error but not stored. Hierarchies define drill-down level ordering (e.g., year -> quarter -> month) and cross-cube level references. They have no Sidemantic equivalent.
226+
Supported. `hierarchies:` blocks on cubes are parsed and used to set `Dimension.parent` chains.
227+
228+
| Feature | Status |
229+
|---------|--------|
230+
| `hierarchies: [{ name, levels }]` | Supported (level ordering sets `Dimension.parent` on each child level) |
231+
| Multiple hierarchies per cube | Supported |
232+
| Cross-cube level references (e.g., `users.city`) | Partial support: cross-cube references (containing dots) are silently skipped. Only same-cube levels are linked. |
233+
| `title` on hierarchies | Not stored (used for display only) |
212234

213235
---
214236

@@ -226,19 +248,27 @@ Unsupported. Access policy blocks on cubes and views are parsed by YAML without
226248

227249
## Multi-Stage Calculations
228250

229-
Unsupported. The `multi_stage: true` flag on measures is parsed by YAML without error but not stored. Multi-stage calculations in Cube enable measures that reference other measures as inputs, run in separate query stages, and support features like `group_by` (for percent-of-total) and `time_shift` (for period comparisons). The adapter parses the measure's `sql` and `type` normally, so the measure still appears in the graph, but multi-stage execution semantics are not reproduced.
251+
Partial support. The `multi_stage: true` flag on measures is parsed by YAML without error but not stored. Multi-stage calculations in Cube enable measures that reference other measures as inputs, run in separate query stages, and support features like `group_by` (for percent-of-total) and `time_shift` (for period comparisons). The adapter parses the measure's `sql` and `type` normally, so the measure still appears in the graph, but multi-stage execution semantics are not reproduced.
230252

231-
Related unsupported sub-features:
232-
- `time_shift: [{ time_dimension, interval, type }]` on measures
233-
- `group_by` (percent-of-total grouping)
234-
- `order_by` / `reduce_by` (ranking)
235-
- `case` / `switch` / `when` / `else` on measures
253+
| Feature | Status |
254+
|---------|--------|
255+
| `time_shift: [{ time_dimension, interval, type: prior }]` | Supported (maps to `Metric.type="time_comparison"` with `comparison_type` and `time_offset`) |
256+
| `group_by` (percent-of-total grouping) | Unsupported |
257+
| `order_by` / `reduce_by` (ranking) | Partial support: stored in `Metric.meta` for `type: rank` measures |
258+
| `case` / `switch` / `when` / `else` on measures | Unsupported |
236259

237260
---
238261

239262
## Custom Calendars and Granularities
240263

241-
Unsupported. Cube supports custom calendar cubes (`calendar: true`) with custom granularity definitions on time dimensions (`granularities: [{ name, sql, interval, origin }]`) and dimension-level `time_shift` definitions. The adapter parses these structures without error but does not store them. Time dimensions always default to `granularity: day`.
264+
Partial support. Cube supports custom calendar cubes (`calendar: true`) with custom granularity definitions on time dimensions. The `calendar: true` flag is not stored, but custom granularities are now parsed.
265+
266+
| Feature | Status |
267+
|---------|--------|
268+
| `granularities: [{ name, ... }]` on time dimensions | Supported (granularity names stored in `Dimension.supported_granularities`) |
269+
| `granularities[].sql`, `interval`, `origin` | Unsupported (only the name is stored) |
270+
| `calendar: true` on cubes | Partial support (parsed without error, not stored) |
271+
| Dimension-level `time_shift` | Unsupported |
242272

243273
---
244274

@@ -268,21 +298,30 @@ Sidemantic can export its semantic model back to Cube YAML format.
268298
| Cubes with `sql_table` | Supported |
269299
| Cubes with `sql` (inline query) | Supported |
270300
| `description` | Supported |
301+
| `meta` on cubes | Supported |
271302
| Dimensions (string, number, time, boolean) | Supported (`{model}` mapped back to Cube types) |
272303
| `primary_key: true` on dimensions | Supported |
273304
| `format` on dimensions | Supported |
305+
| `title` on dimensions | Supported (exported from `Dimension.label`) |
306+
| `meta` on dimensions | Supported |
307+
| `shown: false` on dimensions | Supported (exported when `Dimension.public` is False) |
274308
| Standard measures (count, count_distinct, sum, avg, min, max) | Supported |
275309
| Derived measures (`type: number`) | Supported |
276310
| Ratio metrics | Supported (exported as `type: number` with `${numerator}::float / NULLIF(${denominator}, 0)`) |
277-
| Cumulative metrics | Supported (exported with `rolling_window: { trailing }`) |
311+
| Cumulative metrics | Supported (exported with `rolling_window: { trailing }` or `rolling_window: { type: to_date, granularity }`) |
278312
| Time comparison metrics | Partial support: exported as `type: number` with a description annotation; no `time_shift` block generated. |
279313
| Measures with filters | Supported (exported as `filters: [{ sql }]`) |
280314
| `format` on measures | Supported |
281-
| `drill_members` on measures | Supported (exported from hierarchy dimensions when available) |
315+
| `title` on measures | Supported (exported from `Metric.label`) |
316+
| `meta` on measures | Supported |
317+
| `shown: false` on measures | Supported (exported when `Metric.public` is False) |
318+
| `drill_members` on measures | Supported (exported from `Metric.drill_fields` or hierarchy dimensions) |
282319
| Segments | Supported (`{model}` replaced back with `${CUBE}`) |
283-
| Joins (many_to_one) | Supported (generates `sql` join expression from foreign key and primary key) |
284-
| Joins (one_to_many, one_to_one) | Partial support: only `many_to_one` relationships are exported as joins. Other relationship types are omitted. |
285-
| Pre-aggregations | Unsupported (not exported) |
320+
| `shown: false` on segments | Supported (exported when `Segment.public` is False) |
321+
| Joins (many_to_one, one_to_one) | Supported (generates `${CUBE}.fk = ${target}.pk` join SQL) |
322+
| Joins (one_to_many) | Supported (generates `${CUBE}.pk = ${target}.fk` with swapped direction) |
323+
| Joins (many_to_many) | Unsupported (skipped; requires junction table info) |
324+
| Pre-aggregations | Supported (all fields exported including refresh_key, indexes, build_range) |
286325
| Model inheritance resolution | Supported (inheritance resolved before export) |
287-
| Roundtrip fidelity (Cube -> parse -> export -> re-parse) | Supported for dimensions, metrics, and segments. Relationships are not fully round-tripped due to the export limitation above. |
288-
326+
| View models | Supported (skipped during export, identified by `meta.cube_type == "view"`) |
327+
| Roundtrip fidelity (Cube -> parse -> export -> re-parse) | Supported for dimensions, metrics, segments, and pre-aggregations. |

sidemantic-schema.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@
111111
"description": "Parent dimension for hierarchies (e.g., 'state' parent is 'country')",
112112
"title": "Parent"
113113
},
114+
"public": {
115+
"default": true,
116+
"description": "Whether dimension is visible in API/UI",
117+
"title": "Public",
118+
"type": "boolean"
119+
},
114120
"sql": {
115121
"anyOf": [
116122
{
@@ -547,6 +553,12 @@
547553
"description": "Time offset for denominator (e.g., '1 month')",
548554
"title": "Offset Window"
549555
},
556+
"public": {
557+
"default": true,
558+
"description": "Whether metric is visible in API/UI",
559+
"title": "Public",
560+
"type": "boolean"
561+
},
550562
"sql": {
551563
"anyOf": [
552564
{
@@ -1474,6 +1486,12 @@
14741486
"description": "Time offset for denominator (e.g., '1 month')",
14751487
"title": "Offset Window"
14761488
},
1489+
"public": {
1490+
"default": true,
1491+
"description": "Whether metric is visible in API/UI",
1492+
"title": "Public",
1493+
"type": "boolean"
1494+
},
14771495
"sql": {
14781496
"anyOf": [
14791497
{
@@ -1709,6 +1727,12 @@
17091727
"description": "Parent dimension for hierarchies (e.g., 'state' parent is 'country')",
17101728
"title": "Parent"
17111729
},
1730+
"public": {
1731+
"default": true,
1732+
"description": "Whether dimension is visible in API/UI",
1733+
"title": "Public",
1734+
"type": "boolean"
1735+
},
17121736
"sql": {
17131737
"anyOf": [
17141738
{
@@ -2145,6 +2169,12 @@
21452169
"description": "Time offset for denominator (e.g., '1 month')",
21462170
"title": "Offset Window"
21472171
},
2172+
"public": {
2173+
"default": true,
2174+
"description": "Whether metric is visible in API/UI",
2175+
"title": "Public",
2176+
"type": "boolean"
2177+
},
21482178
"sql": {
21492179
"anyOf": [
21502180
{

0 commit comments

Comments
 (0)