Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Online: https://sqlbuild.com/docs/cli/janitor/

- Usage
- Flags
- Targets
- Lifecycle
- Compatibility views
- Pending migration origins
Expand All @@ -33,11 +34,34 @@ sqb --project-dir <path> janitor [flags]

| Flag | Description |
|------|-------------|
| `--target <name>` | Clean up a configured target instead of the active/default target, using that target's connection, schemas and state |
| `--as <name>` | Preview the janitor plan for another configured target's schemas through the active target's connection. Inspection only: nothing is archived, dropped or pruned, and there is no prompt. Cannot be combined with `--auto-approve` |
| `--auto-approve` | Skip the confirmation prompt and run the planned actions immediately |
| `--retention-days` | Override the configured retention period (days) before a stale relation is archived |
| `--direct-state-history-versions` | Override how many state-history versions are kept per identity |
| `--drop-old-name-view <name>` | Drop the [compatibility view](../concepts/models/migrations.md#old-names) at a renamed model's old name before it expires. Repeat for several views. Fails with `C503` if no compatibility view is recorded at the name |

## Targets

By default the janitor cleans up the active target. `--target <name>` selects another configured target, exactly like `sqb build --target`: the janitor connects with that target's connection and inspects and cleans that target's schemas and state tables. Other targets are never touched.

`--as <name>` previews what the janitor would do in another target without changing anything, like [`sqb plan --as`](plan.md). The project compiles for that target, and its schemas are inspected through the active target's connection, which can be the default target or the one given with `--target`. The janitor lists every relation it would archive, every archive it would delete, every compatibility view it would drop and every state table it would prune, then exits. It drops nothing, writes no audit or state rows, prunes nothing and does not prompt. `--drop-old-name-view` is allowed with `--as` and only previews the drop.

```
Previewing janitor as target 'prod' through the connection of target 'dev' (inspection only).
...
Relations to archive
prod.customers -> prod._sqb_archive__20261001t101500z__customers age unknown, delete after 2026-10-15 10:15:00 UTC

Archives to delete
prod._sqb_archive__20200101t000000z__old_products archived 2020-01-01 00:00:00 UTC, age 2465d, expired 2020-01-15 00:00:00 UTC

Previewed janitor as target 'prod' through the connection of target 'dev'. Nothing was changed.
Rerun with `--target prod` instead of `--as prod` to apply it.
```

An unknown target name fails before anything connects. A preview exits with `0`, or with `1` when a schema is blocked, as a normal run would.

## Lifecycle

The janitor never drops a live relation directly. Each stale relation goes through two stages:
Expand Down Expand Up @@ -134,6 +158,12 @@ sqb janitor --auto-approve

# Override retention to 7 days
sqb janitor --retention-days 7

# Preview the prod cleanup through the current connection, changing nothing
sqb janitor --as prod

# Clean up the prod target
sqb janitor --target prod
```

## State history pruning
Expand Down
2 changes: 2 additions & 0 deletions src/sqlbuild/cli/commands/_helpers/entry/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ def dispatch_cli_command(*, args: CliNamespace, handlers: CliEntrypointHandlers)
retention_days=args.retention_days,
direct_state_history_versions=args.direct_state_history_versions,
drop_old_name_views=tuple(args.drop_old_name_views),
selected_target=args.target,
as_target=args.as_target,
)
)
return _dispatch_local_command(
Expand Down
10 changes: 10 additions & 0 deletions src/sqlbuild/cli/commands/_helpers/entry/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,16 @@ def _add_maintenance_parsers(
metavar="NAME",
dest="drop_old_name_views",
)
janitor_parser.add_argument("--target", default=None)
janitor_parser.add_argument(
"--as",
dest="as_target",
default=None,
help=(
"Preview the janitor plan as another configured target through the active "
"target's connection; inspection only"
),
)


def _add_workspace_parsers(
Expand Down
4 changes: 4 additions & 0 deletions src/sqlbuild/cli/commands/_helpers/entry/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ def parse_cli_invocation(
_validate_scope_args(args=args, parser=parser)
if args.command == CliCommand.TEST:
_validate_test_args(args=args, parser=parser)
if args.command == CliCommand.JANITOR and args.as_target is not None and args.auto_approve:
parser.error(
"janitor --as is inspection only and cannot be combined with --auto-approve"
)
except SystemExit as error:
exit_code: int = error.code if isinstance(error.code, int) else 1
return ParsedCliInvocation(args=None, exit_code=exit_code)
Expand Down
33 changes: 33 additions & 0 deletions src/sqlbuild/cli/commands/_helpers/janitor_output/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,39 @@ def write_janitor_disabled(*, invocation: JanitorInvocation) -> None:
write_disabled(stream=sys.stdout, use_color=invocation.use_color)


def write_janitor_preview_start(*, invocation: JanitorInvocation) -> None:
"""Announce an inspection-only ``--as`` janitor preview before any work starts."""

sys.stdout.write(
f"Previewing janitor as target '{invocation.as_target}' through "
f"{_active_connection_label(invocation=invocation)} (inspection only).\n"
)
sys.stdout.flush()


def write_janitor_preview_complete(*, invocation: JanitorInvocation) -> None:
"""Close an ``--as`` janitor preview with an unambiguous nothing-changed state."""

style: CliStyle = CliStyle(use_color=invocation.use_color)
sys.stdout.write(
style.success(
f"Previewed janitor as target '{invocation.as_target}' through "
f"{_active_connection_label(invocation=invocation)}. Nothing was changed."
)
+ "\n"
)
sys.stdout.write(
f"Rerun with `--target {invocation.as_target}` instead of "
f"`--as {invocation.as_target}` to apply it.\n"
)


def _active_connection_label(*, invocation: JanitorInvocation) -> str:
if invocation.active_target_name is None:
return "the active connection"
return f"the connection of target '{invocation.active_target_name}'"


def write_janitor_plan(
*, invocation: JanitorInvocation, planning_result: JanitorPlanningResult
) -> None:
Expand Down
43 changes: 20 additions & 23 deletions src/sqlbuild/cli/commands/_helpers/janitor_runtime/compilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,28 @@
import sys
import time

from sqlbuild.adapter.contract.classes.base_adapter import BaseAdapter
from sqlbuild.cli.commands._helpers.runtime.adapters import resolve_adapter
from sqlbuild.cli.commands._helpers.runtime.connection import resolve_connection_config
from sqlbuild.cli.commands._helpers.runtime.adapter_context import (
resolve_adapter_connection_context,
)
from sqlbuild.cli.commands.models import (
AdapterConnectionContext,
JanitorCompileContext,
JanitorInvocation,
)
from sqlbuild.cli.progress.classes.planning_progress_reporter import PlanningProgressReporter
from sqlbuild.compiler.compile.models import CompiledProject
from sqlbuild.compiler.pipeline.main.project import compile_project
from sqlbuild.runtime.observability.classes.operation_lifecycle import OperationLifecycle
from sqlbuild.spec.contracts.main.resolve_effective_adapter_name import (
resolve_effective_adapter_name,
)


def compile_janitor_project(*, invocation: JanitorInvocation) -> JanitorCompileContext:
"""Resolve adapter, compile project, and prepare connection config."""
"""Compile for the janitor or ``--as`` target and resolve the active target connection."""

adapter_name: str = resolve_effective_adapter_name(
project_config=invocation.discovered_inputs.project_config,
local_config=invocation.discovered_inputs.local_config,
)
adapter: BaseAdapter = resolve_adapter(
adapter_name=adapter_name,
project_dir=invocation.effective_project_dir,
adapter_context: AdapterConnectionContext = resolve_adapter_connection_context(
discovered_inputs=invocation.discovered_inputs,
effective_project_dir=invocation.effective_project_dir,
selected_target=invocation.selected_target,
cli_vars=None,
)
compile_start: float = time.perf_counter()
status: PlanningProgressReporter = PlanningProgressReporter(
Expand All @@ -41,17 +37,18 @@ def compile_janitor_project(*, invocation: JanitorInvocation) -> JanitorCompileC
status.on_progress("Compiling project...")
project: CompiledProject = compile_project(
discovered_inputs=invocation.discovered_inputs,
adapter=adapter,
adapter=adapter_context.adapter,
selected_target=(
invocation.as_target
if invocation.as_target is not None
else invocation.selected_target
),
resolved_connection=adapter_context.connection_config,
)
status.on_progress(f"Compiled project. ({time.perf_counter() - compile_start:.2f}s)")
connection_config: dict[str, object] = resolve_connection_config(
raw_config=project.effective_connection,
project_dir=invocation.effective_project_dir,
adapter_name=adapter_name,
)
return JanitorCompileContext(
adapter_name=adapter_name,
adapter=adapter,
adapter_name=adapter_context.adapter_name,
adapter=adapter_context.adapter,
project=project,
connection_config=connection_config,
connection_config=adapter_context.connection_config,
)
13 changes: 13 additions & 0 deletions src/sqlbuild/cli/commands/_helpers/janitor_runtime/invocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pathlib import Path

from sqlbuild.cli.commands._helpers.runtime.preview_target import validate_preview_target
from sqlbuild.cli.commands.exceptions import CliUserError
from sqlbuild.cli.commands.models import (
JanitorCommandRequest,
Expand All @@ -14,6 +15,7 @@
from sqlbuild.compiler.discovery.models import DiscoveredProjectInputs
from sqlbuild.executor.janitor.main.resolve_retention_days import resolve_janitor_retention_days
from sqlbuild.presentation.main.supports_color import supports_color
from sqlbuild.spec.contracts.main.resolve_target_name import resolve_target_name


def resolve_janitor_invocation(*, request: JanitorCommandRequest) -> JanitorInvocation:
Expand All @@ -25,10 +27,21 @@ def resolve_janitor_invocation(*, request: JanitorCommandRequest) -> JanitorInvo
discovered_inputs: DiscoveredProjectInputs = discover_project_inputs(
project_dir=effective_project_dir
)
validate_preview_target(
discovered_inputs=discovered_inputs, as_target=request.as_target, command_name="janitor"
)
active_target_name: str | None = resolve_target_name(
project_config=discovered_inputs.project_config,
local_config=discovered_inputs.local_config,
selected_target=request.selected_target,
)
return JanitorInvocation(
effective_project_dir=effective_project_dir,
discovered_inputs=discovered_inputs,
use_color=not request.no_color and supports_color(),
selected_target=request.selected_target,
active_target_name=active_target_name,
as_target=request.as_target,
)


Expand Down
21 changes: 4 additions & 17 deletions src/sqlbuild/cli/commands/_helpers/plan/invocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from sqlbuild.cli.commands._helpers.runtime.adapter_context import (
resolve_adapter_connection_context,
)
from sqlbuild.cli.commands.exceptions import CliUserError
from sqlbuild.cli.commands._helpers.runtime.preview_target import validate_preview_target
from sqlbuild.cli.commands.models import (
AdapterConnectionContext,
PlanCommandRequest,
Expand All @@ -34,7 +34,9 @@ def resolve_plan_invocation(*, request: PlanCommandRequest) -> PlanInvocation:
discovered_inputs: DiscoveredProjectInputs = discover_project_inputs(
project_dir=effective_project_dir, extract_output_column_locations=False
)
_validate_preview_target(discovered_inputs=discovered_inputs, as_target=request.as_target)
validate_preview_target(
discovered_inputs=discovered_inputs, as_target=request.as_target, command_name="plan"
)
adapter_context: AdapterConnectionContext = resolve_adapter_connection_context(
discovered_inputs=discovered_inputs,
effective_project_dir=effective_project_dir,
Expand Down Expand Up @@ -64,18 +66,3 @@ def resolve_plan_invocation(*, request: PlanCommandRequest) -> PlanInvocation:
planning_progress=reporters.planning,
should_load_sources=should_load_sources,
)


def _validate_preview_target(
*, discovered_inputs: DiscoveredProjectInputs, as_target: str | None
) -> None:
if as_target is None:
return
configured: set[str] = set(discovered_inputs.project_config.targets) | set(
discovered_inputs.local_config.targets
)
if as_target not in configured:
raise CliUserError(
f"unknown target '{as_target}' for plan --as",
help=f"Configured targets: {', '.join(sorted(configured)) or 'none'}.",
)
23 changes: 23 additions & 0 deletions src/sqlbuild/cli/commands/_helpers/runtime/preview_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Shared validation for inspection-only ``--as`` target previews."""

from __future__ import annotations

from sqlbuild.cli.commands.exceptions import CliUserError
from sqlbuild.compiler.discovery.models import DiscoveredProjectInputs


def validate_preview_target(
*, discovered_inputs: DiscoveredProjectInputs, as_target: str | None, command_name: str
) -> None:
"""Fail clearly when ``--as`` names a target that is not configured."""

if as_target is None:
return
configured: set[str] = set(discovered_inputs.project_config.targets) | set(
discovered_inputs.local_config.targets
)
if as_target not in configured:
raise CliUserError(
f"unknown target '{as_target}' for {command_name} --as",
help=f"Configured targets: {', '.join(sorted(configured)) or 'none'}.",
)
7 changes: 7 additions & 0 deletions src/sqlbuild/cli/commands/main/state/_janitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
write_janitor_completion,
write_janitor_disabled,
write_janitor_plan,
write_janitor_preview_complete,
write_janitor_preview_start,
)
from sqlbuild.cli.commands._helpers.janitor_runtime.compilation import compile_janitor_project
from sqlbuild.cli.commands._helpers.janitor_runtime.connections import (
Expand Down Expand Up @@ -42,6 +44,8 @@ def run_janitor(request: JanitorCommandRequest) -> int:
write_janitor_disabled(invocation=invocation)
return 0
settings: JanitorSettings = resolve_janitor_settings(request=request, invocation=invocation)
if invocation.as_target is not None:
write_janitor_preview_start(invocation=invocation)
compile_context: JanitorCompileContext = compile_janitor_project(invocation=invocation)
connection_context: JanitorConnectionContext = connect_janitor_warehouse(
invocation=invocation,
Expand All @@ -55,6 +59,9 @@ def run_janitor(request: JanitorCommandRequest) -> int:
connection_context=connection_context,
)
write_janitor_plan(invocation=invocation, planning_result=planning_result)
if invocation.as_target is not None:
write_janitor_preview_complete(invocation=invocation)
return 1 if planning_result.plan.blocked_schemas else 0
if planning_result.plan.blocked_schemas:
return 1
if not janitor_plan_has_work(planning_result):
Expand Down
5 changes: 5 additions & 0 deletions src/sqlbuild/cli/commands/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,8 @@ class JanitorCommandRequest:
retention_days: int | None = None
direct_state_history_versions: int | None = None
drop_old_name_views: tuple[str, ...] = ()
selected_target: str | None = None
as_target: str | None = None


@dataclass(frozen=True)
Expand All @@ -661,6 +663,9 @@ class JanitorInvocation:
effective_project_dir: Path
discovered_inputs: DiscoveredProjectInputs
use_color: bool
selected_target: str | None = None
active_target_name: str | None = None
as_target: str | None = None


@dataclass(frozen=True)
Expand Down
4 changes: 4 additions & 0 deletions src/sqlbuild/compiler/pipeline/main/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ def compile_project(
*,
discovered_inputs: DiscoveredProjectInputs,
adapter: BaseAdapter,
selected_target: str | None = None,
resolved_connection: dict[str, object] | None = None,
no_sql_validation: bool = False,
external_sql_reference_resolver: ExternalSqlReferenceResolver | None = None,
) -> CompiledProject:
Expand All @@ -21,6 +23,8 @@ def compile_project(
return build_compiled_project(
discovered_inputs=discovered_inputs,
adapter=adapter,
selected_target=selected_target,
resolved_connection=resolved_connection,
no_sql_validation=no_sql_validation,
external_sql_reference_resolver=external_sql_reference_resolver,
)
Loading
Loading