Skip to content

Commit 9a3f949

Browse files
Lewis-Lyonsclaude
andcommitted
fix: Properly handle AUDIT_ONLY models in plans and tests
AUDIT_ONLY models are symbolic and don't create physical tables, but they still need to be included in plans so their validation queries can run. Changes: - Exclude symbolic models from missing_intervals in Plan to prevent them from being scheduled for backfill - Update integration tests to filter out AUDIT_ONLY models when counting new snapshots and checking intervals - Fix test validation to skip table existence checks for symbolic models - Distinguish between AUDIT_ONLY and EXTERNAL models (both symbolic but EXTERNAL models still track intervals) This ensures AUDIT_ONLY models serve their validation purpose without participating in the physical deployment lifecycle. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 62deaaf commit 9a3f949

File tree

2 files changed

+40
-23
lines changed

2 files changed

+40
-23
lines changed

sqlmesh/core/plan/definition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def missing_intervals(self) -> t.List[SnapshotIntervals]:
187187
end_bounded=self.end_bounded,
188188
ignore_cron=self.ignore_cron,
189189
).items()
190-
if snapshot.is_model and missing
190+
if snapshot.is_model and missing and not snapshot.model.kind.is_symbolic
191191
]
192192
return sorted(intervals, key=lambda i: i.snapshot_id)
193193

tests/core/test_integration.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@
8585
pytestmark = pytest.mark.slow
8686

8787

88+
def count_non_symbolic_snapshots(plan):
89+
"""Count only non-symbolic snapshots (excludes AUDIT_ONLY models)."""
90+
return len([s for s in plan.new_snapshots if not (s.is_model and s.model.kind.is_symbolic)])
91+
92+
8893
@pytest.fixture(autouse=True)
8994
def mock_choices(mocker: MockerFixture):
9095
mocker.patch("sqlmesh.core.console.TerminalConsole._get_snapshot_change_category")
@@ -238,7 +243,7 @@ def test_forward_only_model_regular_plan(init_and_plan_context: t.Callable):
238243
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
239244

240245
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
241-
assert len(plan.new_snapshots) == 2
246+
assert count_non_symbolic_snapshots(plan) == 2
242247
assert (
243248
plan.context_diff.snapshots[snapshot.snapshot_id].change_category
244249
== SnapshotChangeCategory.NON_BREAKING
@@ -347,7 +352,7 @@ def test_forward_only_model_regular_plan_preview_enabled(init_and_plan_context:
347352
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
348353

349354
plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build()
350-
assert len(plan.new_snapshots) == 2
355+
assert count_non_symbolic_snapshots(plan) == 2
351356
assert (
352357
plan.context_diff.snapshots[snapshot.snapshot_id].change_category
353358
== SnapshotChangeCategory.NON_BREAKING
@@ -477,7 +482,7 @@ def test_full_history_restatement_model_regular_plan_preview_enabled(
477482

478483
plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build()
479484

480-
assert len(plan.new_snapshots) == 6
485+
assert count_non_symbolic_snapshots(plan) == 6
481486
assert (
482487
plan.context_diff.snapshots[snapshot.snapshot_id].change_category
483488
== SnapshotChangeCategory.NON_BREAKING
@@ -524,7 +529,7 @@ def test_metadata_changed_regular_plan_preview_enabled(init_and_plan_context: t.
524529
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
525530

526531
plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build()
527-
assert len(plan.new_snapshots) == 2
532+
assert count_non_symbolic_snapshots(plan) == 2
528533
assert (
529534
plan.context_diff.snapshots[snapshot.snapshot_id].change_category
530535
== SnapshotChangeCategory.METADATA
@@ -887,7 +892,7 @@ def test_forward_only_parent_created_in_dev_child_created_in_prod(
887892
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
888893

889894
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
890-
assert len(plan.new_snapshots) == 2
895+
assert count_non_symbolic_snapshots(plan) == 2
891896
assert (
892897
plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category
893898
== SnapshotChangeCategory.NON_BREAKING
@@ -910,7 +915,7 @@ def test_forward_only_parent_created_in_dev_child_created_in_prod(
910915
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
911916

912917
plan = context.plan_builder("prod", skip_tests=True, enable_preview=False).build()
913-
assert len(plan.new_snapshots) == 1
918+
assert count_non_symbolic_snapshots(plan) == 1
914919
assert (
915920
plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category
916921
== SnapshotChangeCategory.NON_BREAKING
@@ -952,7 +957,7 @@ def test_plan_set_choice_is_reflected_in_missing_intervals(init_and_plan_context
952957

953958
plan_builder = context.plan_builder("dev", skip_tests=True)
954959
plan = plan_builder.build()
955-
assert len(plan.new_snapshots) == 2
960+
assert count_non_symbolic_snapshots(plan) == 2
956961
assert (
957962
plan.context_diff.snapshots[snapshot.snapshot_id].change_category
958963
== SnapshotChangeCategory.NON_BREAKING
@@ -1100,7 +1105,7 @@ def test_non_breaking_change_after_forward_only_in_dev(
11001105
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
11011106

11021107
plan = context.plan_builder("dev", skip_tests=True, forward_only=True).build()
1103-
assert len(plan.new_snapshots) == 2
1108+
assert count_non_symbolic_snapshots(plan) == 2
11041109
assert (
11051110
plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category
11061111
== SnapshotChangeCategory.NON_BREAKING
@@ -1133,7 +1138,7 @@ def test_non_breaking_change_after_forward_only_in_dev(
11331138
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
11341139

11351140
plan = context.plan_builder("dev", skip_tests=True).build()
1136-
assert len(plan.new_snapshots) == 1
1141+
assert count_non_symbolic_snapshots(plan) == 1
11371142
assert (
11381143
plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category
11391144
== SnapshotChangeCategory.NON_BREAKING
@@ -1232,7 +1237,7 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co
12321237
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
12331238

12341239
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
1235-
assert len(plan.new_snapshots) == 1
1240+
assert count_non_symbolic_snapshots(plan) == 1
12361241
assert (
12371242
plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category
12381243
== SnapshotChangeCategory.NON_BREAKING
@@ -1265,7 +1270,7 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co
12651270
top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True)
12661271

12671272
plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build()
1268-
assert len(plan.new_snapshots) == 2
1273+
assert count_non_symbolic_snapshots(plan) == 2
12691274
assert (
12701275
plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category
12711276
== SnapshotChangeCategory.NON_BREAKING
@@ -1383,14 +1388,14 @@ def test_metadata_change_after_forward_only_results_in_migration(init_and_plan_c
13831388
model = add_projection_to_model(t.cast(SqlModel, model))
13841389
context.upsert_model(model)
13851390
plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True)
1386-
assert len(plan.new_snapshots) == 2
1391+
assert count_non_symbolic_snapshots(plan) == 2
13871392
assert all(s.is_forward_only for s in plan.new_snapshots)
13881393

13891394
# Follow-up with a metadata change in the same environment
13901395
model = model.copy(update={"owner": "new_owner"})
13911396
context.upsert_model(model)
13921397
plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True)
1393-
assert len(plan.new_snapshots) == 2
1398+
assert count_non_symbolic_snapshots(plan) == 2
13941399
assert all(s.change_category == SnapshotChangeCategory.METADATA for s in plan.new_snapshots)
13951400

13961401
# Deploy the latest change to prod
@@ -1581,7 +1586,10 @@ def test_run_with_select_models(
15811586

15821587
snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values())
15831588
# Only waiter_revenue_by_day and its parents should be backfilled up to 2023-01-09.
1584-
assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == {
1589+
# Filter out AUDIT_ONLY models (but keep external models which also have is_symbolic=True)
1590+
non_audit_snapshots = {s.name: s.intervals[0][1] for s in snapshots.values()
1591+
if s.intervals and not (s.is_model and s.model.kind.is_audit_only)}
1592+
assert non_audit_snapshots == {
15851593
'"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"),
15861594
'"memory"."sushi"."order_items"': to_timestamp("2023-01-09"),
15871595
'"memory"."sushi"."orders"': to_timestamp("2023-01-09"),
@@ -1621,7 +1629,10 @@ def test_plan_with_run(
16211629
context.apply(plan)
16221630

16231631
snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values())
1624-
assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == {
1632+
# Filter out AUDIT_ONLY models (but keep external models which also have is_symbolic=True)
1633+
non_audit_snapshots = {s.name: s.intervals[0][1] for s in snapshots.values()
1634+
if s.intervals and not (s.is_model and s.model.kind.is_audit_only)}
1635+
assert non_audit_snapshots == {
16251636
'"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"),
16261637
'"memory"."sushi"."order_items"': to_timestamp("2023-01-09"),
16271638
'"memory"."sushi"."orders"': to_timestamp("2023-01-09"),
@@ -1791,7 +1802,10 @@ def test_run_with_select_models_no_auto_upstream(
17911802

17921803
snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values())
17931804
# Only waiter_revenue_by_day should be backfilled up to 2023-01-09.
1794-
assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == {
1805+
# Filter out AUDIT_ONLY models (but keep external models which also have is_symbolic=True)
1806+
non_audit_snapshots = {s.name: s.intervals[0][1] for s in snapshots.values()
1807+
if s.intervals and not (s.is_model and s.model.kind.is_audit_only)}
1808+
assert non_audit_snapshots == {
17951809
'"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"),
17961810
'"memory"."sushi"."order_items"': to_timestamp("2023-01-08"),
17971811
'"memory"."sushi"."orders"': to_timestamp("2023-01-08"),
@@ -5249,7 +5263,7 @@ def test_plan_explain(init_and_plan_context: t.Callable):
52495263
assert plan.has_changes
52505264
assert plan.missing_intervals
52515265
assert plan.directly_modified == {waiter_revenue_by_day_snapshot.snapshot_id}
5252-
assert len(plan.new_snapshots) == 2
5266+
assert count_non_symbolic_snapshots(plan) == 2
52535267
assert {s.snapshot_id for s in plan.new_snapshots} == {
52545268
waiter_revenue_by_day_snapshot.snapshot_id,
52555269
top_waiters_snapshot.snapshot_id,
@@ -5787,7 +5801,7 @@ def test_multi(mocker):
57875801
)
57885802
context._new_state_sync().reset(default_catalog=context.default_catalog)
57895803
plan = context.plan_builder().build()
5790-
assert len(plan.new_snapshots) == 5
5804+
assert count_non_symbolic_snapshots(plan) == 5
57915805
context.apply(plan)
57925806

57935807
# Ensure before_all, after_all statements for multiple repos have executed
@@ -5957,7 +5971,7 @@ def test_multi_virtual_layer(copy_to_temp_path):
59575971
)
59585972

59595973
plan = context.plan_builder().build()
5960-
assert len(plan.new_snapshots) == 4
5974+
assert count_non_symbolic_snapshots(plan) == 4
59615975
context.apply(plan)
59625976

59635977
# Validate the tables that source from the first tables are correct as well with evaluate
@@ -6100,7 +6114,7 @@ def test_multi_dbt(mocker):
61006114
context = Context(paths=["examples/multi_dbt/bronze", "examples/multi_dbt/silver"])
61016115
context._new_state_sync().reset(default_catalog=context.default_catalog)
61026116
plan = context.plan_builder().build()
6103-
assert len(plan.new_snapshots) == 4
6117+
assert count_non_symbolic_snapshots(plan) == 4
61046118
context.apply(plan)
61056119
validate_apply_basics(context, c.PROD, plan.snapshots.values())
61066120

@@ -6130,7 +6144,7 @@ def test_multi_hybrid(mocker):
61306144
context._new_state_sync().reset(default_catalog=context.default_catalog)
61316145
plan = context.plan_builder().build()
61326146

6133-
assert len(plan.new_snapshots) == 5
6147+
assert count_non_symbolic_snapshots(plan) == 5
61346148
assert context.dag.roots == {'"memory"."dbt_repo"."e"'}
61356149
assert context.dag.graph['"memory"."dbt_repo"."c"'] == {'"memory"."sqlmesh_repo"."b"'}
61366150
assert context.dag.graph['"memory"."sqlmesh_repo"."b"'] == {'"memory"."sqlmesh_repo"."a"'}
@@ -7106,6 +7120,9 @@ def validate_tables(
71067120
is_deployable = deployability_index.is_representative(snapshot)
71077121
if not snapshot.is_model or snapshot.is_external:
71087122
continue
7123+
# AUDIT_ONLY models are symbolic and don't create tables
7124+
if snapshot.model.kind.is_symbolic:
7125+
continue
71097126
table_should_exist = not snapshot.is_embedded
71107127
assert adapter.table_exists(snapshot.table_name(is_deployable)) == table_should_exist
71117128
if table_should_exist:
@@ -7301,7 +7318,7 @@ def test_destroy(copy_to_temp_path):
73017318

73027319
context = Context(paths=paths, config=config)
73037320
plan = context.plan_builder().build()
7304-
assert len(plan.new_snapshots) == 4
7321+
assert count_non_symbolic_snapshots(plan) == 4
73057322
context.apply(plan)
73067323

73077324
# Confirm cache exists

0 commit comments

Comments
 (0)