Edit on GitHub

sqlmesh.core.engine_adapter.fabric

  1from __future__ import annotations
  2
  3import typing as t
  4import logging
  5import requests
  6import time
  7from functools import cached_property
  8from sqlglot import exp
  9from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_result
 10from sqlmesh.core.engine_adapter.mssql import MSSQLEngineAdapter
 11from sqlmesh.core.engine_adapter.shared import (
 12    CommentCreationTable,
 13    CommentCreationView,
 14    InsertOverwriteStrategy,
 15)
 16from sqlmesh.utils.errors import SQLMeshError
 17from sqlmesh.utils.connection_pool import ConnectionPool
 18from sqlmesh.core.schema_diff import TableAlterOperation
 19from sqlmesh.utils import random_id
 20
 21
 22logger = logging.getLogger(__name__)
 23
 24
 25class FabricEngineAdapter(MSSQLEngineAdapter):
 26    """
 27    Adapter for Microsoft Fabric.
 28    """
 29
 30    DIALECT = "fabric"
 31    SUPPORTS_INDEXES = False
 32    SUPPORTS_TRANSACTIONS = False
 33    SUPPORTS_CREATE_DROP_CATALOG = True
 34    INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.DELETE_INSERT
 35    # There is no standard method to handle comments in Fabric for now, so we disable it.
 36    # Otherwise, it would be inherited from MSSQL and would not work.
 37    COMMENT_CREATION_TABLE = CommentCreationTable.UNSUPPORTED
 38    COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED
 39
 40    def __init__(
 41        self, connection_factory_or_pool: t.Union[t.Callable, t.Any], *args: t.Any, **kwargs: t.Any
 42    ) -> None:
 43        # Wrap connection factory to support changing the catalog dynamically at runtime
 44        if not isinstance(connection_factory_or_pool, ConnectionPool):
 45            original_connection_factory = connection_factory_or_pool
 46
 47            connection_factory_or_pool = lambda *args, **kwargs: original_connection_factory(
 48                target_catalog=self._target_catalog, *args, **kwargs
 49            )
 50
 51        super().__init__(connection_factory_or_pool, *args, **kwargs)
 52
 53    @property
 54    def _target_catalog(self) -> t.Optional[str]:
 55        return self._connection_pool.get_attribute("target_catalog")
 56
 57    @_target_catalog.setter
 58    def _target_catalog(self, value: t.Optional[str]) -> None:
 59        self._connection_pool.set_attribute("target_catalog", value)
 60
 61    @property
 62    def api_client(self) -> FabricHttpClient:
 63        # the requests Session is not guaranteed to be threadsafe
 64        # so we create a http client per thread on demand
 65        if existing_client := self._connection_pool.get_attribute("api_client"):
 66            return existing_client
 67
 68        tenant_id: t.Optional[str] = self._extra_config.get("tenant_id")
 69        workspace_id: t.Optional[str] = self._extra_config.get("workspace_id")
 70        client_id: t.Optional[str] = self._extra_config.get("user")
 71        client_secret: t.Optional[str] = self._extra_config.get("password")
 72
 73        if not tenant_id or not client_id or not client_secret:
 74            raise SQLMeshError(
 75                "Service Principal authentication requires tenant_id, client_id, and client_secret "
 76                "in the Fabric connection configuration"
 77            )
 78
 79        if not workspace_id:
 80            raise SQLMeshError(
 81                "Fabric requires the workspace_id to be configured in the connection configuration to create / drop catalogs"
 82            )
 83
 84        client = FabricHttpClient(
 85            tenant_id=tenant_id,
 86            workspace_id=workspace_id,
 87            client_id=client_id,
 88            client_secret=client_secret,
 89        )
 90
 91        self._connection_pool.set_attribute("api_client", client)
 92        return client
 93
 94    def _create_catalog(self, catalog_name: exp.Identifier) -> None:
 95        """Create a catalog (warehouse) in Microsoft Fabric via REST API."""
 96        warehouse_name = catalog_name.sql(dialect=self.dialect, identify=False)
 97        logger.info(f"Creating Fabric warehouse: {warehouse_name}")
 98
 99        self.api_client.create_warehouse(warehouse_name)
100
101    def _drop_catalog(self, catalog_name: exp.Identifier) -> None:
102        """Drop a catalog (warehouse) in Microsoft Fabric via REST API."""
103        warehouse_name = catalog_name.sql(dialect=self.dialect, identify=False)
104        current_catalog = self.get_current_catalog()
105
106        logger.info(f"Deleting Fabric warehouse: {warehouse_name}")
107        self.api_client.delete_warehouse(warehouse_name)
108
109        if warehouse_name == current_catalog:
110            # Somewhere around 2025-09-08, Fabric started validating the "Database=" connection argument and throwing 'Authentication failed' if the database doesnt exist
111            # In addition, set_current_catalog() is implemented using a threadlocal variable "target_catalog"
112            # So, when we drop a warehouse, and there are still threads with "target_catalog" set to reference it, any operations on those threads
113            # that use an either use an existing connection pointing to this warehouse or trigger a new connection
114            # will fail with an 'Authentication Failed' error unless we close all connections here, which also clears all the threadlocal data
115            self.close()
116
117    def set_current_catalog(self, catalog_name: str) -> None:
118        """
119        Set the current catalog for Microsoft Fabric connections.
120
121        Override to handle Fabric's stateless session limitation where USE statements
122        don't persist across queries. Instead, we close existing connections and
123        recreate them with the new catalog in the connection configuration.
124
125        Args:
126            catalog_name: The name of the catalog (warehouse) to switch to
127
128        Note:
129            Fabric doesn't support catalog switching via USE statements because each
130            statement runs as an independent session. This method works around this
131            limitation by updating the connection pool with new catalog configuration.
132
133        See:
134            https://learn.microsoft.com/en-us/fabric/data-warehouse/sql-query-editor#limitations
135        """
136        current_catalog = self.get_current_catalog()
137
138        # If already using the requested catalog, do nothing
139        if current_catalog and current_catalog == catalog_name:
140            logger.debug(f"Already using catalog '{catalog_name}', no action needed")
141            return
142
143        logger.info(f"Switching from catalog '{current_catalog}' to '{catalog_name}'")
144
145        # commit the transaction before closing the connection to help prevent errors like:
146        # > Snapshot isolation transaction failed in database because the object accessed by the statement has been modified by a
147        # > DDL statement in another concurrent transaction since the start of this transaction
148        # on subsequent queries in the new connection
149        self._connection_pool.commit()
150
151        # note: we call close() on the connection pool instead of self.close() because self.close() calls close_all()
152        # on the connection pool but we just want to close the connection for this thread
153        self._connection_pool.close()
154        self._target_catalog = catalog_name  # new connections will use this catalog
155
156        catalog_after_switch = self.get_current_catalog()
157
158        if catalog_after_switch != catalog_name:
159            # We need to raise an error if the catalog switch failed to prevent the operation that needed the catalog switch from being run against the wrong catalog
160            raise SQLMeshError(
161                f"Unable to switch catalog to {catalog_name}, catalog ended up as {catalog_after_switch}"
162            )
163
164    def alter_table(
165        self, alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]]
166    ) -> None:
167        """
168        Applies alter expressions to a table. Fabric has limited support for ALTER TABLE,
169        so this method implements a workaround for column type changes.
170        This method is self-contained and sets its own catalog context.
171        """
172        if not alter_expressions:
173            return
174
175        # Get the target table from the first expression to determine the correct catalog.
176        first_op = alter_expressions[0]
177        expression = first_op.expression if isinstance(first_op, TableAlterOperation) else first_op
178        if not isinstance(expression, exp.Alter) or not expression.this.catalog:
179            # Fallback for unexpected scenarios
180            logger.warning(
181                "Could not determine catalog from alter expression, executing with current context."
182            )
183            super().alter_table(alter_expressions)
184            return
185
186        target_catalog = expression.this.catalog
187        self.set_current_catalog(target_catalog)
188
189        with self.transaction():
190            for op in alter_expressions:
191                expression = op.expression if isinstance(op, TableAlterOperation) else op
192
193                if not isinstance(expression, exp.Alter):
194                    self.execute(expression)
195                    continue
196
197                for action in expression.actions:
198                    table_name = expression.this
199
200                    table_name_without_catalog = table_name.copy()
201                    table_name_without_catalog.set("catalog", None)
202
203                    is_type_change = isinstance(action, exp.AlterColumn) and action.args.get(
204                        "dtype"
205                    )
206
207                    if is_type_change:
208                        column_to_alter = action.this
209                        new_type = action.args["dtype"]
210                        temp_column_name_str = f"{column_to_alter.name}__{random_id(short=True)}"
211                        temp_column_name = exp.to_identifier(temp_column_name_str)
212
213                        logger.info(
214                            "Applying workaround for column '%s' on table '%s' to change type to '%s'.",
215                            column_to_alter.sql(),
216                            table_name.sql(),
217                            new_type.sql(),
218                        )
219
220                        # Step 1: Add a temporary column.
221                        add_column_expr = exp.Alter(
222                            this=table_name_without_catalog.copy(),
223                            kind="TABLE",
224                            actions=[
225                                exp.ColumnDef(this=temp_column_name.copy(), kind=new_type.copy())
226                            ],
227                        )
228                        add_sql = self._to_sql(add_column_expr)
229                        self.execute(add_sql)
230
231                        # Step 2: Copy and cast data.
232                        update_sql = self._to_sql(
233                            exp.Update(
234                                this=table_name_without_catalog.copy(),
235                                expressions=[
236                                    exp.EQ(
237                                        this=temp_column_name.copy(),
238                                        expression=exp.Cast(
239                                            this=column_to_alter.copy(), to=new_type.copy()
240                                        ),
241                                    )
242                                ],
243                            )
244                        )
245                        self.execute(update_sql)
246
247                        # Step 3: Drop the original column.
248                        drop_sql = self._to_sql(
249                            exp.Alter(
250                                this=table_name_without_catalog.copy(),
251                                kind="TABLE",
252                                actions=[exp.Drop(this=column_to_alter.copy(), kind="COLUMN")],
253                            )
254                        )
255                        self.execute(drop_sql)
256
257                        # Step 4: Rename the temporary column.
258                        old_name_qualified = f"{table_name_without_catalog.sql(dialect=self.dialect)}.{temp_column_name.sql(dialect=self.dialect)}"
259                        new_name_unquoted = column_to_alter.sql(
260                            dialect=self.dialect, identify=False
261                        )
262                        rename_sql = f"EXEC sp_rename '{old_name_qualified}', '{new_name_unquoted}', 'COLUMN'"
263                        self.execute(rename_sql)
264                    else:
265                        # For other alterations, execute directly.
266                        direct_alter_expr = exp.Alter(
267                            this=table_name_without_catalog.copy(), kind="TABLE", actions=[action]
268                        )
269                        self.execute(direct_alter_expr)
270
271
272class FabricHttpClient:
273    def __init__(self, tenant_id: str, workspace_id: str, client_id: str, client_secret: str):
274        self.tenant_id = tenant_id
275        self.client_id = client_id
276        self.client_secret = client_secret
277        self.workspace_id = workspace_id
278
279    def create_warehouse(
280        self, warehouse_name: str, if_not_exists: bool = True, attempt: int = 0
281    ) -> None:
282        """Create a catalog (warehouse) in Microsoft Fabric via REST API."""
283
284        # attempt count is arbitrary, it essentially equates to 5 minutes of 30 second waits
285        if attempt > 10:
286            raise SQLMeshError(
287                f"Gave up waiting for Fabric warehouse {warehouse_name} to become available"
288            )
289
290        logger.info(f"Creating Fabric warehouse: {warehouse_name}")
291
292        request_data = {
293            "displayName": warehouse_name,
294            "description": f"Warehouse created by SQLMesh: {warehouse_name}",
295        }
296
297        response = self.session.post(self._endpoint_url("warehouses"), json=request_data)
298
299        if (
300            if_not_exists
301            and response.status_code == 400
302            and (errorCode := response.json().get("errorCode", None))
303        ):
304            if errorCode == "ItemDisplayNameAlreadyInUse":
305                logger.warning(f"Fabric warehouse {warehouse_name} already exists")
306                return
307            if errorCode == "ItemDisplayNameNotAvailableYet":
308                logger.warning(f"Fabric warehouse {warehouse_name} is still spinning up; waiting")
309                # Fabric error message is something like:
310                #  - "Requested 'circleci_51d7087e__dev' is not available yet and is expected to become available in the upcoming minutes."
311                # This seems to happen if a catalog is dropped and then a new one with the same name is immediately created.
312                # There appears to be some delayed async process on the Fabric side that actually drops the warehouses and frees up the names to be used again
313                time.sleep(30)
314                return self.create_warehouse(
315                    warehouse_name=warehouse_name, if_not_exists=if_not_exists, attempt=attempt + 1
316                )
317
318        try:
319            response.raise_for_status()
320        except:
321            # the important information to actually debug anything is in the response body which Requests never prints
322            logger.exception(
323                f"Failed to create warehouse {warehouse_name}. status: {response.status_code}, body: {response.text}"
324            )
325            raise
326
327        # Handle direct success (201) or async creation (202)
328        if response.status_code == 201:
329            logger.info(f"Successfully created Fabric warehouse: {warehouse_name}")
330            return
331
332        if response.status_code == 202 and (location_header := response.headers.get("location")):
333            logger.info(f"Warehouse creation initiated for: {warehouse_name}")
334            self._wait_for_completion(location_header, warehouse_name)
335            logger.info(f"Successfully created Fabric warehouse: {warehouse_name}")
336        else:
337            logger.error(f"Unexpected response from Fabric API: {response}\n{response.text}")
338            raise SQLMeshError(f"Unable to create warehouse: {response}")
339
340    def delete_warehouse(self, warehouse_name: str, if_exists: bool = True) -> None:
341        """Drop a catalog (warehouse) in Microsoft Fabric via REST API."""
342        logger.info(f"Deleting Fabric warehouse: {warehouse_name}")
343
344        # Get the warehouse ID by listing warehouses
345        # TODO: handle continuationUri for pagination, ref: https://learn.microsoft.com/en-us/rest/api/fabric/warehouse/items/list-warehouses?tabs=HTTP#warehouses
346        response = self.session.get(self._endpoint_url("warehouses"))
347        response.raise_for_status()
348
349        warehouse_name_to_id = {
350            warehouse.get("displayName"): warehouse.get("id")
351            for warehouse in response.json().get("value", [])
352        }
353
354        warehouse_id = warehouse_name_to_id.get(warehouse_name, None)
355
356        if not warehouse_id:
357            logger.warning(
358                f"Fabric warehouse does not exist: {warehouse_name}\n(available warehouses: {', '.join(warehouse_name_to_id)})"
359            )
360            if if_exists:
361                return
362
363            raise SQLMeshError(
364                f"Unable to delete Fabric warehouse {warehouse_name} as it doesnt exist"
365            )
366
367        # Delete the warehouse by ID
368        response = self.session.delete(self._endpoint_url(f"warehouses/{warehouse_id}"))
369        response.raise_for_status()
370
371        logger.info(f"Successfully deleted Fabric warehouse: {warehouse_name}")
372
373    @cached_property
374    def session(self) -> requests.Session:
375        s = requests.Session()
376
377        access_token = self._get_access_token()
378        s.headers.update({"Authorization": f"Bearer {access_token}"})
379
380        return s
381
382    def _endpoint_url(self, endpoint: str) -> str:
383        if endpoint.startswith("/"):
384            endpoint = endpoint[1:]
385
386        return f"https://api.fabric.microsoft.com/v1/workspaces/{self.workspace_id}/{endpoint}"
387
388    def _get_access_token(self) -> str:
389        """Get access token using Service Principal authentication."""
390
391        # Use Azure AD OAuth2 token endpoint
392        token_url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
393
394        data = {
395            "grant_type": "client_credentials",
396            "client_id": self.client_id,
397            "client_secret": self.client_secret,
398            "scope": "https://api.fabric.microsoft.com/.default",
399        }
400
401        response = requests.post(token_url, data=data)
402        response.raise_for_status()
403        token_data = response.json()
404        return token_data["access_token"]
405
406    def _wait_for_completion(self, location_url: str, operation_name: str) -> None:
407        """Poll the operation status until completion."""
408
409        @retry(
410            wait=wait_exponential(multiplier=1, min=1, max=30),
411            stop=stop_after_attempt(20),
412            retry=retry_if_result(lambda result: result not in ["Succeeded", "Failed"]),
413        )
414        def _poll() -> str:
415            response = self.session.get(location_url)
416            response.raise_for_status()
417
418            result = response.json()
419            status = result.get("status", "Unknown")
420
421            logger.debug(f"Operation {operation_name} status: {status}")
422
423            if status == "Failed":
424                error_msg = result.get("error", {}).get("message", "Unknown error")
425                raise SQLMeshError(f"Operation {operation_name} failed: {error_msg}")
426            elif status in ["InProgress", "Running"]:
427                logger.debug(f"Operation {operation_name} still in progress...")
428            elif status not in ["Succeeded"]:
429                logger.warning(f"Unknown status '{status}' for operation {operation_name}")
430
431            return status
432
433        final_status = _poll()
434        if final_status != "Succeeded":
435            raise SQLMeshError(f"Operation {operation_name} completed with status: {final_status}")
logger = <Logger sqlmesh.core.engine_adapter.fabric (WARNING)>
class FabricEngineAdapter(sqlmesh.core.engine_adapter.mssql.MSSQLEngineAdapter):
 26class FabricEngineAdapter(MSSQLEngineAdapter):
 27    """
 28    Adapter for Microsoft Fabric.
 29    """
 30
 31    DIALECT = "fabric"
 32    SUPPORTS_INDEXES = False
 33    SUPPORTS_TRANSACTIONS = False
 34    SUPPORTS_CREATE_DROP_CATALOG = True
 35    INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.DELETE_INSERT
 36    # There is no standard method to handle comments in Fabric for now, so we disable it.
 37    # Otherwise, it would be inherited from MSSQL and would not work.
 38    COMMENT_CREATION_TABLE = CommentCreationTable.UNSUPPORTED
 39    COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED
 40
 41    def __init__(
 42        self, connection_factory_or_pool: t.Union[t.Callable, t.Any], *args: t.Any, **kwargs: t.Any
 43    ) -> None:
 44        # Wrap connection factory to support changing the catalog dynamically at runtime
 45        if not isinstance(connection_factory_or_pool, ConnectionPool):
 46            original_connection_factory = connection_factory_or_pool
 47
 48            connection_factory_or_pool = lambda *args, **kwargs: original_connection_factory(
 49                target_catalog=self._target_catalog, *args, **kwargs
 50            )
 51
 52        super().__init__(connection_factory_or_pool, *args, **kwargs)
 53
 54    @property
 55    def _target_catalog(self) -> t.Optional[str]:
 56        return self._connection_pool.get_attribute("target_catalog")
 57
 58    @_target_catalog.setter
 59    def _target_catalog(self, value: t.Optional[str]) -> None:
 60        self._connection_pool.set_attribute("target_catalog", value)
 61
 62    @property
 63    def api_client(self) -> FabricHttpClient:
 64        # the requests Session is not guaranteed to be threadsafe
 65        # so we create a http client per thread on demand
 66        if existing_client := self._connection_pool.get_attribute("api_client"):
 67            return existing_client
 68
 69        tenant_id: t.Optional[str] = self._extra_config.get("tenant_id")
 70        workspace_id: t.Optional[str] = self._extra_config.get("workspace_id")
 71        client_id: t.Optional[str] = self._extra_config.get("user")
 72        client_secret: t.Optional[str] = self._extra_config.get("password")
 73
 74        if not tenant_id or not client_id or not client_secret:
 75            raise SQLMeshError(
 76                "Service Principal authentication requires tenant_id, client_id, and client_secret "
 77                "in the Fabric connection configuration"
 78            )
 79
 80        if not workspace_id:
 81            raise SQLMeshError(
 82                "Fabric requires the workspace_id to be configured in the connection configuration to create / drop catalogs"
 83            )
 84
 85        client = FabricHttpClient(
 86            tenant_id=tenant_id,
 87            workspace_id=workspace_id,
 88            client_id=client_id,
 89            client_secret=client_secret,
 90        )
 91
 92        self._connection_pool.set_attribute("api_client", client)
 93        return client
 94
 95    def _create_catalog(self, catalog_name: exp.Identifier) -> None:
 96        """Create a catalog (warehouse) in Microsoft Fabric via REST API."""
 97        warehouse_name = catalog_name.sql(dialect=self.dialect, identify=False)
 98        logger.info(f"Creating Fabric warehouse: {warehouse_name}")
 99
100        self.api_client.create_warehouse(warehouse_name)
101
102    def _drop_catalog(self, catalog_name: exp.Identifier) -> None:
103        """Drop a catalog (warehouse) in Microsoft Fabric via REST API."""
104        warehouse_name = catalog_name.sql(dialect=self.dialect, identify=False)
105        current_catalog = self.get_current_catalog()
106
107        logger.info(f"Deleting Fabric warehouse: {warehouse_name}")
108        self.api_client.delete_warehouse(warehouse_name)
109
110        if warehouse_name == current_catalog:
111            # Somewhere around 2025-09-08, Fabric started validating the "Database=" connection argument and throwing 'Authentication failed' if the database doesnt exist
112            # In addition, set_current_catalog() is implemented using a threadlocal variable "target_catalog"
113            # So, when we drop a warehouse, and there are still threads with "target_catalog" set to reference it, any operations on those threads
114            # that use an either use an existing connection pointing to this warehouse or trigger a new connection
115            # will fail with an 'Authentication Failed' error unless we close all connections here, which also clears all the threadlocal data
116            self.close()
117
118    def set_current_catalog(self, catalog_name: str) -> None:
119        """
120        Set the current catalog for Microsoft Fabric connections.
121
122        Override to handle Fabric's stateless session limitation where USE statements
123        don't persist across queries. Instead, we close existing connections and
124        recreate them with the new catalog in the connection configuration.
125
126        Args:
127            catalog_name: The name of the catalog (warehouse) to switch to
128
129        Note:
130            Fabric doesn't support catalog switching via USE statements because each
131            statement runs as an independent session. This method works around this
132            limitation by updating the connection pool with new catalog configuration.
133
134        See:
135            https://learn.microsoft.com/en-us/fabric/data-warehouse/sql-query-editor#limitations
136        """
137        current_catalog = self.get_current_catalog()
138
139        # If already using the requested catalog, do nothing
140        if current_catalog and current_catalog == catalog_name:
141            logger.debug(f"Already using catalog '{catalog_name}', no action needed")
142            return
143
144        logger.info(f"Switching from catalog '{current_catalog}' to '{catalog_name}'")
145
146        # commit the transaction before closing the connection to help prevent errors like:
147        # > Snapshot isolation transaction failed in database because the object accessed by the statement has been modified by a
148        # > DDL statement in another concurrent transaction since the start of this transaction
149        # on subsequent queries in the new connection
150        self._connection_pool.commit()
151
152        # note: we call close() on the connection pool instead of self.close() because self.close() calls close_all()
153        # on the connection pool but we just want to close the connection for this thread
154        self._connection_pool.close()
155        self._target_catalog = catalog_name  # new connections will use this catalog
156
157        catalog_after_switch = self.get_current_catalog()
158
159        if catalog_after_switch != catalog_name:
160            # We need to raise an error if the catalog switch failed to prevent the operation that needed the catalog switch from being run against the wrong catalog
161            raise SQLMeshError(
162                f"Unable to switch catalog to {catalog_name}, catalog ended up as {catalog_after_switch}"
163            )
164
165    def alter_table(
166        self, alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]]
167    ) -> None:
168        """
169        Applies alter expressions to a table. Fabric has limited support for ALTER TABLE,
170        so this method implements a workaround for column type changes.
171        This method is self-contained and sets its own catalog context.
172        """
173        if not alter_expressions:
174            return
175
176        # Get the target table from the first expression to determine the correct catalog.
177        first_op = alter_expressions[0]
178        expression = first_op.expression if isinstance(first_op, TableAlterOperation) else first_op
179        if not isinstance(expression, exp.Alter) or not expression.this.catalog:
180            # Fallback for unexpected scenarios
181            logger.warning(
182                "Could not determine catalog from alter expression, executing with current context."
183            )
184            super().alter_table(alter_expressions)
185            return
186
187        target_catalog = expression.this.catalog
188        self.set_current_catalog(target_catalog)
189
190        with self.transaction():
191            for op in alter_expressions:
192                expression = op.expression if isinstance(op, TableAlterOperation) else op
193
194                if not isinstance(expression, exp.Alter):
195                    self.execute(expression)
196                    continue
197
198                for action in expression.actions:
199                    table_name = expression.this
200
201                    table_name_without_catalog = table_name.copy()
202                    table_name_without_catalog.set("catalog", None)
203
204                    is_type_change = isinstance(action, exp.AlterColumn) and action.args.get(
205                        "dtype"
206                    )
207
208                    if is_type_change:
209                        column_to_alter = action.this
210                        new_type = action.args["dtype"]
211                        temp_column_name_str = f"{column_to_alter.name}__{random_id(short=True)}"
212                        temp_column_name = exp.to_identifier(temp_column_name_str)
213
214                        logger.info(
215                            "Applying workaround for column '%s' on table '%s' to change type to '%s'.",
216                            column_to_alter.sql(),
217                            table_name.sql(),
218                            new_type.sql(),
219                        )
220
221                        # Step 1: Add a temporary column.
222                        add_column_expr = exp.Alter(
223                            this=table_name_without_catalog.copy(),
224                            kind="TABLE",
225                            actions=[
226                                exp.ColumnDef(this=temp_column_name.copy(), kind=new_type.copy())
227                            ],
228                        )
229                        add_sql = self._to_sql(add_column_expr)
230                        self.execute(add_sql)
231
232                        # Step 2: Copy and cast data.
233                        update_sql = self._to_sql(
234                            exp.Update(
235                                this=table_name_without_catalog.copy(),
236                                expressions=[
237                                    exp.EQ(
238                                        this=temp_column_name.copy(),
239                                        expression=exp.Cast(
240                                            this=column_to_alter.copy(), to=new_type.copy()
241                                        ),
242                                    )
243                                ],
244                            )
245                        )
246                        self.execute(update_sql)
247
248                        # Step 3: Drop the original column.
249                        drop_sql = self._to_sql(
250                            exp.Alter(
251                                this=table_name_without_catalog.copy(),
252                                kind="TABLE",
253                                actions=[exp.Drop(this=column_to_alter.copy(), kind="COLUMN")],
254                            )
255                        )
256                        self.execute(drop_sql)
257
258                        # Step 4: Rename the temporary column.
259                        old_name_qualified = f"{table_name_without_catalog.sql(dialect=self.dialect)}.{temp_column_name.sql(dialect=self.dialect)}"
260                        new_name_unquoted = column_to_alter.sql(
261                            dialect=self.dialect, identify=False
262                        )
263                        rename_sql = f"EXEC sp_rename '{old_name_qualified}', '{new_name_unquoted}', 'COLUMN'"
264                        self.execute(rename_sql)
265                    else:
266                        # For other alterations, execute directly.
267                        direct_alter_expr = exp.Alter(
268                            this=table_name_without_catalog.copy(), kind="TABLE", actions=[action]
269                        )
270                        self.execute(direct_alter_expr)

Adapter for Microsoft Fabric.

FabricEngineAdapter( connection_factory_or_pool: Union[Callable, Any], *args: Any, **kwargs: Any)
41    def __init__(
42        self, connection_factory_or_pool: t.Union[t.Callable, t.Any], *args: t.Any, **kwargs: t.Any
43    ) -> None:
44        # Wrap connection factory to support changing the catalog dynamically at runtime
45        if not isinstance(connection_factory_or_pool, ConnectionPool):
46            original_connection_factory = connection_factory_or_pool
47
48            connection_factory_or_pool = lambda *args, **kwargs: original_connection_factory(
49                target_catalog=self._target_catalog, *args, **kwargs
50            )
51
52        super().__init__(connection_factory_or_pool, *args, **kwargs)
DIALECT = 'fabric'
SUPPORTS_INDEXES = False
SUPPORTS_TRANSACTIONS = False
SUPPORTS_CREATE_DROP_CATALOG = True
INSERT_OVERWRITE_STRATEGY = <InsertOverwriteStrategy.DELETE_INSERT: 1>
COMMENT_CREATION_TABLE = <CommentCreationTable.UNSUPPORTED: 1>
COMMENT_CREATION_VIEW = <CommentCreationView.UNSUPPORTED: 1>
api_client: FabricHttpClient
62    @property
63    def api_client(self) -> FabricHttpClient:
64        # the requests Session is not guaranteed to be threadsafe
65        # so we create a http client per thread on demand
66        if existing_client := self._connection_pool.get_attribute("api_client"):
67            return existing_client
68
69        tenant_id: t.Optional[str] = self._extra_config.get("tenant_id")
70        workspace_id: t.Optional[str] = self._extra_config.get("workspace_id")
71        client_id: t.Optional[str] = self._extra_config.get("user")
72        client_secret: t.Optional[str] = self._extra_config.get("password")
73
74        if not tenant_id or not client_id or not client_secret:
75            raise SQLMeshError(
76                "Service Principal authentication requires tenant_id, client_id, and client_secret "
77                "in the Fabric connection configuration"
78            )
79
80        if not workspace_id:
81            raise SQLMeshError(
82                "Fabric requires the workspace_id to be configured in the connection configuration to create / drop catalogs"
83            )
84
85        client = FabricHttpClient(
86            tenant_id=tenant_id,
87            workspace_id=workspace_id,
88            client_id=client_id,
89            client_secret=client_secret,
90        )
91
92        self._connection_pool.set_attribute("api_client", client)
93        return client
def set_current_catalog(self, catalog_name: str) -> None:
118    def set_current_catalog(self, catalog_name: str) -> None:
119        """
120        Set the current catalog for Microsoft Fabric connections.
121
122        Override to handle Fabric's stateless session limitation where USE statements
123        don't persist across queries. Instead, we close existing connections and
124        recreate them with the new catalog in the connection configuration.
125
126        Args:
127            catalog_name: The name of the catalog (warehouse) to switch to
128
129        Note:
130            Fabric doesn't support catalog switching via USE statements because each
131            statement runs as an independent session. This method works around this
132            limitation by updating the connection pool with new catalog configuration.
133
134        See:
135            https://learn.microsoft.com/en-us/fabric/data-warehouse/sql-query-editor#limitations
136        """
137        current_catalog = self.get_current_catalog()
138
139        # If already using the requested catalog, do nothing
140        if current_catalog and current_catalog == catalog_name:
141            logger.debug(f"Already using catalog '{catalog_name}', no action needed")
142            return
143
144        logger.info(f"Switching from catalog '{current_catalog}' to '{catalog_name}'")
145
146        # commit the transaction before closing the connection to help prevent errors like:
147        # > Snapshot isolation transaction failed in database because the object accessed by the statement has been modified by a
148        # > DDL statement in another concurrent transaction since the start of this transaction
149        # on subsequent queries in the new connection
150        self._connection_pool.commit()
151
152        # note: we call close() on the connection pool instead of self.close() because self.close() calls close_all()
153        # on the connection pool but we just want to close the connection for this thread
154        self._connection_pool.close()
155        self._target_catalog = catalog_name  # new connections will use this catalog
156
157        catalog_after_switch = self.get_current_catalog()
158
159        if catalog_after_switch != catalog_name:
160            # We need to raise an error if the catalog switch failed to prevent the operation that needed the catalog switch from being run against the wrong catalog
161            raise SQLMeshError(
162                f"Unable to switch catalog to {catalog_name}, catalog ended up as {catalog_after_switch}"
163            )

Set the current catalog for Microsoft Fabric connections.

Override to handle Fabric's stateless session limitation where USE statements don't persist across queries. Instead, we close existing connections and recreate them with the new catalog in the connection configuration.

Arguments:
  • catalog_name: The name of the catalog (warehouse) to switch to
Note:

Fabric doesn't support catalog switching via USE statements because each statement runs as an independent session. This method works around this limitation by updating the connection pool with new catalog configuration.

See:

https://learn.microsoft.com/en-us/fabric/data-warehouse/sql-query-editor#limitations

def alter_table( self, alter_expressions: Union[List[sqlglot.expressions.ddl.Alter], List[sqlmesh.core.schema_diff.TableAlterOperation]]) -> None:
165    def alter_table(
166        self, alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]]
167    ) -> None:
168        """
169        Applies alter expressions to a table. Fabric has limited support for ALTER TABLE,
170        so this method implements a workaround for column type changes.
171        This method is self-contained and sets its own catalog context.
172        """
173        if not alter_expressions:
174            return
175
176        # Get the target table from the first expression to determine the correct catalog.
177        first_op = alter_expressions[0]
178        expression = first_op.expression if isinstance(first_op, TableAlterOperation) else first_op
179        if not isinstance(expression, exp.Alter) or not expression.this.catalog:
180            # Fallback for unexpected scenarios
181            logger.warning(
182                "Could not determine catalog from alter expression, executing with current context."
183            )
184            super().alter_table(alter_expressions)
185            return
186
187        target_catalog = expression.this.catalog
188        self.set_current_catalog(target_catalog)
189
190        with self.transaction():
191            for op in alter_expressions:
192                expression = op.expression if isinstance(op, TableAlterOperation) else op
193
194                if not isinstance(expression, exp.Alter):
195                    self.execute(expression)
196                    continue
197
198                for action in expression.actions:
199                    table_name = expression.this
200
201                    table_name_without_catalog = table_name.copy()
202                    table_name_without_catalog.set("catalog", None)
203
204                    is_type_change = isinstance(action, exp.AlterColumn) and action.args.get(
205                        "dtype"
206                    )
207
208                    if is_type_change:
209                        column_to_alter = action.this
210                        new_type = action.args["dtype"]
211                        temp_column_name_str = f"{column_to_alter.name}__{random_id(short=True)}"
212                        temp_column_name = exp.to_identifier(temp_column_name_str)
213
214                        logger.info(
215                            "Applying workaround for column '%s' on table '%s' to change type to '%s'.",
216                            column_to_alter.sql(),
217                            table_name.sql(),
218                            new_type.sql(),
219                        )
220
221                        # Step 1: Add a temporary column.
222                        add_column_expr = exp.Alter(
223                            this=table_name_without_catalog.copy(),
224                            kind="TABLE",
225                            actions=[
226                                exp.ColumnDef(this=temp_column_name.copy(), kind=new_type.copy())
227                            ],
228                        )
229                        add_sql = self._to_sql(add_column_expr)
230                        self.execute(add_sql)
231
232                        # Step 2: Copy and cast data.
233                        update_sql = self._to_sql(
234                            exp.Update(
235                                this=table_name_without_catalog.copy(),
236                                expressions=[
237                                    exp.EQ(
238                                        this=temp_column_name.copy(),
239                                        expression=exp.Cast(
240                                            this=column_to_alter.copy(), to=new_type.copy()
241                                        ),
242                                    )
243                                ],
244                            )
245                        )
246                        self.execute(update_sql)
247
248                        # Step 3: Drop the original column.
249                        drop_sql = self._to_sql(
250                            exp.Alter(
251                                this=table_name_without_catalog.copy(),
252                                kind="TABLE",
253                                actions=[exp.Drop(this=column_to_alter.copy(), kind="COLUMN")],
254                            )
255                        )
256                        self.execute(drop_sql)
257
258                        # Step 4: Rename the temporary column.
259                        old_name_qualified = f"{table_name_without_catalog.sql(dialect=self.dialect)}.{temp_column_name.sql(dialect=self.dialect)}"
260                        new_name_unquoted = column_to_alter.sql(
261                            dialect=self.dialect, identify=False
262                        )
263                        rename_sql = f"EXEC sp_rename '{old_name_qualified}', '{new_name_unquoted}', 'COLUMN'"
264                        self.execute(rename_sql)
265                    else:
266                        # For other alterations, execute directly.
267                        direct_alter_expr = exp.Alter(
268                            this=table_name_without_catalog.copy(), kind="TABLE", actions=[action]
269                        )
270                        self.execute(direct_alter_expr)

Applies alter expressions to a table. Fabric has limited support for ALTER TABLE, so this method implements a workaround for column type changes. This method is self-contained and sets its own catalog context.

Inherited Members
sqlmesh.core.engine_adapter.mssql.MSSQLEngineAdapter
SUPPORTS_TUPLE_IN
SUPPORTS_MATERIALIZED_VIEWS
CURRENT_CATALOG_EXPRESSION
SUPPORTS_REPLACE_TABLE
MAX_IDENTIFIER_LENGTH
SUPPORTS_QUERY_EXECUTION_TRACKING
SCHEMA_DIFFER_KWARGS
VARIABLE_LENGTH_DATA_TYPES
catalog_support
columns
table_exists
drop_schema
merge
delete_from
sqlmesh.core.engine_adapter.mixins.GetCurrentCatalogFromFunctionMixin
get_current_catalog
sqlmesh.core.engine_adapter.mixins.RowDiffMixin
MAX_TIMESTAMP_PRECISION
concat_columns
normalize_value
sqlmesh.core.engine_adapter.base.EngineAdapter
DEFAULT_BATCH_SIZE
DATA_OBJECT_FILTER_BATCH_SIZE
MAX_TABLE_COMMENT_LENGTH
MAX_COLUMN_COMMENT_LENGTH
SUPPORTS_MATERIALIZED_VIEW_SCHEMA
SUPPORTS_VIEW_SCHEMA
SUPPORTS_CLONING
SUPPORTS_MANAGED_MODELS
SUPPORTED_DROP_CASCADE_OBJECT_KINDS
HAS_VIEW_BINDING
RECREATE_MATERIALIZED_VIEW_ON_EVALUATION
SUPPORTS_GRANTS
DEFAULT_CATALOG_TYPE
QUOTE_IDENTIFIERS_IN_VIEWS
ATTACH_CORRELATION_ID
SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS
RESOLVE_TABLE_REFS_IN_PHYSICAL_PROPERTIES
dialect
correlation_id
with_settings
cursor
connection
spark
snowpark
bigframe
comments_enabled
supports_virtual_catalog
inject_virtual_catalog
schema_differ
default_catalog
engine_run_mode
recycle
close
get_catalog_type
get_catalog_type_from_table
current_catalog_type
replace_query
create_index
create_table
create_managed_table
ctas
create_state_table
create_table_like
clone_table
drop_data_object
drop_table
drop_managed_table
get_alter_operations
create_view
create_schema
drop_view
create_catalog
drop_catalog
insert_append
insert_overwrite_by_partition
insert_overwrite_by_time_partition
update_table
scd_type_2_by_time
scd_type_2_by_column
rename_table
get_data_object
get_data_objects
fetchone
fetchall
fetchdf
fetch_pyspark_df
wap_enabled
wap_supported
wap_table_name
wap_prepare
wap_publish
sync_grants_config
transaction
session
execute
temp_table
adjust_physical_properties_for_incremental
drop_data_object_on_type_mismatch
ensure_nulls_for_unmatched_after_join
use_server_nulls_for_unmatched_after_join
ping
get_table_last_modified_ts
class FabricHttpClient:
273class FabricHttpClient:
274    def __init__(self, tenant_id: str, workspace_id: str, client_id: str, client_secret: str):
275        self.tenant_id = tenant_id
276        self.client_id = client_id
277        self.client_secret = client_secret
278        self.workspace_id = workspace_id
279
280    def create_warehouse(
281        self, warehouse_name: str, if_not_exists: bool = True, attempt: int = 0
282    ) -> None:
283        """Create a catalog (warehouse) in Microsoft Fabric via REST API."""
284
285        # attempt count is arbitrary, it essentially equates to 5 minutes of 30 second waits
286        if attempt > 10:
287            raise SQLMeshError(
288                f"Gave up waiting for Fabric warehouse {warehouse_name} to become available"
289            )
290
291        logger.info(f"Creating Fabric warehouse: {warehouse_name}")
292
293        request_data = {
294            "displayName": warehouse_name,
295            "description": f"Warehouse created by SQLMesh: {warehouse_name}",
296        }
297
298        response = self.session.post(self._endpoint_url("warehouses"), json=request_data)
299
300        if (
301            if_not_exists
302            and response.status_code == 400
303            and (errorCode := response.json().get("errorCode", None))
304        ):
305            if errorCode == "ItemDisplayNameAlreadyInUse":
306                logger.warning(f"Fabric warehouse {warehouse_name} already exists")
307                return
308            if errorCode == "ItemDisplayNameNotAvailableYet":
309                logger.warning(f"Fabric warehouse {warehouse_name} is still spinning up; waiting")
310                # Fabric error message is something like:
311                #  - "Requested 'circleci_51d7087e__dev' is not available yet and is expected to become available in the upcoming minutes."
312                # This seems to happen if a catalog is dropped and then a new one with the same name is immediately created.
313                # There appears to be some delayed async process on the Fabric side that actually drops the warehouses and frees up the names to be used again
314                time.sleep(30)
315                return self.create_warehouse(
316                    warehouse_name=warehouse_name, if_not_exists=if_not_exists, attempt=attempt + 1
317                )
318
319        try:
320            response.raise_for_status()
321        except:
322            # the important information to actually debug anything is in the response body which Requests never prints
323            logger.exception(
324                f"Failed to create warehouse {warehouse_name}. status: {response.status_code}, body: {response.text}"
325            )
326            raise
327
328        # Handle direct success (201) or async creation (202)
329        if response.status_code == 201:
330            logger.info(f"Successfully created Fabric warehouse: {warehouse_name}")
331            return
332
333        if response.status_code == 202 and (location_header := response.headers.get("location")):
334            logger.info(f"Warehouse creation initiated for: {warehouse_name}")
335            self._wait_for_completion(location_header, warehouse_name)
336            logger.info(f"Successfully created Fabric warehouse: {warehouse_name}")
337        else:
338            logger.error(f"Unexpected response from Fabric API: {response}\n{response.text}")
339            raise SQLMeshError(f"Unable to create warehouse: {response}")
340
341    def delete_warehouse(self, warehouse_name: str, if_exists: bool = True) -> None:
342        """Drop a catalog (warehouse) in Microsoft Fabric via REST API."""
343        logger.info(f"Deleting Fabric warehouse: {warehouse_name}")
344
345        # Get the warehouse ID by listing warehouses
346        # TODO: handle continuationUri for pagination, ref: https://learn.microsoft.com/en-us/rest/api/fabric/warehouse/items/list-warehouses?tabs=HTTP#warehouses
347        response = self.session.get(self._endpoint_url("warehouses"))
348        response.raise_for_status()
349
350        warehouse_name_to_id = {
351            warehouse.get("displayName"): warehouse.get("id")
352            for warehouse in response.json().get("value", [])
353        }
354
355        warehouse_id = warehouse_name_to_id.get(warehouse_name, None)
356
357        if not warehouse_id:
358            logger.warning(
359                f"Fabric warehouse does not exist: {warehouse_name}\n(available warehouses: {', '.join(warehouse_name_to_id)})"
360            )
361            if if_exists:
362                return
363
364            raise SQLMeshError(
365                f"Unable to delete Fabric warehouse {warehouse_name} as it doesnt exist"
366            )
367
368        # Delete the warehouse by ID
369        response = self.session.delete(self._endpoint_url(f"warehouses/{warehouse_id}"))
370        response.raise_for_status()
371
372        logger.info(f"Successfully deleted Fabric warehouse: {warehouse_name}")
373
374    @cached_property
375    def session(self) -> requests.Session:
376        s = requests.Session()
377
378        access_token = self._get_access_token()
379        s.headers.update({"Authorization": f"Bearer {access_token}"})
380
381        return s
382
383    def _endpoint_url(self, endpoint: str) -> str:
384        if endpoint.startswith("/"):
385            endpoint = endpoint[1:]
386
387        return f"https://api.fabric.microsoft.com/v1/workspaces/{self.workspace_id}/{endpoint}"
388
389    def _get_access_token(self) -> str:
390        """Get access token using Service Principal authentication."""
391
392        # Use Azure AD OAuth2 token endpoint
393        token_url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
394
395        data = {
396            "grant_type": "client_credentials",
397            "client_id": self.client_id,
398            "client_secret": self.client_secret,
399            "scope": "https://api.fabric.microsoft.com/.default",
400        }
401
402        response = requests.post(token_url, data=data)
403        response.raise_for_status()
404        token_data = response.json()
405        return token_data["access_token"]
406
407    def _wait_for_completion(self, location_url: str, operation_name: str) -> None:
408        """Poll the operation status until completion."""
409
410        @retry(
411            wait=wait_exponential(multiplier=1, min=1, max=30),
412            stop=stop_after_attempt(20),
413            retry=retry_if_result(lambda result: result not in ["Succeeded", "Failed"]),
414        )
415        def _poll() -> str:
416            response = self.session.get(location_url)
417            response.raise_for_status()
418
419            result = response.json()
420            status = result.get("status", "Unknown")
421
422            logger.debug(f"Operation {operation_name} status: {status}")
423
424            if status == "Failed":
425                error_msg = result.get("error", {}).get("message", "Unknown error")
426                raise SQLMeshError(f"Operation {operation_name} failed: {error_msg}")
427            elif status in ["InProgress", "Running"]:
428                logger.debug(f"Operation {operation_name} still in progress...")
429            elif status not in ["Succeeded"]:
430                logger.warning(f"Unknown status '{status}' for operation {operation_name}")
431
432            return status
433
434        final_status = _poll()
435        if final_status != "Succeeded":
436            raise SQLMeshError(f"Operation {operation_name} completed with status: {final_status}")
FabricHttpClient( tenant_id: str, workspace_id: str, client_id: str, client_secret: str)
274    def __init__(self, tenant_id: str, workspace_id: str, client_id: str, client_secret: str):
275        self.tenant_id = tenant_id
276        self.client_id = client_id
277        self.client_secret = client_secret
278        self.workspace_id = workspace_id
tenant_id
client_id
client_secret
workspace_id
def create_warehouse( self, warehouse_name: str, if_not_exists: bool = True, attempt: int = 0) -> None:
280    def create_warehouse(
281        self, warehouse_name: str, if_not_exists: bool = True, attempt: int = 0
282    ) -> None:
283        """Create a catalog (warehouse) in Microsoft Fabric via REST API."""
284
285        # attempt count is arbitrary, it essentially equates to 5 minutes of 30 second waits
286        if attempt > 10:
287            raise SQLMeshError(
288                f"Gave up waiting for Fabric warehouse {warehouse_name} to become available"
289            )
290
291        logger.info(f"Creating Fabric warehouse: {warehouse_name}")
292
293        request_data = {
294            "displayName": warehouse_name,
295            "description": f"Warehouse created by SQLMesh: {warehouse_name}",
296        }
297
298        response = self.session.post(self._endpoint_url("warehouses"), json=request_data)
299
300        if (
301            if_not_exists
302            and response.status_code == 400
303            and (errorCode := response.json().get("errorCode", None))
304        ):
305            if errorCode == "ItemDisplayNameAlreadyInUse":
306                logger.warning(f"Fabric warehouse {warehouse_name} already exists")
307                return
308            if errorCode == "ItemDisplayNameNotAvailableYet":
309                logger.warning(f"Fabric warehouse {warehouse_name} is still spinning up; waiting")
310                # Fabric error message is something like:
311                #  - "Requested 'circleci_51d7087e__dev' is not available yet and is expected to become available in the upcoming minutes."
312                # This seems to happen if a catalog is dropped and then a new one with the same name is immediately created.
313                # There appears to be some delayed async process on the Fabric side that actually drops the warehouses and frees up the names to be used again
314                time.sleep(30)
315                return self.create_warehouse(
316                    warehouse_name=warehouse_name, if_not_exists=if_not_exists, attempt=attempt + 1
317                )
318
319        try:
320            response.raise_for_status()
321        except:
322            # the important information to actually debug anything is in the response body which Requests never prints
323            logger.exception(
324                f"Failed to create warehouse {warehouse_name}. status: {response.status_code}, body: {response.text}"
325            )
326            raise
327
328        # Handle direct success (201) or async creation (202)
329        if response.status_code == 201:
330            logger.info(f"Successfully created Fabric warehouse: {warehouse_name}")
331            return
332
333        if response.status_code == 202 and (location_header := response.headers.get("location")):
334            logger.info(f"Warehouse creation initiated for: {warehouse_name}")
335            self._wait_for_completion(location_header, warehouse_name)
336            logger.info(f"Successfully created Fabric warehouse: {warehouse_name}")
337        else:
338            logger.error(f"Unexpected response from Fabric API: {response}\n{response.text}")
339            raise SQLMeshError(f"Unable to create warehouse: {response}")

Create a catalog (warehouse) in Microsoft Fabric via REST API.

def delete_warehouse(self, warehouse_name: str, if_exists: bool = True) -> None:
341    def delete_warehouse(self, warehouse_name: str, if_exists: bool = True) -> None:
342        """Drop a catalog (warehouse) in Microsoft Fabric via REST API."""
343        logger.info(f"Deleting Fabric warehouse: {warehouse_name}")
344
345        # Get the warehouse ID by listing warehouses
346        # TODO: handle continuationUri for pagination, ref: https://learn.microsoft.com/en-us/rest/api/fabric/warehouse/items/list-warehouses?tabs=HTTP#warehouses
347        response = self.session.get(self._endpoint_url("warehouses"))
348        response.raise_for_status()
349
350        warehouse_name_to_id = {
351            warehouse.get("displayName"): warehouse.get("id")
352            for warehouse in response.json().get("value", [])
353        }
354
355        warehouse_id = warehouse_name_to_id.get(warehouse_name, None)
356
357        if not warehouse_id:
358            logger.warning(
359                f"Fabric warehouse does not exist: {warehouse_name}\n(available warehouses: {', '.join(warehouse_name_to_id)})"
360            )
361            if if_exists:
362                return
363
364            raise SQLMeshError(
365                f"Unable to delete Fabric warehouse {warehouse_name} as it doesnt exist"
366            )
367
368        # Delete the warehouse by ID
369        response = self.session.delete(self._endpoint_url(f"warehouses/{warehouse_id}"))
370        response.raise_for_status()
371
372        logger.info(f"Successfully deleted Fabric warehouse: {warehouse_name}")

Drop a catalog (warehouse) in Microsoft Fabric via REST API.

session: requests.sessions.Session
374    @cached_property
375    def session(self) -> requests.Session:
376        s = requests.Session()
377
378        access_token = self._get_access_token()
379        s.headers.update({"Authorization": f"Bearer {access_token}"})
380
381        return s