From 056af4ed3fbaa827d7ee3d4a1b4a9eb68207471d Mon Sep 17 00:00:00 2001 From: Michel El Nacouzi Date: Fri, 10 Jan 2025 09:23:06 -0500 Subject: [PATCH] Add support for --create-version in snow app publish (#1982) --- .../cli/_plugins/nativeapp/commands.py | 35 ++- .../nativeapp/entities/application_package.py | 50 +++- .../cli/_plugins/nativeapp/sf_sql_facade.py | 17 +- tests/__snapshots__/test_help_messages.ambr | 180 +++++++------- .../test_application_package_entity.py | 226 ++++++++++++++++++ tests/nativeapp/test_sf_sql_facade.py | 118 +++++++++ tests/nativeapp/test_version_create.py | 2 +- 7 files changed, 527 insertions(+), 101 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/commands.py b/src/snowflake/cli/_plugins/nativeapp/commands.py index af35acc208..9c764c5272 100644 --- a/src/snowflake/cli/_plugins/nativeapp/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/commands.py @@ -22,6 +22,7 @@ from typing import Generator, Iterable, List, Optional, cast import typer +from snowflake.cli._plugins.nativeapp.artifacts import VersionInfo from snowflake.cli._plugins.nativeapp.common_flags import ( ForceOption, InteractiveOption, @@ -538,13 +539,15 @@ def result(self): @with_project_definition() @force_project_definition_v2() def app_publish( - version: str = typer.Option( + version: Optional[str] = typer.Option( + default=None, show_default=False, - help="The version to publish to the release channel. The version must be created fist using 'snow app version create'.", + help="The version to publish to the provided release channel and release directive. Version is required to exist unless `--create-version` flag is used.", ), - patch: int = typer.Option( + patch: Optional[int] = typer.Option( + default=None, show_default=False, - help="The patch number under the given version. The patch number must be created first using 'snow app version create'.", + help="The patch number under the given version. This will be used when setting the release directive. Patch is required to exist unless `--create-version` flag is used.", ), channel: Optional[str] = typer.Option( "DEFAULT", @@ -556,6 +559,23 @@ def app_publish( ), interactive: bool = InteractiveOption, force: Optional[bool] = ForceOption, + create_version: bool = typer.Option( + False, + "--create-version", + help="Create a new version or patch based on the provided `--version` and `--patch` values. Fallback to the manifest values if not provided.", + is_flag=True, + ), + from_stage: bool = typer.Option( + False, + "--from-stage", + help="When enabled, the Snowflake CLI creates a version from the current application package stage without syncing to the stage first. Can only be used with `--create-version` flag.", + is_flag=True, + ), + label: Optional[str] = typer.Option( + None, + "--label", + help="A label for the version that is displayed to consumers. Can only be used with `--create-version` flag.", + ), **options, ) -> CommandResult: """ @@ -567,7 +587,7 @@ def app_publish( project_root=cli_context.project_root, ) package_id = options["package_entity_id"] - ws.perform_action( + version_info: VersionInfo = ws.perform_action( package_id, EntityActions.PUBLISH, version=version, @@ -576,7 +596,10 @@ def app_publish( release_directive=directive, interactive=interactive, force=force, + create_version=create_version, + from_stage=from_stage, + label=label, ) return MessageResult( - f"Version {version} and patch {patch} published to release directive {directive} of release channel {channel}." + f"Version {version_info.version_name} and patch {version_info.patch_number} published to release directive {directive} of release channel {channel}." ) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py index 353207c8af..0a5bd308ac 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py @@ -446,11 +446,6 @@ def action_version_create( else: policy = DenyAlwaysPolicy() - if skip_git_check: - git_policy = DenyAlwaysPolicy() - else: - git_policy = AllowAlwaysPolicy() - bundle_map = self._bundle(action_ctx) resolved_version, resolved_patch, resolved_label = self.resolve_version_info( version=version, @@ -461,7 +456,7 @@ def action_version_create( interactive=interactive, ) - if git_policy.should_proceed(): + if not skip_git_check: self.check_index_changes_in_git_repo(policy=policy, interactive=interactive) # if user is asking to create the version from the current stage, @@ -1005,15 +1000,18 @@ def _find_version_with_no_recent_update( def action_publish( self, action_ctx: ActionContext, - version: str, - patch: int, + version: Optional[str], + patch: Optional[int], release_channel: Optional[str], release_directive: str, interactive: bool, force: bool, *args, + create_version: bool = False, + from_stage: bool = False, + label: Optional[str] = None, **kwargs, - ) -> None: + ) -> VersionInfo: """ Publishes a version and a patch to a release directive of a release channel. @@ -1030,7 +1028,37 @@ def action_publish( else: policy = DenyAlwaysPolicy() + if from_stage and not create_version: + raise UsageError( + "--from-stage flag can only be used with --create-version flag." + ) + if label is not None and not create_version: + raise UsageError("--label can only be used with --create-version flag.") + console = self._workspace_ctx.console + if create_version: + result = self.action_version_create( + action_ctx=action_ctx, + version=version, + patch=patch, + label=label, + skip_git_check=True, + interactive=interactive, + force=force, + from_stage=from_stage, + ) + version = result.version_name + patch = result.patch_number + + if version is None: + raise UsageError( + "Please provide a version using --version or use --create-version flag to create a version based on the manifest file." + ) + if patch is None: + raise UsageError( + "Please provide a patch number using --patch or use --create-version flag to auto create a patch." + ) + versions_info = get_snowflake_facade().show_versions(self.name, self.role) available_patches = [ @@ -1123,6 +1151,7 @@ def action_publish( patch=patch, role=self.role, ) + return VersionInfo(version, patch, None) def _bundle_children(self, action_ctx: ActionContext) -> List[str]: # Create _children directory @@ -1694,7 +1723,8 @@ def resolve_version_info( resolved_label = label if label is not None else label_manifest # Check if patch needs to throw a bad option error, either if application package does not exist or if version does not exist - if resolved_patch is not None: + # If patch is 0 and version does not exist, it is a valid case, because patch 0 is the first patch in a version. + if resolved_patch: try: if not self.get_existing_version_info(to_identifier(resolved_version)): raise BadOptionUsage( diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 4c92c8ccea..ca4d113268 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -51,6 +51,7 @@ ACCOUNT_DOES_NOT_EXIST, ACCOUNT_HAS_TOO_MANY_QUALIFIERS, APPLICATION_PACKAGE_MAX_VERSIONS_HIT, + APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS, APPLICATION_REQUIRES_TELEMETRY_SHARING, CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL, CANNOT_DISABLE_MANDATORY_TELEMETRY, @@ -374,6 +375,10 @@ def drop_version_from_package( raise UserInputError( f"Cannot drop version {version} from application package {package_name} because it is associated with a release channel." ) from err + if err.errno == VERSION_DOES_NOT_EXIST: + raise UserInputError( + f"Version {version} does not exist in application package {package_name}." + ) from err handle_unclassified_error( err, f"Failed to {action} version {version} from application package {package_name}.", @@ -407,11 +412,12 @@ def add_patch_to_package_version( with_label_clause = ( f"\nlabel={to_string_literal(label)}" if label is not None else "" ) - patch_query = f"{patch}" if patch else "" + + patch_query = f" {patch}" if patch is not None else "" add_patch_query = dedent( f"""\ alter application package {package_name} - add patch {patch_query} for version {version} + add patch{patch_query} for version {version} using @{stage_fqn}{with_label_clause} """ ) @@ -421,9 +427,14 @@ def add_patch_to_package_version( add_patch_query, cursor_class=DictCursor ).fetchall() except Exception as err: + if isinstance(err, ProgrammingError): + if err.errno == APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS: + raise UserInputError( + f"Patch {patch} already exists for version {version} in application package {package_name}." + ) from err handle_unclassified_error( err, - f"Failed to create patch {patch_query} for version {version} in application package {package_name}.", + f"Failed to create patch{patch_query} for version {version} in application package {package_name}.", ) try: show_row = result_cursor[0] diff --git a/tests/__snapshots__/test_help_messages.ambr b/tests/__snapshots__/test_help_messages.ambr index 241471f576..08f91eec3b 100644 --- a/tests/__snapshots__/test_help_messages.ambr +++ b/tests/__snapshots__/test_help_messages.ambr @@ -589,87 +589,105 @@ the new version and patch. +- Options --------------------------------------------------------------------+ - | * --version TEXT The version to | - | publish to the | - | release channel. | - | The version must be | - | created fist using | - | 'snow app version | - | create'. | - | [required] | - | * --patch INTEGER The patch number | - | under the given | - | version. The patch | - | number must be | - | created first using | - | 'snow app version | - | create'. | - | [required] | - | --channel TEXT The name of the | - | release channel to | - | publish to. If not | - | provided, the | - | default release | - | channel is used. | - | [default: DEFAULT] | - | --directive TEXT The name of the | - | release directive | - | to update with the | - | specified version | - | and patch. If not | - | provided, the | - | default release | - | directive is used. | - | [default: DEFAULT] | - | --interactive --no-interactive When enabled, this | - | option displays | - | prompts even if the | - | standard input and | - | output are not | - | terminal devices. | - | Defaults to True in | - | an interactive | - | shell environment, | - | and False | - | otherwise. | - | --force When enabled, this | - | option causes the | - | command to | - | implicitly approve | - | any prompts that | - | arise. You should | - | enable this option | - | if interactive mode | - | is not specified | - | and if you want | - | perform potentially | - | destructive | - | actions. Defaults | - | to unset. | - | --package-entity-id TEXT The ID of the | - | package entity on | - | which to operate | - | when | - | definition_version | - | is 2 or higher. | - | --app-entity-id TEXT The ID of the | - | application entity | - | on which to operate | - | when | - | definition_version | - | is 2 or higher. | - | --project -p TEXT Path where | - | Snowflake project | - | resides. Defaults | - | to current working | - | directory. | - | --env TEXT String in format of | - | key=value. | - | Overrides variables | - | from env section | - | used for templates. | - | --help -h Show this message | - | and exit. | + | --version TEXT The version to publish | + | to the provided | + | release channel and | + | release directive. | + | Version is required to | + | exist unless | + | --create-version flag | + | is used. | + | --patch INTEGER The patch number under | + | the given version. | + | This will be used when | + | setting the release | + | directive. Patch is | + | required to exist | + | unless | + | --create-version flag | + | is used. | + | --channel TEXT The name of the | + | release channel to | + | publish to. If not | + | provided, the default | + | release channel is | + | used. | + | [default: DEFAULT] | + | --directive TEXT The name of the | + | release directive to | + | update with the | + | specified version and | + | patch. If not | + | provided, the default | + | release directive is | + | used. | + | [default: DEFAULT] | + | --interactive --no-interactive When enabled, this | + | option displays | + | prompts even if the | + | standard input and | + | output are not | + | terminal devices. | + | Defaults to True in an | + | interactive shell | + | environment, and False | + | otherwise. | + | --force When enabled, this | + | option causes the | + | command to implicitly | + | approve any prompts | + | that arise. You should | + | enable this option if | + | interactive mode is | + | not specified and if | + | you want perform | + | potentially | + | destructive actions. | + | Defaults to unset. | + | --create-version Create a new version | + | or patch based on the | + | provided --version and | + | --patch values. | + | Fallback to the | + | manifest values if not | + | provided. | + | --from-stage When enabled, the | + | Snowflake CLI creates | + | a version from the | + | current application | + | package stage without | + | syncing to the stage | + | first. Can only be | + | used with | + | --create-version flag. | + | --label TEXT A label for the | + | version that is | + | displayed to | + | consumers. Can only be | + | used with | + | --create-version flag. | + | [default: None] | + | --package-entity-id TEXT The ID of the package | + | entity on which to | + | operate when | + | definition_version is | + | 2 or higher. | + | --app-entity-id TEXT The ID of the | + | application entity on | + | which to operate when | + | definition_version is | + | 2 or higher. | + | --project -p TEXT Path where Snowflake | + | project resides. | + | Defaults to current | + | working directory. | + | --env TEXT String in format of | + | key=value. Overrides | + | variables from env | + | section used for | + | templates. | + | --help -h Show this message and | + | exit. | +------------------------------------------------------------------------------+ +- Connection configuration ---------------------------------------------------+ | --connection,--environment -c TEXT Name of the connection, as | diff --git a/tests/nativeapp/test_application_package_entity.py b/tests/nativeapp/test_application_package_entity.py index 7b3f8200f1..5d088d7f7b 100644 --- a/tests/nativeapp/test_application_package_entity.py +++ b/tests/nativeapp/test_application_package_entity.py @@ -22,6 +22,7 @@ import yaml from click import ClickException, UsageError from snowflake.cli._plugins.connection.util import UIParameter +from snowflake.cli._plugins.nativeapp.artifacts import VersionInfo from snowflake.cli._plugins.nativeapp.constants import ( LOOSE_FILES_MAGIC_VERSION, SPECIAL_COMMENT, @@ -1902,3 +1903,228 @@ def test_given_only_one_version_referenced_by_existing_release_directive_when_pu remove_version_from_release_channel.assert_not_called() add_version_to_release_channel.assert_not_called() set_release_directive.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +@mock.patch(f"{APP_PACKAGE_ENTITY}.action_version_create") +def test_given_release_channel_and_version_when_publish_with_create_version_then_success( + action_version_create, + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + action_version_create.return_value = VersionInfo( + version_name="1.0", patch_number=1, label=None + ) + + show_release_channels.return_value = [{"name": "TEST_CHANNEL", "versions": []}] + show_release_directives.return_value = [{"name": "TEST_DIRECTIVE"}] + show_versions.return_value = [{"version": "1.0", "patch": 1}] + + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version="x", + patch=33, + interactive=False, + force=False, + create_version=True, + ) + + action_version_create.assert_called_once_with( + action_ctx=action_context, + version="x", + patch=33, + label=None, + skip_git_check=True, + interactive=False, + force=False, + from_stage=False, + ) + + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + show_release_directives.assert_not_called() + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + add_version_to_release_channel.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + version="1.0", + ) + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + version="1.0", + patch=1, + release_channel="test_channel", + release_directive="test_directive", + target_accounts=None, + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +@mock.patch(f"{APP_PACKAGE_ENTITY}.action_version_create") +def test_given_release_channel_and_no_version_when_publish_with_create_version_then_success( + action_version_create, + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + action_version_create.return_value = VersionInfo( + version_name="1.0", patch_number=1, label=None + ) + + show_release_channels.return_value = [{"name": "TEST_CHANNEL", "versions": []}] + show_release_directives.return_value = [{"name": "TEST_DIRECTIVE"}] + show_versions.return_value = [{"version": "1.0", "patch": 1}] + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version=None, + patch=None, + interactive=False, + force=False, + create_version=True, + from_stage=True, + label="some_label", + ) + + action_version_create.assert_called_once_with( + action_ctx=action_context, + version=None, + patch=None, + label="some_label", + skip_git_check=True, + interactive=False, + force=False, + from_stage=True, + ) + + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + show_release_directives.assert_not_called() + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + add_version_to_release_channel.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + version="1.0", + ) + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + version="1.0", + patch=1, + release_channel="test_channel", + release_directive="test_directive", + target_accounts=None, + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +@mock.patch(f"{APP_PACKAGE_ENTITY}.action_version_create") +def test_given_release_channel_and_from_stage_without_create_version_when_publish_then_error( + action_version_create, + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + with pytest.raises(UsageError) as e: + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version=None, + patch=None, + interactive=False, + force=False, + create_version=False, + from_stage=True, + ) + + assert ( + str(e.value) == "--from-stage flag can only be used with --create-version flag." + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +@mock.patch(f"{APP_PACKAGE_ENTITY}.action_version_create") +def test_given_release_channel_and_label_without_create_version_when_publish_then_error( + action_version_create, + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + with pytest.raises(UsageError) as e: + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version=None, + patch=None, + interactive=False, + force=False, + create_version=False, + label="label", + ) + + assert str(e.value) == "--label can only be used with --create-version flag." diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index 23e6a56ac7..f473c43bdb 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -49,6 +49,7 @@ ACCOUNT_HAS_TOO_MANY_QUALIFIERS, APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, APPLICATION_PACKAGE_MAX_VERSIONS_HIT, + APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS, APPLICATION_REQUIRES_TELEMETRY_SHARING, CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL, CANNOT_DISABLE_MANDATORY_TELEMETRY, @@ -3806,6 +3807,11 @@ def test_drop_version_from_package_with_special_characters( UserInputError, "Cannot drop version v1 from application package test_package because it is associated with a release channel.", ), + ( + ProgrammingError(errno=VERSION_DOES_NOT_EXIST), + UserInputError, + "Version v1 does not exist in application package test_package.", + ), ( ProgrammingError(), InvalidSQLError, @@ -3843,6 +3849,118 @@ def test_drop_version_from_package_with_error( assert error_message in str(err) +def test_add_patch_to_package_version_valid_input_then_success( + mock_use_role, mock_execute_query, mock_cursor +): + package_name = "test_package" + version = "v1" + patch = 1 + stage_fqn = "src.stage" + + expected_query = dedent( + f"""\ + alter application package test_package + add patch 1 for version v1 + using @src.stage + """ + ) + + mock_execute_query.side_effect = [mock_cursor([{"patch": 1}], [])] + result = sql_facade.add_patch_to_package_version( + package_name=package_name, stage_fqn=stage_fqn, version=version, patch=patch + ) + + assert result == patch + mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) + + +# patch 0 shouldn't be treated in a special way (rely on backend saying 0 already exists) +def test_add_patch_to_package_version_valid_input_then_success_patch_0( + mock_use_role, mock_execute_query, mock_cursor +): + package_name = "test_package" + version = "v1" + patch = 0 + stage_fqn = "src.stage" + + expected_query = dedent( + """\ + alter application package test_package + add patch 0 for version v1 + using @src.stage + """ + ) + + mock_execute_query.side_effect = [mock_cursor([{"patch": 0}], [])] + result = sql_facade.add_patch_to_package_version( + package_name=package_name, stage_fqn=stage_fqn, version=version, patch=patch + ) + + assert result == patch + mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) + + +def test_add_patch_to_package_version_valid_input_then_success_no_patch_in_input( + mock_use_role, mock_execute_query, mock_cursor +): + package_name = "test_package" + version = "v1" + stage_fqn = "src.stage" + + expected_query = dedent( + """\ + alter application package test_package + add patch for version v1 + using @src.stage + """ + ) + + mock_execute_query.side_effect = [mock_cursor([{"patch": 5}], [])] + result = sql_facade.add_patch_to_package_version( + package_name=package_name, stage_fqn=stage_fqn, version=version, patch=None + ) + + assert result == 5 + mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) + + +@pytest.mark.parametrize( + "error_raised, error_caught, error_message", + [ + ( + ProgrammingError(errno=APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS), + UserInputError, + "Patch 1 already exists for version v1 in application package test_package.", + ), + ( + ProgrammingError(), + InvalidSQLError, + "Failed to create patch 1 for version v1 in application package test_package.", + ), + ( + DatabaseError("some database error"), + UnknownSQLError, + "Unknown SQL error occurred. Failed to create patch 1 for version v1 in application package test_package. some database error", + ), + ], +) +def test_add_patch_to_package_version_with_error( + mock_use_role, mock_execute_query, error_raised, error_caught, error_message +): + package_name = "test_package" + version = "v1" + patch = 1 + stage_fqn = "src.stage" + + mock_execute_query.side_effect = error_raised + + with pytest.raises(error_caught) as err: + sql_facade.add_patch_to_package_version( + package_name=package_name, stage_fqn=stage_fqn, version=version, patch=patch + ) + assert error_message in str(err) + + def test_add_accounts_to_release_channel_valid_input_then_success( mock_use_role, mock_execute_query ): diff --git a/tests/nativeapp/test_version_create.py b/tests/nativeapp/test_version_create.py index 72c6d0c9c6..3d43e2c667 100644 --- a/tests/nativeapp/test_version_create.py +++ b/tests/nativeapp/test_version_create.py @@ -195,7 +195,7 @@ def test_add_new_patch_auto( dedent( f"""\ alter application package app_pkg - add patch for version {version_identifier} + add patch for version {version_identifier} using @app_pkg.app_src.stage """ ),