Skip to content

Commit 94a66db

Browse files
Feat: Add destroy command to remove all project resources
1 parent 51fe510 commit 94a66db

12 files changed

Lines changed: 292 additions & 10 deletions

File tree

docs/reference/cli.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Commands:
2323
create_external_models Create a schema file containing external model...
2424
create_test Generate a unit test fixture for a given model.
2525
dag Render the DAG as an html file.
26+
destroy The destroy command removes all project resources.
2627
diff Show the diff between the local state and the...
2728
dlt_refresh Attaches to a DLT pipeline with the option to...
2829
environments Prints the list of SQLMesh environments with...
@@ -143,6 +144,17 @@ Options:
143144
--help Show this message and exit.
144145
```
145146

147+
## destroy
148+
149+
```
150+
Usage: sqlmesh destroy
151+
152+
Removes all project resources, including warehouse objects, state tables, the SQLMesh cache and any build artifacts.
153+
154+
Options:
155+
--help Show this message and exit.
156+
```
157+
146158
## dlt_refresh
147159

148160
```

docs/reference/notebook.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,13 @@ options:
232232
--file FILE, -f FILE An optional file path to write the HTML output to.
233233
```
234234

235+
#### destroy
236+
```
237+
%destroy
238+
239+
Removes all project resources, including warehouse objects, state tables, the SQLMesh cache and any build artifacts.
240+
```
241+
235242
#### dlt_refresh
236243
```
237244
%dlt_refresh PIPELINE [--table] TABLE [--force]

sqlmesh/cli/main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,19 @@ def janitor(ctx: click.Context, ignore_ttl: bool, **kwargs: t.Any) -> None:
546546
ctx.obj.run_janitor(ignore_ttl, **kwargs)
547547

548548

549+
@cli.command("destroy")
550+
@click.pass_context
551+
@error_handler
552+
@cli_analytics
553+
def destroy(ctx: click.Context, **kwargs: t.Any) -> None:
554+
"""
555+
The destroy command removes all project resources.
556+
557+
This includes engine-managed objects, state tables, the SQLMesh cache and any build artifacts.
558+
"""
559+
ctx.obj.destroy(**kwargs)
560+
561+
549562
@cli.command("dag")
550563
@click.argument("file", required=True)
551564
@click.option(

sqlmesh/core/console.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,26 @@ def stop_cleanup(self, success: bool = True) -> None:
181181
"""
182182

183183

184+
class DestroyConsole(abc.ABC):
185+
"""Console for describing a destroy operation"""
186+
187+
@abc.abstractmethod
188+
def start_destroy(self) -> bool:
189+
"""Start a destroy operation.
190+
191+
Returns:
192+
Whether or not the destroy operation should proceed
193+
"""
194+
195+
@abc.abstractmethod
196+
def stop_destroy(self, success: bool = True) -> None:
197+
"""Indicates the destroy operation has ended
198+
199+
Args:
200+
success: Whether or not the cleanup completed successfully
201+
"""
202+
203+
184204
class EnvironmentsConsole(abc.ABC):
185205
"""Console for displaying environments"""
186206

@@ -304,6 +324,7 @@ class Console(
304324
StateExporterConsole,
305325
StateImporterConsole,
306326
JanitorConsole,
327+
DestroyConsole,
307328
EnvironmentsConsole,
308329
DifferenceConsole,
309330
TableDiffConsole,
@@ -744,6 +765,12 @@ def print_connection_config(
744765
) -> None:
745766
pass
746767

768+
def start_destroy(self) -> bool:
769+
return True
770+
771+
def stop_destroy(self, success: bool = True) -> None:
772+
pass
773+
747774

748775
def make_progress_bar(
749776
message: str,
@@ -1092,6 +1119,25 @@ def stop_cleanup(self, success: bool = False) -> None:
10921119
else:
10931120
self.log_error("Cleanup failed!")
10941121

1122+
def start_destroy(self) -> bool:
1123+
self.log_warning(
1124+
(
1125+
"This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n"
1126+
"The operation is irreversible and may disrupt any currently running or scheduled plans.\n"
1127+
"Use this command only when you intend to fully reset the project."
1128+
)
1129+
)
1130+
if not self._confirm("Proceed?"):
1131+
self.log_error("Destroy aborted!")
1132+
return False
1133+
return True
1134+
1135+
def stop_destroy(self, success: bool = False) -> None:
1136+
if success:
1137+
self.log_success("Destroy completed successfully.")
1138+
else:
1139+
self.log_error("Destroy failed!")
1140+
10951141
def start_promotion_progress(
10961142
self,
10971143
snapshots: t.List[SnapshotTableInfo],

sqlmesh/core/context.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,19 @@ def run_janitor(self, ignore_ttl: bool) -> bool:
831831

832832
return success
833833

834+
@python_api_analytics
835+
def destroy(self) -> bool:
836+
success = False
837+
838+
if self.console.start_destroy():
839+
try:
840+
self._destroy()
841+
success = True
842+
finally:
843+
self.console.stop_destroy(success=success)
844+
845+
return success
846+
834847
@t.overload
835848
def get_model(
836849
self, model_or_snapshot: ModelOrSnapshot, raise_if_missing: Literal[True] = True
@@ -1516,16 +1529,19 @@ def apply(
15161529
)
15171530

15181531
@python_api_analytics
1519-
def invalidate_environment(self, name: str, sync: bool = False) -> None:
1532+
def invalidate_environment(
1533+
self, name: str, sync: bool = False, protect_prod: t.Optional[bool] = True
1534+
) -> None:
15201535
"""Invalidates the target environment by setting its expiration timestamp to now.
15211536
15221537
Args:
15231538
name: The name of the environment to invalidate.
15241539
sync: If True, the call blocks until the environment is deleted. Otherwise, the environment will
15251540
be deleted asynchronously by the janitor process.
1541+
protect_prod: If True, prevents invalidation of the production environment.
15261542
"""
15271543
name = Environment.sanitize_name(name)
1528-
self.state_sync.invalidate_environment(name)
1544+
self.state_sync.invalidate_environment(name, protect_prod)
15291545
if sync:
15301546
self._cleanup_environments()
15311547
self.console.log_success(f"Environment '{name}' deleted.")
@@ -2499,6 +2515,21 @@ def _context_diff(
24992515
gateway_managed_virtual_layer=self.gateway_managed_virtual_layer,
25002516
)
25012517

2518+
def _destroy(self) -> None:
2519+
# Invalidate all environments, including prod
2520+
for environment in self.state_reader.get_environments():
2521+
self.invalidate_environment(name=environment.name, protect_prod=False)
2522+
2523+
# Run janitor to clean up all objects
2524+
self._run_janitor(ignore_ttl=True)
2525+
2526+
# Remove state tables
2527+
self.state_sync.remove_state()
2528+
self.console.log_status_update("State tables removed.")
2529+
2530+
# Finally clear caches
2531+
self.clear_caches()
2532+
25022533
def _run_janitor(self, ignore_ttl: bool = False) -> None:
25032534
current_ts = now_timestamp()
25042535

sqlmesh/core/state_sync/base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,13 +347,18 @@ def delete_expired_snapshots(
347347
"""
348348

349349
@abc.abstractmethod
350-
def invalidate_environment(self, name: str) -> None:
350+
def invalidate_environment(self, name: str, protect_prod: t.Optional[bool] = True) -> None:
351351
"""Invalidates the target environment by setting its expiration timestamp to now.
352352
353353
Args:
354354
name: The name of the environment to invalidate.
355+
protect_prod: If True, prevents invalidation of the production environment.
355356
"""
356357

358+
@abc.abstractmethod
359+
def remove_state(self) -> None:
360+
"""Removes the state store objects."""
361+
357362
@abc.abstractmethod
358363
def remove_intervals(
359364
self,

sqlmesh/core/state_sync/db/environment.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,15 @@ def update_environment_statements(
108108
columns_to_types=self._environment_statements_columns_to_types,
109109
)
110110

111-
def invalidate_environment(self, name: str) -> None:
111+
def invalidate_environment(self, name: str, protect_prod: t.Optional[bool] = True) -> None:
112112
"""Invalidates the environment.
113113
114114
Args:
115115
name: The name of the environment
116+
protect_prod: If True, prevents invalidation of the production environment.
116117
"""
117118
name = name.lower()
118-
if name == c.PROD:
119+
if protect_prod and name == c.PROD:
119120
raise SQLMeshError("Cannot invalidate the production environment.")
120121

121122
filter_expr = exp.column("name").eq(name)

sqlmesh/core/state_sync/db/facade.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,8 @@ def unpause_snapshots(
270270
) -> None:
271271
self.snapshot_state.unpause_snapshots(snapshots, unpaused_dt, self.interval_state)
272272

273-
def invalidate_environment(self, name: str) -> None:
274-
self.environment_state.invalidate_environment(name)
273+
def invalidate_environment(self, name: str, protect_prod: t.Optional[bool] = True) -> None:
274+
self.environment_state.invalidate_environment(name, protect_prod)
275275

276276
def get_expired_snapshots(
277277
self, current_ts: int, ignore_ttl: bool = False
@@ -313,18 +313,23 @@ def snapshots_exist(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> t.Set[Sna
313313
def nodes_exist(self, names: t.Iterable[str], exclude_external: bool = False) -> t.Set[str]:
314314
return self.snapshot_state.nodes_exist(names, exclude_external)
315315

316-
def reset(self, default_catalog: t.Optional[str]) -> None:
317-
"""Resets the state store to the state when it was first initialized."""
316+
def remove_state(self) -> None:
317+
"""Removes the state store objects."""
318318
for table in (
319319
self.snapshot_state.snapshots_table,
320320
self.snapshot_state.auto_restatements_table,
321321
self.environment_state.environments_table,
322+
self.environment_state.environment_statements_table,
322323
self.interval_state.intervals_table,
323324
self.plan_dags_table,
324325
self.version_state.versions_table,
325326
):
326327
self.engine_adapter.drop_table(table)
327328
self.snapshot_state.clear_cache()
329+
330+
def reset(self, default_catalog: t.Optional[str]) -> None:
331+
"""Resets the state store to the state when it was first initialized."""
332+
self.remove_state()
328333
self.migrate(default_catalog)
329334

330335
@transactional()

sqlmesh/magics.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,13 @@ def lint(self, context: Context, line: str) -> None:
11111111
args = parse_argstring(self.lint, line)
11121112
context.lint_models(args.models)
11131113

1114+
@magic_arguments()
1115+
@line_magic
1116+
@pass_sqlmesh_context
1117+
def destroy(self, context: Context, line: str) -> None:
1118+
"""Removes all project resources, engine-managed objects, state tables and clears the SQLMesh cache."""
1119+
context.destroy()
1120+
11141121

11151122
def register_magics() -> None:
11161123
try:

0 commit comments

Comments
 (0)