From 02c0c691b06124142a523b51d7abdd91b720493a Mon Sep 17 00:00:00 2001 From: ReguiguiMohamed Date: Tue, 18 Aug 2026 22:20:10 +0100 Subject: [PATCH 1/2] fix(model): keep quoting when normalizing column_descriptions keys _column_descriptions_validator built each key with part.this, which is the bare identifier string, so the quoted flag was gone before normalize_identifiers ran. A quoted key was then normalized as if it were unquoted, and on dialects where quoting makes a column case-sensitive the resulting name matched no column, so the description was dropped along with the rest of the table's comments. Normalize each part while it is still an identifier. Unquoted keys normalize exactly as before. Fixes #5943 Signed-off-by: ReguiguiMohamed --- sqlmesh/core/model/meta.py | 18 ++++++++++-------- tests/core/test_model.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 94956dff99..67ea32f9d9 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -321,17 +321,19 @@ def _column_descriptions_validator( if isinstance(vs, (exp.Tuple, exp.Array)): vs = vs.expressions - raw_col_descriptions = ( - vs + # Normalize each part while it is still an identifier, so that a quoted + # column keeps its case on dialects where quoting makes it significant. + col_descriptions = ( + {normalize_identifiers(k, dialect=dialect).name: v for k, v in vs.items()} if isinstance(vs, dict) - else {".".join([part.this for part in v.this.parts]): v.expression.name for v in vs} + else { + ".".join( + normalize_identifiers(part, dialect=dialect).name for part in v.this.parts + ): v.expression.name + for v in vs + } ) - col_descriptions = { - normalize_identifiers(k, dialect=dialect).name: v - for k, v in raw_col_descriptions.items() - } - columns_to_types = data.get("columns_to_types_") if columns_to_types: from sqlmesh.core.console import get_console diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 1f3cde265b..3de4ff2ebc 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1000,6 +1000,32 @@ def test_column_descriptions(sushi_context, assert_exp_eq): assert model.column_descriptions == {"id": "primary key", "foo": "bar"} +def test_column_descriptions_quoted_identifier(): + expressions = d.parse( + """ + MODEL ( + name db.table, + kind FULL, + dialect snowflake, + column_descriptions ( + "myColumn" = 'a case-sensitive column', + other_column = 'an unquoted column' + ) + ); + + SELECT 1 AS "myColumn", 2 AS other_column + """ + ) + model = load_sql_based_model(expressions, dialect="snowflake") + + # A quoted key keeps its case, an unquoted one is still normalized. + assert model.column_descriptions == { + "myColumn": "a case-sensitive column", + "OTHER_COLUMN": "an unquoted column", + } + assert set(model.column_descriptions) <= set(model.columns_to_types) + + def test_model_jinja_macro_reference_extraction(): @macro() def test_macro(**kwargs) -> None: From 6df27fef6ce7a8aa2936ba85b10a55a7dc28d31d Mon Sep 17 00:00:00 2001 From: ReguiguiMohamed Date: Tue, 25 Aug 2026 09:55:11 +0100 Subject: [PATCH 2/2] test(model): cover dotted column_descriptions keys The join-by-parts path had no test. A BigQuery nested path normalizes the same as before the fix, since joining the parts and re-parsing produced a quoted identifier that BigQuery lowercases anyway. On Snowflake an unquoted path now normalizes per part, which is what an unquoted name should do there, and a quoted one keeps its case. Signed-off-by: ReguiguiMohamed --- tests/core/test_model.py | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 3de4ff2ebc..dd8de9ebaa 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1026,6 +1026,49 @@ def test_column_descriptions_quoted_identifier(): assert set(model.column_descriptions) <= set(model.columns_to_types) +def test_column_descriptions_dotted_identifier(): + # A nested field is looked up by its dotted path, so every part normalizes on its own. + expressions = d.parse( + """ + MODEL ( + name db.table, + kind FULL, + dialect bigquery, + column_descriptions ( + record.`myField` = 'a nested field' + ) + ); + + SELECT STRUCT(1 AS `myField`) AS record + """ + ) + model = load_sql_based_model(expressions, dialect="bigquery") + + assert model.column_descriptions == {"record.myfield": "a nested field"} + + expressions = d.parse( + """ + MODEL ( + name db.table, + kind FULL, + dialect snowflake, + column_descriptions ( + nested.field = 'an unquoted path', + "MyStruct"."myField" = 'a quoted path' + ) + ); + + SELECT 1 AS c + """ + ) + model = load_sql_based_model(expressions, dialect="snowflake") + + assert model.column_descriptions == { + "NESTED.FIELD": "an unquoted path", + "MyStruct.myField": "a quoted path", + } + + def test_model_jinja_macro_reference_extraction(): @macro() def test_macro(**kwargs) -> None: