Skip to content

Commit 13312b6

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

2 files changed

Lines changed: 148 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: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ def _conflict_response(mocker: MockerFixture, error_code: str) -> t.Any:
3939
resp.json.return_value = {"errorCode": error_code}
4040
resp.raise_for_status.side_effect = requests.HTTPError(response=resp)
4141
return resp
42+
def _record_catalogs_at_execute(adapter: FabricEngineAdapter) -> t.List[t.Optional[str]]:
43+
catalogs: t.List[t.Optional[str]] = []
44+
real_execute = adapter.execute
45+
46+
def execute(*args: t.Any, **kwargs: t.Any) -> None:
47+
catalogs.append(adapter._resolved_catalog())
48+
return real_execute(*args, **kwargs)
49+
50+
adapter.execute = execute # type: ignore[method-assign]
51+
return catalogs
4252

4353

4454
def test_get_current_catalog_uses_only_explicit_target_catalog(
@@ -514,11 +524,129 @@ def test_get_data_objects_uses_default_catalog_when_current_is_none(
514524
return_value=pd.DataFrame([{"name": "test_table", "schema_name": "dbo", "type": "TABLE"}]),
515525
)
516526

517-
assert adapter._get_data_objects("dbo") == [
527+
assert adapter.get_data_objects("ci_abc.dbo") == [
518528
DataObject(
519529
catalog="ci_abc",
520530
schema="dbo",
521531
name="test_table",
522532
type=DataObjectType.TABLE,
523533
)
524534
]
535+
536+
537+
def test_get_data_objects_cache_hits_for_default_catalog(
538+
make_mocked_engine_adapter: t.Callable,
539+
mocker: MockerFixture,
540+
) -> None:
541+
"""Listing fills catalog on the return value. Cache must still hit."""
542+
adapter = make_mocked_engine_adapter(
543+
FabricEngineAdapter,
544+
default_catalog="ci_abc",
545+
database="ci_abc",
546+
patch_get_data_objects=False,
547+
)
548+
fetchdf = mocker.patch.object(
549+
adapter,
550+
"fetchdf",
551+
return_value=pd.DataFrame([{"name": "t", "schema_name": "dbo", "type": "TABLE"}]),
552+
)
553+
554+
first = adapter.get_data_objects("ci_abc.dbo", {"t"}, safe_to_cache=True)
555+
second = adapter.get_data_objects("ci_abc.dbo", {"t"}, safe_to_cache=True)
556+
557+
assert first[0].catalog == "ci_abc"
558+
assert second[0].catalog == "ci_abc"
559+
assert fetchdf.call_count == 1
560+
561+
562+
def test_get_data_objects_labels_connected_warehouse_after_lazy_restore(
563+
make_mocked_engine_adapter: t.Callable,
564+
mocker: MockerFixture,
565+
) -> None:
566+
"""Logical catalog is None; the connection is still on planning.
567+
568+
Unqualified list must be planning. ci_abc.dbo must still switch.
569+
"""
570+
adapter = make_mocked_engine_adapter(
571+
FabricEngineAdapter,
572+
default_catalog="ci_abc",
573+
database="ci_abc",
574+
patch_get_data_objects=False,
575+
)
576+
adapter.set_current_catalog("planning")
577+
adapter.set_current_catalog(None)
578+
assert adapter.get_current_catalog() is None
579+
580+
fetchdf = mocker.patch.object(
581+
adapter,
582+
"fetchdf",
583+
return_value=pd.DataFrame([{"name": "t", "schema_name": "dbo", "type": "TABLE"}]),
584+
)
585+
586+
objects = adapter.get_data_objects("dbo")
587+
588+
assert objects == [
589+
DataObject(
590+
catalog="planning",
591+
schema="dbo",
592+
name="t",
593+
type=DataObjectType.TABLE,
594+
)
595+
]
596+
597+
assert adapter.get_data_objects("ci_abc.dbo") == [
598+
DataObject(
599+
catalog="ci_abc",
600+
schema="dbo",
601+
name="t",
602+
type=DataObjectType.TABLE,
603+
)
604+
]
605+
assert fetchdf.call_count == 2
606+
607+
608+
def test_drop_data_object_default_catalog_drops_without_requalifying(
609+
make_mocked_engine_adapter: t.Callable,
610+
mocker: MockerFixture,
611+
) -> None:
612+
"""DROP SQL has no warehouse. Prove it ran on ci_abc, without reconnecting."""
613+
adapter = make_mocked_engine_adapter(
614+
FabricEngineAdapter,
615+
default_catalog="ci_abc",
616+
database="ci_abc",
617+
)
618+
close = mocker.spy(adapter._connection_pool, "close")
619+
catalogs_at_execute = _record_catalogs_at_execute(adapter)
620+
621+
adapter.drop_data_object(
622+
DataObject(catalog="ci_abc", schema="dbo", name="v", type=DataObjectType.VIEW)
623+
)
624+
625+
close.assert_not_called()
626+
assert catalogs_at_execute == ["ci_abc"]
627+
assert to_sql_calls(adapter) == ["DROP VIEW IF EXISTS [dbo].[v];"]
628+
629+
630+
def test_drop_data_object_default_catalog_reconnects_when_connected_elsewhere(
631+
make_mocked_engine_adapter: t.Callable,
632+
mocker: MockerFixture,
633+
) -> None:
634+
"""DROP SQL has no warehouse. Prove it ran on ci_abc, then restored planning."""
635+
adapter = make_mocked_engine_adapter(
636+
FabricEngineAdapter,
637+
default_catalog="ci_abc",
638+
database="ci_abc",
639+
)
640+
adapter.set_current_catalog("planning")
641+
assert adapter.get_current_catalog() == "planning"
642+
close = mocker.spy(adapter._connection_pool, "close")
643+
catalogs_at_execute = _record_catalogs_at_execute(adapter)
644+
645+
adapter.drop_data_object(
646+
DataObject(catalog="ci_abc", schema="dbo", name="v", type=DataObjectType.VIEW)
647+
)
648+
649+
assert catalogs_at_execute == ["ci_abc"]
650+
assert adapter.get_current_catalog() == "planning"
651+
assert close.call_count == 2
652+
assert "DROP VIEW IF EXISTS [dbo].[v];" in to_sql_calls(adapter)

0 commit comments

Comments
 (0)