Custom Rules
Overview
Define repository-owned checks with one typed Python API.
Custom Rules are ordinary Python under rules/**/*.py. Only functions decorated with @rule
register; helpers, constants, dataclasses, and classes remain ordinary Python.
Model-subject Rules
Section titled “Model-subject Rules”from sqlbuild.rules import Finding, Model, RuleContext, rule
@rule( code="XSQBRARCH001", message="Final models must declare an order identifier", remediation="Declare order_id in the model contract.",)def final_order_identifier(*, model: Model, ctx: RuleContext) -> list[Finding]: declared = {column.name for column in ctx.columns.declared(model)} return [] if "order_id" in declared else [ctx.finding(subject=model)]Annotations determine invocation. The signature must contain exactly one typed Model or Project
subject and one typed RuleContext, all keyword-only. Parameter names and position are not semantic.
A model-subject Rule can still inspect project-wide facts.
Project-subject Rules
Section titled “Project-subject Rules”Use Project when an invariant has no natural model subject:
from sqlbuild.rules import Finding, Project, RuleContext, rule
@rule( code="XSQBRARCH002", message="The project must contain a final model directory", remediation="Add models/final and place final outputs beneath it.",)def final_directory(*, ctx: RuleContext, project: Project) -> list[Finding]: del project paths = ctx.project.tree.glob("models/final/**") return [] if paths else [ctx.finding(subject="models")]Rules cannot mutate SQL, resources, adapter lowering, or compiler output. They return diagnostics only.
Typed options
Section titled “Typed options”from sqlbuild.rules import Finding, Model, RuleContext, RuleOption, rule
REQUIRED_PREFIX = RuleOption.string( name="required_prefix", default="customer", description="Required model-name prefix.",)
@rule( code="XSQBRNAME001", message="The model name uses the wrong prefix", remediation="Rename the model with the configured prefix.", options=(REQUIRED_PREFIX,),)def required_prefix(*, model: Model, ctx: RuleContext) -> list[Finding]: prefix = ctx.option(REQUIRED_PREFIX) return [] if model.name.startswith(prefix) else [ctx.finding(subject=model)][rules.rule_options.XSQBRNAME001]required_prefix = "order"Unknown codes, option names, or invalid values fail configuration.