Skip to content

Commit d35b3d4

Browse files
committed
fix: implicit view schema changes with alter view approach
Signed-off-by: Can Bekleyici <can.bekleyici@deepl.com>
1 parent c02466f commit d35b3d4

8 files changed

Lines changed: 126 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
- Fix `dbt docs generate` failing with `RuntimeError: Tables contain columns with the same names ... but different types` during catalog merge across schemas ([#1392](https://github.com/databricks/dbt-databricks/issues/1392))
1919
- Fix column comments being permanently dropped from views when `view_update_via_alter` issues `ALTER VIEW AS`; reapply persisted column comments after the query update ([#1357](https://github.com/databricks/dbt-databricks/issues/1357))
2020

21+
### Under the Hood
22+
23+
- Views with enabled config `view_update_via_alter` now update the view query independent of diffs in the query definition. This ensures that changes in underlying tables are also reflected when using star select semantics. ([#1356](https://github.com/databricks/dbt-databricks/issues/1356))
24+
2125
## dbt-databricks 1.11.7 (Apr 17, 2026)
2226

2327
### Features

dbt/adapters/databricks/relation_configs/query.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ def get_diff(self, other: "QueryConfig") -> Optional["QueryConfig"]:
2222
return None
2323

2424

25+
class ViewQueryConfig(QueryConfig):
26+
def get_diff(self, other: "ViewQueryConfig") -> "ViewQueryConfig":
27+
# Return self so the view query always appears changed in the diff.
28+
# View query diffing is unreliable (star selects, upstream schema changes etc.)
29+
return self
30+
31+
2532
class QueryProcessor(DatabricksComponentProcessor[QueryConfig]):
2633
name: ClassVar[str] = "query"
2734

@@ -50,3 +57,15 @@ def from_relation_results(cls, result: RelationResults) -> QueryConfig:
5057
table = result["describe_extended"]
5158
row = next(x for x in table if x[0] == "View Text")
5259
return QueryConfig(query=SqlUtils.clean_sql(row[1]))
60+
61+
62+
class ViewQueryProcessor(QueryProcessor):
63+
@classmethod
64+
def from_relation_results(cls, result: RelationResults) -> ViewQueryConfig:
65+
base = super().from_relation_results(result)
66+
return ViewQueryConfig(query=base.query)
67+
68+
@classmethod
69+
def from_relation_config(cls, relation_config: RelationConfig) -> ViewQueryConfig:
70+
base = super().from_relation_config(relation_config)
71+
return ViewQueryConfig(query=base.query)

dbt/adapters/databricks/relation_configs/view.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
from typing import Optional
2-
31
from typing_extensions import Self
42

53
from dbt.adapters.databricks.logging import logger
64
from dbt.adapters.databricks.relation_configs.base import (
75
DatabricksRelationChangeSet,
86
DatabricksRelationConfigBase,
97
)
10-
from dbt.adapters.databricks.relation_configs.column_comments import ColumnCommentsProcessor
118
from dbt.adapters.databricks.relation_configs.column_tags import ColumnTagsProcessor
129
from dbt.adapters.databricks.relation_configs.comment import CommentProcessor
13-
from dbt.adapters.databricks.relation_configs.query import QueryProcessor
10+
from dbt.adapters.databricks.relation_configs.query import ViewQueryProcessor
1411
from dbt.adapters.databricks.relation_configs.tags import TagsProcessor
1512
from dbt.adapters.databricks.relation_configs.tblproperties import TblPropertiesProcessor
1613

@@ -19,15 +16,17 @@ class ViewConfig(DatabricksRelationConfigBase):
1916
config_components = [
2017
TagsProcessor,
2118
TblPropertiesProcessor,
22-
QueryProcessor,
19+
ViewQueryProcessor,
2320
CommentProcessor,
24-
ColumnCommentsProcessor,
2521
ColumnTagsProcessor,
2622
]
2723

28-
def get_changeset(self, existing: Self) -> Optional[DatabricksRelationChangeSet]:
24+
def get_changeset(self, existing: Self) -> DatabricksRelationChangeSet:
2925
changeset = super().get_changeset(existing)
30-
if changeset and "comment" in changeset.changes:
26+
if changeset is None:
27+
# ViewQueryProcessor always returns a diff, so this should be unreachable
28+
raise RuntimeError("Expected a non-empty changeset for a view relation")
29+
if "comment" in changeset.changes:
3130
logger.debug(
3231
"View description changed, requiring replace, as there is"
3332
" no API yet to update comments."

dbt/include/databricks/macros/materializations/view.sql

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,13 @@
1111
{% if existing_relation %}
1212
{% if relation_should_be_altered(existing_relation) %}
1313
{% set configuration_changes = get_configuration_changes(existing_relation) %}
14-
{% if configuration_changes and configuration_changes.changes %}
15-
{% if configuration_changes.requires_full_refresh %}
16-
{{ log('Using replace_with_view') }}
17-
{{ replace_with_view(existing_relation, target_relation) }}
18-
{% else %}
19-
{{ log('Using alter_view') }}
20-
{{ log(configuration_changes.changes) }}
21-
{{ alter_view(target_relation, configuration_changes.changes) }}
22-
{% endif %}
14+
{% if configuration_changes.requires_full_refresh %}
15+
{{ log('Using replace_with_view') }}
16+
{{ replace_with_view(existing_relation, target_relation) }}
2317
{% else %}
24-
{{ execute_no_op(target_relation) }}
18+
{{ log('Using alter_view') }}
19+
{{ log(configuration_changes.changes) }}
20+
{{ alter_view(target_relation, configuration_changes.changes) }}
2521
{% endif %}
2622
{% else %}
2723
{{ replace_with_view(existing_relation, target_relation) }}

dbt/include/databricks/macros/relations/view/alter.sql

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,4 @@
2323
{{ alter_column_comment(target_relation, columns_to_persist) }}
2424
{% endif %}
2525
{% endif %}
26-
{% if column_comments %}
27-
{{ alter_column_comments(target_relation, column_comments.comments) }}
28-
{% endif %}
2926
{% endmacro %}

tests/functional/adapter/views/fixtures.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,10 @@
5858
{{ config(materialized='view') }}
5959
select id from {{ ref('seed') }};
6060
"""
61+
62+
seed_with_extra_csv = """id,msg,extra
63+
1,hello,a
64+
2,goodbye,b
65+
2,yo,c
66+
3,anyway,d
67+
"""

tests/functional/adapter/views/test_views.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,24 @@ def test_view_update_query_preserves_column_comments(self, project):
104104
assert results[0][2] == "This is the id column"
105105

106106

107+
class BaseUpdateUpstreamSchema(BaseUpdateView):
108+
"""Test that a star-select view is re-applied when the upstream table gains a new column."""
109+
110+
def test_view_update_with_upstream_schema_change(self, project):
111+
util.run_dbt(["build"])
112+
util.write_file(fixtures.seed_with_extra_csv, "seeds", "seed.csv")
113+
util.run_dbt(["seed", "--full-refresh"])
114+
util.run_dbt(["run"])
115+
116+
results = project.run_sql(
117+
"describe {database}.{schema}.initial_view",
118+
fetch="all",
119+
)
120+
121+
# check that `extra` column was added, even though the view's sql definition hasn't changed
122+
assert any([col.col_name == "extra" for col in results])
123+
124+
107125
class BaseRemoveTags(BaseUpdateView):
108126
def test_view_update_remove_tags(self, project):
109127
util.run_dbt(["build"])
@@ -222,6 +240,22 @@ def project_config_update(self):
222240
}
223241

224242

243+
@pytest.mark.skip_profile("databricks_cluster")
244+
class TestUpdateViewViaAlterUpstreamSchema(BaseUpdateUpstreamSchema):
245+
@pytest.fixture(scope="class")
246+
def project_config_update(self):
247+
return {
248+
"flags": {"use_materialization_v2": True},
249+
"models": {
250+
"+view_update_via_alter": True,
251+
"+persist_docs": {
252+
"relation": True,
253+
"columns": True,
254+
},
255+
},
256+
}
257+
258+
225259
@pytest.mark.skip_profile("databricks_cluster")
226260
class TestUpdateViewViaAlterRemoveTags(BaseRemoveTags):
227261
@pytest.fixture(scope="class")

tests/unit/relation_configs/test_query.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
from agate import Row
55
from dbt.exceptions import DbtRuntimeError
66

7-
from dbt.adapters.databricks.relation_configs.query import QueryConfig, QueryProcessor
7+
from dbt.adapters.databricks.relation_configs.query import (
8+
QueryConfig,
9+
QueryProcessor,
10+
ViewQueryConfig,
11+
ViewQueryProcessor,
12+
)
813

914
sql = "select * from foo"
1015

@@ -50,3 +55,46 @@ def test_get_diff__different_query(self):
5055
}
5156
other = QueryProcessor.from_relation_results(results)
5257
assert model.get_diff(other) is model
58+
59+
60+
class TestViewQueryProcessor:
61+
def test_from_results(self):
62+
results = {"information_schema.views": Row([sql, "other"], ["view_definition", "comment"])}
63+
spec = ViewQueryProcessor.from_relation_results(results)
64+
assert spec == ViewQueryConfig(query=sql)
65+
66+
def test_from_model_node__with_query(self):
67+
model = Mock()
68+
model.compiled_code = sql
69+
spec = ViewQueryProcessor.from_relation_config(model)
70+
assert spec == ViewQueryConfig(query=sql)
71+
72+
def test_from_model_node__without_query(self):
73+
model = Mock()
74+
model.compiled_code = None
75+
model.identifier = "1"
76+
with pytest.raises(
77+
DbtRuntimeError,
78+
match="Cannot compile model 1 with no SQL query",
79+
):
80+
_ = ViewQueryProcessor.from_relation_config(model)
81+
82+
def test_get_diff__always_returns_self_for_identical_query(self):
83+
model = ViewQueryConfig(query="select * from foo")
84+
results = {
85+
"information_schema.views": Row(
86+
["(\nselect * from foo\n)", "other"], ["view_definition", "comment"]
87+
)
88+
}
89+
other = ViewQueryProcessor.from_relation_results(results)
90+
assert model.get_diff(other) is model
91+
92+
def test_get_diff__always_returns_self_for_different_query(self):
93+
model = ViewQueryConfig(query="select * from foo")
94+
results = {
95+
"information_schema.views": Row(
96+
["(\nselect * from bar\n)", "other"], ["view_definition", "comment"]
97+
)
98+
}
99+
other = ViewQueryProcessor.from_relation_results(results)
100+
assert model.get_diff(other) is model

0 commit comments

Comments
 (0)