Skip to content

Commit abe112b

Browse files
committed
fix(fabric): object list now contains catalog
Signed-off-by: Cortland Goffena <30168413+cmgoffena13@users.noreply.github.com>
1 parent d9af742 commit abe112b

2 files changed

Lines changed: 150 additions & 11 deletions

File tree

‎sqlmesh/core/engine_adapter/fabric.py‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sqlglot import exp
99
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_result
1010
from sqlmesh.core.engine_adapter.mssql import MSSQLEngineAdapter
11+
from sqlmesh.core.dialect import to_schema
1112
from sqlmesh.core.engine_adapter.shared import (
1213
CommentCreationTable,
1314
CommentCreationView,
@@ -85,6 +86,14 @@ def _catalog_state_label(self, catalog_name: t.Optional[str]) -> str:
8586
or "<default>"
8687
)
8788

89+
def _resolved_catalog(self) -> t.Optional[str]:
90+
return (
91+
self.get_current_catalog()
92+
or self._normalize_catalog(self._connected_catalog)
93+
or self._default_catalog
94+
or self._extra_config.get("database")
95+
)
96+
8897
@property
8998
def api_client(self) -> FabricHttpClient:
9099
# the requests Session is not guaranteed to be threadsafe
@@ -224,18 +233,18 @@ def set_current_catalog(self, catalog_name: t.Optional[str]) -> None:
224233

225234
self._target_catalog = target_catalog
226235

227-
def _get_data_objects(
228-
self, schema_name: t.Union[str, exp.Table], object_names: t.Optional[t.Set[str]] = None
236+
def get_data_objects(
237+
self,
238+
schema_name: t.Union[str, exp.Table],
239+
object_names: t.Optional[t.Set[str]] = None,
240+
safe_to_cache: bool = False,
229241
) -> t.List[DataObject]:
230-
objects = super()._get_data_objects(schema_name, object_names)
231242
# Fabric uses None as "default catalog" so we skip reconnects. Other engines
232-
# return a real warehouse name here; fill it in so this listing matches them
233-
# and so the data-object cache key matches lookups that use that name.
234-
catalog = (
235-
self.get_current_catalog()
236-
or self._default_catalog
237-
or self._extra_config.get("database")
238-
)
243+
# return a real warehouse name here.
244+
# Cache on schema.table due to @set_catalog stripping the catalog. Then put a
245+
# warehouse name on the returned objects so listing matches other engines.
246+
objects = super().get_data_objects(schema_name, object_names, safe_to_cache)
247+
catalog = to_schema(schema_name).catalog or self._resolved_catalog()
239248
if not catalog:
240249
return objects
241250
return [

‎tests/core/engine_adapter/test_fabric.py‎

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ def adapter(make_mocked_engine_adapter: t.Callable) -> FabricEngineAdapter:
1919
return make_mocked_engine_adapter(FabricEngineAdapter)
2020

2121

22+
def _record_catalogs_at_execute(adapter: FabricEngineAdapter) -> t.List[t.Optional[str]]:
23+
catalogs: t.List[t.Optional[str]] = []
24+
real_execute = adapter.execute
25+
26+
def execute(*args: t.Any, **kwargs: t.Any) -> None:
27+
catalogs.append(adapter._resolved_catalog())
28+
return real_execute(*args, **kwargs)
29+
30+
adapter.execute = execute # type: ignore[method-assign]
31+
return catalogs
32+
33+
2234
def test_get_current_catalog_uses_only_explicit_target_catalog(
2335
make_mocked_engine_adapter: t.Callable,
2436
):
@@ -471,11 +483,129 @@ def test_get_data_objects_uses_default_catalog_when_current_is_none(
471483
return_value=pd.DataFrame([{"name": "test_table", "schema_name": "dbo", "type": "TABLE"}]),
472484
)
473485

474-
assert adapter._get_data_objects("dbo") == [
486+
assert adapter.get_data_objects("ci_abc.dbo") == [
475487
DataObject(
476488
catalog="ci_abc",
477489
schema="dbo",
478490
name="test_table",
479491
type=DataObjectType.TABLE,
480492
)
481493
]
494+
495+
496+
def test_get_data_objects_cache_hits_for_default_catalog(
497+
make_mocked_engine_adapter: t.Callable,
498+
mocker: MockerFixture,
499+
) -> None:
500+
"""Listing fills catalog on the return value. Cache must still hit."""
501+
adapter = make_mocked_engine_adapter(
502+
FabricEngineAdapter,
503+
default_catalog="ci_abc",
504+
database="ci_abc",
505+
patch_get_data_objects=False,
506+
)
507+
fetchdf = mocker.patch.object(
508+
adapter,
509+
"fetchdf",
510+
return_value=pd.DataFrame([{"name": "t", "schema_name": "dbo", "type": "TABLE"}]),
511+
)
512+
513+
first = adapter.get_data_objects("ci_abc.dbo", {"t"}, safe_to_cache=True)
514+
second = adapter.get_data_objects("ci_abc.dbo", {"t"}, safe_to_cache=True)
515+
516+
assert first[0].catalog == "ci_abc"
517+
assert second[0].catalog == "ci_abc"
518+
assert fetchdf.call_count == 1
519+
520+
521+
def test_get_data_objects_labels_connected_warehouse_after_lazy_restore(
522+
make_mocked_engine_adapter: t.Callable,
523+
mocker: MockerFixture,
524+
) -> None:
525+
"""Logical catalog is None; the connection is still on planning.
526+
527+
Unqualified list must be planning. ci_abc.dbo must still switch.
528+
"""
529+
adapter = make_mocked_engine_adapter(
530+
FabricEngineAdapter,
531+
default_catalog="ci_abc",
532+
database="ci_abc",
533+
patch_get_data_objects=False,
534+
)
535+
adapter.set_current_catalog("planning")
536+
adapter.set_current_catalog(None)
537+
assert adapter.get_current_catalog() is None
538+
539+
fetchdf = mocker.patch.object(
540+
adapter,
541+
"fetchdf",
542+
return_value=pd.DataFrame([{"name": "t", "schema_name": "dbo", "type": "TABLE"}]),
543+
)
544+
545+
objects = adapter.get_data_objects("dbo")
546+
547+
assert objects == [
548+
DataObject(
549+
catalog="planning",
550+
schema="dbo",
551+
name="t",
552+
type=DataObjectType.TABLE,
553+
)
554+
]
555+
556+
assert adapter.get_data_objects("ci_abc.dbo") == [
557+
DataObject(
558+
catalog="ci_abc",
559+
schema="dbo",
560+
name="t",
561+
type=DataObjectType.TABLE,
562+
)
563+
]
564+
assert fetchdf.call_count == 2
565+
566+
567+
def test_drop_data_object_default_catalog_drops_without_requalifying(
568+
make_mocked_engine_adapter: t.Callable,
569+
mocker: MockerFixture,
570+
) -> None:
571+
"""DROP SQL has no warehouse. Prove it ran on ci_abc, without reconnecting."""
572+
adapter = make_mocked_engine_adapter(
573+
FabricEngineAdapter,
574+
default_catalog="ci_abc",
575+
database="ci_abc",
576+
)
577+
close = mocker.spy(adapter._connection_pool, "close")
578+
catalogs_at_execute = _record_catalogs_at_execute(adapter)
579+
580+
adapter.drop_data_object(
581+
DataObject(catalog="ci_abc", schema="dbo", name="v", type=DataObjectType.VIEW)
582+
)
583+
584+
close.assert_not_called()
585+
assert catalogs_at_execute == ["ci_abc"]
586+
assert to_sql_calls(adapter) == ["DROP VIEW IF EXISTS [dbo].[v];"]
587+
588+
589+
def test_drop_data_object_default_catalog_reconnects_when_connected_elsewhere(
590+
make_mocked_engine_adapter: t.Callable,
591+
mocker: MockerFixture,
592+
) -> None:
593+
"""DROP SQL has no warehouse. Prove it ran on ci_abc, then restored planning."""
594+
adapter = make_mocked_engine_adapter(
595+
FabricEngineAdapter,
596+
default_catalog="ci_abc",
597+
database="ci_abc",
598+
)
599+
adapter.set_current_catalog("planning")
600+
assert adapter.get_current_catalog() == "planning"
601+
close = mocker.spy(adapter._connection_pool, "close")
602+
catalogs_at_execute = _record_catalogs_at_execute(adapter)
603+
604+
adapter.drop_data_object(
605+
DataObject(catalog="ci_abc", schema="dbo", name="v", type=DataObjectType.VIEW)
606+
)
607+
608+
assert catalogs_at_execute == ["ci_abc"]
609+
assert adapter.get_current_catalog() == "planning"
610+
assert close.call_count == 2
611+
assert "DROP VIEW IF EXISTS [dbo].[v];" in to_sql_calls(adapter)

0 commit comments

Comments
 (0)