Skip to content

Commit 1411f9f

Browse files
authored
Fix: set expr on column-based dimensions/entities when name differs from column (#12513)
* Fix FK constraint ref() resolving to deferred relation when target is selected During deferral, foreign key constraint ref() was always resolving to the deferred (production) relation even when the referenced model was being built as part of the current selection. This caused FK constraints to reference the wrong schema. The fix adds selection awareness to FK constraint compilation: - Track selected node IDs on the Compiler instance - Use deferred relation only if FK target is NOT in selection - Propagate selection info from task to runner compilers This mirrors the behavior of RuntimeRefResolver.create_relation() for model body refs, ensuring FK constraints behave consistently. Fixes #12455 * Set expr on column-based dimensions and entities when name differs from column (#12512) When using v2 semantic layer YAML with column-level dimension/entity definitions, if the name differs from the column name, expr was not set in the manifest. This caused MetricFlow to query the dimension/entity name instead of the actual warehouse column, resulting in invalid identifier errors during `dbt sl validate`.
1 parent 0168c62 commit 1411f9f

File tree

4 files changed

+177
-2
lines changed

4 files changed

+177
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: Set expr to column name for column-based dimensions and entities when the semantic layer name differs from the column name, so MetricFlow queries the correct warehouse column
3+
time: 2026-02-19T17:00:00.000000+00:00
4+
custom:
5+
Author: b-per
6+
Issue: "12512"

core/dbt/parser/schema_yaml_readers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,20 +912,23 @@ def _parse_v2_column_dimensions(self, columns: Dict[str, ColumnInfo]) -> List[Di
912912
meta = dict(column.config.get("meta", {}))
913913
meta.update((column.dimension.config or {}).get("meta", {}))
914914
config = SemanticLayerElementConfig(meta=meta)
915+
dimension_name = column.dimension.name or column.name
915916
dimensions.append(
916917
Dimension(
917918
# required
918919
type=DimensionType(column.dimension.type),
919920
# fields that use column's values as fallback values
920-
name=column.dimension.name or column.name,
921+
name=dimension_name,
921922
description=column.dimension.description or column.description,
922923
config=config,
923924
# optional fields
924925
label=column.dimension.label,
925926
is_partition=column.dimension.is_partition,
926927
type_params=type_params,
927928
metadata=None, # Not yet supported in v1 or v2 YAML
928-
# expr argument is not supported for column-based dimensions
929+
# When the dimension name differs from the column name, set expr
930+
# to the column name so MetricFlow queries the correct warehouse column.
931+
expr=column.name if dimension_name != column.name else None,
929932
)
930933
)
931934
return dimensions
@@ -978,6 +981,9 @@ def _parse_v2_column_entities(self, columns: Dict[str, ColumnInfo]) -> List[Enti
978981
type=column.entity.type,
979982
description=column.entity.description,
980983
label=column.entity.label,
984+
# When the entity name differs from the column name, set expr
985+
# to the column name so MetricFlow queries the correct warehouse column.
986+
expr=column.name if column.entity.name != column.name else None,
981987
config=SemanticLayerElementConfig(
982988
meta=column.entity.config.get("meta", column.config.get("meta", {}))
983989
),

tests/functional/semantic_models/test_semantic_model_v2_parsing.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ def test_semantic_model_parsing(self, project) -> None:
7878
assert id_dim.label == "ID Dimension"
7979
assert id_dim.is_partition is True
8080
assert id_dim.config.meta == {"component_level": "dimension_override"}
81+
# dimension name "id_dim" differs from column name "id", so expr must
82+
# be set to the column name for MetricFlow to query the correct column.
83+
assert id_dim.expr == "id"
8184
second_dim = dimensions["second_dim"]
8285
assert second_dim.type == DimensionType.TIME
8386
assert second_dim.description == "This is the second column (dim)."
@@ -86,6 +89,9 @@ def test_semantic_model_parsing(self, project) -> None:
8689
assert second_dim.config.meta == {}
8790
assert second_dim.type_params.validity_params.is_start is True
8891
assert second_dim.type_params.validity_params.is_end is True
92+
# dimension name "second_dim" differs from column name "second_col",
93+
# so expr must be set to the column name.
94+
assert second_dim.expr == "second_col"
8995
col_with_default_dimensions = dimensions["col_with_default_dimensions"]
9096
assert col_with_default_dimensions.type == DimensionType.CATEGORICAL
9197
assert (
@@ -96,6 +102,8 @@ def test_semantic_model_parsing(self, project) -> None:
96102
assert col_with_default_dimensions.is_partition is False
97103
assert col_with_default_dimensions.config.meta == {}
98104
assert col_with_default_dimensions.validity_params is None
105+
# dimension name matches column name, so expr should not be set.
106+
assert col_with_default_dimensions.expr is None
99107

100108
# Entities
101109
assert len(semantic_model.entities) == 3
@@ -105,12 +113,17 @@ def test_semantic_model_parsing(self, project) -> None:
105113
assert primary_entity.description == "This is the id entity, and it is the primary entity."
106114
assert primary_entity.label == "ID Entity"
107115
assert primary_entity.config.meta == {"component_level": "entity_override"}
116+
# entity name "id_entity" differs from column name "id", so expr must
117+
# be set to the column name for MetricFlow to query the correct column.
118+
assert primary_entity.expr == "id"
108119

109120
foreign_id_col = entities["foreign_id_col"]
110121
assert foreign_id_col.type == EntityType.FOREIGN
111122
assert foreign_id_col.description == "This is a foreign id column."
112123
assert foreign_id_col.label is None
113124
assert foreign_id_col.config.meta == {}
125+
# entity name matches column name, so expr should not be set.
126+
assert foreign_id_col.expr is None
114127

115128
col_with_default_entity_testing_default_desc = entities[
116129
"col_with_default_entity_testing_default_desc"
@@ -122,6 +135,8 @@ def test_semantic_model_parsing(self, project) -> None:
122135
)
123136
assert col_with_default_entity_testing_default_desc.label is None
124137
assert col_with_default_entity_testing_default_desc.config.meta == {}
138+
# entity name differs from column name, so expr must be set.
139+
assert col_with_default_entity_testing_default_desc.expr == "col_with_default_dimensions"
125140

126141
# No measures in v2 YAML
127142
assert len(semantic_model.measures) == 0
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Unit tests for _parse_v2_column_dimensions and _parse_v2_column_entities.
2+
3+
These methods transform column-level dimension/entity definitions into Dimension
4+
and Entity objects for the semantic manifest. When a dimension or entity name
5+
differs from the column name, the resulting object must have expr set to the
6+
column name so that MetricFlow generates SQL against the correct warehouse column.
7+
"""
8+
9+
from collections import OrderedDict
10+
11+
from dbt.artifacts.resources import ColumnDimension, ColumnEntity, ColumnInfo
12+
from dbt.parser.schema_yaml_readers import SemanticModelParser
13+
from dbt_semantic_interfaces.type_enums import DimensionType, EntityType
14+
15+
16+
def _make_column(name, description="", dimension=None, entity=None, granularity=None, config=None):
17+
"""Helper to construct a ColumnInfo with only the fields we need."""
18+
return ColumnInfo(
19+
name=name,
20+
description=description,
21+
dimension=dimension,
22+
entity=entity,
23+
granularity=granularity,
24+
config=config or {},
25+
)
26+
27+
28+
def _make_columns_dict(*columns):
29+
"""Build an OrderedDict of ColumnInfo keyed by name, as the parser expects."""
30+
return OrderedDict((col.name, col) for col in columns)
31+
32+
33+
class TestParseV2ColumnDimensionsExpr:
34+
"""Test that _parse_v2_column_dimensions sets expr correctly."""
35+
36+
def test_dimension_name_override_sets_expr_to_column_name(self):
37+
"""When dimension.name differs from column.name, expr must be the column name."""
38+
columns = _make_columns_dict(
39+
_make_column(
40+
name="afe_number",
41+
description="AFE identifier",
42+
dimension=ColumnDimension(
43+
name="approval_afe_number",
44+
type=DimensionType.CATEGORICAL,
45+
),
46+
),
47+
)
48+
dimensions = SemanticModelParser._parse_v2_column_dimensions(None, columns)
49+
assert len(dimensions) == 1
50+
dim = dimensions[0]
51+
assert dim.name == "approval_afe_number"
52+
assert dim.expr == "afe_number"
53+
54+
def test_dimension_name_matches_column_name_expr_is_none(self):
55+
"""When dimension.name matches column.name, expr should be None."""
56+
columns = _make_columns_dict(
57+
_make_column(
58+
name="status",
59+
dimension=ColumnDimension(
60+
name="status",
61+
type=DimensionType.CATEGORICAL,
62+
),
63+
),
64+
)
65+
dimensions = SemanticModelParser._parse_v2_column_dimensions(None, columns)
66+
assert len(dimensions) == 1
67+
assert dimensions[0].name == "status"
68+
assert dimensions[0].expr is None
69+
70+
def test_dimension_empty_name_defaults_to_column_name(self):
71+
"""When dimension.name is empty string, name defaults to column.name and expr is None."""
72+
columns = _make_columns_dict(
73+
_make_column(
74+
name="category",
75+
dimension=ColumnDimension(
76+
name="",
77+
type=DimensionType.CATEGORICAL,
78+
),
79+
),
80+
)
81+
dimensions = SemanticModelParser._parse_v2_column_dimensions(None, columns)
82+
assert len(dimensions) == 1
83+
assert dimensions[0].name == "category"
84+
assert dimensions[0].expr is None
85+
86+
def test_dimension_shorthand_type_expr_is_none(self):
87+
"""When dimension is just a DimensionType (shorthand), expr should be None."""
88+
columns = _make_columns_dict(
89+
_make_column(
90+
name="color",
91+
dimension=DimensionType.CATEGORICAL,
92+
),
93+
)
94+
dimensions = SemanticModelParser._parse_v2_column_dimensions(None, columns)
95+
assert len(dimensions) == 1
96+
assert dimensions[0].name == "color"
97+
assert dimensions[0].expr is None
98+
99+
100+
class TestParseV2ColumnEntitiesExpr:
101+
"""Test that _parse_v2_column_entities sets expr correctly."""
102+
103+
def test_entity_name_override_sets_expr_to_column_name(self):
104+
"""When entity.name differs from column.name, expr must be the column name."""
105+
columns = _make_columns_dict(
106+
_make_column(
107+
name="id",
108+
description="Primary key",
109+
entity=ColumnEntity(
110+
name="id_entity",
111+
type=EntityType.PRIMARY,
112+
),
113+
),
114+
)
115+
entities = SemanticModelParser._parse_v2_column_entities(None, columns)
116+
assert len(entities) == 1
117+
ent = entities[0]
118+
assert ent.name == "id_entity"
119+
assert ent.expr == "id"
120+
121+
def test_entity_name_matches_column_name_expr_is_none(self):
122+
"""When entity.name matches column.name, expr should be None."""
123+
columns = _make_columns_dict(
124+
_make_column(
125+
name="user_id",
126+
entity=ColumnEntity(
127+
name="user_id",
128+
type=EntityType.FOREIGN,
129+
),
130+
),
131+
)
132+
entities = SemanticModelParser._parse_v2_column_entities(None, columns)
133+
assert len(entities) == 1
134+
assert entities[0].name == "user_id"
135+
assert entities[0].expr is None
136+
137+
def test_entity_shorthand_type_expr_is_none(self):
138+
"""When entity is just an EntityType (shorthand), name defaults to column.name and expr is None."""
139+
columns = _make_columns_dict(
140+
_make_column(
141+
name="foreign_id_col",
142+
entity=EntityType.FOREIGN,
143+
),
144+
)
145+
entities = SemanticModelParser._parse_v2_column_entities(None, columns)
146+
assert len(entities) == 1
147+
assert entities[0].name == "foreign_id_col"
148+
assert entities[0].expr is None

0 commit comments

Comments
 (0)