Skip to content

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 SQL
  • ctx.graph: compiler-resolved dependencies and dependents
  • ctx.columns: declared and inferred output columns
  • ctx.contracts: enforcement and grain facts
  • ctx.tests and ctx.audits: checks associated with a model
  • ctx.declarations: public and model-scoped enums and constants
  • ctx.project: compiled resources and deterministic project-tree observations

Choose the SQL representation that matches the requirement:

sql = ctx.sql.for_model(model)
authored_text = sql.authored.source
expanded_text = sql.expanded.source

An authoring convention may inspect what a developer wrote. A compiler-output convention may need the expanded SQL after interpolation, declarations, and macros.

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.

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":
...

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:

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.

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.