Skip to content

Commit edf8d50

Browse files
joostboonclaude
andauthored
feat: add _with_context variants for generic tests (#975)
* 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> * fix: address CodeRabbit review comments on _with_context tests - accepted_range_with_context: raise compiler error when both min_value and max_value are omitted, preventing a silent always-pass condition - expect_column_values_to_be_unique_with_context: use explicit column list from adapter.get_columns_in_relation instead of SELECT * to prevent the computed n_records window column from leaking into the output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: apply sqlfmt formatting to _with_context tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 17aaf7f commit edf8d50

7 files changed

+269
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{% test accepted_range_with_context(
2+
model,
3+
column_name,
4+
min_value=none,
5+
max_value=none,
6+
inclusive=true,
7+
context_columns=none
8+
) %}
9+
{%- if min_value is none and max_value is none %}
10+
{{
11+
exceptions.raise_compiler_error(
12+
"accepted_range_with_context: at least one of min_value or max_value must be provided."
13+
)
14+
}}
15+
{%- endif %}
16+
17+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
18+
{%- set existing_column_names = (
19+
adapter.get_columns_in_relation(model)
20+
| map(attribute="name")
21+
| map("lower")
22+
| list
23+
) %}
24+
{%- set select_cols = [column_name] %}
25+
{%- for col in context_columns %}
26+
{%- if col | lower == column_name | lower %}
27+
{# already included, skip #}
28+
{%- elif col | lower not in existing_column_names %}
29+
{%- do log(
30+
"WARNING [accepted_range_with_context]: column '"
31+
~ col
32+
~ "' does not exist in model '"
33+
~ model.name
34+
~ "' and will be skipped.",
35+
info=true,
36+
) %}
37+
{%- else %} {%- do select_cols.append(col) %}
38+
{%- endif %}
39+
{%- endfor %}
40+
{%- set select_clause = select_cols | join(", ") %}
41+
{%- else %} {%- set select_clause = "*" %}
42+
{%- endif %}
43+
44+
select {{ select_clause }}
45+
from {{ model }}
46+
where
47+
1 = 2
48+
{%- if min_value is not none %}
49+
or not {{ column_name }} >{{- "=" if inclusive }} {{ min_value }}
50+
{%- endif %}
51+
{%- if max_value is not none %}
52+
or not {{ column_name }} <{{- "=" if inclusive }} {{ max_value }}
53+
{%- endif %}
54+
{% endtest %}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{% test expect_column_values_to_be_unique_with_context(
2+
model, column_name, row_condition=none, context_columns=none
3+
) %}
4+
{%- set all_columns = (
5+
adapter.get_columns_in_relation(model) | map(attribute="name") | list
6+
) %}
7+
{%- set all_columns_lower = all_columns | map("lower") | list %}
8+
9+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
10+
{%- set select_cols = [column_name] %}
11+
{%- for col in context_columns %}
12+
{%- if col | lower == column_name | lower %}
13+
{# already included, skip #}
14+
{%- elif col | lower not in all_columns_lower %}
15+
{%- do log(
16+
"WARNING [expect_column_values_to_be_unique_with_context]: column '"
17+
~ col
18+
~ "' does not exist in model '"
19+
~ model.name
20+
~ "' and will be skipped.",
21+
info=true,
22+
) %}
23+
{%- else %} {%- do select_cols.append(col) %}
24+
{%- endif %}
25+
{%- endfor %}
26+
{%- set select_clause = select_cols | join(", ") %}
27+
{%- else %} {%- set select_clause = all_columns | join(", ") %}
28+
{%- endif %}
29+
30+
select {{ select_clause }}
31+
from
32+
(
33+
select *, count(*) over (partition by {{ column_name }}) as n_records
34+
from {{ model }}
35+
{%- if row_condition %} where {{ row_condition }} {%- endif %}
36+
) validation
37+
where n_records > 1
38+
{% endtest %}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{% test expect_column_values_to_match_regex_with_context(
2+
model,
3+
column_name,
4+
regex,
5+
row_condition=none,
6+
is_raw=false,
7+
flags="",
8+
context_columns=none
9+
) %}
10+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
11+
{%- set existing_column_names = (
12+
adapter.get_columns_in_relation(model)
13+
| map(attribute="name")
14+
| map("lower")
15+
| list
16+
) %}
17+
{%- set select_cols = [column_name] %}
18+
{%- for col in context_columns %}
19+
{%- if col | lower == column_name | lower %}
20+
{# already included, skip #}
21+
{%- elif col | lower not in existing_column_names %}
22+
{%- do log(
23+
"WARNING [expect_column_values_to_match_regex_with_context]: column '"
24+
~ col
25+
~ "' does not exist in model '"
26+
~ model.name
27+
~ "' and will be skipped.",
28+
info=true,
29+
) %}
30+
{%- else %} {%- do select_cols.append(col) %}
31+
{%- endif %}
32+
{%- endfor %}
33+
{%- set select_clause = select_cols | join(", ") %}
34+
{%- else %} {%- set select_clause = "*" %}
35+
{%- endif %}
36+
37+
select {{ select_clause }}
38+
from {{ model }}
39+
where
40+
{{
41+
dbt_expectations.regexp_instr(
42+
column_name, regex, is_raw=is_raw, flags=flags
43+
)
44+
}} = 0 {%- if row_condition %} and {{ row_condition }} {%- endif %}
45+
{% endtest %}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{% test expect_column_values_to_not_be_null_with_context(
2+
model, column_name, row_condition=none, context_columns=none
3+
) %}
4+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
5+
{%- set existing_column_names = (
6+
adapter.get_columns_in_relation(model)
7+
| map(attribute="name")
8+
| map("lower")
9+
| list
10+
) %}
11+
{%- set select_cols = [column_name] %}
12+
{%- for col in context_columns %}
13+
{%- if col | lower == column_name | lower %}
14+
{# already included, skip #}
15+
{%- elif col | lower not in existing_column_names %}
16+
{%- do log(
17+
"WARNING [expect_column_values_to_not_be_null_with_context]: column '"
18+
~ col
19+
~ "' does not exist in model '"
20+
~ model.name
21+
~ "' and will be skipped.",
22+
info=true,
23+
) %}
24+
{%- else %} {%- do select_cols.append(col) %}
25+
{%- endif %}
26+
{%- endfor %}
27+
{%- set select_clause = select_cols | join(", ") %}
28+
{%- else %} {%- set select_clause = "*" %}
29+
{%- endif %}
30+
31+
select {{ select_clause }}
32+
from {{ model }}
33+
where
34+
{{ column_name }} is null
35+
{%- if row_condition %} and {{ row_condition }} {%- endif %}
36+
{% endtest %}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 = (
4+
adapter.get_columns_in_relation(model)
5+
| map(attribute="name")
6+
| map("lower")
7+
| list
8+
) %}
9+
10+
{%- set select_cols = [column_name] %}
11+
{%- for col in context_columns %}
12+
{%- if col | lower == column_name | lower %}
13+
{# already included, skip #}
14+
{%- elif col | lower not in existing_column_names %}
15+
{%- do log(
16+
"WARNING [not_null_with_context]: column '"
17+
~ col
18+
~ "' does not exist in model '"
19+
~ model.name
20+
~ "' and will be skipped.",
21+
info=true,
22+
) %}
23+
{%- else %} {%- do select_cols.append(col) %}
24+
{%- endif %}
25+
{%- endfor %}
26+
select {{ select_cols | join(", ") }}
27+
from {{ model }}
28+
where {{ column_name }} is null
29+
{%- else %} select * from {{ model }} where {{ column_name }} is null
30+
{%- endif %}
31+
{% endtest %}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{% test relationships_with_context(
2+
model, column_name, to, field, context_columns=none
3+
) %}
4+
{%- if context_columns is not none and context_columns is iterable and context_columns is not string %}
5+
{%- set existing_column_names = (
6+
adapter.get_columns_in_relation(model)
7+
| map(attribute="name")
8+
| map("lower")
9+
| list
10+
) %}
11+
{%- set select_cols = ["child." ~ column_name] %}
12+
{%- for col in context_columns %}
13+
{%- if col | lower == column_name | lower %}
14+
{# already included, skip #}
15+
{%- elif col | lower not in existing_column_names %}
16+
{%- do log(
17+
"WARNING [relationships_with_context]: column '"
18+
~ col
19+
~ "' does not exist in model '"
20+
~ model.name
21+
~ "' and will be skipped.",
22+
info=true,
23+
) %}
24+
{%- else %} {%- do select_cols.append("child." ~ col) %}
25+
{%- endif %}
26+
{%- endfor %}
27+
{%- set select_clause = select_cols | join(", ") %}
28+
{%- else %} {%- set select_clause = "child.*" %}
29+
{%- endif %}
30+
31+
select {{ select_clause }}
32+
from {{ model }} as child
33+
left join {{ to }} as parent on child.{{ column_name }} = parent.{{ field }}
34+
where child.{{ column_name }} is not null and parent.{{ field }} is null
35+
{% 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)