Skip to content
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

dbt-materialize: Store Failures As #23025

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'] %}
morsapaes marked this conversation as resolved.
Show resolved Hide resolved
{{ 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)