Skip to content

DEV-1712 Stage 8: order-only hidden slots + plan-time validations (order-only refs, CMA trim, partition_by) - #274

Open
ZmeiGorynych wants to merge 2 commits into
egor/dev-1703-comprehensive-approach-expressions-crossing-joins-on-thefrom
egor/dev-1712-dev-1703-stage-8-hidden-slots-plan-time-validations-order
Open

DEV-1712 Stage 8: order-only hidden slots + plan-time validations (order-only refs, CMA trim, partition_by)#274
ZmeiGorynych wants to merge 2 commits into
egor/dev-1703-comprehensive-approach-expressions-crossing-joins-on-thefrom
egor/dev-1712-dev-1703-stage-8-hidden-slots-plan-time-validations-order

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented Aug 3, 2026

Copy link
Copy Markdown
Member

DEV-1703 Stage 8. Closes DEV-1712; closes DEV-1472, DEV-1495 bug 2, DEV-1497; promotes the Stage-8 DEV-1645 Flavor-A ORDER-BY pins.

What lands

Order-only hidden slots (Law 2) — an ORDER BY ref not declared as a dimension/measure is classified at plan time (stage_planner.plan_query, right after _bucket_slots):

Order target (undeclared) Behavior
aggregate — local or cross-model materialised hidden, sorted on, stripped from result + StageSchema (never rejected)
local row column, raw-rows query (distinct_dimension_values=false) split emission ORDER BY orders.<col> (mixed-case-aware)
local row column, grouped/dedup query ValueError (HTTP 400) — not in GROUP BY; add to dims or order by an aggregate
joined row column UnresolvableOrderColumnError (HTTP 400)
inline transform / composite (change(amount:sum)) ValueError — declare it as a measure. Deferred to DEV-1733 (worktree + strict-xfail future tests). Composite arithmetic is unexpressible via OrderItem (Pydantic rejects at construction).

The grouping predicate is planner-semantic (agg_slots present, or dims/tds with dedup), so a hidden order aggregate that induces grouping correctly forces a row column in the same query to be rejected. The generator's old NotImplementedError becomes a defensive assertion — the plan-time pass guarantees only the split shape reaches it.

Hidden cross-model-aggregate trim (DEV-1495 bug 2) — an order-only CMA is hidden=True/public_alias=None and skips the combined projection, with a CTE-qualified ORDER BY term. Gated on not transform_layers so a hidden CMA feeding a cumsum(...) step stays projected for the step CTE (the transform outer-wrap trims there). The malformed orders.customers._sum alias half was already fixed by Stage 9's naming module.

partition_by grain guard (DEV-1497) — a pre-intern pass (planning.rewrite_rank_partition_keys, mirroring lower_sugar_transforms' identity-preserving rebuild) validates every rank-family partition_by key resolves to a query dimension/time-dimension by exact ValueKey membership (the typed binder resolves it to a ValueKey before validation, so legacy string-match ambiguity can't arise). A time-dim source column is rewritten to its TimeTruncKey bucket (kills the raw-timestamp grain widening + duplicate alias); a non-dimension raises the restored legacy message (transform + column + available dims).

DEV-1645 Flavor-A ORDER BY — ported main's lost legacy _OrderColRef / _order_split_sql / _resolve_order_column fix (split-not-composite + UnresolvableOrderColumnError) into the legacy generator across all three order-emission sites, so the 7 pinned unit tests + 1 integration pin promote. tests/parity_xfails.py is now empty — the DEV-1485 (Stage 11) end-state.

Deliberate divergence from main: the typed pipeline rejects a grouped raw-row order at plan time (HTTP 400) instead of emitting SQL the database rejects at execution.

Tests

New tests/test_dev1712_order_only_hidden_slots.py (the contract table incl. execution/response-strip, diamond CMA, partition matrix, transform deferral + DEV-1733 xfail). Un-pinned: 7 Flavor-A unit + 1 Postgres integration + the DEV-1495/DEV-1497 xfails. The skipped cross-stage test became 3 passing variants. Full non-integration suite green (8970 passed, 0 failed, 31 xfailed); ruff clean.

Out of scope

Result-key naming (Stage 9, already landed); windowed measures (Stage 10); inline transform/composite order targets (DEV-1733).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for ordering by undeclared aggregates without including them in results.
    • Improved ordering for supported raw-row fields, derived columns, and time dimensions.
    • Added validation and time-bucket handling for ranking partition fields.
  • Bug Fixes

    • Improved hidden ordering and cross-model query handling.
    • Added clearer errors for unsupported grouped, joined, and transformed fields.
    • Restored ordering consistency across query paths.
  • Documentation

    • Documented ordering rules and ranking partition validation.

Implements the DEV-1703 Stage 8 slice: ORDER BY refs not declared as
dimensions/measures, hidden cross-model-aggregate projection trim, and
the rank-family partition_by grain guard.

Order-only refs (Law 2), classified at plan time in stage_planner.plan_query:
- aggregate (local or cross-model): materialised hidden, sorted on, stripped
  from the result / StageSchema (never rejected).
- local row column: split emission `orders.<col>` in the raw-rows case; a
  grouped query raises ValueError (not in GROUP BY).
- joined row column: UnresolvableOrderColumnError.
- inline transform/composite (change(...) etc.): ValueError -> declare as a
  measure. Full support deferred to DEV-1733 (worktree + strict-xfail future
  tests). Composite arithmetic is unexpressible via OrderItem (Pydantic).
The generator's old NotImplementedError becomes a defensive assertion.

Hidden cross-model aggregate trim (DEV-1495 bug 2): an order-only CMA is
hidden=True/public_alias=None and skips the combined projection, with a
CTE-qualified ORDER BY term -- gated on `not transform_layers` so a hidden
CMA feeding a cumsum step stays projected for the step CTE. The malformed
`._sum` alias half was already fixed by Stage 9's naming module.

partition_by grain guard (DEV-1497): a pre-intern pass
(planning.rewrite_rank_partition_keys, mirroring lower_sugar_transforms)
validates each rank-family partition_by key is a query dimension/time-dim by
exact ValueKey membership, and rewrites a time-dim source column to its
TimeTruncKey bucket (no more raw-timestamp grain widening / duplicate alias);
a non-dimension raises the restored legacy message.

DEV-1645 Flavor-A ORDER BY: ported main's lost legacy _OrderColRef /
_order_split_sql / _resolve_order_column fix (split-not-composite +
UnresolvableOrderColumnError) into the legacy generator so the 7 pinned unit
tests + 1 integration pin promote. tests/parity_xfails.py is now empty (the
DEV-1485 Stage 11 end-state). Deliberate typed-pipeline divergence: a grouped
raw-row order is rejected at plan time (HTTP 400) rather than emitting SQL the
database rejects at execution.

Docs: queries.md ORDER BY semantics, formulas.md partition_by note,
slayer-query skill, DECISIONS.md entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear

linear Bot commented Aug 3, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e03c4f5a-8ef8-40d3-b43b-1c664e1aae94

📥 Commits

Reviewing files that changed from the base of the PR and between f5e588b and b6fec80.

📒 Files selected for processing (4)
  • slayer/engine/planning.py
  • slayer/engine/stage_planner.py
  • tests/test_dev1712_order_only_hidden_slots.py
  • tests/test_nested_dag_cross_stage_refs.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_dev1712_order_only_hidden_slots.py
  • slayer/engine/planning.py
  • tests/test_nested_dag_cross_stage_refs.py

📝 Walkthrough

Walkthrough

Changes

Order planning and SQL generation

Layer / File(s) Summary
Rank partition validation and rewriting
slayer/engine/planning.py, slayer/engine/stage_planner.py, docs/concepts/formulas.md, DECISIONS.md
Rank-family partition_by keys are validated against query dimensions. Time source columns are rewritten to truncated dimension keys.
Order target classification and reference rendering
slayer/engine/stage_planner.py, slayer/sql/generator.py, docs/concepts/queries.md, .claude/skills/slayer-query.md, tests/test_sql_generator.py
Undeclared order targets are classified during planning. SQL generation distinguishes projected aliases from qualified base-column references and rejects unsupported targets.
Hidden cross-model aggregate ordering
slayer/sql/generator.py, tests/test_nested_dag_cross_stage_refs.py, tests/test_projection_trim.py
Order-only cross-model aggregates can be removed from combined projections while retaining CTE-qualified ORDER BY references.
Stage 8 regression coverage
tests/test_dev1712_order_only_hidden_slots.py, tests/test_nested_dag_cross_stage_refs.py, tests/parity_xfails.py
Tests cover hidden slots, grouping rules, joined-column errors, partition validation, deferred expressions, filtered aggregates, and restored parity cases.

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

Possibly related PRs

  • MotleyAI/slayer#257: Both changes update planned-query ORDER BY generation in slayer/sql/generator.py.
  • MotleyAI/slayer#266: Both changes modify cross-model aggregate handling in slayer/sql/generator.py.
  • MotleyAI/slayer#269: Both changes update ORDER BY resolution and alias handling in the planner and SQL generator.

Sequence Diagram(s)

sequenceDiagram
  participant Query
  participant StagePlanner
  participant SQLGenerator
  participant SQLResult
  Query->>StagePlanner: submit order and partition_by targets
  StagePlanner->>StagePlanner: validate and classify targets
  StagePlanner->>SQLGenerator: provide planned order references
  SQLGenerator->>SQLResult: render aliases, qualified columns, and CTE references
  SQLResult-->>Query: return ordered projection and rows
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the pull request's main changes: order-only hidden slots and plan-time validations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch egor/dev-1712-dev-1703-stage-8-hidden-slots-plan-time-validations-order

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
slayer/engine/planning.py (1)

405-431: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make rewrite_fn keyword-only.

rewrite_rank_partition_keys takes two parameters and both are passed positionally at slayer/engine/stage_planner.py line 605. The coding guidelines require keyword arguments for functions with more than one parameter. A keyword-only callback also documents intent at every recursive call site.

Also consider annotating the callback type so the contract described in the docstring is machine-checkable.

♻️ Proposed signature change
-def rewrite_rank_partition_keys(key: ValueKey, rewrite_fn) -> ValueKey:
+def rewrite_rank_partition_keys(
+    key: ValueKey, *, rewrite_fn: Callable[[TransformKey], frozenset],
+) -> ValueKey:

Every recursive call and the stage_planner._rw call site then pass rewrite_fn=....

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

In `@slayer/engine/planning.py` around lines 405 - 431, Update
rewrite_rank_partition_keys so rewrite_fn is a keyword-only parameter, and add a
callable type annotation matching its TransformKey-to-frozenset contract. Change
every recursive invocation and the stage_planner _rw call site to pass
rewrite_fn by keyword, preserving existing behavior.

Source: Coding guidelines

slayer/sql/generator.py (1)

6258-6273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Compute the trimmed-plan set once.

The trim predicate plan.hidden and not planned_query.transform_layers appears twice: at line 6140 (trim_hidden, which drops the aggregate from combined_parts) and again at line 6266 (which decides whether the ORDER BY term is CTE-qualified). The two must always agree. If they diverge, the projection drops the column while the ORDER BY still names the bare alias, and the emitted SQL is invalid.

Note also that this block runs after the return self._render_cross_model_transform_chain(...) at line 6238, so not planned_query.transform_layers is always true here.

Build one set of trimmed plan ids in the projection loop and reuse it.

♻️ Proposed refactor
+        trimmed_cma_plan_ids: Set[str] = set()
         for plan in planned_query.cross_model_aggregate_plans:
             ...
             trim_hidden = plan.hidden and not planned_query.transform_layers
+            if trim_hidden:
+                trimmed_cma_plan_ids.add(plan.aggregate_slot_id)
         hidden_cma_order_ref: Dict[str, str] = {}
         for plan in planned_query.cross_model_aggregate_plans:
-            # Only CMAs actually trimmed from the projection (hidden + no
-            # transform chain) need the CTE-qualified ORDER BY reference.
-            if not (plan.hidden and not planned_query.transform_layers):
+            # Only CMAs actually trimmed from the projection need the
+            # CTE-qualified ORDER BY reference.
+            if plan.aggregate_slot_id not in trimmed_cma_plan_ids:
                 continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/sql/generator.py` around lines 6258 - 6273, Compute a set of trimmed
cross-model aggregate plan IDs in the projection loop using the existing trim
predicate, then reuse that set when building hidden_cma_order_ref instead of
repeating the predicate. Since this block runs after the transform-chain return,
retain the projection loop’s established conditions and use the shared IDs to
CTE-qualify exactly the aggregates removed from combined_parts.
tests/test_dev1712_order_only_hidden_slots.py (1)

298-317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the error text for the joined-column rejection.

Both tests assert only the exception type. UnresolvableOrderColumnError embeds the column and the qualifier in its message, and the planner builds those two fields at slayer/engine/stage_planner.py line 677. A type-only assertion cannot detect a malformed message there — see the separate comment on that line, where the qualifier is currently duplicated into the rendered text.

Add a message assertion so the user-facing text is pinned.

💚 Proposed test tightening
-        with pytest.raises(UnresolvableOrderColumnError):
-            await _sql(engine, query)
+        with pytest.raises(UnresolvableOrderColumnError) as ei:
+            await _sql(engine, query)
+        msg = str(ei.value)
+        assert "'customers.region'" in msg, f"malformed qualifier/column: {msg}"

Apply the same change to test_joined_row_column_grouped_raises.

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

In `@tests/test_dev1712_order_only_hidden_slots.py` around lines 298 - 317, Update
both test_joined_row_column_ungrouped_raises and
test_joined_row_column_grouped_raises to assert the exact user-facing message of
UnresolvableOrderColumnError, including the joined column and qualifier, while
preserving the existing exception-type assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@slayer/engine/stage_planner.py`:
- Around line 673-677: Update the UnresolvableOrderColumnError construction in
the shown partition-key branch to pass the bare leaf column as column and the
full joined row-key path as qualifier, avoiding duplicated qualifiers in the
formatted message. Prefer deriving both values directly from the ColumnKey,
ColumnSqlKey, and TimeTruncKey structure via a small helper adjacent to
_row_key_path rather than splitting _partition_key_display output.
- Around line 580-594: Update the time-dimension lookup near `_td_by_source` to
detect source columns mapped to multiple `TimeTruncKey` granularities, storing
those columns in `_td_ambiguous_sources` while preserving unambiguous mappings.
In `_validate_partition_keys`, check ambiguous source columns before resolving
through `_td_by_source` and raise an error naming the column and competing
granularities, directing the user to specify the qualified time bucket.

In `@slayer/sql/generator.py`:
- Around line 11018-11045: Update the hidden local ColumnSqlKey handling in the
split-emission path so derived SQL expressions are expanded and their join paths
registered before generating the order expression. Ensure local expressions such
as customers.region bind the required relation instead of producing an unbound
ORDER BY reference, while preserving existing ColumnKey and TimeTruncKey
behavior. Add an ungrouped order-only regression test covering this case.

In `@tests/test_dev1712_order_only_hidden_slots.py`:
- Around line 691-697: Update
test_composite_order_string_rejected_at_construction to expect
pydantic.ValidationError instead of the broad Exception type, and add the
ValidationError import with the module’s existing top-level imports. Keep the
test’s current construction and boundary assertion unchanged.

---

Nitpick comments:
In `@slayer/engine/planning.py`:
- Around line 405-431: Update rewrite_rank_partition_keys so rewrite_fn is a
keyword-only parameter, and add a callable type annotation matching its
TransformKey-to-frozenset contract. Change every recursive invocation and the
stage_planner _rw call site to pass rewrite_fn by keyword, preserving existing
behavior.

In `@slayer/sql/generator.py`:
- Around line 6258-6273: Compute a set of trimmed cross-model aggregate plan IDs
in the projection loop using the existing trim predicate, then reuse that set
when building hidden_cma_order_ref instead of repeating the predicate. Since
this block runs after the transform-chain return, retain the projection loop’s
established conditions and use the shared IDs to CTE-qualify exactly the
aggregates removed from combined_parts.

In `@tests/test_dev1712_order_only_hidden_slots.py`:
- Around line 298-317: Update both test_joined_row_column_ungrouped_raises and
test_joined_row_column_grouped_raises to assert the exact user-facing message of
UnresolvableOrderColumnError, including the joined column and qualifier, while
preserving the existing exception-type assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 402dea2a-27a0-4970-b673-bff8d384a3f2

📥 Commits

Reviewing files that changed from the base of the PR and between 385d377 and f5e588b.

📒 Files selected for processing (12)
  • .claude/skills/slayer-query.md
  • DECISIONS.md
  • docs/concepts/formulas.md
  • docs/concepts/queries.md
  • slayer/engine/planning.py
  • slayer/engine/stage_planner.py
  • slayer/sql/generator.py
  • tests/parity_xfails.py
  • tests/test_dev1712_order_only_hidden_slots.py
  • tests/test_nested_dag_cross_stage_refs.py
  • tests/test_projection_trim.py
  • tests/test_sql_generator.py
💤 Files with no reviewable changes (1)
  • tests/test_projection_trim.py

Comment on lines +580 to +594
_td_by_source = {
dm.bound.value_key.column: dm.bound.value_key
for dm in _td_dms
if isinstance(dm.bound.value_key, TimeTruncKey)
}
_td_key_set = set(_td_by_source.values())
_available_dims = [dm.declared_name for dm in (*_dim_dms, *_td_dms)]

def _validate_partition_keys(tk: TransformKey) -> frozenset:
new_pks = []
for pk in tk.partition_keys:
if pk in _dim_key_set or pk in _td_key_set:
new_pks.append(pk) # already a query dim / td bucket
elif pk in _td_by_source:
new_pks.append(_td_by_source[pk]) # td source col -> bucket

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Two time dimensions over one source column collapse in _td_by_source.

_td_by_source is keyed by the TimeTruncKey.column. TimeTruncKey treats the same column at different granularities as distinct slots (see the class docstring in slayer/core/keys.py). A query with time_dimensions=[{created_at, month}, {created_at, day}] therefore produces two entries with the same key, and the later one overwrites the earlier one.

rank(amount:sum, partition_by=created_at) then partitions by whichever bucket happens to be last in declared_measures order. The choice is silent and order-dependent.

Reject the ambiguous case instead, and point the user at the qualified bucket they mean.

🛡️ Proposed guard
-    _td_by_source = {
-        dm.bound.value_key.column: dm.bound.value_key
-        for dm in _td_dms
-        if isinstance(dm.bound.value_key, TimeTruncKey)
-    }
+    _td_by_source: dict = {}
+    _td_ambiguous_sources: set = set()
+    for dm in _td_dms:
+        vk = dm.bound.value_key
+        if not isinstance(vk, TimeTruncKey):
+            continue
+        if vk.column in _td_by_source:
+            _td_ambiguous_sources.add(vk.column)
+        _td_by_source[vk.column] = vk

Then in _validate_partition_keys, raise when pk in _td_ambiguous_sources with a message that names the column and the competing granularities.

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

In `@slayer/engine/stage_planner.py` around lines 580 - 594, Update the
time-dimension lookup near `_td_by_source` to detect source columns mapped to
multiple `TimeTruncKey` granularities, storing those columns in
`_td_ambiguous_sources` while preserving unambiguous mappings. In
`_validate_partition_keys`, check ambiguous source columns before resolving
through `_td_by_source` and raise an error naming the column and competing
granularities, directing the user to specify the qualified time bucket.

Comment on lines +673 to +677
if isinstance(okey, (ColumnKey, ColumnSqlKey, TimeTruncKey)):
disp = _partition_key_display(okey)
path = _row_key_path(okey)
if path:
raise UnresolvableOrderColumnError(column=disp, qualifier=path[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The UnresolvableOrderColumnError message repeats the join qualifier.

UnresolvableOrderColumnError.__init__ formats the message as f"ORDER BY column '{qualifier}.{column}' ..." (see slayer/core/errors.py lines 516-519). It expects column to be the bare leaf. Here disp is the full dotted path from _partition_key_display, and qualifier is path[0].

For order=[{"column": "customers.region"}] the message reads ORDER BY column 'customers.customers.region' cannot be resolved. For a multi-hop path it reads 'customers.customers.regions.name'.

Pass the leaf as column and the joined path as qualifier.

🐛 Proposed fix
         if isinstance(okey, (ColumnKey, ColumnSqlKey, TimeTruncKey)):
             disp = _partition_key_display(okey)
             path = _row_key_path(okey)
             if path:
-                raise UnresolvableOrderColumnError(column=disp, qualifier=path[0])
+                leaf = disp.split(".")[-1]
+                raise UnresolvableOrderColumnError(
+                    column=leaf, qualifier=".".join(path),
+                )

A cleaner variant adds a small helper next to _row_key_path that returns (path, leaf) for the three row key shapes, so the leaf is read from the key instead of re-split from the display string.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if isinstance(okey, (ColumnKey, ColumnSqlKey, TimeTruncKey)):
disp = _partition_key_display(okey)
path = _row_key_path(okey)
if path:
raise UnresolvableOrderColumnError(column=disp, qualifier=path[0])
if isinstance(okey, (ColumnKey, ColumnSqlKey, TimeTruncKey)):
disp = _partition_key_display(okey)
path = _row_key_path(okey)
if path:
leaf = disp.split(".")[-1]
raise UnresolvableOrderColumnError(
column=leaf, qualifier=".".join(path),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/engine/stage_planner.py` around lines 673 - 677, Update the
UnresolvableOrderColumnError construction in the shown partition-key branch to
pass the bare leaf column as column and the full joined row-key path as
qualifier, avoiding duplicated qualifiers in the formatted message. Prefer
deriving both values directly from the ColumnKey, ColumnSqlKey, and TimeTruncKey
structure via a small helper adjacent to _row_key_path rather than splitting
_partition_key_display output.

Comment thread slayer/sql/generator.py
Comment on lines +11018 to +11045
# DEV-1712 (Law 2, split emission): a hidden LOCAL ROW column
# ordered in an UNGROUPED query. The plan-time order validation
# (``plan_query``) guarantees the only hidden ROW slot that
# reaches here is a local (empty-path) column in a query with no
# GROUP BY — grouped row columns and joined columns are rejected
# up front, aggregates take the branch above. Emit a SPLIT
# ``<relation>.<column>`` reference (mixed-case-aware) against
# the base FROM scope, identical to how the column would render
# if it were a projected dimension.
key = slot.key
if source_model is not None and isinstance(
key, (ColumnKey, ColumnSqlKey, TimeTruncKey)
):
row_key = key.column if isinstance(key, TimeTruncKey) else key
if isinstance(row_key, ColumnKey):
path, leaf = row_key.path, row_key.leaf
else: # ColumnSqlKey
path, leaf = row_key.path, row_key.column_name
if not path:
order_col = self._joined_or_local_dim_expr(
path=(), leaf=leaf, source_model=source_model,
source_relation=source_relation, bundle=bundle,
)
ascending = order_entry.direction == "asc"
select = select.order_by(
self._ordered(order_col, ascending=ascending),
)
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find order-only tests that use a derived column whose Column.sql crosses a join.
set -euo pipefail

# 1. Locate the Stage 8 order tests and the sibling projection-side expansion helper.
fd -t f 'test_dev1712_order_only_hidden_slots.py' tests

# 2. Look for order items over derived columns in the order-only suites.
rg -n -C 5 'distinct_dimension_values=False' tests/test_dev1712_order_only_hidden_slots.py

# 3. Show every model fixture column that sets a dotted Column.sql (a crossing derived column).
rg -nP -C 3 'Column\(\s*name=[^)]*sql="[A-Za-z_]+\.[A-Za-z_]+"' tests

# 4. Confirm the projection path expands derived dims while the order path does not.
ast-grep run --pattern $'def _dim_column_expr_from_planned($$$) {
  $$$
}' --lang python slayer/sql/generator.py || \
  rg -n -A 15 'def _dim_column_expr_from_planned' slayer/sql/generator.py

Repository: MotleyAI/slayer

Length of output: 36502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- generator order branch ---'
sed -n '10940,11070p' slayer/sql/generator.py

printf '%s\n' '--- derived-dimension and planned-expression helpers ---'
sed -n '9480,9655p' slayer/sql/generator.py

printf '%s\n' '--- order classification and row-key path helpers ---'
rg -n -C 12 '_row_key_path|ColumnSqlKey|_expand_derived_row_dims|plan_query|hidden.*ROW|LOCAL ROW' slayer/sql/generator.py

printf '%s\n' '--- relevant order-only tests and fixtures ---'
sed -n '1,380p' tests/test_dev1712_order_only_hidden_slots.py

printf '%s\n' '--- related derived-order tests ---'
rg -n -C 12 'order=.*(derived|cust_|region_|ColumnRef)|OrderItem\(column=.*(derived|cust_|region_)|ColumnSqlKey' tests

Repository: MotleyAI/slayer

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

path = Path("slayer/sql/generator.py")
lines = path.read_text().splitlines()

names = {
    "_dim_column_expr_from_planned",
    "_joined_or_local_dim_expr",
    "_expand_derived_row_dims",
    "_row_key_path",
    "plan_query",
}
for name in names:
    hits = [i for i, line in enumerate(lines) if re.search(rf"^\s*(async\s+)?def\s+{re.escape(name)}\b", line)]
    for start in hits:
        indent = len(lines[start]) - len(lines[start].lstrip())
        end = start + 1
        while end < len(lines):
            text = lines[end]
            stripped = text.lstrip()
            current_indent = len(text) - len(stripped)
            if stripped and current_indent <= indent and re.match(r"^(async\s+)?def\s+", stripped):
                break
            end += 1
        print(f"\n--- {name}: lines {start + 1}-{end} ---")
        print("\n".join(f"{i + 1:5}: {lines[i]}" for i in range(start, min(end, start + 180))))
PY

printf '%s\n' '--- exact order-emission context ---'
sed -n '10980,11065p' slayer/sql/generator.py

printf '%s\n' '--- targeted order/derived references in tests ---'
rg -n -C 8 \
  'ColumnSqlKey|derived.*order|order.*derived|cust_signup|region_payment|customer_score|deep_pop|OrderItem\(column=ColumnRef\(name="[^"]+"\)' \
  tests/test_dev1712_order_only_hidden_slots.py \
  tests/test_sql_generator.py \
  tests/test_dev1476_first_last_explicit_time.py \
  tests/test_dev1474_time_shift_cross_model_partition.py

Repository: MotleyAI/slayer

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- generator symbols ---'
rg -n \
  '_row_key_path|_order|UnresolvableOrderColumnError|ColumnSqlKey|hidden|GROUP BY|group_by' \
  slayer/sql/generator.py | head -n 240

printf '%s\n' '--- planner symbols across source ---'
rg -n \
  '_row_key_path|UnresolvableOrderColumnError|ColumnSqlKey|order.*path|path.*order|hidden.*ROW|ROW.*hidden' \
  slayer --glob '*.py' | head -n 320

printf '%s\n' '--- order-only fixture definitions ---'
sed -n '1,205p' tests/test_dev1712_order_only_hidden_slots.py

printf '%s\n' '--- order-only tests after joined-column cases ---'
sed -n '295,390p' tests/test_dev1712_order_only_hidden_slots.py

printf '%s\n' '--- all direct order references to derived fixture names ---'
rg -n -C 6 \
  'order=\[OrderItem\(column=ColumnRef\(name="(cust_signup|customer_score|deep_pop|region_payment|cust_balance|region_weight|li_weight|b_display|net_amount|signup_at_alias)' \
  tests --glob '*.py'

Repository: MotleyAI/slayer

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- stage-planner row-path and order validation ---'
sed -n '150,215p' slayer/engine/stage_planner.py
sed -n '620,705p' slayer/engine/stage_planner.py

printf '%s\n' '--- binding of local and joined derived columns ---'
sed -n '500,625p' slayer/engine/binding.py
sed -n '680,715p' slayer/engine/binding.py

printf '%s\n' '--- base-select scope registration and derived expansion order ---'
sed -n '4200,4420p' slayer/sql/generator.py

printf '%s\n' '--- narrow test-name inventory for order and derived columns ---'
rg -n '^(\s+async\s+def|\s+def)\s+.*(order|derived|ColumnSqlKey|source_sql|cust_signup|deep_pop|region_payment)' \
  tests/test_dev1712_order_only_hidden_slots.py \
  tests/test_sql_generator.py \
  tests/test_dev1476_first_last_explicit_time.py \
  tests/test_dev1708_stage4_cte_scope.py

Repository: MotleyAI/slayer

Length of output: 46876


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- construction of base_render_order and hidden order slots ---'
sed -n '3235,3385p' slayer/sql/generator.py
sed -n '3790,4005p' slayer/sql/generator.py

printf '%s\n' '--- current hidden-row regression coverage ---'
sed -n '7145,7195p' tests/test_sql_generator.py
sed -n '200,325p' tests/test_dev1712_order_only_hidden_slots.py

printf '%s\n' '--- derived expansion and join collection contracts ---'
sed -n '7750,7845p' slayer/sql/generator.py
sed -n '9425,9520p' slayer/sql/generator.py

printf '%s\n' '--- static behavioral verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

stage = Path("slayer/engine/stage_planner.py").read_text()
binding = Path("slayer/engine/binding.py").read_text()
generator = Path("slayer/sql/generator.py").read_text()
tests = Path("tests/test_dev1712_order_only_hidden_slots.py").read_text()

checks = {
    "local derived refs become ColumnSqlKey(path=())":
        "return ColumnSqlKey(path=(), model=model.name, column_name=col.name)" in binding,
    "row-path helper returns the key path":
        'return tuple(getattr(key, "path", ()))' in stage,
    "empty derived path is accepted by order validation":
        "if path:\n                raise UnresolvableOrderColumnError" in stage
        and "continue  # ungrouped local row column -> split emission" in stage,
    "derived expansion registers SQL-crossed joins":
        "_expand_derived_column_sql(" in generator
        and "_joined_paths_in_sql(" in generator
        and "scope.join_paths.add(path)" in generator,
    "order split uses direct planned resolver":
        "return self._dim_column_expr_from_planned(" in generator
        and "sql=col.sql, name=col.name, model_name=source_relation" in generator,
    "order-only test file has no derived-column order target":
        not bool(re.search(
            r'order=\[OrderItem\(column=ColumnRef\(name="(?:[A-Za-z_]+)"',
            tests
        )) or "Column(name=" not in tests,
}

for name, result in checks.items():
    print(f"{'PASS' if result else 'FAIL'}: {name}")
PY

Repository: MotleyAI/slayer

Length of output: 34113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

source = Path("tests/test_dev1712_order_only_hidden_slots.py").read_text()
tree = ast.parse(source)
order_refs = []

for node in ast.walk(tree):
    if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
        if node.func.id != "OrderItem":
            continue
        for kw in node.keywords:
            if kw.arg != "column":
                continue
            value = kw.value
            if (
                isinstance(value, ast.Call)
                and isinstance(value.func, ast.Name)
                and value.func.id == "ColumnRef"
                and value.args
                and isinstance(value.args[0], ast.Constant)
            ):
                order_refs.append(value.args[0].value)

print("order-only ColumnRef targets:", order_refs)
print(
    "derived order regression present:",
    any(name in {"cust_signup", "customer_score", "deep_pop", "region_payment"}
        for name in order_refs),
)
PY

poetry run ruff check slayer/ tests/

Repository: MotleyAI/slayer

Length of output: 271


Expand joins for hidden local ColumnSqlKey order targets.

A local derived column such as sql="customers.region" has path=() and passes plan validation, but hidden row targets are excluded from base_render_order. The split-emission path then calls _resolve_sql without registering customers, producing an unbound ORDER BY reference. Expand the derived expression and register its join paths before emission. Add an ungrouped order-only regression test.

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

In `@slayer/sql/generator.py` around lines 11018 - 11045, Update the hidden local
ColumnSqlKey handling in the split-emission path so derived SQL expressions are
expanded and their join paths registered before generating the order expression.
Ensure local expressions such as customers.region bind the required relation
instead of producing an unbound ORDER BY reference, while preserving existing
ColumnKey and TimeTruncKey behavior. Add an ungrouped order-only regression test
covering this case.

Comment thread tests/test_dev1712_order_only_hidden_slots.py
…t cleanups

Codex (correctness): a joined ORDER BY ref whose leaf collides with a local
declared dimension/measure (order=owners.status when `status` is a local dim)
silently bound to the local column via the bare-leaf shortcut in the
order-binding loop, sorting by the wrong field. Guard the bare-name shortcuts
(stage_planner.plan_query) so they apply only to unqualified refs or refs
qualified with the host model; a foreign-qualified ref falls through to the
dotted/bind_expr path, where a truly-joined order ref is then rejected by the
plan-time order validation. Regression test added.

Sonar S3776 (planning.py): flattened rewrite_rank_partition_keys (cognitive
complexity 26 -> under threshold) using a local recursion helper + ternary
rebuilds; also made rewrite_fn keyword-only + type-annotated (CodeRabbit).
Sonar S5685 (stage_planner.py): replaced the walrus-in-argument-list in the
bound_filters rebuild with an explicit loop.
Sonar S9073 (tests): split composite `assert a and b` into separate asserts.
Sonar S5958 (tests): pytest.raises(Exception) -> pytest.raises(ValidationError).

Full non-integration suite green (8971 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant