From 8d958ecc5df829d070354c787e2fcb2d848071df Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Fri, 25 Oct 2024 16:30:07 -0400 Subject: [PATCH] refactor post deploy hooks to use sql facade and clean up tests with factories --- .../nativeapp/entities/application.py | 29 +- .../nativeapp/entities/application_package.py | 55 +-- src/snowflake/cli/api/entities/utils.py | 36 +- tests/nativeapp/test_post_deploy_for_app.py | 235 ++++----- .../nativeapp/test_post_deploy_for_package.py | 449 ++++++++++-------- 5 files changed, 399 insertions(+), 405 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 7173f64ef8..66c9813228 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -42,7 +42,6 @@ ) from snowflake.cli._plugins.nativeapp.utils import needs_confirmation from snowflake.cli._plugins.workspace.context import ActionContext -from snowflake.cli.api.cli_global_context import get_cli_context from snowflake.cli.api.entities.common import EntityBase, get_sql_executor from snowflake.cli.api.entities.utils import ( drop_generic_object, @@ -59,7 +58,6 @@ NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, ) -from snowflake.cli.api.metrics import CLICounterField from snowflake.cli.api.project.schemas.entities.common import ( EntityModelBase, Identifier, @@ -465,18 +463,15 @@ def create_or_upgrade_app( model = self._entity_model workspace_ctx = self._workspace_ctx console = workspace_ctx.console - project_root = workspace_ctx.project_root app_name = model.fqn.identifier debug_mode = model.debug if model.meta: app_role = model.meta.role or workspace_ctx.default_role app_warehouse = model.meta.warehouse or workspace_ctx.default_warehouse - post_deploy_hooks = model.meta.post_deploy else: app_role = workspace_ctx.default_role app_warehouse = workspace_ctx.default_warehouse - post_deploy_hooks = None package_name = package_model.fqn.identifier if package_model.meta and package_model.meta.role: @@ -587,23 +582,23 @@ def execute_post_deploy_hooks(self): workspace_ctx = self._workspace_ctx console = workspace_ctx.console project_root = workspace_ctx.project_root + app_role = (model.meta and model.meta.role) or workspace_ctx.default_role + app_warehouse = ( + model.meta and model.meta.warehouse + ) or workspace_ctx.default_warehouse app_name = model.fqn.identifier post_deploy_hooks = model.meta and model.meta.post_deploy - get_cli_context().metrics.set_counter_default( - CLICounterField.POST_DEPLOY_SCRIPTS, 0 + execute_post_deploy_hooks( + console=console, + project_root=project_root, + post_deploy_hooks=post_deploy_hooks, + deployed_object_type="application", + role_name=app_role, + warehouse_name=app_warehouse, + database_name=app_name, ) - if post_deploy_hooks: - with self.use_application_warehouse(): - execute_post_deploy_hooks( - console=console, - project_root=project_root, - post_deploy_hooks=post_deploy_hooks, - deployed_object_type="application", - database_name=app_name, - ) - @contextmanager def use_application_warehouse(self): model = self._entity_model diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py index 2864377851..1fb99e3c86 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -from contextlib import contextmanager from pathlib import Path from textwrap import dedent from typing import List, Literal, Optional, Union @@ -44,7 +43,6 @@ from snowflake.cli._plugins.stage.diff import DiffResult from snowflake.cli._plugins.stage.manager import StageManager from snowflake.cli._plugins.workspace.context import ActionContext -from snowflake.cli.api.cli_global_context import get_cli_context from snowflake.cli.api.entities.common import EntityBase, get_sql_executor from snowflake.cli.api.entities.utils import ( drop_generic_object, @@ -55,7 +53,6 @@ ) from snowflake.cli.api.errno import DOES_NOT_EXIST_OR_NOT_AUTHORIZED from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError -from snowflake.cli.api.metrics import CLICounterField from snowflake.cli.api.project.schemas.entities.common import ( EntityModelBase, Identifier, @@ -609,9 +606,6 @@ def _deploy( package_role = (model.meta and model.meta.role) or workspace_ctx.default_role package_distribution = model.distribution stage_fqn = stage_fqn or f"{package_name}.{model.stage}" - package_warehouse = ( - model.meta and model.meta.warehouse - ) or workspace_ctx.default_warehouse # 1. Create a bundle if one wasn't passed in bundle_map = bundle_map or self.bundle( @@ -648,8 +642,8 @@ def _deploy( print_diff=print_diff, ) - if run_post_deploy_hooks: - self.execute_post_deploy_hooks() + if run_post_deploy_hooks: + self.execute_post_deploy_hooks() if validate: self.validate_setup_script( @@ -911,27 +905,6 @@ def verify_project_distribution( return False return True - @contextmanager - def use_package_warehouse(self): - model = self._entity_model - ctx = self._workspace_ctx - package_warehouse = ( - model.meta and model.meta.warehouse and to_identifier(model.meta.warehouse) - ) or to_identifier(ctx.default_warehouse) - - if package_warehouse: - with get_sql_executor().use_warehouse(package_warehouse): - yield - else: - raise ClickException( - dedent( - f"""\ - Application package warehouse cannot be empty. - Please provide a value for it in your connection information or your project definition file. - """ - ) - ) - def create_app_package(self) -> None: """ Creates the application package with our up-to-date stage if none exists. @@ -984,23 +957,23 @@ def execute_post_deploy_hooks(self): workspace_ctx = self._workspace_ctx console = workspace_ctx.console project_root = workspace_ctx.project_root + package_role = (model.meta and model.meta.role) or workspace_ctx.default_role + package_warehouse = ( + model.meta and model.meta.warehouse + ) or workspace_ctx.default_warehouse package_name = model.fqn.identifier post_deploy_hooks = model.meta and model.meta.post_deploy - get_cli_context().metrics.set_counter_default( - CLICounterField.POST_DEPLOY_SCRIPTS, 0 + execute_post_deploy_hooks( + console=console, + project_root=project_root, + post_deploy_hooks=post_deploy_hooks, + deployed_object_type="application package", + role_name=package_role, + warehouse_name=package_warehouse, + database_name=package_name, ) - if post_deploy_hooks: - with self.use_package_warehouse(): - execute_post_deploy_hooks( - console=console, - project_root=project_root, - post_deploy_hooks=post_deploy_hooks, - deployed_object_type="application package", - database_name=package_name, - ) - def validate_setup_script( self, use_scratch_stage: bool, interactive: bool, force: bool ): diff --git a/src/snowflake/cli/api/entities/utils.py b/src/snowflake/cli/api/entities/utils.py index d131342cb0..a8b706b193 100644 --- a/src/snowflake/cli/api/entities/utils.py +++ b/src/snowflake/cli/api/entities/utils.py @@ -12,6 +12,7 @@ InvalidTemplateInFileError, MissingScriptError, ) +from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade from snowflake.cli._plugins.nativeapp.utils import verify_exists, verify_no_directories from snowflake.cli._plugins.stage.diff import ( DiffResult, @@ -195,36 +196,24 @@ def sync_deploy_root_with_stage( return diff -def _execute_sql_script( - script_content: str, - database_name: Optional[str] = None, -) -> None: - """ - Executing the provided SQL script content. - This assumes that a relevant warehouse is already active. - If database_name is passed in, it will be used first. - """ - try: - sql_executor = get_sql_executor() - if database_name is not None: - sql_executor.execute_query(f"use database {database_name}") - sql_executor.execute_queries(script_content) - except ProgrammingError as err: - generic_sql_error_handler(err) - - def execute_post_deploy_hooks( console: AbstractConsole, project_root: Path, post_deploy_hooks: Optional[List[PostDeployHook]], deployed_object_type: str, + role_name: str, database_name: str, + warehouse_name: str, ) -> None: """ Executes post-deploy hooks for the given object type. While executing SQL post deploy hooks, it first switches to the database provided in the input. All post deploy scripts templates will first be expanded using the global template context. """ + get_cli_context().metrics.set_counter_default( + CLICounterField.POST_DEPLOY_SCRIPTS, 0 + ) + if not post_deploy_hooks: return @@ -248,11 +237,16 @@ def execute_post_deploy_hooks( sql_scripts_paths, ) + sql_facade = get_snowflake_facade() + for index, sql_script_path in enumerate(display_paths): console.step(f"Executing SQL script: {sql_script_path}") - _execute_sql_script( - script_content=scripts_content_list[index], - database_name=database_name, + sql_facade.execute_user_script( + queries=scripts_content_list[index], + script_name=sql_script_path, + role=role_name, + warehouse=warehouse_name, + database=database_name, ) diff --git a/tests/nativeapp/test_post_deploy_for_app.py b/tests/nativeapp/test_post_deploy_for_app.py index f8f5a26f7a..3666092eff 100644 --- a/tests/nativeapp/test_post_deploy_for_app.py +++ b/tests/nativeapp/test_post_deploy_for_app.py @@ -14,6 +14,7 @@ import os from textwrap import dedent +from typing import Optional from unittest import mock import pytest @@ -23,8 +24,6 @@ ApplicationEntityModel, ) from snowflake.cli._plugins.nativeapp.exceptions import MissingScriptError -from snowflake.cli.api.console import cli_console as cc -from snowflake.cli.api.entities.utils import execute_post_deploy_hooks from snowflake.cli.api.exceptions import InvalidTemplate from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.cli.api.project.errors import SchemaValidationError @@ -46,31 +45,22 @@ MOCK_CONNECTION_DB = "tests.testing_utils.fixtures.MockConnectionCtx.database" MOCK_CONNECTION_WH = "tests.testing_utils.fixtures.MockConnectionCtx.warehouse" +DEFAULT_POST_DEPLOY_CONTENT_1 = dedent( + """\ + -- app post-deploy script (1/2) -@mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) -@mock.patch(CLI_GLOBAL_TEMPLATE_CONTEXT, new_callable=mock.PropertyMock) -@mock.patch.dict(os.environ, {"USER": "test_user"}) -@mock_connection() -def test_sql_scripts( - mock_conn, - mock_cli_ctx, - mock_execute_queries, - mock_execute_query, - temp_dir, - workspace_context, -): - mock_conn.return_value = MockConnectionCtx() - post_deploy_1 = dedent( - """\ - -- app post-deploy script (1/2) + select myapp; + select bar; + """ +) + +DEFAULT_POST_DEPLOY_CONTENT_2 = "-- app post-deploy script (2/2)\n" - select myapp; - select bar; - """ - ) - post_deploy_2 = "-- app post-deploy script (2/2)\n" +def app_post_deploy_project_factory( + custom_post_deploy_content_1: Optional[str] = None, + custom_post_deploy_content_2: Optional[str] = None, +) -> None: ProjectV2Factory( pdf__entities=dict( pkg=ApplicationPackageEntityModelFactory( @@ -87,11 +77,30 @@ def test_sql_scripts( ), pdf__env__foo="bar", files={ - "scripts/app_post_deploy1.sql": post_deploy_1, - "scripts/app_post_deploy2.sql": post_deploy_2, + "scripts/app_post_deploy1.sql": custom_post_deploy_content_1 + or DEFAULT_POST_DEPLOY_CONTENT_1, + "scripts/app_post_deploy2.sql": custom_post_deploy_content_2 + or DEFAULT_POST_DEPLOY_CONTENT_2, }, ) + +@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) +@mock.patch(CLI_GLOBAL_TEMPLATE_CONTEXT, new_callable=mock.PropertyMock) +@mock.patch.dict(os.environ, {"USER": "test_user"}) +@mock_connection() +def test_sql_scripts( + mock_conn, + mock_cli_ctx, + mock_execute_queries, + mock_execute_query, + temp_dir, + workspace_context, +): + mock_conn.return_value = MockConnectionCtx() + app_post_deploy_project_factory() + dm = DefinitionManager() mock_cli_ctx.return_value = dm.template_context app_model: ApplicationEntityModel = dm.project_definition.entities["app"] @@ -102,69 +111,15 @@ def test_sql_scripts( [ mock.call(f"use database {app_model.fqn.name}"), mock.call(f"use database {app_model.fqn.name}"), - ] + ], + any_order=True, ) assert mock_execute_queries.mock_calls == [ - mock.call(post_deploy_1), - mock.call(post_deploy_2), + mock.call(DEFAULT_POST_DEPLOY_CONTENT_1), + mock.call(DEFAULT_POST_DEPLOY_CONTENT_2), ] -@mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) -@mock.patch(CLI_GLOBAL_TEMPLATE_CONTEXT, new_callable=mock.PropertyMock) -@mock_connection() -@mock.patch(MOCK_CONNECTION_DB, new_callable=mock.PropertyMock) -@mock.patch(MOCK_CONNECTION_WH, new_callable=mock.PropertyMock) -@mock.patch.dict(os.environ, {"USER": "test_user"}) -def test_sql_scripts_with_no_warehouse_no_database( - mock_conn_wh, - mock_conn_db, - mock_conn, - mock_cli_ctx, - mock_execute_queries, - mock_execute_query, - project_directory, -): - mock_conn_wh.return_value = None - mock_conn_db.return_value = None - mock_conn.return_value = MockConnectionCtx(None) - with project_directory("napp_post_deploy_v2") as project_dir: - dm = DefinitionManager() - app_model: ApplicationEntityModel = dm.project_definition.entities["myapp"] - mock_cli_ctx.return_value = dm.template_context - - # Directly testing the function without the use_warehouse - # that ApplicationEntity.execute_post_deploy_hooks adds - execute_post_deploy_hooks( - console=cc, - project_root=dm.project_root, - post_deploy_hooks=app_model.meta.post_deploy, - deployed_object_type="application", - database_name=app_model.fqn.name, - ) - - # Verify no "use warehouse" - # Verify "use database" applies to current application - assert mock_execute_query.mock_calls == [ - mock.call("use database myapp"), - mock.call("use database myapp"), - ] - assert mock_execute_queries.mock_calls == [ - mock.call( - dedent( - """\ - -- app post-deploy script (1/2) - - select myapp; - select bar; - """ - ) - ), - mock.call("-- app post-deploy script (2/2)\n"), - ] - - @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() def test_missing_sql_script( @@ -215,47 +170,47 @@ def test_app_post_deploy_with_template( project_directory, template_syntax, workspace_context, + temp_dir, ): mock_conn.return_value = MockConnectionCtx() mock_cli_ctx.return_value = {"ctx": {"env": {"test": "test_value"}}} - with project_directory("napp_post_deploy_v2") as project_dir: - # edit scripts/app_post_deploy1.sql to include template variables - with open(project_dir / "scripts" / "app_post_deploy1.sql", "w") as f: - f.write( - dedent( - f"""\ - -- app post-deploy script (1/2) - - select '{template_syntax}'; - """ - ) - ) - dm = DefinitionManager() - app_model: ApplicationEntityModel = dm.project_definition.entities["myapp"] - app = ApplicationEntity(app_model, workspace_context) - - app.execute_post_deploy_hooks() - - mock_execute_query.assert_has_calls( - [ - mock.call(f"use database {app_model.fqn.name}"), - mock.call(f"use database {app_model.fqn.name}"), - ] - ) - assert mock_execute_queries.mock_calls == [ - # Verify template variables were expanded correctly - mock.call( - dedent( - """\ + # edit scripts/app_post_deploy1.sql to include template variables + app_post_deploy_project_factory( + custom_post_deploy_content_1=dedent( + f"""\ -- app post-deploy script (1/2) - select 'test_value'; + select '{template_syntax}'; """ - ) - ), - mock.call("-- app post-deploy script (2/2)\n"), - ] + ) + ) + dm = DefinitionManager() + app_model: ApplicationEntityModel = dm.project_definition.entities["app"] + app = ApplicationEntity(app_model, workspace_context) + + app.execute_post_deploy_hooks() + + mock_execute_query.assert_has_calls( + [ + mock.call(f"use database {app_model.fqn.name}"), + mock.call(f"use database {app_model.fqn.name}"), + ], + any_order=True, + ) + assert mock_execute_queries.mock_calls == [ + # Verify template variables were expanded correctly + mock.call( + dedent( + """\ + -- app post-deploy script (1/2) + + select 'test_value'; + """ + ) + ), + mock.call("-- app post-deploy script (2/2)\n"), + ] @mock.patch(SQL_EXECUTOR_EXECUTE) @@ -270,31 +225,31 @@ def test_app_post_deploy_with_mixed_syntax_template( mock_execute_query, project_directory, workspace_context, + temp_dir, ): mock_conn.return_value = MockConnectionCtx() mock_cli_ctx.return_value = {"ctx": {"env": {"test": "test_value"}}} - with project_directory("napp_post_deploy_v2") as project_dir: - # edit scripts/app_post_deploy1.sql to include template variables - with open(project_dir / "scripts" / "app_post_deploy1.sql", "w") as f: - f.write( - dedent( - """\ - -- app post-deploy script (1/2) - - select '<% ctx.env.test %>'; - select '&{ ctx.env.test }'; - """ - ) - ) - dm = DefinitionManager() - app_model: ApplicationEntityModel = dm.project_definition.entities["myapp"] - app = ApplicationEntity(app_model, workspace_context) - - with pytest.raises(InvalidTemplate) as err: - app.execute_post_deploy_hooks() + # edit scripts/app_post_deploy1.sql to include template variables + app_post_deploy_project_factory( + custom_post_deploy_content_1=dedent( + """\ + -- app post-deploy script (1/2) - assert ( - "The SQL query in scripts/app_post_deploy1.sql mixes &{ ... } syntax and <% ... %> syntax." - == str(err.value) + select '<% ctx.env.test %>'; + select '&{ ctx.env.test }'; + """ ) + ) + + dm = DefinitionManager() + app_model: ApplicationEntityModel = dm.project_definition.entities["app"] + app = ApplicationEntity(app_model, workspace_context) + + with pytest.raises(InvalidTemplate) as err: + app.execute_post_deploy_hooks() + + assert ( + "The SQL query in scripts/app_post_deploy1.sql mixes &{ ... } syntax and <% ... %> syntax." + == str(err.value) + ) diff --git a/tests/nativeapp/test_post_deploy_for_package.py b/tests/nativeapp/test_post_deploy_for_package.py index f67bf66901..7b2fb5a106 100644 --- a/tests/nativeapp/test_post_deploy_for_package.py +++ b/tests/nativeapp/test_post_deploy_for_package.py @@ -15,6 +15,7 @@ import os from textwrap import dedent +from typing import Optional from unittest import mock import pytest @@ -23,6 +24,7 @@ ApplicationPackageEntityModel, ) from snowflake.cli._plugins.nativeapp.exceptions import MissingScriptError +from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import UserScriptError from snowflake.cli.api.console import cli_console as cc from snowflake.cli.api.entities.utils import execute_post_deploy_hooks from snowflake.cli.api.exceptions import InvalidTemplate @@ -30,6 +32,12 @@ from snowflake.cli.api.project.errors import SchemaValidationError from snowflake.connector import ProgrammingError +from tests.nativeapp.factories import ( + ApplicationEntityModelFactory, + ApplicationPackageEntityModelFactory, + ProjectV2Factory, + ProjectV11Factory, +) from tests.nativeapp.patch_utils import mock_connection from tests.nativeapp.utils import ( CLI_GLOBAL_TEMPLATE_CONTEXT, @@ -39,6 +47,45 @@ ) from tests.testing_utils.fixtures import MockConnectionCtx +DEFAULT_POST_DEPLOY_CONTENT_1 = dedent( + """\ + -- package post-deploy script (1/2) + + select myapp; + select package_bar; + """ +) + +DEFAULT_POST_DEPLOY_CONTENT_2 = "-- package post-deploy script (2/2)\n" + + +def pkg_post_deploy_project_factory( + custom_post_deploy_content_1: Optional[str] = None, + custom_post_deploy_content_2: Optional[str] = None, +) -> None: + ProjectV2Factory( + pdf__entities=dict( + pkg=ApplicationPackageEntityModelFactory( + identifier="myapp_pkg", + meta__post_deploy=[ + {"sql_script": "scripts/pkg_post_deploy1.sql"}, + {"sql_script": "scripts/pkg_post_deploy2.sql"}, + ], + ), + app=ApplicationEntityModelFactory( + identifier="myapp", + fromm__target="pkg", + ), + ), + pdf__env__foo="bar", + files={ + "scripts/pkg_post_deploy1.sql": custom_post_deploy_content_1 + or DEFAULT_POST_DEPLOY_CONTENT_1, + "scripts/pkg_post_deploy2.sql": custom_post_deploy_content_2 + or DEFAULT_POST_DEPLOY_CONTENT_2, + }, + ) + @mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) @@ -53,44 +100,32 @@ def test_package_post_deploy_scripts( project_directory, mock_cursor, workspace_context, + temp_dir, ): mock_conn.return_value = MockConnectionCtx() - with project_directory("napp_post_deploy_v2") as project_dir: - dm = DefinitionManager(project_dir) - pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities[ - "myapp_pkg" - ] - pkg = ApplicationPackageEntity(pkg_model, workspace_context) - mock_cli_ctx.return_value = dm.template_context - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([(workspace_context.default_warehouse,)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use database myapp_pkg_test_user")), - (None, mock.call("use database myapp_pkg_test_user")), - ] - ) - mock_execute_query.side_effect = side_effects - - pkg.execute_post_deploy_hooks() - - assert mock_execute_query.mock_calls == expected - assert mock_execute_queries.mock_calls == [ - # Verify template variables were expanded correctly - mock.call( - dedent( - """\ - -- package post-deploy script (1/2) - - select myapp; - select package_bar; - """ - ) - ), - mock.call("-- package post-deploy script (2/2)\n"), - ] + + pkg_post_deploy_project_factory() + + dm = DefinitionManager() + pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities["pkg"] + pkg = ApplicationPackageEntity(pkg_model, workspace_context) + mock_cli_ctx.return_value = dm.template_context + + pkg.execute_post_deploy_hooks() + + mock_execute_query.assert_has_calls( + [ + mock.call("select current_warehouse()"), + mock.call(f"use database {pkg_model.fqn.name}"), + mock.call(f"use database {pkg_model.fqn.name}"), + ], + any_order=True, + ) + assert mock_execute_queries.mock_calls == [ + # Verify template variables were expanded correctly + mock.call(DEFAULT_POST_DEPLOY_CONTENT_1), + mock.call(DEFAULT_POST_DEPLOY_CONTENT_2), + ] @mock.patch(SQL_EXECUTOR_EXECUTE) @@ -104,28 +139,39 @@ def test_package_post_deploy_scripts_with_no_scripts( mock_execute_queries, mock_execute_query, project_directory, + workspace_context, + temp_dir, ): mock_conn.return_value = MockConnectionCtx() - with project_directory( - "napp_project_2", - {"entities": {"myapp_pkg_polly": {"meta": {"post_deploy": []}}}}, - ) as project_dir: - dm = DefinitionManager(project_dir) - pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities[ - "myapp_pkg_polly" - ] - mock_cli_ctx.return_value = dm.template_context + ProjectV2Factory( + pdf__entities=dict( + pkg=ApplicationPackageEntityModelFactory( + identifier="myapp_pkg", + ), + app=ApplicationEntityModelFactory( + identifier="myapp", + fromm__target="pkg", + ), + ), + pdf__env__foo="bar", + ) - execute_post_deploy_hooks( - console=cc, - project_root=project_dir, - post_deploy_hooks=pkg_model.meta.post_deploy, - deployed_object_type=pkg_model.get_type(), - database_name=pkg_model.fqn.name, - ) + dm = DefinitionManager() + pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities["pkg"] + mock_cli_ctx.return_value = dm.template_context - assert mock_execute_query.mock_calls == [] - assert mock_execute_queries.mock_calls == [] + execute_post_deploy_hooks( + console=cc, + project_root=temp_dir, + post_deploy_hooks=pkg_model.meta.post_deploy, + deployed_object_type=pkg_model.get_type(), + database_name=pkg_model.fqn.name, + role_name=workspace_context.default_role, + warehouse_name=workspace_context.default_warehouse, + ) + + assert mock_execute_query.mock_calls == [] + assert mock_execute_queries.mock_calls == [] @mock.patch(SQL_EXECUTOR_EXECUTE) @@ -133,85 +179,125 @@ def test_package_post_deploy_scripts_with_no_scripts( @mock.patch.dict(os.environ, {"USER": "test_user"}) @mock_connection() def test_package_post_deploy_scripts_with_non_existing_scripts( - mock_conn, mock_cli_ctx, mock_execute_query, project_directory, mock_cursor + mock_conn, + mock_cli_ctx, + mock_execute_query, + project_directory, + mock_cursor, + workspace_context, + temp_dir, ): mock_conn.return_value = MockConnectionCtx() - with project_directory("napp_post_deploy_missing_file_v2") as project_dir: - dm = DefinitionManager(project_dir) - pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities[ - "myapp_pkg" + + ProjectV2Factory( + pdf__entities=dict( + pkg=ApplicationPackageEntityModelFactory( + identifier="myapp_pkg", + meta__post_deploy=[ + {"sql_script": "scripts/package_missing_script.sql"}, + ], + ), + app=ApplicationEntityModelFactory( + identifier="myapp", + fromm__target="pkg", + ), + ), + pdf__env__foo="bar", + ) + + dm = DefinitionManager() + pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities["pkg"] + mock_cli_ctx.return_value = dm.template_context + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([("MockWarehouse",)], []), + mock.call("select current_warehouse()"), + ), ] - mock_cli_ctx.return_value = dm.template_context - - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("MockWarehouse",)], []), - mock.call("select current_warehouse()"), - ), - ] - ) - mock_execute_query.side_effect = side_effects - - with pytest.raises(MissingScriptError) as err: - execute_post_deploy_hooks( - console=cc, - project_root=project_dir, - post_deploy_hooks=pkg_model.meta.post_deploy, - deployed_object_type=pkg_model.get_type(), - database_name=pkg_model.fqn.name, - ) + ) + mock_execute_query.side_effect = side_effects - assert ( - err.value.message - == 'Script "scripts/package_missing_script.sql" does not exist' + with pytest.raises(MissingScriptError) as err: + execute_post_deploy_hooks( + console=cc, + project_root=temp_dir, + post_deploy_hooks=pkg_model.meta.post_deploy, + deployed_object_type=pkg_model.get_type(), + database_name=pkg_model.fqn.name, + role_name=workspace_context.default_role, + warehouse_name=workspace_context.default_warehouse, ) + assert ( + err.value.message + == 'Script "scripts/package_missing_script.sql" does not exist' + ) + @mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) @mock.patch(CLI_GLOBAL_TEMPLATE_CONTEXT, new_callable=mock.PropertyMock) @mock.patch.dict(os.environ, {"USER": "test_user"}) @mock_connection() def test_package_post_deploy_scripts_with_sql_error( mock_conn, mock_cli_ctx, + mock_execute_queries, mock_execute_query, project_directory, + workspace_context, + temp_dir, ): mock_conn.return_value = MockConnectionCtx() - with project_directory("napp_post_deploy_v2") as project_dir: - dm = DefinitionManager(project_dir) - pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities[ - "myapp_pkg" - ] - mock_cli_ctx.return_value = dm.template_context - mock_execute_query.side_effect = ProgrammingError() - - with pytest.raises(ProgrammingError): - execute_post_deploy_hooks( - console=cc, - project_root=project_dir, - post_deploy_hooks=pkg_model.meta.post_deploy, - deployed_object_type=pkg_model.get_type(), - database_name=pkg_model.fqn.name, - ) + + pkg_post_deploy_project_factory() + + dm = DefinitionManager() + pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities["pkg"] + mock_cli_ctx.return_value = dm.template_context + mock_execute_queries.side_effect = ProgrammingError() + + with pytest.raises(UserScriptError): + execute_post_deploy_hooks( + console=cc, + project_root=temp_dir, + post_deploy_hooks=pkg_model.meta.post_deploy, + deployed_object_type=pkg_model.get_type(), + database_name=pkg_model.fqn.name, + role_name=workspace_context.default_role, + warehouse_name=workspace_context.default_warehouse, + ) @mock.patch.dict(os.environ, {"USER": "test_user"}) def test_package_scripts_and_post_deploy_found( - project_directory, + temp_dir, ): - with project_directory( - "napp_post_deploy", - {"native_app": {"package": {"scripts": ["scripts/package_post_deploy2.sql"]}}}, - ) as project_dir: - with pytest.raises(SchemaValidationError) as err: - DefinitionManager(project_dir).project_definition # noqa - - assert ( - "package.scripts and package.post_deploy fields cannot be used together" - in err.value.message - ) + ProjectV11Factory( + pdf__native_app__package__scripts=["scripts/package_script1.sql"], + pdf__native_app__artifacts=["README.md", "setup.sql", "manifest.yml"], + pdf__native_app__package__post_deploy=[ + {"sql_script": "post_deploy1.sql"}, + ], + pdf__native_app__package__warehouse="non_existent_warehouse", + files={ + "README.md": "", + "setup.sql": "select 1", + "manifest.yml": "\n", + "scripts/package_script1.sql": "\n", + "post_deploy1.sql": "\n", + }, + ) + + with pytest.raises(SchemaValidationError) as err: + DefinitionManager().project_definition # noqa + + assert ( + "package.scripts and package.post_deploy fields cannot be used together" + in err.value.message + ) @pytest.mark.parametrize( @@ -231,56 +317,48 @@ def test_package_post_deploy_scripts_with_templates( template_syntax, mock_cursor, workspace_context, + temp_dir, ): mock_conn.return_value = MockConnectionCtx() - with project_directory("napp_post_deploy_v2") as project_dir: - # edit scripts/package_post_deploy1.sql to include template variables - with open(project_dir / "scripts" / "package_post_deploy1.sql", "w") as f: - f.write( - dedent( - f"""\ - -- package post-deploy script (1/2) - - select '{template_syntax}'; - """ - ) - ) - - dm = DefinitionManager(project_dir, {"ctx": {"env": {"test": "test_value"}}}) - pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities[ - "myapp_pkg" - ] - pkg = ApplicationPackageEntity(pkg_model, workspace_context) - mock_cli_ctx.return_value = dm.template_context - - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([(workspace_context.default_warehouse,)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use database myapp_pkg_test_user")), - (None, mock.call("use database myapp_pkg_test_user")), - ] - ) - mock_execute_query.side_effect = side_effects - - pkg.execute_post_deploy_hooks() - assert mock_execute_query.mock_calls == expected - assert mock_execute_queries.mock_calls == [ - # Verify template variables were expanded correctly - mock.call( - dedent( - """\ - -- package post-deploy script (1/2) + pkg_post_deploy_project_factory( + custom_post_deploy_content_1=dedent( + f"""\ + -- package post-deploy script (1/2) - select 'test_value'; - """ - ) - ), - mock.call("-- package post-deploy script (2/2)\n"), - ] + select '{template_syntax}'; + """ + ) + ) + + dm = DefinitionManager(context_overrides={"ctx": {"env": {"test": "test_value"}}}) + pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities["pkg"] + pkg = ApplicationPackageEntity(pkg_model, workspace_context) + mock_cli_ctx.return_value = dm.template_context + + pkg.execute_post_deploy_hooks() + + mock_execute_query.assert_has_calls( + [ + mock.call("select current_warehouse()"), + mock.call(f"use database {pkg_model.fqn.name}"), + mock.call(f"use database {pkg_model.fqn.name}"), + ], + any_order=True, + ) + assert mock_execute_queries.mock_calls == [ + # Verify template variables were expanded correctly + mock.call( + dedent( + """\ + -- package post-deploy script (1/2) + + select 'test_value'; + """ + ) + ), + mock.call("-- package post-deploy script (2/2)\n"), + ] @mock.patch(SQL_EXECUTOR_EXECUTE) @@ -294,38 +372,37 @@ def test_package_post_deploy_scripts_with_mix_syntax_templates( mock_execute_queries, mock_execute_query, project_directory, + workspace_context, + temp_dir, ): mock_conn.return_value = MockConnectionCtx() - with project_directory("napp_post_deploy_v2") as project_dir: - # edit scripts/package_post_deploy1.sql to include template variables - with open(project_dir / "scripts" / "package_post_deploy1.sql", "w") as f: - f.write( - dedent( - """\ - -- package post-deploy script (1/2) - - select '<% ctx.env.test %>'; - select '&{ ctx.env.test }'; - """ - ) - ) + pkg_post_deploy_project_factory( + custom_post_deploy_content_1=dedent( + """\ + -- package post-deploy script (1/2) + + select '<% ctx.env.test %>'; + select '&{ ctx.env.test }'; + """ + ) + ) - dm = DefinitionManager(project_dir, {"ctx": {"env": {"test": "test_value"}}}) - pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities[ - "myapp_pkg" - ] - mock_cli_ctx.return_value = dm.template_context - - with pytest.raises(InvalidTemplate) as err: - execute_post_deploy_hooks( - console=cc, - project_root=project_dir, - post_deploy_hooks=pkg_model.meta.post_deploy, - deployed_object_type=pkg_model.get_type(), - database_name=pkg_model.fqn.name, - ) + dm = DefinitionManager(context_overrides={"ctx": {"env": {"test": "test_value"}}}) + pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities["pkg"] + mock_cli_ctx.return_value = dm.template_context - assert ( - "The SQL query in scripts/package_post_deploy1.sql mixes &{ ... } syntax and <% ... %> syntax." - == str(err.value) + with pytest.raises(InvalidTemplate) as err: + execute_post_deploy_hooks( + console=cc, + project_root=temp_dir, + post_deploy_hooks=pkg_model.meta.post_deploy, + deployed_object_type=pkg_model.get_type(), + database_name=pkg_model.fqn.name, + role_name=workspace_context.default_role, + warehouse_name=workspace_context.default_warehouse, ) + + assert ( + "The SQL query in scripts/pkg_post_deploy1.sql mixes &{ ... } syntax and <% ... %> syntax." + == str(err.value) + )