Skip to content

Commit 126d687

Browse files
github-actions[bot]jtcohen6MichelleArk
authored
Respect column quote config in model contracts (#7537) (#7858)
(cherry picked from commit 83d163a) Co-authored-by: Jeremy Cohen <[email protected]> Co-authored-by: Michelle Ark <[email protected]>
1 parent 83f0251 commit 126d687

File tree

9 files changed

+115
-34
lines changed

9 files changed

+115
-34
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: Respect column 'quote' config in model contracts
3+
time: 2023-05-06T19:18:13.351819+02:00
4+
custom:
5+
Author: jtcohen6
6+
Issue: "7370"

core/dbt/adapters/base/impl.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1345,7 +1345,8 @@ def render_raw_columns_constraints(cls, raw_columns: Dict[str, Dict[str, Any]])
13451345
rendered_column_constraints = []
13461346

13471347
for v in raw_columns.values():
1348-
rendered_column_constraint = [f"{v['name']} {v['data_type']}"]
1348+
col_name = cls.quote(v["name"]) if v.get("quote") else v["name"]
1349+
rendered_column_constraint = [f"{col_name} {v['data_type']}"]
13491350
for con in v.get("constraints", None):
13501351
constraint = cls._parse_column_constraint(con)
13511352
c = cls.process_parsed_constraint(constraint, cls.render_column_constraint)

core/dbt/adapters/sql/impl.py

+1
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def list_relations_without_caching(
197197
)
198198
return relations
199199

200+
@classmethod
200201
def quote(self, identifier):
201202
return '"{}"'.format(identifier)
202203

core/dbt/include/global_project/macros/adapters/columns.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
{%- if col['data_type'] is not defined -%}
5050
{{ col_err.append(col['name']) }}
5151
{%- endif -%}
52-
cast(null as {{ col['data_type'] }}) as {{ col['name'] }}{{ ", " if not loop.last }}
52+
{% set col_name = adapter.quote(col['name']) if col.get('quote') else col['name'] %}
53+
cast(null as {{ col['data_type'] }}) as {{ col_name }}{{ ", " if not loop.last }}
5354
{%- endfor -%}
5455
{%- if (col_err | length) > 0 -%}
5556
{{ exceptions.column_type_missing(column_names=col_err) }}

core/dbt/include/global_project/macros/materializations/models/table/create_table_as.sql

+13-4
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,24 @@
3636
);
3737
{%- endmacro %}
3838

39+
40+
{% macro default__get_column_names() %}
41+
{#- loop through user_provided_columns to get column names -#}
42+
{%- set user_provided_columns = model['columns'] -%}
43+
{%- for i in user_provided_columns %}
44+
{%- set col = user_provided_columns[i] -%}
45+
{%- set col_name = adapter.quote(col['name']) if col.get('quote') else col['name'] -%}
46+
{{ col_name }}{{ ", " if not loop.last }}
47+
{%- endfor -%}
48+
{% endmacro %}
49+
50+
3951
{% macro get_select_subquery(sql) %}
4052
{{ return(adapter.dispatch('get_select_subquery', 'dbt')(sql)) }}
4153
{% endmacro %}
4254

4355
{% macro default__get_select_subquery(sql) %}
44-
select
45-
{% for column in model['columns'] %}
46-
{{ column }}{{ ", " if not loop.last }}
47-
{% endfor %}
56+
select {{ adapter.dispatch('get_column_names', 'dbt')() }}
4857
from (
4958
{{ sql }}
5059
) as model_subq

plugins/postgres/dbt/include/postgres/macros/adapters.sql

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
{% if contract_config.enforced %}
1414
{{ get_assert_columns_equivalent(sql) }}
1515
{{ get_table_columns_and_constraints() }} ;
16-
insert into {{ relation }} {{ get_column_names() }}
16+
insert into {{ relation }} (
17+
{{ adapter.dispatch('get_column_names', 'dbt')() }}
18+
)
1719
{%- set sql = get_select_subquery(sql) %}
1820
{% else %}
1921
as
Original file line numberDiff line numberDiff line change
@@ -1,10 +0,0 @@
1-
{% macro get_column_names() %}
2-
{# loop through user_provided_columns to get column names #}
3-
{%- set user_provided_columns = model['columns'] -%}
4-
(
5-
{% for i in user_provided_columns %}
6-
{% set col = user_provided_columns[i] %}
7-
{{ col['name'] }} {{ "," if not loop.last }}
8-
{% endfor %}
9-
)
10-
{% endmacro %}

tests/adapter/dbt/tests/adapter/constraints/fixtures.py

+41-3
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,16 @@
251251
'2019-01-01' as date_day
252252
"""
253253

254+
255+
# 'from' is a reserved word, so it must be quoted
256+
my_model_with_quoted_column_name_sql = """
257+
select
258+
'blue' as {{ adapter.quote('from') }},
259+
1 as id,
260+
'2019-01-01' as date_day
261+
"""
262+
263+
254264
model_schema_yml = """
255265
version: 2
256266
models:
@@ -260,7 +270,6 @@
260270
enforced: true
261271
columns:
262272
- name: id
263-
quote: true
264273
data_type: integer
265274
description: hello
266275
constraints:
@@ -344,7 +353,6 @@
344353
enforced: true
345354
columns:
346355
- name: id
347-
quote: true
348356
data_type: integer
349357
description: hello
350358
constraints:
@@ -454,7 +462,6 @@
454462
expression: {schema}.foreign_key_model (id)
455463
columns:
456464
- name: id
457-
quote: true
458465
data_type: integer
459466
description: hello
460467
constraints:
@@ -490,6 +497,37 @@
490497
data_type: {data_type}
491498
"""
492499

500+
501+
model_quoted_column_schema_yml = """
502+
version: 2
503+
models:
504+
- name: my_model
505+
config:
506+
contract:
507+
enforced: true
508+
materialized: table
509+
constraints:
510+
- type: check
511+
# this one is the on the user
512+
expression: ("from" = 'blue')
513+
columns: [ '"from"' ]
514+
columns:
515+
- name: id
516+
data_type: integer
517+
description: hello
518+
constraints:
519+
- type: not_null
520+
tests:
521+
- unique
522+
- name: from # reserved word
523+
quote: true
524+
data_type: text
525+
constraints:
526+
- type: not_null
527+
- name: date_day
528+
data_type: text
529+
"""
530+
493531
model_contract_header_schema_yml = """
494532
version: 2
495533
models:

tests/adapter/dbt/tests/adapter/constraints/test_constraints.py

+47-14
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
my_model_incremental_wrong_name_sql,
2424
my_model_with_nulls_sql,
2525
my_model_incremental_with_nulls_sql,
26+
my_model_with_quoted_column_name_sql,
2627
model_schema_yml,
2728
model_fk_constraint_schema_yml,
2829
constrained_model_schema_yml,
30+
model_quoted_column_schema_yml,
2931
foreign_key_model_sql,
3032
my_model_wrong_order_depends_on_fk_sql,
3133
my_model_incremental_wrong_order_depends_on_fk_sql,
@@ -165,11 +167,12 @@ def test__constraints_correct_column_data_types(self, project, data_types):
165167

166168

167169
def _normalize_whitespace(input: str) -> str:
168-
return re.sub(r"\s+", " ", input).lower().strip()
170+
subbed = re.sub(r"\s+", " ", input)
171+
return re.sub(r"\s?([\(\),])\s?", r"\1", subbed).lower().strip()
169172

170173

171174
def _find_and_replace(sql, find, replace):
172-
sql_tokens = sql.split(" ")
175+
sql_tokens = sql.split()
173176
for idx in [n for n, x in enumerate(sql_tokens) if find in x]:
174177
sql_tokens[idx] = replace
175178
return " ".join(sql_tokens)
@@ -235,17 +238,12 @@ def test__constraints_ddl(self, project, expected_sql):
235238
# the name is not what we're testing here anyways and varies based on materialization
236239
# TODO: consider refactoring this to introspect logs instead
237240
generated_sql = read_file("target", "run", "test", "models", "my_model.sql")
238-
generated_sql_modified = _normalize_whitespace(generated_sql)
239-
generated_sql_generic = _find_and_replace(
240-
generated_sql_modified, "my_model", "<model_identifier>"
241-
)
241+
generated_sql_generic = _find_and_replace(generated_sql, "my_model", "<model_identifier>")
242242
generated_sql_generic = _find_and_replace(
243243
generated_sql_generic, "foreign_key_model", "<foreign_key_model_identifier>"
244244
)
245245

246-
expected_sql_check = _normalize_whitespace(expected_sql)
247-
248-
assert expected_sql_check == generated_sql_generic
246+
assert _normalize_whitespace(expected_sql) == _normalize_whitespace(generated_sql_generic)
249247

250248

251249
class BaseConstraintsRollback:
@@ -485,15 +483,50 @@ def test__model_constraints_ddl(self, project, expected_sql):
485483
# assert at least my_model was run - additional upstreams may or may not be provided to the test setup via models fixture
486484
assert len(results) >= 1
487485
generated_sql = read_file("target", "run", "test", "models", "my_model.sql")
488-
generated_sql_modified = _normalize_whitespace(generated_sql)
489-
generated_sql_generic = _find_and_replace(
490-
generated_sql_modified, "my_model", "<model_identifier>"
491-
)
486+
487+
generated_sql_generic = _find_and_replace(generated_sql, "my_model", "<model_identifier>")
492488
generated_sql_generic = _find_and_replace(
493489
generated_sql_generic, "foreign_key_model", "<foreign_key_model_identifier>"
494490
)
495-
assert _normalize_whitespace(expected_sql) == generated_sql_generic
491+
492+
assert _normalize_whitespace(expected_sql) == _normalize_whitespace(generated_sql_generic)
496493

497494

498495
class TestModelConstraintsRuntimeEnforcement(BaseModelConstraintsRuntimeEnforcement):
499496
pass
497+
498+
499+
class BaseConstraintQuotedColumn(BaseConstraintsRuntimeDdlEnforcement):
500+
@pytest.fixture(scope="class")
501+
def models(self):
502+
return {
503+
"my_model.sql": my_model_with_quoted_column_name_sql,
504+
"constraints_schema.yml": model_quoted_column_schema_yml,
505+
}
506+
507+
@pytest.fixture(scope="class")
508+
def expected_sql(self):
509+
return """
510+
create table <model_identifier> (
511+
id integer not null,
512+
"from" text not null,
513+
date_day text,
514+
check (("from" = 'blue'))
515+
) ;
516+
insert into <model_identifier> (
517+
id, "from", date_day
518+
)
519+
(
520+
select id, "from", date_day
521+
from (
522+
select
523+
'blue' as "from",
524+
1 as id,
525+
'2019-01-01' as date_day
526+
) as model_subq
527+
);
528+
"""
529+
530+
531+
class TestConstraintQuotedColumn(BaseConstraintQuotedColumn):
532+
pass

0 commit comments

Comments
 (0)