diff --git a/.changes/unreleased/Features-20240131-125318.yaml b/.changes/unreleased/Features-20240131-125318.yaml new file mode 100644 index 000000000..63771d71e --- /dev/null +++ b/.changes/unreleased/Features-20240131-125318.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Support refresh_mode and initialize parameters for dynamic tables +time: 2024-01-31T12:53:18.111616Z +custom: + Author: HenkvanDyk,mikealfare + Issue: "1076" diff --git a/.changes/unreleased/Fixes-20240522-160538.yaml b/.changes/unreleased/Fixes-20240522-160538.yaml new file mode 100644 index 000000000..4921706a9 --- /dev/null +++ b/.changes/unreleased/Fixes-20240522-160538.yaml @@ -0,0 +1,6 @@ +kind: Fixes +body: 'Rename targets for tables and views use fully qualified names' +time: 2024-05-22T16:05:38.602074-04:00 +custom: + Author: mikealfare + Issue: "1031" diff --git a/.github/workflows/docs-issues.yml b/.github/workflows/docs-issues.yml index 00a098df8..f49cf517c 100644 --- a/.github/workflows/docs-issues.yml +++ b/.github/workflows/docs-issues.yml @@ -1,19 +1,18 @@ # **what?** -# Open an issue in docs.getdbt.com when a PR is labeled `user docs` +# Open an issue in docs.getdbt.com when an issue is labeled `user docs` and closed as completed # **why?** # To reduce barriers for keeping docs up to date # **when?** -# When a PR is labeled `user docs` and is merged. Runs on pull_request_target to run off the workflow already merged, -# not the workflow that existed on the PR branch. This allows old PRs to get comments. +# When an issue is labeled `user docs` and is closed as completed. Can be labeled before or after the issue is closed. -name: Open issues in docs.getdbt.com repo when a PR is labeled -run-name: "Open an issue in docs.getdbt.com for PR #${{ github.event.pull_request.number }}" +name: Open issues in docs.getdbt.com repo when an issue is labeled +run-name: "Open an issue in docs.getdbt.com for issue #${{ github.event.issue.number }}" on: - pull_request_target: + issues: types: [labeled, closed] defaults: @@ -21,23 +20,22 @@ defaults: shell: bash permissions: - issues: write # opens new issues - pull-requests: write # comments on PRs - + issues: write # comments on issues jobs: open_issues: - # we only want to run this when the PR has been merged or the label in the labeled event is `user docs`. Otherwise it runs the + # we only want to run this when the issue is closed as completed and the label `user docs` has been assigned. + # If this logic does not exist in this workflow, it runs the # risk of duplicaton of issues being created due to merge and label both triggering this workflow to run and neither having # generating the comment before the other runs. This lives here instead of the shared workflow because this is where we # decide if it should run or not. if: | - (github.event.pull_request.merged == true) && - ((github.event.action == 'closed' && contains( github.event.pull_request.labels.*.name, 'user docs')) || + (github.event.issue.state == 'closed' && github.event.issue.state_reason == 'completed') && ( + (github.event.action == 'closed' && contains(github.event.issue.labels.*.name, 'user docs')) || (github.event.action == 'labeled' && github.event.label.name == 'user docs')) uses: dbt-labs/actions/.github/workflows/open-issue-in-repo.yml@main with: issue_repository: "dbt-labs/docs.getdbt.com" - issue_title: "Docs Changes Needed from ${{ github.event.repository.name }} PR #${{ github.event.pull_request.number }}" + issue_title: "Docs Changes Needed from ${{ github.event.repository.name }} Issue #${{ github.event.issue.number }}" issue_body: "At a minimum, update body to include a link to the page on docs.getdbt.com requiring updates and what part(s) of the page you would like to see updated." secrets: inherit diff --git a/dbt/adapters/snowflake/relation.py b/dbt/adapters/snowflake/relation.py index ff94abc33..f477265f0 100644 --- a/dbt/adapters/snowflake/relation.py +++ b/dbt/adapters/snowflake/relation.py @@ -2,13 +2,19 @@ from typing import FrozenSet, Optional, Type from dbt.adapters.base.relation import BaseRelation -from dbt.adapters.relation_configs import RelationConfigChangeAction, RelationResults +from dbt.adapters.relation_configs import ( + RelationConfigBase, + RelationConfigChangeAction, + RelationResults, +) from dbt.adapters.contracts.relation import RelationConfig from dbt.adapters.utils import classproperty +from dbt_common.exceptions import DbtRuntimeError from dbt.adapters.snowflake.relation_configs import ( SnowflakeDynamicTableConfig, SnowflakeDynamicTableConfigChangeset, + SnowflakeDynamicTableRefreshModeConfigChange, SnowflakeDynamicTableTargetLagConfigChange, SnowflakeDynamicTableWarehouseConfigChange, SnowflakeQuotePolicy, @@ -21,6 +27,9 @@ class SnowflakeRelation(BaseRelation): type: Optional[SnowflakeRelationType] = None quote_policy: SnowflakeQuotePolicy = field(default_factory=lambda: SnowflakeQuotePolicy()) require_alias: bool = False + relation_configs = { + SnowflakeRelationType.DynamicTable: SnowflakeDynamicTableConfig, + } renameable_relations: FrozenSet[SnowflakeRelationType] = field( default_factory=lambda: frozenset( { @@ -52,6 +61,17 @@ def DynamicTable(cls) -> str: def get_relation_type(cls) -> Type[SnowflakeRelationType]: return SnowflakeRelationType + @classmethod + def from_config(cls, config: RelationConfig) -> RelationConfigBase: + relation_type: str = config.config.materialized + + if relation_config := cls.relation_configs.get(relation_type): + return relation_config.from_relation_config(config) + + raise DbtRuntimeError( + f"from_config() is not supported for the provided relation type: {relation_type}" + ) + @classmethod def dynamic_table_config_changeset( cls, relation_results: RelationResults, relation_config: RelationConfig @@ -77,6 +97,12 @@ def dynamic_table_config_changeset( ) ) + if new_dynamic_table.refresh_mode != existing_dynamic_table.refresh_mode: + config_change_collection.refresh_mode = SnowflakeDynamicTableRefreshModeConfigChange( + action=RelationConfigChangeAction.create, + context=new_dynamic_table.refresh_mode, + ) + if config_change_collection.has_changes: return config_change_collection return None diff --git a/dbt/adapters/snowflake/relation_configs/__init__.py b/dbt/adapters/snowflake/relation_configs/__init__.py index e5ceabe49..62f95faff 100644 --- a/dbt/adapters/snowflake/relation_configs/__init__.py +++ b/dbt/adapters/snowflake/relation_configs/__init__.py @@ -1,6 +1,7 @@ from dbt.adapters.snowflake.relation_configs.dynamic_table import ( SnowflakeDynamicTableConfig, SnowflakeDynamicTableConfigChangeset, + SnowflakeDynamicTableRefreshModeConfigChange, SnowflakeDynamicTableWarehouseConfigChange, SnowflakeDynamicTableTargetLagConfigChange, ) diff --git a/dbt/adapters/snowflake/relation_configs/dynamic_table.py b/dbt/adapters/snowflake/relation_configs/dynamic_table.py index 74735cbd5..2e227d3a4 100644 --- a/dbt/adapters/snowflake/relation_configs/dynamic_table.py +++ b/dbt/adapters/snowflake/relation_configs/dynamic_table.py @@ -4,6 +4,8 @@ from dbt.adapters.relation_configs import RelationConfigChange, RelationResults from dbt.adapters.contracts.relation import RelationConfig from dbt.adapters.contracts.relation import ComponentName +from dbt_common.dataclass_schema import StrEnum # doesn't exist in standard library until py3.11 +from typing_extensions import Self from dbt.adapters.snowflake.relation_configs.base import SnowflakeRelationConfigBase @@ -11,6 +13,25 @@ import agate +class RefreshMode(StrEnum): + AUTO = "AUTO" + FULL = "FULL" + INCREMENTAL = "INCREMENTAL" + + @classmethod + def default(cls) -> Self: + return cls("AUTO") + + +class Initialize(StrEnum): + ON_CREATE = "ON_CREATE" + ON_SCHEDULE = "ON_SCHEDULE" + + @classmethod + def default(cls) -> Self: + return cls("ON_CREATE") + + @dataclass(frozen=True, eq=True, unsafe_hash=True) class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): """ @@ -22,6 +43,8 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): - query: the query behind the table - target_lag: the maximum amount of time that the dynamic table’s content should lag behind updates to the base tables - snowflake_warehouse: the name of the warehouse that provides the compute resources for refreshing the dynamic table + - refresh_mode: specifies the refresh type for the dynamic table + - initialize: specifies the behavior of the initial refresh of the dynamic table There are currently no non-configurable parameters. """ @@ -32,6 +55,8 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): query: str target_lag: str snowflake_warehouse: str + refresh_mode: Optional[RefreshMode] = RefreshMode.default() + initialize: Optional[Initialize] = Initialize.default() @classmethod def from_dict(cls, config_dict) -> "SnowflakeDynamicTableConfig": @@ -44,6 +69,8 @@ def from_dict(cls, config_dict) -> "SnowflakeDynamicTableConfig": "query": config_dict.get("query"), "target_lag": config_dict.get("target_lag"), "snowflake_warehouse": config_dict.get("snowflake_warehouse"), + "refresh_mode": config_dict.get("refresh_mode"), + "initialize": config_dict.get("initialize"), } dynamic_table: "SnowflakeDynamicTableConfig" = super().from_dict(kwargs_dict) @@ -60,6 +87,12 @@ def parse_relation_config(cls, relation_config: RelationConfig) -> Dict[str, Any "snowflake_warehouse": relation_config.config.extra.get("snowflake_warehouse"), } + if refresh_mode := relation_config.config.extra.get("refresh_mode"): + config_dict.update(refresh_mode=refresh_mode.upper()) + + if initialize := relation_config.config.extra.get("initialize"): + config_dict.update(initialize=initialize.upper()) + return config_dict @classmethod @@ -73,6 +106,8 @@ def parse_relation_results(cls, relation_results: RelationResults) -> Dict: "query": dynamic_table.get("text"), "target_lag": dynamic_table.get("target_lag"), "snowflake_warehouse": dynamic_table.get("warehouse"), + "refresh_mode": dynamic_table.get("refresh_mode"), + # we don't get initialize since that's a one-time scheduler attribute, not a DT attribute } return config_dict @@ -96,10 +131,20 @@ def requires_full_refresh(self) -> bool: return False +@dataclass(frozen=True, eq=True, unsafe_hash=True) +class SnowflakeDynamicTableRefreshModeConfigChange(RelationConfigChange): + context: Optional[str] = None + + @property + def requires_full_refresh(self) -> bool: + return True + + @dataclass class SnowflakeDynamicTableConfigChangeset: target_lag: Optional[SnowflakeDynamicTableTargetLagConfigChange] = None snowflake_warehouse: Optional[SnowflakeDynamicTableWarehouseConfigChange] = None + refresh_mode: Optional[SnowflakeDynamicTableRefreshModeConfigChange] = None @property def requires_full_refresh(self) -> bool: @@ -111,9 +156,10 @@ def requires_full_refresh(self) -> bool: if self.snowflake_warehouse else False ), + self.refresh_mode.requires_full_refresh if self.refresh_mode else False, ] ) @property def has_changes(self) -> bool: - return any([self.target_lag, self.snowflake_warehouse]) + return any([self.target_lag, self.snowflake_warehouse, self.refresh_mode]) diff --git a/dbt/include/snowflake/macros/relations/create_backup.sql b/dbt/include/snowflake/macros/relations/create_backup.sql new file mode 100644 index 000000000..b5f347cd9 --- /dev/null +++ b/dbt/include/snowflake/macros/relations/create_backup.sql @@ -0,0 +1,12 @@ +{%- macro snowflake__get_create_backup_sql(relation) -%} + + -- get the standard backup name + {% set backup_relation = make_backup_relation(relation, relation.type) %} + + -- drop any pre-existing backup + {{ get_drop_sql(backup_relation) }}; + + -- use `render` to ensure that the fully qualified name is used + {{ get_rename_sql(relation, backup_relation.render()) }} + +{%- endmacro -%} diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql index 1d76c417c..253788779 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql @@ -1,8 +1,16 @@ {% macro snowflake__get_create_dynamic_table_as_sql(relation, sql) -%} + {%- set dynamic_table = relation.from_config(config.model) -%} + create dynamic table {{ relation }} - target_lag = '{{ config.get("target_lag") }}' - warehouse = {{ config.get("snowflake_warehouse") }} + target_lag = '{{ dynamic_table.target_lag }}' + warehouse = {{ dynamic_table.snowflake_warehouse }} + {% if dynamic_table.refresh_mode %} + refresh_mode = {{ dynamic_table.refresh_mode }} + {% endif %} + {% if dynamic_table.initialize %} + initialize = {{ dynamic_table.initialize }} + {% endif %} as ( {{ sql }} ) diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql b/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql index a5f612039..cc79328fe 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql @@ -10,7 +10,8 @@ "database_name", "text", "target_lag", - "warehouse" + "warehouse", + "refresh_mode" from table(result_scan(last_query_id())) {%- endset %} {% set _dynamic_table = run_query(_dynamic_table_sql) %} diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql b/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql index 385ce119c..dbe27d66e 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql @@ -1,12 +1,18 @@ -{% macro snowflake__get_replace_dynamic_table_sql(relation, sql) %} +{% macro snowflake__get_replace_dynamic_table_sql(relation, sql) -%} + + {%- set dynamic_table = relation.from_config(config.model) -%} create or replace dynamic table {{ relation }} - target_lag = '{{ config.get("target_lag") }}' - warehouse = {{ config.get("snowflake_warehouse") }} + target_lag = '{{ dynamic_table.target_lag }}' + warehouse = {{ dynamic_table.snowflake_warehouse }} + {% if dynamic_table.refresh_mode %} + refresh_mode = {{ dynamic_table.refresh_mode }} + {% endif %} + {% if dynamic_table.initialize %} + initialize = {{ dynamic_table.initialize }} + {% endif %} as ( {{ sql }} ) - ; - {{ snowflake__refresh_dynamic_table(relation) }} -{% endmacro %} +{%- endmacro %} diff --git a/dbt/include/snowflake/macros/relations/rename_intermediate.sql b/dbt/include/snowflake/macros/relations/rename_intermediate.sql new file mode 100644 index 000000000..abd5fee92 --- /dev/null +++ b/dbt/include/snowflake/macros/relations/rename_intermediate.sql @@ -0,0 +1,9 @@ +{%- macro snowflake__get_rename_intermediate_sql(relation) -%} + + -- get the standard intermediate name + {% set intermediate_relation = make_intermediate_relation(relation) %} + + -- use `render` to ensure that the fully qualified name is used + {{ get_rename_sql(intermediate_relation, relation.render()) }} + +{%- endmacro -%} diff --git a/dbt/include/snowflake/macros/relations/table/rename.sql b/dbt/include/snowflake/macros/relations/table/rename.sql index 7b363e03d..699debf28 100644 --- a/dbt/include/snowflake/macros/relations/table/rename.sql +++ b/dbt/include/snowflake/macros/relations/table/rename.sql @@ -1,3 +1,13 @@ {%- macro snowflake__get_rename_table_sql(relation, new_name) -%} + /* + Rename or move a table to the new name. + + Args: + relation: SnowflakeRelation - relation to be renamed + new_name: Union[str, SnowflakeRelation] - new name for `relation` + if providing a string, the default database/schema will be used if that string is just an identifier + if providing a SnowflakeRelation, `render` will be used to produce a fully qualified name + Returns: templated string + */ alter table {{ relation }} rename to {{ new_name }} {%- endmacro -%} diff --git a/dbt/include/snowflake/macros/relations/view/rename.sql b/dbt/include/snowflake/macros/relations/view/rename.sql index 4cfd410a4..add2f49b9 100644 --- a/dbt/include/snowflake/macros/relations/view/rename.sql +++ b/dbt/include/snowflake/macros/relations/view/rename.sql @@ -1,3 +1,13 @@ {%- macro snowflake__get_rename_view_sql(relation, new_name) -%} + /* + Rename or move a view to the new name. + + Args: + relation: SnowflakeRelation - relation to be renamed + new_name: Union[str, SnowflakeRelation] - new name for `relation` + if providing a string, the default database/schema will be used if that string is just an identifier + if providing a SnowflakeRelation, `render` will be used to produce a fully qualified name + Returns: templated string + */ alter view {{ relation }} rename to {{ new_name }} {%- endmacro -%} diff --git a/tests/functional/adapter/dynamic_table_tests/files.py b/tests/functional/adapter/dynamic_table_tests/files.py index 8239eb0de..ef8d2bf1f 100644 --- a/tests/functional/adapter/dynamic_table_tests/files.py +++ b/tests/functional/adapter/dynamic_table_tests/files.py @@ -27,6 +27,7 @@ materialized='dynamic_table', snowflake_warehouse='DBT_TESTING', target_lag='2 minutes', + refresh_mode='INCREMENTAL', ) }} select * from {{ ref('my_seed') }} """ diff --git a/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py b/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py index 98a872923..a58b76f29 100644 --- a/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py +++ b/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py @@ -17,6 +17,7 @@ MY_SEED, ) from tests.functional.adapter.dynamic_table_tests.utils import ( + query_refresh_mode, query_relation_type, query_target_lag, query_warehouse, @@ -28,6 +29,7 @@ class SnowflakeDynamicTableChanges: def check_start_state(project, dynamic_table): assert query_target_lag(project, dynamic_table) == "2 minutes" assert query_warehouse(project, dynamic_table) == "DBT_TESTING" + assert query_refresh_mode(project, dynamic_table) == "INCREMENTAL" @staticmethod def change_config_via_alter(project, dynamic_table): @@ -57,13 +59,13 @@ def check_state_alter_change_is_applied_downstream(project, dynamic_table): @staticmethod def change_config_via_replace(project, dynamic_table): - # dbt-snowflake does not currently monitor any changes that trigger a full refresh - pass + initial_model = get_model_file(project, dynamic_table) + new_model = initial_model.replace("refresh_mode='INCREMENTAL'", "refresh_mode='FULL'") + set_model_file(project, dynamic_table, new_model) @staticmethod def check_state_replace_change_is_applied(project, dynamic_table): - # dbt-snowflake does not currently monitor any changes that trigger a full refresh - pass + assert query_refresh_mode(project, dynamic_table) == "FULL" @staticmethod def query_relation_type(project, relation: SnowflakeRelation) -> Optional[str]: diff --git a/tests/functional/adapter/dynamic_table_tests/utils.py b/tests/functional/adapter/dynamic_table_tests/utils.py index 6d1ba85ae..d72b231c9 100644 --- a/tests/functional/adapter/dynamic_table_tests/utils.py +++ b/tests/functional/adapter/dynamic_table_tests/utils.py @@ -39,6 +39,11 @@ def query_warehouse(project, dynamic_table: SnowflakeRelation) -> Optional[str]: return config.get("warehouse") +def query_refresh_mode(project, dynamic_table: SnowflakeRelation) -> Optional[str]: + config = describe_dynamic_table(project, dynamic_table) + return config.get("refresh_mode") + + def describe_dynamic_table(project, dynamic_table: SnowflakeRelation) -> agate.Row: with get_connection(project.adapter): macro_results = project.adapter.execute_macro( diff --git a/tests/functional/relation_tests/base.py b/tests/functional/relation_tests/base.py new file mode 100644 index 000000000..d08a6945b --- /dev/null +++ b/tests/functional/relation_tests/base.py @@ -0,0 +1,75 @@ +import pytest + +from dbt.tests.util import run_dbt, run_dbt_and_capture + + +SEED = """ +id +0 +1 +2 +""".strip() + + +TABLE = """ +{{ config(materialized="table") }} +select * from {{ ref('my_seed') }} +""" + + +VIEW = """ +{{ config(materialized="view") }} +select * from {{ ref('my_seed') }} +""" + + +MACRO__GET_CREATE_BACKUP_SQL = """ +{% macro test__get_create_backup_sql(database, schema, identifier, relation_type) -%} + {%- set relation = adapter.Relation.create(database=database, schema=schema, identifier=identifier, type=relation_type) -%} + {% call statement('test__get_create_backup_sql') -%} + {{ get_create_backup_sql(relation) }} + {%- endcall %} +{% endmacro %}""" + + +MACRO__GET_RENAME_INTERMEDIATE_SQL = """ +{% macro test__get_rename_intermediate_sql(database, schema, identifier, relation_type) -%} + {%- set relation = adapter.Relation.create(database=database, schema=schema, identifier=identifier, type=relation_type) -%} + {% call statement('test__get_rename_intermediate_sql') -%} + {{ get_rename_intermediate_sql(relation) }} + {%- endcall %} +{% endmacro %}""" + + +class RelationOperation: + @pytest.fixture(scope="class") + def seeds(self): + yield {"my_seed.csv": SEED} + + @pytest.fixture(scope="class") + def models(self): + yield { + "my_table.sql": TABLE, + "my_table__dbt_tmp.sql": TABLE, + "my_view.sql": VIEW, + "my_view__dbt_tmp.sql": VIEW, + } + + @pytest.fixture(scope="class") + def macros(self): + yield { + "test__get_create_backup_sql.sql": MACRO__GET_CREATE_BACKUP_SQL, + "test__get_rename_intermediate_sql.sql": MACRO__GET_RENAME_INTERMEDIATE_SQL, + } + + @pytest.fixture(scope="class", autouse=True) + def setup(self, project): + run_dbt(["seed"]) + run_dbt(["run"]) + + def assert_operation(self, project, operation, args, expected_statement): + results, logs = run_dbt_and_capture( + ["--debug", "run-operation", operation, "--args", str(args)] + ) + assert len(results) == 1 + assert expected_statement in logs diff --git a/tests/functional/relation_tests/test_table.py b/tests/functional/relation_tests/test_table.py new file mode 100644 index 000000000..b4a8709ea --- /dev/null +++ b/tests/functional/relation_tests/test_table.py @@ -0,0 +1,25 @@ +from tests.functional.relation_tests.base import RelationOperation + + +class TestTable(RelationOperation): + + def test_get_create_backup_and_rename_intermediate_sql(self, project): + args = { + "database": project.database, + "schema": project.test_schema, + "identifier": "my_table", + "relation_type": "table", + } + expected_statement = ( + f"alter table {project.database}.{project.test_schema}.my_table " + f"rename to {project.database}.{project.test_schema}.my_table__dbt_backup" + ) + self.assert_operation(project, "test__get_create_backup_sql", args, expected_statement) + + expected_statement = ( + f"alter table {project.database}.{project.test_schema}.my_table__dbt_tmp " + f"rename to {project.database}.{project.test_schema}.my_table" + ) + self.assert_operation( + project, "test__get_rename_intermediate_sql", args, expected_statement + ) diff --git a/tests/functional/relation_tests/test_view.py b/tests/functional/relation_tests/test_view.py new file mode 100644 index 000000000..721455da1 --- /dev/null +++ b/tests/functional/relation_tests/test_view.py @@ -0,0 +1,25 @@ +from tests.functional.relation_tests.base import RelationOperation + + +class TestView(RelationOperation): + + def test_get_create_backup_and_rename_intermediate_sql(self, project): + args = { + "database": project.database, + "schema": project.test_schema, + "identifier": "my_view", + "relation_type": "view", + } + expected_statement = ( + f"alter view {project.database}.{project.test_schema}.my_view " + f"rename to {project.database}.{project.test_schema}.my_view__dbt_backup" + ) + self.assert_operation(project, "test__get_create_backup_sql", args, expected_statement) + + expected_statement = ( + f"alter view {project.database}.{project.test_schema}.my_view__dbt_tmp " + f"rename to {project.database}.{project.test_schema}.my_view" + ) + self.assert_operation( + project, "test__get_rename_intermediate_sql", args, expected_statement + )