|  | 
| 1 | 1 | import abc | 
| 2 | 2 | import base64 | 
| 3 | 3 | import csv | 
| 4 |  | -import datetime | 
| 5 | 4 | import json | 
| 6 | 5 | import os | 
| 7 | 6 | import shutil | 
| 8 |  | -import struct | 
| 9 | 7 | import tempfile | 
| 10 | 8 | from urllib.parse import parse_qs, quote, urlparse | 
| 11 | 9 | 
 | 
| 12 | 10 | import dlt | 
| 13 | 11 | import dlt.destinations.impl.filesystem.filesystem | 
| 14 | 12 | from dlt.common.configuration.specs import AwsCredentials | 
| 15 |  | -from dlt.common.destination.capabilities import DestinationCapabilitiesContext | 
| 16 |  | -from dlt.common.schema import Schema | 
| 17 | 13 | from dlt.common.storages.configuration import FileSystemCredentials | 
| 18 | 14 | from dlt.destinations.impl.clickhouse.configuration import ( | 
| 19 | 15 |     ClickHouseCredentials, | 
| 20 | 16 | ) | 
| 21 |  | -from dlt.destinations.impl.mssql.configuration import MsSqlClientConfiguration | 
| 22 |  | -from dlt.destinations.impl.mssql.mssql import ( | 
| 23 |  | -    HINT_TO_MSSQL_ATTR, | 
| 24 |  | -    MsSqlJobClient, | 
| 25 |  | -) | 
| 26 |  | -from dlt.destinations.impl.mssql.sql_client import ( | 
| 27 |  | -    PyOdbcMsSqlClient, | 
| 28 |  | -) | 
| 29 | 17 | 
 | 
| 30 | 18 | from ingestr.src.errors import MissingValueError | 
| 31 | 19 | from ingestr.src.loader import load_dlt_file | 
| @@ -155,88 +143,12 @@ def dlt_dest(self, uri: str, **kwargs): | 
| 155 | 143 |         return dlt.destinations.duckdb(uri, **kwargs) | 
| 156 | 144 | 
 | 
| 157 | 145 | 
 | 
| 158 |  | -def handle_datetimeoffset(dto_value: bytes) -> datetime.datetime: | 
| 159 |  | -    # ref: https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794 | 
| 160 |  | -    tup = struct.unpack( | 
| 161 |  | -        "<6hI2h", dto_value | 
| 162 |  | -    )  # e.g., (2017, 3, 16, 10, 35, 18, 500000000, -6, 0) | 
| 163 |  | -    return datetime.datetime( | 
| 164 |  | -        tup[0], | 
| 165 |  | -        tup[1], | 
| 166 |  | -        tup[2], | 
| 167 |  | -        tup[3], | 
| 168 |  | -        tup[4], | 
| 169 |  | -        tup[5], | 
| 170 |  | -        tup[6] // 1000, | 
| 171 |  | -        datetime.timezone(datetime.timedelta(hours=tup[7], minutes=tup[8])), | 
| 172 |  | -    ) | 
| 173 |  | - | 
| 174 |  | - | 
| 175 |  | -class OdbcMsSqlClient(PyOdbcMsSqlClient): | 
| 176 |  | -    SQL_COPT_SS_ACCESS_TOKEN = 1256 | 
| 177 |  | -    SKIP_CREDENTIALS = {"PWD", "AUTHENTICATION", "UID"} | 
| 178 |  | - | 
| 179 |  | -    def open_connection(self): | 
| 180 |  | -        cfg = self.credentials._get_odbc_dsn_dict() | 
| 181 |  | -        if ( | 
| 182 |  | -            cfg.get("AUTHENTICATION", "").strip().lower() | 
| 183 |  | -            != "activedirectoryaccesstoken" | 
| 184 |  | -        ): | 
| 185 |  | -            return super().open_connection() | 
| 186 |  | - | 
| 187 |  | -        import pyodbc  # type: ignore | 
| 188 |  | - | 
| 189 |  | -        dsn = ";".join( | 
| 190 |  | -            [f"{k}={v}" for k, v in cfg.items() if k not in self.SKIP_CREDENTIALS] | 
| 191 |  | -        ) | 
| 192 |  | - | 
| 193 |  | -        self._conn = pyodbc.connect( | 
| 194 |  | -            dsn, | 
| 195 |  | -            timeout=self.credentials.connect_timeout, | 
| 196 |  | -            attrs_before={ | 
| 197 |  | -                self.SQL_COPT_SS_ACCESS_TOKEN: self.serialize_token(cfg["PWD"]), | 
| 198 |  | -            }, | 
| 199 |  | -        ) | 
| 200 |  | - | 
| 201 |  | -        # https://github.com/mkleehammer/pyodbc/wiki/Using-an-Output-Converter-function | 
| 202 |  | -        self._conn.add_output_converter(-155, handle_datetimeoffset) | 
| 203 |  | -        self._conn.autocommit = True | 
| 204 |  | -        return self._conn | 
| 205 |  | - | 
| 206 |  | -    def serialize_token(self, token): | 
| 207 |  | -        # https://github.com/mkleehammer/pyodbc/issues/228#issuecomment-494773723 | 
| 208 |  | -        encoded = token.encode("utf_16_le") | 
| 209 |  | -        return struct.pack("<i", len(encoded)) + encoded | 
| 210 |  | - | 
| 211 |  | - | 
| 212 |  | -class MsSqlClient(MsSqlJobClient): | 
| 213 |  | -    def __init__( | 
| 214 |  | -        self, | 
| 215 |  | -        schema: Schema, | 
| 216 |  | -        config: MsSqlClientConfiguration, | 
| 217 |  | -        capabilities: DestinationCapabilitiesContext, | 
| 218 |  | -    ) -> None: | 
| 219 |  | -        sql_client = OdbcMsSqlClient( | 
| 220 |  | -            config.normalize_dataset_name(schema), | 
| 221 |  | -            config.normalize_staging_dataset_name(schema), | 
| 222 |  | -            config.credentials, | 
| 223 |  | -            capabilities, | 
| 224 |  | -        ) | 
| 225 |  | -        super(MsSqlJobClient, self).__init__(schema, config, sql_client) | 
| 226 |  | -        self.config: MsSqlClientConfiguration = config | 
| 227 |  | -        self.sql_client = sql_client | 
| 228 |  | -        self.active_hints = HINT_TO_MSSQL_ATTR if self.config.create_indexes else {} | 
| 229 |  | -        self.type_mapper = capabilities.get_type_mapper() | 
| 230 |  | - | 
| 231 |  | - | 
| 232 |  | -class MsSqlDestImpl(dlt.destinations.mssql): | 
| 233 |  | -    @property | 
| 234 |  | -    def client_class(self): | 
| 235 |  | -        return MsSqlClient | 
| 236 |  | - | 
| 237 |  | - | 
| 238 | 146 | class MsSQLDestination(GenericSqlDestination): | 
| 239 | 147 |     def dlt_dest(self, uri: str, **kwargs): | 
|  | 148 | +        from ingestr.src.destinations_mssql import (  # type: ignore[import-untyped] | 
|  | 149 | +            MsSqlDestImpl, | 
|  | 150 | +        ) | 
|  | 151 | + | 
| 240 | 152 |         return MsSqlDestImpl(credentials=uri, **kwargs) | 
| 241 | 153 | 
 | 
| 242 | 154 | 
 | 
|  | 
0 commit comments