Skip to content

Commit ae50f5b

Browse files
fix(schema_loader): quote case-sensitive column names in external models
create_external_models wrote column names bare, so a Postgres column "ID" was read back as id. A name is now quoted only when the loader's identifier normalization would change it. Names that already round-trip are written as before. Before the fix the new test failed with '"ID"' != 'ID' on Postgres and '"lower_case"' != 'lower_case' on Snowflake. Fixes #6058 Signed-off-by: ReguiguiMohamed <mohamedreguigui2004@gmail.com>
1 parent f6527ff commit ae50f5b

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

‎sqlmesh/core/schema_loader.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from sqlglot import exp
88
from sqlglot.dialects.dialect import DialectType
9+
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
910

1011
from sqlmesh.core.console import get_console
1112
from sqlmesh.core.engine_adapter import EngineAdapter
@@ -98,7 +99,12 @@ def get_columns(
9899
"""
99100
try:
100101
columns = adapter.columns(table, include_pseudo_columns=True)
101-
return {c: dtype.sql(dialect=dialect) for c, dtype in columns.items()}
102+
result = {}
103+
for c, dtype in columns.items():
104+
if normalize_identifiers(c, dialect=dialect).name != c:
105+
c = exp.to_identifier(c).sql(dialect=dialect, identify=True)
106+
result[c] = dtype.sql(dialect=dialect)
107+
return result
102108
except Exception as e:
103109
msg = f"Unable to get schema for '{table}': '{e}'."
104110
if strict:

‎tests/core/test_schema_loader.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,52 @@ def test_no_internal_model_conversion(tmp_path: Path, mocker: MockerFixture):
389389
create_external_model(**row, dialect="bigquery")
390390

391391

392+
@pytest.mark.parametrize(
393+
"dialect, columns, expected_keys",
394+
[
395+
(
396+
"postgres",
397+
["ID", "MixedCase", "lower_case", "a.b"],
398+
['"ID"', '"MixedCase"', "lower_case", "a.b"],
399+
),
400+
("snowflake", ["ID", "lower_case"], ["ID", '"lower_case"']),
401+
("clickhouse", ["a\\b"], ["a\\b"]),
402+
],
403+
)
404+
def test_create_external_models_quotes_case_sensitive_columns(
405+
tmp_path: Path,
406+
mocker: MockerFixture,
407+
dialect: str,
408+
columns: t.List[str],
409+
expected_keys: t.List[str],
410+
):
411+
engine_adapter_mock = mocker.Mock()
412+
engine_adapter_mock.columns.return_value = {
413+
name: exp.DataType.build("text") for name in columns
414+
}
415+
416+
state_reader_mock = mocker.Mock()
417+
state_reader_mock.nodes_exist.return_value = set()
418+
419+
model = SqlModel(name="a", query=parse_one("select * FROM tbl"))
420+
421+
filename = tmp_path / c.EXTERNAL_MODELS_YAML
422+
create_external_models_file(
423+
filename,
424+
{"a": model}, # type: ignore
425+
engine_adapter_mock,
426+
state_reader_mock,
427+
dialect,
428+
)
429+
430+
schema = yaml.load(filename)
431+
assert list(schema[0]["columns"]) == expected_keys
432+
433+
external_model = create_external_model(**schema[0], dialect=dialect)
434+
assert external_model.columns_to_types is not None
435+
assert list(external_model.columns_to_types) == columns
436+
437+
392438
def test_missing_table(tmp_path: Path):
393439
config = Config(gateways=GatewayConfig(connection=DuckDBConnectionConfig()))
394440
context = Context(paths=[str(tmp_path.absolute())], config=config)

0 commit comments

Comments
 (0)