Skip to content

Commit df220c6

Browse files
committed
Rework and extend drop intention cache to include columns; Prevent generation of DROP PRIMARY KEY, DROP UNIQUE KEY, DROP FOREIGN KEY commands if relevant tables or columns are about to be dropped
1 parent 32a8bb8 commit df220c6

File tree

14 files changed

+113
-237
lines changed

14 files changed

+113
-237
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [0.56.0] - 2025-09-02
4+
5+
- Reworked and extended drop intention cache logic to table columns.
6+
- Prevented generation of `DROP PRIMARY KEY`, `DROP UNIQUE KEY` and `DROP FOREIGN KEY` commands if relevant tables or columns are about to be dropped.
7+
- Removed unused resolver for `TAG` object type.
8+
39
## [0.55.1] - 2025-07-23
410

511
- Prevented errors for outbound shares with organization listings.

snowddl/app/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ def output_engine_stats(self, engine: SnowDDLEngine):
722722
self.logger.info(f"Executed {len(engine.executed_ddl)} DDL queries, Suggested {len(engine.suggested_ddl)} DDL queries")
723723

724724
def output_engine_warnings(self, engine: SnowDDLEngine):
725-
for object_type, object_names in engine.intention_cache.invalid_name_warning.items():
725+
for object_type, object_names in engine.intention_cache.object_name_warning.items():
726726
for name in object_names:
727727
self.logger.warning(
728728
f"Detected {object_type.name} with name [{name}] "

snowddl/cache/intention_cache.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,24 @@ class IntentionCache:
2020
def __init__(self, engine: "SnowDDLEngine"):
2121
self.engine = engine
2222

23-
self.drop_intention: Dict[ObjectType, Set[str]] = defaultdict(set)
24-
self.replace_intention: Dict[ObjectType, Set[str]] = defaultdict(set)
23+
self.object_drop_intention: Dict[ObjectType, Set[str]] = defaultdict(set)
24+
self.object_name_warning: Dict[ObjectType, Set[str]] = defaultdict(set)
2525

26-
self.invalid_name_warning: Dict[ObjectType, Set[str]] = defaultdict(set)
26+
self.column_drop_intention: Dict[str, Set[str]] = defaultdict(set)
2727

28-
def add_drop_intention(self, object_type: ObjectType, object_full_name: str):
29-
self.drop_intention[object_type].add(object_full_name)
28+
def add_object_drop_intention(self, object_type: ObjectType, object_full_name: str):
29+
self.object_drop_intention[object_type].add(object_full_name)
3030

31-
def add_replace_intention(self, object_type: ObjectType, object_full_name: str):
32-
self.replace_intention[object_type].add(object_full_name)
31+
def add_object_name_warning(self, object_type: ObjectType, object_full_name: str):
32+
self.object_name_warning[object_type].add(object_full_name)
3333

34-
def add_invalid_name_warning(self, object_type: ObjectType, object_full_name: str):
35-
self.invalid_name_warning[object_type].add(object_full_name)
34+
def add_column_drop_intention(self, object_full_name: str, column_name: str):
35+
self.column_drop_intention[object_full_name].add(column_name)
3636

37-
def check_drop_intention(self, object_type: ObjectType, object_full_name: str):
38-
return object_full_name in self.drop_intention[object_type]
37+
def check_object_drop_intention(self, object_type: ObjectType, object_full_name: str):
38+
return object_full_name in self.object_drop_intention[object_type]
3939

40-
def check_parent_drop_intention(self, object_type: ObjectType, object_full_name: str):
40+
def check_parent_object_drop_intention(self, object_type: ObjectType, object_full_name: str):
4141
blueprint_cls = object_type.blueprint_cls
4242
object_full_name_parts = object_full_name.partition("(")[0].split(".")
4343

@@ -47,24 +47,27 @@ def check_parent_drop_intention(self, object_type: ObjectType, object_full_name:
4747

4848
# All schemas and schema objects are implicitly dropped by DATABASE
4949
if issubclass(blueprint_cls, (SchemaBlueprint, SchemaObjectBlueprint, DatabaseRoleBlueprint)) and (
50-
database_name in self.drop_intention[ObjectType.DATABASE]
50+
database_name in self.object_drop_intention[ObjectType.DATABASE]
5151
):
5252
return True
5353

5454
# All schema objects are implicitly dropped by SCHEMA
55-
if issubclass(blueprint_cls, SchemaObjectBlueprint) and (schema_name in self.drop_intention[ObjectType.SCHEMA]):
55+
if issubclass(blueprint_cls, SchemaObjectBlueprint) and (schema_name in self.object_drop_intention[ObjectType.SCHEMA]):
5656
return True
5757

5858
# All stage files are implicitly dropped by STAGE
59-
if issubclass(blueprint_cls, StageFileBlueprint) and (schema_object_name in self.drop_intention[ObjectType.STAGE]):
59+
if issubclass(blueprint_cls, StageFileBlueprint) and (schema_object_name in self.object_drop_intention[ObjectType.STAGE]):
6060
return True
6161

6262
# All table constraints are implicitly dropped by various TABLE types which support constraints
6363
if issubclass(blueprint_cls, (ForeignKeyBlueprint, PrimaryKeyBlueprint, UniqueKeyBlueprint)) and (
64-
schema_object_name in self.drop_intention[ObjectType.TABLE]
65-
or schema_object_name in self.drop_intention[ObjectType.EXTERNAL_TABLE]
66-
or schema_object_name in self.drop_intention[ObjectType.HYBRID_TABLE]
64+
schema_object_name in self.object_drop_intention[ObjectType.TABLE]
65+
or schema_object_name in self.object_drop_intention[ObjectType.EXTERNAL_TABLE]
66+
or schema_object_name in self.object_drop_intention[ObjectType.HYBRID_TABLE]
6767
):
6868
return True
6969

7070
return False
71+
72+
def check_column_drop_intention(self, object_full_name: str, column_name: str):
73+
return column_name in self.column_drop_intention[object_full_name]

snowddl/resolver/abc_resolver.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ def _resolve_drop(self):
9595
if object_full_name in self.blueprints:
9696
continue
9797

98-
# Parent object is going to be dropped
99-
if self.engine.intention_cache.check_parent_drop_intention(self.object_type, object_full_name):
98+
# Another object is going to be dropped, which implicitly drops this object
99+
if self._check_implicit_drop_intention(object_full_name):
100100
continue
101101

102102
tasks[object_full_name] = (self.drop_object, self.existing_objects[object_full_name])
@@ -107,15 +107,22 @@ def _destroy_drop(self):
107107
tasks = {}
108108

109109
# Drop all existing objects
110-
for full_name in sorted(self.existing_objects):
111-
# Parent object is going to be dropped
112-
if self.engine.intention_cache.check_parent_drop_intention(self.object_type, full_name):
110+
for object_full_name in sorted(self.existing_objects):
111+
# Another object is going to be dropped, which implicitly drops this object
112+
if self._check_implicit_drop_intention(object_full_name):
113113
continue
114114

115-
tasks[full_name] = (self.drop_object, self.existing_objects[full_name])
115+
tasks[object_full_name] = (self.drop_object, self.existing_objects[object_full_name])
116116

117117
self._process_tasks(tasks)
118118

119+
def _check_implicit_drop_intention(self, object_full_name: str) -> bool:
120+
# Dropping or replacing any parent object implicitly drops this object
121+
if self.engine.intention_cache.check_parent_object_drop_intention(self.object_type, object_full_name):
122+
return True
123+
124+
return False
125+
119126
def _process_tasks(self, tasks):
120127
futures = {}
121128

@@ -128,11 +135,8 @@ def _process_tasks(self, tasks):
128135
try:
129136
result = f.result()
130137

131-
if result == ResolveResult.REPLACE:
132-
self.engine.intention_cache.add_replace_intention(self.object_type, full_name)
133-
134-
if result == ResolveResult.DROP:
135-
self.engine.intention_cache.add_drop_intention(self.object_type, full_name)
138+
if result in (ResolveResult.REPLACE, ResolveResult.DROP):
139+
self.engine.intention_cache.add_object_drop_intention(self.object_type, full_name)
136140

137141
if result == ResolveResult.NOCHANGE:
138142
self.engine.logger.debug(f"Resolved {self.object_type.name} [{full_name}]: {result.value}")

snowddl/resolver/abc_role_resolver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def get_existing_role_grants(self, role_name):
9999
try:
100100
grant_name = build_grant_name_ident(self.config.env_prefix, r["name"], object_type)
101101
except (KeyError, ValueError):
102-
self.engine.intention_cache.add_invalid_name_warning(object_type, r["name"])
102+
self.engine.intention_cache.add_object_name_warning(object_type, r["name"])
103103
continue
104104

105105
grants.append(
@@ -128,7 +128,7 @@ def get_existing_role_grants(self, role_name):
128128
try:
129129
grant_name = build_future_grant_name_ident(object_type, r["name"])
130130
except ValueError:
131-
self.engine.intention_cache.add_invalid_name_warning(object_type, r["name"])
131+
self.engine.intention_cache.add_object_name_warning(object_type, r["name"])
132132
continue
133133

134134
future_grants.append(

snowddl/resolver/abc_schema_object_resolver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ def _resolve_drop(self):
2828
if object_full_name in self.blueprints:
2929
continue
3030

31-
# Parent object is going to be dropped
32-
if self.engine.intention_cache.check_parent_drop_intention(self.object_type, object_full_name):
31+
# Another object is going to be dropped, which implicitly drops this object
32+
if self._check_implicit_drop_intention(object_full_name):
3333
continue
3434

3535
schema_full_name = ".".join(object_full_name.split(".")[:2])

snowddl/resolver/foreign_key.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,33 @@ def drop_object(self, row: dict):
107107
)
108108

109109
return ResolveResult.DROP
110+
111+
def _check_implicit_drop_intention(self, object_full_name: str) -> bool:
112+
# Dropping any of parent objects implicitly drops this object
113+
if self.engine.intention_cache.check_parent_object_drop_intention(self.object_type, object_full_name):
114+
return True
115+
116+
fk = self.existing_objects[object_full_name]
117+
118+
for ref_table_object_type in [ObjectType.TABLE, ObjectType.EXTERNAL_TABLE]:
119+
# Dropping ref_table implicitly drops all foreign keys pointing to it
120+
if self.engine.intention_cache.check_object_drop_intention(ref_table_object_type, fk["ref_table_name"]):
121+
return True
122+
123+
# Dropping any of parent objects of ref_table implicitly drops this object
124+
if self.engine.intention_cache.check_parent_object_drop_intention(ref_table_object_type, fk["ref_table_name"]):
125+
return True
126+
127+
table_full_name = f"{fk['database']}.{fk['schema']}.{fk['table']}"
128+
129+
# Dropping one of foreign key columns implicitly drops the entire key
130+
for col_name in fk["columns"]:
131+
if self.engine.intention_cache.check_column_drop_intention(table_full_name, col_name):
132+
return True
133+
134+
# Dropping one of foreign key columns in ref_table implicitly drops the entire key
135+
for col_name in fk["ref_columns"]:
136+
if self.engine.intention_cache.check_column_drop_intention(fk["ref_table_name"], col_name):
137+
return True
138+
139+
return False

snowddl/resolver/primary_key.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,15 @@ def drop_object(self, row: dict):
9292
)
9393

9494
return ResolveResult.DROP
95+
96+
def _check_implicit_drop_intention(self, object_full_name: str) -> bool:
97+
# Dropping any of parent objects implicitly drops the entire key
98+
if self.engine.intention_cache.check_parent_object_drop_intention(self.object_type, object_full_name):
99+
return True
100+
101+
# Dropping one of primary key columns implicitly drops the entire key
102+
for col_name in self.existing_objects[object_full_name]["columns"]:
103+
if self.engine.intention_cache.check_column_drop_intention(object_full_name, col_name):
104+
return True
105+
106+
return False

snowddl/resolver/schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def _resolve_drop(self):
108108
continue
109109

110110
# Parent object is going to be dropped
111-
if self.engine.intention_cache.check_parent_drop_intention(self.object_type, schema_full_name):
111+
if self.engine.intention_cache.check_parent_object_drop_intention(self.object_type, schema_full_name):
112112
continue
113113

114114
database_full_name = ".".join(schema_full_name.split(".")[:1])

snowddl/resolver/table.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def compare_object(self, bp: TableBlueprint, row: dict):
7979
bp_cols = {str(c.name): c for c in bp.columns}
8080
snow_cols = self._get_existing_columns(bp)
8181

82+
dropping_col_names = []
8283
remaining_col_names = list(snow_cols.keys())
8384

8485
for col_name, snow_c in snow_cols.items():
@@ -93,7 +94,9 @@ def compare_object(self, bp: TableBlueprint, row: dict):
9394
)
9495
)
9596

97+
dropping_col_names.append(col_name)
9698
remaining_col_names.remove(col_name)
99+
97100
replace_notices.append(f"Column {col_name} is about to be dropped")
98101
continue
99102

@@ -354,6 +357,9 @@ def compare_object(self, bp: TableBlueprint, row: dict):
354357

355358
result = ResolveResult.ALTER
356359

360+
for col_name in dropping_col_names:
361+
self.engine.intention_cache.add_column_drop_intention(str(bp.full_name), col_name)
362+
357363
# If table was re-created, apply or suggest search optimization using exactly the same condition value
358364
if result == ResolveResult.REPLACE:
359365
self._create_search_optimization(bp, condition=self.engine.settings.execute_replace_table)

0 commit comments

Comments
 (0)