diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index b5f3ccff40..df330f5bd1 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -22,6 +22,7 @@ ## Fixes and improvements * Fixed crashes with older x86_64 Intel CPUs. +* Fixed inability to add patches to lowercase quoted versions # v3.2.0 diff --git a/src/snowflake/cli/api/project/util.py b/src/snowflake/cli/api/project/util.py index 564ebbd867..9564e2bae2 100644 --- a/src/snowflake/cli/api/project/util.py +++ b/src/snowflake/cli/api/project/util.py @@ -148,6 +148,10 @@ def unquote_identifier(identifier: str) -> str: string for a LIKE clause, or to match an identifier passed back as a value from a SQL statement. """ + # ensure input is a valid identifier - otherwise, it could accidentally uppercase + # a quoted identifier + identifier = to_identifier(identifier) + if match := re.fullmatch(QUOTED_IDENTIFIER_REGEX, identifier): return match.group(1).replace('""', '"') # unquoted identifiers are internally represented as uppercase diff --git a/tests/project/test_util.py b/tests/project/test_util.py index 02a1ac272c..dc33902968 100644 --- a/tests/project/test_util.py +++ b/tests/project/test_util.py @@ -29,6 +29,7 @@ to_identifier, to_quoted_identifier, to_string_literal, + unquote_identifier, ) VALID_UNQUOTED_IDENTIFIERS = ( @@ -335,3 +336,24 @@ def test_identifier_to_str(identifier, expected_value): ) def test_sanitize_identifier(identifier, expected_value): assert sanitize_identifier(identifier) == expected_value + + +@pytest.mark.parametrize( + "identifier, expected", + [ + # valid unquoted id -> return upper case version + ("Id_1", "ID_1"), + # valid quoted id -> remove quotes and keep case + ('"Id""1"', 'Id"1'), + # unquoted id with special characters -> treat it as quoted ID and reserve case + ("Id.aBc", "Id.aBc"), + # unquoted id with double quotes inside -> treat is quoted ID + ('Id"1', 'Id"1'), + # quoted id with escaped double quotes -> unescape and keep case + ('"Id""1"', 'Id"1'), + # empty string -> return the same + ("", ""), + ], +) +def test_unquote_identifier(identifier, expected): + assert unquote_identifier(identifier) == expected diff --git a/tests/test_utils.py b/tests/test_utils.py index 10eadc479a..7e24e002e6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -165,7 +165,7 @@ def test_path_resolver(mock_system, argument, expected): ("my_app", "MY_APP"), ('"My App"', "My%20App"), ("SYSTEM$GET", "SYSTEM%24GET"), - ("mailorder_!@#$%^&*()/_app", "MAILORDER_!%40%23%24%25%5E%26*()%2F_APP"), + ("mailorder_!@#$%^&*()/_app", "mailorder_!%40%23%24%25%5E%26*()%2F_app"), ('"Mailorder *App* is /cool/"', "Mailorder%20*App*%20is%20%2Fcool%2F"), ], ) diff --git a/tests_integration/nativeapp/test_version.py b/tests_integration/nativeapp/test_version.py index 701f9e9fc0..61d8fb5fe9 100644 --- a/tests_integration/nativeapp/test_version.py +++ b/tests_integration/nativeapp/test_version.py @@ -517,3 +517,53 @@ def test_version_create_with_manage_versions_only( ] ) assert result.exit_code == 0, result.output + + +@pytest.mark.integration +def test_nativeapp_version_create_quoted_identifiers( + runner, + snowflake_session, + default_username, + resource_suffix, + nativeapp_project_directory, +): + project_name = "myapp" + with nativeapp_project_directory("napp_init_v2"): + package_name = f"{project_name}_pkg_{default_username}{resource_suffix}".upper() + + # create version + result = runner.invoke_with_connection_json( + ["app", "version", "create", "v1.0"] + ) + assert result.exit_code == 0 + + # create another patch + result = runner.invoke_with_connection_json( + ["app", "version", "create", "v1.0"] + ) + assert result.exit_code == 0 + + # create custom patch + result = runner.invoke_with_connection_json( + ["app", "version", "create", "v1.0", "--patch", "4"] + ) + assert result.exit_code == 0 + + # app package contains 3 patches for version v1.0 + expect = row_from_snowflake_session( + snowflake_session.execute_string( + f"show versions in application package {package_name}" + ) + ) + assert contains_row_with(expect, {"version": "v1.0", "patch": 0}) + assert contains_row_with(expect, {"version": "v1.0", "patch": 1}) + assert contains_row_with(expect, {"version": "v1.0", "patch": 4}) + + # drop the version + result_drop = runner.invoke_with_connection_json( + ["app", "version", "drop", "v1.0", "--force"] + ) + assert result_drop.exit_code == 0 + + actual = runner.invoke_with_connection_json(["app", "version", "list"]) + assert len(actual.json) == 0