-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New test configs: where, limit, warn_if, error_if, fail_calc #3336
Changes from 11 commits
9be02a2
b952b70
c5292f5
80473b3
6ca56dc
1f0de79
e7defd8
618c6f8
64b4d3f
39c61f0
5a27ba3
7c136b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -438,6 +438,34 @@ class SeedConfig(NodeConfig): | |
class TestConfig(NodeConfig): | ||
materialized: str = 'test' | ||
severity: Severity = Severity('ERROR') | ||
where: Optional[str] = None | ||
limit: Optional[int] = None | ||
fail_calc: str = "count(*)" | ||
warn_if: str = "!= 0" | ||
error_if: str = "!= 0" | ||
|
||
@classmethod | ||
def same_contents( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah! I see now that this is the proper place for it. Nice work, this logic is much clearer. |
||
cls, unrendered: Dict[str, Any], other: Dict[str, Any] | ||
) -> bool: | ||
"""This is like __eq__, except it explicitly checks certain fields.""" | ||
modifiers = [ | ||
'severity', | ||
'where', | ||
'limit', | ||
'fail_calc', | ||
'warn_if', | ||
'error_if' | ||
] | ||
|
||
seen = set() | ||
for _, target_name in cls._get_fields(): | ||
key = target_name | ||
seen.add(key) | ||
if key in modifiers: | ||
if not cls.compare_key(unrendered, other, key): | ||
return False | ||
return True | ||
|
||
|
||
@dataclass | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,21 @@ | ||
{%- materialization test, default -%} | ||
|
||
{% set limit = config.get('limit') %} | ||
{% set fail_calc = config.get('fail_calc') %} | ||
{% set warn_if = config.get('warn_if') %} | ||
{% set error_if = config.get('error_if') %} | ||
|
||
{% call statement('main', fetch_result=True) -%} | ||
select count(*) as validation_errors | ||
|
||
select | ||
{{ fail_calc }} as failures, | ||
{{ fail_calc }} {{ warn_if }} as should_warn, | ||
{{ fail_calc }} {{ error_if }} as should_error | ||
from ( | ||
{{ sql }} | ||
{{ "limit " ~ limit if limit }} | ||
) _dbt_internal_test | ||
{%- endcall %} | ||
|
||
{% endcall %} | ||
|
||
{%- endmaterialization -%} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -185,7 +185,9 @@ class TestBuilder(Generic[Testable]): | |
r'(?P<test_name>([a-zA-Z_][0-9a-zA-Z_]*))' | ||
) | ||
# kwargs representing test configs | ||
MODIFIER_ARGS = ('severity', 'tags', 'enabled') | ||
MODIFIER_ARGS = ( | ||
'severity', 'tags', 'enabled', 'where', 'limit', 'warn_if', 'error_if', 'fail_calc' | ||
) | ||
|
||
def __init__( | ||
self, | ||
|
@@ -278,6 +280,21 @@ def severity(self) -> Optional[str]: | |
else: | ||
return None | ||
|
||
def where(self) -> Optional[str]: | ||
return self.modifiers.get('where') | ||
|
||
def limit(self) -> Optional[int]: | ||
return self.modifiers.get('limit') | ||
|
||
def warn_if(self) -> Optional[str]: | ||
return self.modifiers.get('warn_if') | ||
|
||
def error_if(self) -> Optional[str]: | ||
return self.modifiers.get('error_if') | ||
|
||
def fail_calc(self) -> Optional[str]: | ||
return self.modifiers.get('fail_calc') | ||
|
||
def tags(self) -> List[str]: | ||
tags = self.modifiers.get('tags', []) | ||
if isinstance(tags, str): | ||
|
@@ -334,10 +351,13 @@ def build_raw_sql(self) -> str: | |
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to reorder # this is the 'raw_sql' that's used in 'render_update' and execution
# of the test macro
# config needs to be rendered last to take precedence over default configs
# set in the macro (test definition)
def build_raw_sql(self) -> str:
return (
"{{{{ {macro}(**{kwargs_name}) }}}}{config}"
).format(
macro=self.macro_name(),
config=self.construct_config(),
kwargs_name=SCHEMA_TEST_KWARGS_NAME,
) |
||
|
||
def build_model_str(self): | ||
targ = self.target | ||
cfg_where = "config.get('where')" | ||
if isinstance(self.target, UnparsedNodeUpdate): | ||
fmt = "{{{{ ref('{0.name}') }}}}" | ||
identifier = self.target.name | ||
target_str = f"{{{{ ref('{targ.name}') }}}}" | ||
elif isinstance(self.target, UnpatchedSourceDefinition): | ||
fmt = "{{{{ source('{0.source.name}', '{0.table.name}') }}}}" | ||
else: | ||
raise self._bad_type() | ||
return fmt.format(self.target) | ||
identifier = self.target.table.name | ||
target_str = f"{{{{ source('{targ.source.name}', '{targ.table.name}') }}}}" | ||
filtered = f"(select * from {target_str} where {{{{{cfg_where}}}}}) {identifier}" | ||
return f"{{% if {cfg_where} %}}{filtered}{{% else %}}{target_str}{{% endif %}}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes... indeed it is. I confirmed that
dbt test -m state:modified
works as expected, leveraging theunrendered_config
.