Skip to content

Commit 4b53296

Browse files
committed
only sort root dict
1 parent 60ee5ee commit 4b53296

File tree

6 files changed

+87
-184
lines changed

6 files changed

+87
-184
lines changed

sqlmesh/core/model/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,15 @@ def _add_variables_to_python_env(
153153

154154
variables = {k: v for k, v in (variables or {}).items() if k in used_variables}
155155
if variables:
156-
python_env[c.SQLMESH_VARS] = Executable.value(variables, use_deterministic_repr=True)
156+
python_env[c.SQLMESH_VARS] = Executable.value(variables, sort_root_dict=True)
157157

158158
if blueprint_variables:
159159
blueprint_variables = {
160160
k: SqlValue(sql=v.sql(dialect=dialect)) if isinstance(v, exp.Expression) else v
161161
for k, v in blueprint_variables.items()
162162
}
163163
python_env[c.SQLMESH_BLUEPRINT_VARS] = Executable.value(
164-
blueprint_variables, use_deterministic_repr=True
164+
blueprint_variables, sort_root_dict=True
165165
)
166166

167167
return python_env

sqlmesh/migrations/v0085_deterministic_repr.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import json
7+
import logging
78
import typing as t
89
from dataclasses import dataclass
910

@@ -12,6 +13,9 @@
1213
from sqlmesh.utils.migration import index_text_type, blob_text_type
1314

1415

16+
logger = logging.getLogger(__name__)
17+
18+
1519
KEYS_TO_MAKE_DETERMINISTIC = ["__sqlmesh__vars__", "__sqlmesh__blueprint__vars__"]
1620

1721

@@ -23,25 +27,13 @@ class SqlValue:
2327
sql: str
2428

2529

26-
def _deterministic_repr(obj: t.Any) -> str:
27-
"""
28-
This is a copy of the function from utils.metaprogramming
29-
"""
30-
31-
def _normalize_for_repr(o: t.Any) -> t.Any:
32-
if isinstance(o, dict):
33-
sorted_items = sorted(o.items(), key=lambda x: str(x[0]))
34-
return {k: _normalize_for_repr(v) for k, v in sorted_items}
35-
if isinstance(o, (list, tuple)):
36-
# Recursively normalize nested structures
37-
normalized = [_normalize_for_repr(item) for item in o]
38-
return type(o)(normalized)
39-
return o
40-
30+
def _dict_sort(obj: t.Any) -> str:
4131
try:
42-
return repr(_normalize_for_repr(obj))
32+
if isinstance(obj, dict):
33+
obj = dict(sorted(obj.items(), key=lambda x: str(x[0])))
4334
except Exception:
44-
return repr(obj)
35+
logger.warning("Failed to sort non-recursive dict", exc_info=True)
36+
return repr(obj)
4537

4638

4739
def migrate(state_sync, **kwargs): # type: ignore
@@ -92,15 +84,15 @@ def migrate(state_sync, **kwargs): # type: ignore
9284
try:
9385
# Try to parse the old payload and re-serialize it deterministically
9486
parsed_value = eval(old_payload)
95-
new_payload = _deterministic_repr(parsed_value)
87+
new_payload = _dict_sort(parsed_value)
9688

9789
# Only update if the representation changed
9890
if old_payload != new_payload:
9991
executable["payload"] = new_payload
10092
migration_needed = True
10193
except Exception:
10294
# If we still can't eval it, leave it as-is
103-
pass
95+
logger.warning("Exception trying to eval payload", exc_info=True)
10496

10597
new_snapshots.append(
10698
{

sqlmesh/migrations/v0086_check_deterministic_bug.py

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,15 @@
11
import json
2-
import typing as t
2+
import logging
3+
34
from sqlglot import exp
45

56
from sqlmesh.core.console import get_console
67

78

9+
logger = logging.getLogger(__name__)
810
KEYS_TO_MAKE_DETERMINISTIC = ["__sqlmesh__vars__", "__sqlmesh__blueprint__vars__"]
911

1012

11-
def would_sorting_be_applied(obj: t.Any) -> bool:
12-
"""
13-
Detects if sorting would be applied to an object based on the
14-
deterministic_repr logic.
15-
16-
Returns True if the object is a dictionary or contains a dictionary
17-
at any nesting level (in lists or tuples).
18-
19-
Args:
20-
obj: The object to check
21-
22-
Returns:
23-
bool: True if sorting would be applied, False otherwise
24-
"""
25-
26-
def _check_for_dict(o: t.Any) -> bool:
27-
if isinstance(o, dict):
28-
return True
29-
if isinstance(o, (list, tuple)):
30-
return any(_check_for_dict(item) for item in o)
31-
32-
return False
33-
34-
try:
35-
return _check_for_dict(obj)
36-
except Exception:
37-
# If any error occurs during checking, assume no sorting
38-
return False
39-
40-
4113
def migrate(state_sync, **kwargs): # type: ignore
4214
engine_adapter = state_sync.engine_adapter
4315
schema = state_sync.schema
@@ -103,8 +75,8 @@ def migrate(state_sync, **kwargs): # type: ignore
10375
):
10476
try:
10577
parsed_value = eval(executable["payload"])
106-
if would_sorting_be_applied(parsed_value):
78+
if isinstance(parsed_value, dict):
10779
get_console().log_warning(warning)
10880
return
10981
except Exception:
110-
pass
82+
logger.warning("Exception trying to eval payload", exc_info=True)

sqlmesh/utils/metaprogramming.py

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import importlib
66
import inspect
77
import linecache
8+
import logging
89
import os
910
import re
1011
import sys
@@ -23,6 +24,9 @@
2324
from sqlmesh.utils.errors import SQLMeshError
2425
from sqlmesh.utils.pydantic import PydanticModel
2526

27+
logger = logging.getLogger(__name__)
28+
29+
2630
IGNORE_DECORATORS = {"macro", "model", "signal"}
2731
SERIALIZABLE_CALLABLES = (type, types.FunctionType)
2832
LITERALS = (Number, str, bytes, tuple, list, dict, set, bool)
@@ -425,9 +429,9 @@ def is_value(self) -> bool:
425429

426430
@classmethod
427431
def value(
428-
cls, v: t.Any, is_metadata: t.Optional[bool] = None, use_deterministic_repr: bool = False
432+
cls, v: t.Any, is_metadata: t.Optional[bool] = None, sort_root_dict: bool = False
429433
) -> Executable:
430-
payload = _deterministic_repr(v) if use_deterministic_repr else repr(v)
434+
payload = _dict_sort(v) if sort_root_dict else repr(v)
431435
return Executable(payload=payload, kind=ExecutableKind.VALUE, is_metadata=is_metadata)
432436

433437

@@ -636,36 +640,13 @@ def print_exception(
636640
out.write(tb)
637641

638642

639-
def _deterministic_repr(obj: t.Any) -> str:
640-
"""Create a deterministic representation by ensuring consistent ordering before repr().
641-
642-
For dictionaries, ensures consistent key ordering to prevent non-deterministic
643-
serialization that affects fingerprinting. Uses Python's native repr() logic
644-
for all formatting to handle edge cases properly.
645-
646-
Note that this function assumes list/tuple order is significant and therefore does not sort them.
647-
648-
Args:
649-
obj: The object to represent as a string.
650-
651-
Returns:
652-
A deterministic string representation of the object.
653-
"""
654-
655-
def _normalize_for_repr(o: t.Any) -> t.Any:
656-
if isinstance(o, dict):
657-
sorted_items = sorted(o.items(), key=lambda x: str(x[0]))
658-
return {k: _normalize_for_repr(v) for k, v in sorted_items}
659-
if isinstance(o, (list, tuple)):
660-
# Recursively normalize nested structures
661-
normalized = [_normalize_for_repr(item) for item in o]
662-
return type(o)(normalized)
663-
return o
664-
643+
def _dict_sort(obj: t.Any) -> str:
665644
try:
666-
return repr(_normalize_for_repr(obj))
645+
if isinstance(obj, dict):
646+
obj = dict(sorted(obj.items(), key=lambda x: str(x[0])))
667647
except Exception:
668-
return repr(obj)
648+
logger.warning("Failed to sort non-recursive dict", exc_info=True)
649+
return repr(obj)
669650

670651

671652
def import_python_file(path: Path, relative_base: Path = Path()) -> types.ModuleType:

tests/core/test_model.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6117,7 +6117,7 @@ def test_named_variable_macros() -> None:
61176117

61186118
assert model.python_env[c.SQLMESH_VARS] == Executable.value(
61196119
{c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"},
6120-
use_deterministic_repr=True,
6120+
sort_root_dict=True,
61216121
)
61226122
assert (
61236123
model.render_query_or_raise().sql()
@@ -6144,7 +6144,7 @@ def test_variables_in_templates() -> None:
61446144

61456145
assert model.python_env[c.SQLMESH_VARS] == Executable.value(
61466146
{c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"},
6147-
use_deterministic_repr=True,
6147+
sort_root_dict=True,
61486148
)
61496149
assert (
61506150
model.render_query_or_raise().sql()
@@ -6169,7 +6169,7 @@ def test_variables_in_templates() -> None:
61696169

61706170
assert model.python_env[c.SQLMESH_VARS] == Executable.value(
61716171
{c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"},
6172-
use_deterministic_repr=True,
6172+
sort_root_dict=True,
61736173
)
61746174
assert (
61756175
model.render_query_or_raise().sql()
@@ -6309,7 +6309,7 @@ def test_variables_migrated_dbt_package_macro():
63096309
)
63106310
assert model.python_env[c.SQLMESH_VARS] == Executable.value(
63116311
{"test_var_a": "test_var_a_value", "__dbt_packages__.test.test_var_b": "test_var_b_value"},
6312-
use_deterministic_repr=True,
6312+
sort_root_dict=True,
63136313
)
63146314
assert (
63156315
model.render_query().sql(dialect="bigquery")
@@ -6535,7 +6535,7 @@ def test_unrendered_macros_sql_model(mocker: MockerFixture) -> None:
65356535
"virtual_var": "blb",
65366536
"session_var": "blc",
65376537
},
6538-
use_deterministic_repr=True,
6538+
sort_root_dict=True,
65396539
)
65406540

65416541
assert "location1" in model.physical_properties
@@ -6623,7 +6623,7 @@ def model_with_macros(evaluator, **kwargs):
66236623
"virtual_var": "blb",
66246624
"session_var": "blc",
66256625
},
6626-
use_deterministic_repr=True,
6626+
sort_root_dict=True,
66276627
)
66286628
assert python_sql_model.enabled
66296629

@@ -10583,10 +10583,10 @@ def unimportant_testing_macro(evaluator, *projections):
1058310583

1058410584
assert m.python_env.get(c.SQLMESH_VARS) == Executable.value(
1058510585
{"selector": "bla", "bla_variable": 1, "baz_variable": 2},
10586-
use_deterministic_repr=True,
10586+
sort_root_dict=True,
1058710587
)
1058810588
assert m.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value(
10589-
{"selector": "baz"}, use_deterministic_repr=True
10589+
{"selector": "baz"}, sort_root_dict=True
1059010590
)
1059110591

1059210592

0 commit comments

Comments
 (0)