--- name: new-check description: Scaffold a new dbt-bouncer check class with tests --- # Create a New Check Follow these steps to add a new check to dbt-bouncer. ## 1. Determine Check Location - **Category:** manifest, catalog, or run_results? - **Resource type:** model, source, seed, exposure, macro, etc.? - **File:** place in the appropriate submodule under `src/dbt_bouncer/checks//`. ## 2. Write the Check Use the `@check` decorator, passing the rule code. Everything else is inferred from the function signature: ```python from dbt_bouncer.check_framework.decorator import check, fail @check(code="XX000") def check_model_xxx(model): """Check description.""" if some_condition: fail(f"`{model.unique_id}` failed because ...") ``` ## 3. Decorator API Reference `code` is the only argument `@check` takes. All other metadata is inferred from the function signature: - **code** — the check's unique rule code, e.g. `MO048`. See "Assign a rule code" below. - **name** — the function name (must match the `name:` value in YAML config). - **iterate_over** — the first positional parameter (excluding `ctx`). If there are none, the check is global (runs once with context only). - **params** — keyword-only arguments (after `*`) become user-configurable Pydantic fields. - **ctx** — optional; only include in the signature if the function actually uses it. - **Parameter ordering** — must be `(resource, ctx, *, params)`. Resource first, `ctx` second. Putting `ctx` before the resource breaks iterate_over inference. For context-only checks, use `(ctx, *, params)`. ### Simple check (resource only) ```python @check(code="MO021") def check_model_description_populated(model): """Models must have a populated description.""" if not model.description or len(model.description.strip()) < 4: fail(f"`{model.unique_id}` does not have a populated description.") ``` ### Check with params ```python @check(code="MO038") def check_model_names(model, *, model_name_pattern: str): """Models must have a name matching the supplied regex.""" import re if not re.match(model_name_pattern, model.name, re.IGNORECASE): fail(f"`{model.unique_id}` does not match pattern `{model_name_pattern}`.") ``` ### Context-only check (no resource iteration) ```python @check(code="MO044") def check_model_test_coverage(ctx, *, min_model_test_coverage_pct: float = 100): """Set the minimum percentage of models that have at least one test.""" ... ``` ### `fail()` — raises `DbtBouncerFailedCheckError` ```python fail("message") ``` ### Assign a rule code Every check needs a unique rule code: a 2-letter resource prefix plus a 3-digit number, e.g. `MO048`. Two steps: 1. Pass it to the decorator: `@check(code="MO048")`. 2. Add the matching member to the resource's `*RuleCode` enum in `src/dbt_bouncer/enums.py`, keeping alphabetical order: ```python class ModelRuleCode(StrEnum): CHECK_MODEL_XXX = "MO048" ``` Use the next free number for the prefix — read the enum to find it. Never reuse or renumber a published code; users reference codes in their config. Prefixes: `CA` catalog, `EX` exposure, `LI` lineage, `MA` macro, `ME` metadata, `MO` model, `RR` run results, `SE` seed, `SM` semantic model, `SN` snapshot, `SO` source, `TE` test, `UT` unit test. ## 4. Register the Check - Add the check to `dbt-bouncer-example.yml` - Validate: `dbt-bouncer run --config-file dbt-bouncer-example.yml` - Ensure alphabetical ordering is maintained ## 5. Write Tests Use `check_passes` / `check_fails` from `dbt_bouncer.testing`: ```python from dbt_bouncer.testing import check_fails, check_passes def test_pass(): check_passes("check_model_xxx", model={"name": "valid"}, my_param="value") def test_fail(): check_fails("check_model_xxx", model={"name": "invalid"}, my_param="value") # For context-dependent checks: def test_with_context(): check_passes("check_model_xxx", model={"name": "m1"}, ctx_models=[{"name": "m1"}, {"name": "m2"}]) ``` - Resource dicts are auto-merged with sensible defaults (no fixture setup needed) - `ctx_*` kwargs build the `CheckContext` automatically - Include at least one happy path and one unhappy path test - Ensure `__init__.py` exists in the test subdirectory ## 6. Verify ```bash mise run generate-schema mise run generate-rule-codes-doc mise run test-unit prek run --all-files ``` The `rule-codes-doc-check` hook fails if a check has no code, if a declared code is unused, or if `docs/checks/rule_codes.md` has drifted.