diff --git a/pkg-py/src/commons/_backends.py b/pkg-py/src/commons/_backends.py index 14f1e426..67a88c2e 100644 --- a/pkg-py/src/commons/_backends.py +++ b/pkg-py/src/commons/_backends.py @@ -107,18 +107,13 @@ def list_tables(self) -> list[str]: return list(sqlalchemy.inspect(self._engine).get_table_names()) def quote(self, table_id: TableId) -> str: + # quote_identifier() always quotes. quote() leaves an all-lowercase + # name bare, because SQLAlchemy reads it as case-insensitive, and a + # warehouse that folds to upper case then looks up a different name. + # Each component is quoted on its own: one call on "ANALYTICS.PUBLIC" + # names a schema with a dot in it. preparer = self._engine.dialect.identifier_preparer - quoted = preparer.quote(table_id.table) - if table_id.schema is None: - return quoted - # quote_schema() is given one component at a time: handed - # "ANALYTICS.PUBLIC" it produces one identifier containing a dot. - outer = ".".join( - preparer.quote_schema(part) - for part in (table_id.catalog, table_id.schema) - if part is not None - ) - return f"{outer}.{quoted}" + return ".".join(preparer.quote_identifier(part) for part in table_id.parts) def columns(self, table_id: TableId) -> list[dict[str, Any]]: inspector = sqlalchemy.inspect(self._engine) diff --git a/pkg-py/tests/test_data_source_engine.py b/pkg-py/tests/test_data_source_engine.py index 0ee82055..88dafc49 100644 --- a/pkg-py/tests/test_data_source_engine.py +++ b/pkg-py/tests/test_data_source_engine.py @@ -207,13 +207,23 @@ def test_the_engine_backend_quotes_each_component_separately() -> None: assert quoted == '"ANALYTICS"."PUBLIC"."ORDERS"' -def test_the_engine_backend_still_quotes_a_two_part_name() -> None: +def test_the_engine_backend_quotes_a_lowercase_name_too() -> None: backend = EngineBackend(sqlalchemy.create_engine("sqlite://")) assert backend.quote(TableId(table="sales", schema="analytics")) == ( - "analytics.sales" + '"analytics"."sales"' ) +def test_a_lowercase_snowflake_table_keeps_its_case() -> None: + # SQLAlchemy treats an all-lowercase name as case-insensitive and leaves + # it bare, which Snowflake folds to upper case: a table created as + # "survey" would be looked up as SURVEY. + pytest.importorskip("snowflake.sqlalchemy") + backend = EngineBackend(sqlalchemy.create_engine("snowflake://u@account/db")) + quoted = backend.quote(TableId(table="survey", schema="LIVE", catalog="COMMONS_CI")) + assert quoted == '"COMMONS_CI"."LIVE"."survey"' + + def test_an_engine_backend_reports_columns_from_its_inspector( engine: sqlalchemy.Engine, ) -> None: