Skip to content

Commit ff9a0f1

Browse files
joostboonclaude
andcommitted
feat: add _with_context variants for generic tests
Adds six new elementary generic tests that extend common dbt/dbt_utils/dbt_expectations tests with a `context_columns` parameter, allowing users to specify which additional columns are returned alongside failing rows. If `context_columns` is omitted, all columns are returned. New tests: - elementary.not_null_with_context - elementary.accepted_range_with_context - elementary.expect_column_values_to_not_be_null_with_context - elementary.expect_column_values_to_be_unique_with_context - elementary.expect_column_values_to_match_regex_with_context - elementary.relationships_with_context Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7a2b542 commit ff9a0f1

7 files changed

Lines changed: 189 additions & 0 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{% test accepted_range_with_context(model, column_name, min_value=none, max_value=none, inclusive=true, context_columns=none) %}
2+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
3+
{%- set existing_column_names = adapter.get_columns_in_relation(model) | map(attribute='name') | map('lower') | list %}
4+
{%- set select_cols = [column_name] %}
5+
{%- for col in context_columns %}
6+
{%- if col | lower == column_name | lower %}
7+
{# already included, skip #}
8+
{%- elif col | lower not in existing_column_names %}
9+
{%- do log("WARNING [accepted_range_with_context]: column '" ~ col ~ "' does not exist in model '" ~ model.name ~ "' and will be skipped.", info=true) %}
10+
{%- else %}
11+
{%- do select_cols.append(col) %}
12+
{%- endif %}
13+
{%- endfor %}
14+
{%- set select_clause = select_cols | join(', ') %}
15+
{%- else %}
16+
{%- set select_clause = '*' %}
17+
{%- endif %}
18+
19+
select {{ select_clause }}
20+
from {{ model }}
21+
where
22+
1 = 2
23+
{%- if min_value is not none %}
24+
or not {{ column_name }} >{{- "=" if inclusive }} {{ min_value }}
25+
{%- endif %}
26+
{%- if max_value is not none %}
27+
or not {{ column_name }} <{{- "=" if inclusive }} {{ max_value }}
28+
{%- endif %}
29+
{% endtest %}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{% test expect_column_values_to_be_unique_with_context(model, column_name, row_condition=none, context_columns=none) %}
2+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
3+
{%- set existing_column_names = adapter.get_columns_in_relation(model) | map(attribute='name') | map('lower') | list %}
4+
{%- set select_cols = [column_name] %}
5+
{%- for col in context_columns %}
6+
{%- if col | lower == column_name | lower %}
7+
{# already included, skip #}
8+
{%- elif col | lower not in existing_column_names %}
9+
{%- do log("WARNING [expect_column_values_to_be_unique_with_context]: column '" ~ col ~ "' does not exist in model '" ~ model.name ~ "' and will be skipped.", info=true) %}
10+
{%- else %}
11+
{%- do select_cols.append(col) %}
12+
{%- endif %}
13+
{%- endfor %}
14+
{%- set select_clause = select_cols | join(', ') %}
15+
{%- else %}
16+
{%- set select_clause = '*' %}
17+
{%- endif %}
18+
19+
select {{ select_clause }}
20+
from (
21+
select
22+
*,
23+
count(*) over (partition by {{ column_name }}) as n_records
24+
from {{ model }}
25+
{%- if row_condition %}
26+
where {{ row_condition }}
27+
{%- endif %}
28+
) validation
29+
where n_records > 1
30+
{% endtest %}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{% test expect_column_values_to_match_regex_with_context(model, column_name, regex, row_condition=none, is_raw=false, flags="", context_columns=none) %}
2+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
3+
{%- set existing_column_names = adapter.get_columns_in_relation(model) | map(attribute='name') | map('lower') | list %}
4+
{%- set select_cols = [column_name] %}
5+
{%- for col in context_columns %}
6+
{%- if col | lower == column_name | lower %}
7+
{# already included, skip #}
8+
{%- elif col | lower not in existing_column_names %}
9+
{%- do log("WARNING [expect_column_values_to_match_regex_with_context]: column '" ~ col ~ "' does not exist in model '" ~ model.name ~ "' and will be skipped.", info=true) %}
10+
{%- else %}
11+
{%- do select_cols.append(col) %}
12+
{%- endif %}
13+
{%- endfor %}
14+
{%- set select_clause = select_cols | join(', ') %}
15+
{%- else %}
16+
{%- set select_clause = '*' %}
17+
{%- endif %}
18+
19+
select {{ select_clause }}
20+
from {{ model }}
21+
where
22+
{{ dbt_expectations.regexp_instr(column_name, regex, is_raw=is_raw, flags=flags) }} = 0
23+
{%- if row_condition %}
24+
and {{ row_condition }}
25+
{%- endif %}
26+
{% endtest %}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% test expect_column_values_to_not_be_null_with_context(model, column_name, row_condition=none, context_columns=none) %}
2+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
3+
{%- set existing_column_names = adapter.get_columns_in_relation(model) | map(attribute='name') | map('lower') | list %}
4+
{%- set select_cols = [column_name] %}
5+
{%- for col in context_columns %}
6+
{%- if col | lower == column_name | lower %}
7+
{# already included, skip #}
8+
{%- elif col | lower not in existing_column_names %}
9+
{%- do log("WARNING [expect_column_values_to_not_be_null_with_context]: column '" ~ col ~ "' does not exist in model '" ~ model.name ~ "' and will be skipped.", info=true) %}
10+
{%- else %}
11+
{%- do select_cols.append(col) %}
12+
{%- endif %}
13+
{%- endfor %}
14+
{%- set select_clause = select_cols | join(', ') %}
15+
{%- else %}
16+
{%- set select_clause = '*' %}
17+
{%- endif %}
18+
19+
select {{ select_clause }}
20+
from {{ model }}
21+
where {{ column_name }} is null
22+
{%- if row_condition %}
23+
and {{ row_condition }}
24+
{%- endif %}
25+
{% endtest %}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{% test not_null_with_context(model, column_name, context_columns=none) %}
2+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
3+
{%- set existing_column_names = adapter.get_columns_in_relation(model) | map(attribute='name') | map('lower') | list %}
4+
5+
{%- set select_cols = [column_name] %}
6+
{%- for col in context_columns %}
7+
{%- if col | lower == column_name | lower %}
8+
{# already included, skip #}
9+
{%- elif col | lower not in existing_column_names %}
10+
{%- do log("WARNING [not_null_with_context]: column '" ~ col ~ "' does not exist in model '" ~ model.name ~ "' and will be skipped.", info=true) %}
11+
{%- else %}
12+
{%- do select_cols.append(col) %}
13+
{%- endif %}
14+
{%- endfor %}
15+
select
16+
{{ select_cols | join(', ') }}
17+
from {{ model }}
18+
where {{ column_name }} is null
19+
{%- else %}
20+
select *
21+
from {{ model }}
22+
where {{ column_name }} is null
23+
{%- endif %}
24+
{% endtest %}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% test relationships_with_context(model, column_name, to, field, context_columns=none) %}
2+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
3+
{%- set existing_column_names = adapter.get_columns_in_relation(model) | map(attribute='name') | map('lower') | list %}
4+
{%- set select_cols = ['child.' ~ column_name] %}
5+
{%- for col in context_columns %}
6+
{%- if col | lower == column_name | lower %}
7+
{# already included, skip #}
8+
{%- elif col | lower not in existing_column_names %}
9+
{%- do log("WARNING [relationships_with_context]: column '" ~ col ~ "' does not exist in model '" ~ model.name ~ "' and will be skipped.", info=true) %}
10+
{%- else %}
11+
{%- do select_cols.append('child.' ~ col) %}
12+
{%- endif %}
13+
{%- endfor %}
14+
{%- set select_clause = select_cols | join(', ') %}
15+
{%- else %}
16+
{%- set select_clause = 'child.*' %}
17+
{%- endif %}
18+
19+
select {{ select_clause }}
20+
from {{ model }} as child
21+
left join {{ to }} as parent
22+
on child.{{ column_name }} = parent.{{ field }}
23+
where child.{{ column_name }} is not null
24+
and parent.{{ field }} is null
25+
{% endtest %}

macros/utils/common_test_configs.sql

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,36 @@
450450
"collect_metrics": {
451451
"description": "Collects metrics for the specified column or table. The test will always pass."
452452
},
453+
"not_null_with_context": {
454+
"quality_dimension": "completeness",
455+
"failed_row_count_calc": "count(*)",
456+
"description": "Validates that there are no null values in a column, returning additional context columns alongside failing rows. If no context_columns are specified, all columns are returned.",
457+
},
458+
"accepted_range_with_context": {
459+
"quality_dimension": "validity",
460+
"failed_row_count_calc": "count(*)",
461+
"description": "Validates that column values fall within an accepted range, returning additional context columns alongside failing rows. If no context_columns are specified, all columns are returned.",
462+
},
463+
"expect_column_values_to_not_be_null_with_context": {
464+
"quality_dimension": "completeness",
465+
"failed_row_count_calc": "count(*)",
466+
"description": "Expects column values to not be null, returning additional context columns alongside failing rows. If no context_columns are specified, all columns are returned.",
467+
},
468+
"expect_column_values_to_be_unique_with_context": {
469+
"quality_dimension": "uniqueness",
470+
"failed_row_count_calc": "count(*)",
471+
"description": "Expects column values to be unique, returning all duplicate rows with additional context columns. If no context_columns are specified, all columns are returned.",
472+
},
473+
"expect_column_values_to_match_regex_with_context": {
474+
"quality_dimension": "validity",
475+
"failed_row_count_calc": "count(*)",
476+
"description": "Expects column values to match a given regular expression, returning additional context columns alongside failing rows. If no context_columns are specified, all columns are returned.",
477+
},
478+
"relationships_with_context": {
479+
"quality_dimension": "consistency",
480+
"failed_row_count_calc": "count(*)",
481+
"description": "Validates referential integrity between a child and parent table, returning additional context columns alongside failing rows. If no context_columns are specified, all columns are returned.",
482+
},
453483
},
454484
} %}
455485
{% do return(common_tests_configs_mapping) %}

0 commit comments

Comments
 (0)