Skip to content

Commit 8a42599

Browse files
committed
fix(dbt): apply sqb build plan safety policies on dbt run and build
sqb dbt run and build enforced only the missing-origin and retention decrease policies before executing SQLBuild work, so a table_type_downgrade, snapshot full-refresh or max_models policy could be bypassed through the dbt interop path. Both commands now share the sqb build gate sequence. sqb dbt has no allow flags: a policy that needs confirmation prompts on a terminal and otherwise refuses with guidance naming the matching sqb build flag or policy setting.
1 parent 671791a commit 8a42599

21 files changed

Lines changed: 496 additions & 39 deletions

File tree

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/dbt.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ Build dbt models first (including dbt tests), then SQLBuild models with audits.
3434
sqb dbt build [--select <selector>...] [--exclude <selector>...]
3535
```
3636

37+
### Plan safety policies
38+
39+
After dbt finishes, `sqb dbt run` and `sqb dbt build` apply the same plan safety policies as
40+
[`sqb build`](build.md) before any SQLBuild model executes: blocked model and column
41+
migrations, compatibility-view conflicts, the target's `execution_limits.max_models`, the
42+
snapshot full-refresh policy, `table_type_downgrade`, `time_travel_retention_decrease`, and
43+
`missing_migration_origin`. `sqb dbt` has no `--allow-*` flags. A policy that requires
44+
confirmation asks for the typed confirmation on an interactive terminal and refuses otherwise;
45+
build the affected models with `sqb build` and its allow flag instead, or relax the policy. A
46+
refusal at this point leaves completed dbt work in place and builds no SQLBuild models.
47+
3748
## sqb dbt debug
3849

3950
Run dbt diagnostics followed by SQLBuild diagnostics.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from sqlbuild.cli.commands._helpers.planning.external_refs import (
1414
resolve_external_sql_reference_resolver,
1515
)
16+
from sqlbuild.cli.commands.constants import EXECUTION_LIMIT_BUILD_NOTE
1617
from sqlbuild.cli.commands.exceptions import CliUserError
1718
from sqlbuild.cli.commands.models import (
1819
BuildCommandRequest,
@@ -196,6 +197,7 @@ def run_defer_clone_boundary_prephase(
196197
model_count=selected_model_count,
197198
target_name=invocation.effective_target_name,
198199
limits=invocation.execution_limits,
200+
refusal_note=EXECUTION_LIMIT_BUILD_NOTE,
199201
)
200202
run_defer_clone_prephase(
201203
inputs=DeferClonePrephaseInputs(

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def enforce_model_execution_limit(
1919
model_count: int,
2020
target_name: str | None,
2121
limits: ExecutionLimitsConfig,
22+
refusal_note: str,
2223
) -> None:
2324
"""Reject a build whose expanded model count exceeds its target policy."""
2425

@@ -33,7 +34,7 @@ def enforce_model_execution_limit(
3334
f"Maximum models: {maximum}\n"
3435
"Configuration: "
3536
f"targets.{target_label}.execution_limits.max_models\n\n"
36-
"No warehouse changes were made.",
37+
f"{refusal_note}",
3738
code="C413",
3839
help=limits.remediation,
3940
)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def enforce_snapshot_full_refresh_policy(
2525
plan: PlanOutput,
2626
snapshots_config: SnapshotsConfig,
2727
allow_snapshot_full_refresh: bool,
28+
non_interactive_help: str,
2829
input_stream: TextIO,
2930
output_stream: TextIO,
3031
) -> None:
@@ -69,7 +70,7 @@ def enforce_snapshot_full_refresh_policy(
6970
raise CliUserError(
7071
"snapshot full refresh requires confirmation",
7172
code="C239",
72-
help="Pass --allow-snapshot-full-refresh to confirm in non-interactive runs.",
73+
help=non_interactive_help,
7374
)
7475

7576
if not _confirm(

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

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,20 @@
1919
from sqlbuild.cli.commands._helpers.build_planning.table_type import (
2020
enforce_table_type_downgrade_policy,
2121
)
22-
from sqlbuild.cli.commands.constants import MISSING_ORIGIN_BUILD_HELP
22+
from sqlbuild.cli.commands.constants import (
23+
EXECUTION_LIMIT_BUILD_NOTE,
24+
EXECUTION_LIMIT_DBT_NOTE,
25+
MISSING_ORIGIN_BUILD_HELP,
26+
MISSING_ORIGIN_DBT_HELP,
27+
RETENTION_DECREASE_BUILD_HELP,
28+
RETENTION_DECREASE_DBT_HELP,
29+
SNAPSHOT_FULL_REFRESH_BUILD_HELP,
30+
SNAPSHOT_FULL_REFRESH_DBT_HELP,
31+
TABLE_TYPE_DOWNGRADE_BUILD_HELP,
32+
TABLE_TYPE_DOWNGRADE_DBT_HELP,
33+
)
2334
from sqlbuild.cli.commands.exceptions import CliUserError
24-
from sqlbuild.cli.commands.models import BuildCommandRequest, BuildInvocation
35+
from sqlbuild.cli.commands.models import BuildCommandRequest, BuildInvocation, PlanSafetyGates
2536
from sqlbuild.compiler.migrations.types import ColumnMigrationDecision, MigrationDecision
2637
from sqlbuild.compiler.planner.constants import HIDDEN_ORIGIN_REMEDY
2738
from sqlbuild.compiler.planner.models import (
@@ -31,6 +42,7 @@
3142
PlanWarning,
3243
)
3344
from sqlbuild.compiler.planner.types import WarningSeverity
45+
from sqlbuild.spec.contracts.models import ExecutionLimitsConfig, SnapshotsConfig
3446
from sqlbuild.spec.contracts.types import MissingMigrationOriginPolicy
3547

3648
_OLD_NAME_CODES: frozenset[str] = frozenset({"M114", "P008"})
@@ -44,40 +56,92 @@
4456

4557
def enforce_build_plan_policies(
4658
*, request: BuildCommandRequest, invocation: BuildInvocation, plan: PlanOutput
59+
) -> None:
60+
"""Apply every plan safety gate before a direct build executes anything."""
61+
62+
enforce_plan_safety_policies(
63+
plan=plan,
64+
gates=PlanSafetyGates(
65+
allow_missing_migration_origin=request.allow_missing_migration_origin,
66+
allow_snapshot_full_refresh=request.allow_snapshot_full_refresh,
67+
allow_table_type_downgrade=request.allow_table_type_downgrade,
68+
allow_retention_decrease=request.allow_retention_decrease,
69+
missing_origin_help=MISSING_ORIGIN_BUILD_HELP,
70+
snapshot_full_refresh_help=SNAPSHOT_FULL_REFRESH_BUILD_HELP,
71+
table_type_downgrade_help=TABLE_TYPE_DOWNGRADE_BUILD_HELP,
72+
retention_decrease_help=RETENTION_DECREASE_BUILD_HELP,
73+
execution_limit_note=EXECUTION_LIMIT_BUILD_NOTE,
74+
),
75+
snapshots_config=invocation.discovered_inputs.project_config.snapshots,
76+
target_name=invocation.effective_target_name,
77+
execution_limits=invocation.execution_limits,
78+
streams=(sys.stdin, sys.stdout),
79+
)
80+
81+
82+
def dbt_interop_plan_safety_gates() -> PlanSafetyGates:
83+
"""Gates for sqb dbt, which has no allow flags and confirms only on a terminal."""
84+
85+
return PlanSafetyGates(
86+
allow_missing_migration_origin=False,
87+
allow_snapshot_full_refresh=False,
88+
allow_table_type_downgrade=False,
89+
allow_retention_decrease=False,
90+
missing_origin_help=MISSING_ORIGIN_DBT_HELP,
91+
snapshot_full_refresh_help=SNAPSHOT_FULL_REFRESH_DBT_HELP,
92+
table_type_downgrade_help=TABLE_TYPE_DOWNGRADE_DBT_HELP,
93+
retention_decrease_help=RETENTION_DECREASE_DBT_HELP,
94+
execution_limit_note=EXECUTION_LIMIT_DBT_NOTE,
95+
)
96+
97+
98+
def enforce_plan_safety_policies(
99+
*,
100+
plan: PlanOutput,
101+
gates: PlanSafetyGates,
102+
snapshots_config: SnapshotsConfig,
103+
target_name: str | None,
104+
execution_limits: ExecutionLimitsConfig,
105+
streams: tuple[TextIO, TextIO],
47106
) -> None:
48107
"""Apply migration, execution-limit, and storage safety gates in their required order."""
49108

109+
input_stream, output_stream = streams
50110
enforce_migration_plan_policies(
51111
plan=plan,
52-
allow_missing_migration_origin=request.allow_missing_migration_origin,
53-
non_interactive_help=MISSING_ORIGIN_BUILD_HELP,
54-
input_stream=sys.stdin,
55-
output_stream=sys.stdout,
112+
allow_missing_migration_origin=gates.allow_missing_migration_origin,
113+
non_interactive_help=gates.missing_origin_help,
114+
input_stream=input_stream,
115+
output_stream=output_stream,
56116
)
57117
_enforce_old_name_policy(plan=plan)
58118
enforce_model_execution_limit(
59119
model_count=executable_model_count(plan=plan),
60-
target_name=invocation.effective_target_name,
61-
limits=invocation.execution_limits,
120+
target_name=target_name,
121+
limits=execution_limits,
122+
refusal_note=gates.execution_limit_note,
62123
)
63124
enforce_snapshot_full_refresh_policy(
64125
plan=plan,
65-
snapshots_config=invocation.discovered_inputs.project_config.snapshots,
66-
allow_snapshot_full_refresh=request.allow_snapshot_full_refresh,
67-
input_stream=sys.stdin,
68-
output_stream=sys.stdout,
126+
snapshots_config=snapshots_config,
127+
allow_snapshot_full_refresh=gates.allow_snapshot_full_refresh,
128+
non_interactive_help=gates.snapshot_full_refresh_help,
129+
input_stream=input_stream,
130+
output_stream=output_stream,
69131
)
70132
enforce_table_type_downgrade_policy(
71133
plan=plan,
72-
allow_table_type_downgrade=request.allow_table_type_downgrade,
73-
input_stream=sys.stdin,
74-
output_stream=sys.stdout,
134+
allow_table_type_downgrade=gates.allow_table_type_downgrade,
135+
non_interactive_help=gates.table_type_downgrade_help,
136+
input_stream=input_stream,
137+
output_stream=output_stream,
75138
)
76139
enforce_retention_decrease_policy(
77140
plan=plan,
78-
allow_retention_decrease=request.allow_retention_decrease,
79-
input_stream=sys.stdin,
80-
output_stream=sys.stdout,
141+
allow_retention_decrease=gates.allow_retention_decrease,
142+
non_interactive_help=gates.retention_decrease_help,
143+
input_stream=input_stream,
144+
output_stream=output_stream,
81145
)
82146

83147

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def enforce_retention_decrease_policy(
1313
*,
1414
plan: PlanOutput,
1515
allow_retention_decrease: bool,
16+
non_interactive_help: str,
1617
input_stream: TextIO,
1718
output_stream: TextIO,
1819
) -> None:
@@ -42,7 +43,7 @@ def enforce_retention_decrease_policy(
4243
if not input_stream.isatty():
4344
raise CliUserError(
4445
"time travel retention decrease requires confirmation",
45-
help="Pass --allow-retention-decrease to confirm in non-interactive runs.",
46+
help=non_interactive_help,
4647
)
4748
expected: str = _confirmation_text(confirmation)
4849
output_stream.write(

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def enforce_table_type_downgrade_policy(
1313
*,
1414
plan: PlanOutput,
1515
allow_table_type_downgrade: bool,
16+
non_interactive_help: str,
1617
input_stream: TextIO,
1718
output_stream: TextIO,
1819
) -> None:
@@ -38,7 +39,7 @@ def enforce_table_type_downgrade_policy(
3839
if not input_stream.isatty():
3940
raise CliUserError(
4041
"table-type downgrade requires confirmation",
41-
help="Pass --allow-table-type-downgrade to confirm in non-interactive runs.",
42+
help=non_interactive_help,
4243
)
4344
expected: str = _confirmation_text(confirmation)
4445
output_stream.write(

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

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@
88

99
from sqlbuild.adapter.contract.classes.base_adapter import BaseAdapter
1010
from sqlbuild.cli.commands._helpers.build_planning.plan_policies import (
11-
enforce_migration_plan_policies,
12-
)
13-
from sqlbuild.cli.commands._helpers.build_planning.retention_decrease import (
14-
enforce_retention_decrease_policy,
11+
dbt_interop_plan_safety_gates,
12+
enforce_plan_safety_policies,
1513
)
1614
from sqlbuild.cli.commands.classes.build_progress_callbacks import (
1715
BuildProgressCallbacks,
1816
format_build_footer,
1917
)
20-
from sqlbuild.cli.commands.constants import MISSING_ORIGIN_DBT_HELP
2118
from sqlbuild.cli.commands.models import DbtSqlbuildWorkContext
2219
from sqlbuild.cli.progress.classes.connection_progress_reporter import ConnectionProgressReporter
2320
from sqlbuild.cli.progress.main._execution_header import format_execution_header
@@ -54,18 +51,13 @@ def execute_sqlbuild_build_work(
5451
adapter_name: str = context.adapter_name
5552
output_stream: TextIO = context.output_stream
5653
use_color: bool = context.use_color
57-
enforce_migration_plan_policies(
58-
plan=plan_output,
59-
allow_missing_migration_origin=False,
60-
non_interactive_help=MISSING_ORIGIN_DBT_HELP,
61-
input_stream=sys.stdin,
62-
output_stream=output_stream,
63-
)
64-
enforce_retention_decrease_policy(
54+
enforce_plan_safety_policies(
6555
plan=plan_output,
66-
allow_retention_decrease=False,
67-
input_stream=sys.stdin,
68-
output_stream=output_stream,
56+
gates=dbt_interop_plan_safety_gates(),
57+
snapshots_config=context.snapshots_config,
58+
target_name=project.effective_target_name,
59+
execution_limits=context.execution_limits,
60+
streams=(sys.stdin, output_stream),
6961
)
7062
callbacks: BuildProgressCallbacks = BuildProgressCallbacks(
7163
plan=plan_output, use_color=use_color, verbose=verbose, debug=False

‎src/sqlbuild/cli/commands/constants.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,30 @@
111111
"models with sqb build --allow-missing-migration-origin and leave them out of this dbt "
112112
"selection, or set the target's missing_migration_origin to 'allow'."
113113
)
114+
SNAPSHOT_FULL_REFRESH_BUILD_HELP: str = (
115+
"Pass --allow-snapshot-full-refresh to confirm in non-interactive runs."
116+
)
117+
SNAPSHOT_FULL_REFRESH_DBT_HELP: str = (
118+
"sqb dbt has no confirmation flag: confirm on an interactive terminal, build the affected "
119+
"snapshots with sqb build --allow-snapshot-full-refresh and leave them out of this dbt "
120+
"selection, or relax the snapshot_full_refresh policy."
121+
)
122+
TABLE_TYPE_DOWNGRADE_BUILD_HELP: str = (
123+
"Pass --allow-table-type-downgrade to confirm in non-interactive runs."
124+
)
125+
TABLE_TYPE_DOWNGRADE_DBT_HELP: str = (
126+
"sqb dbt has no confirmation flag: confirm on an interactive terminal, build the affected "
127+
"models with sqb build --allow-table-type-downgrade and leave them out of this dbt "
128+
"selection, or set the target's table_type_downgrade to 'allow'."
129+
)
130+
RETENTION_DECREASE_BUILD_HELP: str = (
131+
"Pass --allow-retention-decrease to confirm in non-interactive runs."
132+
)
133+
RETENTION_DECREASE_DBT_HELP: str = (
134+
"sqb dbt has no confirmation flag: confirm on an interactive terminal, build the affected "
135+
"models with sqb build --allow-retention-decrease and leave them out of this dbt "
136+
"selection, or set the target's time_travel_retention_decrease to 'allow'."
137+
)
138+
EXECUTION_LIMIT_BUILD_NOTE: str = "No warehouse changes were made."
139+
EXECUTION_LIMIT_DBT_NOTE: str = "dbt work has already run; no SQLBuild models were built."
114140
QUERY_DIFF_INCOMPLETE_EXECUTION_CODES: frozenset[str] = frozenset({"C237", "C238", "C239"})

‎src/sqlbuild/cli/commands/models.py‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@
8383
from sqlbuild.presentation.classes.transient_status_reporter import TransientStatusReporter
8484
from sqlbuild.python_nodes.models import SqlResourceRef
8585
from sqlbuild.runtime.contracts.types import NodeStartCallback
86-
from sqlbuild.spec.contracts.models import CostConfig, ExecutionLimitsConfig, SourceEntry
86+
from sqlbuild.spec.contracts.models import (
87+
CostConfig,
88+
ExecutionLimitsConfig,
89+
SnapshotsConfig,
90+
SourceEntry,
91+
)
8792

8893

8994
@dataclass(frozen=True)
@@ -390,6 +395,23 @@ class DbtSqlbuildWorkContext:
390395
adapter_name: str
391396
output_stream: TextIO
392397
use_color: bool
398+
snapshots_config: SnapshotsConfig
399+
execution_limits: ExecutionLimitsConfig
400+
401+
402+
@dataclass(frozen=True)
403+
class PlanSafetyGates:
404+
"""Pre-confirmed plan safety gates and the guidance shown when one needs a terminal."""
405+
406+
allow_missing_migration_origin: bool
407+
allow_snapshot_full_refresh: bool
408+
allow_table_type_downgrade: bool
409+
allow_retention_decrease: bool
410+
missing_origin_help: str
411+
snapshot_full_refresh_help: str
412+
table_type_downgrade_help: str
413+
retention_decrease_help: str
414+
execution_limit_note: str
393415

394416

395417
@dataclass(frozen=True)

0 commit comments

Comments
 (0)