Skip to content

Commit 40e26aa

Browse files
committed
fix(plan): add explainer visit method for PhysicalLayerSchemaCreationStage
`RichExplainerConsole` had no `visit_physical_layer_schema_creation_stage`, so explaining a plan that includes `PhysicalLayerSchemaCreationStage` logged `ERROR - Unexpected stage: PhysicalLayerSchemaCreationStage` and silently omitted the stage from the explained plan. Add the missing visit method. It mirrors the schema derivation in `SnapshotEvaluator.create_physical_schemas` (non-symbolic models only, table name resolved through the deployability index) and lists the distinct physical schemas that will be created. `test_plan_explain` now asserts that no stage is reported as unexpected, which covers this stage and guards against the same omission for future ones. Fixes #5619 Signed-off-by: nkwork9999 <143652584+nkwork9999@users.noreply.github.com>
1 parent 61082a6 commit 40e26aa

2 files changed

Lines changed: 38 additions & 8 deletions

File tree

sqlmesh/core/plan/explainer.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
from rich.console import Console as RichConsole
1010
from rich.tree import Tree
11+
from sqlglot import exp
1112
from sqlglot.dialects.dialect import DialectType
1213
from sqlmesh.core import constants as c
1314
from sqlmesh.core.console import Console, TerminalConsole, get_console
15+
from sqlmesh.core.dialect import schema_
1416
from sqlmesh.core.environment import EnvironmentNamingInfo
1517
from sqlmesh.core.plan.common import (
1618
SnapshotIntervalClearRequest,
@@ -145,6 +147,29 @@ def visit_before_all_stage(self, stage: stages.BeforeAllStage) -> Tree:
145147
def visit_after_all_stage(self, stage: stages.AfterAllStage) -> Tree:
146148
return Tree("[bold]Execute after all statements[/bold]")
147149

150+
def visit_physical_layer_schema_creation_stage(
151+
self, stage: stages.PhysicalLayerSchemaCreationStage
152+
) -> t.Optional[Tree]:
153+
tables = [
154+
exp.to_table(
155+
snapshot.table_name(is_deployable=stage.deployability_index.is_deployable(snapshot))
156+
)
157+
for snapshot in stage.snapshots
158+
if snapshot.is_model and not snapshot.is_symbolic
159+
]
160+
schemas = {
161+
schema_(table.args["db"], table.args.get("catalog")).sql(dialect=self.dialect)
162+
for table in tables
163+
if table.db
164+
}
165+
if not schemas:
166+
return None
167+
168+
tree = Tree("[bold]Create physical schemas if they do not exist[/bold]")
169+
for schema in sorted(schemas):
170+
tree.add(schema)
171+
return tree
172+
148173
def visit_physical_layer_update_stage(self, stage: stages.PhysicalLayerUpdateStage) -> Tree:
149174
snapshots = [
150175
s for s in stage.snapshots if s.snapshot_id in stage.snapshots_with_missing_intervals

tests/core/integration/test_plan_options.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import logging
34
import typing as t
45
import pytest
56
from sqlmesh.core.console import (
@@ -112,7 +113,7 @@ def test_empty_backfill_new_model(init_and_plan_context: t.Callable):
112113

113114

114115
@time_machine.travel("2023-01-08 15:00:00 UTC")
115-
def test_plan_explain(init_and_plan_context: t.Callable):
116+
def test_plan_explain(init_and_plan_context: t.Callable, caplog):
116117
old_console = get_console()
117118
set_console(TerminalConsole())
118119

@@ -131,16 +132,20 @@ def test_plan_explain(init_and_plan_context: t.Callable):
131132
common_kwargs = dict(skip_tests=True, no_prompts=True, explain=True)
132133

133134
# For now just making sure the plan doesn't error
134-
context.plan("dev", **common_kwargs)
135-
context.plan("dev", **common_kwargs, skip_backfill=True)
136-
context.plan("dev", **common_kwargs, empty_backfill=True)
137-
context.plan("dev", **common_kwargs, forward_only=True, enable_preview=True)
138-
context.plan("prod", **common_kwargs)
139-
context.plan("prod", **common_kwargs, forward_only=True)
140-
context.plan("prod", **common_kwargs, restate_models=[waiter_revenue_by_day_model.name])
135+
with caplog.at_level(logging.ERROR, logger="sqlmesh.core.plan.explainer"):
136+
context.plan("dev", **common_kwargs)
137+
context.plan("dev", **common_kwargs, skip_backfill=True)
138+
context.plan("dev", **common_kwargs, empty_backfill=True)
139+
context.plan("dev", **common_kwargs, forward_only=True, enable_preview=True)
140+
context.plan("prod", **common_kwargs)
141+
context.plan("prod", **common_kwargs, forward_only=True)
142+
context.plan("prod", **common_kwargs, restate_models=[waiter_revenue_by_day_model.name])
141143

142144
set_console(old_console)
143145

146+
# Every stage produced by the plan must have a corresponding visit method on the explainer
147+
assert "Unexpected stage" not in caplog.text
148+
144149
# Make sure that the now changes were actually applied
145150
for target_env in ("dev", "prod"):
146151
plan = context.plan_builder(target_env, skip_tests=True).build()

0 commit comments

Comments
 (0)