Skip to content

Commit

Permalink
dbt-materialize: store failures as (#23025)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: morsapaes <marta.paes.moreira@gmail.com>
  • Loading branch information
dehume and morsapaes authored Nov 20, 2023
1 parent 2f6d505 commit d7f5b00
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 10 deletions.
4 changes: 2 additions & 2 deletions doc/user/content/manage/dbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,8 @@ That's it! From here on, Materialize makes sure that your models are **increment

## Test and document a dbt project

[//]: # "TODO(morsapaes) Call out the cluster configuration for tests once this
page is rehashed."
[//]: # "TODO(morsapaes) Call out the cluster configuration for tests and
store_failures_as once this page is rehashed."
### Configure continuous testing
Expand Down
29 changes: 29 additions & 0 deletions misc/dbt-materialize/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

## Unreleased

* Support specifying the materialization type used to store test failures via
the new [`store_failures_as` configuration](https://docs.getdbt.com/reference/resource-configs/store_failures_as).
Accepted values: `materialized_view` (default), `view`, `ephemeral`.

* **Project level**
```yaml
tests:
my_project:
+store_failures_as: view
```
* **Model level**
```yaml
models:
- name: my_model
columns:
- name: id
tests:
- not_null:
config:
store_failures_as: view
- unique:
config:
store_failures_as: ephemeral
```
If both [`store_failures`](https://docs.getdbt.com/reference/resource-configs/store_failures)
and `store_failures_as` are specified, `store_failures_as` takes precedence.

* Mark `dbt source freshness` as not supported. Materialize supports the
functionality required to enable column- and metadata-based source freshness
checks, but the value of this feature in a real-time data warehouse is
Expand Down
12 changes: 8 additions & 4 deletions misc/dbt-materialize/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,14 @@ as well as [`--persist-docs`](https://docs.getdbt.com/reference/resource-configs

[`dbt test`](https://docs.getdbt.com/reference/commands/test) is supported.

If you set the optional `--store-failures` flag or [`store-failures` config](https://docs.getdbt.com/reference/resource-configs/store_failures),
dbt will save the results of a test query to a `materialized_view`. These will
be created in a schema suffixed or named `dbt_test__audit` by default. Change
this value by setting a `schema` config.
If you set the optional [`--store-failures` flag or `store-failures` config](https://docs.getdbt.com/reference/resource-configs/store_failures),
dbt will save the results of a test query to a `materialized_view`. To use a
`view` instead, use the [`store_failures_as` config](https://docs.getdbt.com/reference/resource-configs/store_failures_as).

These objects will be created in a schema suffixed or named `dbt_test__audit` by
default. Change this value by setting a `schema` config. If both
[`store_failures`](https://docs.getdbt.com/reference/resource-configs/store_failures) and
`store_failures_as` are specified, `store_failures_as` takes precedence.

### Snapshots

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,38 @@

{% set relations = [] %}

-- For an overview of the precedence logic behind store_failures and
-- store_failures_at, see dbt-core #8653.
{% if should_store_failures() %}

{% set identifier = model['alias'] %}
{% set old_relation = adapter.get_relation(database=database, schema=schema, identifier=identifier) %}

{% set store_failures_as = config.get('store_failures_as') %}
{% if store_failures_as == none %}{% set store_failures_as = 'table' %}{% endif %}
{% if store_failures_as not in ['table', 'view', 'materialized_view'] %}
{{ exceptions.raise_compiler_error(
"'" ~ store_failures_as ~ "' is not a valid value for `store_failures_as`. "
"Accepted values are: ['ephemeral', 'table', 'view', 'materialized_view']"
) }}
{% endif %}

{% set target_relation = api.Relation.create(
identifier=identifier, schema=schema, database=database, type='materializedview') -%} %}
identifier=identifier, schema=schema, database=database, type=store_failures_as) -%} %}

{% if old_relation %}
{% do adapter.drop_relation(old_relation) %}
{% endif %}

{% call statement(auto_begin=True) %}
{{ materialize__create_materialized_view_as(target_relation, sql) }}
{% endcall %}
{% if store_failures_as == 'view' %}
{% call statement(auto_begin=True) %}
{{ materialize__create_view_as(target_relation, sql) }}
{% endcall %}
{% else %}
{% call statement(auto_begin=True) %}
{{ materialize__create_materialized_view_as(target_relation, sql) }}
{% endcall %}
{% endif %}

{% do relations.append(target_relation) %}

Expand Down
142 changes: 142 additions & 0 deletions misc/dbt-materialize/tests/adapter/test_store_test_failures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright Materialize, Inc. and contributors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the LICENSE file at the
# root of this repository, or online at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest
from dbt.contracts.results import TestStatus
from dbt.tests.adapter.store_test_failures_tests.basic import (
StoreTestFailuresAsBase,
StoreTestFailuresAsExceptions,
StoreTestFailuresAsGeneric,
StoreTestFailuresAsInteractions,
StoreTestFailuresAsProjectLevelEphemeral,
StoreTestFailuresAsProjectLevelOff,
StoreTestFailuresAsProjectLevelView,
TestResult,
)
from dbt.tests.adapter.store_test_failures_tests.fixtures import (
models__file_model_but_with_a_no_good_very_long_name,
models__fine_model,
)
from dbt.tests.adapter.store_test_failures_tests.test_store_test_failures import (
StoreTestFailuresBase,
)
from dbt.tests.util import run_dbt

TEST__MATERIALIZED_VIEW_TRUE = """
{{ config(store_failures_as="materialized_view", store_failures=True) }}
select *
from {{ ref('chipmunks') }}
where shirt = 'green'
"""


TEST__MATERIALIZED_VIEW_FALSE = """
{{ config(store_failures_as="materialized_view", store_failures=False) }}
select *
from {{ ref('chipmunks') }}
where shirt = 'green'
"""


TEST__MATERIALIZED_VIEW_UNSET = """
{{ config(store_failures_as="materialized_view") }}
select *
from {{ ref('chipmunks') }}
where shirt = 'green'
"""


class TestStoreTestFailures(StoreTestFailuresBase):
@pytest.fixture(scope="class")
def models(self):
return {
"fine_model.sql": models__fine_model,
"fine_model_but_with_a_no_good_very_long_name.sql": models__file_model_but_with_a_no_good_very_long_name,
}


class TestMaterializeStoreTestFailures(TestStoreTestFailures):
pass


class TestStoreTestFailuresAsInteractions(StoreTestFailuresAsInteractions):
pass


class TestStoreTestFailuresAsProjectLevelOff(StoreTestFailuresAsProjectLevelOff):
pass


class TestStoreTestFailuresAsProjectLevelView(StoreTestFailuresAsProjectLevelView):
pass


class TestStoreTestFailuresAsGeneric(StoreTestFailuresAsGeneric):
pass


class TestStoreTestFailuresAsProjectLevelEphemeral(
StoreTestFailuresAsProjectLevelEphemeral
):
pass


class TestStoreTestFailuresAsExceptions(StoreTestFailuresAsExceptions):
def test_tests_run_unsuccessfully_and_raise_appropriate_exception(self, project):
results = run_dbt(["test"], expect_pass=False)
assert len(results) == 1
result = results[0]
assert "Compilation Error" in result.message
assert "'error' is not a valid value" in result.message
assert (
"Accepted values are: ['ephemeral', 'table', 'view', 'materialized_view']"
in result.message
)


class TestStoreTestFailuresAsProjectLevelMaterializeView(StoreTestFailuresAsBase):
"""
These scenarios test that `store_failures_as` at the project level takes precedence over `store_failures`
at the model level.
Test Scenarios:
- If `store_failures_as = "materialized_view"` in the project and `store_failures = False` in the model,
then store the failures in a materialized view.
- If `store_failures_as = "materialized_view"` in the project and `store_failures = True` in the model,
then store the failures in a materialized view.
- If `store_failures_as = "materialized_view"` in the project and `store_failures` is not set,
then store the failures in a materialized view.
"""

@pytest.fixture(scope="class")
def tests(self):
return {
"results_true.sql": TEST__MATERIALIZED_VIEW_TRUE,
"results_false.sql": TEST__MATERIALIZED_VIEW_FALSE,
"results_unset.sql": TEST__MATERIALIZED_VIEW_UNSET,
}

@pytest.fixture(scope="class")
def project_config_update(self):
return {"tests": {"store_failures_as": "materialized_view"}}

def test_tests_run_successfully_and_are_stored_as_expected(self, project):
expected_results = {
TestResult("results_true", TestStatus.Fail, "materialized_view"),
TestResult("results_false", TestStatus.Fail, "materialized_view"),
TestResult("results_unset", TestStatus.Fail, "materialized_view"),
}
self.run_and_assert(project, expected_results)

0 comments on commit d7f5b00

Please sign in to comment.