Skip to content

DEV-1608: Cube → SLayer ingestion (Stage 1)#211

Open
ZmeiGorynych wants to merge 6 commits into
mainfrom
egor/dev-1608-cube-to-slayer-ingestion
Open

DEV-1608: Cube → SLayer ingestion (Stage 1)#211
ZmeiGorynych wants to merge 6 commits into
mainfrom
egor/dev-1608-cube-to-slayer-ingestion

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented Jun 30, 2026

Copy link
Copy Markdown
Member

Stage 1 of Cube (Cube.js / Cube.dev) YAML ingestion. Adds slayer/cube/ — an offline importer that converts Cube data models into persisted SLayer models, mirroring the existing slayer/dbt/ importer, exposed via a new slayer import-cube CLI. Full spec lives in the DEV-1608 issue body.

Approach

  • Fully offline — types come from Cube's declared dimension/measure types; no DB connection required.
  • Everything that can't map cleanly is recorded in a structured CubeConversionReport (written to cube_import_report.json), never silently dropped.

Mapping

  • cube → model; view → facade model (re-exports members as derived columns + local/cross-model ModelMeasures anchored on the join_path root; mirrors the root cube's source mode).
  • measuresColumn + ModelMeasure split: count_distinct_approxcount_distinct; conditional filtersColumn.filter with a filter-aware dedup key; finite trailing rolling_window→windowed aggregation; calculated number/string/time/boolean measures→ModelMeasure formula.
  • dimensions → typed columns (caseCASE WHEN); joinsjoin_pairs with member→physical-column resolution (non-equi/non-column ON reported); segments → boolean columns; extends flattened (abstract bases emitted hidden).
  • No-SLayer-home features (pre_aggregations, refresh_key, calendar, hierarchies, drill_members, access_policy, sql_alias, geo, sub_query, custom granularities) reported + stashed under meta.cube_unmapped.
  • Tesseract features (switch, number_agg, case measures, measure filter) deferred to a follow-up. Native model inheritance (the future replacement for flatten) is tracked in DEV-1610.

A namespace allocator + offline sqlglot/formula validation ensure a broken member is reported rather than crashing whole-model construction.

Tests

  • 111 new tests across parser, refs, converter, extends, views, boundaries, report, CLI, and engine-level smoke (the smoke tests push converted models through real enrichment/SQL generation, catching facade cross-model resolution breaks).
  • Full unit suite green (5743 passed), ruff clean.

Docs

  • New docs/cube/cube_import.md + mkdocs.yml nav entry.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added offline slayer import-cube to convert Cube YAML cubes and views into persisted SLayer models and generate a JSON conversion report.
    • Implemented extends flattening, view facade model generation, join/segment/default-filter mapping, and structured “report-and-skip” conversion with deferred Stage 2 handling.
  • Documentation
    • Added Cube import docs and included them in navigation.
    • Added a developer-facing Cube import specification.
  • Bug Fixes
    • Improved resilience via templating detection, parse/validation boundaries, and cycle/unsupported-shape reporting.
  • Tests
    • Added parser/converter/ref/CLI/reporting and smoke/extends coverage.

Add `slayer/cube/` — an offline importer that converts Cube (Cube.js /
Cube.dev) YAML data models into persisted SLayer models, mirroring the
existing `slayer/dbt/` importer. Exposed via a new `slayer import-cube` CLI.

Conversion is fully offline (types come from Cube's declared dimension/
measure types). Everything that can't map cleanly is recorded in a structured
`CubeConversionReport` (also written to `cube_import_report.json`) rather than
silently dropped.

Mapping:
- cube → table-owning model; view → facade model (re-exports members as
  derived columns + local/cross-model ModelMeasures on the join_path root).
- measures → Column + ModelMeasure split (count_distinct_approx →
  count_distinct; conditional filters → Column.filter with a filter-aware
  dedup key; finite trailing rolling_window → windowed aggregation;
  calculated number/string/time/boolean measures → ModelMeasure formula).
- dimensions → typed columns (case dim → CASE WHEN); joins → join_pairs with
  member→physical-column resolution (non-equi/non-column ON reported);
  segments → boolean columns; `extends` flattened (abstract bases hidden).
- no-SLayer-home features (pre_aggregations, refresh_key, calendar,
  hierarchies, drill_members, access_policy, sql_alias, geo, sub_query,
  custom granularities) reported + stashed under meta.cube_unmapped.
- Tesseract features (switch, number_agg, case measures, measure filter)
  deferred to a follow-up (DEV-1610 tracks native inheritance).

Includes a namespace allocator + offline sqlglot/formula validation so a
broken member is reported, never crashes whole-model construction. Docs:
docs/cube/cube_import.md (+ mkdocs nav).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear

linear Bot commented Jun 30, 2026

Copy link
Copy Markdown

DEV-1608

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 654117ab-aed6-46e5-89b9-6ece10eae780

📥 Commits

Reviewing files that changed from the base of the PR and between 3959d5f and 488f5d0.

📒 Files selected for processing (12)
  • slayer/cli.py
  • slayer/cube/converter.py
  • slayer/cube/extends.py
  • slayer/cube/models.py
  • slayer/cube/parser.py
  • slayer/cube/refs.py
  • tests/test_cube_cli.py
  • tests/test_cube_converter.py
  • tests/test_cube_extends.py
  • tests/test_cube_parser.py
  • tests/test_cube_refs.py
  • tests/test_cube_views.py
📝 Walkthrough

Walkthrough

Adds an offline Cube import pipeline that parses Cube YAML projects, flattens inheritance, converts cubes and views into SlayerModels, records structured issues, and wires the flow into slayer import-cube.

Changes

Cube→SLayer Import Pipeline

Layer / File(s) Summary
Spec and docs
CUBE_IMPORT_SPEC.md, docs/cube/cube_import.md, mkdocs.yml
Adds the import specification, user-facing documentation, and mkdocs navigation for the Cube import page.
Cube models and report types
slayer/cube/__init__.py, slayer/cube/models.py, slayer/cube/report.py
Defines the Cube YAML Pydantic models and the structured conversion report/result types.
Parser and refs
slayer/cube/parser.py, slayer/cube/refs.py
Parses Cube project YAML with templating-aware skipping and rewrites Cube refs and join ON clauses.
Extends flattening
slayer/cube/extends.py
Flattens cube and view inheritance, merging members and reporting cycles.
Converter core
slayer/cube/converter.py
Converts cubes into SLayer models, including dimensions, measures, joins, segments, and offline validation cleanup.
View facades
slayer/cube/converter.py
Builds facade models for Cube views, including member selection, re-exported measures, default filters, and unmapped view metadata.
CLI wiring
slayer/cli.py
Adds the import-cube subcommand, dispatch, summary output, and JSON report writing.
Fixtures
tests/fixtures/cube_project/model/cubes/*, tests/fixtures/cube_project/model/views/*
Adds Cube YAML fixtures for cubes, views, Jinja templating, inheritance, malformed input, and view coverage.
Tests
tests/test_cube_parser.py, tests/test_cube_refs.py, tests/test_cube_report.py, tests/test_cube_extends.py, tests/test_cube_converter.py, tests/test_cube_views.py, tests/test_cube_boundaries.py, tests/test_cube_cli.py, tests/test_cube_smoke.py
Adds parser, refs, report, extends, converter, view, boundary, CLI, and smoke tests covering the import flow.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • MotleyAI/slayer#9: Adds join conversion infrastructure that this Cube import work builds on for join_pairs and model joins.
  • MotleyAI/slayer#26: Adds formatting primitives that this importer maps Cube formats into.

Suggested reviewers: AivanF

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: Stage 1 Cube-to-SLayer ingestion for DEV-1608.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch egor/dev-1608-cube-to-slayer-ingestion

Comment @coderabbitai help to get the list of available commands.

Quality-gate blockers:
- refs.py: rewrite the join-ON splitter from `\s+AND\s+` to `\bAND\b`
  (removes the polynomial-backtracking shape Sonar S5852 flagged).
- cli.py: NOSONAR(S8707) on the report-file write — the path is an
  intended, user-specified CLI output location.

Codex correctness:
- Map Cube `public: false` dimensions/segments to `Column.hidden`, and
  skip private members when a view uses `includes: "*"`.
- SQL-escape default-filter literal values (single quotes doubled).

Sonar maintainability:
- `*:count` literal → `_STAR_COUNT` constant; drop unnecessary `list()`;
  `dict.fromkeys`; validate the `mode` arg (was unused); fix a noqa-comment
  syntax; replace unused test locals with `_`.
- Reduce cognitive complexity of `parse_cube_project` / `_strip_jinja_members`
  / `_member_has_jinja` / `translate_cube_refs` / `_run_import_cube` by
  extracting helpers.

Adds tests for the hidden-mapping, includes-"*" privacy filter, filter
escaping, and mode validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (1)
slayer/cli.py (1)

1477-1486: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider isolating save_model failures so the report is still emitted.

The converter routes invalid members into the report and aims to never crash, but storage.save_model runs save-time validation (e.g. derived-column cycle detection) that can still raise on a converted model. A single failing model here aborts the loop before _write_cube_report/the summary run, leaving partially-saved state and no report — the opposite of the importer's "report, don't crash" contract. Wrapping the per-model save in a try/except that records a parse_error/save issue would keep the run resilient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/cli.py` around lines 1477 - 1486, The per-model persistence loop in
the import flow should not let a single `storage.save_model` failure abort the
entire run, because that prevents `_write_cube_report` and the final summary
from running. Update the loop around `run_sync(storage.save_model(model))` to
catch save-time exceptions, record the failure as a report issue/`parse_error`
on the relevant model or report object, and then continue saving the remaining
models. Keep the existing `_print_cube_import_summary` and `_write_cube_report`
calls in place so the importer still emits a report even when one model fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CUBE_IMPORT_SPEC.md`:
- Around line 21-30: Update the Markdown in CUBE_IMPORT_SPEC.md to satisfy the
fence-lint issue by adding explicit language tags to the unlabeled fenced blocks
in the spec and removing the stray standalone fence near the end of the file.
Focus on the fenced sections around the referenced diagram/spec blocks and the
empty fence at the EOF so the rendered document and MD040 checks are both
correct.

In `@docs/cube/cube_import.md`:
- Around line 82-86: The joined-measure example in the Cube import spec uses the
cube measure name instead of the underlying column, which conflicts with the
facade contract. Update the Cube view documentation in the import spec so the
`ModelMeasure` example references `<joinpath>.<underlying_column>:<agg>` rather
than `customers.ltv:sum`, using the relevant Cube import docs section to keep
the terminology aligned with how `join_path` and included measures are
described.

In `@slayer/cli.py`:
- Around line 1507-1510: The report path logic in the import flow is only using
args.storage, so it can miss args.models_dir and place cube_import_report.json
in the wrong location. Update the report-path resolution near the storage_base
handling to follow the same fallback order as _resolve_storage (args.storage,
then args.models_dir, then _STORAGE_DEFAULT), and keep the .db dirname
normalization consistent so the report is written alongside the resolved storage
backend.

In `@slayer/cube/converter.py`:
- Around line 332-365: Keep the measure tracking state in sync with what
actually gets emitted: `_convert_calc_measure()` should record calculated
measures as `kind="calc"` when `_emit_measure()` succeeds, and
`_convert_agg_measure()` should only populate `_MeasureInfo` after a successful
emit. Update `_emit_measure()` to return success/failure so callers like
`_convert_calc_measure()`, `_convert_agg_measure()`, and `_facade_measure()` can
avoid keeping stale entries when validation or emission drops a measure. After
`_validate_offline()`/related pruning, remove any `_measure_info` entries for
measures that were not retained so view re-exports stay consistent with the
source model.
- Around line 416-428: The join conversion logic in _convert_joins currently
allows a ModelJoin to be created even when cj.name does not exist in
self._cubes, because _resolve_join_pairs() can still return pairs based on the
raw target member. Add a target-cube existence check before appending the join:
if cj.name is missing from self._cubes, report a CubeConversionIssue for the
unsupported join and skip it. Keep the fix localized to _convert_joins and, if
needed, _resolve_join_pairs so only joins targeting real cubes are persisted.
- Around line 290-298: The CASE SQL builder is embedding raw labels into
single-quoted SQL, which breaks on apostrophes and other special characters.
Update `_build_case_sql()` in `converter.py` to pass each `when["label"]` and
the `else["label"]` through `_sql_str_literal()` before appending them. Keep the
existing `translate_cube_refs()` flow intact and ensure the `CASE`, `WHEN`,
`THEN`, and `ELSE` fragments still assemble the same way.

In `@slayer/cube/extends.py`:
- Around line 53-65: The cycle handling in `resolve()` for
`flatten_cube_extends()` only stops at the node that detects the loop, so parent
frames still merge that partially resolved object and continue inheritance.
Update the `resolve()` logic to propagate a cycle-failure state back through the
entire call chain for all nodes in the cycle, so none of them get merged, and
apply the same fix in `flatten_view_extends()`; keep `EXTENDS_CYCLE` reporting
on the detected cycle while ensuring `converter.py` receives fully flattened
results with no inherited members from cyclic chains.

In `@slayer/cube/models.py`:
- Around line 102-114: `CubeViewCubeRef` currently models only flat `alias`,
`title`, and `description` fields, but the view spec expects per-member override
payloads and also includes `format` and `meta`; expand this model so it can
preserve the documented override shape instead of dropping it through Pydantic.
Update `CubeViewCubeRef` in `models.py` to add the missing override fields and
represent member-specific overrides in a way the converter can inspect later,
while keeping the existing `join_path`, `includes`, `excludes`, and `prefix`
behavior intact.

In `@slayer/cube/parser.py`:
- Around line 88-102: The _load_yaml helper currently only handles
yaml.YAMLError, so unreadable files can still crash import before a
CubeConversionIssue is recorded. Update _load_yaml to catch OSError from the
open(path, encoding="utf-8") read path and treat it like a non-fatal parse
failure by appending a warning issue with CubeIssueCategory.PARSE_ERROR (or
templating category only if raw text is available and contains_jinja applies),
then return None. Keep the behavior aligned with the existing _load_yaml and
CubeConversionIssue flow so malformed and unreadable YAML are reported the same
way through the parser/CLI contract.

In `@slayer/cube/refs.py`:
- Around line 11-12: The literal-skipping regex in refs.py is too naive and
breaks on SQL strings with doubled single quotes, so placeholders inside valid
literals can be rewritten. Update the literal handling in _LITERAL_RE (and any
related parsing in refs.py) to correctly match SQL string literals with escaped
quotes like 'can''t {CUBE}', and ensure _REF_RE substitution still ignores text
inside those literals. Verify the resulting SQL remains valid for the
rewrite/validation path used by converter.py’s rewrite-and-validate flow so
legitimate Cube models are not downgraded to COMPLEX_SQL.

In `@tests/fixtures/cube_project/model/cubes/malformed.yml`:
- Around line 1-5: The malformed cube fixture is currently failing earlier on
schema validation because it has a sql_table but no name, so it never reaches
the CubeCube/_cube_source NO_SOURCE path in converter.py. Update the fixture to
use a valid cube name and remove the source-related fields (for example, the
sql_table entry) so the converter can instantiate the cube and report
CubeIssueCategory.NO_SOURCE as intended.

In `@tests/test_cube_views.py`:
- Around line 254-256: The test is too permissive because it accepts a
disconnected-view failure when this case is specifically about the root cube not
being emitted. Update the assertion in the cube views test to check only for the
root-specific issue category, using the existing report.issues check and the
CubeIssueCategory.AMBIGUOUS_VIEW_ROOT symbol, and remove the fallback to
CubeIssueCategory.DISCONNECTED_VIEW so the test enforces the intended
root-resolution behavior.

---

Nitpick comments:
In `@slayer/cli.py`:
- Around line 1477-1486: The per-model persistence loop in the import flow
should not let a single `storage.save_model` failure abort the entire run,
because that prevents `_write_cube_report` and the final summary from running.
Update the loop around `run_sync(storage.save_model(model))` to catch save-time
exceptions, record the failure as a report issue/`parse_error` on the relevant
model or report object, and then continue saving the remaining models. Keep the
existing `_print_cube_import_summary` and `_write_cube_report` calls in place so
the importer still emits a report even when one model fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b8dd63f0-00f5-486f-a578-da03875c3ea2

📥 Commits

Reviewing files that changed from the base of the PR and between 3ae4a5a and c1990c4.

📒 Files selected for processing (27)
  • CUBE_IMPORT_SPEC.md
  • docs/cube/cube_import.md
  • mkdocs.yml
  • slayer/cli.py
  • slayer/cube/__init__.py
  • slayer/cube/converter.py
  • slayer/cube/extends.py
  • slayer/cube/models.py
  • slayer/cube/parser.py
  • slayer/cube/refs.py
  • slayer/cube/report.py
  • tests/fixtures/cube_project/model/cubes/customers.yml
  • tests/fixtures/cube_project/model/cubes/events.yml
  • tests/fixtures/cube_project/model/cubes/for_loop.yml
  • tests/fixtures/cube_project/model/cubes/malformed.yml
  • tests/fixtures/cube_project/model/cubes/orders.yml
  • tests/fixtures/cube_project/model/cubes/templated_member.yml
  • tests/fixtures/cube_project/model/views/orders_overview.yml
  • tests/test_cube_boundaries.py
  • tests/test_cube_cli.py
  • tests/test_cube_converter.py
  • tests/test_cube_extends.py
  • tests/test_cube_parser.py
  • tests/test_cube_refs.py
  • tests/test_cube_report.py
  • tests/test_cube_smoke.py
  • tests/test_cube_views.py

Comment thread CUBE_IMPORT_SPEC.md Outdated
Comment thread docs/cube/cube_import.md Outdated
Comment thread slayer/cli.py Outdated
Comment thread slayer/cube/converter.py
Comment thread slayer/cube/converter.py Outdated
Comment thread slayer/cube/models.py
Comment thread slayer/cube/parser.py
Comment thread slayer/cube/refs.py Outdated
Comment thread tests/fixtures/cube_project/model/cubes/malformed.yml
Comment thread tests/test_cube_views.py Outdated
ZmeiGorynych and others added 4 commits June 30, 2026 14:36
- refs.py: extract `_equality_pair` from `parse_join_on` to bring its
  cognitive complexity under the threshold (Sonar S3776).
- CUBE_IMPORT_SPEC.md: add fence languages to code blocks and drop the
  stray trailing fence (CodeRabbit MD040).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spell out that a facade re-exports a joined measure via its *underlying
column* (`customers.amount:sum`), not the Cube measure name — matching the
converter's facade contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_write_cube_report` now resolves its base via `args.storage or
args.models_dir or _STORAGE_DEFAULT`, matching `_resolve_storage`, so a
`--models-dir`-only invocation writes the report next to the models as
documented. Adds a CLI test for the parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correctness:
- extends: detect the full extends cycle up front so no node on the cycle
  inherits (previously only the closing frame was un-merged, letting ancestors
  still merge a cyclic node). Applies to cubes and views.
- converter: reject joins whose target cube isn't in the project (was
  persisting a ModelJoin to a non-existent model); keep _measure_info in sync
  with emitted measures (record calc measures as kind="calc", record only on
  successful emit, prune after offline validation) so view facades never
  re-export a measure the model no longer has.
- refs: doubled-quote-aware SQL string literal regex (`'can''t {CUBE}'` no
  longer leaks `{CUBE}` to the translator); escape CASE labels via
  _sql_str_literal.
- parser: catch OSError in _load_yaml (broken symlink / permission) and report
  it like a malformed file instead of aborting.
- models/converter: CubeViewCubeRef.includes accepts Cube's per-member override
  object form (was crashing the view parse); overrides are reported as
  unsupported (Stage 1) rather than silently dropped. Dropped the dead flat
  title/description fields.
- cli: isolate per-model save_model failures so the report is still written.

Tests for each; also tightened the root-not-emitted view assertion to the
exact AMBIGUOUS_VIEW_ROOT category.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant