Skip to content

Commit 1b5b22d

Browse files
committed
feat: Add SQL Server view materialization no-op optimization
Add get_view_definition_sql metadata macro for SQL Server views Detect unchanged view definition and skip rebuild by using a no-op statement Add test ensuring rerunning an unchanged view does not alter modify_date
1 parent a47f6a5 commit 1b5b22d

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

dbt/include/sqlserver/macros/adapters/metadata.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@
9090
{{ return(load_result('get_relation_without_caching').table) }}
9191
{% endmacro %}
9292

93+
{% macro get_view_definition_sql(relation) %}
94+
{{ return(adapter.dispatch('get_view_definition_sql')(relation)) }}
95+
{% endmacro %}
96+
97+
{% macro sqlserver__get_view_definition_sql(relation) -%}
98+
{{ get_use_database_sql(relation.database) }}
99+
select object_definition(v.object_id) as definition
100+
from sys.views as v {{ information_schema_hints() }}
101+
inner join sys.schemas as s {{ information_schema_hints() }}
102+
on v.schema_id = s.schema_id
103+
where upper(s.name) = upper('{{ relation.schema }}')
104+
and upper(v.name) = upper('{{ relation.identifier }}')
105+
{% endmacro %}
106+
93107
{% macro sqlserver__get_relation_last_modified(information_schema, relations) -%}
94108
{%- call statement('last_modified', fetch_result=True) -%}
95109
select

dbt/include/sqlserver/macros/materializations/models/view/view.sql

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,29 @@
2727
-- grab current tables grants config for comparision later on
2828
{% set grant_config = config.get('grants') %}
2929
{% set preserved_grants = {} %}
30+
{% set should_skip_view_update = false %}
31+
{% set build_sql = none %}
3032

3133
{% if existing_relation is not none and existing_relation.type != 'view' %}
3234
{% set current_grants_table = run_query(get_show_grant_sql(existing_relation)) %}
3335
{% set current_grants_dict = adapter.standardize_grants_dict(current_grants_table) %}
3436
{% set preserved_grants = diff_of_two_dicts(current_grants_dict, grant_config) %}
37+
{% set build_sql = get_create_view_as_sql(intermediate_relation, sql) %}
38+
{% elif existing_relation is not none and existing_relation.type == 'view' %}
39+
{% set current_view_definition_table = run_query(get_view_definition_sql(existing_relation)) %}
40+
{% if current_view_definition_table is not none and current_view_definition_table.rows | length > 0 %}
41+
{% set normalized_relation = target_relation.include(database=False) | lower | replace('\n', '') | replace('\r', '') | replace('\t', '') | replace(' ', '') | replace(';', '') %}
42+
{% set normalized_sql = sql | lower | replace('\n', '') | replace('\r', '') | replace('\t', '') | replace(' ', '') | replace(';', '') %}
43+
{% set normalized_definition = current_view_definition_table.rows[0][0] | lower | replace('\n', '') | replace('\r', '') | replace('\t', '') | replace(' ', '') | replace(';', '') %}
44+
{% set should_skip_view_update = normalized_definition.endswith(normalized_sql) %}
45+
{% endif %}
46+
{% if should_skip_view_update %}
47+
{% set build_sql = 'declare @dbt_sqlserver_noop int;' %}
48+
{% else %}
49+
{% set build_sql = get_create_view_as_sql(target_relation, sql) %}
50+
{% endif %}
51+
{% else %}
52+
{% set build_sql = get_create_view_as_sql(target_relation, sql) %}
3553
{% endif %}
3654

3755
{{ run_hooks(pre_hooks, inside_transaction=False) }}
@@ -46,7 +64,7 @@
4664
{% if existing_relation is not none and existing_relation.type != 'view' %}
4765
-- build model
4866
{% call statement('main') -%}
49-
{{ get_create_view_as_sql(intermediate_relation, sql) }}
67+
{{ build_sql }}
5068
{%- endcall %}
5169

5270
-- cleanup
@@ -60,7 +78,7 @@
6078
{% else %}
6179
-- build model
6280
{% call statement('main') -%}
63-
{{ get_create_view_as_sql(target_relation, sql) }}
81+
{{ build_sql }}
6482
{%- endcall %}
6583
{% endif %}
6684

tests/functional/adapter/mssql/test_materialize_change.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,44 @@ def test_public_select_grant_survives_swap(self, project):
110110
assert ("PUBLIC", "SELECT") in [(row[0].upper(), row[1].upper()) for row in grants]
111111

112112

113+
class TestViewMaterializationNoOp(BaseTableView):
114+
"""Test that rerunning an unchanged view avoids altering the view."""
115+
116+
@pytest.fixture(scope="class")
117+
def models(self):
118+
return {"mat_object.sql": view_mat, "schema.yml": schema}
119+
120+
def test_unchanged_view_does_not_alter(self, project):
121+
self.create_object(project, f"CREATE VIEW {project.test_schema}.mat_object AS {model_sql}")
122+
123+
before_modify_date = project.run_sql(
124+
f"""
125+
select modify_date
126+
from sys.objects o
127+
join sys.schemas s on o.schema_id = s.schema_id
128+
where upper(s.name) = upper('{project.test_schema}')
129+
and upper(o.name) = upper('mat_object')
130+
""",
131+
fetch="one",
132+
)[0]
133+
134+
results = run_dbt(["run"])
135+
assert len(results) == 1
136+
137+
after_modify_date = project.run_sql(
138+
f"""
139+
select modify_date
140+
from sys.objects o
141+
join sys.schemas s on o.schema_id = s.schema_id
142+
where upper(s.name) = upper('{project.test_schema}')
143+
and upper(o.name) = upper('mat_object')
144+
""",
145+
fetch="one",
146+
)[0]
147+
148+
assert after_modify_date == before_modify_date
149+
150+
113151
class TestViewtoTable(BaseTableView):
114152
"""Test if changing from a view object to a table object correctly replaces"""
115153

0 commit comments

Comments
 (0)