Skip to content

Commit 4b1593d

Browse files
committed
fix(migrations): keep still-live dev intervals in v0071
The v0071 intervals migration compared a bare dev version string against `used_dev_versions`, which only ever holds `(name, dev_version)` tuples. A string is never an element of a set of 2-tuples, so `dev_version not in used_dev_versions` was always true and the guard collapsed to `if is_dev: continue`. Every dev interval whose version was still live (it had already passed the `used_versions` check) was dropped on upgrade, forcing a re-backfill of dev environments. Compare the correctly-typed `(name, dev_version)` key so only genuinely unused dev intervals are removed. Add a regression test that runs the migration against an in-memory DuckDB state with a still-live dev interval and asserts it survives. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
1 parent 417573f commit 4b1593d

2 files changed

Lines changed: 136 additions & 1 deletion

File tree

sqlmesh/migrations/v0071_add_dev_version_to_intervals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def _migrate_intervals(
106106
continue
107107

108108
dev_version = snapshot_ids_to_dev_versions.get((name, identifier))
109-
if dev_version not in used_dev_versions and is_dev:
109+
if (name, dev_version) not in used_dev_versions and is_dev:
110110
# If the interval's dev version is no longer used and this is a dev interval, we can safely delete it
111111
continue
112112

tests/core/state_sync/test_state_sync.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2937,6 +2937,141 @@ def test_migrate_rows(state_sync: EngineAdapterStateSync, mocker: MockerFixture)
29372937
)
29382938

29392939

2940+
def test_migrate_v0071_keeps_still_live_dev_intervals() -> None:
2941+
from sqlmesh.migrations import v0071_add_dev_version_to_intervals as migration
2942+
2943+
engine_adapter = create_engine_adapter(duckdb.connect, "duckdb")
2944+
engine_adapter.create_schema(c.SQLMESH)
2945+
2946+
engine_adapter.create_table(
2947+
"sqlmesh._snapshots",
2948+
{
2949+
"name": exp.DataType.build("text"),
2950+
"identifier": exp.DataType.build("text"),
2951+
"version": exp.DataType.build("text"),
2952+
"snapshot": exp.DataType.build("text"),
2953+
"kind_name": exp.DataType.build("text"),
2954+
"updated_ts": exp.DataType.build("bigint"),
2955+
"unpaused_ts": exp.DataType.build("bigint"),
2956+
"ttl_ms": exp.DataType.build("bigint"),
2957+
"unrestorable": exp.DataType.build("boolean"),
2958+
},
2959+
)
2960+
# The intervals table at schema version 70, before v0071 adds the dev_version column.
2961+
engine_adapter.create_table(
2962+
"sqlmesh._intervals",
2963+
{
2964+
"id": exp.DataType.build("text"),
2965+
"created_ts": exp.DataType.build("bigint"),
2966+
"name": exp.DataType.build("text"),
2967+
"identifier": exp.DataType.build("text"),
2968+
"version": exp.DataType.build("text"),
2969+
"start_ts": exp.DataType.build("bigint"),
2970+
"end_ts": exp.DataType.build("bigint"),
2971+
"is_dev": exp.DataType.build("boolean"),
2972+
"is_removed": exp.DataType.build("boolean"),
2973+
"is_compacted": exp.DataType.build("boolean"),
2974+
"is_pending_restatement": exp.DataType.build("boolean"),
2975+
},
2976+
)
2977+
2978+
def snapshot_blob(name: str, identifier: str, version: str, dev_version: str) -> str:
2979+
return json.dumps(
2980+
{
2981+
"name": name,
2982+
"identifier": identifier,
2983+
"version": version,
2984+
"dev_version": dev_version,
2985+
"fingerprint": {
2986+
"data_hash": "data",
2987+
"metadata_hash": "metadata",
2988+
"parent_data_hash": "parent_data",
2989+
"parent_metadata_hash": "parent_metadata",
2990+
},
2991+
"previous_versions": [],
2992+
}
2993+
)
2994+
2995+
engine_adapter.insert_append(
2996+
"sqlmesh._snapshots",
2997+
pd.DataFrame(
2998+
[
2999+
{
3000+
"name": '"db"."prod_model"',
3001+
"identifier": "prod_ident",
3002+
"version": "prod_ver",
3003+
"snapshot": snapshot_blob(
3004+
'"db"."prod_model"', "prod_ident", "prod_ver", "prod_dev"
3005+
),
3006+
"kind_name": "FULL",
3007+
"updated_ts": 1,
3008+
"unpaused_ts": None,
3009+
"ttl_ms": None,
3010+
"unrestorable": False,
3011+
},
3012+
{
3013+
"name": '"db"."dev_model"',
3014+
"identifier": "dev_ident",
3015+
"version": "dev_ver",
3016+
"snapshot": snapshot_blob(
3017+
'"db"."dev_model"', "dev_ident", "dev_ver", "dev_dev"
3018+
),
3019+
"kind_name": "FULL",
3020+
"updated_ts": 1,
3021+
"unpaused_ts": None,
3022+
"ttl_ms": None,
3023+
"unrestorable": False,
3024+
},
3025+
]
3026+
),
3027+
)
3028+
3029+
engine_adapter.insert_append(
3030+
"sqlmesh._intervals",
3031+
pd.DataFrame(
3032+
[
3033+
# A non-dev interval that is always kept; it forces the migration to
3034+
# rewrite the intervals table so a wrongly-dropped dev interval is
3035+
# actually deleted rather than left behind by the empty-result guard.
3036+
{
3037+
"id": "prod_int",
3038+
"created_ts": 1,
3039+
"name": '"db"."prod_model"',
3040+
"identifier": "prod_ident",
3041+
"version": "prod_ver",
3042+
"start_ts": 100,
3043+
"end_ts": 200,
3044+
"is_dev": False,
3045+
"is_removed": False,
3046+
"is_compacted": False,
3047+
"is_pending_restatement": False,
3048+
},
3049+
# A dev interval whose version and dev version are both still live.
3050+
# It must be preserved by the migration.
3051+
{
3052+
"id": "dev_int",
3053+
"created_ts": 1,
3054+
"name": '"db"."dev_model"',
3055+
"identifier": "dev_ident",
3056+
"version": "dev_ver",
3057+
"start_ts": 100,
3058+
"end_ts": 200,
3059+
"is_dev": True,
3060+
"is_removed": False,
3061+
"is_compacted": False,
3062+
"is_pending_restatement": False,
3063+
},
3064+
]
3065+
),
3066+
)
3067+
3068+
migration.migrate_schemas(engine_adapter, c.SQLMESH)
3069+
migration.migrate_rows(engine_adapter, c.SQLMESH)
3070+
3071+
surviving_ids = {row[0] for row in engine_adapter.fetchall("SELECT id FROM sqlmesh._intervals")}
3072+
assert surviving_ids == {"prod_int", "dev_int"}
3073+
3074+
29403075
def test_backup_state(state_sync: EngineAdapterStateSync, mocker: MockerFixture) -> None:
29413076
state_sync.engine_adapter.replace_query(
29423077
"sqlmesh._snapshots",

0 commit comments

Comments
 (0)