Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/databricks/labs/dqx/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ def normalize_bound_args(val: Any, allow_simple_expressions_only: bool = True) -
"""
Normalize a value or collection of values for consistent processing.

Handles primitives, dates, Decimal, and column-like objects. Lists, tuples, and sets are
recursively normalized with type preserved.
Handles primitives, dates, Decimal, and column-like objects. Collections are recursively
normalized, preserving list and tuple order while canonicalizing set and frozenset order.

For Decimal values, uses a special JSON-serializable format to preserve type information
for round-trip deserialization.
Expand All @@ -211,7 +211,11 @@ def normalize_bound_args(val: Any, allow_simple_expressions_only: bool = True) -
if val is None:
return None

if isinstance(val, (list, tuple, set, frozenset)):
if isinstance(val, (set, frozenset)):
normalized = [normalize_bound_args(v, allow_simple_expressions_only) for v in val]
return sorted(normalized, key=lambda item: json.dumps(item, sort_keys=True, default=str))

if isinstance(val, (list, tuple)):
return [normalize_bound_args(v, allow_simple_expressions_only) for v in val]

if isinstance(val, dict):
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/test_rule.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Unit tests for rule fingerprinting, replacement, expansion, and serialization alignment."""

from collections.abc import Iterator
from enum import Enum
import re

import pytest
Expand Down Expand Up @@ -31,6 +33,29 @@ def test_compute_rule_fingerprint_same_rule_same_fingerprint():
assert _hex_sha256_pattern().match(fp1)


def test_compute_rule_fingerprint_set_arguments_order_independent():
"""Set arguments with mixed nested values produce the same fingerprint in any iteration order."""

class NestedValue(Enum):
DICT = {"nested": [1, "1"]}

class IterationOrderedSet(set[object]):
def __init__(self, values: list[object]) -> None:
super().__init__(values)
self.iteration_order = values

def __iter__(self) -> Iterator[object]:
return iter(self.iteration_order)

values: list[object] = [NestedValue.DICT, 1, "1", ("pair", 2), frozenset({3, "3"})]
check = {"check": {"function": "is_in_list", "arguments": {"allowed": IterationOrderedSet(values)}}}
reversed_check = {
"check": {"function": "is_in_list", "arguments": {"allowed": IterationOrderedSet(list(reversed(values)))}}
}

assert compute_rule_fingerprint(check) == compute_rule_fingerprint(reversed_check)


def test_compute_rule_set_fingerprint_by_metadata_same_set_same_fingerprint():
"""Same rule set produces the same rule_set_fingerprint (determinism)."""
checks = [
Expand Down
Loading