Repository: TobikoData/sqlmesh (python)

Issue:
Problem with column_descriptions on Python models.
Hi, 
I'm having problems using this feature on databricks.

``` python
@model(
    name="raw.ps_mcortes",
    kind="FULL",
    cron="@daily",
    grain="MCR_CVE_N",
    description="Tabla con informacion sobre tipos de corte",
    columns={
        "MCR_CVE_N": "BIGINT",
        "MCR_DESC_STR": "STRING",
        "MCR_TIPO_STR": "STRING",
        "MCR_EST_STR": "STRING",
    },
    column_descriptions_={
        "MCR_CVE_N": "Clave de material de corte",
        "MCR_DESC_STR": "Descripción del corte",
        "MCR_TIPO_STR": "Tipo de material de corte",
        "MCR_EST_STR": "Estatus del tipo de material de corte",
    },
)
```
I have this model definition. When I try to do a sqlmesh plan or migrate, I'm getting the following error:
`sqlmesh.utils.errors.ConfigError: In model 'raw.ps_mcortes', a description is provided for column 'MCR_CVE_N' but it is not a column in the model.`

I think is because of the case of the columns.
I've prnted the values of the columns and the descriptions and this is what I get:

```
2024-06-04 11:50:23,146 - MainThread - sqlmesh.core.model.meta - ERROR - {'mcr_cve_n': DataType(this=Type.BIGINT, nested=False), 'mcr_desc_str': DataType(this=Type.TEXT, nested=False), 'mcr_tipo_str': DataType(this=Type.TEXT, nested=False), 'mcr_est_str': DataType(this=Type.TEXT, nested=False)} (meta.py:236)
2024-06-04 11:50:23,146 - MainThread - sqlmesh.core.model.meta - ERROR - {'MCR_CVE_N': 'Clave de material de corte', 'MCR_DESC_STR': 'Descripción del corte', 'MCR_TIPO_STR': 'Tipo de material de corte', 'MCR_EST_STR': 'Estatus del tipo de material de corte'} (meta.py:237)
```

It looks like the column type mapppings are in lowercase and the column_descriptions are in uppercase so it fails. It doesn't matter if I change the keys of the descriptions to be lowercase.

_Originally posted by @zerodarkzone in https://github.com/TobikoData/sqlmesh/issues/2710#issuecomment-2147999123_
            

Pull request:
Fix: normalize column names in column_descriptions dict
Closes #2730 

Patch:
diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py
index a825ff53..c4568e5b 100644
--- a/sqlmesh/core/model/meta.py
+++ b/sqlmesh/core/model/meta.py
@@ -219,6 +219,8 @@ class ModelMeta(_Node):
     def _column_descriptions_validator(
         cls, vs: t.Any, values: t.Dict[str, t.Any]
     ) -> t.Optional[t.Dict[str, str]]:
+        dialect = values.get("dialect")
+
         if vs is None:
             return None
 
@@ -228,10 +230,15 @@ class ModelMeta(_Node):
         if isinstance(vs, (exp.Tuple, exp.Array)):
             vs = vs.expressions
 
-        col_descriptions = (
+        raw_col_descriptions = (
             vs if isinstance(vs, dict) else {v.this.name: 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 = values.get("columns_to_types_")
         if columns_to_types:
             for column_name in col_descriptions:


Test patch:
diff --git a/tests/core/test_model.py b/tests/core/test_model.py
index 3a8c7993..b78b658b 100644
--- a/tests/core/test_model.py
+++ b/tests/core/test_model.py
@@ -1752,6 +1752,35 @@ def test_python_model_decorator_kind() -> None:
         assert not mock_logger.call_args
 
 
+def test_python_model_decorator_col_descriptions() -> None:
+    # `columns` and `column_descriptions` column names are different cases, but name normalization makes both lower
+    @model("col_descriptions", columns={"col": "int"}, column_descriptions={"COL": "a column"})
+    def a_model(context):
+        pass
+
+    py_model = model.get_registry()["col_descriptions"].model(
+        module_path=Path("."),
+        path=Path("."),
+    )
+
+    assert py_model.columns_to_types.keys() == py_model.column_descriptions.keys()
+
+    # error: `columns` and `column_descriptions` column names are different cases, quoting preserves case
+    @model(
+        "col_descriptions_quoted",
+        columns={'"col"': "int"},
+        column_descriptions={'"COL"': "a column"},
+    )
+    def b_model(context):
+        pass
+
+    with pytest.raises(ConfigError, match="a description is provided for column 'COL'"):
+        py_model = model.get_registry()["col_descriptions_quoted"].model(
+            module_path=Path("."),
+            path=Path("."),
+        )
+
+
 def test_star_expansion(assert_exp_eq) -> None:
     context = Context(config=Config())