Edit on GitHub

Contains MSSQLEngineAdapter.

  1"""Contains MSSQLEngineAdapter."""
  2
  3from __future__ import annotations
  4
  5from textwrap import dedent
  6import typing as t
  7import logging
  8
  9from sqlglot import exp
 10
 11from sqlmesh.core.dialect import to_schema, add_table
 12from sqlmesh.core.engine_adapter.base import (
 13    EngineAdapterWithIndexSupport,
 14    EngineAdapter,
 15    InsertOverwriteStrategy,
 16    MERGE_SOURCE_ALIAS,
 17    MERGE_TARGET_ALIAS,
 18    _get_data_object_cache_key,
 19)
 20from sqlmesh.core.engine_adapter.mixins import (
 21    GetCurrentCatalogFromFunctionMixin,
 22    PandasNativeFetchDFSupportMixin,
 23    VarcharSizeWorkaroundMixin,
 24    RowDiffMixin,
 25)
 26from sqlmesh.core.engine_adapter.shared import (
 27    CatalogSupport,
 28    CommentCreationTable,
 29    CommentCreationView,
 30    DataObject,
 31    DataObjectType,
 32    SourceQuery,
 33    set_catalog,
 34)
 35from sqlmesh.utils import get_source_columns_to_types
 36
 37if t.TYPE_CHECKING:
 38    from sqlmesh.core._typing import SchemaName, TableName
 39    from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF
 40
 41
 42logger = logging.getLogger(__name__)
 43
 44
 45@set_catalog()
 46class MSSQLEngineAdapter(
 47    EngineAdapterWithIndexSupport,
 48    PandasNativeFetchDFSupportMixin,
 49    GetCurrentCatalogFromFunctionMixin,
 50    VarcharSizeWorkaroundMixin,
 51    RowDiffMixin,
 52):
 53    DIALECT: str = "tsql"
 54    SUPPORTS_TUPLE_IN = False
 55    SUPPORTS_MATERIALIZED_VIEWS = False
 56    CURRENT_CATALOG_EXPRESSION = exp.func("db_name")
 57    COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY
 58    COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY
 59    SUPPORTS_REPLACE_TABLE = False
 60    MAX_IDENTIFIER_LENGTH = 128
 61    SUPPORTS_QUERY_EXECUTION_TRACKING = True
 62    SCHEMA_DIFFER_KWARGS = {
 63        "parameterized_type_defaults": {
 64            exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 0), (0,)],
 65            exp.DataType.build("BINARY", dialect=DIALECT).this: [(1,)],
 66            exp.DataType.build("VARBINARY", dialect=DIALECT).this: [(1,)],
 67            exp.DataType.build("CHAR", dialect=DIALECT).this: [(1,)],
 68            exp.DataType.build("VARCHAR", dialect=DIALECT).this: [(1,)],
 69            exp.DataType.build("NCHAR", dialect=DIALECT).this: [(1,)],
 70            exp.DataType.build("NVARCHAR", dialect=DIALECT).this: [(1,)],
 71            exp.DataType.build("TIME", dialect=DIALECT).this: [(7,)],
 72            exp.DataType.build("DATETIME2", dialect=DIALECT).this: [(7,)],
 73            exp.DataType.build("DATETIMEOFFSET", dialect=DIALECT).this: [(7,)],
 74        },
 75        "max_parameter_length": {
 76            exp.DataType.build("VARBINARY", dialect=DIALECT).this: 2147483647,  # 2 GB
 77            exp.DataType.build("VARCHAR", dialect=DIALECT).this: 2147483647,
 78            exp.DataType.build("NVARCHAR", dialect=DIALECT).this: 2147483647,
 79        },
 80    }
 81    VARIABLE_LENGTH_DATA_TYPES = {"binary", "varbinary", "char", "varchar", "nchar", "nvarchar"}
 82    INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.MERGE
 83
 84    @property
 85    def catalog_support(self) -> CatalogSupport:
 86        # MSSQL and AzureSQL both use this engine adapter, but they differ in catalog support.
 87        # Therefore, we specify the catalog support in the connection config `_extra_engine_config`
 88        # instead of in the adapter itself.
 89        return self._extra_config["catalog_support"]
 90
 91    def columns(
 92        self,
 93        table_name: TableName,
 94        include_pseudo_columns: bool = True,
 95    ) -> t.Dict[str, exp.DataType]:
 96        """MsSql doesn't support describe so we query information_schema."""
 97
 98        table = exp.to_table(table_name)
 99
100        sql = (
101            exp.select(
102                "COLUMN_NAME",
103                "DATA_TYPE",
104                "CHARACTER_MAXIMUM_LENGTH",
105                "NUMERIC_PRECISION",
106                "NUMERIC_SCALE",
107            )
108            .from_("INFORMATION_SCHEMA.COLUMNS")
109            .where(f"TABLE_NAME = '{table.name}'")
110        )
111        database_name = table.db
112        if database_name:
113            sql = sql.where(f"TABLE_SCHEMA = '{database_name}'")
114
115        columns_raw = self.fetchall(sql, quote_identifiers=True)
116
117        def build_var_length_col(
118            column_name: str,
119            data_type: str,
120            character_maximum_length: t.Optional[int] = None,
121            numeric_precision: t.Optional[int] = None,
122            numeric_scale: t.Optional[int] = None,
123        ) -> tuple:
124            data_type = data_type.lower()
125            if (
126                data_type in self.VARIABLE_LENGTH_DATA_TYPES
127                and character_maximum_length is not None
128                and character_maximum_length > 0
129            ):
130                return (column_name, f"{data_type}({character_maximum_length})")
131            if (
132                data_type in ("varbinary", "varchar", "nvarchar")
133                and character_maximum_length is not None
134                and character_maximum_length == -1
135            ):
136                return (column_name, f"{data_type}(max)")
137            if data_type in ("decimal", "numeric"):
138                return (column_name, f"{data_type}({numeric_precision}, {numeric_scale})")
139            if data_type == "float":
140                return (column_name, f"{data_type}({numeric_precision})")
141
142            return (column_name, data_type)
143
144        columns = [build_var_length_col(*row) for row in columns_raw]
145
146        return {
147            column_name: exp.DataType.build(data_type, dialect=self.dialect)
148            for column_name, data_type in columns
149        }
150
151    def table_exists(self, table_name: TableName) -> bool:
152        """MsSql doesn't support describe so we query information_schema."""
153        table = exp.to_table(table_name)
154        data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name)
155        if data_object_cache_key in self._data_object_cache:
156            logger.debug("Table existence cache hit: %s", data_object_cache_key)
157            return self._data_object_cache[data_object_cache_key] is not None
158
159        sql = (
160            exp.select("1")
161            .from_("INFORMATION_SCHEMA.TABLES")
162            .where(f"TABLE_NAME = '{table.alias_or_name}'")
163        )
164        database_name = table.db
165        if database_name:
166            sql = sql.where(f"TABLE_SCHEMA = '{database_name}'")
167
168        result = self.fetchone(sql, quote_identifiers=True)
169
170        return result[0] == 1 if result else False
171
172    def set_current_catalog(self, catalog_name: str) -> None:
173        self.execute(exp.Use(this=exp.to_identifier(catalog_name)))
174
175    def drop_schema(
176        self,
177        schema_name: SchemaName,
178        ignore_if_not_exists: bool = True,
179        cascade: bool = False,
180        **drop_args: t.Dict[str, exp.Expr],
181    ) -> None:
182        """
183        MsSql doesn't support CASCADE clause and drops schemas unconditionally.
184        """
185        if cascade:
186            objects = self._get_data_objects(schema_name)
187            for obj in objects:
188                # Build properly quoted table for MSSQL using square brackets when needed
189                object_table = exp.table_(obj.name, obj.schema_name)
190
191                # _get_data_objects is catalog-specific, so these can't accidentally drop view/tables in another catalog
192                if obj.type == DataObjectType.VIEW:
193                    self.drop_view(
194                        object_table,
195                        ignore_if_not_exists=ignore_if_not_exists,
196                    )
197                else:
198                    self.drop_table(
199                        object_table,
200                        exists=ignore_if_not_exists,
201                    )
202        super().drop_schema(schema_name, ignore_if_not_exists=ignore_if_not_exists, cascade=False)
203
204    def merge(
205        self,
206        target_table: TableName,
207        source_table: QueryOrDF,
208        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]],
209        unique_key: t.Sequence[exp.Expr],
210        when_matched: t.Optional[exp.Whens] = None,
211        merge_filter: t.Optional[exp.Expr] = None,
212        source_columns: t.Optional[t.List[str]] = None,
213        **kwargs: t.Any,
214    ) -> None:
215        mssql_merge_exists = kwargs.get("physical_properties", {}).get("mssql_merge_exists")
216
217        source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types(
218            source_table,
219            target_columns_to_types,
220            target_table=target_table,
221            source_columns=source_columns,
222        )
223        target_columns_to_types = target_columns_to_types or self.columns(target_table)
224        on = exp.and_(
225            *(
226                add_table(part, MERGE_TARGET_ALIAS).eq(add_table(part, MERGE_SOURCE_ALIAS))
227                for part in unique_key
228            )
229        )
230        if merge_filter:
231            on = exp.and_(merge_filter, on)
232
233        match_expressions = []
234        if not when_matched:
235            unique_key_names = [y.name for y in unique_key]
236            columns_to_types_no_keys = [
237                c for c in target_columns_to_types if c not in unique_key_names
238            ]
239
240            target_columns_no_keys = [
241                exp.column(c, MERGE_TARGET_ALIAS) for c in columns_to_types_no_keys
242            ]
243            source_columns_no_keys = [
244                exp.column(c, MERGE_SOURCE_ALIAS) for c in columns_to_types_no_keys
245            ]
246
247            match_condition = (
248                exp.Exists(
249                    this=exp.select(*target_columns_no_keys).except_(
250                        exp.select(*source_columns_no_keys)
251                    )
252                )
253                if mssql_merge_exists
254                else None
255            )
256
257            if target_columns_no_keys:
258                match_expressions.append(
259                    exp.When(
260                        matched=True,
261                        source=False,
262                        condition=match_condition,
263                        then=exp.Update(
264                            expressions=[
265                                exp.column(col, MERGE_TARGET_ALIAS).eq(
266                                    exp.column(col, MERGE_SOURCE_ALIAS)
267                                )
268                                for col in columns_to_types_no_keys
269                            ],
270                        ),
271                    )
272                )
273        else:
274            match_expressions.extend(when_matched.copy().expressions)
275
276        match_expressions.append(
277            exp.When(
278                matched=False,
279                source=False,
280                then=exp.Insert(
281                    this=exp.Tuple(
282                        expressions=[exp.column(col) for col in target_columns_to_types]
283                    ),
284                    expression=exp.Tuple(
285                        expressions=[
286                            exp.column(col, MERGE_SOURCE_ALIAS) for col in target_columns_to_types
287                        ]
288                    ),
289                ),
290            )
291        )
292        for source_query in source_queries:
293            with source_query as query:
294                self._merge(
295                    target_table=target_table,
296                    query=query,
297                    on=on,
298                    whens=exp.Whens(expressions=match_expressions),
299                )
300
301    def _convert_df_datetime(self, df: DF, columns_to_types: t.Dict[str, exp.DataType]) -> None:
302        import pandas as pd
303        from pandas.api.types import is_datetime64_any_dtype  # type: ignore
304
305        # pymssql doesn't convert Pandas Timestamp (datetime64) types
306        # - this code is based on snowflake adapter implementation
307        for column, kind in columns_to_types.items():
308            # pymssql errors if the column contains a datetime.date object
309            if kind.is_type("date"):  # type: ignore
310                df[column] = pd.to_datetime(df[column]).dt.strftime("%Y-%m-%d")  # type: ignore
311            elif is_datetime64_any_dtype(df.dtypes[column]):  # type: ignore
312                if getattr(df.dtypes[column], "tz", None) is not None:  # type: ignore
313                    # MSSQL requires a colon in the offset (+00:00) so we use isoformat() instead of strftime()
314                    df[column] = pd.to_datetime(df[column]).map(lambda x: x.isoformat(" "))  # type: ignore
315
316                    # bulk_copy() doesn't work with TZ timestamp, so load into string column and cast to
317                    # timestamp in SELECT statement
318                    columns_to_types[column] = exp.DataType.build("TEXT")
319                else:
320                    df[column] = pd.to_datetime(df[column]).dt.strftime("%Y-%m-%d %H:%M:%S.%f")  # type: ignore
321
322    def _df_to_source_queries(
323        self,
324        df: DF,
325        target_columns_to_types: t.Dict[str, exp.DataType],
326        batch_size: int,
327        target_table: TableName,
328        source_columns: t.Optional[t.List[str]] = None,
329    ) -> t.List[SourceQuery]:
330        import pandas as pd
331        import numpy as np
332
333        assert isinstance(df, pd.DataFrame)
334        temp_table = self._get_temp_table(target_table or "pandas")
335
336        # Return the superclass implementation if the connection pool doesn't support bulk_copy
337        if not hasattr(self._connection_pool.get(), "bulk_copy"):
338            return super()._df_to_source_queries(
339                df, target_columns_to_types, batch_size, target_table, source_columns=source_columns
340            )
341
342        def query_factory() -> Query:
343            # It is possible for the factory to be called multiple times and if so then the temp table will already
344            # be created so we skip creating again. This means we are assuming the first call is the same result
345            # as later calls.
346            if not self.table_exists(temp_table):
347                source_columns_to_types = get_source_columns_to_types(
348                    target_columns_to_types, source_columns
349                )
350                ordered_df = df[
351                    list(source_columns_to_types)
352                ]  # reorder DataFrame so it matches columns_to_types
353                self._convert_df_datetime(ordered_df, source_columns_to_types)
354                self.create_table(temp_table, source_columns_to_types)
355                rows: t.List[t.Tuple[t.Any, ...]] = list(
356                    ordered_df.replace({np.nan: None}).itertuples(index=False, name=None)  # type: ignore
357                )
358                conn = self._connection_pool.get()
359                conn.bulk_copy(temp_table.sql(dialect=self.dialect), rows)
360            return exp.select(
361                *self._casted_columns(target_columns_to_types, source_columns=source_columns)
362            ).from_(temp_table)  # type: ignore
363
364        return [
365            SourceQuery(
366                query_factory=query_factory,
367                cleanup_func=lambda: self.drop_table(temp_table),
368            )
369        ]
370
371    def _get_data_objects(
372        self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None
373    ) -> t.List[DataObject]:
374        """
375        Returns all the data objects that exist in the given schema and catalog.
376        """
377        import pandas as pd
378
379        catalog = self.get_current_catalog()
380        query = (
381            exp.select(
382                exp.column("TABLE_NAME").as_("name"),
383                exp.column("TABLE_SCHEMA").as_("schema_name"),
384                exp.case()
385                .when(exp.column("TABLE_TYPE").eq("BASE TABLE"), exp.Literal.string("TABLE"))
386                .else_(exp.column("TABLE_TYPE"))
387                .as_("type"),
388            )
389            .from_(exp.table_("TABLES", db="INFORMATION_SCHEMA"))
390            .where(exp.column("TABLE_SCHEMA").eq(to_schema(schema_name).db))
391        )
392        if object_names:
393            query = query.where(exp.column("TABLE_NAME").isin(*object_names))
394        dataframe: pd.DataFrame = self.fetchdf(query)
395        return [
396            DataObject(
397                catalog=catalog,  # type: ignore
398                schema=row.schema_name,  # type: ignore
399                name=row.name,  # type: ignore
400                type=DataObjectType.from_str(row.type),  # type: ignore
401            )
402            for row in dataframe.itertuples()
403        ]
404
405    def _to_sql(self, expression: exp.Expr, quote: bool = True, **kwargs: t.Any) -> str:
406        sql = super()._to_sql(expression, quote=quote, **kwargs)
407        return f"{sql};"
408
409    def _rename_table(
410        self,
411        old_table_name: TableName,
412        new_table_name: TableName,
413    ) -> None:
414        # The function that renames tables in MSSQL takes string literals as arguments instead of identifiers,
415        # so we shouldn't quote the identifiers.
416        self.execute(exp.rename_table(old_table_name, new_table_name), quote_identifiers=False)
417
418    def _insert_overwrite_by_condition(
419        self,
420        table_name: TableName,
421        source_queries: t.List[SourceQuery],
422        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
423        where: t.Optional[exp.Condition] = None,
424        insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None,
425        **kwargs: t.Any,
426    ) -> None:
427        # note that this is passed as table_properties here rather than physical_properties
428        use_merge_strategy = kwargs.get("table_properties", {}).get("mssql_merge_exists")
429        if (not where or where == exp.true()) and not use_merge_strategy:
430            # this is a full table replacement, call the base strategy to do DELETE+INSERT
431            # which will result in TRUNCATE+INSERT due to how we have overridden self.delete_from()
432            return EngineAdapter._insert_overwrite_by_condition(
433                self,
434                table_name=table_name,
435                source_queries=source_queries,
436                target_columns_to_types=target_columns_to_types,
437                where=where,
438                insert_overwrite_strategy_override=InsertOverwriteStrategy.DELETE_INSERT,
439                **kwargs,
440            )
441
442        # For conditional overwrites or when mssql_merge_exists is set use MERGE
443        return super()._insert_overwrite_by_condition(
444            table_name=table_name,
445            source_queries=source_queries,
446            target_columns_to_types=target_columns_to_types,
447            where=where,
448            insert_overwrite_strategy_override=insert_overwrite_strategy_override,
449            **kwargs,
450        )
451
452    def delete_from(self, table_name: TableName, where: t.Union[str, exp.Expr]) -> None:
453        if where == exp.true():
454            # "A TRUNCATE TABLE operation can be rolled back within a transaction."
455            # ref: https://learn.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-ver15#remarks
456            return self.execute(
457                exp.TruncateTable(expressions=[exp.to_table(table_name, dialect=self.dialect)])
458            )
459
460        return super().delete_from(table_name, where)
461
462    def _build_create_comment_table_exp(
463        self, table: exp.Table, table_comment: str, table_kind: str = "TABLE"
464    ) -> exp.Comment | str:
465        template = dedent("""
466            DECLARE @comment sql_variant = {comment};
467            DECLARE @property_name VARCHAR(128) = 'MS_Description';
468            DECLARE @schema_name VARCHAR(128) = {schema_name};
469            DECLARE @object_name VARCHAR(128) = {object_name};
470            DECLARE @object_kind VARCHAR(128) = '{object_kind}';
471            DECLARE @existing sql_variant;
472
473            SELECT TOP 1 @existing = CAST(VALUE AS NVARCHAR) FROM fn_listextendedproperty(@property_name, 'schema', @schema_name, @object_kind, @object_name, DEFAULT, DEFAULT);
474
475            IF @comment IS NULL
476                BEGIN
477                    IF @existing IS NOT NULL
478                        EXEC sp_dropextendedproperty @property_name, 'schema', @schema_name, @object_kind, @object_name;
479                END
480            ELSE
481                BEGIN
482                    IF @existing IS NULL
483                        EXEC sp_addextendedproperty @property_name,@comment, 'schema', @schema_name, @object_kind, @object_name;
484                    ELSE IF @existing != @comment
485                        EXEC sp_updateextendedproperty @property_name, @comment, 'schema', @schema_name, @object_kind, @object_name;
486                END
487        """)
488        tsql_text = template.format(
489            comment=exp.Literal.string(table_comment or "NULL").sql(
490                dialect=self.dialect, identify=False
491            ),
492            schema_name=exp.Literal.string(table.db or "dbo").sql(
493                dialect=self.dialect, identify=False
494            ),
495            object_name=exp.Literal.string(table.name).sql(dialect=self.dialect, identify=False),
496            object_kind=table_kind,
497        )
498        return tsql_text
499
500    def _build_create_comment_column_exp(
501        self, table: exp.Table, column_name: str, column_comment: str, table_kind: str = "TABLE"
502    ) -> exp.Comment | str:
503        template = dedent("""
504            DECLARE @comment sql_variant = {comment};
505            DECLARE @property_name VARCHAR(128) = 'MS_Description';
506            DECLARE @schema_name VARCHAR(128) = {schema_name};
507            DECLARE @object_name VARCHAR(128) = {object_name};
508            DECLARE @object_kind VARCHAR(128) = '{object_kind}';
509            DECLARE @column_name VARCHAR(128) = {column_name};
510            DECLARE @existing sql_variant;
511
512            SELECT TOP 1 @existing = CAST(VALUE AS NVARCHAR) FROM fn_listextendedproperty(@property_name, 'schema', @schema_name, @object_kind, @object_name, 'column', @column_name);
513
514            IF @comment IS NULL
515                BEGIN
516                    IF @existing IS NOT NULL
517                        EXEC sp_dropextendedproperty @property_name, 'schema', @schema_name, @object_kind, @object_name, 'column', @column_name;
518                END
519            ELSE
520                BEGIN
521                    IF @existing IS NULL
522                        EXEC sp_addextendedproperty @property_name,@comment, 'schema', @schema_name, @object_kind, @object_name, 'column', @column_name;
523                    ELSE IF @existing != @comment
524                        EXEC sp_updateextendedproperty @property_name, @comment, 'schema', @schema_name, @object_kind, @object_name, 'column', @column_name;
525                END
526        """)
527
528        tsql_text = template.format(
529            comment=exp.Literal.string(column_comment or "NULL").sql(
530                dialect=self.dialect, identify=False
531            ),
532            schema_name=exp.Literal.string(table.db or "dbo").sql(
533                dialect=self.dialect, identify=False
534            ),
535            object_name=exp.Literal.string(table.name).sql(dialect=self.dialect, identify=False),
536            object_kind=table_kind,
537            column_name=exp.Literal.string(column_name).sql(dialect=self.dialect, identify=False),
538        )
539
540        return tsql_text
logger = <Logger sqlmesh.core.engine_adapter.mssql (WARNING)>
 46@set_catalog()
 47class MSSQLEngineAdapter(
 48    EngineAdapterWithIndexSupport,
 49    PandasNativeFetchDFSupportMixin,
 50    GetCurrentCatalogFromFunctionMixin,
 51    VarcharSizeWorkaroundMixin,
 52    RowDiffMixin,
 53):
 54    DIALECT: str = "tsql"
 55    SUPPORTS_TUPLE_IN = False
 56    SUPPORTS_MATERIALIZED_VIEWS = False
 57    CURRENT_CATALOG_EXPRESSION = exp.func("db_name")
 58    COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY
 59    COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY
 60    SUPPORTS_REPLACE_TABLE = False
 61    MAX_IDENTIFIER_LENGTH = 128
 62    SUPPORTS_QUERY_EXECUTION_TRACKING = True
 63    SCHEMA_DIFFER_KWARGS = {
 64        "parameterized_type_defaults": {
 65            exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 0), (0,)],
 66            exp.DataType.build("BINARY", dialect=DIALECT).this: [(1,)],
 67            exp.DataType.build("VARBINARY", dialect=DIALECT).this: [(1,)],
 68            exp.DataType.build("CHAR", dialect=DIALECT).this: [(1,)],
 69            exp.DataType.build("VARCHAR", dialect=DIALECT).this: [(1,)],
 70            exp.DataType.build("NCHAR", dialect=DIALECT).this: [(1,)],
 71            exp.DataType.build("NVARCHAR", dialect=DIALECT).this: [(1,)],
 72            exp.DataType.build("TIME", dialect=DIALECT).this: [(7,)],
 73            exp.DataType.build("DATETIME2", dialect=DIALECT).this: [(7,)],
 74            exp.DataType.build("DATETIMEOFFSET", dialect=DIALECT).this: [(7,)],
 75        },
 76        "max_parameter_length": {
 77            exp.DataType.build("VARBINARY", dialect=DIALECT).this: 2147483647,  # 2 GB
 78            exp.DataType.build("VARCHAR", dialect=DIALECT).this: 2147483647,
 79            exp.DataType.build("NVARCHAR", dialect=DIALECT).this: 2147483647,
 80        },
 81    }
 82    VARIABLE_LENGTH_DATA_TYPES = {"binary", "varbinary", "char", "varchar", "nchar", "nvarchar"}
 83    INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.MERGE
 84
 85    @property
 86    def catalog_support(self) -> CatalogSupport:
 87        # MSSQL and AzureSQL both use this engine adapter, but they differ in catalog support.
 88        # Therefore, we specify the catalog support in the connection config `_extra_engine_config`
 89        # instead of in the adapter itself.
 90        return self._extra_config["catalog_support"]
 91
 92    def columns(
 93        self,
 94        table_name: TableName,
 95        include_pseudo_columns: bool = True,
 96    ) -> t.Dict[str, exp.DataType]:
 97        """MsSql doesn't support describe so we query information_schema."""
 98
 99        table = exp.to_table(table_name)
100
101        sql = (
102            exp.select(
103                "COLUMN_NAME",
104                "DATA_TYPE",
105                "CHARACTER_MAXIMUM_LENGTH",
106                "NUMERIC_PRECISION",
107                "NUMERIC_SCALE",
108            )
109            .from_("INFORMATION_SCHEMA.COLUMNS")
110            .where(f"TABLE_NAME = '{table.name}'")
111        )
112        database_name = table.db
113        if database_name:
114            sql = sql.where(f"TABLE_SCHEMA = '{database_name}'")
115
116        columns_raw = self.fetchall(sql, quote_identifiers=True)
117
118        def build_var_length_col(
119            column_name: str,
120            data_type: str,
121            character_maximum_length: t.Optional[int] = None,
122            numeric_precision: t.Optional[int] = None,
123            numeric_scale: t.Optional[int] = None,
124        ) -> tuple:
125            data_type = data_type.lower()
126            if (
127                data_type in self.VARIABLE_LENGTH_DATA_TYPES
128                and character_maximum_length is not None
129                and character_maximum_length > 0
130            ):
131                return (column_name, f"{data_type}({character_maximum_length})")
132            if (
133                data_type in ("varbinary", "varchar", "nvarchar")
134                and character_maximum_length is not None
135                and character_maximum_length == -1
136            ):
137                return (column_name, f"{data_type}(max)")
138            if data_type in ("decimal", "numeric"):
139                return (column_name, f"{data_type}({numeric_precision}, {numeric_scale})")
140            if data_type == "float":
141                return (column_name, f"{data_type}({numeric_precision})")
142
143            return (column_name, data_type)
144
145        columns = [build_var_length_col(*row) for row in columns_raw]
146
147        return {
148            column_name: exp.DataType.build(data_type, dialect=self.dialect)
149            for column_name, data_type in columns
150        }
151
152    def table_exists(self, table_name: TableName) -> bool:
153        """MsSql doesn't support describe so we query information_schema."""
154        table = exp.to_table(table_name)
155        data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name)
156        if data_object_cache_key in self._data_object_cache:
157            logger.debug("Table existence cache hit: %s", data_object_cache_key)
158            return self._data_object_cache[data_object_cache_key] is not None
159
160        sql = (
161            exp.select("1")
162            .from_("INFORMATION_SCHEMA.TABLES")
163            .where(f"TABLE_NAME = '{table.alias_or_name}'")
164        )
165        database_name = table.db
166        if database_name:
167            sql = sql.where(f"TABLE_SCHEMA = '{database_name}'")
168
169        result = self.fetchone(sql, quote_identifiers=True)
170
171        return result[0] == 1 if result else False
172
173    def set_current_catalog(self, catalog_name: str) -> None:
174        self.execute(exp.Use(this=exp.to_identifier(catalog_name)))
175
176    def drop_schema(
177        self,
178        schema_name: SchemaName,
179        ignore_if_not_exists: bool = True,
180        cascade: bool = False,
181        **drop_args: t.Dict[str, exp.Expr],
182    ) -> None:
183        """
184        MsSql doesn't support CASCADE clause and drops schemas unconditionally.
185        """
186        if cascade:
187            objects = self._get_data_objects(schema_name)
188            for obj in objects:
189                # Build properly quoted table for MSSQL using square brackets when needed
190                object_table = exp.table_(obj.name, obj.schema_name)
191
192                # _get_data_objects is catalog-specific, so these can't accidentally drop view/tables in another catalog
193                if obj.type == DataObjectType.VIEW:
194                    self.drop_view(
195                        object_table,
196                        ignore_if_not_exists=ignore_if_not_exists,
197                    )
198                else:
199                    self.drop_table(
200                        object_table,
201                        exists=ignore_if_not_exists,
202                    )
203        super().drop_schema(schema_name, ignore_if_not_exists=ignore_if_not_exists, cascade=False)
204
205    def merge(
206        self,
207        target_table: TableName,
208        source_table: QueryOrDF,
209        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]],
210        unique_key: t.Sequence[exp.Expr],
211        when_matched: t.Optional[exp.Whens] = None,
212        merge_filter: t.Optional[exp.Expr] = None,
213        source_columns: t.Optional[t.List[str]] = None,
214        **kwargs: t.Any,
215    ) -> None:
216        mssql_merge_exists = kwargs.get("physical_properties", {}).get("mssql_merge_exists")
217
218        source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types(
219            source_table,
220            target_columns_to_types,
221            target_table=target_table,
222            source_columns=source_columns,
223        )
224        target_columns_to_types = target_columns_to_types or self.columns(target_table)
225        on = exp.and_(
226            *(
227                add_table(part, MERGE_TARGET_ALIAS).eq(add_table(part, MERGE_SOURCE_ALIAS))
228                for part in unique_key
229            )
230        )
231        if merge_filter:
232            on = exp.and_(merge_filter, on)
233
234        match_expressions = []
235        if not when_matched:
236            unique_key_names = [y.name for y in unique_key]
237            columns_to_types_no_keys = [
238                c for c in target_columns_to_types if c not in unique_key_names
239            ]
240
241            target_columns_no_keys = [
242                exp.column(c, MERGE_TARGET_ALIAS) for c in columns_to_types_no_keys
243            ]
244            source_columns_no_keys = [
245                exp.column(c, MERGE_SOURCE_ALIAS) for c in columns_to_types_no_keys
246            ]
247
248            match_condition = (
249                exp.Exists(
250                    this=exp.select(*target_columns_no_keys).except_(
251                        exp.select(*source_columns_no_keys)
252                    )
253                )
254                if mssql_merge_exists
255                else None
256            )
257
258            if target_columns_no_keys:
259                match_expressions.append(
260                    exp.When(
261                        matched=True,
262                        source=False,
263                        condition=match_condition,
264                        then=exp.Update(
265                            expressions=[
266                                exp.column(col, MERGE_TARGET_ALIAS).eq(
267                                    exp.column(col, MERGE_SOURCE_ALIAS)
268                                )
269                                for col in columns_to_types_no_keys
270                            ],
271                        ),
272                    )
273                )
274        else:
275            match_expressions.extend(when_matched.copy().expressions)
276
277        match_expressions.append(
278            exp.When(
279                matched=False,
280                source=False,
281                then=exp.Insert(
282                    this=exp.Tuple(
283                        expressions=[exp.column(col) for col in target_columns_to_types]
284                    ),
285                    expression=exp.Tuple(
286                        expressions=[
287                            exp.column(col, MERGE_SOURCE_ALIAS) for col in target_columns_to_types
288                        ]
289                    ),
290                ),
291            )
292        )
293        for source_query in source_queries:
294            with source_query as query:
295                self._merge(
296                    target_table=target_table,
297                    query=query,
298                    on=on,
299                    whens=exp.Whens(expressions=match_expressions),
300                )
301
302    def _convert_df_datetime(self, df: DF, columns_to_types: t.Dict[str, exp.DataType]) -> None:
303        import pandas as pd
304        from pandas.api.types import is_datetime64_any_dtype  # type: ignore
305
306        # pymssql doesn't convert Pandas Timestamp (datetime64) types
307        # - this code is based on snowflake adapter implementation
308        for column, kind in columns_to_types.items():
309            # pymssql errors if the column contains a datetime.date object
310            if kind.is_type("date"):  # type: ignore
311                df[column] = pd.to_datetime(df[column]).dt.strftime("%Y-%m-%d")  # type: ignore
312            elif is_datetime64_any_dtype(df.dtypes[column]):  # type: ignore
313                if getattr(df.dtypes[column], "tz", None) is not None:  # type: ignore
314                    # MSSQL requires a colon in the offset (+00:00) so we use isoformat() instead of strftime()
315                    df[column] = pd.to_datetime(df[column]).map(lambda x: x.isoformat(" "))  # type: ignore
316
317                    # bulk_copy() doesn't work with TZ timestamp, so load into string column and cast to
318                    # timestamp in SELECT statement
319                    columns_to_types[column] = exp.DataType.build("TEXT")
320                else:
321                    df[column] = pd.to_datetime(df[column]).dt.strftime("%Y-%m-%d %H:%M:%S.%f")  # type: ignore
322
323    def _df_to_source_queries(
324        self,
325        df: DF,
326        target_columns_to_types: t.Dict[str, exp.DataType],
327        batch_size: int,
328        target_table: TableName,
329        source_columns: t.Optional[t.List[str]] = None,
330    ) -> t.List[SourceQuery]:
331        import pandas as pd
332        import numpy as np
333
334        assert isinstance(df, pd.DataFrame)
335        temp_table = self._get_temp_table(target_table or "pandas")
336
337        # Return the superclass implementation if the connection pool doesn't support bulk_copy
338        if not hasattr(self._connection_pool.get(), "bulk_copy"):
339            return super()._df_to_source_queries(
340                df, target_columns_to_types, batch_size, target_table, source_columns=source_columns
341            )
342
343        def query_factory() -> Query:
344            # It is possible for the factory to be called multiple times and if so then the temp table will already
345            # be created so we skip creating again. This means we are assuming the first call is the same result
346            # as later calls.
347            if not self.table_exists(temp_table):
348                source_columns_to_types = get_source_columns_to_types(
349                    target_columns_to_types, source_columns
350                )
351                ordered_df = df[
352                    list(source_columns_to_types)
353                ]  # reorder DataFrame so it matches columns_to_types
354                self._convert_df_datetime(ordered_df, source_columns_to_types)
355                self.create_table(temp_table, source_columns_to_types)
356                rows: t.List[t.Tuple[t.Any, ...]] = list(
357                    ordered_df.replace({np.nan: None}).itertuples(index=False, name=None)  # type: ignore
358                )
359                conn = self._connection_pool.get()
360                conn.bulk_copy(temp_table.sql(dialect=self.dialect), rows)
361            return exp.select(
362                *self._casted_columns(target_columns_to_types, source_columns=source_columns)
363            ).from_(temp_table)  # type: ignore
364
365        return [
366            SourceQuery(
367                query_factory=query_factory,
368                cleanup_func=lambda: self.drop_table(temp_table),
369            )
370        ]
371
372    def _get_data_objects(
373        self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None
374    ) -> t.List[DataObject]:
375        """
376        Returns all the data objects that exist in the given schema and catalog.
377        """
378        import pandas as pd
379
380        catalog = self.get_current_catalog()
381        query = (
382            exp.select(
383                exp.column("TABLE_NAME").as_("name"),
384                exp.column("TABLE_SCHEMA").as_("schema_name"),
385                exp.case()
386                .when(exp.column("TABLE_TYPE").eq("BASE TABLE"), exp.Literal.string("TABLE"))
387                .else_(exp.column("TABLE_TYPE"))
388                .as_("type"),
389            )
390            .from_(exp.table_("TABLES", db="INFORMATION_SCHEMA"))
391            .where(exp.column("TABLE_SCHEMA").eq(to_schema(schema_name).db))
392        )
393        if object_names:
394            query = query.where(exp.column("TABLE_NAME").isin(*object_names))
395        dataframe: pd.DataFrame = self.fetchdf(query)
396        return [
397            DataObject(
398                catalog=catalog,  # type: ignore
399                schema=row.schema_name,  # type: ignore
400                name=row.name,  # type: ignore
401                type=DataObjectType.from_str(row.type),  # type: ignore
402            )
403            for row in dataframe.itertuples()
404        ]
405
406    def _to_sql(self, expression: exp.Expr, quote: bool = True, **kwargs: t.Any) -> str:
407        sql = super()._to_sql(expression, quote=quote, **kwargs)
408        return f"{sql};"
409
410    def _rename_table(
411        self,
412        old_table_name: TableName,
413        new_table_name: TableName,
414    ) -> None:
415        # The function that renames tables in MSSQL takes string literals as arguments instead of identifiers,
416        # so we shouldn't quote the identifiers.
417        self.execute(exp.rename_table(old_table_name, new_table_name), quote_identifiers=False)
418
419    def _insert_overwrite_by_condition(
420        self,
421        table_name: TableName,
422        source_queries: t.List[SourceQuery],
423        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
424        where: t.Optional[exp.Condition] = None,
425        insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None,
426        **kwargs: t.Any,
427    ) -> None:
428        # note that this is passed as table_properties here rather than physical_properties
429        use_merge_strategy = kwargs.get("table_properties", {}).get("mssql_merge_exists")
430        if (not where or where == exp.true()) and not use_merge_strategy:
431            # this is a full table replacement, call the base strategy to do DELETE+INSERT
432            # which will result in TRUNCATE+INSERT due to how we have overridden self.delete_from()
433            return EngineAdapter._insert_overwrite_by_condition(
434                self,
435                table_name=table_name,
436                source_queries=source_queries,
437                target_columns_to_types=target_columns_to_types,
438                where=where,
439                insert_overwrite_strategy_override=InsertOverwriteStrategy.DELETE_INSERT,
440                **kwargs,
441            )
442
443        # For conditional overwrites or when mssql_merge_exists is set use MERGE
444        return super()._insert_overwrite_by_condition(
445            table_name=table_name,
446            source_queries=source_queries,
447            target_columns_to_types=target_columns_to_types,
448            where=where,
449            insert_overwrite_strategy_override=insert_overwrite_strategy_override,
450            **kwargs,
451        )
452
453    def delete_from(self, table_name: TableName, where: t.Union[str, exp.Expr]) -> None:
454        if where == exp.true():
455            # "A TRUNCATE TABLE operation can be rolled back within a transaction."
456            # ref: https://learn.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-ver15#remarks
457            return self.execute(
458                exp.TruncateTable(expressions=[exp.to_table(table_name, dialect=self.dialect)])
459            )
460
461        return super().delete_from(table_name, where)
462
463    def _build_create_comment_table_exp(
464        self, table: exp.Table, table_comment: str, table_kind: str = "TABLE"
465    ) -> exp.Comment | str:
466        template = dedent("""
467            DECLARE @comment sql_variant = {comment};
468            DECLARE @property_name VARCHAR(128) = 'MS_Description';
469            DECLARE @schema_name VARCHAR(128) = {schema_name};
470            DECLARE @object_name VARCHAR(128) = {object_name};
471            DECLARE @object_kind VARCHAR(128) = '{object_kind}';
472            DECLARE @existing sql_variant;
473
474            SELECT TOP 1 @existing = CAST(VALUE AS NVARCHAR) FROM fn_listextendedproperty(@property_name, 'schema', @schema_name, @object_kind, @object_name, DEFAULT, DEFAULT);
475
476            IF @comment IS NULL
477                BEGIN
478                    IF @existing IS NOT NULL
479                        EXEC sp_dropextendedproperty @property_name, 'schema', @schema_name, @object_kind, @object_name;
480                END
481            ELSE
482                BEGIN
483                    IF @existing IS NULL
484                        EXEC sp_addextendedproperty @property_name,@comment, 'schema', @schema_name, @object_kind, @object_name;
485                    ELSE IF @existing != @comment
486                        EXEC sp_updateextendedproperty @property_name, @comment, 'schema', @schema_name, @object_kind, @object_name;
487                END
488        """)
489        tsql_text = template.format(
490            comment=exp.Literal.string(table_comment or "NULL").sql(
491                dialect=self.dialect, identify=False
492            ),
493            schema_name=exp.Literal.string(table.db or "dbo").sql(
494                dialect=self.dialect, identify=False
495            ),
496            object_name=exp.Literal.string(table.name).sql(dialect=self.dialect, identify=False),
497            object_kind=table_kind,
498        )
499        return tsql_text
500
501    def _build_create_comment_column_exp(
502        self, table: exp.Table, column_name: str, column_comment: str, table_kind: str = "TABLE"
503    ) -> exp.Comment | str:
504        template = dedent("""
505            DECLARE @comment sql_variant = {comment};
506            DECLARE @property_name VARCHAR(128) = 'MS_Description';
507            DECLARE @schema_name VARCHAR(128) = {schema_name};
508            DECLARE @object_name VARCHAR(128) = {object_name};
509            DECLARE @object_kind VARCHAR(128) = '{object_kind}';
510            DECLARE @column_name VARCHAR(128) = {column_name};
511            DECLARE @existing sql_variant;
512
513            SELECT TOP 1 @existing = CAST(VALUE AS NVARCHAR) FROM fn_listextendedproperty(@property_name, 'schema', @schema_name, @object_kind, @object_name, 'column', @column_name);
514
515            IF @comment IS NULL
516                BEGIN
517                    IF @existing IS NOT NULL
518                        EXEC sp_dropextendedproperty @property_name, 'schema', @schema_name, @object_kind, @object_name, 'column', @column_name;
519                END
520            ELSE
521                BEGIN
522                    IF @existing IS NULL
523                        EXEC sp_addextendedproperty @property_name,@comment, 'schema', @schema_name, @object_kind, @object_name, 'column', @column_name;
524                    ELSE IF @existing != @comment
525                        EXEC sp_updateextendedproperty @property_name, @comment, 'schema', @schema_name, @object_kind, @object_name, 'column', @column_name;
526                END
527        """)
528
529        tsql_text = template.format(
530            comment=exp.Literal.string(column_comment or "NULL").sql(
531                dialect=self.dialect, identify=False
532            ),
533            schema_name=exp.Literal.string(table.db or "dbo").sql(
534                dialect=self.dialect, identify=False
535            ),
536            object_name=exp.Literal.string(table.name).sql(dialect=self.dialect, identify=False),
537            object_kind=table_kind,
538            column_name=exp.Literal.string(column_name).sql(dialect=self.dialect, identify=False),
539        )
540
541        return tsql_text

Base class wrapping a Database API compliant connection.

The EngineAdapter is an easily-subclassable interface that interacts with the underlying engine and data store.

Arguments:
  • connection_factory_or_pool: a callable which produces a new Database API-compliant connection on every call.
  • dialect: The dialect with which this adapter is associated.
  • multithreaded: Indicates whether this adapter will be used by more than one thread.
DIALECT: str = 'tsql'
SUPPORTS_TUPLE_IN = False
SUPPORTS_MATERIALIZED_VIEWS = False
CURRENT_CATALOG_EXPRESSION = Anonymous(this=db_name)
COMMENT_CREATION_TABLE = <CommentCreationTable.COMMENT_COMMAND_ONLY: 4>
COMMENT_CREATION_VIEW = <CommentCreationView.COMMENT_COMMAND_ONLY: 4>
SUPPORTS_REPLACE_TABLE = False
MAX_IDENTIFIER_LENGTH = 128
SUPPORTS_QUERY_EXECUTION_TRACKING = True
SCHEMA_DIFFER_KWARGS = {'parameterized_type_defaults': {<DType.DECIMAL: 'DECIMAL'>: [(18, 0), (0,)], <DType.BINARY: 'BINARY'>: [(1,)], <DType.VARBINARY: 'VARBINARY'>: [(1,)], <DType.CHAR: 'CHAR'>: [(1,)], <DType.VARCHAR: 'VARCHAR'>: [(1,)], <DType.NCHAR: 'NCHAR'>: [(1,)], <DType.NVARCHAR: 'NVARCHAR'>: [(1,)], <DType.TIME: 'TIME'>: [(7,)], <DType.DATETIME2: 'DATETIME2'>: [(7,)], <DType.TIMESTAMPTZ: 'TIMESTAMPTZ'>: [(7,)]}, 'max_parameter_length': {<DType.VARBINARY: 'VARBINARY'>: 2147483647, <DType.VARCHAR: 'VARCHAR'>: 2147483647, <DType.NVARCHAR: 'NVARCHAR'>: 2147483647}}
VARIABLE_LENGTH_DATA_TYPES = {'binary', 'char', 'varbinary', 'varchar', 'nchar', 'nvarchar'}
INSERT_OVERWRITE_STRATEGY = <InsertOverwriteStrategy.MERGE: 5>
85    @property
86    def catalog_support(self) -> CatalogSupport:
87        # MSSQL and AzureSQL both use this engine adapter, but they differ in catalog support.
88        # Therefore, we specify the catalog support in the connection config `_extra_engine_config`
89        # instead of in the adapter itself.
90        return self._extra_config["catalog_support"]
def columns( self, table_name: Union[str, sqlglot.expressions.query.Table], include_pseudo_columns: bool = True) -> Dict[str, sqlglot.expressions.datatypes.DataType]:
 92    def columns(
 93        self,
 94        table_name: TableName,
 95        include_pseudo_columns: bool = True,
 96    ) -> t.Dict[str, exp.DataType]:
 97        """MsSql doesn't support describe so we query information_schema."""
 98
 99        table = exp.to_table(table_name)
100
101        sql = (
102            exp.select(
103                "COLUMN_NAME",
104                "DATA_TYPE",
105                "CHARACTER_MAXIMUM_LENGTH",
106                "NUMERIC_PRECISION",
107                "NUMERIC_SCALE",
108            )
109            .from_("INFORMATION_SCHEMA.COLUMNS")
110            .where(f"TABLE_NAME = '{table.name}'")
111        )
112        database_name = table.db
113        if database_name:
114            sql = sql.where(f"TABLE_SCHEMA = '{database_name}'")
115
116        columns_raw = self.fetchall(sql, quote_identifiers=True)
117
118        def build_var_length_col(
119            column_name: str,
120            data_type: str,
121            character_maximum_length: t.Optional[int] = None,
122            numeric_precision: t.Optional[int] = None,
123            numeric_scale: t.Optional[int] = None,
124        ) -> tuple:
125            data_type = data_type.lower()
126            if (
127                data_type in self.VARIABLE_LENGTH_DATA_TYPES
128                and character_maximum_length is not None
129                and character_maximum_length > 0
130            ):
131                return (column_name, f"{data_type}({character_maximum_length})")
132            if (
133                data_type in ("varbinary", "varchar", "nvarchar")
134                and character_maximum_length is not None
135                and character_maximum_length == -1
136            ):
137                return (column_name, f"{data_type}(max)")
138            if data_type in ("decimal", "numeric"):
139                return (column_name, f"{data_type}({numeric_precision}, {numeric_scale})")
140            if data_type == "float":
141                return (column_name, f"{data_type}({numeric_precision})")
142
143            return (column_name, data_type)
144
145        columns = [build_var_length_col(*row) for row in columns_raw]
146
147        return {
148            column_name: exp.DataType.build(data_type, dialect=self.dialect)
149            for column_name, data_type in columns
150        }

MsSql doesn't support describe so we query information_schema.

def table_exists(self, table_name: Union[str, sqlglot.expressions.query.Table]) -> bool:
152    def table_exists(self, table_name: TableName) -> bool:
153        """MsSql doesn't support describe so we query information_schema."""
154        table = exp.to_table(table_name)
155        data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name)
156        if data_object_cache_key in self._data_object_cache:
157            logger.debug("Table existence cache hit: %s", data_object_cache_key)
158            return self._data_object_cache[data_object_cache_key] is not None
159
160        sql = (
161            exp.select("1")
162            .from_("INFORMATION_SCHEMA.TABLES")
163            .where(f"TABLE_NAME = '{table.alias_or_name}'")
164        )
165        database_name = table.db
166        if database_name:
167            sql = sql.where(f"TABLE_SCHEMA = '{database_name}'")
168
169        result = self.fetchone(sql, quote_identifiers=True)
170
171        return result[0] == 1 if result else False

MsSql doesn't support describe so we query information_schema.

def set_current_catalog(self, catalog_name: str) -> None:
173    def set_current_catalog(self, catalog_name: str) -> None:
174        self.execute(exp.Use(this=exp.to_identifier(catalog_name)))

Sets the catalog name of the current connection.

def drop_schema( self, schema_name: Union[str, sqlglot.expressions.query.Table], ignore_if_not_exists: bool = True, cascade: bool = False, **drop_args: Dict[str, sqlglot.expressions.core.Expr]) -> None:
176    def drop_schema(
177        self,
178        schema_name: SchemaName,
179        ignore_if_not_exists: bool = True,
180        cascade: bool = False,
181        **drop_args: t.Dict[str, exp.Expr],
182    ) -> None:
183        """
184        MsSql doesn't support CASCADE clause and drops schemas unconditionally.
185        """
186        if cascade:
187            objects = self._get_data_objects(schema_name)
188            for obj in objects:
189                # Build properly quoted table for MSSQL using square brackets when needed
190                object_table = exp.table_(obj.name, obj.schema_name)
191
192                # _get_data_objects is catalog-specific, so these can't accidentally drop view/tables in another catalog
193                if obj.type == DataObjectType.VIEW:
194                    self.drop_view(
195                        object_table,
196                        ignore_if_not_exists=ignore_if_not_exists,
197                    )
198                else:
199                    self.drop_table(
200                        object_table,
201                        exists=ignore_if_not_exists,
202                    )
203        super().drop_schema(schema_name, ignore_if_not_exists=ignore_if_not_exists, cascade=False)

MsSql doesn't support CASCADE clause and drops schemas unconditionally.

def merge( self, target_table: Union[str, sqlglot.expressions.query.Table], source_table: <MagicMock id='130969797157808'>, target_columns_to_types: Optional[Dict[str, sqlglot.expressions.datatypes.DataType]], unique_key: Sequence[sqlglot.expressions.core.Expr], when_matched: Optional[sqlglot.expressions.dml.Whens] = None, merge_filter: Optional[sqlglot.expressions.core.Expr] = None, source_columns: Optional[List[str]] = None, **kwargs: Any) -> None:
205    def merge(
206        self,
207        target_table: TableName,
208        source_table: QueryOrDF,
209        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]],
210        unique_key: t.Sequence[exp.Expr],
211        when_matched: t.Optional[exp.Whens] = None,
212        merge_filter: t.Optional[exp.Expr] = None,
213        source_columns: t.Optional[t.List[str]] = None,
214        **kwargs: t.Any,
215    ) -> None:
216        mssql_merge_exists = kwargs.get("physical_properties", {}).get("mssql_merge_exists")
217
218        source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types(
219            source_table,
220            target_columns_to_types,
221            target_table=target_table,
222            source_columns=source_columns,
223        )
224        target_columns_to_types = target_columns_to_types or self.columns(target_table)
225        on = exp.and_(
226            *(
227                add_table(part, MERGE_TARGET_ALIAS).eq(add_table(part, MERGE_SOURCE_ALIAS))
228                for part in unique_key
229            )
230        )
231        if merge_filter:
232            on = exp.and_(merge_filter, on)
233
234        match_expressions = []
235        if not when_matched:
236            unique_key_names = [y.name for y in unique_key]
237            columns_to_types_no_keys = [
238                c for c in target_columns_to_types if c not in unique_key_names
239            ]
240
241            target_columns_no_keys = [
242                exp.column(c, MERGE_TARGET_ALIAS) for c in columns_to_types_no_keys
243            ]
244            source_columns_no_keys = [
245                exp.column(c, MERGE_SOURCE_ALIAS) for c in columns_to_types_no_keys
246            ]
247
248            match_condition = (
249                exp.Exists(
250                    this=exp.select(*target_columns_no_keys).except_(
251                        exp.select(*source_columns_no_keys)
252                    )
253                )
254                if mssql_merge_exists
255                else None
256            )
257
258            if target_columns_no_keys:
259                match_expressions.append(
260                    exp.When(
261                        matched=True,
262                        source=False,
263                        condition=match_condition,
264                        then=exp.Update(
265                            expressions=[
266                                exp.column(col, MERGE_TARGET_ALIAS).eq(
267                                    exp.column(col, MERGE_SOURCE_ALIAS)
268                                )
269                                for col in columns_to_types_no_keys
270                            ],
271                        ),
272                    )
273                )
274        else:
275            match_expressions.extend(when_matched.copy().expressions)
276
277        match_expressions.append(
278            exp.When(
279                matched=False,
280                source=False,
281                then=exp.Insert(
282                    this=exp.Tuple(
283                        expressions=[exp.column(col) for col in target_columns_to_types]
284                    ),
285                    expression=exp.Tuple(
286                        expressions=[
287                            exp.column(col, MERGE_SOURCE_ALIAS) for col in target_columns_to_types
288                        ]
289                    ),
290                ),
291            )
292        )
293        for source_query in source_queries:
294            with source_query as query:
295                self._merge(
296                    target_table=target_table,
297                    query=query,
298                    on=on,
299                    whens=exp.Whens(expressions=match_expressions),
300                )
def delete_from( self, table_name: Union[str, sqlglot.expressions.query.Table], where: Union[str, sqlglot.expressions.core.Expr]) -> None:
453    def delete_from(self, table_name: TableName, where: t.Union[str, exp.Expr]) -> None:
454        if where == exp.true():
455            # "A TRUNCATE TABLE operation can be rolled back within a transaction."
456            # ref: https://learn.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-ver15#remarks
457            return self.execute(
458                exp.TruncateTable(expressions=[exp.to_table(table_name, dialect=self.dialect)])
459            )
460
461        return super().delete_from(table_name, where)
Inherited Members
sqlmesh.core.engine_adapter.base.EngineAdapter
EngineAdapter
DEFAULT_BATCH_SIZE
DATA_OBJECT_FILTER_BATCH_SIZE
SUPPORTS_TRANSACTIONS
MAX_TABLE_COMMENT_LENGTH
MAX_COLUMN_COMMENT_LENGTH
SUPPORTS_MATERIALIZED_VIEW_SCHEMA
SUPPORTS_VIEW_SCHEMA
SUPPORTS_CLONING
SUPPORTS_MANAGED_MODELS
SUPPORTS_CREATE_DROP_CATALOG
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
alter_table
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
sqlmesh.core.engine_adapter.base.EngineAdapterWithIndexSupport
SUPPORTS_INDEXES
sqlmesh.core.engine_adapter.mixins.GetCurrentCatalogFromFunctionMixin
get_current_catalog
sqlmesh.core.engine_adapter.mixins.RowDiffMixin
MAX_TIMESTAMP_PRECISION
concat_columns
normalize_value