Skip to content

Commit

Permalink
added test support for databases without boolean types (#4091)
Browse files Browse the repository at this point in the history
* added support for dbs without boolean types

* catch errors a bit better

* moved changelog entry

* fixed tests and updated exception

* cleaned up bool check

* added positive test, removed self ref
  • Loading branch information
emmyoop committed Oct 21, 2021
1 parent 1c61bb1 commit eace5b7
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Contributors:
- Performance: Use child_map to find tests for nodes in resolve_graph ([#4012](https://github.com/dbt-labs/dbt/issues/4012), [#4022](https://github.com/dbt-labs/dbt/pull/4022))
- Switch `unique_field` from abstractproperty to optional property. Add docstring ([#4025](https://github.com/dbt-labs/dbt/issues/4025), [#4028](https://github.com/dbt-labs/dbt/pull/4028))
- Include only relational nodes in `database_schema_set` ([#4063](https://github.com/dbt-labs/dbt-core/issues/4063), [#4077](https://github.com/dbt-labs/dbt-core/pull/4077))
- Added support for tests on databases that lack real boolean types. ([#4084](https://github.com/dbt-labs/dbt-core/issues/4084))

Contributors:
- [@ljhopkins2](https://github.com/ljhopkins2) ([#4077](https://github.com/dbt-labs/dbt-core/pull/4077))
Expand Down
9 changes: 9 additions & 0 deletions core/dbt/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,15 @@ def invalid_type_error(method_name, arg_name, got_value, expected_type,
got_value=got_value, got_type=got_type))


def invalid_bool_error(got_value, macro_name) -> NoReturn:
"""Raise a CompilationException when an macro expects a boolean but gets some
other value.
"""
msg = ("Macro '{macro_name}' returns '{got_value}'. It is not type 'bool' "
"and cannot not be converted reliably to a bool.")
raise_compiler_error(msg.format(macro_name=macro_name, got_value=got_value))


def ref_invalid_args(model, args) -> NoReturn:
raise_compiler_error(
"ref() takes at most two arguments ({} given)".format(len(args)),
Expand Down
20 changes: 20 additions & 0 deletions core/dbt/task/test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from distutils.util import strtobool

from dataclasses import dataclass
from dbt import utils
from dbt.dataclass_schema import dbtClassMixin
Expand All @@ -19,6 +21,7 @@
from dbt.clients.jinja import MacroGenerator
from dbt.exceptions import (
InternalException,
invalid_bool_error,
missing_materialization
)
from dbt.graph import (
Expand All @@ -34,6 +37,23 @@ class TestResultData(dbtClassMixin):
should_warn: bool
should_error: bool

@classmethod
def validate(cls, data):
data['should_warn'] = cls.convert_bool_type(data['should_warn'])
data['should_error'] = cls.convert_bool_type(data['should_error'])
super().validate(data)

def convert_bool_type(field) -> bool:
# if it's type string let python decide if it's a valid value to convert to bool
if isinstance(field, str):
try:
return bool(strtobool(field)) # type: ignore
except ValueError:
raise invalid_bool_error(field, 'get_test_sql')

# need this so we catch both true bools and 0/1
return bool(field)


class TestRunner(CompileRunner):
def describe_node(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% macro get_test_sql(main_sql, fail_calc, warn_if, error_if, limit) -%}
select
{{ fail_calc }} as failures,
case when {{ fail_calc }} {{ warn_if }} then 1 else 0 end as should_warn,
case when {{ fail_calc }} {{ error_if }} then 1 else 0 end as should_error
from (
{{ main_sql }}
{{ "limit " ~ limit if limit != none }}
) dbt_internal_test
{%- endmacro %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% macro get_test_sql(main_sql, fail_calc, warn_if, error_if, limit) -%}
select
{{ fail_calc }} as failures,
case when {{ fail_calc }} {{ warn_if }} then 'x' else 'y' end as should_warn,
case when {{ fail_calc }} {{ error_if }} then 'x' else 'y' end as should_error
from (
{{ main_sql }}
{{ "limit " ~ limit if limit != none }}
) dbt_internal_test
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
select * from {{ ref('my_model_pass') }}
UNION ALL
select null as id
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
select 1 as id
UNION ALL
select null as id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
select * from {{ ref('my_model_pass') }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: 2

models:
- name: my_model_pass
description: "The table has 1 null values, and we're okay with that, until it's more than 1."
columns:
- name: id
description: "The number of responses for this favorite color - purple will be null"
tests:
- not_null:
error_if: '>1'
warn_if: '>1'

- name: my_model_warning
description: "The table has 1 null values, and we're okay with that, but let us know"
columns:
- name: id
description: "The number of responses for this favorite color - purple will be null"
tests:
- not_null:
error_if: '>1'

- name: my_model_failure
description: "The table has 2 null values, and we're not okay with that"
columns:
- name: id
description: "The number of responses for this favorite color - purple will be null"
tests:
- not_null:
error_if: '>1'

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
select 1 as id
UNION ALL
select null as id
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2

models:
- name: my_model
description: "The table has 1 null values, and we're not okay with that."
columns:
- name: id
description: "The number of responses for this favorite color - purple will be null"
tests:
- not_null


173 changes: 172 additions & 1 deletion test/integration/008_schema_tests_test/test_schema_v2_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,178 @@ def test_postgres_limit_schema_tests(self):
self.assertEqual(sum(x.failures for x in test_results), 3)


class TestDefaultBoolType(DBTIntegrationTest):
# test with default True/False in get_test_sql macro

def setUp(self):
DBTIntegrationTest.setUp(self)

@property
def schema(self):
return "schema_tests_008"

@property
def models(self):
return "models-v2/override_get_test_models"

def run_schema_validations(self):
args = FakeArgs()
test_task = TestTask(args, self.config)
return test_task.run()

def assertTestFailed(self, result):
self.assertEqual(result.status, "fail")
self.assertFalse(result.skipped)
self.assertTrue(
result.failures > 0,
'test {} did not fail'.format(result.node.name)
)

def assertTestWarn(self, result):
self.assertEqual(result.status, "warn")
self.assertFalse(result.skipped)
self.assertTrue(
result.failures > 0,
'test {} passed without expected warning'.format(result.node.name)
)

def assertTestPassed(self, result):
self.assertEqual(result.status, "pass")
self.assertFalse(result.skipped)
self.assertEqual(
result.failures, 0,
'test {} failed'.format(result.node.name)
)

@use_profile('postgres')
def test_postgres_limit_schema_tests(self):
results = self.run_dbt()
self.assertEqual(len(results), 3)
test_results = self.run_schema_validations()
self.assertEqual(len(test_results), 3)

for result in test_results:
# assert that all deliberately failing tests actually fail
if 'failure' in result.node.name:
self.assertTestFailed(result)
# assert that tests with warnings have them
elif 'warning' in result.node.name:
self.assertTestWarn(result)
# assert that actual tests pass
else:
self.assertTestPassed(result)
# warnings are also marked as failures
self.assertEqual(sum(x.failures for x in test_results), 3)


class TestOtherBoolType(DBTIntegrationTest):
# test with expected 0/1 in custom get_test_sql macro

def setUp(self):
DBTIntegrationTest.setUp(self)

@property
def schema(self):
return "schema_tests_008"

@property
def models(self):
return "models-v2/override_get_test_models"

@property
def project_config(self):
return {
'config-version': 2,
"macro-paths": ["macros-v2/override_get_test_macros"],
}

def run_schema_validations(self):
args = FakeArgs()
test_task = TestTask(args, self.config)
return test_task.run()

def assertTestFailed(self, result):
self.assertEqual(result.status, "fail")
self.assertFalse(result.skipped)
self.assertTrue(
result.failures > 0,
'test {} did not fail'.format(result.node.name)
)

def assertTestWarn(self, result):
self.assertEqual(result.status, "warn")
self.assertFalse(result.skipped)
self.assertTrue(
result.failures > 0,
'test {} passed without expected warning'.format(result.node.name)
)

def assertTestPassed(self, result):
self.assertEqual(result.status, "pass")
self.assertFalse(result.skipped)
self.assertEqual(
result.failures, 0,
'test {} failed'.format(result.node.name)
)

@use_profile('postgres')
def test_postgres_limit_schema_tests(self):
results = self.run_dbt()
self.assertEqual(len(results), 3)
test_results = self.run_schema_validations()
self.assertEqual(len(test_results), 3)

for result in test_results:
# assert that all deliberately failing tests actually fail
if 'failure' in result.node.name:
self.assertTestFailed(result)
# assert that tests with warnings have them
elif 'warning' in result.node.name:
self.assertTestWarn(result)
# assert that actual tests pass
else:
self.assertTestPassed(result)
# warnings are also marked as failures
self.assertEqual(sum(x.failures for x in test_results), 3)


class TestNonBoolType(DBTIntegrationTest):
# test with invalid 'x'/'y' in custom get_test_sql macro
def setUp(self):
DBTIntegrationTest.setUp(self)

@property
def schema(self):
return "schema_tests_008"

@property
def models(self):
return "models-v2/override_get_test_models_fail"

@property
def project_config(self):
return {
'config-version': 2,
"macro-paths": ["macros-v2/override_get_test_macros_fail"],
}

def run_schema_validations(self):
args = FakeArgs()

test_task = TestTask(args, self.config)
return test_task.run()

@use_profile('postgres')
def test_postgres_limit_schema_tests(self):
results = self.run_dbt()
self.assertEqual(len(results), 1)
run_result = self.run_dbt(['test'], expect_pass=False)
results = run_result.results
self.assertEqual(len(results), 1)
self.assertEqual(results[0].status, TestStatus.Error)
self.assertRegex(results[0].message, r"'get_test_sql' returns 'x'")


class TestMalformedSchemaTests(DBTIntegrationTest):

def setUp(self):
Expand Down Expand Up @@ -621,4 +793,3 @@ def test_postgres_wrong_specification_block(self):

assert len(results) == 1
assert results[0] == '{"name": "some_seed", "description": ""}'

0 comments on commit eace5b7

Please sign in to comment.