Repository: databricks/dbt-databricks
Pull request #959 (2025-03-12 21:48:40)

View V2 Testing
### Description

Makes small modifications to Views V2 to make it more testable and added unit tests of the macros.  Interestingly, I tried to update column comments with the same methodology as the other changes and backed it out because altering the query blows comments away.  This means that if I modify the query, I need to full set of comments, not just the changes. As such, I'm sticking with persist_docs and running after the query has potentially been updated.

### Checklist

- [x] I have run this code in development and it appears to resolve the stated issue
- [x] This PR includes tests, or tests are not required/relevant for this PR
- [ ] I have updated the `CHANGELOG.md` and added information about my change to the "dbt-databricks next" section.



Patch:
diff --git a/dbt/adapters/databricks/relation_configs/query.py b/dbt/adapters/databricks/relation_configs/query.py
index e9b8f73b..651bb82e 100644
--- a/dbt/adapters/databricks/relation_configs/query.py
+++ b/dbt/adapters/databricks/relation_configs/query.py
@@ -1,4 +1,4 @@
-from typing import ClassVar
+from typing import ClassVar, Optional
 
 from dbt_common.exceptions import DbtRuntimeError
 
@@ -15,14 +15,21 @@ class QueryConfig(DatabricksComponentConfig):
 
     query: str
 
+    def get_diff(self, other: "QueryConfig") -> Optional["QueryConfig"]:
+        if self.query.strip() != other.query.strip():
+            return self
+        return None
+
 
 class QueryProcessor(DatabricksComponentProcessor[QueryConfig]):
     name: ClassVar[str] = "query"
 
     @classmethod
     def from_relation_results(cls, result: RelationResults) -> QueryConfig:
-        row = result["information_schema.views"]
-        return QueryConfig(query=row["view_definition"])
+        view_definition = result["information_schema.views"]["view_definition"].strip()
+        if view_definition[0] == "(" and view_definition[-1] == ")":
+            view_definition = view_definition[1:-1]
+        return QueryConfig(query=view_definition.strip())
 
     @classmethod
     def from_relation_config(cls, relation_config: RelationConfig) -> QueryConfig:
diff --git a/dbt/adapters/databricks/relation_configs/tags.py b/dbt/adapters/databricks/relation_configs/tags.py
index 461fa77b..1e46c990 100644
--- a/dbt/adapters/databricks/relation_configs/tags.py
+++ b/dbt/adapters/databricks/relation_configs/tags.py
@@ -22,7 +22,7 @@ class TagsConfig(DatabricksComponentConfig):
         for k in other.set_tags.keys():
             if k not in self.set_tags:
                 to_unset.append(k)
-        if self.set_tags or to_unset:
+        if self.set_tags != other.set_tags or to_unset:
             return TagsConfig(set_tags=self.set_tags, unset_tags=to_unset)
         return None
 
diff --git a/dbt/include/databricks/macros/materializations/materialized_view.sql b/dbt/include/databricks/macros/materializations/materialized_view.sql
index 82b556df..8fbc5b2b 100644
--- a/dbt/include/databricks/macros/materializations/materialized_view.sql
+++ b/dbt/include/databricks/macros/materializations/materialized_view.sql
@@ -7,7 +7,7 @@
     {% set build_sql = materialized_view_get_build_sql(existing_relation, target_relation) %}
 
     {% if build_sql == '' %}
-        {{ materialized_view_execute_no_op(target_relation) }}
+        {{ execute_no_op(target_relation) }}
     {% else %}
         {{ materialized_view_execute_build_sql(build_sql, existing_relation, target_relation, post_hooks) }}
     {% endif %}
@@ -18,7 +18,6 @@
 
 {% endmaterialization %}
 
-
 {% macro materialized_view_get_build_sql(existing_relation, target_relation) %}
 
     {% set full_refresh_mode = should_full_refresh() %}
@@ -58,16 +57,6 @@
 {% endmacro %}
 
 
-{% macro materialized_view_execute_no_op(target_relation) %}
-    {% do store_raw_result(
-        name="main",
-        message="skip " ~ target_relation,
-        code="skip",
-        rows_affected="-1"
-    ) %}
-{% endmacro %}
-
-
 {% macro materialized_view_execute_build_sql(build_sql, existing_relation, target_relation, post_hooks) %}
 
     -- `BEGIN` happens here:
diff --git a/dbt/include/databricks/macros/materializations/streaming_table.sql b/dbt/include/databricks/macros/materializations/streaming_table.sql
index d6468e4b..8e9221a3 100644
--- a/dbt/include/databricks/macros/materializations/streaming_table.sql
+++ b/dbt/include/databricks/macros/materializations/streaming_table.sql
@@ -7,7 +7,7 @@
 {% set build_sql = streaming_table_get_build_sql(existing_relation, target_relation) %}
 
     {% if build_sql == '' %}
-        {{ streaming_table_execute_no_op(target_relation) }}
+        {{ execute_no_op(target_relation) }}
     {% else %}
         {{ streaming_table_execute_build_sql(build_sql, existing_relation, target_relation, post_hooks) }}
     {% endif %}
@@ -56,17 +56,6 @@
 
 {% endmacro %}
 
-
-{% macro streaming_table_execute_no_op(target_relation) %}
-    {% do store_raw_result(
-        name="main",
-        message="skip " ~ target_relation,
-        code="skip",
-        rows_affected="-1"
-    ) %}
-{% endmacro %}
-
-
 {% macro streaming_table_execute_build_sql(build_sql, existing_relation, target_relation, post_hooks) %}
 
     -- `BEGIN` happens here:
diff --git a/dbt/include/databricks/macros/materializations/view.sql b/dbt/include/databricks/macros/materializations/view.sql
index 787114f8..10206ea5 100644
--- a/dbt/include/databricks/macros/materializations/view.sql
+++ b/dbt/include/databricks/macros/materializations/view.sql
@@ -8,17 +8,18 @@
   {% if adapter.behavior.use_materialization_v2 %}
     {{ run_pre_hooks() }}
     {% if existing_relation %}
-      {% set update_via_alter = config.get('view_update_via_alter', False) | as_bool %}
-      {% if existing_relation.is_view and update_via_alter %}
-        {% if target_relation.is_hive_metastore() %}
-          {{ exceptions.raise_compiler_error("Cannot update a view in the Hive metastore via ALTER VIEW. Please set `view_update_via_alter: false` in your model configuration.") }}
-        {% endif %}
+      {% if relation_should_be_altered(existing_relation) %}
         {% set configuration_changes = get_configuration_changes(existing_relation) %}
-        {% if configuration_changes and not configuration_changes.requires_full_refresh %}
-          {{ alter_view(target_relation, configuration_changes) }}
-        {% else %}
-          {{ replace_with_view(existing_relation, target_relation) }}
+        {% if configuration_changes and configuration_changes.changes %}
+          {% if configuration_changes.requires_full_refresh %}
+            {{ replace_with_view(existing_relation, target_relation) }}
+          {% else %}
+            {{ alter_view(target_relation, configuration_changes.changes) }}
+          {% endif %}
         {% endif %}
+        {# This is to satisfy dbt as there are no changes needed here #}
+        {{ execute_no_op(target_relation) }}
+        {{ persist_docs(target_relation, model, for_relation=False) }}
       {% else %}
         {{ replace_with_view(existing_relation, target_relation) }}
       {% endif %}
@@ -26,6 +27,7 @@
       {% call statement('main') -%}
         {{ get_create_view_as_sql(target_relation, sql) }}
       {%- endcall %}
+      {{ apply_tags(target_relation, tags) }}
     {% endif %}
     {% set should_revoke = should_revoke(exists_as_view, full_refresh_mode=True) %}
     {% do apply_grants(target_relation, grant_config, should_revoke=True) %}
@@ -63,4 +65,15 @@
   {% set tags = config.get('databricks_tags') %}
   {{ execute_multiple_statements(get_replace_sql(existing_relation, target_relation, sql)) }}
   {%- do apply_tags(target_relation, tags) -%}
+{% endmacro %}
+
+{% macro relation_should_be_altered(existing_relation) %}
+  {% set update_via_alter = config.get('view_update_via_alter', False) | as_bool %}
+  {% if existing_relation.is_view and update_via_alter %}
+    {% if existing_relation.is_hive_metastore() %}
+      {{ exceptions.raise_compiler_error("Cannot update a view in the Hive metastore via ALTER VIEW. Please set `view_update_via_alter: false` in your model configuration.") }}
+    {% endif %}
+    {{ return(True) }}
+  {% endif %}
+  {{ return(False) }}
 {% endmacro %}
\ No newline at end of file
diff --git a/dbt/include/databricks/macros/relations/components/query.sql b/dbt/include/databricks/macros/relations/components/query.sql
new file mode 100644
index 00000000..c6f81c4d
--- /dev/null
+++ b/dbt/include/databricks/macros/relations/components/query.sql
@@ -0,0 +1,14 @@
+{% macro alter_query(target_relation, query) %}
+  {{ log("Altering query") }}
+  {% if query %}
+    {% call statement('main') %}
+      {{- get_alter_query_sql(target_relation, query) }}
+    {% endcall %}
+  {% endif %}
+{% endmacro %}
+
+{% macro get_alter_query_sql(target_relation, query) -%}
+  ALTER {{ target_relation.type|upper }} {{ target_relation.render() }} AS (
+    {{ query }}
+  )
+{%- endmacro %}
\ No newline at end of file
diff --git a/dbt/include/databricks/macros/relations/execute.sql b/dbt/include/databricks/macros/relations/execute.sql
new file mode 100644
index 00000000..e5d3d2d0
--- /dev/null
+++ b/dbt/include/databricks/macros/relations/execute.sql
@@ -0,0 +1,8 @@
+{% macro execute_no_op(target_relation) %}
+    {% do store_raw_result(
+        name="main",
+        message="skip " ~ target_relation,
+        code="skip",
+        rows_affected="-1"
+    ) %}
+{% endmacro %}
\ No newline at end of file
diff --git a/dbt/include/databricks/macros/relations/view/alter.sql b/dbt/include/databricks/macros/relations/view/alter.sql
index 5bbe8e44..858a9dce 100644
--- a/dbt/include/databricks/macros/relations/view/alter.sql
+++ b/dbt/include/databricks/macros/relations/view/alter.sql
@@ -1,22 +1,19 @@
-{% macro alter_view(target_relation, configuration_changes) %}
-  {{ log("Updating view via ALTER VIEW") }}
-  {% if configuration_changes %}
-    {% set tags = configuration_changes.changes.get("tags") %}
-    {% set tblproperties = configuration_changes.changes.get("tblproperties") %}
-    {% set query = configuration_changes.changes.get("query") %}
-    {% if tags %}
-      {% do apply_tags(target_relation, tags.set_tags, tags.unset_tags) %}
-    {%- endif -%}
-    {% if tblproperties %}
-      {% do apply_tblproperties(target_relation, tblproperties.tblproperties) %}
-    {%- endif -%}
-    {%- if query -%}
-      {% call statement('main') -%}
-  ALTER VIEW {{ target_relation.render() }} AS (
-    {{ query.query }}
-  )
-      {% endcall %}
-    {%- endif -%}
-    {% do persist_docs(target_relation, model, for_relation=False) %}
+{% macro alter_view(target_relation, changes) %}
+  {{ log("Updating view via ALTER") }}
+  {{ adapter.dispatch('alter_view', 'databricks')(target_relation, changes) }}
+{% endmacro %}
+
+{% macro databricks__alter_view(target_relation, changes) %}
+  {% set tags = changes.get("tags") %}
+  {% set tblproperties = changes.get("tblproperties") %}
+  {% set query = changes.get("query") %}
+  {% if tags %}
+    {{ apply_tags(target_relation, tags.set_tags, tags.unset_tags) }}
+  {% endif %}
+  {% if tblproperties %}
+    {{ apply_tblproperties(target_relation, tblproperties.tblproperties) }}
+  {% endif %}
+  {% if query %}
+    {{ alter_query(target_relation, query.query) }}
   {% endif %}
 {% endmacro %}


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index db002e87..c2ef51e7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,7 +6,7 @@ pytest_plugins = ["dbt.tests.fixtures.project"]
 
 
 def pytest_addoption(parser):
-    parser.addoption("--profile", action="store", default="databricks_cluster", type=str)
+    parser.addoption("--profile", action="store", default="databricks_uc_sql_endpoint", type=str)
 
 
 # Using @pytest.mark.skip_profile('databricks_cluster') uses the 'skip_by_adapter_type'
diff --git a/tests/functional/adapter/views/test_views.py b/tests/functional/adapter/views/test_views.py
index 74919731..e00f0849 100644
--- a/tests/functional/adapter/views/test_views.py
+++ b/tests/functional/adapter/views/test_views.py
@@ -14,6 +14,8 @@ class BaseUpdateView:
     def models(self):
         return {"initial_view.sql": fixtures.view_sql, "schema.yml": fixtures.schema_yml}
 
+
+class BaseUpdateDescription(BaseUpdateView):
     def test_view_update_with_description(self, project):
         util.run_dbt(["build"])
         schema = util.read_file("models", "schema.yml")
@@ -29,6 +31,8 @@ class BaseUpdateView:
             if row[0] == "comment":
                 assert row[1] == "This is an updated view"
 
+
+class BaseUpdateQuery(BaseUpdateView):
     def test_view_update_with_query(self, project):
         util.run_dbt(["build"])
         util.write_file(fixtures.altered_view_sql, "models", "initial_view.sql")
@@ -40,6 +44,22 @@ class BaseUpdateView:
         )
         assert results[0] == Row([1], ["id"])
 
+
+class BaseUpdateNothing(BaseUpdateView):
+    """This test is only to ensure that we don't error out due to lack of changes."""
+
+    def test_view_update_nothing(self, project):
+        util.run_dbt(["build"])
+        util.run_dbt(["run"])
+
+        results = project.run_sql(
+            "describe extended {database}.{schema}.initial_view",
+            fetch="all",
+        )
+        assert results[0][2] == "This is the id column"
+
+
+class BaseUpdateTblProperties(BaseUpdateView):
     def test_view_update_tblproperties(self, project):
         util.run_dbt(["build"])
         schema = util.read_file("models", "schema.yml")
@@ -54,8 +74,55 @@ class BaseUpdateView:
         assert results[0][1] == "value2"
 
 
+class BaseUpdateColumnComments(BaseUpdateView):
+    def test_view_update_with_column_comments(self, project):
+        util.run_dbt(["build"])
+        schema = util.read_file("models", "schema.yml")
+        schema_2 = schema.replace("This is the id column", "This is an id column")
+        util.write_file(schema_2, "models", "schema.yml")
+        util.run_dbt(["run"])
+
+        results = project.run_sql(
+            "describe extended {database}.{schema}.initial_view",
+            fetch="all",
+        )
+        assert results[0][2] == "This is an id column"
+
+
+@pytest.mark.skip_profile("databricks_cluster")
+class TestUpdateViewViaAlterDescription(BaseUpdateDescription):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+view_update_via_alter": True,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_cluster")
+class TestUpdateViewViaAlterNothing(BaseUpdateNothing):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+view_update_via_alter": True,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
 @pytest.mark.skip_profile("databricks_cluster")
-class TestUpdateViewViaAlter(BaseUpdateView):
+class TestUpdateViewViaAlterQuery(BaseUpdateQuery):
     @pytest.fixture(scope="class")
     def project_config_update(self):
         return {
@@ -71,7 +138,71 @@ class TestUpdateViewViaAlter(BaseUpdateView):
 
 
 @pytest.mark.skip_profile("databricks_cluster")
-class TestUpdateViewSafeReplace(BaseUpdateView):
+class TestUpdateViewViaAlterTblproperties(BaseUpdateTblProperties):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+view_update_via_alter": True,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_cluster")
+class TestUpdateViewViaAlterColumnComments(BaseUpdateColumnComments):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+view_update_via_alter": True,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_cluster")
+class TestUpdateViewSafeReplaceDescription(BaseUpdateDescription):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+use_safer_relation_operations": True,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_cluster")
+class TestUpdateViewSafeReplaceQuery(BaseUpdateQuery):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+use_safer_relation_operations": True,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_cluster")
+class TestUpdateViewSafeReplaceTblproperties(BaseUpdateTblProperties):
     @pytest.fixture(scope="class")
     def project_config_update(self):
         return {
@@ -87,7 +218,115 @@ class TestUpdateViewSafeReplace(BaseUpdateView):
 
 
 @pytest.mark.skip_profile("databricks_cluster")
-class TestUpdateUnsafeReplace(BaseUpdateView):
+class TestUpdateUnsafeReplaceDescription(BaseUpdateDescription):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+use_safer_relation_operations": False,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_cluster")
+class TestUpdateUnsafeReplaceQuery(BaseUpdateQuery):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+use_safer_relation_operations": False,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_cluster")
+class TestUpdateUnsafeReplaceTblproperties(BaseUpdateTblProperties):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+use_safer_relation_operations": False,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+class HiveBaseUpdateDescription(BaseUpdateDescription):
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {"initial_view.sql": fixtures.view_sql, "schema.yml": fixtures.hive_schema_yml}
+
+
+@pytest.mark.skip_profile("databricks_uc_cluster", "databricks_uc_sql_endpoint")
+class TestHiveUpdateViewSafeReplaceDescription(HiveBaseUpdateDescription):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+use_safer_relation_operations": True,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_uc_cluster", "databricks_uc_sql_endpoint")
+class TestHiveUpdateUnsafeReplaceDescription(HiveBaseUpdateDescription):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+use_safer_relation_operations": False,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+class HiveBaseUpdateQuery(BaseUpdateQuery):
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {"initial_view.sql": fixtures.view_sql, "schema.yml": fixtures.hive_schema_yml}
+
+
+@pytest.mark.skip_profile("databricks_uc_cluster", "databricks_uc_sql_endpoint")
+class TestHiveUpdateViewSafeReplaceQuery(HiveBaseUpdateQuery):
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+            "models": {
+                "+use_safer_relation_operations": True,
+                "+persist_docs": {
+                    "relation": True,
+                    "columns": True,
+                },
+            },
+        }
+
+
+@pytest.mark.skip_profile("databricks_uc_cluster", "databricks_uc_sql_endpoint")
+class TestHiveUpdateUnsafeReplaceQuery(HiveBaseUpdateQuery):
     @pytest.fixture(scope="class")
     def project_config_update(self):
         return {
@@ -102,14 +341,14 @@ class TestUpdateUnsafeReplace(BaseUpdateView):
         }
 
 
-class HiveBaseUpdateView(BaseUpdateView):
+class HiveBaseUpdateTblproperties(BaseUpdateTblProperties):
     @pytest.fixture(scope="class")
     def models(self):
         return {"initial_view.sql": fixtures.view_sql, "schema.yml": fixtures.hive_schema_yml}
 
 
 @pytest.mark.skip_profile("databricks_uc_cluster", "databricks_uc_sql_endpoint")
-class TestHiveUpdateViewSafeReplace(HiveBaseUpdateView):
+class TestHiveUpdateViewSafeReplaceTblproperties(HiveBaseUpdateTblproperties):
     @pytest.fixture(scope="class")
     def project_config_update(self):
         return {
@@ -125,7 +364,7 @@ class TestHiveUpdateViewSafeReplace(HiveBaseUpdateView):
 
 
 @pytest.mark.skip_profile("databricks_uc_cluster", "databricks_uc_sql_endpoint")
-class TestHiveUpdateUnsafeReplace(HiveBaseUpdateView):
+class TestHiveUpdateUnsafeReplaceTblproperties(HiveBaseUpdateTblproperties):
     @pytest.fixture(scope="class")
     def project_config_update(self):
         return {
diff --git a/tests/unit/macros/adapters/test_python_macros.py b/tests/unit/macros/adapters/test_python_macros.py
index bedc4812..2f64c850 100644
--- a/tests/unit/macros/adapters/test_python_macros.py
+++ b/tests/unit/macros/adapters/test_python_macros.py
@@ -6,17 +6,17 @@ from tests.unit.macros.base import MacroTestBase
 
 
 class TestPythonMacros(MacroTestBase):
-    @pytest.fixture(scope="class", autouse=True)
+    @pytest.fixture(autouse=True)
     def modify_context(self, default_context) -> None:
         default_context["model"] = MagicMock()
         d = {"alias": "schema"}
         default_context["model"].__getitem__.side_effect = d.__getitem__
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def macro_folders_to_load(self) -> list:
         return ["macros/adapters"]
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def template_name(self) -> str:
         return "python.sql"
 
diff --git a/tests/unit/macros/base.py b/tests/unit/macros/base.py
index 3d129b6a..11bf3383 100644
--- a/tests/unit/macros/base.py
+++ b/tests/unit/macros/base.py
@@ -5,6 +5,7 @@ from unittest.mock import Mock
 import pytest
 from jinja2 import Environment, FileSystemLoader, PackageLoader, Template
 
+from dbt.adapters.databricks.column import DatabricksColumn
 from dbt.adapters.databricks.relation import DatabricksRelation
 
 
@@ -34,7 +35,7 @@ class MacroTestBase:
         context["var"] = lambda key, default=None, **kwargs: local_var.get(key, default)
         return local_var
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def default_context(self) -> dict:
         """
         This is the default context used in all tests.
@@ -50,11 +51,12 @@ class MacroTestBase:
             "log": Mock(return_value=""),
             "return": lambda r: r,
             "is_incremental": Mock(return_value=False),
+            "api": Mock(Column=DatabricksColumn),
+            "local_md5": lambda s: f"hash({s})",
         }
-
         return context
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def spark_env(self) -> Environment:
         """
         The environment used for rendering dbt-spark macros
@@ -64,7 +66,7 @@ class MacroTestBase:
             extensions=["jinja2.ext.do"],
         )
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def spark_template_names(self) -> list:
         """
         The list of Spark templates to load for the tests.
@@ -72,14 +74,14 @@ class MacroTestBase:
         """
         return ["adapters.sql"]
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def spark_context(self, default_context, spark_env, spark_template_names) -> dict:
         """
         Adds all the requested Spark macros to the context
         """
         return self.build_up_context(default_context, spark_env, spark_template_names)
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def macro_folders_to_load(self) -> list:
         """
         This is a list of folders from which we look to load Databricks macro templates.
@@ -88,7 +90,7 @@ class MacroTestBase:
         """
         return ["macros"]
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def databricks_env(self, macro_folders_to_load) -> Environment:
         """
         The environment used for rendering Databricks macros
@@ -100,7 +102,7 @@ class MacroTestBase:
             extensions=["jinja2.ext.do"],
         )
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def databricks_template_names(self) -> list:
         """
         The list of databricks templates to load for referencing imported macros in the
@@ -111,7 +113,7 @@ class MacroTestBase:
         """
         return []
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def databricks_context(self, spark_context, databricks_env, databricks_template_names) -> dict:
         """
         Adds all the requested Databricks macros to the context
@@ -131,7 +133,7 @@ class MacroTestBase:
 
         return new_context
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def template_name(self) -> str:
         """
         The name of the Databricks template you want to test, not including the path.
@@ -171,7 +173,7 @@ class MacroTestBase:
         """
         return template.globals
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def relation(self):
         """
         Dummy relation to use in tests.
@@ -182,7 +184,7 @@ class MacroTestBase:
                 "schema": "some_schema",
                 "identifier": "some_table",
             },
-            "type": None,
+            "type": "view",
         }
 
         return DatabricksRelation.from_dict(data)
diff --git a/tests/unit/macros/relations/test_constraint_macros.py b/tests/unit/macros/relations/test_constraint_macros.py
index feac1797..4ee67c4c 100644
--- a/tests/unit/macros/relations/test_constraint_macros.py
+++ b/tests/unit/macros/relations/test_constraint_macros.py
@@ -1,8 +1,5 @@
-from unittest.mock import Mock
-
 import pytest
 
-from dbt.adapters.databricks.column import DatabricksColumn
 from tests.unit.macros.base import MacroTestBase
 
 
@@ -11,16 +8,10 @@ class TestConstraintMacros(MacroTestBase):
     def template_name(self) -> str:
         return "constraints.sql"
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def macro_folders_to_load(self) -> list:
         return ["macros/relations", "macros"]
 
-    @pytest.fixture(scope="class", autouse=True)
-    def modify_context(self, default_context) -> None:
-        # Mock local_md5
-        default_context["local_md5"] = lambda s: f"hash({s})"
-        default_context["api"] = Mock(Column=DatabricksColumn)
-
     def render_constraints(self, template, *args):
         return self.run_macro(template, "databricks_constraints_to_dbt", *args)
 
@@ -68,7 +59,7 @@ class TestConstraintMacros(MacroTestBase):
 
         assert r == "[{'type': 'not_null', 'columns': ['col']}]"
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def constraint_model(self):
         columns = {
             "id": {"name": "id", "data_type": "int"},
@@ -175,7 +166,7 @@ class TestConstraintMacros(MacroTestBase):
             *args,
         )
 
-    @pytest.fixture(scope="class")
+    @pytest.fixture
     def model(self):
         columns = {
             "id": {"name": "id", "data_type": "int"},
diff --git a/tests/unit/macros/relations/test_query_macros.py b/tests/unit/macros/relations/test_query_macros.py
new file mode 100644
index 00000000..dfda5ee4
--- /dev/null
+++ b/tests/unit/macros/relations/test_query_macros.py
@@ -0,0 +1,29 @@
+import pytest
+
+from tests.unit.macros.base import MacroTestBase
+
+
+class TestAlterQuery(MacroTestBase):
+    @pytest.fixture(scope="class")
+    def template_name(self) -> str:
+        return "query.sql"
+
+    @pytest.fixture(scope="class")
+    def macro_folders_to_load(self) -> list:
+        return ["macros", "macros/relations/components"]
+
+    def render_alter_query(self, template_bundle, sql="select 1"):
+        return self.run_macro(
+            template_bundle.template,
+            "get_alter_query_sql",
+            template_bundle.relation,
+            sql,
+        )
+
+    def test_macros__alter_query(self, template_bundle):
+        sql = self.render_alter_query(template_bundle)
+        expected = (
+            f"ALTER {str(template_bundle.relation.type).upper()}"
+            f" {template_bundle.relation.render()} AS ( select 1 )"
+        )
+        assert sql == expected
diff --git a/tests/unit/macros/relations/test_tags_macros.py b/tests/unit/macros/relations/test_tags_macros.py
index e44cb494..77035386 100644
--- a/tests/unit/macros/relations/test_tags_macros.py
+++ b/tests/unit/macros/relations/test_tags_macros.py
@@ -25,7 +25,7 @@ class TestTagsMacros(MacroTestBase):
     def test_macros_alter_set_tags(self, template_bundle):
         sql = self.render_bundle(template_bundle, "alter_set_tags", {"a": "valA", "b": "valB"})
         expected = (
-            "ALTER None `some_database`.`some_schema`.`some_table` "
+            "ALTER view `some_database`.`some_schema`.`some_table` "
             "SET TAGS ( 'a' = 'valA', 'b' = 'valB' )"
         )
 
@@ -33,6 +33,6 @@ class TestTagsMacros(MacroTestBase):
 
     def test_macros_alter_unset_tags(self, template_bundle):
         sql = self.render_bundle(template_bundle, "alter_unset_tags", ["a", "b"])
-        expected = "ALTER None `some_database`.`some_schema`.`some_table` UNSET TAGS ( 'a','b' )"
+        expected = "ALTER view `some_database`.`some_schema`.`some_table` UNSET TAGS ( 'a','b' )"
 
         assert sql == expected
diff --git a/tests/unit/macros/relations/test_view_macros.py b/tests/unit/macros/relations/test_view_macros.py
index 9914d34d..e206a819 100644
--- a/tests/unit/macros/relations/test_view_macros.py
+++ b/tests/unit/macros/relations/test_view_macros.py
@@ -33,3 +33,51 @@ class TestCreateViewAs(MacroTestBase):
         )
 
         assert sql == expected
+
+
+class TestAlterView(MacroTestBase):
+    @pytest.fixture(scope="class")
+    def template_name(self) -> str:
+        return "alter.sql"
+
+    @pytest.fixture(scope="class")
+    def macro_folders_to_load(self) -> list:
+        return ["macros", "macros/relations/view"]
+
+    @pytest.fixture(autouse=True, scope="function")
+    def mocks(self, context):
+        context["apply_tags"] = Mock()
+        context["apply_tblproperties"] = Mock()
+        context["alter_query"] = Mock()
+
+    def render_alter_view(self, template_bundle, changes):
+        return self.run_macro(
+            template_bundle.template,
+            "alter_view",
+            template_bundle.relation,
+            changes,
+        )
+
+    def test_macros__alter_view_empty_changes(self, context, template_bundle):
+        self.render_alter_view(template_bundle, {})
+        context["apply_tags"].assert_not_called()
+        context["apply_tblproperties"].assert_not_called()
+        context["alter_query"].assert_not_called()
+
+    def test_macros__alter_view_with_tags(self, context, template_bundle):
+        self.render_alter_view(template_bundle, {"tags": Mock()})
+        context["apply_tags"].assert_called_once()
+        context["apply_tblproperties"].assert_not_called()
+        context["alter_query"].assert_not_called()
+
+    def test_macros__alter_view_with_tblproperties(self, context, template_bundle):
+        self.render_alter_view(template_bundle, {"tblproperties": Mock()})
+        context["apply_tags"].assert_not_called()
+        context["apply_tblproperties"].assert_called_once()
+        context["alter_query"].assert_not_called()
+
+    def test_macros__alter_view_with_query(self, context, template_bundle):
+        self.render_alter_view(template_bundle, {"query": Mock()})
+        context["apply_tags"].assert_not_called()
+        context["apply_tblproperties"].assert_not_called()
+        context["alter_query"].assert_called_once()
diff --git a/tests/unit/relation_configs/test_query.py b/tests/unit/relation_configs/test_query.py
index 3cd1f1b3..11294b6c 100644
--- a/tests/unit/relation_configs/test_query.py
+++ b/tests/unit/relation_configs/test_query.py
@@ -30,3 +30,23 @@ class TestQueryProcessor:
             match="Cannot compile model 1 with no SQL query",
         ):
             _ = QueryProcessor.from_relation_config(model)
+
+    def test_get_diff__similar_query(self):
+        model = QueryConfig(query="select * from foo")
+        results = {
+            "information_schema.views": Row(
+                ["(\nselect * from foo\n)", "other"], ["view_definition", "comment"]
+            )
+        }
+        other = QueryProcessor.from_relation_results(results)
+        assert model.get_diff(other) is None
+
+    def test_get_diff__different_query(self):
+        model = QueryConfig(query="select * from foo")
+        results = {
+            "information_schema.views": Row(
+                ["(\nselect * from bar\n)", "other"], ["view_definition", "comment"]
+            )
+        }
+        other = QueryProcessor.from_relation_results(results)
+        assert model.get_diff(other) is model