Custom Rules
RuleContext and compiler facts
Inspect stable compiler-owned facts and opt into lazy SQL AST access.
RuleContext exposes typed, read-only views over the compiled project:
ctx.sql: authored and expanded SQLctx.graph: compiler-resolved dependencies and dependentsctx.columns: declared and inferred output columnsctx.contracts: enforcement and grain factsctx.testsandctx.audits: checks associated with a modelctx.declarations: public and model-scoped enums and constantsctx.project: compiled resources and deterministic project-tree observations
Authored and expanded SQL
Section titled “Authored and expanded SQL”Choose the SQL representation that matches the requirement:
sql = ctx.sql.for_model(model)
authored_text = sql.authored.sourceexpanded_text = sql.expanded.sourceAn authoring convention may inspect what a developer wrote. A compiler-output convention may need the expanded SQL after interpolation, declarations, and macros.
Common SQL structures
Section titled “Common SQL structures”Common structures are available on expanded SQL as SqlNode source locations, each with a kind,
a line and a column:
for cte in sql.expanded.ctes(): ...
for star in sql.expanded.star_projections(): ...| Method | Finds |
|---|---|
ctes() |
Each common table expression, at its name |
joins() |
Each explicit join clause, at its join keyword, not a comma-separated FROM item |
subqueries() |
Each parenthesized subquery, at its opening parenthesis |
star_projections() |
Each * and table.* that expands columns, including a function argument such as OBJECT_CONSTRUCT(*), but not an aggregate’s *, as in COUNT(*) |
nodes(kind=...) |
Each syntax tree node of that kind, compared without case |
line and column count within the document’s source. For expanded SQL that is the query
text, not the model file. Authored SQL is the whole model file, including its MODEL header, so it
does not parse as SQL; read its text through source.
Declared enum types
Section titled “Declared enum types”ctx.columns.declared(model) reports each declared column with its portable scalar type, so an
enum-typed column reads as VARCHAR or INTEGER. ctx.columns.enum_types(model) maps each
enum-typed column’s name to the enum it declares:
enum_types = ctx.columns.enum_types(model)if enum_types.get("fulfillment_method") != "fulfillment_method": ...Syntax tree
Section titled “Syntax tree”Use the syntax tree only when the common structures are insufficient:
for node in sql.expanded.ast().walk(): if node.kind == "window_function": ...ast() returns a document node whose children are the statements. Every SqlTreeNode has a
kind, a line and column where it starts, children, and walk(), which yields the node and
its descendants in pre-order. Kinds are SQLQuack’s snake_case names, such as select, subquery,
star, join, cte, column, function and window_function.
Parsing is lazy. A path or contract Rule does not pay for SQL parsing merely because syntax tree access exists elsewhere.
The tree comes from the SQLQuack parser SQLBuild uses for the project’s dialect:
| Dialect | Syntax tree |
|---|---|
| PostgreSQL | Native PostgreSQL parser |
| DuckDB, MotherDuck, Snowflake, SQLite | SQLQuack’s shared parser |
| Other dialects | Unavailable |
For the shared parser, a node the parser records no position for, such as a bare * or the query
inside a subquery, reports the position of its parent.
Reading the tree, or any common structure, raises RuleUsageError when:
| Cause | Message starts with |
|---|---|
| The dialect has no syntax tree | SQL syntax trees are unavailable for the <dialect> dialect |
| The SQL does not parse | SQL could not be parsed: |
| A statement uses syntax the parser keeps as unparsed text, or nests too deeply | SQL syntax tree is unavailable: |
Project structure
Section titled “Project structure”Use the compiler-owned tree rather than direct filesystem calls:
parts = ctx.project.tree.relative_parts(path=model.path, under="models")models = ctx.project.tree.resources_under("models/orders")config = ctx.project.tree.read_text("rules/requirements.yaml")Paths remain project-relative, and observations participate in cache invalidation.
Choose the subject deliberately
Section titled “Choose the subject deliberately”A model-subject Rule is evaluated and cached per model. A project-subject Rule runs once and can
iterate ctx.project.models. Use a project subject only when the invariant genuinely needs a
project-wide view; its cache invalidation is intentionally broader.