Skip to content

feat: check enum literals at compile time and fix rule helper and slot checks - #1258

Merged
kvlonge merged 5 commits into
mainfrom
fix/bug-batch-3
Oct 26, 2026
Merged

kvlonge merged 5 commits into
mainfrom
fix/bug-batch-3

Conversation

@kvlonge

@kvlonge kvlonge commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Why

  • evaluate_rule copied only the rule's own file, so a rule importing a shared helper under rules/ could not be tested.
  • An enum-typed column filled by a string literal outside the enum compiled cleanly and only failed in the generated accepted_values audit after the build.
  • Two relation-slot checks had gaps: LATERAL subqueries escaped the AS rule, and P018 missed a relation-qualified read such as @weekly.region beside a group holding the same column.

Changes

  • Rule harness: when the rule file belongs to a project's rules/ directory, the harness copies that directory (symlinks kept, caches and dot-directories skipped) and writes the rule under test last. The testing docs cover the pythonpath setting --import-mode=importlib needs.
  • New K013 contract error: for an enforced contract, every constant string literal that fills an enum-typed column must be a member of the enum. This covers a literal written directly, one in any UNION branch, and one passed through a CTE or derived table. The error names the value, model and enum, and points at the projected literal. To avoid errors where the audit would pass, passthroughs through a filter, join or row limit, EXCEPT and by-name positions are not followed, and SQL Server is skipped. Computed values stay with the audit.
  • ctx.columns.enum_types(model) maps each enum-typed column to its enum for Rules. Declared column types are unchanged. The Rules cache tracks it.
  • The output-select walk now treats LATERAL items like derived tables. Undeclared relation reads count as output columns in the P018 clash check.
  • Regression test for forwarded aliased slot reads the outer macro does not declare.

Verification

  • Rust unit tests for the contract and macro changes.
  • DuckDB integration tests for K013, rule tests under both pytest import modes, and the Rules fact digests and payload.
  • CLI E2E tests for the slot checks.
  • make test and make check-ci pass.

evaluate_rule copied only the rule's own file into its temporary
project, so a rule importing a helper under rules/ failed to load in
its tests. When the rule file belongs to a project's rules/ directory,
the harness now copies that directory, as sqb compile sees it, keeping
symlinks as links and leaving out bytecode and dot-directories such as
tool caches. The rule under test is still written last, so a RuleCase
file cannot replace it.

An integration test runs a project's rule tests in a pytest subprocess
under the prepend and importlib import modes, and the testing docs
describe the pytest configuration importlib needs.
…compile time

An enum-typed column of an enforced contract was only checked by its
generated accepted_values audit, so a union branch filling it with a
string that is not an enum member compiled cleanly and failed after the
build. sqb compile now reports K013 for each constant string literal
that fills such a column, written directly, in any UNION branch, or
passed through a CTE or derived table, naming the value, the model and
the enum and pointing at the projected literal.

The check never fires where the audit could pass: it stops at a
passthrough read through a filter, join or row limit, at EXCEPT and
INTERSECT, and at a positional read of a by-name union, and it is
skipped on SQL Server, whose default collation ignores case and
trailing spaces. Computed values stay with the audit.

Rules can read each declared column's enum with
ctx.columns.enum_types(model); declared column types are unchanged.
The new fact is tracked for the Rules cache and invalidated by header
column edits.
…checks

The output-select walk shared the capture walk's scope classification,
which treats a LATERAL subquery as an expression, so a mapped
single-column slot read without AS inside a LATERAL whose columns reach
the output through l.* escaped the AS rule. The output walk now treats
every FROM and JOIN item, LATERAL included, like a derived table; the
capture check is unchanged.

The P018 clash check counted only bare columns beside a group, so an
undeclared column read through the relation, such as @weekly.region,
next to a group holding the same column produced a duplicate output
column. Such reads now count as output columns.
…es not declare

A forwarded aliased slot read whose slot the outer macro's relation
does not declare compiles and reads the column through the alias,
without a P022 at the outer macro.
@kvlonge
kvlonge enabled auto-merge (squash) October 26, 2026 06:09
@kvlonge
kvlonge merged commit 229570f into main Oct 26, 2026
46 checks passed
@kvlonge
kvlonge deleted the fix/bug-batch-3 branch October 26, 2026 07:10
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