Skip to content

Commit 9998db4

Browse files
committed
Chore: Allow duplicate keys in dbt project yaml files
1 parent 4e2dd29 commit 9998db4

File tree

3 files changed

+57
-1
lines changed

3 files changed

+57
-1
lines changed

sqlmesh/dbt/common.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636

3737
def load_yaml(source: str | Path) -> t.Dict:
3838
try:
39-
return load(source, render_jinja=False)
39+
return load(
40+
source, render_jinja=False, allow_duplicate_keys=True, keep_last_duplicate_key=True
41+
)
4042
except DuplicateKeyError as ex:
4143
raise ConfigError(f"{source}: {ex}" if isinstance(source, Path) else f"{ex}")
4244

sqlmesh/utils/yaml.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pathlib import Path
99

1010
from ruamel import yaml
11+
from ruamel.yaml.constructor import SafeConstructor
1112

1213
from sqlmesh.core.constants import VAR
1314
from sqlmesh.utils.errors import SQLMeshError
@@ -32,12 +33,30 @@ def YAML(typ: t.Optional[str] = "safe") -> yaml.YAML:
3233
return yaml_obj
3334

3435

36+
class SafeConstructorOverride(SafeConstructor):
37+
def check_mapping_key(
38+
self,
39+
node: t.Any,
40+
key_node: t.Any,
41+
mapping: t.Any,
42+
key: t.Any,
43+
value: t.Any,
44+
) -> bool:
45+
"""This function normally returns True if key is unique.
46+
47+
It is only used by the construct_mapping function. By always returning True,
48+
keys will always be updated and so the last value will be kept for mappings.
49+
"""
50+
return True
51+
52+
3553
def load(
3654
source: str | Path,
3755
raise_if_empty: bool = True,
3856
render_jinja: bool = True,
3957
allow_duplicate_keys: bool = False,
4058
variables: t.Optional[t.Dict[str, t.Any]] = None,
59+
keep_last_duplicate_key: bool = False,
4160
) -> t.Dict:
4261
"""Loads a YAML object from either a raw string or a file."""
4362
path: t.Optional[Path] = None
@@ -56,6 +75,8 @@ def load(
5675
)
5776

5877
yaml = YAML()
78+
if allow_duplicate_keys and keep_last_duplicate_key:
79+
yaml.Constructor = SafeConstructorOverride
5980
yaml.allow_duplicate_keys = allow_duplicate_keys
6081
contents = yaml.load(source)
6182
if contents is None:

tests/utils/test_yaml.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,36 @@ def test_yaml() -> None:
4545

4646
decimal_value = Decimal(123.45)
4747
assert yaml.load(yaml.dump(decimal_value)) == str(decimal_value)
48+
49+
50+
def test_load_keep_last_duplicate_key() -> None:
51+
input_str = """
52+
name: first_name
53+
name: second_name
54+
name: third_name
55+
56+
foo: bar
57+
58+
mapping:
59+
key: first_value
60+
key: third_value
61+
62+
sequence:
63+
- one
64+
- two
65+
"""
66+
# Default behavior of ruamel is to keep the first key encountered
67+
assert yaml.load(input_str, allow_duplicate_keys=True) == {
68+
"name": "first_name",
69+
"foo": "bar",
70+
"mapping": {"key": "first_value"},
71+
"sequence": ["one", "two"],
72+
}
73+
74+
# Test keeping last key
75+
assert yaml.load(input_str, allow_duplicate_keys=True, keep_last_duplicate_key=True) == {
76+
"name": "third_name",
77+
"foo": "bar",
78+
"mapping": {"key": "third_value"},
79+
"sequence": ["one", "two"],
80+
}

0 commit comments

Comments
 (0)