From f48b69f681bc20a2d00438932a426846d1c9ef26 Mon Sep 17 00:00:00 2001 From: Gregory Clunies Date: Wed, 30 Sep 2026 17:29:10 -0700 Subject: [PATCH 1/3] fix(lifecycle): transfer ownership with the object type GRANT OWNERSHIP accepts GRANT OWNERSHIP rejects integration subtype names (SECURITY INTEGRATION, STORAGE INTEGRATION, ...), MATERIALIZED VIEW and HYBRID TABLE. Snowflake names them INTEGRATION, VIEW and TABLE. A plan that transferred ownership of any of these failed at apply with a SQL compilation error, for example: GRANT OWNERSHIP ON SECURITY INTEGRATION X TO ROLE ACCOUNTADMIN COPY CURRENT GRANTS. https://docs.snowflake.com/en/sql-reference/sql/grant-ownership --- snowcap/lifecycle.py | 17 ++++++++++++++++- tests/test_lifecycle.py | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/snowcap/lifecycle.py b/snowcap/lifecycle.py index 701f4f5..8070b3b 100644 --- a/snowcap/lifecycle.py +++ b/snowcap/lifecycle.py @@ -868,6 +868,21 @@ def transfer_resource( ) +# GRANT OWNERSHIP names these resource types by a broader object type, and rejects +# their own names. Every integration subtype is also named INTEGRATION. +# https://docs.snowflake.com/en/sql-reference/sql/grant-ownership +_OWNERSHIP_OBJECT_TYPES = { + ResourceType.MATERIALIZED_VIEW: "VIEW", + ResourceType.HYBRID_TABLE: "TABLE", +} + + +def _ownership_object_type(resource_type: ResourceType) -> str: + if "INTEGRATION" in str(resource_type): + return "INTEGRATION" + return _OWNERSHIP_OBJECT_TYPES.get(resource_type, str(resource_type)) + + def transfer__default( urn: URN, owner: str, @@ -877,7 +892,7 @@ def transfer__default( ) -> str: return tidy_sql( "GRANT OWNERSHIP ON", - urn.resource_type, + _ownership_object_type(urn.resource_type), urn.fqn, "TO", owner_resource_type, diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 1d08006..89534f7 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1316,6 +1316,30 @@ def test_transfer_default(self): # ResourceType.DATABASE_ROLE renders as "DATABASE ROLE" assert "TO DATABASE ROLE NEW_OWNER" in result + @pytest.mark.parametrize( + "resource_type,schema,object_type", + [ + (ResourceType.API_INTEGRATION, None, "INTEGRATION"), + (ResourceType.CATALOG_INTEGRATION, None, "INTEGRATION"), + (ResourceType.EXTERNAL_ACCESS_INTEGRATION, None, "INTEGRATION"), + (ResourceType.NOTIFICATION_INTEGRATION, None, "INTEGRATION"), + (ResourceType.SECURITY_INTEGRATION, None, "INTEGRATION"), + (ResourceType.STORAGE_INTEGRATION, None, "INTEGRATION"), + (ResourceType.MATERIALIZED_VIEW, "MY_SCHEMA", "VIEW"), + (ResourceType.HYBRID_TABLE, "MY_SCHEMA", "TABLE"), + ], + ) + def test_transfer_uses_snowflake_ownership_object_type(self, resource_type, schema, object_type): + """GRANT OWNERSHIP names some resource types by a broader object type. + + https://docs.snowflake.com/en/sql-reference/sql/grant-ownership + """ + database = "MY_DB" if schema else None + urn = make_urn(resource_type, "MY_OBJECT", database=database, schema=schema) + result = transfer_resource(urn, "NEW_OWNER", ResourceType.ROLE, copy_current_grants=True) + assert result.startswith(f"GRANT OWNERSHIP ON {object_type} ") + assert "MY_OBJECT TO ROLE NEW_OWNER COPY CURRENT GRANTS" in result + # ============================================================================ # Test tag masking policy reference functions From b0ddb015304be6b8aef5b7c6cff13861f2a4fdde Mon Sep 17 00:00:00 2001 From: Gregory Clunies Date: Thu, 1 Oct 2026 12:06:33 -0700 Subject: [PATCH 2/3] fix(lifecycle): transfer external function ownership as FUNCTION GRANT OWNERSHIP has no EXTERNAL FUNCTION object type, so a transfer away from the default SYSADMIN owner failed with a syntax error. External functions are granted as FUNCTION. --- snowcap/lifecycle.py | 8 +++++--- tests/test_lifecycle.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/snowcap/lifecycle.py b/snowcap/lifecycle.py index 8070b3b..a4f03a4 100644 --- a/snowcap/lifecycle.py +++ b/snowcap/lifecycle.py @@ -868,12 +868,14 @@ def transfer_resource( ) -# GRANT OWNERSHIP names these resource types by a broader object type, and rejects -# their own names. Every integration subtype is also named INTEGRATION. +# GRANT OWNERSHIP names these resource types by a broader object type. Snowflake +# rejects EXTERNAL FUNCTION and the integration subtype names, and its usage notes +# say to use VIEW for materialized views and TABLE for hybrid tables. # https://docs.snowflake.com/en/sql-reference/sql/grant-ownership _OWNERSHIP_OBJECT_TYPES = { - ResourceType.MATERIALIZED_VIEW: "VIEW", + ResourceType.EXTERNAL_FUNCTION: "FUNCTION", ResourceType.HYBRID_TABLE: "TABLE", + ResourceType.MATERIALIZED_VIEW: "VIEW", } diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 89534f7..e1eab64 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1325,8 +1325,9 @@ def test_transfer_default(self): (ResourceType.NOTIFICATION_INTEGRATION, None, "INTEGRATION"), (ResourceType.SECURITY_INTEGRATION, None, "INTEGRATION"), (ResourceType.STORAGE_INTEGRATION, None, "INTEGRATION"), - (ResourceType.MATERIALIZED_VIEW, "MY_SCHEMA", "VIEW"), + (ResourceType.EXTERNAL_FUNCTION, "MY_SCHEMA", "FUNCTION"), (ResourceType.HYBRID_TABLE, "MY_SCHEMA", "TABLE"), + (ResourceType.MATERIALIZED_VIEW, "MY_SCHEMA", "VIEW"), ], ) def test_transfer_uses_snowflake_ownership_object_type(self, resource_type, schema, object_type): From dd3c5914e666c93dc446e1c01e7d6e7d65425085 Mon Sep 17 00:00:00 2001 From: Gregory Clunies Date: Thu, 1 Oct 2026 12:08:08 -0700 Subject: [PATCH 3/3] test(lifecycle): check that types near the mapped ones keep their own ownership names --- tests/test_lifecycle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index e1eab64..c4bcb26 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -1328,6 +1328,10 @@ def test_transfer_default(self): (ResourceType.EXTERNAL_FUNCTION, "MY_SCHEMA", "FUNCTION"), (ResourceType.HYBRID_TABLE, "MY_SCHEMA", "TABLE"), (ResourceType.MATERIALIZED_VIEW, "MY_SCHEMA", "VIEW"), + # Types close to the mapped ones keep their own names. + (ResourceType.DYNAMIC_TABLE, "MY_SCHEMA", "DYNAMIC TABLE"), + (ResourceType.ICEBERG_TABLE, "MY_SCHEMA", "ICEBERG TABLE"), + (ResourceType.VIEW, "MY_SCHEMA", "VIEW"), ], ) def test_transfer_uses_snowflake_ownership_object_type(self, resource_type, schema, object_type):