diff --git a/DECISIONS.md b/DECISIONS.md index e5c1c75b..2160f44c 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -117,3 +117,4 @@ implementation detail. Include issue refs when known. - 2026-08-11 — Deletion, sweep, docs: P-J states 2+3 executed (DEV-1749, PR 6 of 6, closing the DEV-1742 consolidation). Every mechanism PRs 1–5 + DEV-1763 left production-unreferenced (P-J state 1) is now **deleted** together with its pinning tests (state 3), after confirming the desired behaviour is pinned by tests on the new code. Removed from `generator.py`: the five legacy per-path `ValueKey` renderers and the three arithmetic composer shims (`render_value_key` / `render_arithmetic` are the sole paths, P-G); the first/last host-base ranked machinery — `_build_first_last_base_select`, `_build_ranked_subquery_from_planned`, `_has_first_last_aggregate`, the rn-suffix + filtered-rn/match maps and raw-filter-leak fallback in `_build_agg`, `FirstLastRenderState` and its `first_last_state` threading, and the production-dead `is_first_or_last` arm of `_render_cross_model_cte` (first/last is a `RankedAggregatePlan` CTE since DEV-1748, P-C); the Mode-A model-filter qualify chain (`_render_model_filter_sql`, `_qualify_mode_a_sql_filter`, `_render_mode_a_predicate`, `_filter_join_paths`, `_expand_degenerate_derived_root`, `_column_ref_is_derived`, `_predicate_references_derived`) plus the dead `FilterPhase.text_columns` field — the Mode-A door (`ScopeFrame.enter_predicate`, DEV-1745) is the one door, P-A; the four legacy ORDER BY resolvers (`_build_combined_order_by_sql`, `_resolve_combined_order_term`, `_planned_order_by_sql`, `_apply_order_limit_from_planned`) superseded by `resolve_order_term`; `_null_safe_join_pair_sql` (string round-trip superseded by `render/joins.py`, P-I); `_build_transform_sql`+`_SELF_JOIN_TRANSFORMS`, `_build_outer_wrap`+`_strip_trailing_pagination` (planned outer-wrap delegates to `SqlDialect.emit_outer_wrap`, P-H), and `_cte_name_from_alias` (superseded by `naming.cte_name_from_alias`, P-F). Removed from `cross_model_planner.py`: the formula-text re-rooting island (`_local_agg_formula`, `_render_ref_formula`, `_scalar_formula_literal`, `_reroot_ref`, `_host_ref_path`, `_REROOT_BIND_ERRORS`) and two further dead helpers (`_classify_subplan_filters`, `_filter_ref_paths`) — cross-model re-rooting is typed keys end to end (`reroot_aggregate_key` / `reroot_value_key`), no text round-trip, P-E. **B12 (ratified):** `_build_agg`'s dispatch now reads the single `AGG_REGISTRY` classification table (DEV-1744) — `_AGG_FUNCTION_MAP` (with its dead `COUNT_DISTINCT`/`MEDIAN` string values), the second inline class map, and the generator-local stat-name frozenset are gone; the two-phase resolution order (own-inner builders → shared inner + filter wrap → distinct/median/simple) is preserved byte-for-byte, SQL-identical. **Consolidations landed:** transform-op registries single-sourced (`RANK_FAMILY_TRANSFORMS`, `TIME_TRANSFORMS` from `core/formula.py`), the cube identifier regex re-pointed at `core/refs.IDENTIFIER_RE`; the `_bare_column_refs` regex kept for its remaining validation role only. **Deferred to DEV-1777** (pure refactors, no SQL change, carry regression risk needing their own byte-identity checks): step-CTE emission extraction, the throwaway-`ScopeFrame` consolidation + `_resolve_explicit_time_col` dead-branch removal, and the positional-index couplings. The remaining bare-identifier divergences (`schema_drift`, `.isidentifier()` sites) went to DEV-1771. `docs/architecture/sql-generation.md` was rewritten as the P-A – P-J principles document. Every deleted symbol's docstring/comment references were swept to present-tense truth across 16 files. No emitted SQL changed; the full non-integration suite is green (3 xfails, all ticketed — DEV-1752 / DEV-1729 / DEV-1445 — retained per the F18 inventory). - 2026-08-12 — Single-source bare-identifier detection narrowed to `schema_drift` only (DEV-1771). Of the three ad-hoc detectors the issue named, only `schema_drift._is_bare_identifier` was re-pointed at the canonical `core/refs.IDENTIFIER_RE` (its char-loop `all(c.isalnum() or c == "_")` accepted non-ASCII in every position; the regex rejects only a non-ASCII *leading* char, since `\w*` still matches Unicode after the ASCII-only lead class). The two `stage_planner` `.isidentifier()` sites (`_saved_model_measure_type`, `_bare_saved_measure_name`) were left as-is: they gate `get_measure(name)` and `ModelMeasure.name` is already ASCII-constrained by `_NAME_PATTERN`, so any string that could match a real measure passes both predicates identically — the flip is observationally a no-op, so no test could fail without it. The two `generator` `.isidentifier()` sites (`_resolve_sql`; the no-bundle branch reached via the `col.name` fallback) were also left as-is: `Column.name`/`Column.sql` are NOT ASCII-constrained, and routing a non-ASCII-leading physical column (e.g. Cyrillic `год`, common in RU/UA schemas) through `_parse()` drops the model-relation qualifier (`m."год"` → bare `год`) — a correctness regression on legitimate input, exactly the issue's "surface and stop" guard. The one shipped behavior change is a drift false-negative: a base column aliasing a bare non-ASCII-leading physical name (`Column(name="year", sql="год")`) reclassifies base→derived, so a dropped physical `год` is no longer flagged by `_diff_sql_table_columns` and is instead scanned as a ref by `_first_dropped_sql_column_ref` (author-accepted; advisory-only, never query correctness). `.match()` (not `fullmatch`) and the retained `.strip()` match the existing `cube`/`dbt`/`osi` reuse sites. - 2026-08-16 — Aggregated slot-type and display-format inference share one classifier (DEV-1788, follow-up to DEV-1784's Option A). `aggregated_type` (slot `DataType`) and `_infer_aggregated_format` (response `NumberFormat`) had disagreed on the stat/parametric family: type said `DOUBLE` while format fell through to inherit the source column's format, so `revenue:stddev_samp` was typed `DOUBLE` yet displayed as currency. Both now read a single `classify_aggregation` (`core/enums.py`) returning one of four `AggregationValueClass` buckets, and each function maps the bucket to its own output — no per-name branching survives, so the two axes cannot drift. The four builtin frozensets (`INTEGER_AGGREGATIONS`, `PRESERVING_AGGREGATIONS`, `FLOAT_SOURCE_UNIT_AGGREGATIONS`, `FLOAT_PLAIN_AGGREGATIONS`) partition `BUILTIN_AGGREGATIONS`, pinned by a completeness test; custom/model-defined aggregations hit the `PRESERVING` fallback (inherit type & format), unchanged. **Semantics chosen (Option B, unit-correct):** `avg`/`median`/`weighted_avg`/`percentile`/`stddev*` are `DOUBLE` but keep the source's UNITS, so display format inherits the source (falling back to `FLOAT` when the source has none — keeping type `DOUBLE` and format `FLOAT` coherent for unformatted measures, and confining the change to formatted ones); `corr`/`var*`/`covar*` are dimensionless/squared/product units, so they display as plain `FLOAT` regardless of source. `aggregated_type` is behaviourally unchanged (only restructured). **Net user-visible change, all in `_infer_aggregated_format`:** avg-family of a FORMATTED measure now inherits that format (was `FLOAT`); `corr`/`var*`/`covar*` now `FLOAT` (was inherit); `stddev*`/`percentile` unchanged (already inherited). Drift guard extended to the full four-bucket table and routed through the public callers (`measure_key_type` / `measure_key_format_description`), plus response-metadata assertions for the stat/parametric family. +- 2026-08-18 — `time_shift` / `consecutive_periods` over a cross-model aggregate render; the blanket 7b.15e guard is narrowed (DEV-1750, spun out of DEV-1745, deliberately OUTSIDE the ratified DEV-1742 6-PR chain). **Part 1** wires the Mode-A template-fragment join discovery into the shifted (`time_shift`) CTE through the same one door the host base (`_resolve_agg_inputs_via_scope`) and the `_cm_` CTE (DEV-1745) use: `_emit_time_shift_ctes_for_planned`'s inner-aggregate block now calls `_register_fragment_kwarg_joins` and resolves the aggregate's source (widened to path-bearing via `getattr(source, "path", ())`, Codex F5), positional column args (skipping the DEV-1526 path-bearing `ColumnSqlKey` residual), and column-ref kwargs through `shifted_scope` — so a crossing default param (`amount:wscaled_sum`, `w='customers__regions.weight'`) pulls its `customers`→`regions` join into the shifted CTE's FROM instead of emitting `SUM(orders.amount * customers__regions.weight)` with no join (SQL no database binds). The prior "provably no-ops today" comment is now load-bearing. **Part 2** lifts the guard by giving the cross-model transform chain (`_render_cross_model_transform_chain`) the same three-arm Kahn loop the local chain runs — window batch + `time_shift` + `consecutive_periods` — dispatching the two temporal ops to the SAME per-op emitters (`_emit_time_shift_ctes_for_planned` / `_emit_consecutive_periods_ctes_for_planned`), so Part 1's join discovery serves both chains and the loops cannot drift on discovery. The shared step BODIES — the Kahn-batch split, the window step CTE, the two temporal emitters, the POST-phase arith/scalar materialisation step, and the finalise/outer-wrap tail — are extracted into shared `SQLGenerator` helpers (`_classify_ready_transform_layers`, `_emit_window_batch_step`, `_emit_time_shift_layers`, `_emit_cp_layers`, `_emit_unmaterialised_post_phase_step`, `_finalize_planned_transform_chain`) that both chains call, verbatim → byte-identical emission (a Sonar-duplication down-payment on DEV-1799). The ONLY remaining per-chain difference is the window-vs-temporal dispatch ORDER within a Kahn batch (local: window first; cross-model: temporal first) — unifying that reorders cross-model CTEs and re-blesses golden, so it stays deferred to DEV-1799. `source_model` + `bundle` are threaded in; `_build_shifted_cte_where_parts` carries the 7b.3c frame-bound invariant unchanged. **Guard narrowed, not removed** (`_guard_target_grain_time_shift`, runs before the emitter): the ONE unrenderable shape is a `time_shift` whose inner aggregate is TARGET-GRAIN cross-model — detected by **plan ownership** (its `CrossModelAggregatePlan.cte_root_model is None`; host-rooted isolation sets it to the host name — Codex F1, pinned at the planner level in `test_dev1750_guard_ownership.py`), NOT by formula text — because host-rooted re-aggregation would multiply target rows through the 1:N join. `consecutive_periods` is lifted **entirely**: it reads a materialised alias and never re-aggregates, so it has no target-grain failure mode (executed for the target-grain-inner shape). A `time_shift` over a ranked `first`/`last` aggregate raises a distinct loud 7b.15e (the shifted CTE re-aggregates flatly and cannot reproduce the ROW_NUMBER ranking; the crossing time arg is still registered for Law-1 totality). **Scope boundary (author-ratified):** `change`/`change_pct` render over a **local/host-rooted** inner (the chain's existing outer arithmetic-materialization step handles the subtraction) but over a **cross-model** inner they hit a separate pre-existing `RenderContextMissingFacilityError` in the combined SELECT (arithmetic-over-transform rendered before the transform materialises) — out of scope, tracked as **DEV-1800** and pinned as a loud, specific error. Execution ground truth (SQLite + DuckDB) for shapes a/b, cp, change/change_pct, NULL-dim null-safe survival, `date_range` frame-bound omission, and sibling-protection under a genuine 1:N (`line_items`) fan-out; per-dialect emission (incl. Postgres/T-SQL/BigQuery) pinned in `tests/golden/dev1750_sql_baseline.json`. Full non-integration suite green; no emitted SQL changed for any pre-existing shape. diff --git a/docs/architecture/cross-model-aggregates.md b/docs/architecture/cross-model-aggregates.md index 2535dda6..508bd7d9 100644 --- a/docs/architecture/cross-model-aggregates.md +++ b/docs/architecture/cross-model-aggregates.md @@ -323,9 +323,19 @@ null-safe form retains it. - A host-local filter on a **no-dimension** cross-model-agg query is applied nowhere (the empty `_base` placeholder doesn't filter; host-local filters are excluded from the re-rooted CTE). Semantically ambiguous; rare. -- `time_shift` / `consecutive_periods` / `change` / `change_pct` over (or - alongside) a cross-model aggregate raise `NotImplementedError` — factor the - temporal transform into an earlier stage. +- `time_shift` / `consecutive_periods` (and `change` / `change_pct`, which + desugar to `time_shift`) **render** over a **local** or **host-rooted** inner + aggregate that coexists with a cross-model aggregate (DEV-1750). The + cross-model transform chain gained the same shifted / cp CTE emitters the local + chain uses, so a crossing template fragment (`amount:wscaled_sum` with a + default `w='customers__regions.weight'`) pulls its join into the shifted CTE. + Two shapes stay guarded, loudly: a `time_shift` whose inner aggregate is + **target-grain** cross-model (`cte_root_model is None` — host-rooted + re-aggregation would multiply target rows through the 1:N join), and a + `time_shift` over a **ranked `first`/`last`** aggregate (the shifted CTE + re-aggregates flatly and cannot reproduce the ROW_NUMBER ranking). + `change` / `change_pct` over a **cross-model** inner aggregate hits a separate + combined-arithmetic-over-transform gap (DEV-1800), tracked independently. - Cross-model parametric-agg result keys diverge from legacy **by design**: `customers.revenue:percentile(p=0.5)` → `…revenue_percentile_p_0_5` where legacy dropped the kwarg suffix (`…revenue_percentile`). Legacy's drop was a diff --git a/slayer/sql/generator.py b/slayer/sql/generator.py index da944eaa..4831bba6 100644 --- a/slayer/sql/generator.py +++ b/slayer/sql/generator.py @@ -1641,122 +1641,282 @@ def _generate_from_planned_impl( # NOSONAR(S3776) — top-level dispatch over c # the list can never silently retarget where the chain continues. chain_tail = ctes[-1].name while pending_layers: - ready_window: list = [] - ready_time_shift: list = [] - ready_cp: list = [] - not_ready: list = [] - for layer in pending_layers: - if not self._transform_layer_deps_ready( - layer=layer, + (ready_window, ready_time_shift, ready_cp, not_ready) = ( + self._classify_ready_transform_layers( + pending_layers=pending_layers, slots_by_id=slots_by_id, slot_id_by_key=slot_id_by_key, available_alias_by_slot_id=available_alias_by_slot_id, - ): - not_ready.append(layer) - elif layer.op == "time_shift": - ready_time_shift.append(layer) - elif layer.op == "consecutive_periods": - ready_cp.append(layer) - else: - ready_window.append(layer) + ) + ) if not (ready_window or ready_time_shift or ready_cp): pending_ops = [layer.op for layer in pending_layers] raise RuntimeError( f"DEV-1450 stage 7b.11: transform layer dependencies " f"could not be resolved; pending ops: {pending_ops!r}.", ) - # --- Window batch (one step CTE per Kahn batch) ---------- + # This chain dispatches window batch first, then the temporal ops + # (the cross-model chain reverses that order — DEV-1799 unifies it). if ready_window: - step_num += 1 - step_name = cte_allocator.allocate_cte(f"step{step_num}") - prev_cte = chain_tail - carry_aliases = self._carry_aliases_in_plan_order( - aliases_by_slot_id, + chain_tail, step_num = self._emit_window_batch_step( + ready_window=ready_window, + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + step_num=step_num, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + source_relation=source_relation, + planned_query=planned_query, ) - step_parts = [ - exp.column(a, quoted=True) for a in carry_aliases - ] - for layer in ready_window: - for slot_id in layer.slot_ids: - slot = slots_by_id[slot_id] - alias = ( - slot.public_aliases[0] - if slot.public_aliases - else slot.declared_name - ) - full_alias = f"{source_relation}.{alias}" - window_expr = self._render_window_transform_sql( - slot=slot, - slots_by_id=slots_by_id, - slot_id_by_key=slot_id_by_key, - available_alias_by_slot_id=available_alias_by_slot_id, - planned_query=planned_query, - ) - if slot.type is not None: - window_expr = _wrap_cast_for_type( - window_expr, slot.type, - ) - step_parts.append( - window_expr.as_(full_alias, quoted=True), - ) - aliases_by_slot_id.setdefault(slot_id, []).append( - full_alias, - ) - available_alias_by_slot_id.setdefault( - slot_id, full_alias, - ) - ctes.append(CteEntry( - name=step_name, - query=exp.Select().select(*step_parts).from_(prev_cte), - depends_on=[prev_cte], - )) - chain_tail = step_name - # --- time_shift layers (each gets shifted_ + sjoin_ pair) - - for layer in ready_time_shift: - for slot_id in layer.slot_ids: - slot = slots_by_id[slot_id] - chain_tail = self._emit_time_shift_ctes_for_planned( - slot=slot, - ctes=ctes, - chain_tail=chain_tail, - cte_allocator=cte_allocator, - slots_by_id=slots_by_id, - slot_id_by_key=slot_id_by_key, - available_alias_by_slot_id=available_alias_by_slot_id, - aliases_by_slot_id=aliases_by_slot_id, - source_model=source_model, - source_relation=source_relation, - shifted_where_parts=shifted_where_parts, - shifted_where_join_paths=shifted_where_join_paths, - planned_query=planned_query, - bundle=bundle, - ) - # --- consecutive_periods layers (cp_reset_ + cp_value_ pair) - for layer in ready_cp: - for slot_id in layer.slot_ids: - slot = slots_by_id[slot_id] - chain_tail = self._emit_consecutive_periods_ctes_for_planned( - slot=slot, - ctes=ctes, - chain_tail=chain_tail, - cte_allocator=cte_allocator, - slots_by_id=slots_by_id, - slot_id_by_key=slot_id_by_key, - available_alias_by_slot_id=available_alias_by_slot_id, - aliases_by_slot_id=aliases_by_slot_id, - planned_query=planned_query, - source_relation=source_relation, - ) + chain_tail = self._emit_time_shift_layers( + ready_time_shift=ready_time_shift, + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + source_model=source_model, + source_relation=source_relation, + shifted_where_parts=shifted_where_parts, + shifted_where_join_paths=shifted_where_join_paths, + planned_query=planned_query, + bundle=bundle, + ) + chain_tail = self._emit_cp_layers( + ready_cp=ready_cp, + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + planned_query=planned_query, + source_relation=source_relation, + ) pending_layers = not_ready - # 7b.11 — materialise POST-phase ArithmeticKey / ScalarCallKey - # slots that the user projected but no transform layer rendered. - # ``change(amount:sum)`` lowers to ``amount:sum - time_shift(...)``; - # the time_shift slot is rendered as a self-join CTE pair, but - # the outer ArithmeticKey slot that subtracts them needs its - # own step CTE. Same shape covers ``change_pct`` (division of - # arithmetic operands) and any future POST-phase non-transform - # slot the planner emits. + # 7b.11 — materialise POST-phase ArithmeticKey / ScalarCallKey slots the + # user projected but no transform layer rendered (``change`` / + # ``change_pct`` desugarings), then assemble the chain and outer wrap. + chain_tail, step_num = self._emit_unmaterialised_post_phase_step( + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + step_num=step_num, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + source_relation=source_relation, + planned_query=planned_query, + ) + return self._finalize_planned_transform_chain( + ctes=ctes, + chain_tail=chain_tail, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + planned_query=planned_query, + ) + + # ----------------------------------------------------------------- + # Shared transform-chain steps (DEV-1750) + # + # The local (``generate_from_planned``) and cross-model + # (``_render_cross_model_transform_chain``) chains layer the SAME step CTEs + # over their base: a Kahn batch split, a per-batch window step, per-op + # temporal emitters, a POST-phase materialisation step, and an identical + # finalise/outer-wrap tail. These helpers hold those step BODIES once; each + # chain keeps only its own loop skeleton (they differ solely in the order + # they dispatch window vs temporal within a batch — DEV-1799 unifies that, + # which moves cross-model CTE order and re-blesses golden). Extracting the + # bodies verbatim leaves emitted SQL byte-identical. + # ----------------------------------------------------------------- + + def _classify_ready_transform_layers( + self, + *, + pending_layers, + slots_by_id, + slot_id_by_key, + available_alias_by_slot_id, + ) -> tuple: + """Kahn split of ``pending_layers`` into + ``(ready_window, ready_time_shift, ready_cp, not_ready)`` by dependency + readiness then op. A dep-blocked layer is ``not_ready`` regardless of op. + """ + ready_window: list = [] + ready_time_shift: list = [] + ready_cp: list = [] + not_ready: list = [] + for layer in pending_layers: + if not self._transform_layer_deps_ready( + layer=layer, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + ): + not_ready.append(layer) + elif layer.op == "time_shift": + ready_time_shift.append(layer) + elif layer.op == "consecutive_periods": + ready_cp.append(layer) + else: + ready_window.append(layer) + return ready_window, ready_time_shift, ready_cp, not_ready + + def _emit_window_batch_step( + self, + *, + ready_window, + ctes, + chain_tail, + cte_allocator, + step_num, + slots_by_id, + slot_id_by_key, + available_alias_by_slot_id, + aliases_by_slot_id, + source_relation, + planned_query, + ) -> tuple: + """One ``step`` CTE for a Kahn batch of window layers, carrying every + prior alias forward. Returns ``(new_chain_tail, new_step_num)``.""" + step_num += 1 + step_name = cte_allocator.allocate_cte(f"step{step_num}") + prev_cte = chain_tail + carry_aliases = self._carry_aliases_in_plan_order(aliases_by_slot_id) + step_parts = [exp.column(a, quoted=True) for a in carry_aliases] + for layer in ready_window: + for slot_id in layer.slot_ids: + slot = slots_by_id[slot_id] + alias = ( + slot.public_aliases[0] + if slot.public_aliases + else slot.declared_name + ) + full_alias = f"{source_relation}.{alias}" + window_expr = self._render_window_transform_sql( + slot=slot, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + planned_query=planned_query, + ) + if slot.type is not None: + window_expr = _wrap_cast_for_type(window_expr, slot.type) + step_parts.append(window_expr.as_(full_alias, quoted=True)) + aliases_by_slot_id.setdefault(slot_id, []).append(full_alias) + available_alias_by_slot_id.setdefault(slot_id, full_alias) + ctes.append(CteEntry( + name=step_name, + query=exp.Select().select(*step_parts).from_(prev_cte), + depends_on=[prev_cte], + )) + return step_name, step_num + + def _emit_time_shift_layers( + self, + *, + ready_time_shift, + ctes, + chain_tail, + cte_allocator, + slots_by_id, + slot_id_by_key, + available_alias_by_slot_id, + aliases_by_slot_id, + source_model, + source_relation, + shifted_where_parts, + shifted_where_join_paths, + planned_query, + bundle, + ): + """Emit the ``shifted_`` + ``sjoin_`` CTE pair for each ready + ``time_shift`` layer's slot. Returns the advanced ``chain_tail``.""" + for layer in ready_time_shift: + for slot_id in layer.slot_ids: + slot = slots_by_id[slot_id] + chain_tail = self._emit_time_shift_ctes_for_planned( + slot=slot, + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + source_model=source_model, + source_relation=source_relation, + shifted_where_parts=shifted_where_parts, + shifted_where_join_paths=shifted_where_join_paths, + planned_query=planned_query, + bundle=bundle, + ) + return chain_tail + + def _emit_cp_layers( + self, + *, + ready_cp, + ctes, + chain_tail, + cte_allocator, + slots_by_id, + slot_id_by_key, + available_alias_by_slot_id, + aliases_by_slot_id, + planned_query, + source_relation, + ): + """Emit the ``cp_reset_`` + ``cp_value_`` CTE pair for each ready + ``consecutive_periods`` layer's slot. Returns the advanced ``chain_tail``. + """ + for layer in ready_cp: + for slot_id in layer.slot_ids: + slot = slots_by_id[slot_id] + chain_tail = self._emit_consecutive_periods_ctes_for_planned( + slot=slot, + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + planned_query=planned_query, + source_relation=source_relation, + ) + return chain_tail + + def _emit_unmaterialised_post_phase_step( + self, + *, + ctes, + chain_tail, + cte_allocator, + step_num, + slots_by_id, + slot_id_by_key, + available_alias_by_slot_id, + aliases_by_slot_id, + source_relation, + planned_query, + ) -> tuple: + """Materialise projected POST-phase ``ArithmeticKey`` / ``ScalarCallKey`` + slots no transform layer rendered (``change`` / ``change_pct`` desugar to + an outer subtraction/division over a ``time_shift`` slot; ``cumsum(x)+1`` + is the window analogue). Transform-key slots are materialised by their + layers, so they are skipped. Returns ``(new_chain_tail, new_step_num)``. + """ from slayer.core.keys import ( ArithmeticKey as _ArithKey, ScalarCallKey as _ScalarKey, @@ -1765,7 +1925,6 @@ def _generate_from_planned_impl( # NOSONAR(S3776) — top-level dispatch over c unmaterialised: list = [] for cslot in planned_query.combined_expression_slots: if isinstance(cslot.key, _TKey): - # Transform-key slots are materialised by transform_layers. continue if cslot.id in aliases_by_slot_id: continue @@ -1775,9 +1934,7 @@ def _generate_from_planned_impl( # NOSONAR(S3776) — top-level dispatch over c step_num += 1 step_name = cte_allocator.allocate_cte(f"step{step_num}") prev_cte = chain_tail - carry_aliases = self._carry_aliases_in_plan_order( - aliases_by_slot_id, - ) + carry_aliases = self._carry_aliases_in_plan_order(aliases_by_slot_id) step_parts = [exp.column(a, quoted=True) for a in carry_aliases] for cslot in unmaterialised: alias = ( @@ -1799,35 +1956,39 @@ def _generate_from_planned_impl( # NOSONAR(S3776) — top-level dispatch over c if cslot.type is not None: rendered = _wrap_cast_for_type(rendered, cslot.type) step_parts.append(rendered.as_(full_alias, quoted=True)) - aliases_by_slot_id.setdefault(cslot.id, []).append( - full_alias, - ) - available_alias_by_slot_id.setdefault( - cslot.id, full_alias, - ) + aliases_by_slot_id.setdefault(cslot.id, []).append(full_alias) + available_alias_by_slot_id.setdefault(cslot.id, full_alias) ctes.append(CteEntry( name=step_name, query=exp.Select().select(*step_parts).from_(prev_cte), depends_on=[prev_cte], )) chain_tail = step_name + return chain_tail, step_num - # Inner SELECT inside _outer wrap: ALL carried aliases sorted - # in PLAN order (B8 — this list used to be sorted alphabetically to - # match the legacy renderer byte-for-byte). + def _finalize_planned_transform_chain( + self, + *, + ctes, + chain_tail, + slots_by_id, + slot_id_by_key, + available_alias_by_slot_id, + aliases_by_slot_id, + planned_query, + ) -> str: + """Assemble the ``WITH`` chain, apply the POST-phase filter wrap, and emit + the outer projection wrap in user-projection order (per-slot index walks + C13 duplicate aliases). Returns the finished statement SQL.""" final_cte = chain_tail inner_aliases = self._carry_aliases_in_plan_order(aliases_by_slot_id) inner_select = exp.Select().select( *(exp.column(a, quoted=True) for a in inner_aliases), ).from_(final_cte) - chain_sql = assemble_with_chain( entries=ctes, final=inner_select, ).sql(dialect=self.dialect, pretty=True) - # POST-phase filter wrap (filters referencing transform / arith - # slots). Mirrors legacy _generate_with_computed:1627-1648 — - # ``SELECT * FROM () AS _filtered WHERE ``. post_filter_conditions = self._render_post_phase_filter_conditions( planned_query=planned_query, slot_id_by_key=slot_id_by_key, @@ -1839,9 +2000,6 @@ def _generate_from_planned_impl( # NOSONAR(S3776) — top-level dispatch over c f"\nWHERE {_SQL_AND_JOINER.join(post_filter_conditions)}" ) - # Outer SELECT in user-projection order (public slots only). - # Per-slot index walks each slot's public_aliases so duplicate - # interned names (DEV-1450 C13) both surface in the result. public_aliases_user_order: list[str] = [] outer_alias_index: Dict[str, int] = {} for sid in planned_query.projection: @@ -4578,6 +4736,8 @@ def _render_outer_composite(cslot) -> exp.Expression: slots_by_id=slots_by_id, combined_aliases_by_slot_id=combined_aliases_by_slot_id, source_relation=source_relation, + source_model=source_model, + bundle=bundle, ) # Assemble the WITH chain (§5.6). Dependencies are DECLARED, not @@ -4680,6 +4840,47 @@ def _render_outer_composite(cslot) -> exp.Expression: # may re-enable it. return combined_statement.sql(dialect=self.dialect, pretty=True) + def _guard_target_grain_time_shift( + self, *, planned_query, slots_by_id, slot_id_by_key, + ) -> None: + """Raise the narrowed 7b.15e guard for a ``time_shift`` over a + target-grain cross-model aggregate — re-aggregating it host-rooted in the + shifted CTE would multiply target rows through the 1:N join (DEV-1750). + + Target-grain is read from plan ownership: the inner aggregate's + ``CrossModelAggregatePlan.cte_root_model is None`` (host-rooted isolation + sets it to the host name). A local inner (no plan) or host-rooted inner + renders; ``consecutive_periods`` never re-aggregates and is exempt. + """ + from slayer.core.keys import TransformKey + + target_rooted_agg_slot_ids = { + p.aggregate_slot_id + for p in planned_query.cross_model_aggregate_plans + if p.cte_root_model is None + } + if not target_rooted_agg_slot_ids: + return + for layer in planned_query.transform_layers: + if layer.op != "time_shift": + continue + for sid in layer.slot_ids: + slot = slots_by_id.get(sid) + if slot is None or not isinstance(slot.key, TransformKey): + continue + inner_sid = slot_id_by_key.get(slot.key.input) + if inner_sid in target_rooted_agg_slot_ids: + raise NotImplementedError( + "DEV-1450 stage 7b.15e: time_shift over a TARGET-GRAIN " + "cross-model aggregate (its inner aggregate is grouped " + "at a joined target's grain, not the host's) is not yet " + "rendered — the shifted CTE would re-aggregate it " + "host-rooted and multiply target rows through the 1:N " + "join. Local and host-grain inner aggregates DO render. " + "Factor the temporal transform into an earlier stage, or " + "drop the cross-grain part.", + ) + def _render_cross_model_transform_chain( # NOSONAR(S3776) — pre-existing complexity in the window-layer chain; this PR only threaded the CTE-name allocator through it, which re-attributed the function as new code. The chain is rebuilt as sqlglot AST in the scope-assembly PR, where the layering is what gets simplified. self, *, @@ -4689,28 +4890,26 @@ def _render_cross_model_transform_chain( # NOSONAR(S3776) — pre-existing comp slots_by_id: Dict[str, Any], combined_aliases_by_slot_id: Dict[str, List[str]], source_relation: str, + source_model, + bundle, ) -> str: - """Render window-transform layers over a cross-model combined result. + """Render transform layers over a cross-model combined result. DEV-1450 stage 7b.15e (C2). The combined cross-model SELECT becomes the - ``base`` CTE; window step CTEs (``cumsum`` / ``lag`` / ``lead`` / - ``rank`` …) are layered above it exactly like the local transform path - in ``generate_from_planned``, then an outer wrap projects the public - slots in user order and applies ORDER BY / LIMIT / OFFSET. - - ``time_shift`` / ``consecutive_periods`` over a cross-model aggregate - re-aggregate the *source* and are out of slice scope — they raise. + ``base`` CTE; step CTEs are layered above it exactly like the local + transform path in ``generate_from_planned``, then an outer wrap projects + the public slots in user order and applies ORDER BY / LIMIT / OFFSET. + + DEV-1750: window (``cumsum`` / ``lag`` / ``lead`` / ``rank``), + ``time_shift`` and ``consecutive_periods`` layers all render here — the + Kahn loop dispatches each op to the SAME per-op emitter the local chain + uses, so the shifted-CTE join discovery (Part 1) serves both chains. + Only one shape stays guarded: a ``time_shift`` whose inner aggregate is a + TARGET-GRAIN cross-model aggregate (``cte_root_model is None``) — + re-aggregating it host-rooted in the shifted CTE would multiply target + rows through the 1:N join. ``consecutive_periods`` reads a materialised + alias and never re-aggregates, so it has no such failure mode. """ - for layer in planned_query.transform_layers: - if layer.op in ("time_shift", "consecutive_periods"): - raise NotImplementedError( - f"DEV-1450 stage 7b.15e: self-join transform op " - f"{layer.op!r} is not yet rendered in a query that also has " - f"a cross-model aggregate (window transforms such as cumsum " - f"/ lag / lead / rank are). Factor the temporal transform " - f"(or change / change_pct, which desugar to time_shift) " - f"into an earlier stage.", - ) ctes: List[CteEntry] = [ CteEntry(name=name, query=query) for name, query in prelude_ctes @@ -4740,165 +4939,125 @@ def _render_cross_model_transform_chain( # NOSONAR(S3776) — pre-existing comp sid: a[0] for sid, a in aliases_by_slot_id.items() if a } - # Window-transform Kahn batches (one step CTE per ready batch). + # DEV-1750 narrowed 7b.15e guard — runs before the emitter loop so a + # guarded shape never reaches the shifted CTE. + self._guard_target_grain_time_shift( + planned_query=planned_query, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + ) + + # 7b.11 — WHERE-able row-phase filters for the shifted CTE (source minus + # BetweenKey date_range bounds), built once like the local chain. Only a + # time_shift layer consumes them, so skip the work (and its filter-parse + # failure surface) for a window-only / cp-only chain. + if any( + layer.op == "time_shift" for layer in planned_query.transform_layers + ): + shifted_where_parts, shifted_where_join_paths = ( + self._build_shifted_cte_where_parts( + planned_query=planned_query, + source_relation=source_relation, + source_model=source_model, + bundle=bundle, + ) + ) + else: + shifted_where_parts, shifted_where_join_paths = [], [] + + # Transform Kahn batches (one step CTE per ready batch). Ready layers are + # split by op so each dispatches to the SAME per-op emitter the local + # chain uses (DEV-1750): window layers to the batch step below, temporal + # ops to their shifted / cp CTE emitters. pending_layers = list(planned_query.transform_layers) step_num = 0 # Explicit chain tail (see the host transform chain above): the CTE the # next step reads from, tracked directly instead of as ``ctes[-1]``. chain_tail = ctes[-1].name while pending_layers: - ready: list = [] - not_ready: list = [] - for layer in pending_layers: - if self._transform_layer_deps_ready( - layer=layer, + (ready_window, ready_time_shift, ready_cp, not_ready) = ( + self._classify_ready_transform_layers( + pending_layers=pending_layers, slots_by_id=slots_by_id, slot_id_by_key=slot_id_by_key, available_alias_by_slot_id=available_alias_by_slot_id, - ): - ready.append(layer) - else: - not_ready.append(layer) - if not ready: + ) + ) + if not (ready_window or ready_time_shift or ready_cp): pending_ops = [layer.op for layer in pending_layers] raise RuntimeError( f"DEV-1450 stage 7b.15e: cross-model transform layer " f"dependencies could not be resolved; pending ops: " f"{pending_ops!r}.", ) - step_num += 1 - step_name = cte_allocator.allocate_cte(f"step{step_num}") - prev_cte = chain_tail - carry_aliases = self._carry_aliases_in_plan_order( - aliases_by_slot_id, + # This chain dispatches the temporal ops first, then the window batch + # (the local chain reverses that order — DEV-1799 unifies it). + chain_tail = self._emit_time_shift_layers( + ready_time_shift=ready_time_shift, + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + source_model=source_model, + source_relation=source_relation, + shifted_where_parts=shifted_where_parts, + shifted_where_join_paths=shifted_where_join_paths, + planned_query=planned_query, + bundle=bundle, ) - step_parts = [exp.column(a, quoted=True) for a in carry_aliases] - for layer in ready: - for slot_id in layer.slot_ids: - slot = slots_by_id[slot_id] - alias = ( - slot.public_aliases[0] - if slot.public_aliases - else slot.declared_name - ) - full_alias = f"{source_relation}.{alias}" - window_expr = self._render_window_transform_sql( - slot=slot, - slots_by_id=slots_by_id, - slot_id_by_key=slot_id_by_key, - available_alias_by_slot_id=available_alias_by_slot_id, - planned_query=planned_query, - ) - if slot.type is not None: - window_expr = _wrap_cast_for_type( - window_expr, slot.type, - ) - step_parts.append( - window_expr.as_(full_alias, quoted=True), - ) - aliases_by_slot_id.setdefault(slot_id, []).append(full_alias) - available_alias_by_slot_id.setdefault(slot_id, full_alias) - ctes.append(CteEntry( - name=step_name, - query=exp.Select().select(*step_parts).from_(prev_cte), - depends_on=[prev_cte], - )) - chain_tail = step_name - pending_layers = not_ready - - # Materialise any projected POST-phase ArithmeticKey / ScalarCallKey - # slot a window layer didn't render (``cumsum(x) + 1``-style combos). - from slayer.core.keys import ( - ArithmeticKey as _ArithKey, - ScalarCallKey as _ScalarKey, - TransformKey as _TKey, - ) - unmaterialised: list = [] - for cslot in planned_query.combined_expression_slots: - if isinstance(cslot.key, _TKey): - continue - if cslot.id in aliases_by_slot_id: - continue - if isinstance(cslot.key, (_ArithKey, _ScalarKey)): - unmaterialised.append(cslot) - if unmaterialised: - step_num += 1 - step_name = cte_allocator.allocate_cte(f"step{step_num}") - prev_cte = chain_tail - carry_aliases = self._carry_aliases_in_plan_order( - aliases_by_slot_id, + chain_tail = self._emit_cp_layers( + ready_cp=ready_cp, + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + planned_query=planned_query, + source_relation=source_relation, ) - step_parts = [exp.column(a, quoted=True) for a in carry_aliases] - for cslot in unmaterialised: - alias = ( - cslot.public_aliases[0] - if cslot.public_aliases - else cslot.declared_name - ) - full_alias = f"{source_relation}.{alias}" - rendered = render_value_key( - key=cslot.key, - ctx=RenderContext( - dialect=self._dialect, - aliases=AliasFacilities( - slot_id_by_key=slot_id_by_key, - available_alias_by_slot_id=available_alias_by_slot_id, - ), - ), + if ready_window: + chain_tail, step_num = self._emit_window_batch_step( + ready_window=ready_window, + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + step_num=step_num, + slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, + available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + source_relation=source_relation, + planned_query=planned_query, ) - if cslot.type is not None: - rendered = _wrap_cast_for_type(rendered, cslot.type) - step_parts.append(rendered.as_(full_alias, quoted=True)) - aliases_by_slot_id.setdefault(cslot.id, []).append(full_alias) - available_alias_by_slot_id.setdefault(cslot.id, full_alias) - ctes.append(CteEntry( - name=step_name, - query=exp.Select().select(*step_parts).from_(prev_cte), - depends_on=[prev_cte], - )) - chain_tail = step_name - - final_cte = chain_tail - inner_aliases = self._carry_aliases_in_plan_order(aliases_by_slot_id) - inner_select = exp.Select().select( - *(exp.column(a, quoted=True) for a in inner_aliases), - ).from_(final_cte) - chain_sql = assemble_with_chain( - entries=ctes, final=inner_select, - ).sql(dialect=self.dialect, pretty=True) + pending_layers = not_ready - post_filter_conditions = self._render_post_phase_filter_conditions( - planned_query=planned_query, + # Materialise any projected POST-phase arith/scalar slot no window layer + # rendered (``cumsum(x) + 1``-style combos), then assemble and wrap. + chain_tail, step_num = self._emit_unmaterialised_post_phase_step( + ctes=ctes, + chain_tail=chain_tail, + cte_allocator=cte_allocator, + step_num=step_num, + slots_by_id=slots_by_id, slot_id_by_key=slot_id_by_key, available_alias_by_slot_id=available_alias_by_slot_id, - ) - if post_filter_conditions: - chain_sql = ( - f"SELECT *\nFROM (\n{chain_sql}\n) AS {FILTERED_ALIAS}" - f"\nWHERE {_SQL_AND_JOINER.join(post_filter_conditions)}" - ) - - public_aliases_user_order: list[str] = [] - outer_alias_index: Dict[str, int] = {} - for sid in planned_query.projection: - slot = slots_by_id[sid] - if slot.hidden: - continue - all_aliases = aliases_by_slot_id.get(sid, []) - if not all_aliases: - continue - idx = outer_alias_index.setdefault(sid, 0) - alias = ( - all_aliases[idx] if idx < len(all_aliases) else all_aliases[-1] - ) - outer_alias_index[sid] = idx + 1 - public_aliases_user_order.append(alias) - return self._emit_planned_outer_wrap( - chain_sql=chain_sql, - public_aliases=public_aliases_user_order, + aliases_by_slot_id=aliases_by_slot_id, + source_relation=source_relation, planned_query=planned_query, + ) + return self._finalize_planned_transform_chain( + ctes=ctes, + chain_tail=chain_tail, slots_by_id=slots_by_id, + slot_id_by_key=slot_id_by_key, available_alias_by_slot_id=available_alias_by_slot_id, + aliases_by_slot_id=aliases_by_slot_id, + planned_query=planned_query, ) def _canonical_cross_model_alias( @@ -6774,17 +6933,32 @@ def _add_partition(pk_obj, *, where: str) -> None: for pk in sorted(key.partition_keys, key=lambda k: repr(k)): _add_partition(pk, where="partition_key") - # DEV-1711 defensive completeness: a LOCAL aggregate whose source / - # column-filter / kwargs cross a join is isolated upstream (Stage 5) and - # would have raised 7b.15e before reaching a time_shift CTE, so these - # registrations are provably no-ops today — but routing them through the - # scope keeps Law 1 total (no render path skips join discovery). + # DEV-1750: the shifted CTE re-aggregates the inner aggregate host-rooted, + # so every crossing input registers into ``shifted_scope`` exactly as the + # base SELECT (``_resolve_agg_inputs_via_scope``) and the ``_cm_`` CTE do + # — otherwise a crossing default param (``w='customers__regions.weight'``) + # re-aggregates with no join to regions, which no database binds. if isinstance(inner_key, AggregateKey): - if isinstance(inner_key.source, ColumnSqlKey): + # source: derived (crosses inside Column.sql) or path-bearing. + if isinstance(inner_key.source, ColumnSqlKey) or getattr( + inner_key.source, "path", (), + ): shifted_scope.resolve(inner_key.source) + # positional args (first/last time arg); skip the DEV-1526 + # path-bearing ColumnSqlKey residual (bogus join if anchored here). + for _arg in inner_key.args: + if isinstance(_arg, ColumnSqlKey) and _arg.path: + continue + if isinstance(_arg, (ColumnKey, ColumnSqlKey)): + shifted_scope.resolve(_arg) for _kname, _kval in inner_key.kwargs: if isinstance(_kval, (ColumnKey, ColumnSqlKey)): shifted_scope.resolve(_kval) + # template fragments + non-overridden default AggregationParam.sql — + # the one door shared with the host + ``_cm_`` paths (DEV-1745 W2). + self._register_fragment_kwarg_joins( + key=inner_key, scope=shifted_scope, model=source_model, + ) if ( inner_key.column_filter_key is not None and inner_key.column_filter_key.canonical_sql @@ -6844,6 +7018,17 @@ def _add_partition(pk_obj, *, where: str) -> None: # Aggregate: re-emit the AggregateKey using the same synth / # _build_agg dance the base CTE uses. if isinstance(inner_key, AggregateKey): + # DEV-1750: a first/last inner ranks via a ROW_NUMBER subquery, not + # the flat ``_build_agg`` re-aggregation the shifted CTE performs — + # fail loudly rather than let ``_build_agg`` hit a null node_class. + if inner_key.agg in ("first", "last"): + raise NotImplementedError( + f"DEV-1450 stage 7b.15e: time_shift over a ranked " + f"{inner_key.agg!r} aggregate is not yet rendered — the " + f"shifted CTE re-aggregates flatly and cannot reproduce the " + f"ROW_NUMBER ranking. Factor the temporal transform into an " + f"earlier stage. (slot id={slot.id!r})", + ) # Build a synth ``AggRenderSpec`` for _build_agg. # # The renderer needs a slot-like input with declared_name + diff --git a/tests/_dev1750_fixtures.py b/tests/_dev1750_fixtures.py new file mode 100644 index 00000000..898474b1 --- /dev/null +++ b/tests/_dev1750_fixtures.py @@ -0,0 +1,310 @@ +"""Shared fixtures for DEV-1750 — time_shift / consecutive_periods over a +cross-model or crossing-fragment aggregate. + +The models mirror ``tests/test_dev1745_fragment_joins.py`` (orders → customers → +regions, ``wscaled_sum`` template crossing a join) so the shifted-CTE half reads +against the same shape the ``_cm_`` half was fixed against. Underscore-prefixed +so pytest skips collection here (like ``tests/_engine_helpers.py``). + +The three time_shift inner-aggregate shapes DEV-1750 distinguishes: + +* **(a) local inner** — ``time_shift(amount:sum, -1)`` with a *sibling* + cross-model measure. Guarded today only by association. +* **(b) host-rooted inner** — ``time_shift(amount:wscaled_sum, -1)`` whose + default param crosses ``orders → customers → regions``. The crossing fragment + isolates the aggregate host-rooted (``cte_root_model`` = host); the shifted CTE + re-aggregates host-rooted with the fragment's join pulled in (Part 1). +* **(c) target-grain inner** — ``time_shift(customers.spend:sum, -1)``. + Target-rooted (``cte_root_model`` is None); host-rooted re-aggregation would + multiply target rows, so it stays behind the narrowed guard. +""" + +from __future__ import annotations + +import os +import sqlite3 +import tempfile +from typing import AsyncIterator, List + +import pytest + +from slayer.core.enums import DataType, TimeGranularity +from slayer.core.models import ( + Aggregation, + AggregationParam, + Column, + DatasourceConfig, + ModelJoin, + ModelMeasure, + SlayerModel, +) +from slayer.core.query import ColumnRef, SlayerQuery, TimeDimension +from slayer.engine.query_engine import SlayerQueryEngine +from slayer.storage.yaml_storage import YAMLStorage + +from tests._dev1746_fixtures import cte_names_in_order +from tests._engine_helpers import _engine_generate, _extract_cte_body + + +# --------------------------------------------------------------------------- # +# Models — orders (host) → customers → regions. +# --------------------------------------------------------------------------- # +def regions_model() -> SlayerModel: + return SlayerModel( + name="regions", data_source="test", sql_table="regions", + columns=[ + Column(name="id", type=DataType.INT, primary_key=True), + Column(name="weight", type=DataType.DOUBLE), + ], + ) + + +def customers_model() -> SlayerModel: + """``customers`` carries ``spend`` (the target-grain measure for shape (c)) + and a ``signup_at`` a first/last explicit time arg can rank by.""" + return SlayerModel( + name="customers", data_source="test", sql_table="customers", + columns=[ + Column(name="id", type=DataType.INT, primary_key=True), + Column(name="region_id", type=DataType.INT), + Column(name="spend", type=DataType.DOUBLE), + Column(name="signup_at", type=DataType.TIMESTAMP), + ], + joins=[ModelJoin(target_model="regions", join_pairs=[["region_id", "id"]])], + ) + + +def line_items_model() -> SlayerModel: + """A 1:N child of ``orders`` (each order has several line items). The + ``liscaled_sum`` fragment crosses this ONE-TO-MANY join, so a wrong host-base + isolation would fan the join out and MULTIPLY sibling measures — which is + what the sibling-protection execution test detects (Codex F1).""" + return SlayerModel( + name="line_items", data_source="test", sql_table="line_items", + columns=[ + Column(name="id", type=DataType.INT, primary_key=True), + Column(name="order_id", type=DataType.INT), + Column(name="factor", type=DataType.DOUBLE), + ], + ) + + +def orders_model() -> SlayerModel: + """Host model. ``wscaled_sum`` is declared HERE with a default param crossing + two hops (``customers__regions.weight``), so ``amount:wscaled_sum`` is the + host-rooted crossing-fragment aggregate (shape (b)). ``liscaled_sum`` crosses + the 1:N ``line_items`` join for the sibling-protection fan-out proof.""" + return SlayerModel( + name="orders", data_source="test", sql_table="orders", + default_time_dimension="ordered_at", + columns=[ + Column(name="id", type=DataType.INT, primary_key=True), + Column(name="customer_id", type=DataType.INT), + Column(name="amount", type=DataType.DOUBLE), + Column(name="status", type=DataType.TEXT), + Column(name="ordered_at", type=DataType.TIMESTAMP), + ], + joins=[ + ModelJoin(target_model="customers", join_pairs=[["customer_id", "id"]]), + ModelJoin(target_model="line_items", join_pairs=[["id", "order_id"]]), + ], + aggregations=[ + Aggregation( + name="wscaled_sum", formula="SUM({value} * {w})", + params=[AggregationParam( + name="w", sql="customers__regions.weight", + )], + ), + Aggregation( + name="liscaled_sum", formula="SUM({value} * {f})", + params=[AggregationParam(name="f", sql="line_items.factor")], + ), + ], + ) + + +def dev1750_models() -> List[SlayerModel]: + """``[host, *referenced]`` in the order ``_engine_generate`` wants.""" + return [orders_model(), customers_model(), regions_model(), line_items_model()] + + +# --------------------------------------------------------------------------- # +# SQL-shape generation. +# --------------------------------------------------------------------------- # +async def gen(query: SlayerQuery, *, dialect: str = "duckdb") -> str: + models = dev1750_models() + return await _engine_generate( + query=query, model=models[0], extra_models=models[1:], + dialect=dialect, validate=False, + ) + + +def month_td(column: str = "ordered_at") -> List[TimeDimension]: + return [TimeDimension( + dimension=ColumnRef(name=column), + granularity=TimeGranularity.MONTH, + )] + + +def shifted_cte_body(sql: str) -> str: + """Rendered body of the sole ``shifted_*`` CTE — scope join-registration + assertions belong here, not to whole-SQL substring checks a valid alias + elsewhere could satisfy. Balanced-paren extraction (like DEV-1474) so it + finds the CTE even when the whole ``WITH`` is nested inside the transform + chain's outer ``FROM ( … ) AS _outer`` wrap (sqlglot's top-level CTE walk + does not descend into it).""" + return _extract_cte_body(sql, r"shifted_\w+") + + +def base_cte_body(sql: str) -> str: + """Rendered body of the host ``_base`` CTE (the local-slot spine — where a + sibling ``amount:sum`` lives and the crossing re-aggregation must NOT).""" + return _extract_cte_body(sql, r"_base") + + +def cte_names(sql: str, *, dialect: str = "duckdb") -> List[str]: + return cte_names_in_order(sql, dialect=dialect) + + +# --------------------------------------------------------------------------- # +# Execution dataset — hand-computable. +# --------------------------------------------------------------------------- # +# regions: 1 → weight 2.0, 2 → weight 3.0 +# customers: 1 → region 1 (w=2), 2 → region 2 (w=3), 3 → region 1 (w=2) +# (customer 3 has a NULL-region path only if region_id absent — here +# every customer has a region so weights are well-defined) +# orders (id, customer_id, amount, status, ordered_at): +# Jan: (1,1,10,'ok'), (2,2,5,'ok') amount:sum=15 wscaled=10*2+5*3=35 +# Feb: (3,1,20,'ok') amount:sum=20 wscaled=20*2=40 +# Mar: (4,2,10,'hold') amount:sum=10 wscaled=10*3=30 +# plus a NULL-status row to prove null-safe partition survival: +# Feb: (5,1,4,NULL) (own status group) +# Mar: (6,1,8,NULL) +# +# customers.spend: 1→100, 2→200, 3→50 +# --------------------------------------------------------------------------- # +_REGIONS_ROWS = [(1, 2.0), (2, 3.0)] +_CUSTOMERS_ROWS = [ + # (id, region_id, spend, signup_at) + (1, 1, 100.0, "2020-01-01"), + (2, 2, 200.0, "2020-06-01"), + (3, 1, 50.0, "2021-01-01"), +] +_ORDERS_ROWS = [ + # (id, customer_id, amount, status, ordered_at) + (1, 1, 10.0, "ok", "2024-01-15"), + (2, 2, 5.0, "ok", "2024-01-20"), + (3, 1, 20.0, "ok", "2024-02-10"), + (4, 2, 10.0, "hold", "2024-03-05"), + (5, 1, 4.0, None, "2024-02-12"), + (6, 1, 8.0, None, "2024-03-08"), +] +# line_items (id, order_id, factor) — order 1 fans out to TWO rows, so a leaked +# join would count order 1's amount twice in a sibling amount:sum. +# liscaled_sum = SUM(amount * factor): +# Jan 10*1 + 10*3 + 5*2 = 50 +# Feb 20*1 + 4*1 = 24 +# Mar 10*1 + 8*1 = 18 +_LINE_ITEMS_ROWS = [ + (1, 1, 1.0), (2, 1, 3.0), # order 1 → two line items + (3, 2, 2.0), + (4, 3, 1.0), + (5, 4, 1.0), + (6, 5, 1.0), + (7, 6, 1.0), +] + + +def _seed_sqlite(db_path: str) -> None: + con = sqlite3.connect(db_path) + cur = con.cursor() + cur.execute("CREATE TABLE regions (id INTEGER PRIMARY KEY, weight REAL)") + cur.executemany("INSERT INTO regions VALUES (?,?)", _REGIONS_ROWS) + cur.execute( + "CREATE TABLE customers (id INTEGER PRIMARY KEY, region_id INTEGER, " + "spend REAL, signup_at TEXT)" + ) + cur.executemany("INSERT INTO customers VALUES (?,?,?,?)", _CUSTOMERS_ROWS) + cur.execute( + "CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, " + "amount REAL, status TEXT, ordered_at TEXT)" + ) + cur.executemany("INSERT INTO orders VALUES (?,?,?,?,?)", _ORDERS_ROWS) + cur.execute( + "CREATE TABLE line_items (id INTEGER PRIMARY KEY, order_id INTEGER, " + "factor REAL)" + ) + cur.executemany("INSERT INTO line_items VALUES (?,?,?)", _LINE_ITEMS_ROWS) + con.commit() + con.close() + + +def _seed_duckdb(db_path: str) -> None: + duckdb = pytest.importorskip("duckdb") + con = duckdb.connect(db_path) + con.execute("CREATE TABLE regions (id INTEGER, weight DOUBLE)") + con.executemany("INSERT INTO regions VALUES (?,?)", _REGIONS_ROWS) + con.execute( + "CREATE TABLE customers (id INTEGER, region_id INTEGER, spend DOUBLE, " + "signup_at TIMESTAMP)" + ) + con.executemany("INSERT INTO customers VALUES (?,?,?,?)", _CUSTOMERS_ROWS) + con.execute( + "CREATE TABLE orders (id INTEGER, customer_id INTEGER, amount DOUBLE, " + "status VARCHAR, ordered_at TIMESTAMP)" + ) + con.executemany("INSERT INTO orders VALUES (?,?,?,?,?)", _ORDERS_ROWS) + con.execute("CREATE TABLE line_items (id INTEGER, order_id INTEGER, factor DOUBLE)") + con.executemany("INSERT INTO line_items VALUES (?,?,?)", _LINE_ITEMS_ROWS) + con.close() + + +async def _engine_for(*, dialect: str, db_path: str) -> SlayerQueryEngine: + storage = YAMLStorage(base_dir=os.path.join(os.path.dirname(db_path), "store")) + await storage.save_datasource( + DatasourceConfig(name="test", type=dialect, database=db_path) + ) + for model in dev1750_models(): + await storage.save_model(model, _validate=False) + return SlayerQueryEngine(storage=storage) + + +async def make_exec_engine(request) -> AsyncIterator[SlayerQueryEngine]: + """Body for a ``params=["sqlite", "duckdb"]`` fixture — the issue's required + execution backends. A test module wraps this in ``@pytest.fixture`` so the + fixture name lives where it is consumed (no cross-module import that would + shadow the parameter). Each yields an engine over the hand-computed dataset. + """ + dialect = request.param + if dialect == "duckdb": + pytest.importorskip("duckdb") + with tempfile.TemporaryDirectory() as d: + db_path = os.path.join(d, f"data.{dialect}") + if dialect == "sqlite": + _seed_sqlite(db_path) + else: + _seed_duckdb(db_path) + engine = await _engine_for(dialect=dialect, db_path=db_path) + yield engine + + +def month_key(value) -> str: + """First 7 chars of a DATE_TRUNC'd month value — a stable per-month key + across SQLite (``2024-01-01`` text) and DuckDB (``2024-01-01 00:00:00``).""" + return str(value)[:7] + + +def rows_by(resp, *keys) -> dict: + """Index ``resp.data`` rows by the given result-column key tuple.""" + out = {} + for r in resp.data: + out[tuple(r[k] for k in keys)] = r + return out + + +__all__ = [ + "orders_model", "customers_model", "regions_model", "dev1750_models", + "gen", "month_td", "shifted_cte_body", "base_cte_body", "cte_names", + "make_exec_engine", "month_key", "rows_by", "SlayerQuery", "ModelMeasure", + "ColumnRef", "TimeDimension", "TimeGranularity", +] diff --git a/tests/_golden_harness.py b/tests/_golden_harness.py index 6c5ce2c6..1747d6e9 100644 --- a/tests/_golden_harness.py +++ b/tests/_golden_harness.py @@ -34,16 +34,18 @@ import os import re from pathlib import Path -from typing import Awaitable, Callable, Dict, Iterable, Mapping, Optional +from typing import Any, Awaitable, Callable, Coroutine, Dict, Iterable, Mapping, Optional import pytest __all__ = [ "GoldenSuite", + "bind_golden_tests", "build_baseline", "expected_keys", "load_or_regenerate", "merge_regenerated", + "record_raise", "render_value", ] @@ -244,3 +246,81 @@ def assert_allowed_deltas_carry_a_reason(self) -> None: f"every allowed delta must say WHY the SQL is permitted to change: " f"{blank}" ) + + +#: A ``(query, dialect) -> SQL string`` (or recorded raise) generator, bound to +#: one module's model fixtures. The query param is ``Any`` so a module's +#: concretely-typed ``_generate_one(SlayerQuery, str)`` binds without variance +#: friction; the return is a coroutine so ``asyncio.run`` accepts it. +GenerateOne = Callable[[Any, str], Coroutine[Any, Any, object]] + + +def bind_golden_tests( + *, + namespace: Dict, + golden_path: Path, + cases: Callable[[], Dict], + dialects: Iterable[str], + allowed: Mapping[str, str], + generate_one: GenerateOne, +) -> None: + """Inject the standard golden ``baseline`` fixture and five shared test + functions into a module's ``namespace`` (pass ``globals()``). + + Every golden module ran the SAME blessing-loop wiring — a module-scoped + ``baseline`` fixture, a parametrised match test, and the four + coverage/orphan/manifest guards — differing only in its case matrix, dialect + list, baseline path, and which model fixtures ``generate_one`` renders + against. This binds that wiring once; a caller keeps only what is genuinely + its own (the matrix, the path, and any invariant specific to what it pins, + which it defines normally alongside the injected names). + """ + dialect_list = list(dialects) + case_ids = sorted(cases()) + + def _suite() -> GoldenSuite: + return GoldenSuite( + case_ids=sorted(cases()), dialects=dialect_list, allowed=allowed, + ) + + async def _render(case_id: str, dialect: str): + return await generate_one(cases()[case_id], dialect) + + @pytest.fixture(scope="module") + def baseline() -> Dict: + return load_or_regenerate( + path=golden_path, case_ids=sorted(cases()), dialects=dialect_list, + render=_render, allowed=allowed, + ) + + @pytest.mark.parametrize("case_id", case_ids) + @pytest.mark.parametrize("dialect", dialect_list) + def test_emitted_sql_matches_golden(case_id: str, dialect: str, baseline) -> None: + _suite().assert_matches( + key=f"{case_id}::{dialect}", + actual=asyncio.run(generate_one(cases()[case_id], dialect)), + baseline=baseline, + ) + + def test_baseline_covers_every_case_and_dialect(baseline) -> None: + _suite().assert_covers_every_case(baseline) + + def test_baseline_has_no_orphan_entries(baseline) -> None: + _suite().assert_no_orphans(baseline) + + def test_allowed_deltas_name_real_keys() -> None: + _suite().assert_allowed_deltas_name_real_keys() + + def test_allowed_deltas_carry_a_reason() -> None: + _suite().assert_allowed_deltas_carry_a_reason() + + namespace.update( + baseline=baseline, + test_emitted_sql_matches_golden=test_emitted_sql_matches_golden, + test_baseline_covers_every_case_and_dialect=( + test_baseline_covers_every_case_and_dialect + ), + test_baseline_has_no_orphan_entries=test_baseline_has_no_orphan_entries, + test_allowed_deltas_name_real_keys=test_allowed_deltas_name_real_keys, + test_allowed_deltas_carry_a_reason=test_allowed_deltas_carry_a_reason, + ) diff --git a/tests/golden/dev1750_sql_baseline.json b/tests/golden/dev1750_sql_baseline.json new file mode 100644 index 00000000..4868cd72 --- /dev/null +++ b/tests/golden/dev1750_sql_baseline.json @@ -0,0 +1,127 @@ +{ + "a/local_ts_cm_sibling::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___cm`,\n `orders___prev`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n SUM(orders.amount) AS `orders___amount_sum`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS `orders___customers___spend_sum`\n FROM customers AS customers\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _cm_orders__customers__spend_sum.`orders___customers___spend_sum` AS `orders___cm`,\n _base.`orders___amount_sum`\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted_prev AS (\n SELECT\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH) AS `orders___ordered_at`,\n SUM(orders.amount) AS `orders___amount_sum`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH)\n), sjoin_prev AS (\n SELECT\n base.`orders___ordered_at`,\n base.`orders___amount_sum`,\n base.`orders___cm`,\n shifted_prev.`orders___amount_sum` AS `orders___prev`\n FROM base\n LEFT JOIN shifted_prev\n ON base.`orders___ordered_at` IS NOT DISTINCT FROM shifted_prev.`orders___ordered_at`\n)\nSELECT\n `orders___ordered_at`,\n `orders___amount_sum`,\n `orders___cm`,\n `orders___prev`\nFROM sjoin_prev\n) AS _outer", + "a/local_ts_cm_sibling::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_sum\",\n base.\"orders.cm\",\n shifted_prev.\"orders.amount_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "a/local_ts_cm_sibling::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_sum\",\n base.\"orders.cm\",\n shifted_prev.\"orders.amount_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "a/local_ts_cm_sibling::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted_prev AS (\n SELECT\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months')) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months'))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_sum\",\n base.\"orders.cm\",\n shifted_prev.\"orders.amount_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "a/local_ts_cm_sibling::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n SUM(orders.amount) AS [orders___amount_sum]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS [orders___customers___spend_sum]\n FROM customers AS customers\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _cm_orders__customers__spend_sum.[orders___customers___spend_sum] AS [orders___cm],\n _base.[orders___amount_sum] AS [orders___amount_sum]\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted_prev AS (\n SELECT\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2)) AS [orders___ordered_at],\n SUM(orders.amount) AS [orders___amount_sum]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2))\n), sjoin_prev AS (\n SELECT\n base.[orders___ordered_at] AS [orders___ordered_at],\n base.[orders___amount_sum] AS [orders___amount_sum],\n base.[orders___cm] AS [orders___cm],\n shifted_prev.[orders___amount_sum] AS [orders___prev]\n FROM base\n LEFT JOIN shifted_prev\n ON (\n base.[orders___ordered_at] = shifted_prev.[orders___ordered_at]\n OR (\n base.[orders___ordered_at] IS NULL AND shifted_prev.[orders___ordered_at] IS NULL\n )\n )\n)\nSELECT\n [orders___ordered_at],\n [orders___cm],\n [orders___prev]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___amount_sum] AS [orders___amount_sum],\n [orders___cm] AS [orders___cm],\n [orders___prev] AS [orders___prev]\n FROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___prev`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n CAST(SUM(orders.amount * customers__regions.weight) AS FLOAT64) AS `orders___amount_wscaled_sum`\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _cm_orders__amount_wscaled_sum.`orders___amount_wscaled_sum`\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON _base.`orders___ordered_at` IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum.`orders___ordered_at`\n), shifted_prev AS (\n SELECT\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH) AS `orders___ordered_at`,\n SUM(orders.amount * customers__regions.weight) AS `orders___amount_wscaled_sum`\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH)\n), sjoin_prev AS (\n SELECT\n base.`orders___ordered_at`,\n base.`orders___amount_wscaled_sum`,\n shifted_prev.`orders___amount_wscaled_sum` AS `orders___prev`\n FROM base\n LEFT JOIN shifted_prev\n ON base.`orders___ordered_at` IS NOT DISTINCT FROM shifted_prev.`orders___ordered_at`\n)\nSELECT\n `orders___ordered_at`,\n `orders___amount_wscaled_sum`,\n `orders___prev`\nFROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS DOUBLE) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__amount_wscaled_sum.\"orders.amount_wscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON _base.\"orders.ordered_at\" IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_wscaled_sum\",\n shifted_prev.\"orders.amount_wscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_wscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS DOUBLE PRECISION) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__amount_wscaled_sum.\"orders.amount_wscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON _base.\"orders.ordered_at\" IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_wscaled_sum\",\n shifted_prev.\"orders.amount_wscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_wscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS REAL) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__amount_wscaled_sum.\"orders.amount_wscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON _base.\"orders.ordered_at\" IS _cm_orders__amount_wscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months')) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months'))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_wscaled_sum\",\n shifted_prev.\"orders.amount_wscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_wscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n CAST(SUM(orders.amount * customers__regions.weight) AS FLOAT) AS [orders___amount_wscaled_sum]\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _cm_orders__amount_wscaled_sum.[orders___amount_wscaled_sum] AS [orders___amount_wscaled_sum]\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON (\n _base.[orders___ordered_at] = _cm_orders__amount_wscaled_sum.[orders___ordered_at]\n OR (\n _base.[orders___ordered_at] IS NULL\n AND _cm_orders__amount_wscaled_sum.[orders___ordered_at] IS NULL\n )\n )\n), shifted_prev AS (\n SELECT\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2)) AS [orders___ordered_at],\n SUM(orders.amount * customers__regions.weight) AS [orders___amount_wscaled_sum]\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2))\n), sjoin_prev AS (\n SELECT\n base.[orders___ordered_at] AS [orders___ordered_at],\n base.[orders___amount_wscaled_sum] AS [orders___amount_wscaled_sum],\n shifted_prev.[orders___amount_wscaled_sum] AS [orders___prev]\n FROM base\n LEFT JOIN shifted_prev\n ON (\n base.[orders___ordered_at] = shifted_prev.[orders___ordered_at]\n OR (\n base.[orders___ordered_at] IS NULL AND shifted_prev.[orders___ordered_at] IS NULL\n )\n )\n)\nSELECT\n [orders___ordered_at],\n [orders___prev]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___amount_wscaled_sum] AS [orders___amount_wscaled_sum],\n [orders___prev] AS [orders___prev]\n FROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled_user_kwarg::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___prev`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__amount_wscaled_sum_w_customers__regions_weight AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n CAST(SUM(orders.amount * customers__regions.weight) AS FLOAT64) AS `orders___amount_wscaled_sum_w_customers__regions_weight`\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _cm_orders__amount_wscaled_sum_w_customers__regions_weight.`orders___amount_wscaled_sum_w_customers__regions_weight`\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum_w_customers__regions_weight\n ON _base.`orders___ordered_at` IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum_w_customers__regions_weight.`orders___ordered_at`\n), shifted_prev AS (\n SELECT\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH) AS `orders___ordered_at`,\n SUM(orders.amount * customers__regions.weight) AS `orders___amount_wscaled_sum_w_customers__regions_weight`\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH)\n), sjoin_prev AS (\n SELECT\n base.`orders___ordered_at`,\n base.`orders___amount_wscaled_sum_w_customers__regions_weight`,\n shifted_prev.`orders___amount_wscaled_sum_w_customers__regions_weight` AS `orders___prev`\n FROM base\n LEFT JOIN shifted_prev\n ON base.`orders___ordered_at` IS NOT DISTINCT FROM shifted_prev.`orders___ordered_at`\n)\nSELECT\n `orders___ordered_at`,\n `orders___amount_wscaled_sum_w_customers__regions_weight`,\n `orders___prev`\nFROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled_user_kwarg::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum_w_customers__regions_weight AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS DOUBLE) AS \"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__amount_wscaled_sum_w_customers__regions_weight.\"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum_w_customers__regions_weight\n ON _base.\"orders.ordered_at\" IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum_w_customers__regions_weight.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_wscaled_sum_w_customers__regions_weight\",\n shifted_prev.\"orders.amount_wscaled_sum_w_customers__regions_weight\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_wscaled_sum_w_customers__regions_weight\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled_user_kwarg::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum_w_customers__regions_weight AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS DOUBLE PRECISION) AS \"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__amount_wscaled_sum_w_customers__regions_weight.\"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum_w_customers__regions_weight\n ON _base.\"orders.ordered_at\" IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum_w_customers__regions_weight.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_wscaled_sum_w_customers__regions_weight\",\n shifted_prev.\"orders.amount_wscaled_sum_w_customers__regions_weight\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_wscaled_sum_w_customers__regions_weight\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled_user_kwarg::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum_w_customers__regions_weight AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS REAL) AS \"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__amount_wscaled_sum_w_customers__regions_weight.\"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum_w_customers__regions_weight\n ON _base.\"orders.ordered_at\" IS _cm_orders__amount_wscaled_sum_w_customers__regions_weight.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months')) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum_w_customers__regions_weight\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months'))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_wscaled_sum_w_customers__regions_weight\",\n shifted_prev.\"orders.amount_wscaled_sum_w_customers__regions_weight\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_wscaled_sum_w_customers__regions_weight\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/host_rooted_wscaled_user_kwarg::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__amount_wscaled_sum_w_customers__regions_weight AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n CAST(SUM(orders.amount * customers__regions.weight) AS FLOAT) AS [orders___amount_wscaled_sum_w_customers__regions_weight]\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _cm_orders__amount_wscaled_sum_w_customers__regions_weight.[orders___amount_wscaled_sum_w_customers__regions_weight] AS [orders___amount_wscaled_sum_w_customers__regions_weight]\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum_w_customers__regions_weight\n ON (\n _base.[orders___ordered_at] = _cm_orders__amount_wscaled_sum_w_customers__regions_weight.[orders___ordered_at]\n OR (\n _base.[orders___ordered_at] IS NULL\n AND _cm_orders__amount_wscaled_sum_w_customers__regions_weight.[orders___ordered_at] IS NULL\n )\n )\n), shifted_prev AS (\n SELECT\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2)) AS [orders___ordered_at],\n SUM(orders.amount * customers__regions.weight) AS [orders___amount_wscaled_sum_w_customers__regions_weight]\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2))\n), sjoin_prev AS (\n SELECT\n base.[orders___ordered_at] AS [orders___ordered_at],\n base.[orders___amount_wscaled_sum_w_customers__regions_weight] AS [orders___amount_wscaled_sum_w_customers__regions_weight],\n shifted_prev.[orders___amount_wscaled_sum_w_customers__regions_weight] AS [orders___prev]\n FROM base\n LEFT JOIN shifted_prev\n ON (\n base.[orders___ordered_at] = shifted_prev.[orders___ordered_at]\n OR (\n base.[orders___ordered_at] IS NULL AND shifted_prev.[orders___ordered_at] IS NULL\n )\n )\n)\nSELECT\n [orders___ordered_at],\n [orders___prev]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___amount_wscaled_sum_w_customers__regions_weight] AS [orders___amount_wscaled_sum_w_customers__regions_weight],\n [orders___prev] AS [orders___prev]\n FROM sjoin_prev\n) AS _outer", + "b/liscaled_one_to_many::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___s`,\n `orders___prev`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n CAST(SUM(orders.amount) AS FLOAT64) AS `orders___s`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__amount_liscaled_sum AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n CAST(SUM(orders.amount * line_items.factor) AS FLOAT64) AS `orders___amount_liscaled_sum`\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _base.`orders___s`,\n _cm_orders__amount_liscaled_sum.`orders___amount_liscaled_sum`\n FROM _base\n LEFT JOIN _cm_orders__amount_liscaled_sum\n ON _base.`orders___ordered_at` IS NOT DISTINCT FROM _cm_orders__amount_liscaled_sum.`orders___ordered_at`\n), shifted_prev AS (\n SELECT\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH) AS `orders___ordered_at`,\n SUM(orders.amount * line_items.factor) AS `orders___amount_liscaled_sum`\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH)\n), sjoin_prev AS (\n SELECT\n base.`orders___ordered_at`,\n base.`orders___s`,\n base.`orders___amount_liscaled_sum`,\n shifted_prev.`orders___amount_liscaled_sum` AS `orders___prev`\n FROM base\n LEFT JOIN shifted_prev\n ON base.`orders___ordered_at` IS NOT DISTINCT FROM shifted_prev.`orders___ordered_at`\n)\nSELECT\n `orders___ordered_at`,\n `orders___s`,\n `orders___amount_liscaled_sum`,\n `orders___prev`\nFROM sjoin_prev\n) AS _outer", + "b/liscaled_one_to_many::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount) AS DOUBLE) AS \"orders.s\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__amount_liscaled_sum AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * line_items.factor) AS DOUBLE) AS \"orders.amount_liscaled_sum\"\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _base.\"orders.s\",\n _cm_orders__amount_liscaled_sum.\"orders.amount_liscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_liscaled_sum\n ON _base.\"orders.ordered_at\" IS NOT DISTINCT FROM _cm_orders__amount_liscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount * line_items.factor) AS \"orders.amount_liscaled_sum\"\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.s\",\n base.\"orders.amount_liscaled_sum\",\n shifted_prev.\"orders.amount_liscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.amount_liscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/liscaled_one_to_many::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount) AS DOUBLE PRECISION) AS \"orders.s\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__amount_liscaled_sum AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * line_items.factor) AS DOUBLE PRECISION) AS \"orders.amount_liscaled_sum\"\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _base.\"orders.s\",\n _cm_orders__amount_liscaled_sum.\"orders.amount_liscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_liscaled_sum\n ON _base.\"orders.ordered_at\" IS NOT DISTINCT FROM _cm_orders__amount_liscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount * line_items.factor) AS \"orders.amount_liscaled_sum\"\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.s\",\n base.\"orders.amount_liscaled_sum\",\n shifted_prev.\"orders.amount_liscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.amount_liscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/liscaled_one_to_many::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount) AS REAL) AS \"orders.s\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__amount_liscaled_sum AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * line_items.factor) AS REAL) AS \"orders.amount_liscaled_sum\"\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _base.\"orders.s\",\n _cm_orders__amount_liscaled_sum.\"orders.amount_liscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_liscaled_sum\n ON _base.\"orders.ordered_at\" IS _cm_orders__amount_liscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months')) AS \"orders.ordered_at\",\n SUM(orders.amount * line_items.factor) AS \"orders.amount_liscaled_sum\"\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months'))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.s\",\n base.\"orders.amount_liscaled_sum\",\n shifted_prev.\"orders.amount_liscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.amount_liscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/liscaled_one_to_many::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n CAST(SUM(orders.amount) AS FLOAT) AS [orders___s]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__amount_liscaled_sum AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n CAST(SUM(orders.amount * line_items.factor) AS FLOAT) AS [orders___amount_liscaled_sum]\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _base.[orders___s] AS [orders___s],\n _cm_orders__amount_liscaled_sum.[orders___amount_liscaled_sum] AS [orders___amount_liscaled_sum]\n FROM _base\n LEFT JOIN _cm_orders__amount_liscaled_sum\n ON (\n _base.[orders___ordered_at] = _cm_orders__amount_liscaled_sum.[orders___ordered_at]\n OR (\n _base.[orders___ordered_at] IS NULL\n AND _cm_orders__amount_liscaled_sum.[orders___ordered_at] IS NULL\n )\n )\n), shifted_prev AS (\n SELECT\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2)) AS [orders___ordered_at],\n SUM(orders.amount * line_items.factor) AS [orders___amount_liscaled_sum]\n FROM orders AS orders\n LEFT JOIN line_items AS line_items\n ON orders.id = line_items.order_id\n GROUP BY\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2))\n), sjoin_prev AS (\n SELECT\n base.[orders___ordered_at] AS [orders___ordered_at],\n base.[orders___s] AS [orders___s],\n base.[orders___amount_liscaled_sum] AS [orders___amount_liscaled_sum],\n shifted_prev.[orders___amount_liscaled_sum] AS [orders___prev]\n FROM base\n LEFT JOIN shifted_prev\n ON (\n base.[orders___ordered_at] = shifted_prev.[orders___ordered_at]\n OR (\n base.[orders___ordered_at] IS NULL AND shifted_prev.[orders___ordered_at] IS NULL\n )\n )\n)\nSELECT\n [orders___ordered_at],\n [orders___s],\n [orders___prev]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___s] AS [orders___s],\n [orders___amount_liscaled_sum] AS [orders___amount_liscaled_sum],\n [orders___prev] AS [orders___prev]\n FROM sjoin_prev\n) AS _outer", + "b/wscaled_with_local_sibling::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___s`,\n `orders___prev`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n CAST(SUM(orders.amount) AS FLOAT64) AS `orders___s`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n CAST(SUM(orders.amount * customers__regions.weight) AS FLOAT64) AS `orders___amount_wscaled_sum`\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _base.`orders___s`,\n _cm_orders__amount_wscaled_sum.`orders___amount_wscaled_sum`\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON _base.`orders___ordered_at` IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum.`orders___ordered_at`\n), shifted_prev AS (\n SELECT\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH) AS `orders___ordered_at`,\n SUM(orders.amount * customers__regions.weight) AS `orders___amount_wscaled_sum`\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH)\n), sjoin_prev AS (\n SELECT\n base.`orders___ordered_at`,\n base.`orders___s`,\n base.`orders___amount_wscaled_sum`,\n shifted_prev.`orders___amount_wscaled_sum` AS `orders___prev`\n FROM base\n LEFT JOIN shifted_prev\n ON base.`orders___ordered_at` IS NOT DISTINCT FROM shifted_prev.`orders___ordered_at`\n)\nSELECT\n `orders___ordered_at`,\n `orders___s`,\n `orders___amount_wscaled_sum`,\n `orders___prev`\nFROM sjoin_prev\n) AS _outer", + "b/wscaled_with_local_sibling::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount) AS DOUBLE) AS \"orders.s\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS DOUBLE) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _base.\"orders.s\",\n _cm_orders__amount_wscaled_sum.\"orders.amount_wscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON _base.\"orders.ordered_at\" IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.s\",\n base.\"orders.amount_wscaled_sum\",\n shifted_prev.\"orders.amount_wscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.amount_wscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/wscaled_with_local_sibling::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount) AS DOUBLE PRECISION) AS \"orders.s\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS DOUBLE PRECISION) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _base.\"orders.s\",\n _cm_orders__amount_wscaled_sum.\"orders.amount_wscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON _base.\"orders.ordered_at\" IS NOT DISTINCT FROM _cm_orders__amount_wscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.s\",\n base.\"orders.amount_wscaled_sum\",\n shifted_prev.\"orders.amount_wscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.amount_wscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/wscaled_with_local_sibling::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.prev\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount) AS REAL) AS \"orders.s\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n CAST(SUM(orders.amount * customers__regions.weight) AS REAL) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _base.\"orders.s\",\n _cm_orders__amount_wscaled_sum.\"orders.amount_wscaled_sum\"\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON _base.\"orders.ordered_at\" IS _cm_orders__amount_wscaled_sum.\"orders.ordered_at\"\n), shifted_prev AS (\n SELECT\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months')) AS \"orders.ordered_at\",\n SUM(orders.amount * customers__regions.weight) AS \"orders.amount_wscaled_sum\"\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months'))\n), sjoin_prev AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.s\",\n base.\"orders.amount_wscaled_sum\",\n shifted_prev.\"orders.amount_wscaled_sum\" AS \"orders.prev\"\n FROM base\n LEFT JOIN shifted_prev\n ON base.\"orders.ordered_at\" IS shifted_prev.\"orders.ordered_at\"\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.s\",\n \"orders.amount_wscaled_sum\",\n \"orders.prev\"\nFROM sjoin_prev\n) AS _outer", + "b/wscaled_with_local_sibling::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n CAST(SUM(orders.amount) AS FLOAT) AS [orders___s]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__amount_wscaled_sum AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n CAST(SUM(orders.amount * customers__regions.weight) AS FLOAT) AS [orders___amount_wscaled_sum]\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _base.[orders___s] AS [orders___s],\n _cm_orders__amount_wscaled_sum.[orders___amount_wscaled_sum] AS [orders___amount_wscaled_sum]\n FROM _base\n LEFT JOIN _cm_orders__amount_wscaled_sum\n ON (\n _base.[orders___ordered_at] = _cm_orders__amount_wscaled_sum.[orders___ordered_at]\n OR (\n _base.[orders___ordered_at] IS NULL\n AND _cm_orders__amount_wscaled_sum.[orders___ordered_at] IS NULL\n )\n )\n), shifted_prev AS (\n SELECT\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2)) AS [orders___ordered_at],\n SUM(orders.amount * customers__regions.weight) AS [orders___amount_wscaled_sum]\n FROM orders AS orders\n LEFT JOIN customers AS customers\n ON orders.customer_id = customers.id\n LEFT JOIN regions AS customers__regions\n ON customers.region_id = customers__regions.id\n GROUP BY\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2))\n), sjoin_prev AS (\n SELECT\n base.[orders___ordered_at] AS [orders___ordered_at],\n base.[orders___s] AS [orders___s],\n base.[orders___amount_wscaled_sum] AS [orders___amount_wscaled_sum],\n shifted_prev.[orders___amount_wscaled_sum] AS [orders___prev]\n FROM base\n LEFT JOIN shifted_prev\n ON (\n base.[orders___ordered_at] = shifted_prev.[orders___ordered_at]\n OR (\n base.[orders___ordered_at] IS NULL AND shifted_prev.[orders___ordered_at] IS NULL\n )\n )\n)\nSELECT\n [orders___ordered_at],\n [orders___s],\n [orders___prev]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___s] AS [orders___s],\n [orders___amount_wscaled_sum] AS [orders___amount_wscaled_sum],\n [orders___prev] AS [orders___prev]\n FROM sjoin_prev\n) AS _outer", + "c/target_grain_guarded::bigquery": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a TARGET-GRAIN cross-model aggregate (its inner aggregate is grouped at a joined target's grain, not the host's) is not yet rendered \u2014 the shifted CTE would re-aggregate it host-rooted and multiply target rows through the 1:N join. Local and host-grain inner aggregates DO render. Factor the temporal transform into an earlier stage, or drop the cross-grain part." + }, + "c/target_grain_guarded::duckdb": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a TARGET-GRAIN cross-model aggregate (its inner aggregate is grouped at a joined target's grain, not the host's) is not yet rendered \u2014 the shifted CTE would re-aggregate it host-rooted and multiply target rows through the 1:N join. Local and host-grain inner aggregates DO render. Factor the temporal transform into an earlier stage, or drop the cross-grain part." + }, + "c/target_grain_guarded::postgres": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a TARGET-GRAIN cross-model aggregate (its inner aggregate is grouped at a joined target's grain, not the host's) is not yet rendered \u2014 the shifted CTE would re-aggregate it host-rooted and multiply target rows through the 1:N join. Local and host-grain inner aggregates DO render. Factor the temporal transform into an earlier stage, or drop the cross-grain part." + }, + "c/target_grain_guarded::sqlite": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a TARGET-GRAIN cross-model aggregate (its inner aggregate is grouped at a joined target's grain, not the host's) is not yet rendered \u2014 the shifted CTE would re-aggregate it host-rooted and multiply target rows through the 1:N join. Local and host-grain inner aggregates DO render. Factor the temporal transform into an earlier stage, or drop the cross-grain part." + }, + "c/target_grain_guarded::tsql": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a TARGET-GRAIN cross-model aggregate (its inner aggregate is grouped at a joined target's grain, not the host's) is not yet rendered \u2014 the shifted CTE would re-aggregate it host-rooted and multiply target rows through the 1:N join. Local and host-grain inner aggregates DO render. Factor the temporal transform into an earlier stage, or drop the cross-grain part." + }, + "change/of_wscaled::bigquery": { + "error": "RenderContextMissingFacilityError", + "message": "RenderContextMissingFacilityError: Rendering a TransformKey requires the 'aliases' render-context facility, which was not supplied (TransformKey is not materialised as a slot)." + }, + "change/of_wscaled::duckdb": { + "error": "RenderContextMissingFacilityError", + "message": "RenderContextMissingFacilityError: Rendering a TransformKey requires the 'aliases' render-context facility, which was not supplied (TransformKey is not materialised as a slot)." + }, + "change/of_wscaled::postgres": { + "error": "RenderContextMissingFacilityError", + "message": "RenderContextMissingFacilityError: Rendering a TransformKey requires the 'aliases' render-context facility, which was not supplied (TransformKey is not materialised as a slot)." + }, + "change/of_wscaled::sqlite": { + "error": "RenderContextMissingFacilityError", + "message": "RenderContextMissingFacilityError: Rendering a TransformKey requires the 'aliases' render-context facility, which was not supplied (TransformKey is not materialised as a slot)." + }, + "change/of_wscaled::tsql": { + "error": "RenderContextMissingFacilityError", + "message": "RenderContextMissingFacilityError: Rendering a TransformKey requires the 'aliases' render-context facility, which was not supplied (TransformKey is not materialised as a slot)." + }, + "change_pct/of_local_with_cm_sibling::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___cm`,\n `orders___delta`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n SUM(orders.amount) AS `orders___amount_sum`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS `orders___customers___spend_sum`\n FROM customers AS customers\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _cm_orders__customers__spend_sum.`orders___customers___spend_sum` AS `orders___cm`,\n _base.`orders___amount_sum`\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted__time_shift_inner AS (\n SELECT\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH) AS `orders___ordered_at`,\n SUM(orders.amount) AS `orders___amount_sum`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(CAST(orders.ordered_at + INTERVAL 1 MONTH AS DATETIME), MONTH)\n), sjoin__time_shift_inner AS (\n SELECT\n base.`orders___ordered_at`,\n base.`orders___amount_sum`,\n base.`orders___cm`,\n shifted__time_shift_inner.`orders___amount_sum` AS `orders____time_shift_inner`\n FROM base\n LEFT JOIN shifted__time_shift_inner\n ON base.`orders___ordered_at` IS NOT DISTINCT FROM shifted__time_shift_inner.`orders___ordered_at`\n), step1 AS (\n SELECT\n `orders___ordered_at`,\n `orders___amount_sum`,\n `orders___cm`,\n `orders____time_shift_inner`,\n (\n `orders___amount_sum` - `orders____time_shift_inner`\n ) / NULLIF(`orders____time_shift_inner`, 0) AS `orders___delta`\n FROM sjoin__time_shift_inner\n)\nSELECT\n `orders___ordered_at`,\n `orders___amount_sum`,\n `orders___cm`,\n `orders____time_shift_inner`,\n `orders___delta`\nFROM step1\n) AS _outer", + "change_pct/of_local_with_cm_sibling::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.delta\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted__time_shift_inner AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL 1 MONTH AS TIMESTAMP))\n), sjoin__time_shift_inner AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_sum\",\n base.\"orders.cm\",\n shifted__time_shift_inner.\"orders.amount_sum\" AS \"orders._time_shift_inner\"\n FROM base\n LEFT JOIN shifted__time_shift_inner\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted__time_shift_inner.\"orders.ordered_at\"\n), step1 AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders._time_shift_inner\",\n (\n \"orders.amount_sum\" - \"orders._time_shift_inner\"\n ) / NULLIF(\"orders._time_shift_inner\", 0) AS \"orders.delta\"\n FROM sjoin__time_shift_inner\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders._time_shift_inner\",\n \"orders.delta\"\nFROM step1\n) AS _outer", + "change_pct/of_local_with_cm_sibling::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.delta\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted__time_shift_inner AS (\n SELECT\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP)) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', CAST(orders.ordered_at + INTERVAL '1 MONTH' AS TIMESTAMP))\n), sjoin__time_shift_inner AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_sum\",\n base.\"orders.cm\",\n shifted__time_shift_inner.\"orders.amount_sum\" AS \"orders._time_shift_inner\"\n FROM base\n LEFT JOIN shifted__time_shift_inner\n ON base.\"orders.ordered_at\" IS NOT DISTINCT FROM shifted__time_shift_inner.\"orders.ordered_at\"\n), step1 AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders._time_shift_inner\",\n CAST((\n \"orders.amount_sum\" - \"orders._time_shift_inner\"\n ) AS DOUBLE PRECISION) / NULLIF(\"orders._time_shift_inner\", 0) AS \"orders.delta\"\n FROM sjoin__time_shift_inner\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders._time_shift_inner\",\n \"orders.delta\"\nFROM step1\n) AS _outer", + "change_pct/of_local_with_cm_sibling::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.delta\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted__time_shift_inner AS (\n SELECT\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months')) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', DATE(orders.ordered_at, '1 months'))\n), sjoin__time_shift_inner AS (\n SELECT\n base.\"orders.ordered_at\",\n base.\"orders.amount_sum\",\n base.\"orders.cm\",\n shifted__time_shift_inner.\"orders.amount_sum\" AS \"orders._time_shift_inner\"\n FROM base\n LEFT JOIN shifted__time_shift_inner\n ON base.\"orders.ordered_at\" IS shifted__time_shift_inner.\"orders.ordered_at\"\n), step1 AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders._time_shift_inner\",\n CAST((\n \"orders.amount_sum\" - \"orders._time_shift_inner\"\n ) AS REAL) / NULLIF(\"orders._time_shift_inner\", 0) AS \"orders.delta\"\n FROM sjoin__time_shift_inner\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders._time_shift_inner\",\n \"orders.delta\"\nFROM step1\n) AS _outer", + "change_pct/of_local_with_cm_sibling::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n SUM(orders.amount) AS [orders___amount_sum]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS [orders___customers___spend_sum]\n FROM customers AS customers\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _cm_orders__customers__spend_sum.[orders___customers___spend_sum] AS [orders___cm],\n _base.[orders___amount_sum] AS [orders___amount_sum]\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), shifted__time_shift_inner AS (\n SELECT\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2)) AS [orders___ordered_at],\n SUM(orders.amount) AS [orders___amount_sum]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, CAST(DATEADD(MONTH, 1, orders.ordered_at) AS DATETIME2))\n), sjoin__time_shift_inner AS (\n SELECT\n base.[orders___ordered_at] AS [orders___ordered_at],\n base.[orders___amount_sum] AS [orders___amount_sum],\n base.[orders___cm] AS [orders___cm],\n shifted__time_shift_inner.[orders___amount_sum] AS [orders____time_shift_inner]\n FROM base\n LEFT JOIN shifted__time_shift_inner\n ON (\n base.[orders___ordered_at] = shifted__time_shift_inner.[orders___ordered_at]\n OR (\n base.[orders___ordered_at] IS NULL\n AND shifted__time_shift_inner.[orders___ordered_at] IS NULL\n )\n )\n), step1 AS (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___amount_sum] AS [orders___amount_sum],\n [orders___cm] AS [orders___cm],\n [orders____time_shift_inner] AS [orders____time_shift_inner],\n CAST((\n [orders___amount_sum] - [orders____time_shift_inner]\n ) AS FLOAT) / NULLIF([orders____time_shift_inner], 0) AS [orders___delta]\n FROM sjoin__time_shift_inner\n)\nSELECT\n [orders___ordered_at],\n [orders___cm],\n [orders___delta]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___amount_sum] AS [orders___amount_sum],\n [orders___cm] AS [orders___cm],\n [orders____time_shift_inner] AS [orders____time_shift_inner],\n [orders___delta] AS [orders___delta]\n FROM step1\n) AS _outer", + "composite/still_7b11::bigquery": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.11: composite-input transforms (layer op='time_shift' input=ArithmeticKey) are deferred to a follow-up slice. slot id='s3'." + }, + "composite/still_7b11::duckdb": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.11: composite-input transforms (layer op='time_shift' input=ArithmeticKey) are deferred to a follow-up slice. slot id='s3'." + }, + "composite/still_7b11::postgres": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.11: composite-input transforms (layer op='time_shift' input=ArithmeticKey) are deferred to a follow-up slice. slot id='s3'." + }, + "composite/still_7b11::sqlite": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.11: composite-input transforms (layer op='time_shift' input=ArithmeticKey) are deferred to a follow-up slice. slot id='s3'." + }, + "composite/still_7b11::tsql": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.11: composite-input transforms (layer op='time_shift' input=ArithmeticKey) are deferred to a follow-up slice. slot id='s3'." + }, + "cp/local_with_cm_sibling::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___cm`,\n `orders___streak`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`,\n SUM(orders.amount) AS `orders___amount_sum`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS `orders___customers___spend_sum`\n FROM customers AS customers\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _cm_orders__customers__spend_sum.`orders___customers___spend_sum` AS `orders___cm`,\n _base.`orders___amount_sum`\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n `orders___ordered_at`,\n `orders___amount_sum`,\n `orders___cm`,\n SUM(CASE WHEN COALESCE(`orders___amount_sum` > 0, FALSE) THEN 0 ELSE 1 END) OVER (ORDER BY `orders___ordered_at` ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS `_cp_reset_orders___streak`\n FROM base\n), cp_value_streak AS (\n SELECT\n `orders___ordered_at`,\n `orders___amount_sum`,\n `orders___cm`,\n CASE\n WHEN COALESCE(`orders___amount_sum` > 0, FALSE)\n THEN SUM(CASE WHEN COALESCE(`orders___amount_sum` > 0, FALSE) THEN 1 ELSE 0 END) OVER (\n PARTITION BY `_cp_reset_orders___streak`\n ORDER BY `orders___ordered_at`\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS `orders___streak`\n FROM cp_reset_streak\n)\nSELECT\n `orders___ordered_at`,\n `orders___amount_sum`,\n `orders___cm`,\n `orders___streak`\nFROM cp_value_streak\n) AS _outer", + "cp/local_with_cm_sibling::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.streak\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n SUM(CASE WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE) THEN 0 ELSE 1 END) OVER (ORDER BY \"orders.ordered_at\" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"_cp_reset_orders.streak\"\n FROM base\n), cp_value_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n CASE\n WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE)\n THEN SUM(CASE WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE) THEN 1 ELSE 0 END) OVER (\n PARTITION BY \"_cp_reset_orders.streak\"\n ORDER BY \"orders.ordered_at\"\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS \"orders.streak\"\n FROM cp_reset_streak\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders.streak\"\nFROM cp_value_streak\n) AS _outer", + "cp/local_with_cm_sibling::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.streak\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n SUM(CASE WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE) THEN 0 ELSE 1 END) OVER (ORDER BY \"orders.ordered_at\" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"_cp_reset_orders.streak\"\n FROM base\n), cp_value_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n CASE\n WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE)\n THEN SUM(CASE WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE) THEN 1 ELSE 0 END) OVER (\n PARTITION BY \"_cp_reset_orders.streak\"\n ORDER BY \"orders.ordered_at\"\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS \"orders.streak\"\n FROM cp_reset_streak\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders.streak\"\nFROM cp_value_streak\n) AS _outer", + "cp/local_with_cm_sibling::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.cm\",\n \"orders.streak\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\",\n SUM(orders.amount) AS \"orders.amount_sum\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\" AS \"orders.cm\",\n _base.\"orders.amount_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n SUM(CASE WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE) THEN 0 ELSE 1 END) OVER (ORDER BY \"orders.ordered_at\" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"_cp_reset_orders.streak\"\n FROM base\n), cp_value_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n CASE\n WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE)\n THEN SUM(CASE WHEN COALESCE(\"orders.amount_sum\" > 0, FALSE) THEN 1 ELSE 0 END) OVER (\n PARTITION BY \"_cp_reset_orders.streak\"\n ORDER BY \"orders.ordered_at\"\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS \"orders.streak\"\n FROM cp_reset_streak\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.amount_sum\",\n \"orders.cm\",\n \"orders.streak\"\nFROM cp_value_streak\n) AS _outer", + "cp/local_with_cm_sibling::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at],\n SUM(orders.amount) AS [orders___amount_sum]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS [orders___customers___spend_sum]\n FROM customers AS customers\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _cm_orders__customers__spend_sum.[orders___customers___spend_sum] AS [orders___cm],\n _base.[orders___amount_sum] AS [orders___amount_sum]\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___amount_sum] AS [orders___amount_sum],\n [orders___cm] AS [orders___cm],\n SUM(CASE WHEN COALESCE([orders___amount_sum] > 0, (\n 1 = 0\n )) THEN 0 ELSE 1 END) OVER (ORDER BY [orders___ordered_at] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS [_cp_reset_orders___streak]\n FROM base\n), cp_value_streak AS (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___amount_sum] AS [orders___amount_sum],\n [orders___cm] AS [orders___cm],\n CASE\n WHEN COALESCE([orders___amount_sum] > 0, (\n 1 = 0\n ))\n THEN SUM(CASE WHEN COALESCE([orders___amount_sum] > 0, (\n 1 = 0\n )) THEN 1 ELSE 0 END) OVER (\n PARTITION BY [_cp_reset_orders___streak]\n ORDER BY [orders___ordered_at]\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS [orders___streak]\n FROM cp_reset_streak\n)\nSELECT\n [orders___ordered_at],\n [orders___cm],\n [orders___streak]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___amount_sum] AS [orders___amount_sum],\n [orders___cm] AS [orders___cm],\n [orders___streak] AS [orders___streak]\n FROM cp_value_streak\n) AS _outer", + "cp/over_target_grain::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___streak`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS `orders___customers___spend_sum`\n FROM customers AS customers\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _cm_orders__customers__spend_sum.`orders___customers___spend_sum`\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n `orders___ordered_at`,\n `orders___customers___spend_sum`,\n SUM(CASE WHEN COALESCE(`orders___customers___spend_sum` > 0, FALSE) THEN 0 ELSE 1 END) OVER (ORDER BY `orders___ordered_at` ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS `_cp_reset_orders___streak`\n FROM base\n), cp_value_streak AS (\n SELECT\n `orders___ordered_at`,\n `orders___customers___spend_sum`,\n CASE\n WHEN COALESCE(`orders___customers___spend_sum` > 0, FALSE)\n THEN SUM(CASE WHEN COALESCE(`orders___customers___spend_sum` > 0, FALSE) THEN 1 ELSE 0 END) OVER (\n PARTITION BY `_cp_reset_orders___streak`\n ORDER BY `orders___ordered_at`\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS `orders___streak`\n FROM cp_reset_streak\n)\nSELECT\n `orders___ordered_at`,\n `orders___customers___spend_sum`,\n `orders___streak`\nFROM cp_value_streak\n) AS _outer", + "cp/over_target_grain::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.streak\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n SUM(CASE WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE) THEN 0 ELSE 1 END) OVER (ORDER BY \"orders.ordered_at\" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"_cp_reset_orders.streak\"\n FROM base\n), cp_value_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n CASE\n WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE)\n THEN SUM(CASE WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE) THEN 1 ELSE 0 END) OVER (\n PARTITION BY \"_cp_reset_orders.streak\"\n ORDER BY \"orders.ordered_at\"\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS \"orders.streak\"\n FROM cp_reset_streak\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n \"orders.streak\"\nFROM cp_value_streak\n) AS _outer", + "cp/over_target_grain::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.streak\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n SUM(CASE WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE) THEN 0 ELSE 1 END) OVER (ORDER BY \"orders.ordered_at\" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"_cp_reset_orders.streak\"\n FROM base\n), cp_value_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n CASE\n WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE)\n THEN SUM(CASE WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE) THEN 1 ELSE 0 END) OVER (\n PARTITION BY \"_cp_reset_orders.streak\"\n ORDER BY \"orders.ordered_at\"\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS \"orders.streak\"\n FROM cp_reset_streak\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n \"orders.streak\"\nFROM cp_value_streak\n) AS _outer", + "cp/over_target_grain::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.streak\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n SUM(CASE WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE) THEN 0 ELSE 1 END) OVER (ORDER BY \"orders.ordered_at\" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS \"_cp_reset_orders.streak\"\n FROM base\n), cp_value_streak AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n CASE\n WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE)\n THEN SUM(CASE WHEN COALESCE(\"orders.customers.spend_sum\" > 0, FALSE) THEN 1 ELSE 0 END) OVER (\n PARTITION BY \"_cp_reset_orders.streak\"\n ORDER BY \"orders.ordered_at\"\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS \"orders.streak\"\n FROM cp_reset_streak\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n \"orders.streak\"\nFROM cp_value_streak\n) AS _outer", + "cp/over_target_grain::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS [orders___customers___spend_sum]\n FROM customers AS customers\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _cm_orders__customers__spend_sum.[orders___customers___spend_sum] AS [orders___customers___spend_sum]\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), cp_reset_streak AS (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___customers___spend_sum] AS [orders___customers___spend_sum],\n SUM(\n CASE WHEN COALESCE([orders___customers___spend_sum] > 0, (\n 1 = 0\n )) THEN 0 ELSE 1 END\n ) OVER (ORDER BY [orders___ordered_at] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS [_cp_reset_orders___streak]\n FROM base\n), cp_value_streak AS (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___customers___spend_sum] AS [orders___customers___spend_sum],\n CASE\n WHEN COALESCE([orders___customers___spend_sum] > 0, (\n 1 = 0\n ))\n THEN SUM(\n CASE WHEN COALESCE([orders___customers___spend_sum] > 0, (\n 1 = 0\n )) THEN 1 ELSE 0 END\n ) OVER (\n PARTITION BY [_cp_reset_orders___streak]\n ORDER BY [orders___ordered_at]\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n )\n ELSE 0\n END AS [orders___streak]\n FROM cp_reset_streak\n)\nSELECT\n [orders___ordered_at],\n [orders___streak]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___customers___spend_sum] AS [orders___customers___spend_sum],\n [orders___streak] AS [orders___streak]\n FROM cp_value_streak\n) AS _outer", + "first_last/crossing_time_arg::bigquery": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a ranked 'last' aggregate is not yet rendered \u2014 the shifted CTE re-aggregates flatly and cannot reproduce the ROW_NUMBER ranking. Factor the temporal transform into an earlier stage. (slot id='s3')" + }, + "first_last/crossing_time_arg::duckdb": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a ranked 'last' aggregate is not yet rendered \u2014 the shifted CTE re-aggregates flatly and cannot reproduce the ROW_NUMBER ranking. Factor the temporal transform into an earlier stage. (slot id='s3')" + }, + "first_last/crossing_time_arg::postgres": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a ranked 'last' aggregate is not yet rendered \u2014 the shifted CTE re-aggregates flatly and cannot reproduce the ROW_NUMBER ranking. Factor the temporal transform into an earlier stage. (slot id='s3')" + }, + "first_last/crossing_time_arg::sqlite": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a ranked 'last' aggregate is not yet rendered \u2014 the shifted CTE re-aggregates flatly and cannot reproduce the ROW_NUMBER ranking. Factor the temporal transform into an earlier stage. (slot id='s3')" + }, + "first_last/crossing_time_arg::tsql": { + "error": "NotImplementedError", + "message": "DEV-1450 stage 7b.15e: time_shift over a ranked 'last' aggregate is not yet rendered \u2014 the shifted CTE re-aggregates flatly and cannot reproduce the ROW_NUMBER ranking. Factor the temporal transform into an earlier stage. (slot id='s3')" + }, + "window/cumsum_cross_model::bigquery": "SELECT\n `orders___ordered_at`,\n `orders___run`\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC(orders.ordered_at, MONTH) AS `orders___ordered_at`\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC(orders.ordered_at, MONTH)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS `orders___customers___spend_sum`\n FROM customers AS customers\n), base AS (\n SELECT\n _base.`orders___ordered_at`,\n _cm_orders__customers__spend_sum.`orders___customers___spend_sum`\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), step1 AS (\n SELECT\n `orders___ordered_at`,\n `orders___customers___spend_sum`,\n SUM(`orders___customers___spend_sum`) OVER (ORDER BY `orders___ordered_at`) AS `orders___run`\n FROM base\n)\nSELECT\n `orders___ordered_at`,\n `orders___customers___spend_sum`,\n `orders___run`\nFROM step1\n) AS _outer", + "window/cumsum_cross_model::duckdb": "SELECT\n \"orders.ordered_at\",\n \"orders.run\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), step1 AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n SUM(\"orders.customers.spend_sum\") OVER (ORDER BY \"orders.ordered_at\") AS \"orders.run\"\n FROM base\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n \"orders.run\"\nFROM step1\n) AS _outer", + "window/cumsum_cross_model::postgres": "SELECT\n \"orders.ordered_at\",\n \"orders.run\"\nFROM (\nWITH _base AS (\n SELECT\n DATE_TRUNC('MONTH', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n DATE_TRUNC('MONTH', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), step1 AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n SUM(\"orders.customers.spend_sum\") OVER (ORDER BY \"orders.ordered_at\") AS \"orders.run\"\n FROM base\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n \"orders.run\"\nFROM step1\n) AS _outer", + "window/cumsum_cross_model::sqlite": "SELECT\n \"orders.ordered_at\",\n \"orders.run\"\nFROM (\nWITH _base AS (\n SELECT\n STRFTIME('%Y-%m-01', orders.ordered_at) AS \"orders.ordered_at\"\n FROM orders AS orders\n GROUP BY\n STRFTIME('%Y-%m-01', orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS \"orders.customers.spend_sum\"\n FROM customers AS customers\n), base AS (\n SELECT\n _base.\"orders.ordered_at\",\n _cm_orders__customers__spend_sum.\"orders.customers.spend_sum\"\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), step1 AS (\n SELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n SUM(\"orders.customers.spend_sum\") OVER (ORDER BY \"orders.ordered_at\") AS \"orders.run\"\n FROM base\n)\nSELECT\n \"orders.ordered_at\",\n \"orders.customers.spend_sum\",\n \"orders.run\"\nFROM step1\n) AS _outer", + "window/cumsum_cross_model::tsql": "WITH _base AS (\n SELECT\n DATETRUNC(MONTH, orders.ordered_at) AS [orders___ordered_at]\n FROM orders AS orders\n GROUP BY\n DATETRUNC(MONTH, orders.ordered_at)\n), _cm_orders__customers__spend_sum AS (\n SELECT\n SUM(customers.spend) AS [orders___customers___spend_sum]\n FROM customers AS customers\n), base AS (\n SELECT\n _base.[orders___ordered_at] AS [orders___ordered_at],\n _cm_orders__customers__spend_sum.[orders___customers___spend_sum] AS [orders___customers___spend_sum]\n FROM _base\n CROSS JOIN _cm_orders__customers__spend_sum\n), step1 AS (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___customers___spend_sum] AS [orders___customers___spend_sum],\n SUM([orders___customers___spend_sum]) OVER (ORDER BY [orders___ordered_at]) AS [orders___run]\n FROM base\n)\nSELECT\n [orders___ordered_at],\n [orders___run]\nFROM (\n SELECT\n [orders___ordered_at] AS [orders___ordered_at],\n [orders___customers___spend_sum] AS [orders___customers___spend_sum],\n [orders___run] AS [orders___run]\n FROM step1\n) AS _outer" +} diff --git a/tests/test_dev1748_golden_sql.py b/tests/test_dev1748_golden_sql.py index 4096bbb5..25be5515 100644 --- a/tests/test_dev1748_golden_sql.py +++ b/tests/test_dev1748_golden_sql.py @@ -19,16 +19,13 @@ from __future__ import annotations -import asyncio from pathlib import Path -import pytest - from slayer.core.query import SlayerQuery from tests._dev1748_fixtures import BIG_AMOUNT_THRESHOLD, dev1748_models from tests._engine_helpers import _engine_generate -from tests._golden_harness import GoldenSuite, load_or_regenerate, record_raise +from tests._golden_harness import bind_golden_tests, record_raise GOLDEN_PATH = Path(__file__).parent / "golden" / "dev1748_first_last_baseline.json" @@ -241,48 +238,17 @@ async def _generate_one(query: SlayerQuery, dialect: str): return record_raise(exc) -async def _render(case_id: str, dialect: str): - return await _generate_one(_cases()[case_id], dialect) - - -def _suite() -> GoldenSuite: - return GoldenSuite( - case_ids=sorted(_cases()), dialects=DIALECTS, allowed=ALLOWED_DELTAS, - ) - - -@pytest.fixture(scope="module") -def baseline() -> dict: - return load_or_regenerate( - path=GOLDEN_PATH, case_ids=sorted(_cases()), dialects=DIALECTS, - render=_render, allowed=ALLOWED_DELTAS, - ) - - -@pytest.mark.parametrize("case_id", sorted(_cases())) -@pytest.mark.parametrize("dialect", DIALECTS) -def test_emitted_sql_matches_golden(case_id: str, dialect: str, baseline) -> None: - _suite().assert_matches( - key=f"{case_id}::{dialect}", - actual=asyncio.run(_generate_one(_cases()[case_id], dialect)), - baseline=baseline, - ) - - -def test_baseline_covers_every_case_and_dialect(baseline) -> None: - _suite().assert_covers_every_case(baseline) - - -def test_baseline_has_no_orphan_entries(baseline) -> None: - _suite().assert_no_orphans(baseline) - - -def test_allowed_deltas_name_real_keys() -> None: - _suite().assert_allowed_deltas_name_real_keys() - - -def test_allowed_deltas_carry_a_reason() -> None: - _suite().assert_allowed_deltas_carry_a_reason() +# The blessing-loop wiring (baseline fixture + the five shared guards) is bound +# once in the harness; this module keeps only its matrix, path, dialects, and the +# vacuity guard below. +bind_golden_tests( + namespace=globals(), + golden_path=GOLDEN_PATH, + cases=_cases, + dialects=DIALECTS, + allowed=ALLOWED_DELTAS, + generate_one=_generate_one, +) def test_every_case_actually_ranks(baseline) -> None: diff --git a/tests/test_dev1750_execution.py b/tests/test_dev1750_execution.py new file mode 100644 index 00000000..5aeca394 --- /dev/null +++ b/tests/test_dev1750_execution.py @@ -0,0 +1,280 @@ +"""DEV-1750 — execution ground truth on SQLite AND DuckDB (both required by the +issue). Every expectation is hand-computed from the fixture dataset in +``tests/_dev1750_fixtures.py``; a missing join or a wrong grain is not a cosmetic +difference — the SQL either fails to bind or returns the wrong number. + +Dataset recap (weights: region 1 → 2.0, region 2 → 3.0): + month amount:sum wscaled_sum customers.spend:sum + Jan 15 (10+5) 35 (10*2 + 5*3) 300 (c1 100 + c2 200) + Feb 24 (20+4) 48 (20*2 + 4*2, both c1/reg1) 100 (c1 only, ×orders) + Mar 18 (10+8) 46 (10*3 + 8*2) ... (c2 200 + c1 100) + +The Feb/Mar ``customers.spend:sum`` values depend on the join fan-out and are +asserted only where hand-verifiable; the transform columns are the focus. +""" + +from __future__ import annotations + +import pytest + +from slayer.core.errors import RenderContextMissingFacilityError + +from tests._dev1750_fixtures import ( + ColumnRef, + ModelMeasure, + SlayerQuery, + TimeDimension, + TimeGranularity, + make_exec_engine, + month_key, + month_td, + rows_by, +) + + +@pytest.fixture(params=["sqlite", "duckdb"]) +async def exec_engine(request): + """SQLite + DuckDB engines over the hand-computed dataset (issue-required + backends). Defined here so the fixture name is not a cross-module import that + ruff reads as shadowing the test parameter (F811).""" + async for engine in make_exec_engine(request): + yield engine + + +def _q(*, measures, dimensions=None) -> SlayerQuery: + kw = dict(source_model="orders", time_dimensions=month_td(), measures=measures) + if dimensions is not None: + kw["dimensions"] = dimensions + return SlayerQuery(**kw) + + +def _by_month(resp) -> dict: + return { + month_key(k[0]): r + for k, r in rows_by(resp, "orders.ordered_at").items() + } + + +class TestShapeALocalTimeShift: + """(a) local ``time_shift(amount:sum)`` beside a cross-model sibling.""" + + async def test_prev_amount_sum_and_sibling(self, exec_engine) -> None: + resp = await exec_engine.execute(_q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="amount:sum", name="s"), + ModelMeasure(formula="time_shift(amount:sum, -1)", name="prev"), + ])) + by = _by_month(resp) + # Local sum per month. + assert float(by["2024-01"]["orders.s"]) == pytest.approx(15.0) + assert float(by["2024-02"]["orders.s"]) == pytest.approx(24.0) + assert float(by["2024-03"]["orders.s"]) == pytest.approx(18.0) + # Prior-month sum; Jan (earliest) has none. + assert by["2024-01"]["orders.prev"] is None + assert float(by["2024-02"]["orders.prev"]) == pytest.approx(15.0) + assert float(by["2024-03"]["orders.prev"]) == pytest.approx(24.0) + # The cross-model sibling is unaffected by the shift: customers.spend:sum + # has no shared grain with the month axis, so it broadcasts the + # all-customers total (100 + 200 + 50 = 350) across every month — the + # same value the shift machinery must not perturb. + assert float(by["2024-01"]["orders.cm"]) == pytest.approx(350.0) + assert float(by["2024-03"]["orders.cm"]) == pytest.approx(350.0) + + +class TestShapeBHostRootedCrossingFragment: + """(b) ``time_shift(amount:wscaled_sum)`` — the issue's named repro. The + prior-period WEIGHTED-SCALED sum only computes if the shifted CTE joined + ``regions`` (Part 1). A missing join → the SQL does not bind at all.""" + + async def test_prev_weighted_scaled_sum(self, exec_engine) -> None: + resp = await exec_engine.execute(_q(measures=[ + ModelMeasure(formula="amount:wscaled_sum", name="w"), + ModelMeasure(formula="time_shift(amount:wscaled_sum, -1)", name="prev"), + ])) + by = _by_month(resp) + assert float(by["2024-01"]["orders.w"]) == pytest.approx(35.0) + assert float(by["2024-02"]["orders.w"]) == pytest.approx(48.0) + assert float(by["2024-03"]["orders.w"]) == pytest.approx(46.0) + assert by["2024-01"]["orders.prev"] is None + assert float(by["2024-02"]["orders.prev"]) == pytest.approx(35.0) + assert float(by["2024-03"]["orders.prev"]) == pytest.approx(48.0) + + async def test_sibling_local_sum_not_multiplied_by_fragment_join( + self, exec_engine + ) -> None: + """A local ``amount:sum`` beside the crossing wscaled shift must keep its + true per-month value — the fragment's 1:N-safe join lives only in the + crossing measure's own CTEs, never multiplying the sibling.""" + resp = await exec_engine.execute(_q(measures=[ + ModelMeasure(formula="amount:sum", name="s"), + ModelMeasure(formula="time_shift(amount:wscaled_sum, -1)", name="prev"), + ])) + by = _by_month(resp) + assert float(by["2024-01"]["orders.s"]) == pytest.approx(15.0) + assert float(by["2024-02"]["orders.s"]) == pytest.approx(24.0) + assert float(by["2024-03"]["orders.s"]) == pytest.approx(18.0) + + +class TestChangeDesugars: + """``change`` = current − prior. With a LOCAL inner beside a cross-model + sibling (the shape the guard lift naturally enables — the chain's outer + arithmetic-materialization step renders the subtraction), it must produce the + period-over-period delta.""" + + async def test_change_of_local_with_cross_model_sibling(self, exec_engine) -> None: + resp = await exec_engine.execute(_q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="change(amount:sum)", name="delta"), + ])) + by = _by_month(resp) + assert by["2024-01"]["orders.delta"] is None + assert float(by["2024-02"]["orders.delta"]) == pytest.approx(24.0 - 15.0) + assert float(by["2024-03"]["orders.delta"]) == pytest.approx(18.0 - 24.0) + + async def test_change_pct_of_local_with_cross_model_sibling(self, exec_engine) -> None: + """``change_pct`` = (current − prior) / prior — pins the outer + arithmetic-materialization step, not just CTE creation.""" + resp = await exec_engine.execute(_q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="change_pct(amount:sum)", name="pct"), + ])) + by = _by_month(resp) + assert by["2024-01"]["orders.pct"] is None + assert float(by["2024-02"]["orders.pct"]) == pytest.approx((24.0 - 15.0) / 15.0) + assert float(by["2024-03"]["orders.pct"]) == pytest.approx((18.0 - 24.0) / 24.0) + + +class TestChangeOverCrossModelInnerIsOutOfScope: + """``change`` / ``change_pct`` over a CROSS-MODEL/crossing-fragment inner + adds an arithmetic layer the combined SELECT renders BEFORE the transform + chain, hitting a pre-existing ``RenderContextMissingFacilityError`` (the + TransformKey is not yet materialised as a slot). DEV-1750 does not fix that + separate gap; the contract is that it stays a loud, SPECIFIC error — never + wrong values or unbound SQL. Pinning the exact type stops a real regression + (bad-SQL binding error, silent wrong number) from passing as 'expected'.""" + + async def test_change_of_wscaled_raises_render_context(self, exec_engine) -> None: + q = _q(measures=[ + ModelMeasure(formula="change(amount:wscaled_sum)", name="delta"), + ]) + with pytest.raises(RenderContextMissingFacilityError): + await exec_engine.execute(q) + + async def test_change_pct_of_wscaled_raises_render_context(self, exec_engine) -> None: + q = _q(measures=[ + ModelMeasure(formula="change_pct(amount:wscaled_sum)", name="delta"), + ]) + with pytest.raises(RenderContextMissingFacilityError): + await exec_engine.execute(q) + + +class TestSiblingProtectionUnderFanOut: + """The crossing measure's join is 1:N (``orders → line_items``). If it leaked + into the host base, a sibling ``amount:sum`` would be MULTIPLIED by the + line-item count. Isolation keeps the fan-out inside the crossing measure's + own CTEs, so the sibling stays true — a claim only a fan-out dataset can + actually test (Codex F1).""" + + async def test_local_sibling_not_multiplied_by_one_to_many_fragment( + self, exec_engine + ) -> None: + resp = await exec_engine.execute(_q(measures=[ + ModelMeasure(formula="amount:sum", name="s"), + ModelMeasure(formula="amount:liscaled_sum", name="li"), + ModelMeasure(formula="time_shift(amount:liscaled_sum, -1)", name="prev"), + ])) + by = _by_month(resp) + # Sibling amount:sum stays unmultiplied (order 1 counted ONCE, not twice). + assert float(by["2024-01"]["orders.s"]) == pytest.approx(15.0) + assert float(by["2024-02"]["orders.s"]) == pytest.approx(24.0) + assert float(by["2024-03"]["orders.s"]) == pytest.approx(18.0) + # The crossing measure itself keeps the fanned-out weighted sum... + assert float(by["2024-01"]["orders.li"]) == pytest.approx(50.0) + # ...and its prior-period value (Part 1: the 1:N join is pulled into the + # shifted CTE, so the shifted re-aggregation fans out identically). + assert by["2024-01"]["orders.prev"] is None + assert float(by["2024-02"]["orders.prev"]) == pytest.approx(50.0) + assert float(by["2024-03"]["orders.prev"]) == pytest.approx(24.0) + + +class TestConsecutivePeriodsExecution: + """cp is lifted ENTIRELY (no re-aggregation). Executed streak values pin that + it reads the materialised alias correctly on both backends.""" + + async def test_cp_varying_predicate_with_cross_model_sibling(self, exec_engine) -> None: + # amount:sum > 20 → Jan 15=F, Feb 24=T, Mar 18=F → streak 0,1,0. + resp = await exec_engine.execute(_q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="consecutive_periods(amount:sum > 20)", name="streak"), + ])) + by = _by_month(resp) + assert int(by["2024-01"]["orders.streak"]) == 0 + assert int(by["2024-02"]["orders.streak"]) == 1 + assert int(by["2024-03"]["orders.streak"]) == 0 + + async def test_cp_over_target_grain_inner(self, exec_engine) -> None: + # customers.spend:sum > 0 holds every month → cumulative streak 1,2,3. + # This is the shape that stays GUARDED for time_shift but renders for cp. + resp = await exec_engine.execute(_q(measures=[ + ModelMeasure(formula="consecutive_periods(customers.spend:sum > 0)", + name="streak"), + ])) + by = _by_month(resp) + assert int(by["2024-01"]["orders.streak"]) == 1 + assert int(by["2024-02"]["orders.streak"]) == 2 + assert int(by["2024-03"]["orders.streak"]) == 3 + + +class TestNullDimPartitionSurvival: + """A ``status`` dimension with a NULL group: the shift is partitioned by + status and joined back null-safely, so the NULL-status group keeps its + prior-month value instead of dropping to NULL.""" + + async def test_null_status_group_keeps_prev(self, exec_engine) -> None: + # status groups by month: + # 'ok' : Jan (o1 10, o2 5)=15, Feb (o3 20)=20 + # 'hold': Mar (o4 10)=10 + # NULL : Feb (o5 4)=4, Mar (o6 8)=8 + resp = await exec_engine.execute(_q( + dimensions=["status"], + measures=[ + ModelMeasure(formula="amount:sum", name="s"), + ModelMeasure(formula="time_shift(amount:sum, -1)", name="prev"), + ], + )) + by = { + (r["orders.status"], month_key(r["orders.ordered_at"])): r + for r in resp.data + } + # NULL-status: Feb=4 has no prior (Jan NULL group empty) → None; + # Mar=8's prior is Feb's NULL-group 4 — found only via a null-safe + # join-back on the NULL status grain. + assert float(by[(None, "2024-02")]["orders.s"]) == pytest.approx(4.0) + assert float(by[(None, "2024-03")]["orders.prev"]) == pytest.approx(4.0) + + +class TestDateRangeFrameBoundOmitted: + """7b.3c: a ``date_range`` frame bound is omitted from the shifted CTE, so + the earliest VISIBLE bucket still finds its prior period from raw data + outside the range.""" + + async def test_feb_prev_reads_january_outside_range(self, exec_engine) -> None: + # Restrict to Feb–Mar; Feb's prior (January) is outside the range but the + # shifted CTE reads raw, so Feb.prev = Jan's 15 (local sum). A sibling + # cross-model measure keeps the query on the cross-model chain. + resp = await exec_engine.execute(SlayerQuery( + source_model="orders", + time_dimensions=[TimeDimension( + dimension=ColumnRef(name="ordered_at"), + granularity=TimeGranularity.MONTH, + date_range=["2024-02-01", "2024-03-31"], + )], + measures=[ + ModelMeasure(formula="amount:sum", name="s"), + ModelMeasure(formula="time_shift(amount:sum, -1)", name="prev"), + ModelMeasure(formula="customers.spend:sum", name="cm"), + ], + )) + by = _by_month(resp) + assert "2024-01" not in by, "January must be filtered out of the result" + assert float(by["2024-02"]["orders.prev"]) == pytest.approx(15.0) diff --git a/tests/test_dev1750_golden_sql.py b/tests/test_dev1750_golden_sql.py new file mode 100644 index 00000000..4ddad955 --- /dev/null +++ b/tests/test_dev1750_golden_sql.py @@ -0,0 +1,127 @@ +"""DEV-1750 — golden SQL baseline for the time_shift/cp × cross-model shapes. + +Same harness and four-step blessing loop as ``tests/test_dev1748_golden_sql.py`` +(read ``tests/_golden_harness.py`` for the mechanics); only the matrix differs. + +This pins exactly WHAT the guard-lift emits, as a diff a reviewer reads before +approving — and reaches dialects execution cannot (Postgres, T-SQL, BigQuery). +The baseline was first recorded against PRE-lift code, where every lifted case +is a recorded ``stage 7b.15e`` raise; the lift moves those entries to SQL (and +re-narrows case (c)'s message), each re-blessed through ``ALLOWED_DELTAS`` with +the manifest emptied again. +""" + +from __future__ import annotations + +from pathlib import Path + +from slayer.core.query import SlayerQuery + +from tests._dev1750_fixtures import dev1750_models +from tests._engine_helpers import _engine_generate +from tests._golden_harness import bind_golden_tests, record_raise + + +GOLDEN_PATH = Path(__file__).parent / "golden" / "dev1750_sql_baseline.json" + +#: Postgres/SQLite/DuckDB for the executable regimes; T-SQL and BigQuery because +#: both mangle dotted aliases at emission and T-SQL rejects a nested WITH. +DIALECTS = ["postgres", "sqlite", "duckdb", "tsql", "bigquery"] + +# ``::`` -> why this entry is allowed to change right now. +# A PENDING list, not a log: a committed state always has this empty. +ALLOWED_DELTAS: dict[str, str] = {} + +_MONTH = [{"dimension": "ordered_at", "granularity": "month"}] + + +def _q(**kw) -> SlayerQuery: + kw.setdefault("source_model", "orders") + kw.setdefault("time_dimensions", _MONTH) + return SlayerQuery(**kw) + + +def _cases() -> dict: + """The matrix. Keys are stable ids — renaming one is a golden change.""" + return { + # (a) local time_shift beside a cross-model sibling. + "a/local_ts_cm_sibling": _q(measures=[ + {"formula": "customers.spend:sum", "name": "cm"}, + {"formula": "time_shift(amount:sum, -1)", "name": "prev"}, + ]), + # (b) host-rooted crossing-fragment inner — the named repro. + "b/host_rooted_wscaled": _q(measures=[ + {"formula": "time_shift(amount:wscaled_sum, -1)", "name": "prev"}, + ]), + "b/host_rooted_wscaled_user_kwarg": _q(measures=[ + {"formula": "time_shift(amount:wscaled_sum(w='customers__regions.weight'), -1)", + "name": "prev"}, + ]), + "b/wscaled_with_local_sibling": _q(measures=[ + {"formula": "amount:sum", "name": "s"}, + {"formula": "time_shift(amount:wscaled_sum, -1)", "name": "prev"}, + ]), + # (b) crossing a 1:N join — the shifted CTE must pull the fan-out join. + "b/liscaled_one_to_many": _q(measures=[ + {"formula": "amount:sum", "name": "s"}, + {"formula": "time_shift(amount:liscaled_sum, -1)", "name": "prev"}, + ]), + # consecutive_periods — lifted entirely (no target-grain failure mode). + "cp/local_with_cm_sibling": _q(measures=[ + {"formula": "customers.spend:sum", "name": "cm"}, + {"formula": "consecutive_periods(amount:sum > 0)", "name": "streak"}, + ]), + "cp/over_target_grain": _q(measures=[ + {"formula": "consecutive_periods(customers.spend:sum > 0)", "name": "streak"}, + ]), + # change / change_pct desugar to time_shift. + "change/of_wscaled": _q(measures=[ + {"formula": "change(amount:wscaled_sum)", "name": "delta"}, + ]), + "change_pct/of_local_with_cm_sibling": _q(measures=[ + {"formula": "customers.spend:sum", "name": "cm"}, + {"formula": "change_pct(amount:sum)", "name": "delta"}, + ]), + # (c) target-grain inner — stays guarded (records the narrowed raise). + "c/target_grain_guarded": _q(measures=[ + {"formula": "time_shift(customers.spend:sum, -1)", "name": "prev"}, + ]), + # window op over cross-model — never guarded; a regression anchor. + "window/cumsum_cross_model": _q(measures=[ + {"formula": "cumsum(customers.spend:sum)", "name": "run"}, + ]), + # composite-input transform — 7b.11 must survive the lift (records raise). + "composite/still_7b11": _q(measures=[ + {"formula": "customers.spend:sum", "name": "cm"}, + {"formula": "time_shift(amount:sum + amount:sum, -1)", "name": "prev"}, + ]), + # first/last crossing time arg — records whatever the seam does. + "first_last/crossing_time_arg": _q(measures=[ + {"formula": "customers.spend:sum", "name": "cm"}, + {"formula": "time_shift(amount:last(customers.signup_at), -1)", "name": "prev"}, + ]), + } + + +async def _generate_one(query: SlayerQuery, dialect: str): + """Emitted SQL, or a structured record of the raised error.""" + models = dev1750_models() + try: + return await _engine_generate( + query=query, model=models[0], extra_models=models[1:], + dialect=dialect, validate=False, + ) + except Exception as exc: # noqa: BLE001 — the exception itself is contract + return record_raise(exc) + + +# The blessing-loop wiring (baseline fixture + the five shared guards) is bound +# once in the harness; this module keeps only its matrix, path, and dialects. +bind_golden_tests( + namespace=globals(), + golden_path=GOLDEN_PATH, + cases=_cases, + dialects=DIALECTS, + allowed=ALLOWED_DELTAS, + generate_one=_generate_one, +) diff --git a/tests/test_dev1750_guard_lift.py b/tests/test_dev1750_guard_lift.py new file mode 100644 index 00000000..4ac8c5bb --- /dev/null +++ b/tests/test_dev1750_guard_lift.py @@ -0,0 +1,208 @@ +"""DEV-1750 — lifting the ``time_shift`` / ``consecutive_periods`` × +cross-model guard (``stage 7b.15e``). + +The old guard rejected EITHER temporal op the moment the query also routed +through the cross-model chain, regardless of what the transform's own inner +aggregate was. DEV-1750 narrows it to exactly the one shape that cannot render: +a ``time_shift`` whose inner aggregate is a TARGET-GRAIN cross-model aggregate +(``cte_root_model`` is None) — host-rooted re-aggregation there would multiply +target rows through the 1:N join. Everything else renders: + +* (a) local inner + a sibling cross-model measure, +* (b) host-rooted crossing-fragment inner (``amount:wscaled_sum``), +* ``consecutive_periods`` over any inner (it reads a materialised alias, never + re-aggregates, so it has no target-grain failure mode — lifted entirely), +* ``change`` / ``change_pct`` (they desugar to ``time_shift``). + +Every "renders" case here RAISES ``stage 7b.15e`` on ``main`` — the feature is +missing — so each fails for the right reason. +""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from slayer.sql.generator import SQLGenerator + +from tests._dev1750_fixtures import ( + ModelMeasure, + SlayerQuery, + gen, + month_td, +) + +pytestmark = pytest.mark.asyncio + + +# --------------------------------------------------------------------------- # +# The narrowed-guard message contract (asserted by tests; defined once). +# --------------------------------------------------------------------------- # +_GUARD_TAG = "stage 7b.15e" +_NARROWED_MARKER = "TARGET-GRAIN" # the distinguishing phrase the new message adds + + +def _q(*, measures) -> SlayerQuery: + return SlayerQuery( + source_model="orders", time_dimensions=month_td(), measures=measures, + ) + + +# --------------------------------------------------------------------------- # +# Shapes that must now RENDER (no raise). +# --------------------------------------------------------------------------- # +class TestLiftedShapesRender: + async def test_a_local_time_shift_with_cross_model_sibling(self) -> None: + """(a) A local ``time_shift`` beside a cross-model measure: the sibling + routes the query to the cross-model chain, but the shift's own inner is + local and re-aggregates host-rooted.""" + sql = await gen(_q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="time_shift(amount:sum, -1)", name="prev"), + ])) + assert "shifted_" in sql, sql + assert "_cm_" in sql, sql # the sibling still isolates into its own CTE + + async def test_b_host_rooted_crossing_fragment_inner(self) -> None: + """(b) ``time_shift`` over the crossing-fragment aggregate — the issue's + named repro. Renders; the shifted CTE pulls the fragment's join (pinned + in the fragment-join module).""" + sql = await gen(_q(measures=[ + ModelMeasure(formula="time_shift(amount:wscaled_sum, -1)", name="prev"), + ])) + assert "shifted_" in sql, sql + + async def test_consecutive_periods_with_cross_model_sibling(self) -> None: + sql = await gen(_q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="consecutive_periods(amount:sum > 0)", name="streak"), + ])) + assert "cp_" in sql, sql + + async def test_consecutive_periods_over_target_grain_inner(self) -> None: + """cp has NO target-grain failure mode: it reads the combined SELECT's + already-materialised cross-model alias and never re-aggregates. So cp + over a target-grain aggregate — the shape that stays guarded for + time_shift — renders.""" + sql = await gen(_q(measures=[ + ModelMeasure( + formula="consecutive_periods(customers.spend:sum > 0)", + name="streak", + ), + ])) + assert "cp_" in sql, sql + + async def test_change_desugars_and_renders(self) -> None: + sql = await gen(_q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="change(amount:sum)", name="delta"), + ])) + assert "shifted_" in sql, sql + + async def test_change_pct_desugars_and_renders(self) -> None: + sql = await gen(_q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="change_pct(amount:sum)", name="delta"), + ])) + assert "shifted_" in sql, sql + + +# --------------------------------------------------------------------------- # +# The one shape that must STAY guarded — narrowed message. +# --------------------------------------------------------------------------- # +class TestTargetGrainStaysGuarded: + async def test_c_target_grain_inner_raises_narrowed_guard(self) -> None: + """(c) ``time_shift(customers.spend:sum, -1)`` — the inner aggregate is + target-rooted. Host-rooted re-aggregation would multiply target rows, so + it stays behind a guard whose message now NAMES the target-grain shape + (not the blanket op-based text).""" + q = _q(measures=[ + ModelMeasure(formula="time_shift(customers.spend:sum, -1)", name="prev"), + ]) + with pytest.raises(NotImplementedError) as ei: + await gen(q) + msg = str(ei.value) + assert _GUARD_TAG in msg, msg + assert _NARROWED_MARKER in msg, msg + # It must NOT be the old blanket wording (which raised for ANY cross-model + # coexistence, including the now-supported shapes). + assert "also has a cross-model aggregate" not in msg, msg + + async def test_c_guard_fires_before_the_emitter_runs(self) -> None: + """The narrowed guard is a plan-ownership decision made BEFORE any shifted + CTE is emitted — a target-grain shape must never reach + ``_emit_time_shift_ctes_for_planned`` (which would emit the row- + multiplying host-rooted re-aggregation).""" + real = SQLGenerator._emit_time_shift_ctes_for_planned + calls: list = [] + + def _spy(self, **kwargs): + calls.append(kwargs.get("slot")) + return real(self, **kwargs) + + q = _q(measures=[ + ModelMeasure(formula="time_shift(customers.spend:sum, -1)", name="prev"), + ]) + with patch.object( + SQLGenerator, "_emit_time_shift_ctes_for_planned", _spy, + ): + with pytest.raises(NotImplementedError): + await gen(q) + assert calls == [], ( + "the target-grain guard let the shifted emitter run before raising" + ) + + async def test_b_does_reach_the_emitter(self) -> None: + """Counter-case, so the spy above is not vacuous: the host-rooted shape + (b) DOES reach ``_emit_time_shift_ctes_for_planned``.""" + real = SQLGenerator._emit_time_shift_ctes_for_planned + calls: list = [] + + def _spy(self, **kwargs): + calls.append(kwargs.get("slot")) + return real(self, **kwargs) + + with patch.object( + SQLGenerator, "_emit_time_shift_ctes_for_planned", _spy, + ): + await gen(_q(measures=[ + ModelMeasure(formula="time_shift(amount:wscaled_sum, -1)", name="prev"), + ])) + assert len(calls) == 1, calls + + +# --------------------------------------------------------------------------- # +# Regression: window ops over cross-model were always allowed; still are. +# --------------------------------------------------------------------------- # +class TestWindowOpsUnaffected: + async def test_cumsum_over_cross_model_still_renders(self) -> None: + """``cumsum`` (a window op, never guarded) over a cross-model aggregate + must keep rendering — the narrowing must not disturb the window arm.""" + sql = await gen(_q(measures=[ + ModelMeasure(formula="cumsum(customers.spend:sum)", name="run"), + ])) + # A cumsum renders as a windowed running sum in the transform-chain step + # CTE — pin the actual window shape, not a vacuous SUM( that any aggregate + # SQL contains. + assert "step1" in sql, sql + assert "OVER (" in sql, sql + + +# --------------------------------------------------------------------------- # +# Pre-existing loud errors that must survive the lift. +# --------------------------------------------------------------------------- # +class TestPreExistingGuardsSurvive: + async def test_composite_input_time_shift_still_raises_7b11(self) -> None: + """A composite-input transform (``time_shift`` over an arithmetic of two + aggregates) is deferred by 7b.11 and must keep raising that — the lift + touches only the 7b.15e guard.""" + q = _q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure( + formula="time_shift(amount:sum + amount:sum, -1)", name="prev", + ), + ]) + with pytest.raises(NotImplementedError) as ei: + await gen(q) + assert "7b.11" in str(ei.value), str(ei.value) diff --git a/tests/test_dev1750_guard_ownership.py b/tests/test_dev1750_guard_ownership.py new file mode 100644 index 00000000..db4e7950 --- /dev/null +++ b/tests/test_dev1750_guard_ownership.py @@ -0,0 +1,113 @@ +"""DEV-1750 — the narrowed guard is a PLAN-OWNERSHIP decision, pinned at the +planner level (Codex test-review F1). + +The guard must fire for a ``time_shift`` whose inner aggregate is a TARGET-GRAIN +cross-model aggregate — identified by its ``CrossModelAggregatePlan`` having +``cte_root_model is None`` — and must NOT fire for a host-rooted one +(``cte_root_model`` = the host model name). Asserting only the error message +would let an implementation classify by formula text or op name instead; these +tests pin the exact ownership property the decision must read, so changing only +``cte_root_model`` flips supported vs guarded. +""" + +from __future__ import annotations + +from slayer.core.keys import AggregateKey, TransformKey +from slayer.engine.source_bundle import ResolvedSourceBundle +from slayer.engine.stage_planner import plan_query + +from tests._dev1750_fixtures import ( + ModelMeasure, + SlayerQuery, + customers_model, + line_items_model, + month_td, + orders_model, + regions_model, +) + + +def _bundle() -> ResolvedSourceBundle: + return ResolvedSourceBundle( + source_model=orders_model(), + referenced_models=[customers_model(), regions_model(), line_items_model()], + ) + + +def _plan(formula: str, *, name: str = "prev"): + return plan_query( + query=SlayerQuery( + source_model="orders", time_dimensions=month_td(), + measures=[ModelMeasure(formula=formula, name=name)], + ), + bundle=_bundle(), + ) + + +def _all_slots(planned): + return [ + *planned.row_slots, + *planned.aggregate_slots, + *planned.combined_expression_slots, + ] + + +def _time_shift_inner_slot_id(planned) -> str: + """The slot id of the aggregate a ``time_shift`` layer wraps.""" + layer = next( + layer for layer in planned.transform_layers if layer.op == "time_shift" + ) + slots = _all_slots(planned) + out_slot = next(s for s in slots if s.id in layer.slot_ids) + assert isinstance(out_slot.key, TransformKey), out_slot.key + inner_key = out_slot.key.input + assert isinstance(inner_key, AggregateKey), inner_key + inner_slot = next(s for s in slots if s.key == inner_key) + return inner_slot.id + + +def _plan_owning(planned, slot_id: str): + return next( + (p for p in planned.cross_model_aggregate_plans + if p.aggregate_slot_id == slot_id), + None, + ) + + +class TestGuardOwnership: + def test_b_host_rooted_inner_is_owned_by_a_host_rooted_plan(self) -> None: + """(b) ``time_shift(amount:wscaled_sum)`` — the inner aggregate's plan is + HOST-rooted (``cte_root_model`` == the host), the shape the lift renders.""" + planned = _plan("time_shift(amount:wscaled_sum, -1)") + inner_sid = _time_shift_inner_slot_id(planned) + plan = _plan_owning(planned, inner_sid) + assert plan is not None, "shape (b) inner aggregate has no cross-model plan" + assert plan.cte_root_model == "orders", plan.cte_root_model + + def test_c_target_grain_inner_is_owned_by_a_target_rooted_plan(self) -> None: + """(c) ``time_shift(customers.spend:sum)`` — the inner aggregate's plan is + TARGET-rooted (``cte_root_model is None``), the shape that stays guarded.""" + planned = _plan("time_shift(customers.spend:sum, -1)") + inner_sid = _time_shift_inner_slot_id(planned) + plan = _plan_owning(planned, inner_sid) + assert plan is not None, "shape (c) inner aggregate has no cross-model plan" + assert plan.cte_root_model is None, plan.cte_root_model + + def test_a_local_inner_has_no_cross_model_plan(self) -> None: + """(a) ``time_shift(amount:sum)`` beside a cross-model sibling — the shift's + OWN inner aggregate is local, so it owns NO cross-model plan (the guard's + ownership lookup misses it, and it renders).""" + planned = plan_query( + query=SlayerQuery( + source_model="orders", time_dimensions=month_td(), + measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure(formula="time_shift(amount:sum, -1)", name="prev"), + ], + ), + bundle=_bundle(), + ) + inner_sid = _time_shift_inner_slot_id(planned) + assert _plan_owning(planned, inner_sid) is None + # The sibling DID isolate — so the query really is on the cross-model path. + assert planned.cross_model_aggregate_plans diff --git a/tests/test_dev1750_shifted_fragment_joins.py b/tests/test_dev1750_shifted_fragment_joins.py new file mode 100644 index 00000000..9df06310 --- /dev/null +++ b/tests/test_dev1750_shifted_fragment_joins.py @@ -0,0 +1,161 @@ +"""DEV-1750 Part 1 — the shifted (``time_shift``) CTE must register the joins +its inner aggregate's template FRAGMENTS (and positional column args) cross, +through the same one door the host and ``_cm_`` paths use +(``_register_fragment_kwarg_joins`` / ``scope.resolve``). + +Before this, ``_emit_time_shift_ctes_for_planned`` registered source / typed +column kwargs / ``column_filter_key`` but never the string fragments, so a +crossing default like ``w='customers__regions.weight'`` would re-aggregate +``SUM(orders.amount * customers__regions.weight)`` in the shifted CTE with no +join to ``regions`` — SQL no database binds. + +The SQL-shape assertions target the ``shifted_*`` CTE body specifically: a +whole-SQL substring check can be satisfied by a valid alias in the base or the +combined SELECT. +""" + +from __future__ import annotations + +import pytest + +from slayer.core.keys import AggregateKey, ColumnKey +from slayer.sql.generator import SQLGenerator + +from tests._dev1750_fixtures import ( + ModelMeasure, + SlayerQuery, + base_cte_body, + gen, + month_td, + orders_model, + shifted_cte_body, +) + + +def _q(*, measures) -> SlayerQuery: + return SlayerQuery( + source_model="orders", time_dimensions=month_td(), measures=measures, + ) + + +class TestShiftedCteFragmentDefaultParam: + """Shape (b): the crossing DEFAULT ``AggregationParam.sql`` fragment pulls + its two join hops into the shifted CTE's own FROM.""" + + async def test_shifted_cte_joins_the_fragment_hops(self) -> None: + sql = await gen(_q(measures=[ + ModelMeasure(formula="time_shift(amount:wscaled_sum, -1)", name="prev"), + ])) + shifted = shifted_cte_body(sql) + # Both hops of the fragment's path are real join clauses in the shifted + # CTE (orders → customers → regions), the second under the __-path alias. + assert "JOIN customers" in shifted, shifted + assert "regions AS customers__regions" in shifted, shifted + # The fragment rendered qualified at the host path (not a bare, unbound + # ``regions.weight``), so it binds to the join the CTE now carries. + assert "customers__regions.weight" in shifted, shifted + # And the re-aggregation actually multiplies by the fragment: the host + # ``amount`` operand appears alongside the weight fragment pinned above. + assert "orders.amount" in shifted, shifted + + async def test_host_base_does_not_carry_the_shifted_reaggregation(self) -> None: + """The shifted re-aggregation belongs to the shifted CTE, not the host + ``base`` — a leak there would double-join ``regions`` at host grain.""" + sql = await gen(_q(measures=[ + ModelMeasure(formula="amount:sum", name="s"), + ModelMeasure(formula="time_shift(amount:wscaled_sum, -1)", name="prev"), + ])) + base = base_cte_body(sql) + # The local sibling ``amount:sum`` lives in the base; the crossing + # wscaled re-aggregation does not (it is isolated + shifted elsewhere). + assert "SUM(orders.amount)" in base, base + assert "customers__regions.weight" not in base, base + + +class TestShiftedCteFragmentUserKwarg: + """A user-supplied string kwarg that substitutes into the template is a + fragment too (parity with the ``_cm_`` path) — its crossing join must land + in the shifted CTE.""" + + async def test_user_string_kwarg_crossing_join_registers(self) -> None: + sql = await gen(_q(measures=[ + ModelMeasure( + formula="time_shift(amount:wscaled_sum(w='customers__regions.weight'), -1)", + name="prev", + ), + ])) + shifted = shifted_cte_body(sql) + assert "JOIN customers" in shifted, shifted + assert "regions AS customers__regions" in shifted, shifted + assert "customers__regions.weight" in shifted, shifted + + +class TestFragmentRegistrationUnit: + """Unit-level parity with ``tests/test_dev1745_fragment_joins.py``: the + shared helper treats an OVERRIDDEN default as replaced, so the shifted path + (which calls the same helper) cannot double-register the default fragment's + join.""" + + @staticmethod + def _entered(*, kwargs) -> list: + gen_ = SQLGenerator(dialect="duckdb") + seen: list = [] + gen_._enter_mode_a_expression = ( # type: ignore[method-assign] + lambda **kw: seen.append(kw["sql"]) + ) + gen_._register_fragment_kwarg_joins( + key=AggregateKey( + source=ColumnKey(path=(), leaf="amount"), agg="wscaled_sum", + kwargs=kwargs, + ), + scope=object(), + model=orders_model(), + ) + return seen + + def test_non_overridden_default_is_scanned(self) -> None: + entered = self._entered(kwargs=()) + assert entered == ["customers__regions.weight"], entered + + def test_overridden_default_uses_the_override_not_the_default(self) -> None: + # Overriding ``w`` replaces the default: only the override is scanned, + # never the default ``customers__regions.weight``. + entered = self._entered(kwargs=(("w", "customers.region_id"),)) + assert entered == ["customers.region_id"], entered + assert "customers__regions.weight" not in entered, entered + + +class TestShiftedCtePositionalArgRegistration: + """DEV-1750 (Codex F2/F3): the shifted block must resolve every crossing + positional column arg through the scope, mirroring the ``_cm_`` path's + ``for _arg in local_agg_key.args`` loop — a first/last explicit time arg is + the standing case. + + First/last over ``time_shift`` is not renderable through the shifted + re-aggregation seam (it needs a ranked subquery), so end-to-end this shape + raises a loud error rather than emitting wrong SQL. The contract pinned here + is: NO silent scope leak / unbound alias — either the join is present, or a + specific NotImplementedError is raised.""" + + @pytest.mark.parametrize("agg", ["last", "first"]) + async def test_first_last_crossing_time_arg_no_silent_leak(self, agg: str) -> None: + q = _q(measures=[ + ModelMeasure(formula="customers.spend:sum", name="cm"), + ModelMeasure( + formula=f"time_shift(amount:{agg}(customers.signup_at), -1)", + name="prev", + ), + ]) + try: + sql = await gen(q) + except NotImplementedError as exc: + # A loud deferral is acceptable (the ranked-in-shifted seam does not + # exist) — but it must be a SPECIFIC deferral, never the old blanket + # op guard, whose wording the lift removes. + assert "also has a cross-model aggregate" not in str(exc), str(exc) + return + # If it DID render, the ranking arg's join must be in the shifted CTE — + # never an unbound reference / silent scope leak. + shifted = shifted_cte_body(sql) + assert "customers.signup_at" in shifted, shifted + assert "JOIN customers" in shifted, shifted