Re-normalize python_env payloads using ast.unparse after dropping astor.
SQLMesh previously used the third-party astor library to serialise Python
function source code (normalize_source). That library has been replaced with
the stdlib ast.unparse, which produces subtly different text for the same
AST (e.g. lambda : x → lambda: x, condensed multi-line signatures, etc.).
Because python_env payloads are included in each snapshot's data_hash,
any model that contains Python code (Python models, SQL models with Python
macros/signals) would otherwise appear as Directly Modified after the upgrade,
potentially triggering a full backfill.
This migration re-normalises every stored Executable payload of
kind == "definition" via ast.unparse(ast.parse(payload)). The
subsequent _migrate_rows pass then recomputes fingerprints from the updated
payloads so that they match what the current code produces when loading models
from disk. The migrated snapshots are flagged migrated = True, so no
unexpected backfills are scheduled.
1"""Re-normalize python_env payloads using ast.unparse after dropping astor. 2 3SQLMesh previously used the third-party `astor` library to serialise Python 4function source code (`normalize_source`). That library has been replaced with 5the stdlib `ast.unparse`, which produces subtly different text for the same 6AST (e.g. `lambda : x` → `lambda: x`, condensed multi-line signatures, etc.). 7 8Because `python_env` payloads are included in each snapshot's `data_hash`, 9any model that contains Python code (Python models, SQL models with Python 10macros/signals) would otherwise appear as *Directly Modified* after the upgrade, 11potentially triggering a full backfill. 12 13This migration re-normalises every stored `Executable` payload of 14`kind == "definition"` via `ast.unparse(ast.parse(payload))`. The 15subsequent `_migrate_rows` pass then recomputes fingerprints from the updated 16payloads so that they match what the current code produces when loading models 17from disk. The migrated snapshots are flagged `migrated = True`, so no 18unexpected backfills are scheduled. 19""" 20 21import ast 22import json 23 24from sqlglot import exp 25 26from sqlmesh.utils.migration import index_text_type, blob_text_type 27 28 29def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore 30 pass 31 32 33def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore 34 import pandas as pd 35 36 snapshots_table = "_snapshots" 37 if schema: 38 snapshots_table = f"{schema}.{snapshots_table}" 39 40 index_type = index_text_type(engine_adapter.dialect) 41 blob_type = blob_text_type(engine_adapter.dialect) 42 43 new_snapshots = [] 44 migration_needed = False 45 46 for ( 47 name, 48 identifier, 49 version, 50 snapshot, 51 kind_name, 52 updated_ts, 53 unpaused_ts, 54 ttl_ms, 55 unrestorable, 56 forward_only, 57 dev_version, 58 fingerprint, 59 ) in engine_adapter.fetchall( 60 exp.select( 61 "name", 62 "identifier", 63 "version", 64 "snapshot", 65 "kind_name", 66 "updated_ts", 67 "unpaused_ts", 68 "ttl_ms", 69 "unrestorable", 70 "forward_only", 71 "dev_version", 72 "fingerprint", 73 ).from_(snapshots_table), 74 quote_identifiers=True, 75 ): 76 parsed_snapshot = json.loads(snapshot) 77 python_env = parsed_snapshot["node"].get("python_env") or {} 78 for executable in python_env.values(): 79 if executable.get("kind") != "definition": 80 continue 81 new_payload = ast.unparse(ast.parse(executable["payload"])).strip() 82 if new_payload != executable["payload"]: 83 executable["payload"] = new_payload 84 migration_needed = True 85 86 new_snapshots.append( 87 { 88 "name": name, 89 "identifier": identifier, 90 "version": version, 91 "snapshot": json.dumps(parsed_snapshot), 92 "kind_name": kind_name, 93 "updated_ts": updated_ts, 94 "unpaused_ts": unpaused_ts, 95 "ttl_ms": ttl_ms, 96 "unrestorable": unrestorable, 97 "forward_only": forward_only, 98 "dev_version": dev_version, 99 "fingerprint": fingerprint, 100 } 101 ) 102 103 if migration_needed and new_snapshots: 104 engine_adapter.delete_from(snapshots_table, "TRUE") 105 106 engine_adapter.insert_append( 107 snapshots_table, 108 pd.DataFrame(new_snapshots), 109 target_columns_to_types={ 110 "name": exp.DataType.build(index_type), 111 "identifier": exp.DataType.build(index_type), 112 "version": exp.DataType.build(index_type), 113 "snapshot": exp.DataType.build(blob_type), 114 "kind_name": exp.DataType.build(index_type), 115 "updated_ts": exp.DataType.build("bigint"), 116 "unpaused_ts": exp.DataType.build("bigint"), 117 "ttl_ms": exp.DataType.build("bigint"), 118 "unrestorable": exp.DataType.build("boolean"), 119 "forward_only": exp.DataType.build("boolean"), 120 "dev_version": exp.DataType.build(index_type), 121 "fingerprint": exp.DataType.build(blob_type), 122 }, 123 )
34def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore 35 import pandas as pd 36 37 snapshots_table = "_snapshots" 38 if schema: 39 snapshots_table = f"{schema}.{snapshots_table}" 40 41 index_type = index_text_type(engine_adapter.dialect) 42 blob_type = blob_text_type(engine_adapter.dialect) 43 44 new_snapshots = [] 45 migration_needed = False 46 47 for ( 48 name, 49 identifier, 50 version, 51 snapshot, 52 kind_name, 53 updated_ts, 54 unpaused_ts, 55 ttl_ms, 56 unrestorable, 57 forward_only, 58 dev_version, 59 fingerprint, 60 ) in engine_adapter.fetchall( 61 exp.select( 62 "name", 63 "identifier", 64 "version", 65 "snapshot", 66 "kind_name", 67 "updated_ts", 68 "unpaused_ts", 69 "ttl_ms", 70 "unrestorable", 71 "forward_only", 72 "dev_version", 73 "fingerprint", 74 ).from_(snapshots_table), 75 quote_identifiers=True, 76 ): 77 parsed_snapshot = json.loads(snapshot) 78 python_env = parsed_snapshot["node"].get("python_env") or {} 79 for executable in python_env.values(): 80 if executable.get("kind") != "definition": 81 continue 82 new_payload = ast.unparse(ast.parse(executable["payload"])).strip() 83 if new_payload != executable["payload"]: 84 executable["payload"] = new_payload 85 migration_needed = True 86 87 new_snapshots.append( 88 { 89 "name": name, 90 "identifier": identifier, 91 "version": version, 92 "snapshot": json.dumps(parsed_snapshot), 93 "kind_name": kind_name, 94 "updated_ts": updated_ts, 95 "unpaused_ts": unpaused_ts, 96 "ttl_ms": ttl_ms, 97 "unrestorable": unrestorable, 98 "forward_only": forward_only, 99 "dev_version": dev_version, 100 "fingerprint": fingerprint, 101 } 102 ) 103 104 if migration_needed and new_snapshots: 105 engine_adapter.delete_from(snapshots_table, "TRUE") 106 107 engine_adapter.insert_append( 108 snapshots_table, 109 pd.DataFrame(new_snapshots), 110 target_columns_to_types={ 111 "name": exp.DataType.build(index_type), 112 "identifier": exp.DataType.build(index_type), 113 "version": exp.DataType.build(index_type), 114 "snapshot": exp.DataType.build(blob_type), 115 "kind_name": exp.DataType.build(index_type), 116 "updated_ts": exp.DataType.build("bigint"), 117 "unpaused_ts": exp.DataType.build("bigint"), 118 "ttl_ms": exp.DataType.build("bigint"), 119 "unrestorable": exp.DataType.build("boolean"), 120 "forward_only": exp.DataType.build("boolean"), 121 "dev_version": exp.DataType.build(index_type), 122 "fingerprint": exp.DataType.build(blob_type), 123 }, 124 )