Skip to content

Commit 75d1615

Browse files
Chore: Show resources to be deleted and enhance warnings in destroy (#4965)
1 parent 4ee6cb3 commit 75d1615

File tree

6 files changed

+109
-24
lines changed

6 files changed

+109
-24
lines changed

docs/reference/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ Options:
149149
```
150150
Usage: sqlmesh destroy
151151
152-
Removes all project resources, including warehouse objects, state tables, the SQLMesh cache and any build artifacts.
152+
Removes all state tables, the SQLMesh cache and all project resources, including warehouse objects. This includes all tables, views and schemas managed by SQLMesh, as well as any external resources that may have been created by other tools within those schemas.
153153
154154
Options:
155155
--help Show this message and exit.

docs/reference/notebook.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ options:
250250
```
251251
%destroy
252252
253-
Removes all project resources, including warehouse objects, state tables, the SQLMesh cache and any build artifacts.
253+
Removes all state tables, the SQLMesh cache, and other project resources, including warehouse objects. This includes all tables, views, and schemas managed by SQLMesh, as well as any external resources that may have been created by other tools within those schemas.
254254
```
255255

256256
#### dlt_refresh

sqlmesh/core/console.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,19 @@ class DestroyConsole(abc.ABC):
186186
"""Console for describing a destroy operation"""
187187

188188
@abc.abstractmethod
189-
def start_destroy(self) -> bool:
189+
def start_destroy(
190+
self,
191+
schemas_to_delete: t.Optional[t.Set[str]] = None,
192+
views_to_delete: t.Optional[t.Set[str]] = None,
193+
tables_to_delete: t.Optional[t.Set[str]] = None,
194+
) -> bool:
190195
"""Start a destroy operation.
191196
197+
Args:
198+
schemas_to_delete: Set of schemas that will be deleted
199+
views_to_delete: Set of views that will be deleted
200+
tables_to_delete: Set of tables that will be deleted
201+
192202
Returns:
193203
Whether or not the destroy operation should proceed
194204
"""
@@ -830,7 +840,12 @@ def print_connection_config(
830840
) -> None:
831841
pass
832842

833-
def start_destroy(self) -> bool:
843+
def start_destroy(
844+
self,
845+
schemas_to_delete: t.Optional[t.Set[str]] = None,
846+
views_to_delete: t.Optional[t.Set[str]] = None,
847+
tables_to_delete: t.Optional[t.Set[str]] = None,
848+
) -> bool:
834849
return True
835850

836851
def stop_destroy(self, success: bool = True) -> None:
@@ -1282,16 +1297,40 @@ def stop_cleanup(self, success: bool = False) -> None:
12821297
else:
12831298
self.log_error("Cleanup failed!")
12841299

1285-
def start_destroy(self) -> bool:
1300+
def start_destroy(
1301+
self,
1302+
schemas_to_delete: t.Optional[t.Set[str]] = None,
1303+
views_to_delete: t.Optional[t.Set[str]] = None,
1304+
tables_to_delete: t.Optional[t.Set[str]] = None,
1305+
) -> bool:
12861306
self.log_warning(
1287-
(
1288-
"This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n"
1289-
"The operation is irreversible and may disrupt any currently running or scheduled plans.\n"
1290-
"Use this command only when you intend to fully reset the project."
1291-
)
1307+
"This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n"
1308+
"The operation may disrupt any currently running or scheduled plans.\n"
12921309
)
1293-
if not self._confirm("Proceed?"):
1294-
self.log_error("Destroy aborted!")
1310+
1311+
if schemas_to_delete or views_to_delete or tables_to_delete:
1312+
if schemas_to_delete:
1313+
self.log_error("Schemas to be deleted:")
1314+
for schema in sorted(schemas_to_delete):
1315+
self.log_error(f" • {schema}")
1316+
1317+
if views_to_delete:
1318+
self.log_error("\nEnvironment views to be deleted:")
1319+
for view in sorted(views_to_delete):
1320+
self.log_error(f" • {view}")
1321+
1322+
if tables_to_delete:
1323+
self.log_error("\nSnapshot tables to be deleted:")
1324+
for table in sorted(tables_to_delete):
1325+
self.log_error(f" • {table}")
1326+
1327+
self.log_error(
1328+
"\nThis action will DELETE ALL the above resources managed by SQLMesh AND\n"
1329+
"potentially external resources created by other tools in these schemas.\n"
1330+
)
1331+
1332+
if not self._confirm("Are you ABSOLUTELY SURE you want to proceed with deletion?"):
1333+
self.log_error("Destroy operation cancelled.")
12951334
return False
12961335
return True
12971336

sqlmesh/core/context.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -858,10 +858,52 @@ def run_janitor(self, ignore_ttl: bool) -> bool:
858858
def destroy(self) -> bool:
859859
success = False
860860

861-
if self.console.start_destroy():
861+
# Collect resources to be deleted
862+
environments = self.state_reader.get_environments()
863+
schemas_to_delete = set()
864+
tables_to_delete = set()
865+
views_to_delete = set()
866+
all_snapshot_infos = set()
867+
868+
# For each environment find schemas and tables
869+
for environment in environments:
870+
all_snapshot_infos.update(environment.snapshots)
871+
snapshots = self.state_reader.get_snapshots(environment.snapshots).values()
872+
for snapshot in snapshots:
873+
if snapshot.is_model and not snapshot.is_symbolic:
874+
# Get the appropriate adapter
875+
if environment.gateway_managed and snapshot.model_gateway:
876+
adapter = self.engine_adapters.get(
877+
snapshot.model_gateway, self.engine_adapter
878+
)
879+
else:
880+
adapter = self.engine_adapter
881+
882+
if environment.suffix_target.is_schema or environment.suffix_target.is_catalog:
883+
schema = snapshot.qualified_view_name.schema_for_environment(
884+
environment.naming_info, dialect=adapter.dialect
885+
)
886+
catalog = snapshot.qualified_view_name.catalog_for_environment(
887+
environment.naming_info, dialect=adapter.dialect
888+
)
889+
if catalog:
890+
schemas_to_delete.add(f"{catalog}.{schema}")
891+
else:
892+
schemas_to_delete.add(schema)
893+
894+
if environment.suffix_target.is_table:
895+
view_name = snapshot.qualified_view_name.for_environment(
896+
environment.naming_info, dialect=adapter.dialect
897+
)
898+
views_to_delete.add(view_name)
899+
900+
# Add snapshot tables
901+
table_name = snapshot.table_name()
902+
tables_to_delete.add(table_name)
903+
904+
if self.console.start_destroy(schemas_to_delete, views_to_delete, tables_to_delete):
862905
try:
863-
self._destroy()
864-
success = True
906+
success = self._destroy()
865907
finally:
866908
self.console.stop_destroy(success=success)
867909

@@ -2723,7 +2765,7 @@ def _context_diff(
27232765
always_recreate_environment=always_recreate_environment,
27242766
)
27252767

2726-
def _destroy(self) -> None:
2768+
def _destroy(self) -> bool:
27272769
# Invalidate all environments, including prod
27282770
for environment in self.state_reader.get_environments():
27292771
self.state_sync.invalidate_environment(name=environment.name, protect_prod=False)
@@ -2739,6 +2781,8 @@ def _destroy(self) -> None:
27392781
# Finally clear caches
27402782
self.clear_caches()
27412783

2784+
return True
2785+
27422786
def _run_janitor(self, ignore_ttl: bool = False) -> None:
27432787
current_ts = now_timestamp()
27442788

tests/core/test_integration.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6354,7 +6354,9 @@ def test_destroy(copy_to_temp_path):
63546354
context.fetchdf(f"SELECT * FROM db_1.first_schema.model_two")
63556355

63566356
# Use the destroy command to remove all data objects and state
6357-
context._destroy()
6357+
# Mock the console confirmation to automatically return True
6358+
with patch.object(context.console, "_confirm", return_value=True):
6359+
context._destroy()
63586360

63596361
# Ensure all tables have been removed
63606362
for table_name in state_tables:

tests/integrations/jupyter/test_magics.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -898,19 +898,19 @@ def test_destroy(
898898
assert not output.stderr
899899
text_output = convert_all_html_output_to_text(output)
900900
expected_messages = [
901-
"[WARNING] This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n"
902-
"The operation is irreversible and may disrupt any currently running or scheduled plans.\n"
903-
"Use this command only when you intend to fully reset the project.",
901+
"[WARNING] This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\nThe operation may disrupt any currently running or scheduled plans.",
902+
"Schemas to be deleted:",
903+
"• memory.sushi",
904+
"Snapshot tables to be deleted:",
905+
"This action will DELETE ALL the above resources managed by SQLMesh AND\npotentially external resources created by other tools in these schemas.",
906+
"Are you ABSOLUTELY SURE you want to proceed with deletion? [y/n]:",
904907
"Environment 'prod' invalidated.",
905908
"Deleted object memory.sushi",
906909
'Deleted object "memory"."raw"."model1"',
907-
'Deleted object "memory"."raw"."model1"',
908-
'Deleted object "memory"."raw"."model2"',
909910
'Deleted object "memory"."raw"."model2"',
910911
'Deleted object "memory"."raw"."demographics"',
911-
'Deleted object "memory"."raw"."demographics"',
912912
"State tables removed.",
913913
"Destroy completed successfully.",
914914
]
915915
for message in expected_messages:
916-
assert message in text_output
916+
assert any(message in line for line in text_output)

0 commit comments

Comments
 (0)