Skip to content

Description#

Note

The below checks require manifest.json to be present.

Checks related to model descriptions and documentation coverage.

Functions:

Name Description
check_column_descriptions_are_consistent

The same column name must not have conflicting descriptions across models.

check_model_description_contains_regexp_pattern

Models must have a description that matches the provided pattern.

check_model_description_populated

Models must have a populated description.

check_model_documentation_coverage

Set the minimum percentage of models that have a populated description.

check_model_documented_in_same_directory

Models must be documented in the same directory where they are defined (i.e. .yml and .sql files are in the same directory).

check_column_descriptions_are_consistent #

The same column name must not have conflicting descriptions across models.

Rationale

When the same logical column (e.g. customer_id) carries a different description in two models, it signals copy-paste drift or inconsistent documentation standards. Consumers of the data catalogue see contradictory definitions, eroding trust. This check enforces a single canonical description per column name project-wide, prompting teams to agree on shared documentation.

Receives at execution time:

Name Type Description
models list[ModelNode]

List of ModelNode objects parsed from manifest.json.

Other Parameters (passed via config file):

Name Type Description
description str | None

Description of what the check does and why it is implemented.

severity Literal[error, warn] | None

Severity level of the check. Default: error.

Example(s):

manifest_checks:
    - name: check_column_descriptions_are_consistent

Source code in src/dbt_bouncer/checks/manifest/models/description.py
@check(code="MO019")
def check_column_descriptions_are_consistent(ctx):
    """The same column name must not have conflicting descriptions across models.

    !!! info "Rationale"

        When the same logical column (e.g. `customer_id`) carries a different description in two models, it signals copy-paste drift or inconsistent documentation standards. Consumers of the data catalogue see contradictory definitions, eroding trust. This check enforces a single canonical description per column name project-wide, prompting teams to agree on shared documentation.

    Receives:
        models (list[ModelNode]): List of ModelNode objects parsed from `manifest.json`.

    Other Parameters:
        description (str | None): Description of what the check does and why it is implemented.
        severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.

    Example(s):
        ```yaml
        manifest_checks:
            - name: check_column_descriptions_are_consistent
        ```

    """
    descs: dict[str, set[str]] = defaultdict(set)
    for model in ctx.models:
        for name, col in (model.columns or {}).items():
            d = (getattr(col, "description", "") or "").strip()
            if d:
                descs[name].add(d)
    conflicts = {name: sorted(v) for name, v in descs.items() if len(v) > 1}
    if conflicts:
        fail(f"Columns have conflicting descriptions across models: {conflicts}.")

check_model_description_contains_regexp_pattern #

Models must have a description that matches the provided pattern.

Rationale

A free-text description field is easy to fill with placeholder or low-quality content. Requiring descriptions to match a pattern (e.g. a minimum sentence structure or a specific prefix) ensures that documentation meets a baseline standard of usefulness rather than just being non-empty.

Parameters:

Name Type Description Default
regexp_pattern str

The regexp pattern that should match the model description.

required

Receives at execution time:

Name Type Description
model ModelNode

The ModelNode object to check.

Other Parameters (passed via config file):

Name Type Description
description str | None

Description of what the check does and why it is implemented.

exclude str | list[str] | None

Regex pattern(s) to match the model path. Model paths that match any pattern will not be checked.

include str | list[str] | None

Regex pattern(s) to match the model path. Only model paths that match any pattern will be checked.

materialization Literal[ephemeral, incremental, table, view] | None

Limit check to models with the specified materialization.

severity Literal[error, warn] | None

Severity level of the check. Default: error.

Example(s):

manifest_checks:
    - name: check_model_description_contains_regexp_pattern
      regexp_pattern: .*pattern_to_match.*

Source code in src/dbt_bouncer/checks/manifest/models/description.py
@check(code="MO020")
def check_model_description_contains_regexp_pattern(model, *, regexp_pattern: str):
    """Models must have a description that matches the provided pattern.

    !!! info "Rationale"

        A free-text description field is easy to fill with placeholder or low-quality content. Requiring descriptions to match a pattern (e.g. a minimum sentence structure or a specific prefix) ensures that documentation meets a baseline standard of usefulness rather than just being non-empty.

    Parameters:
        regexp_pattern (str): The regexp pattern that should match the model description.

    Receives:
        model (ModelNode): The ModelNode object to check.

    Other Parameters:
        description (str | None): Description of what the check does and why it is implemented.
        exclude (str | list[str] | None): Regex pattern(s) to match the model path. Model paths that match any pattern will not be checked.
        include (str | list[str] | None): Regex pattern(s) to match the model path. Only model paths that match any pattern will be checked.
        materialization (Literal["ephemeral", "incremental", "table", "view"] | None): Limit check to models with the specified materialization.
        severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.

    Example(s):
        ```yaml
        manifest_checks:
            - name: check_model_description_contains_regexp_pattern
              regexp_pattern: .*pattern_to_match.*
        ```

    """
    compiled = compile_pattern(regexp_pattern.strip(), flags=re.DOTALL)
    if not compiled.match(str(model.description)):
        fail(
            f"""`{get_clean_model_name(model.unique_id)}`'s description "{model.description}" doesn't match the supplied regex: {regexp_pattern}."""
        )

check_model_description_populated #

Models must have a populated description.

Rationale

Descriptions are the primary way data consumers discover what a model represents and how to use it. Without them, analysts waste time reverse-engineering SQL logic or asking the data team. This check ensures every model is self-documenting, which is critical for onboarding, data catalogues, and self-service analytics.

Parameters:

Name Type Description Default
min_description_length int | None

Minimum length required for the description to be considered populated.

None

Receives at execution time:

Name Type Description
model ModelNode

The ModelNode object to check.

Other Parameters (passed via config file):

Name Type Description
description str | None

Description of what the check does and why it is implemented.

exclude str | list[str] | None

Regex pattern(s) to match the model path. Model paths that match any pattern will not be checked.

include str | list[str] | None

Regex pattern(s) to match the model path. Only model paths that match any pattern will be checked.

materialization Literal[ephemeral, incremental, table, view] | None

Limit check to models with the specified materialization.

severity Literal[error, warn] | None

Severity level of the check. Default: error.

Example(s):

manifest_checks:
    - name: check_model_description_populated
manifest_checks:
    - name: check_model_description_populated
      min_description_length: 25 # Setting a stricter requirement for description length

Source code in src/dbt_bouncer/checks/manifest/models/description.py
@check(code="MO021")
def check_model_description_populated(
    model, *, min_description_length: Annotated[int, Field(gt=0)] | None = None
):
    """Models must have a populated description.

    !!! info "Rationale"

        Descriptions are the primary way data consumers discover what a model represents and how to use it. Without them, analysts waste time reverse-engineering SQL logic or asking the data team. This check ensures every model is self-documenting, which is critical for onboarding, data catalogues, and self-service analytics.

    Parameters:
        min_description_length (int | None): Minimum length required for the description to be considered populated.

    Receives:
        model (ModelNode): The ModelNode object to check.

    Other Parameters:
        description (str | None): Description of what the check does and why it is implemented.
        exclude (str | list[str] | None): Regex pattern(s) to match the model path. Model paths that match any pattern will not be checked.
        include (str | list[str] | None): Regex pattern(s) to match the model path. Only model paths that match any pattern will be checked.
        materialization (Literal["ephemeral", "incremental", "table", "view"] | None): Limit check to models with the specified materialization.
        severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.

    Example(s):
        ```yaml
        manifest_checks:
            - name: check_model_description_populated
        ```
        ```yaml
        manifest_checks:
            - name: check_model_description_populated
              min_description_length: 25 # Setting a stricter requirement for description length
        ```

    """
    if not is_description_populated(
        model.description or "", min_description_length or 4
    ):
        fail(
            f"`{get_clean_model_name(model.unique_id)}` does not have a populated description."
        )

check_model_documentation_coverage #

Set the minimum percentage of models that have a populated description.

Rationale

Rather than requiring every single model to be documented immediately, this check allows teams to set a realistic target and enforce it incrementally. It prevents documentation coverage from silently regressing as new models are added, nudging teams towards full documentation over time.

Parameters:

Name Type Description Default
min_model_documentation_coverage_pct int

The minimum percentage of models that must have a populated description.

100

Receives at execution time:

Name Type Description
models list[ModelNode]

List of ModelNode objects parsed from manifest.json.

Other Parameters (passed via config file):

Name Type Description
description str | None

Description of what the check does and why it is implemented.

severity Literal[error, warn] | None

Severity level of the check. Default: error.

Info

A project with no models passes, as there is nothing left undocumented.

Example(s):

manifest_checks:
    - name: check_model_documentation_coverage
      min_model_documentation_coverage_pct: 90

Source code in src/dbt_bouncer/checks/manifest/models/description.py
@check(code="MO022")
def check_model_documentation_coverage(
    ctx,
    *,
    min_model_documentation_coverage_pct: Annotated[int, Field(ge=0, le=100)] = 100,
):
    """Set the minimum percentage of models that have a populated description.

    !!! info "Rationale"

        Rather than requiring every single model to be documented immediately, this check allows teams to set a realistic target and enforce it incrementally. It prevents documentation coverage from silently regressing as new models are added, nudging teams towards full documentation over time.

    Parameters:
        min_model_documentation_coverage_pct (int): The minimum percentage of models that must have a populated description.

    Receives:
        models (list[ModelNode]): List of ModelNode objects parsed from `manifest.json`.

    Other Parameters:
        description (str | None): Description of what the check does and why it is implemented.
        severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.

    !!! info

        A project with no models passes, as there is nothing left undocumented.

    Example(s):
        ```yaml
        manifest_checks:
            - name: check_model_documentation_coverage
              min_model_documentation_coverage_pct: 90
        ```

    """
    num_models = len(ctx.models)
    if num_models == 0:
        # No models means nothing is left undocumented, so coverage is vacuously
        # complete. Returning early also avoids dividing by zero.
        return

    models_with_description = []
    for model in ctx.models:
        if is_description_populated(
            description=model.description or "", min_description_length=4
        ):
            models_with_description.append(model.unique_id)

    num_models_with_descriptions = len(models_with_description)
    model_description_coverage_pct = (num_models_with_descriptions / num_models) * 100

    if model_description_coverage_pct < min_model_documentation_coverage_pct:
        fail(
            f"Only {model_description_coverage_pct}% of models have a populated description, this is less than the permitted minimum of {min_model_documentation_coverage_pct}%."
        )

check_model_documented_in_same_directory #

Models must be documented in the same directory where they are defined (i.e. .yml and .sql files are in the same directory).

Rationale

Co-locating a model's SQL file and its YAML documentation makes the project easier to navigate. When documentation lives in a different directory, contributors may miss it during code review or forget to update it when changing the model. Keeping both files together reinforces the habit of treating documentation as part of the model definition.

Receives at execution time:

Name Type Description
model ModelNode

The ModelNode object to check.

Other Parameters (passed via config file):

Name Type Description
description str | None

Description of what the check does and why it is implemented.

exclude str | list[str] | None

Regex pattern(s) to match the model path. Model paths that match any pattern will not be checked.

include str | list[str] | None

Regex pattern(s) to match the model path. Only model paths that match any pattern will be checked.

materialization Literal[ephemeral, incremental, table, view] | None

Limit check to models with the specified materialization.

severity Literal[error, warn] | None

Severity level of the check. Default: error.

Info

A model with no properties file at all is reported as not documented.

Example(s):

manifest_checks:
    - name: check_model_documented_in_same_directory

Source code in src/dbt_bouncer/checks/manifest/models/description.py
@check(code="MO023")
def check_model_documented_in_same_directory(model):
    """Models must be documented in the same directory where they are defined (i.e. `.yml` and `.sql` files are in the same directory).

    !!! info "Rationale"

        Co-locating a model's SQL file and its YAML documentation makes the project easier to navigate. When documentation lives in a different directory, contributors may miss it during code review or forget to update it when changing the model. Keeping both files together reinforces the habit of treating documentation as part of the model definition.

    Receives:
        model (ModelNode): The ModelNode object to check.

    Other Parameters:
        description (str | None): Description of what the check does and why it is implemented.
        exclude (str | list[str] | None): Regex pattern(s) to match the model path. Model paths that match any pattern will not be checked.
        include (str | list[str] | None): Regex pattern(s) to match the model path. Only model paths that match any pattern will be checked.
        materialization (Literal["ephemeral", "incremental", "table", "view"] | None): Limit check to models with the specified materialization.
        severity (Literal["error", "warn"] | None): Severity level of the check. Default: `error`.

    !!! info

        A model with no properties file at all is reported as not documented.

    Example(s):
        ```yaml
        manifest_checks:
            - name: check_model_documented_in_same_directory
        ```

    """
    model = cast("Any", model)
    model_sql_path = Path(clean_path_str(model.original_file_path))
    model_sql_dir = model_sql_path.parent.parts

    # A model with no patch_path has no properties file, so there is no
    # documentation directory to compare against.
    patch_path_str = clean_path_str(getattr(model, "patch_path", None) or "")
    if not patch_path_str:
        fail(f"`{get_clean_model_name(model.unique_id)}` is not documented.")

    start_idx = patch_path_str.find("models")
    if start_idx != -1:
        patch_path_str = patch_path_str[start_idx:]

    model_doc_path = Path(patch_path_str)
    model_doc_dir = model_doc_path.parent.parts

    if model_doc_dir != model_sql_dir:
        fail(
            f"`{get_clean_model_name(model.unique_id)}` is documented in a different directory to the `.sql` file: `{'/'.join(model_doc_dir)}` vs `{'/'.join(model_sql_dir)}`."
        )