Contract Checks
A compile-time contract is a
schema agreement that Rocky enforces before it writes a single row.
dagster-rocky surfaces that validation as native Dagster
AssetCheckSpec
and AssetCheckResult
events.
RockyComponent declares the contract check specs at load time. They show on
the asset detail page before any compile or run, so a fresh deployment already
shows which assets have contracts. Which assets get contract
checks says where they attach.
Quickstart
Section titled “Quickstart”- Create a contracts directory next to your
models/:
project/├── rocky.toml├── models/│ ├── orders.toml│ └── orders.sql└── contracts/ └── orders.contract.toml- Declare contract rules in
orders.contract.toml:
# Required columns must be present in the model output[rules]required = ["id", "amount"]protected = ["customer_id"]
# Per-column type and nullability constraints[[columns]]name = "id"type = "Int64"nullable = false
[[columns]]name = "amount"type = "Float64"- Tell
RockyComponentwhere to find contracts in yourdefs.yaml:
type: dagster_rocky.RockyComponentattributes: config_path: rocky.toml models_dir: models contracts_dir: contracts # ← enables contract checksAfter you deploy, each matching asset shows up to three contract checks on its asset detail page. Which of the three appear depends on the rule kinds the contract declares:
contract_required_columns: passes when there are no E010 diagnosticscontract_protected_columns: passes when there are no E013 diagnosticscontract_column_constraints: passes when there are no E011/E012/E014/W010 diagnostics
Diagnostic code mapping
Section titled “Diagnostic code mapping”Rocky’s compiler emits a stable code for each kind of contract violation.
dagster-rocky maps each code to one asset check:
| Code | Severity | Meaning | Maps to check |
|---|---|---|---|
| E010 | ERROR | Required column missing from model output | contract_required_columns |
| E011 | ERROR | Column type mismatch | contract_column_constraints |
| E012 | ERROR | Column nullability violated | contract_column_constraints |
| E013 | ERROR | Protected column removed | contract_protected_columns |
| E014 | ERROR | Under [rules] no_new_nullable: a nullable column the contract does not declare, or the contract declaring no [[columns]] at all, which leaves no baseline for what counts as new |
contract_column_constraints |
| W010 | WARN | Contract column not in model output | contract_column_constraints |
When a check fails, the AssetCheckResult includes:
passed=Falseseverity=ERROR(orWARNif every failing diagnostic is W010)rocky/violation_count: number of contract violations for this checkrocky/violation_<i>: text of each violation in the form[<code>] <message>
Checks that report “not verified”
Section titled “Checks that report “not verified””A check can also fail because nothing was checked. Both cases fail at WARN, because the contract may be violated and nobody looked. Each carries a metadata key that tells it apart from a real violation:
| Case | Metadata key | What happened |
|---|---|---|
| No compile output to read | rocky/compile_missing |
rocky compile failed, or had not run, when the cached state was written. Refresh the state after a successful compile. |
The compiler did not find the model (W011) |
rocky/contract_model_not_found |
The contract names a model the project does not contain, so Rocky never validated it. The contract file may name a model that was renamed or removed. |
Both cases report every check the contract declares, in the same order as an evaluated contract, so a declared check never vanishes.
A passing check can also carry rocky/unverified_<i> entries, with rocky/unverified_count. Those come from I003, where the column’s inferred type is Unknown and the declared type was compared against nothing. They never fail the check; they stop a pass from claiming a constraint was tested when it was not.
Standalone helpers
Section titled “Standalone helpers”The translation logic is exposed as pure functions. Use them without
RockyComponent:
from pathlib import Pathfrom dagster_rocky import ( discover_contract_rules, contract_check_specs_for_model, contract_check_results_from_diagnostics,)
# Walk a contracts directoryrules_by_model = discover_contract_rules(Path("contracts"))
# Build specs for one modelimport dagster as dgasset_key = dg.AssetKey(["acme", "marts", "orders"])rules = rules_by_model["orders"]specs = list(contract_check_specs_for_model(asset_key, rules))
# Translate compile diagnostics into results at materialization timeresults = list( contract_check_results_from_diagnostics( compile_result.diagnostics, asset_key=asset_key, model_name="orders", rules=rules, ))Which assets get contract checks
Section titled “Which assets get contract checks”RockyComponent matches contracts to assets by table name. A contract file
orders.contract.toml attaches to a source-replication asset whose key ends
with orders.
Derived-model assets do not get contract check specs. That holds for both
surface_derived_models: true and dag_mode: true. build_model_specs
accepts contract_rules_by_model but ignores it. To check a derived model’s
contract in Dagster, build the specs yourself with the
standalone helpers.
How discover_contract_rules handles bad input
Section titled “How discover_contract_rules handles bad input”discover_contract_rules is defensive against:
- Missing directory: returns empty dict, no error.
- Empty contract files: silently skipped (no specs declared).
- Malformed TOML: raises
ContractParseErrorwith the offending file path in the message, so users can find and fix the file.
So you can pass contracts_dir="contracts" without first checking that the
directory exists.