From 110dcb805367e1be4a2e4c163957dcf6f57d590e Mon Sep 17 00:00:00 2001 From: Guy Bloom Date: Tue, 27 Aug 2024 17:57:33 -0400 Subject: [PATCH] Workspaces application package - validate setup script (#1495) * extract validate logic to entity * ws deploy validate option * assert validation --- .../cli/_plugins/nativeapp/manager.py | 87 ++++++----------- .../cli/_plugins/workspace/commands.py | 3 + .../entities/application_package_entity.py | 96 ++++++++++++++++++- src/snowflake/cli/api/entities/utils.py | 7 ++ tests/nativeapp/test_manager.py | 18 ++-- tests/nativeapp/test_teardown_processor.py | 2 +- .../test_application_package_entity.py | 9 +- .../nativeapp/__snapshots__/test_deploy.ambr | 12 ++- tests_integration/nativeapp/test_deploy.py | 41 ++++++-- 9 files changed, 191 insertions(+), 84 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/manager.py b/src/snowflake/cli/_plugins/nativeapp/manager.py index ad68a6b767..9705fa8e3f 100644 --- a/src/snowflake/cli/_plugins/nativeapp/manager.py +++ b/src/snowflake/cli/_plugins/nativeapp/manager.py @@ -14,7 +14,6 @@ from __future__ import annotations -import json import time from abc import ABC, abstractmethod from contextlib import contextmanager @@ -37,9 +36,7 @@ NAME_COL, ) from snowflake.cli._plugins.nativeapp.exceptions import ( - ApplicationPackageDoesNotExistError, NoEventTableForAccount, - SetupScriptFailedValidation, ) from snowflake.cli._plugins.nativeapp.project_model import ( NativeAppProjectModel, @@ -47,7 +44,6 @@ from snowflake.cli._plugins.stage.diff import ( DiffResult, ) -from snowflake.cli._plugins.stage.manager import StageManager from snowflake.cli.api.console import cli_console as cc from snowflake.cli.api.entities.application_package_entity import ( ApplicationPackageEntity, @@ -57,10 +53,6 @@ generic_sql_error_handler, sync_deploy_root_with_stage, ) -from snowflake.cli.api.errno import ( - DOES_NOT_EXIST_OR_NOT_AUTHORIZED, -) -from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError from snowflake.cli.api.project.schemas.entities.common import PostDeployHook from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping @@ -381,59 +373,38 @@ def deploy( return diff - def validate(self, use_scratch_stage: bool = False): - """Validates Native App setup script SQL.""" - with cc.phase(f"Validating Snowflake Native App setup script."): - validation_result = self.get_validation_result(use_scratch_stage) - - # First print warnings, regardless of the outcome of validation - for warning in validation_result.get("warnings", []): - cc.warning(_validation_item_to_str(warning)) - - # Then print errors - for error in validation_result.get("errors", []): - # Print them as warnings for now since we're going to be - # revamping CLI output soon - cc.warning(_validation_item_to_str(error)) + def deploy_to_scratch_stage_fn(self): + bundle_map = self.build_bundle() + self.deploy( + bundle_map=bundle_map, + prune=True, + recursive=True, + stage_fqn=self.scratch_stage_fqn, + validate=False, + print_diff=False, + ) - # Then raise an exception if validation failed - if validation_result["status"] == "FAIL": - raise SetupScriptFailedValidation() + def validate(self, use_scratch_stage: bool = False): + return ApplicationPackageEntity.validate_setup_script( + console=cc, + package_name=self.package_name, + package_role=self.package_role, + stage_fqn=self.stage_fqn, + use_scratch_stage=use_scratch_stage, + scratch_stage_fqn=self.scratch_stage_fqn, + deploy_to_scratch_stage_fn=self.deploy_to_scratch_stage_fn, + ) def get_validation_result(self, use_scratch_stage: bool): - """Call system$validate_native_app_setup() to validate deployed Native App setup script.""" - stage_fqn = self.stage_fqn - if use_scratch_stage: - stage_fqn = self.scratch_stage_fqn - bundle_map = self.build_bundle() - self.deploy( - bundle_map=bundle_map, - prune=True, - recursive=True, - stage_fqn=stage_fqn, - validate=False, - print_diff=False, - ) - prefixed_stage_fqn = StageManager.get_standard_stage_prefix(stage_fqn) - try: - cursor = self._execute_query( - f"call system$validate_native_app_setup('{prefixed_stage_fqn}')" - ) - except ProgrammingError as err: - if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED: - raise ApplicationPackageDoesNotExistError(self.package_name) - generic_sql_error_handler(err) - else: - if not cursor.rowcount: - raise SnowflakeSQLExecutionError() - return json.loads(cursor.fetchone()[0]) - finally: - if use_scratch_stage: - cc.step(f"Dropping stage {self.scratch_stage_fqn}.") - with self.use_role(self.package_role): - self._execute_query( - f"drop stage if exists {self.scratch_stage_fqn}" - ) + return ApplicationPackageEntity.get_validation_result( + console=cc, + package_name=self.package_name, + package_role=self.package_role, + stage_fqn=self.stage_fqn, + use_scratch_stage=use_scratch_stage, + scratch_stage_fqn=self.scratch_stage_fqn, + deploy_to_scratch_stage_fn=self.deploy_to_scratch_stage_fn, + ) def get_events( # type: ignore [return] self, diff --git a/src/snowflake/cli/_plugins/workspace/commands.py b/src/snowflake/cli/_plugins/workspace/commands.py index 02a69568a9..998d5132ea 100644 --- a/src/snowflake/cli/_plugins/workspace/commands.py +++ b/src/snowflake/cli/_plugins/workspace/commands.py @@ -23,6 +23,7 @@ import yaml from click import ClickException from snowflake.cli._plugins.nativeapp.artifacts import BundleMap +from snowflake.cli._plugins.nativeapp.common_flags import ValidateOption from snowflake.cli._plugins.snowpark.commands import migrate_v1_snowpark_to_v2 from snowflake.cli._plugins.streamlit.commands import migrate_v1_streamlit_to_v2 from snowflake.cli._plugins.workspace.manager import WorkspaceManager @@ -145,6 +146,7 @@ def deploy( unspecified, the command syncs all local changes to the stage.""" ).strip(), ), + validate: bool = ValidateOption, **options, ): """ @@ -174,5 +176,6 @@ def deploy( prune=prune, recursive=recursive, paths=paths, + validate=validate, ) return MessageResult("Deployed successfully.") diff --git a/src/snowflake/cli/api/entities/application_package_entity.py b/src/snowflake/cli/api/entities/application_package_entity.py index 8c318d2805..37d2851d4b 100644 --- a/src/snowflake/cli/api/entities/application_package_entity.py +++ b/src/snowflake/cli/api/entities/application_package_entity.py @@ -1,7 +1,8 @@ +import json from contextlib import contextmanager from pathlib import Path from textwrap import dedent -from typing import List, Optional +from typing import Callable, List, Optional from click import ClickException from snowflake.cli._plugins.nativeapp.artifacts import build_bundle @@ -16,7 +17,10 @@ ) from snowflake.cli._plugins.nativeapp.exceptions import ( ApplicationPackageAlreadyExistsError, + ApplicationPackageDoesNotExistError, + SetupScriptFailedValidation, ) +from snowflake.cli._plugins.stage.manager import StageManager from snowflake.cli._plugins.workspace.action_context import ActionContext from snowflake.cli.api.console.abc import AbstractConsole from snowflake.cli.api.entities.common import EntityBase, get_sql_executor @@ -25,6 +29,10 @@ generic_sql_error_handler, render_script_templates, sync_deploy_root_with_stage, + validation_item_to_str, +) +from snowflake.cli.api.errno import ( + DOES_NOT_EXIST_OR_NOT_AUTHORIZED, ) from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError from snowflake.cli.api.project.schemas.entities.application_package_entity_model import ( @@ -65,6 +73,7 @@ def action_deploy( prune: bool, recursive: bool, paths: List[Path], + validate: bool, ): model = self._entity_model package_name = model.fqn.identifier @@ -103,7 +112,17 @@ def action_deploy( ) # TODO Execute post-deploy hooks - # TODO Validate + + if validate: + self.validate_setup_script( + console=ctx.console, + package_name=package_name, + package_role=package_role, + stage_fqn=stage_fqn, + use_scratch_stage=False, + scratch_stage_fqn="", + deploy_to_scratch_stage_fn=lambda *args: None, + ) @staticmethod def get_existing_app_pkg_info( @@ -306,3 +325,76 @@ def create_app_package( """ ) ) + + @classmethod + def validate_setup_script( + cls, + console: AbstractConsole, + package_name: str, + package_role: str, + stage_fqn: str, + use_scratch_stage: bool, + scratch_stage_fqn: str, + deploy_to_scratch_stage_fn: Callable, + ): + """Validates Native App setup script SQL.""" + with console.phase(f"Validating Snowflake Native App setup script."): + validation_result = cls.get_validation_result( + console=console, + package_name=package_name, + package_role=package_role, + stage_fqn=stage_fqn, + use_scratch_stage=use_scratch_stage, + scratch_stage_fqn=scratch_stage_fqn, + deploy_to_scratch_stage_fn=deploy_to_scratch_stage_fn, + ) + + # First print warnings, regardless of the outcome of validation + for warning in validation_result.get("warnings", []): + console.warning(validation_item_to_str(warning)) + + # Then print errors + for error in validation_result.get("errors", []): + # Print them as warnings for now since we're going to be + # revamping CLI output soon + console.warning(validation_item_to_str(error)) + + # Then raise an exception if validation failed + if validation_result["status"] == "FAIL": + raise SetupScriptFailedValidation() + + @staticmethod + def get_validation_result( + console: AbstractConsole, + package_name: str, + package_role: str, + stage_fqn: str, + use_scratch_stage: bool, + scratch_stage_fqn: str, + deploy_to_scratch_stage_fn: Callable, + ): + """Call system$validate_native_app_setup() to validate deployed Native App setup script.""" + if use_scratch_stage: + stage_fqn = scratch_stage_fqn + deploy_to_scratch_stage_fn() + prefixed_stage_fqn = StageManager.get_standard_stage_prefix(stage_fqn) + sql_executor = get_sql_executor() + try: + cursor = sql_executor.execute_query( + f"call system$validate_native_app_setup('{prefixed_stage_fqn}')" + ) + except ProgrammingError as err: + if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED: + raise ApplicationPackageDoesNotExistError(package_name) + generic_sql_error_handler(err) + else: + if not cursor.rowcount: + raise SnowflakeSQLExecutionError() + return json.loads(cursor.fetchone()[0]) + finally: + if use_scratch_stage: + console.step(f"Dropping stage {scratch_stage_fqn}.") + with sql_executor.use_role(package_role): + sql_executor.execute_query( + f"drop stage if exists {scratch_stage_fqn}" + ) diff --git a/src/snowflake/cli/api/entities/utils.py b/src/snowflake/cli/api/entities/utils.py index 19e8ca7b17..e67f620ea0 100644 --- a/src/snowflake/cli/api/entities/utils.py +++ b/src/snowflake/cli/api/entities/utils.py @@ -319,3 +319,10 @@ def render_script_templates( raise InvalidScriptError(relpath, e) from e return scripts_contents + + +def validation_item_to_str(item: dict[str, str | int]): + s = item["message"] + if item["errorCode"]: + s = f"{s} (error code {item['errorCode']})" + return s diff --git a/tests/nativeapp/test_manager.py b/tests/nativeapp/test_manager.py index 0ce1369eb5..495ca4df1b 100644 --- a/tests/nativeapp/test_manager.py +++ b/tests/nativeapp/test_manager.py @@ -40,7 +40,6 @@ ) from snowflake.cli._plugins.nativeapp.manager import ( NativeAppManager, - SnowflakeSQLExecutionError, ) from snowflake.cli._plugins.stage.diff import ( DiffResult, @@ -51,6 +50,7 @@ ensure_correct_owner, ) from snowflake.cli.api.errno import DOES_NOT_EXIST_OR_NOT_AUTHORIZED +from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.connector import ProgrammingError from snowflake.connector.cursor import DictCursor @@ -957,7 +957,7 @@ def test_get_paths_to_sync( assert result.sort() == [StagePath(p) for p in expected_result].sort() -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) def test_validate_passing(mock_execute, temp_dir, mock_cursor): create_named_file( file_name="snowflake.yml", @@ -984,7 +984,7 @@ def test_validate_passing(mock_execute, temp_dir, mock_cursor): assert mock_execute.mock_calls == expected -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(f"{NATIVEAPP_MODULE}.cc.warning") def test_validate_passing_with_warnings( mock_warning, mock_execute, temp_dir, mock_cursor @@ -1026,7 +1026,7 @@ def test_validate_passing_with_warnings( assert mock_execute.mock_calls == expected -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(f"{NATIVEAPP_MODULE}.cc.warning") def test_validate_failing(mock_warning, mock_execute, temp_dir, mock_cursor): create_named_file( @@ -1083,7 +1083,7 @@ def test_validate_failing(mock_warning, mock_execute, temp_dir, mock_cursor): assert mock_execute.mock_calls == expected -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) def test_validate_query_error(mock_execute, temp_dir, mock_cursor): create_named_file( file_name="snowflake.yml", @@ -1110,7 +1110,7 @@ def test_validate_query_error(mock_execute, temp_dir, mock_cursor): assert mock_execute.mock_calls == expected -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) def test_validate_not_deployed(mock_execute, temp_dir, mock_cursor): create_named_file( file_name="snowflake.yml", @@ -1142,7 +1142,7 @@ def test_validate_not_deployed(mock_execute, temp_dir, mock_cursor): @mock.patch(NATIVEAPP_MANAGER_BUILD_BUNDLE) @mock.patch(NATIVEAPP_MANAGER_DEPLOY) -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) def test_validate_use_scratch_stage( mock_execute, mock_deploy, mock_build_bundle, temp_dir, mock_cursor ): @@ -1194,7 +1194,7 @@ def test_validate_use_scratch_stage( @mock.patch(NATIVEAPP_MANAGER_BUILD_BUNDLE) @mock.patch(NATIVEAPP_MANAGER_DEPLOY) -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) def test_validate_failing_drops_scratch_stage( mock_execute, mock_deploy, mock_build_bundle, temp_dir, mock_cursor ): @@ -1258,7 +1258,7 @@ def test_validate_failing_drops_scratch_stage( assert mock_execute.mock_calls == expected -@mock.patch(NATIVEAPP_MANAGER_EXECUTE) +@mock.patch(SQL_EXECUTOR_EXECUTE) def test_validate_raw_returns_data(mock_execute, temp_dir, mock_cursor): create_named_file( file_name="snowflake.yml", diff --git a/tests/nativeapp/test_teardown_processor.py b/tests/nativeapp/test_teardown_processor.py index 47f0da7bb1..d43d2d312b 100644 --- a/tests/nativeapp/test_teardown_processor.py +++ b/tests/nativeapp/test_teardown_processor.py @@ -26,7 +26,6 @@ CouldNotDropApplicationPackageWithVersions, UnexpectedOwnerError, ) -from snowflake.cli._plugins.nativeapp.manager import SnowflakeSQLExecutionError from snowflake.cli._plugins.nativeapp.teardown_processor import ( NativeAppTeardownProcessor, ) @@ -34,6 +33,7 @@ APPLICATION_NO_LONGER_AVAILABLE, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, ) +from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.connector import ProgrammingError from snowflake.connector.cursor import DictCursor diff --git a/tests/workspace/test_application_package_entity.py b/tests/workspace/test_application_package_entity.py index a7325d4b10..786c44f912 100644 --- a/tests/workspace/test_application_package_entity.py +++ b/tests/workspace/test_application_package_entity.py @@ -31,6 +31,7 @@ from snowflake.connector.cursor import DictCursor from tests.nativeapp.utils import ( + APP_PACKAGE_ENTITY, APPLICATION_PACKAGE_ENTITY_MODULE, SQL_EXECUTOR_EXECUTE, mock_execute_helper, @@ -65,8 +66,9 @@ def test_bundle(project_directory): @mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(f"{APP_PACKAGE_ENTITY}.validate_setup_script") @mock.patch(f"{APPLICATION_PACKAGE_ENTITY_MODULE}.sync_deploy_root_with_stage") -def test_deploy(mock_sync, mock_execute, project_directory, mock_cursor): +def test_deploy(mock_sync, mock_validate, mock_execute, project_directory, mock_cursor): side_effects, expected = mock_execute_helper( [ ( @@ -121,7 +123,9 @@ def test_deploy(mock_sync, mock_execute, project_directory, mock_cursor): app_pkg, bundle_ctx, mock_console = _get_app_pkg_entity(project_directory) - app_pkg.action_deploy(bundle_ctx, prune=False, recursive=False, paths=["a/b", "c"]) + app_pkg.action_deploy( + bundle_ctx, prune=False, recursive=False, paths=["a/b", "c"], validate=True + ) mock_sync.assert_called_once_with( console=mock_console, @@ -136,4 +140,5 @@ def test_deploy(mock_sync, mock_execute, project_directory, mock_cursor): local_paths_to_sync=["a/b", "c"], print_diff=True, ) + mock_validate.assert_called_once() assert mock_execute.mock_calls == expected diff --git a/tests_integration/nativeapp/__snapshots__/test_deploy.ambr b/tests_integration/nativeapp/__snapshots__/test_deploy.ambr index e94cf0834a..365a612d14 100644 --- a/tests_integration/nativeapp/__snapshots__/test_deploy.ambr +++ b/tests_integration/nativeapp/__snapshots__/test_deploy.ambr @@ -39,6 +39,7 @@ added: app/manifest.yml -> manifest.yml added: app/setup_script.sql -> setup_script.sql Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Validating Snowflake Native App setup script. Deployed successfully. ''' @@ -83,6 +84,7 @@ added: app/manifest.yml -> manifest.yml added: app/setup_script.sql -> setup_script.sql Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Validating Snowflake Native App setup script. Deployed successfully. ''' @@ -113,7 +115,7 @@ ''' # --- -# name: test_nativeapp_deploy_files[ws deploy --entity-id=pkg-napp_init_v2] +# name: test_nativeapp_deploy_files[ws deploy --entity-id=pkg --no-validate-napp_init_v2] ''' Creating new application package myapp_pkg_@@USER@@ in account. Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. @@ -166,6 +168,7 @@ added: app/manifest.yml -> manifest.yml added: app/setup_script.sql -> setup_script.sql Updating the Snowflake stage from your local @@DEPLOY_ROOT@@ directory. + Validating Snowflake Native App setup script. Deployed successfully. ''' @@ -194,7 +197,7 @@ ''' # --- -# name: test_nativeapp_deploy_nested_directories[ws deploy --entity-id=pkg-napp_init_v2] +# name: test_nativeapp_deploy_nested_directories[ws deploy --entity-id=pkg --no-validate-napp_init_v2] ''' Creating new application package myapp_pkg_@@USER@@ in account. Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. @@ -287,11 +290,12 @@ Use the --prune flag to delete them from the stage. Your stage is up-to-date with your local deploy root. + Validating Snowflake Native App setup script. Deployed successfully. ''' # --- -# name: test_nativeapp_deploy_prune[ws deploy --entity-id=pkg --prune-contains2-not_contains2-napp_init_v2] +# name: test_nativeapp_deploy_prune[ws deploy --entity-id=pkg --no-validate-contains5-not_contains5-napp_init_v2] ''' Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. @@ -302,7 +306,7 @@ ''' # --- -# name: test_nativeapp_deploy_prune[ws deploy --entity-id=pkg-contains5-not_contains5-napp_init_v2] +# name: test_nativeapp_deploy_prune[ws deploy --entity-id=pkg --prune --no-validate-contains2-not_contains2-napp_init_v2] ''' Checking if stage myapp_pkg_@@USER@@.app_src.stage exists, or creating a new one if none exists. Performing a diff between the Snowflake stage and your local deploy_root ('@@DEPLOY_ROOT@@') directory. diff --git a/tests_integration/nativeapp/test_deploy.py b/tests_integration/nativeapp/test_deploy.py index 897437a780..3f4925964b 100644 --- a/tests_integration/nativeapp/test_deploy.py +++ b/tests_integration/nativeapp/test_deploy.py @@ -68,6 +68,7 @@ def test_nativeapp_deploy( with nativeapp_project_directory(test_project): result = runner.invoke_with_connection(split(command)) assert result.exit_code == 0 + assert "Validating Snowflake Native App setup script." in result.output assert sanitize_deploy_output(result.output) == snapshot # package exist @@ -123,7 +124,7 @@ def test_nativeapp_deploy( "napp_init_v2", ], [ - "ws deploy --entity-id=pkg --prune", + "ws deploy --entity-id=pkg --prune --no-validate", ["stage/manifest.yml"], ["stage/README.md"], "napp_init_v2", @@ -142,7 +143,7 @@ def test_nativeapp_deploy( "napp_init_v2", ], [ - "ws deploy --entity-id=pkg", + "ws deploy --entity-id=pkg --no-validate", ["stage/manifest.yml"], ["stage/README.md"], "napp_init_v2", @@ -213,7 +214,7 @@ def test_nativeapp_deploy_prune( [ ["app deploy --no-validate", "napp_init_v1"], ["app deploy --no-validate", "napp_init_v2"], - ["ws deploy --entity-id=pkg", "napp_init_v2"], + ["ws deploy --entity-id=pkg --no-validate", "napp_init_v2"], ], ) def test_nativeapp_deploy_files( @@ -258,7 +259,7 @@ def test_nativeapp_deploy_files( [ ["app deploy --no-validate", "napp_init_v1"], ["app deploy --no-validate", "napp_init_v2"], - ["ws deploy --entity-id=pkg", "napp_init_v2"], + ["ws deploy --entity-id=pkg --no-validate", "napp_init_v2"], ], ) def test_nativeapp_deploy_nested_directories( @@ -300,7 +301,7 @@ def test_nativeapp_deploy_nested_directories( [ ["app deploy --no-validate", "napp_init_v1"], ["app deploy --no-validate", "napp_init_v2"], - ["ws deploy --entity-id=pkg", "napp_init_v2"], + ["ws deploy --entity-id=pkg --no-validate", "napp_init_v2"], ], ) def test_nativeapp_deploy_directory( @@ -340,7 +341,7 @@ def test_nativeapp_deploy_directory( [ ["app deploy --no-validate", "napp_init_v1"], ["app deploy --no-validate", "napp_init_v2"], - ["ws deploy --entity-id=pkg", "napp_init_v2"], + ["ws deploy --entity-id=pkg --no-validate", "napp_init_v2"], ], ) def test_nativeapp_deploy_directory_no_recursive( @@ -362,7 +363,7 @@ def test_nativeapp_deploy_directory_no_recursive( [ ["app deploy --no-validate", "napp_init_v1"], ["app deploy --no-validate", "napp_init_v2"], - ["ws deploy --entity-id=pkg", "napp_init_v2"], + ["ws deploy --entity-id=pkg --no-validate", "napp_init_v2"], ], ) def test_nativeapp_deploy_unknown_path( @@ -384,7 +385,7 @@ def test_nativeapp_deploy_unknown_path( [ ["app deploy --no-validate", "napp_init_v1"], ["app deploy --no-validate", "napp_init_v2"], - ["ws deploy --entity-id=pkg", "napp_init_v2"], + ["ws deploy --entity-id=pkg --no-validate", "napp_init_v2"], ], ) def test_nativeapp_deploy_path_with_no_mapping( @@ -552,3 +553,27 @@ def test_nativeapp_deploy_dot( assert contains_row_with(stage_files.json, {"name": "stage/manifest.yml"}) assert contains_row_with(stage_files.json, {"name": "stage/setup_script.sql"}) assert contains_row_with(stage_files.json, {"name": "stage/README.md"}) + + +@pytest.mark.integration +@pytest.mark.parametrize( + "command,test_project", + [ + ["app deploy", "napp_init_v1"], + ["app deploy", "napp_init_v2"], + ["ws deploy --entity-id=pkg", "napp_init_v2"], + ], +) +def test_nativeapp_deploy_validate_failing( + command, test_project, nativeapp_project_directory, runner +): + with nativeapp_project_directory(test_project): + # Create invalid SQL file + Path("app/setup_script.sql").write_text("Lorem ipsum dolor sit amet") + + # validate the app's setup script, this will fail + # because we include an empty file + result = runner.invoke_with_connection(split(command)) + assert result.exit_code == 1, result.output + assert "Snowflake Native App setup script failed validation." in result.output + assert "syntax error" in result.output