Skip to content

Commit e3b709a

Browse files
committed
dbt-materialize: support enforcing contracts for pseudo-types
1 parent 47e5f38 commit e3b709a

File tree

8 files changed

+103
-2
lines changed

8 files changed

+103
-2
lines changed

misc/dbt-materialize/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# dbt-materialize Changelog
22

3+
## Unreleased
4+
5+
* Support enforcing model contracts for the [`map`](https://materialize.com/docs/sql/types/map/),
6+
[`list`](https://materialize.com/docs/sql/types/list/),
7+
and [`record`](https://materialize.com/docs/sql/types/record/) pseudo-types.
8+
39
## 1.7.8 - 2024-05-06
410

511
* Fix permission management in blue/green automation macros for non-admin users

misc/dbt-materialize/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pip install dbt-materialize # install the adapter
1919
## Requirements
2020

2121
<!-- If you update this, bump the constraint in connections.py too. -->
22-
`dbt-materialize` requires Materialize v0.68.0+.
22+
`dbt-materialize` requires Materialize v0.100.0+.
2323

2424
## Configuring your profile
2525

misc/dbt-materialize/dbt/adapters/materialize/connections.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from dbt.semver import versions_compatible
2727

2828
# If you bump this version, bump it in README.md too.
29-
SUPPORTED_MATERIALIZE_VERSIONS = ">=0.68.0"
29+
SUPPORTED_MATERIALIZE_VERSIONS = ">=0.100.0"
3030

3131
logger = AdapterLogger("Materialize")
3232

misc/dbt-materialize/dbt/include/materialize/macros/columns.sql

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,28 @@
2222
{{ select_sql }}
2323
) as __dbt_sbq
2424
{% endmacro %}
25+
26+
{% macro materialize__get_empty_schema_sql(columns) %}
27+
{%- set col_err = [] -%}
28+
{%- set col_naked_numeric = [] -%}
29+
select
30+
{% for i in columns %}
31+
{%- set col = columns[i] -%}
32+
{%- if col['data_type'] is not defined -%}
33+
{%- do col_err.append(col['name']) -%}
34+
{#-- If this column's type is just 'numeric' then it is missing precision/scale, raise a warning --#}
35+
{%- elif col['data_type'].strip().lower() in ('numeric', 'decimal', 'number') -%}
36+
{%- do col_naked_numeric.append(col['name']) -%}
37+
{%- endif -%}
38+
{% set col_name = adapter.quote(col['name']) if col.get('quote') else col['name'] %}
39+
-- NOTE(morsapaes): in a future release of dbt, the default macro will use
40+
-- the global cast macro, as modified here, and we can remove this custom
41+
-- override. See: https://github.com/dbt-labs/dbt-adapters/pull/165
42+
{{ cast("null", col['data_type']) }} as {{ col_name }}{{ ", " if not loop.last }}
43+
{%- endfor -%}
44+
{%- if (col_err | length) > 0 -%}
45+
{{ exceptions.column_type_missing(column_names=col_err) }}
46+
{%- elif (col_naked_numeric | length) > 0 -%}
47+
{{ exceptions.warn("Detected columns with numeric type and unspecified precision/scale, this can lead to unintended rounding: " ~ col_naked_numeric ~ "`") }}
48+
{%- endif -%}
49+
{% endmacro %}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- Copyright Materialize, Inc. and contributors. All rights reserved.
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License in the LICENSE file at the
6+
-- root of this repository, or online at
7+
--
8+
-- http://www.apache.org/licenses/LICENSE-2.0
9+
--
10+
-- Unless required by applicable law or agreed to in writing, software
11+
-- distributed under the License is distributed on an "AS IS" BASIS,
12+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
-- See the License for the specific language governing permissions and
14+
-- limitations under the License.
15+
16+
{% macro materialize__cast(expression, data_type) -%}
17+
{#-- Handle types that don't support cast(NULL as type) --#}
18+
{%- if expression.strip().lower() == "null" and data_type.strip().lower() == "map" -%}
19+
MAP[NULL=>NULL]
20+
{%- elif expression.strip().lower() == "null" and data_type.strip().lower() == "list" -%}
21+
LIST[NULL]
22+
{%- elif expression.strip().lower() == "null" and data_type.strip().lower() == "record" -%}
23+
ROW(NULL)
24+
{%- else -%}
25+
cast({{ expression }} as {{ data_type }})
26+
{%- endif -%}
27+
{%- endmacro %}

misc/dbt-materialize/tests/adapter/fixtures.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,25 @@
299299
- name: c
300300
data_type: string
301301
"""
302+
303+
contract_pseudo_types_yml = """
304+
version: 2
305+
models:
306+
- name: test_pseudo_types
307+
config:
308+
contract:
309+
enforced: true
310+
columns:
311+
- name: a
312+
data_type: map
313+
- name: b
314+
data_type: record
315+
- name: c
316+
data_type: list
317+
"""
318+
319+
test_pseudo_types = """
320+
{{ config(materialized='view') }}
321+
322+
SELECT MAP['a' => 1, 'b' => 2] AS a, ROW(1, 2) AS b, LIST[[1,2],[3]] AS c
323+
"""

misc/dbt-materialize/tests/adapter/test_constraints.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
from dbt.tests.util import run_dbt, run_sql_with_adapter
3333
from fixtures import (
3434
contract_invalid_cluster_schema_yml,
35+
contract_pseudo_types_yml,
3536
nullability_assertions_schema_yml,
3637
test_materialized_view,
38+
test_pseudo_types,
3739
test_view,
3840
)
3941

@@ -162,3 +164,18 @@ def test_materialize_drop_quickstart(self, project):
162164
run_dbt(["run", "--models", "contract_invalid_cluster"], expect_pass=True)
163165

164166
project.run_sql("CREATE CLUSTER quickstart SIZE = '1'")
167+
168+
169+
class TestContractMaterializeTypes:
170+
@pytest.fixture(scope="class")
171+
def models(self):
172+
return {
173+
"contract_pseudo_types.yml": contract_pseudo_types_yml,
174+
"contract_pseudo_types.sql": test_pseudo_types,
175+
}
176+
177+
# Pseudotypes in Materialize cannot be cast using the cast() function, so we
178+
# special-handle their NULL casting for contract validation.
179+
# See #17870: https://github.com/MaterializeInc/materialize/issues/17870
180+
def test_test_pseudo_types(self, project):
181+
run_dbt(["run", "--models", "contract_pseudo_types"], expect_pass=True)

misc/dbt-materialize/tests/adapter/test_utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from dbt.tests.adapter.utils.fixture_get_intervals_between import (
2424
models__test_get_intervals_between_yml,
2525
)
26+
from dbt.tests.adapter.utils.test_cast import BaseCast
2627
from dbt.tests.adapter.utils.test_cast_bool_to_text import BaseCastBoolToText
2728
from dbt.tests.adapter.utils.test_current_timestamp import BaseCurrentTimestampAware
2829
from dbt.tests.adapter.utils.test_date_spine import BaseDateSpine
@@ -96,6 +97,9 @@
9697
11 as expected
9798
"""
9899

100+
class TestCast(BaseCast):
101+
pass
102+
99103
# The `cast_bool_to_text` macro works as expected, but we must alter the test case
100104
# because set operation type conversions do not work properly.
101105
# See https://github.com/MaterializeInc/materialize/issues/3331

0 commit comments

Comments
 (0)