Skip to content

Commit c6a0be9

Browse files
Fix: Changes to before_all/after_all alone now trigger virtual updates (#4788)
1 parent bd2caa9 commit c6a0be9

3 files changed

Lines changed: 174 additions & 2 deletions

File tree

sqlmesh/core/plan/builder.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,7 @@ def _ensure_new_env_with_changes(self) -> None:
835835
and not self._include_unmodified
836836
and self._context_diff.is_new_environment
837837
and not self._context_diff.has_snapshot_changes
838+
and not self._context_diff.has_environment_statements_changes
838839
and not self._backfill_models
839840
):
840841
raise NoChangesPlanError(

tests/core/test_context.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@
5252
to_timestamp,
5353
yesterday_ds,
5454
)
55-
from sqlmesh.utils.errors import ConfigError, SQLMeshError, LinterError, PlanError
55+
from sqlmesh.utils.errors import (
56+
ConfigError,
57+
SQLMeshError,
58+
LinterError,
59+
PlanError,
60+
NoChangesPlanError,
61+
)
5662
from sqlmesh.utils.metaprogramming import Executable
5763
from tests.utils.test_helpers import use_terminal_console
5864
from tests.utils.test_filesystem import create_temp_file
@@ -2218,3 +2224,83 @@ def test_plan_explain_skips_tests(sushi_context: Context, mocker: MockerFixture)
22182224
spy = mocker.spy(sushi_context, "_run_plan_tests")
22192225
sushi_context.plan(environment="dev", explain=True, no_prompts=True, include_unmodified=True)
22202226
spy.assert_called_once_with(skip_tests=True)
2227+
2228+
2229+
def test_dev_environment_virtual_update_with_environment_statements(tmp_path: Path) -> None:
2230+
models_dir = tmp_path / "models"
2231+
models_dir.mkdir()
2232+
model_sql = """
2233+
MODEL (
2234+
name db.test_model,
2235+
kind FULL
2236+
);
2237+
2238+
SELECT 1 as id, 'test' as name
2239+
"""
2240+
2241+
with open(models_dir / "test_model.sql", "w") as f:
2242+
f.write(model_sql)
2243+
2244+
# Create initial context without environment statements
2245+
config = Config(
2246+
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
2247+
gateways={"duckdb": GatewayConfig(connection=DuckDBConnectionConfig())},
2248+
)
2249+
2250+
context = Context(paths=tmp_path, config=config)
2251+
2252+
# First, apply to production
2253+
context.plan("prod", auto_apply=True, no_prompts=True)
2254+
2255+
# Try to create dev environment without changes (should fail)
2256+
with pytest.raises(NoChangesPlanError, match="Creating a new environment requires a change"):
2257+
context.plan("dev", auto_apply=True, no_prompts=True)
2258+
2259+
# Now create a new context with only new environment statements
2260+
config_with_statements = Config(
2261+
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
2262+
gateways={"duckdb": GatewayConfig(connection=DuckDBConnectionConfig())},
2263+
before_all=["CREATE TABLE IF NOT EXISTS audit_log (id INT, action VARCHAR(100))"],
2264+
after_all=["INSERT INTO audit_log VALUES (1, 'environment_created')"],
2265+
)
2266+
2267+
context_with_statements = Context(paths=tmp_path, config=config_with_statements)
2268+
2269+
# This should succeed because environment statements are different
2270+
context_with_statements.plan("dev", auto_apply=True, no_prompts=True)
2271+
env = context_with_statements.state_reader.get_environment("dev")
2272+
assert env is not None
2273+
assert env.name == "dev"
2274+
2275+
# Verify the environment statements were stored
2276+
stored_statements = context_with_statements.state_reader.get_environment_statements("dev")
2277+
assert len(stored_statements) == 1
2278+
assert stored_statements[0].before_all == [
2279+
"CREATE TABLE IF NOT EXISTS audit_log (id INT, action VARCHAR(100))"
2280+
]
2281+
assert stored_statements[0].after_all == [
2282+
"INSERT INTO audit_log VALUES (1, 'environment_created')"
2283+
]
2284+
2285+
# Update environment statements and plan again (should trigger another virtual update)
2286+
config_updated_statements = Config(
2287+
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
2288+
gateways={"duckdb": GatewayConfig(connection=DuckDBConnectionConfig())},
2289+
before_all=[
2290+
"CREATE TABLE IF NOT EXISTS audit_log (id INT, action VARCHAR(100))",
2291+
"CREATE TABLE IF NOT EXISTS metrics (metric_name VARCHAR(50), value INT)",
2292+
],
2293+
after_all=["INSERT INTO audit_log VALUES (1, 'environment_created')"],
2294+
)
2295+
2296+
context_updated = Context(paths=tmp_path, config=config_updated_statements)
2297+
context_updated.plan("dev", auto_apply=True, no_prompts=True)
2298+
2299+
# Verify the updated statements were stored
2300+
updated_statements = context_updated.state_reader.get_environment_statements("dev")
2301+
assert len(updated_statements) == 1
2302+
assert len(updated_statements[0].before_all) == 2
2303+
assert (
2304+
updated_statements[0].before_all[1]
2305+
== "CREATE TABLE IF NOT EXISTS metrics (metric_name VARCHAR(50), value INT)"
2306+
)

tests/core/test_plan.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
to_timestamp,
4242
yesterday_ds,
4343
)
44-
from sqlmesh.utils.errors import PlanError
44+
from sqlmesh.utils.errors import PlanError, NoChangesPlanError
4545
from sqlmesh.utils.rich import strip_ansi_codes
4646

4747

@@ -3204,3 +3204,88 @@ def _build_plan() -> Plan:
32043204
assert to_datetime(plan.start) == to_datetime(output_start)
32053205
assert to_datetime(plan.end) == to_datetime(output_end)
32063206
assert to_datetime(plan.execution_time) == to_datetime(output_execution_time)
3207+
3208+
3209+
def test_environment_statements_change_allows_dev_environment_creation(make_snapshot):
3210+
snapshot = make_snapshot(
3211+
SqlModel(
3212+
name="test_model",
3213+
dialect="duckdb",
3214+
query=parse_one("select 1, ds"),
3215+
kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"),
3216+
)
3217+
)
3218+
3219+
# First context diff of a new 'dev' environment without environment statements
3220+
context_diff_no_statements = ContextDiff(
3221+
environment="dev",
3222+
is_new_environment=True,
3223+
is_unfinalized_environment=False,
3224+
normalize_environment_name=True,
3225+
create_from="prod",
3226+
create_from_env_exists=True,
3227+
added=set(),
3228+
removed_snapshots={},
3229+
modified_snapshots={},
3230+
snapshots={snapshot.snapshot_id: snapshot},
3231+
new_snapshots={},
3232+
previous_plan_id=None,
3233+
previously_promoted_snapshot_ids={snapshot.snapshot_id},
3234+
previous_finalized_snapshots=None,
3235+
previous_gateway_managed_virtual_layer=False,
3236+
gateway_managed_virtual_layer=False,
3237+
environment_statements=[],
3238+
previous_environment_statements=[],
3239+
)
3240+
3241+
# Should fail because no changes
3242+
plan_builder = PlanBuilder(
3243+
context_diff_no_statements,
3244+
is_dev=True,
3245+
)
3246+
3247+
with pytest.raises(NoChangesPlanError, match="Creating a new environment requires a change"):
3248+
plan_builder.build()
3249+
3250+
# Now create context diff with environment statements
3251+
environment_statements = [
3252+
EnvironmentStatements(
3253+
before_all=["CREATE TABLE IF NOT EXISTS test_table (id INT)"],
3254+
after_all=[],
3255+
python_env={},
3256+
jinja_macros=None,
3257+
)
3258+
]
3259+
3260+
context_diff_with_statements = ContextDiff(
3261+
environment="dev",
3262+
is_new_environment=True,
3263+
is_unfinalized_environment=False,
3264+
normalize_environment_name=True,
3265+
create_from="prod",
3266+
create_from_env_exists=True,
3267+
added=set(),
3268+
removed_snapshots={},
3269+
modified_snapshots={},
3270+
snapshots={snapshot.snapshot_id: snapshot},
3271+
new_snapshots={},
3272+
previous_plan_id=None,
3273+
previously_promoted_snapshot_ids={snapshot.snapshot_id},
3274+
previous_finalized_snapshots=None,
3275+
previous_gateway_managed_virtual_layer=False,
3276+
gateway_managed_virtual_layer=False,
3277+
environment_statements=environment_statements,
3278+
previous_environment_statements=[],
3279+
)
3280+
3281+
# Should succeed because there are environment statements changes
3282+
plan_builder_with_statements = PlanBuilder(
3283+
context_diff_with_statements,
3284+
is_dev=True,
3285+
)
3286+
3287+
# Test that allows creating a dev environment without other changes
3288+
plan = plan_builder_with_statements.build()
3289+
assert plan is not None
3290+
assert plan.context_diff.has_environment_statements_changes
3291+
assert plan.context_diff.environment_statements == environment_statements

0 commit comments

Comments
 (0)