Skip to content

Commit afb4489

Browse files
authored
fix!: allow restatement of external models (#3901)
1 parent 8300fad commit afb4489

4 files changed

Lines changed: 89 additions & 4 deletions

File tree

sqlmesh/core/model/kind.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,6 @@ def supports_python_models(self) -> bool:
813813

814814
class EmbeddedKind(_ModelKind):
815815
name: t.Literal[ModelKindName.EMBEDDED] = ModelKindName.EMBEDDED
816-
disable_restatement: t.Literal[True] = True
817816

818817
@property
819818
def supports_python_models(self) -> bool:
@@ -822,7 +821,6 @@ def supports_python_models(self) -> bool:
822821

823822
class ExternalKind(_ModelKind):
824823
name: t.Literal[ModelKindName.EXTERNAL] = ModelKindName.EXTERNAL
825-
disable_restatement: t.Literal[True] = True
826824

827825

828826
class CustomKind(_ModelKind):

sqlmesh/core/plan/builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def _build_restatements(
349349
"If you want to restate this model, change the model's `disable_restatement` setting to `false`."
350350
)
351351
continue
352-
elif snapshot.is_symbolic or snapshot.is_seed:
352+
elif snapshot.is_seed:
353353
logger.info("Skipping restatement for model '%s'", snapshot.name)
354354
continue
355355

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Remove disable restatement from external and embedded models."""
2+
3+
import json
4+
import pandas as pd
5+
6+
from sqlglot import exp
7+
from sqlmesh.utils.migration import index_text_type, blob_text_type
8+
9+
10+
def migrate(state_sync, **kwargs): # type: ignore
11+
engine_adapter = state_sync.engine_adapter
12+
schema = state_sync.schema
13+
snapshots_table = "_snapshots"
14+
if schema:
15+
snapshots_table = f"{schema}.{snapshots_table}"
16+
17+
index_type = index_text_type(engine_adapter.dialect)
18+
blob_type = blob_text_type(engine_adapter.dialect)
19+
snapshots_columns_to_types = {
20+
"name": exp.DataType.build(index_type),
21+
"identifier": exp.DataType.build(index_type),
22+
"version": exp.DataType.build(index_type),
23+
"snapshot": exp.DataType.build(blob_type),
24+
"kind_name": exp.DataType.build(index_type),
25+
"updated_ts": exp.DataType.build("bigint"),
26+
"unpaused_ts": exp.DataType.build("bigint"),
27+
"ttl_ms": exp.DataType.build("bigint"),
28+
"unrestorable": exp.DataType.build("boolean"),
29+
}
30+
31+
new_snapshots = []
32+
for (
33+
name,
34+
identifier,
35+
version,
36+
snapshot,
37+
kind_name,
38+
updated_ts,
39+
unpaused_ts,
40+
ttl_ms,
41+
unrestorable,
42+
) in engine_adapter.fetchall(
43+
exp.select(*snapshots_columns_to_types).from_(snapshots_table),
44+
quote_identifiers=True,
45+
):
46+
parsed_snapshot = json.loads(snapshot)
47+
kind = parsed_snapshot["node"].get("kind")
48+
49+
if kind and kind_name in ("EMBEDDED", "EXTERNAL"):
50+
kind.pop("disable_restatement", None)
51+
52+
new_snapshots.append(
53+
{
54+
"name": name,
55+
"identifier": identifier,
56+
"version": version,
57+
"snapshot": json.dumps(parsed_snapshot),
58+
"kind_name": kind_name,
59+
"updated_ts": updated_ts,
60+
"unpaused_ts": unpaused_ts,
61+
"ttl_ms": ttl_ms,
62+
"unrestorable": unrestorable,
63+
}
64+
)
65+
66+
if new_snapshots:
67+
engine_adapter.delete_from(snapshots_table, "TRUE")
68+
engine_adapter.insert_append(
69+
snapshots_table,
70+
pd.DataFrame(new_snapshots),
71+
columns_to_types=snapshots_columns_to_types,
72+
)

tests/core/test_plan.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,21 @@ def test_restate_models(sushi_context_pre_scheduling: Context):
789789
assert not plan.restatements
790790
assert plan.models_to_backfill is None
791791

792+
plan = sushi_context_pre_scheduling.plan(restate_models=["raw.demographics"], no_prompts=True)
793+
assert not plan.has_changes
794+
assert plan.restatements
795+
assert plan.models_to_backfill == {
796+
'"memory"."raw"."demographics"',
797+
'"memory"."sushi"."active_customers"',
798+
'"memory"."sushi"."customers"',
799+
'"memory"."sushi"."marketing"',
800+
'"memory"."sushi"."orders"',
801+
'"memory"."sushi"."raw_marketing"',
802+
'"memory"."sushi"."waiter_as_customer_by_day"',
803+
'"memory"."sushi"."waiter_names"',
804+
'"memory"."sushi"."waiters"',
805+
}
806+
792807

793808
@pytest.mark.slow
794809
@time_machine.travel(now(minute_floor=False), tick=False)
@@ -884,7 +899,7 @@ def test_restate_symbolic_model(make_snapshot, mocker: MockerFixture):
884899
plan = PlanBuilder(
885900
context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, restate_models=[snapshot_a.name]
886901
).build()
887-
assert not plan.restatements
902+
assert plan.restatements
888903

889904

890905
def test_restate_seed_model(make_snapshot, mocker: MockerFixture):

0 commit comments

Comments
 (0)