Skip to content

DEV-1705 Stage 1: assert_scope_closed validator + carrier×scope acceptance matrix - #264

Merged
ZmeiGorynych merged 3 commits into
egor/dev-1703-comprehensive-approach-expressions-crossing-joins-on-thefrom
egor/dev-1705-dev-1703-stage-1-assert_scope_closed-validator-carrier×scope
Aug 1, 2026

Hidden character warning

The head ref may contain hidden characters: "egor/dev-1705-dev-1703-stage-1-assert_scope_closed-validator-carrier\u00d7scope"
Merged

DEV-1705 Stage 1: assert_scope_closed validator + carrier×scope acceptance matrix#264
ZmeiGorynych merged 3 commits into
egor/dev-1703-comprehensive-approach-expressions-crossing-joins-on-thefrom
egor/dev-1705-dev-1703-stage-1-assert_scope_closed-validator-carrier×scope

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented Aug 1, 2026

Copy link
Copy Markdown
Member

DEV-1703 Stage 1 (DEV-1705). Test-and-validator only — no generator behavior change beyond one env-gated hook.

What lands

Validator — slayer/sql/scope_check.py
assert_scope_closed(sql) walks every sqlglot scope and flags a provable out-of-scope reference:

  • C1 — a table qualifier not bound in that scope's FROM/JOINs.
  • C2 — a cross-scope reference naming a column an inner scope doesn't project (plain/REPLACE star exports every name; * EXCEPT (c) drops c).

Sound-on-corpus (zero false positives): unqualified/ambiguous refs and physical-table column names are unverifiable and never flagged. Pre-RLS by default; allow_rls_correlation=True whitelists the session-policy _rls_src correlated EXISTS. Returns structured ScopeCheckResult / raises ScopeLeakError.

Harness wiring — env-gated maybe_validate_scopes at the generator's post-mangle, pre-RLS terminals; conftest.py sets SLAYER_VALIDATE_SCOPES=1 suite-wide, so a scope leak fails at generation time. Post-mangle (not pre-) because BigQuery/T-SQL dotted aliases become unambiguous ___ names there — pre-mangle dotted refs parse as table.column (false leaks) and trigger BigQuery's TypeError; residual BigQuery parse TypeError is a bounded, reported skip owned by Stage 9 (DEV-1713).

Carrier×scope acceptance matrix — tests/test_carrier_scope_matrix.py
Layer-1 green scope-closure invariant sweep (each cell asserts the scope shape was exercised, then closure) + Layer-2 strict-xfail defect pins (Stages 4/7/10) + F1/F4 semantic pins (SQL-shape) + RLS×isolation-CTE cells.

Harvest from the abandoned point-fix worktrees, each pinned to its owning stage: DEV-1526 (Stage 4), DEV-1531 SQL-shape + DuckDB executed values (Stage 5), DEV-1496 raise-don't-degrade guards (Stage 10), reconstructed DEV-1474 (Stage 7); DEV-1527 covered by the existing placeholder. F1/F4 executed-value pins are green. Manifest in the matrix docstring.

Verification

  • Full non-integration suite: 7956 passed, 0 failed, 127 xfailed, 0 unexpected xpass.
  • Validator units 42/42; matrix 17 passed + 3 xfailed; DuckDB integration 38 passed + 6 xfailed; ruff clean.

Notes

  • Fixed one genuinely-degenerate pre-existing test (__-path reference-semantics fixture had no join target → emitted unbound SQL the validator correctly flags; gave it a real join chain).
  • DECISIONS.md + docs/development.md updated.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional SQL scope validation to detect invalid table and column references before execution.
    • Added clear scope-leak errors with support for dialect-specific SQL, projections, unions, and RLS correlation handling.
    • Added environment-based runtime enablement with SLAYER_VALIDATE_SCOPES=1.
  • Bug Fixes

    • Improved cross-model aggregate handling, including first and last values and filter behavior.
  • Documentation

    • Documented scope validation, runtime configuration, errors, and limitations.
  • Tests

    • Expanded coverage across SQL dialects, joins, time shifts, projections, and aggregate scenarios.

ZmeiGorynych and others added 2 commits August 1, 2026 15:17
Add slayer/sql/scope_check.py — a mechanical scope-closure validator that flags
provable out-of-scope references (C1 unbound table qualifier, C2 unprojected
inner-scope column) by walking sqlglot scopes. Sound-on-corpus (no false
positives): unqualified refs and physical-table column names are unverifiable;
plain/REPLACE stars export every name, `* EXCEPT (c)` drops c. Pre-RLS by
default, with an allow_rls_correlation allowlist for the session-policy
`_rls_src` correlated EXISTS.

Wire it env-gated (SLAYER_VALIDATE_SCOPES) at the generator's post-mangle,
pre-RLS terminals; the test conftest enables it suite-wide so a scope leak
fails at generation time. Post-mangle (not pre-mangle) because BigQuery/T-SQL
dotted aliases are mangled to `___` there — pre-mangle dotted refs parse as
table.column (false leaks) and trigger BigQuery's TypeError.

Add the carrier x scope acceptance matrix (Layer-1 green scope-closure sweep +
Layer-2 strict-xfail defect pins for Stages 4/7/10, F1/F4 semantic pins, and
RLS x isolation-CTE cells) and harvest tests from the abandoned point-fix
worktrees (DEV-1526/1527/1531/1474/1496), each pinned to its owning stage.

Also: give the DEV-1502 `__`-path reference-semantics test a real join chain
(its minimal no-join fixture emitted genuinely-unbound SQL the validator
correctly flags).

Test-and-validator only; no generator behavior change beyond the env-gated hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oach-expressions-crossing-joins-on-the' into egor/dev-1705-dev-1703-stage-1-assert_scope_closed-validator-carrier×scope
@linear

linear Bot commented Aug 1, 2026

Copy link
Copy Markdown

DEV-1705

DEV-1703

@coderabbitai

coderabbitai Bot commented Aug 1, 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: 06bae983-3924-4176-8e81-657288abc4ee

📥 Commits

Reviewing files that changed from the base of the PR and between 7d672b9 and cb2cc87.

📒 Files selected for processing (3)
  • slayer/sql/scope_check.py
  • tests/conftest.py
  • tests/test_sql_generator.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/conftest.py
  • tests/test_sql_generator.py
  • slayer/sql/scope_check.py

📝 Walkthrough

Walkthrough

The PR adds dialect-aware SQL scope validation, integrates it into SQL generation, enables it across tests, and adds coverage for scope closure, RLS correlation, cross-model aggregates, derived first/last measures, and windowed-measure guards.

Changes

Scope validation

Layer / File(s) Summary
Scope checker implementation
slayer/sql/scope_check.py
Adds structured leak results, scope traversal, binding checks, projection analysis, RLS exceptions, and BigQuery parse handling.
Generator activation and validation matrix
slayer/sql/generator.py, tests/conftest.py, tests/test_scope_check.py, tests/test_carrier_scope_matrix.py, docs/development.md, DECISIONS.md
Validates final dialect-mangled SQL before downstream RLS processing and adds suite-wide, documented validation coverage.

Aggregate SQL coverage

Layer / File(s) Summary
Aggregate SQL generation coverage
tests/test_sql_generator.py, tests/test_reference_semantics.py
Adds coverage for cross-model join inference, derived first/last values, join-path aliases, and unsupported windowed measures.
DuckDB aggregate execution coverage
tests/integration/test_integration_duckdb.py
Adds fixtures and execution tests for cross-model aggregates, filters, HAVING clauses, empty host grains, and broadcast scalar results.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • MotleyAI/slayer#257: Both modify the final SQL generation pipeline in slayer/sql/generator.py.
  • MotleyAI/slayer#260: Both modify SQL scope handling around RLS-correlated references and validation sequencing.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.96% 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 identifies the scope validator and carrier-by-scope acceptance matrix, which are the primary changes in the pull request.
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-1705-dev-1703-stage-1-assert_scope_closed-validator-carrier×scope

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: 1

🧹 Nitpick comments (6)
tests/test_sql_generator.py (4)

3526-3544: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate this test with the existing DEV-1531 xfail.

test_local_last_with_path_aliased_derived_source_xfail at lines 3897-3939 already pins the same defect with the same region_payment column, the same region_payment:last(orders.created_at) measure, and a strict xfail. Two strict xfails on one defect must both flip at Stage 5, so both need maintenance and both can block promotion.

Keep the stronger assertion set (this new test) and remove the older one, or reduce the older one to a comment reference.

🤖 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_sql_generator.py` around lines 3526 - 3544, Consolidate the
duplicate DEV-1531 coverage by keeping the stronger
test_local_last_with_path_aliased_derived_source assertions and removing the
existing test_local_last_with_path_aliased_derived_source_xfail test. If
retaining the older test location, replace its implementation with a comment
reference so only one strict xfail remains for this defect.

3516-3519: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Split the composite assertion for clearer failure output.

The assertion combines two independent conditions. Split it so the failure message identifies which condition failed.

♻️ Proposed split
-        first_subquery = norm.find("FROM (")
-        assert first_subquery != -1 and first_proj.start() > first_subquery, (
-            f"DEV-1531: `_val` projection of `{ref}` is not inside the ranked "
-            f"subquery:\n{norm}"
-        )
+        first_subquery = norm.find("FROM (")
+        assert first_subquery != -1, (
+            f"DEV-1531: no ranked subquery (`FROM (`) found:\n{norm}"
+        )
+        assert first_proj.start() > first_subquery, (
+            f"DEV-1531: `_val` projection of `{ref}` is not inside the ranked "
+            f"subquery:\n{norm}"
+        )
🤖 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_sql_generator.py` around lines 3516 - 3519, Split the composite
assertion in the ranked-subquery validation into separate assertions: one
verifying first_subquery is found and another verifying first_proj.start() is
after first_subquery. Give each assertion a failure message that clearly
identifies its specific failed condition while preserving the existing
diagnostic context.

Source: Linters/SAST tools


3502-3524: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make _assert_ref_only_in_val tolerate the CAST-wrapped projection shape.

The helper matches only the bare form <ref> AS _val_<n>. test_same_sql_different_type_no_bad_dedupe at lines 3620-3645 expects the materialised projection to be CAST-wrapped (CAST(... AS DOUBLE PRECISION) AS _val_0). If the Stage 5 fix emits the CAST form, the helper's strip at line 3520 leaves ref in stripped and the assertion at line 3521 fails even though the SQL is correct. That converts an auto-promotion into a false failure.

Also, line 3614 calls .group(1) on an unguarded _re.search result. After promotion, a shape mismatch raises AttributeError instead of a readable assertion.

♻️ Suggested helper adjustment
-        norm = _norm(sql)
-        first_proj = _re.search(rf"{_re.escape(ref)} AS _val_\d+", norm)
+        norm = _norm(sql)
+        # The materialised projection may be CAST-wrapped, so allow an
+        # optional trailing `AS <type>)` between the ref and the alias.
+        val_proj_re = rf"{_re.escape(ref)}[^,]*? AS _val_\d+"
+        first_proj = _re.search(val_proj_re, norm)
@@
-        stripped = _re.sub(rf"{_re.escape(ref)} AS _val_\d+", "", norm)
+        stripped = _re.sub(val_proj_re, "", norm)
🤖 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_sql_generator.py` around lines 3502 - 3524, Update
_assert_ref_only_in_val to recognize and strip both bare and CAST-wrapped
materialized projections, including the CAST form around ref before AS _val_<n>,
while preserving its ranked-subquery and leakage assertions. In
test_same_sql_different_type_no_bad_dedupe, guard the _re.search result before
calling group(1) so shape mismatches produce a clear assertion failure rather
than AttributeError.

11685-11693: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a pre-assertion so each guard xfail fails for the intended reason.

Every test in this class puts only the generation call inside pytest.raises. A strict xfail reports xfail for any failure in the test body, including an unrelated ValueError from SlayerQuery(...) construction or a storage error. The pins therefore cannot distinguish "the guard is missing" from "the query shape is rejected earlier".

Assert that generation currently succeeds before the pytest.raises block, or record the today-behaviour SQL, so the xfail reason stays accurate. This applies to all eight tests in TestWindowedMeasureGuards.

🤖 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_sql_generator.py` around lines 11685 - 11693, Update all eight
tests in TestWindowedMeasureGuards to validate the query setup and generation
path before the pytest.raises assertion, ensuring SlayerQuery construction and
_engine_generate succeed for the intended input. Keep the existing expected
ValueError assertion for the guard failure, so strict xfail only reflects the
missing windowed-measure validation rather than unrelated setup or storage
errors.
tests/integration/test_integration_duckdb.py (2)

959-965: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an executed-value F1 pin, or narrow this header comment.

The header states that this block pins F1 multiply-per-match semantics. No test in TestF1F4SemanticValues asserts an F1 multiplied value. The fixture creates two paid orders for customer 1 precisely to expose that shape, and the F1 pin at lines 1060-1065 defers to the SQL-shape test in tests/test_carrier_scope_matrix.py.

Add a test that groups by a dimension sharing grain with the join so the 1:N multiplication is visible in the returned value, or remove the F1 line from this comment.

Do you want me to draft the F1 executed-value test against this fixture?

🤖 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/integration/test_integration_duckdb.py` around lines 959 - 965, Add an
executed-value F1 test in TestF1F4SemanticValues using the existing fixture,
grouping by a dimension at the join’s grain and asserting the returned aggregate
reflects multiply-per-match semantics for customer 1’s two paid orders. Keep the
existing F4 coverage unchanged; only remove the F1 header claim if such a value
assertion cannot be added.

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

Function-local ModelJoin imports in both new DuckDB fixtures. The module does not import ModelJoin at the top, so each new fixture re-imports it inside its body. Add ModelJoin to the existing module-level slayer.core.models import and remove both local imports.

  • tests/integration/test_integration_duckdb.py#L826-L826: remove the local from slayer.core.models import ModelJoin in _dev1531_duckdb_storage.
  • tests/integration/test_integration_duckdb.py#L971-L971: remove the local from slayer.core.models import ModelJoin in _f1f4_duckdb_storage.

Based on coding guidelines: "Use keyword arguments for functions with more than one parameter, and keep imports at the top of files."

🤖 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/integration/test_integration_duckdb.py` at line 826, Move ModelJoin
into the existing module-level slayer.core.models import, then remove the
function-local imports from _dev1531_duckdb_storage and _f1f4_duckdb_storage in
tests/integration/test_integration_duckdb.py at lines 826-826 and 971-971.

Source: Coding guidelines

🤖 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/sql/scope_check.py`:
- Around line 24-27: Update the docstrings describing maybe_validate_scopes to
reflect post-mangle, pre-RLS validation: in slayer/sql/scope_check.py lines
24-27, replace the pre-mangle wording and note that dialect alias mangling runs
first; in tests/conftest.py lines 83-97, change the generator hook description
to post-mangle, pre-RLS. No code behavior changes are needed.

---

Nitpick comments:
In `@tests/integration/test_integration_duckdb.py`:
- Around line 959-965: Add an executed-value F1 test in TestF1F4SemanticValues
using the existing fixture, grouping by a dimension at the join’s grain and
asserting the returned aggregate reflects multiply-per-match semantics for
customer 1’s two paid orders. Keep the existing F4 coverage unchanged; only
remove the F1 header claim if such a value assertion cannot be added.
- Line 826: Move ModelJoin into the existing module-level slayer.core.models
import, then remove the function-local imports from _dev1531_duckdb_storage and
_f1f4_duckdb_storage in tests/integration/test_integration_duckdb.py at lines
826-826 and 971-971.

In `@tests/test_sql_generator.py`:
- Around line 3526-3544: Consolidate the duplicate DEV-1531 coverage by keeping
the stronger test_local_last_with_path_aliased_derived_source assertions and
removing the existing test_local_last_with_path_aliased_derived_source_xfail
test. If retaining the older test location, replace its implementation with a
comment reference so only one strict xfail remains for this defect.
- Around line 3516-3519: Split the composite assertion in the ranked-subquery
validation into separate assertions: one verifying first_subquery is found and
another verifying first_proj.start() is after first_subquery. Give each
assertion a failure message that clearly identifies its specific failed
condition while preserving the existing diagnostic context.
- Around line 3502-3524: Update _assert_ref_only_in_val to recognize and strip
both bare and CAST-wrapped materialized projections, including the CAST form
around ref before AS _val_<n>, while preserving its ranked-subquery and leakage
assertions. In test_same_sql_different_type_no_bad_dedupe, guard the _re.search
result before calling group(1) so shape mismatches produce a clear assertion
failure rather than AttributeError.
- Around line 11685-11693: Update all eight tests in TestWindowedMeasureGuards
to validate the query setup and generation path before the pytest.raises
assertion, ensuring SlayerQuery construction and _engine_generate succeed for
the intended input. Keep the existing expected ValueError assertion for the
guard failure, so strict xfail only reflects the missing windowed-measure
validation rather than unrelated setup or storage errors.
🪄 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: 6d195e4d-66d0-4caf-a075-8d20637137b0

📥 Commits

Reviewing files that changed from the base of the PR and between ac69ccc and 7d672b9.

📒 Files selected for processing (10)
  • DECISIONS.md
  • docs/development.md
  • slayer/sql/generator.py
  • slayer/sql/scope_check.py
  • tests/conftest.py
  • tests/integration/test_integration_duckdb.py
  • tests/test_carrier_scope_matrix.py
  • tests/test_reference_semantics.py
  • tests/test_scope_check.py
  • tests/test_sql_generator.py

Comment thread slayer/sql/scope_check.py Outdated
- Correct the stale "pre-mangle" wording to post-mangle, pre-RLS in the
  scope_check module docstring, the TypeError carve-out comment, and the
  conftest fixture docstring (the hook validates after rewrite_emitted_sql).
- Consolidate the duplicate DEV-1531 local first/last xfail: the pre-existing
  `test_local_last_with_path_aliased_derived_source_xfail` placeholder is
  subsumed by the harvested `test_local_last_with_path_aliased_derived_source`
  (stronger `_assert_ref_only_in_val` assertion); left a comment pointer so a
  single strict-xfail flips at Stage 5.

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

sonarqubecloud Bot commented Aug 1, 2026

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant