Skip to content

Commit 2eea4d9

Browse files
authored
feat: deny time travel retention decreases by default and support per-materialization target retention (#795)
* feat: deny time travel retention decreases by default and support per-materialization target retention * fix: never lower retention outside gated planned decreases * fix: lower retention on tables recreated during the build
1 parent bfb455f commit 2eea4d9

36 files changed

Lines changed: 1085 additions & 29 deletions

File tree

‎src/sqlbuild/cli/commands/_helpers/build_planning/defer_clone.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
)
5959
from sqlbuild.executor.clone.types import CloneStatus
6060
from sqlbuild.spec.contracts.main.resolve_target_config import resolve_target_config
61+
from sqlbuild.spec.contracts.types import RetentionDecreasePolicy
6162

6263

6364
def build_defer_clone_boundary_selectors(
@@ -304,6 +305,14 @@ def run_clone(on_item: Any) -> CloneExecutionResult:
304305
target_name=destination_target_name,
305306
).owns_time_travel_retention_namespace,
306307
),
308+
allow_namespace_retention_decrease=(
309+
resolve_target_config(
310+
project_config=discovered_inputs.project_config,
311+
local_config=discovered_inputs.local_config,
312+
target_name=destination_target_name,
313+
).time_travel_retention_decrease
314+
== RetentionDecreasePolicy.ALLOW
315+
),
307316
on_item=on_item,
308317
)
309318
)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Time-travel retention decrease safety enforcement for build commands."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TextIO
6+
7+
from sqlbuild.cli.commands.exceptions import CliUserError
8+
from sqlbuild.compiler.planner.models import PlanOutput, RetentionPlanEntry
9+
from sqlbuild.spec.contracts.types import RetentionDecreasePolicy
10+
11+
12+
def enforce_retention_decrease_policy(
13+
*,
14+
plan: PlanOutput,
15+
allow_retention_decrease: bool,
16+
input_stream: TextIO,
17+
output_stream: TextIO,
18+
) -> None:
19+
"""Fail or confirm before lowering live time-travel retention."""
20+
21+
entries: tuple[RetentionPlanEntry, ...] = tuple(
22+
entry for entry in plan.retention_entries if entry.decreases
23+
)
24+
denied: tuple[RetentionPlanEntry, ...] = tuple(
25+
entry for entry in entries if entry.decrease_policy == RetentionDecreasePolicy.DENY
26+
)
27+
if denied:
28+
raise CliUserError(
29+
f"time travel retention decrease is denied for {_names(denied)}",
30+
help=(
31+
"Keep the current retention, or set the target's "
32+
"time_travel_retention_decrease to 'require_confirmation' or 'allow'."
33+
),
34+
)
35+
confirmation: tuple[RetentionPlanEntry, ...] = tuple(
36+
entry
37+
for entry in entries
38+
if entry.decrease_policy == RetentionDecreasePolicy.REQUIRE_CONFIRMATION
39+
)
40+
if not confirmation or allow_retention_decrease:
41+
return
42+
if not input_stream.isatty():
43+
raise CliUserError(
44+
"time travel retention decrease requires confirmation",
45+
help="Pass --allow-retention-decrease to confirm in non-interactive runs.",
46+
)
47+
expected: str = _confirmation_text(confirmation)
48+
output_stream.write(
49+
f"Decreasing time travel retention for {_names(confirmation)} permanently discards "
50+
"history older than the new retention.\n\n"
51+
)
52+
output_stream.write(f"Type `{expected}` to continue: ")
53+
output_stream.flush()
54+
if input_stream.readline().strip() != expected:
55+
raise CliUserError("time travel retention decrease cancelled")
56+
57+
58+
def _model_names(entries: tuple[RetentionPlanEntry, ...]) -> tuple[str, ...]:
59+
names: dict[str, None] = {}
60+
for entry in entries:
61+
names.update(dict.fromkeys(entry.model_names))
62+
return tuple(names)
63+
64+
65+
def _confirmation_text(entries: tuple[RetentionPlanEntry, ...]) -> str:
66+
names: tuple[str, ...] = _model_names(entries)
67+
if len(names) == 1:
68+
return f"decrease retention for {names[0]}"
69+
return f"decrease retention for {len(names)} models"
70+
71+
72+
def _names(entries: tuple[RetentionPlanEntry, ...]) -> str:
73+
return ", ".join(f"'{name}'" for name in _model_names(entries))

‎src/sqlbuild/cli/commands/_helpers/build_virtual/virtual_execution.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def execute_virtual_build(
9696
full_refresh=request.full_refresh,
9797
allow_snapshot_full_refresh=request.allow_snapshot_full_refresh,
9898
allow_table_type_downgrade=request.allow_table_type_downgrade,
99+
allow_retention_decrease=request.allow_retention_decrease,
99100
use_color=request.use_color,
100101
verbose=request.verbose,
101102
debug=request.debug,

‎src/sqlbuild/cli/commands/_helpers/clone/execution.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from sqlbuild.runtime.observability.classes.operation_lifecycle import OperationLifecycle
4545
from sqlbuild.spec.contracts.main.resolve_target_config import resolve_target_config
4646
from sqlbuild.spec.contracts.models import SourceEntry
47+
from sqlbuild.spec.contracts.types import RetentionDecreasePolicy
4748

4849

4950
def execute_clone_plan(
@@ -102,6 +103,14 @@ def execute_clone_plan(
102103
target_name=invocation.destination_target_name,
103104
).owns_time_travel_retention_namespace,
104105
),
106+
allow_namespace_retention_decrease=(
107+
resolve_target_config(
108+
project_config=invocation.discovered_inputs.project_config,
109+
local_config=invocation.discovered_inputs.local_config,
110+
target_name=invocation.destination_target_name,
111+
).time_travel_retention_decrease
112+
== RetentionDecreasePolicy.ALLOW
113+
),
105114
on_item=on_item,
106115
)
107116
)

‎src/sqlbuild/cli/commands/_helpers/dbt/sqlbuild_work.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
from __future__ import annotations
44

5+
import sys
56
from pathlib import Path
67
from typing import TextIO
78

89
from sqlbuild.adapter.contract.classes.base_adapter import BaseAdapter
10+
from sqlbuild.cli.commands._helpers.build_planning.retention_decrease import (
11+
enforce_retention_decrease_policy,
12+
)
913
from sqlbuild.cli.commands.classes.build_progress_callbacks import (
1014
BuildProgressCallbacks,
1115
format_build_footer,
@@ -46,6 +50,12 @@ def execute_sqlbuild_build_work(
4650
adapter_name: str = context.adapter_name
4751
output_stream: TextIO = context.output_stream
4852
use_color: bool = context.use_color
53+
enforce_retention_decrease_policy(
54+
plan=plan_output,
55+
allow_retention_decrease=False,
56+
input_stream=sys.stdin,
57+
output_stream=output_stream,
58+
)
4959
callbacks: BuildProgressCallbacks = BuildProgressCallbacks(
5060
plan=plan_output, use_color=use_color, verbose=verbose, debug=False
5161
)

‎src/sqlbuild/cli/commands/_helpers/entry/dispatch.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def dispatch_cli_command(*, args: CliNamespace, handlers: CliEntrypointHandlers)
165165
include_python=args.include_python,
166166
allow_snapshot_full_refresh=args.allow_snapshot_full_refresh,
167167
allow_table_type_downgrade=args.allow_table_type_downgrade,
168+
allow_retention_decrease=args.allow_retention_decrease,
168169
allow_snapshot_schema_change=args.allow_snapshot_schema_change,
169170
concurrency=args.concurrency,
170171
select=select,

‎src/sqlbuild/cli/commands/_helpers/entry/parser_arguments.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def add_execution_args(parser: argparse.ArgumentParser) -> None:
6262
parser.add_argument("--full-refresh", action="store_true", default=False)
6363
parser.add_argument("--allow-snapshot-full-refresh", action="store_true", default=False)
6464
parser.add_argument("--allow-table-type-downgrade", action="store_true", default=False)
65+
parser.add_argument("--allow-retention-decrease", action="store_true", default=False)
6566
parser.add_argument("--allow-snapshot-schema-change", action="store_true", default=False)
6667
parser.add_argument("--concurrency", type=int, default=None)
6768
parser.add_argument("--verbose", "-v", action="store_true", default=False)

‎src/sqlbuild/cli/commands/classes/cli_namespace.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"run_audits": True,
6262
"allow_snapshot_full_refresh": False,
6363
"allow_table_type_downgrade": False,
64+
"allow_retention_decrease": False,
6465
"allow_snapshot_schema_change": False,
6566
"allow_partial_promotion": False,
6667
"allow_partial_rollback": False,
@@ -234,6 +235,7 @@ class CliNamespace:
234235
run_audits: bool
235236
allow_snapshot_full_refresh: bool
236237
allow_table_type_downgrade: bool
238+
allow_retention_decrease: bool
237239
allow_snapshot_schema_change: bool
238240
allow_partial_promotion: bool
239241
allow_partial_rollback: bool

‎src/sqlbuild/cli/commands/classes/virtual_build_plan_hook.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from sqlbuild.cli.commands._helpers.build_planning.full_refresh import (
1818
enforce_snapshot_full_refresh_policy,
1919
)
20+
from sqlbuild.cli.commands._helpers.build_planning.retention_decrease import (
21+
enforce_retention_decrease_policy,
22+
)
2023
from sqlbuild.cli.commands._helpers.build_planning.table_type import (
2124
enforce_table_type_downgrade_policy,
2225
)
@@ -52,6 +55,7 @@ def __init__(
5255
self._full_refresh = config.full_refresh
5356
self._allow_snapshot_full_refresh = config.allow_snapshot_full_refresh
5457
self._allow_table_type_downgrade = config.allow_table_type_downgrade
58+
self._allow_retention_decrease = config.allow_retention_decrease
5559
self._use_color = config.use_color
5660
self._verbose = config.verbose
5761
self._debug = config.debug
@@ -109,6 +113,12 @@ def on_plan_ready(
109113
input_stream=sys.stdin,
110114
output_stream=sys.stdout,
111115
)
116+
enforce_retention_decrease_policy(
117+
plan=plan_output,
118+
allow_retention_decrease=self._allow_retention_decrease,
119+
input_stream=sys.stdin,
120+
output_stream=sys.stdout,
121+
)
112122
write_compile_target(
113123
target_dir=self._project_dir / "target",
114124
adapter=self._adapter,

‎src/sqlbuild/cli/commands/main/execution/_build.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
)
3535
from sqlbuild.cli.commands._helpers.build_planning.invocation import resolve_build_invocation
3636
from sqlbuild.cli.commands._helpers.build_planning.planning import compile_build_plan
37+
from sqlbuild.cli.commands._helpers.build_planning.retention_decrease import (
38+
enforce_retention_decrease_policy,
39+
)
3740
from sqlbuild.cli.commands._helpers.build_planning.table_type import (
3841
enforce_table_type_downgrade_policy,
3942
)
@@ -129,6 +132,7 @@ def _run_build(
129132
fail_fast=request.fail_fast,
130133
allow_snapshot_full_refresh=request.allow_snapshot_full_refresh,
131134
allow_table_type_downgrade=request.allow_table_type_downgrade,
135+
allow_retention_decrease=request.allow_retention_decrease,
132136
allow_snapshot_schema_change=request.allow_snapshot_schema_change,
133137
concurrency=request.concurrency,
134138
verbose=request.verbose,
@@ -177,6 +181,12 @@ def _run_build(
177181
input_stream=sys.stdin,
178182
output_stream=sys.stdout,
179183
)
184+
enforce_retention_decrease_policy(
185+
plan=pipeline_result.plan_output,
186+
allow_retention_decrease=request.allow_retention_decrease,
187+
input_stream=sys.stdin,
188+
output_stream=sys.stdout,
189+
)
180190
write_build_compile_target(
181191
request=request,
182192
invocation=invocation,

0 commit comments

Comments
 (0)