Skip to content

Commit e43d973

Browse files
datadersclaude
andcommitted
spike: port dbt-sqlserver from pyodbc/ODBC to ADBC
Replace the entire pyodbc/msodbcsql18 stack with ADBC (Arrow Database Connectivity) using the mssql driver from dbc, and remove the dbt-fabric dependency to make dbt-sqlserver a standalone adapter. Key changes: - Remove dbt-fabric dependency; inherit from dbt-adapters base classes - Rewrite connection layer to use adbc-driver-manager dbapi - Build go-mssqldb connection URIs from profile credentials - Inline ~25 fabric macros with sqlserver__ prefix - Rewrite seed loading to inline literal values (ADBC doesn't support parameterized queries with ? placeholders) - Add Arrow type code -> SQL Server type name mapping - Update unit tests for ADBC/URI-based connection model Test results: 123 passed, 8 failed, 45 skipped (10/10 unit tests pass) Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent fc5c77c commit e43d973

37 files changed

Lines changed: 1175 additions & 228 deletions

dbt/adapters/sqlserver/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
adapter=SQLServerAdapter,
1212
credentials=SQLServerCredentials,
1313
include_path=sqlserver.PACKAGE_PATH,
14-
dependencies=["fabric"],
1514
)
1615

1716
__all__ = [

dbt/adapters/sqlserver/sqlserver_adapter.py

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,51 @@
1-
from typing import Optional
1+
from typing import List, Optional
22

3+
import agate
34
import dbt.exceptions
5+
from dbt.adapters.base import available
46
from dbt.adapters.base.impl import ConstraintSupport
5-
from dbt.adapters.fabric import FabricAdapter
7+
from dbt.adapters.base.relation import BaseRelation
8+
from dbt.adapters.sql import SQLAdapter
69
from dbt.contracts.graph.nodes import ConstraintType
10+
from dbt_common.contracts.constraints import ColumnLevelConstraint
711

812
from dbt.adapters.sqlserver.sqlserver_column import SQLServerColumn
913
from dbt.adapters.sqlserver.sqlserver_connections import SQLServerConnectionManager
1014
from dbt.adapters.sqlserver.sqlserver_relation import SQLServerRelation
1115

1216

13-
class SQLServerAdapter(FabricAdapter):
17+
COLUMNS_EQUAL_SQL = """
18+
with diff_count as (
19+
SELECT
20+
1 as id,
21+
COUNT(*) as num_missing FROM (
22+
(SELECT {columns} FROM {relation_a} {except_op}
23+
SELECT {columns} FROM {relation_b})
24+
UNION ALL
25+
(SELECT {columns} FROM {relation_b} {except_op}
26+
SELECT {columns} FROM {relation_a})
27+
) as a
28+
), table_a as (
29+
SELECT COUNT(*) as num_rows FROM {relation_a}
30+
), table_b as (
31+
SELECT COUNT(*) as num_rows FROM {relation_b}
32+
), row_count_diff as (
33+
select
34+
1 as id,
35+
table_a.num_rows - table_b.num_rows as difference
36+
from table_a, table_b
37+
)
38+
select
39+
row_count_diff.difference as row_count_difference,
40+
diff_count.num_missing as num_mismatched
41+
from row_count_diff
42+
join diff_count on row_count_diff.id = diff_count.id
43+
""".strip()
44+
45+
46+
class SQLServerAdapter(SQLAdapter):
1447
"""
15-
Controls actual implmentation of adapter, and ability to override certain methods.
48+
Controls actual implementation of adapter, and ability to override certain methods.
1649
"""
1750

1851
ConnectionManager = SQLServerConnectionManager
@@ -27,6 +60,91 @@ class SQLServerAdapter(FabricAdapter):
2760
ConstraintType.foreign_key: ConstraintSupport.ENFORCED,
2861
}
2962

63+
# -- Type conversions (inlined from FabricAdapter) --
64+
65+
@classmethod
66+
def convert_boolean_type(cls, agate_table, col_idx):
67+
return "bit"
68+
69+
@classmethod
70+
def convert_datetime_type(cls, agate_table, col_idx):
71+
return "datetime2(6)"
72+
73+
@classmethod
74+
def convert_number_type(cls, agate_table, col_idx):
75+
decimals = agate_table.aggregate(agate.MaxPrecision(col_idx))
76+
return "float" if decimals else "int"
77+
78+
@classmethod
79+
def convert_text_type(cls, agate_table, col_idx):
80+
column = agate_table.columns[col_idx]
81+
lens = [len(d.encode("utf-8")) for d in column.values_without_nulls()]
82+
max_len = max(lens) if lens else 64
83+
length = max_len if max_len > 16 else 16
84+
return "varchar({})".format(length)
85+
86+
@classmethod
87+
def convert_time_type(cls, agate_table, col_idx):
88+
return "time(6)"
89+
90+
@classmethod
91+
def date_function(cls):
92+
return "getdate()"
93+
94+
# -- SQL helpers (inlined from FabricAdapter) --
95+
96+
def timestamp_add_sql(self, add_to: str, number: int = 1, interval: str = "hour") -> str:
97+
return f"DATEADD({interval},{number},{add_to})"
98+
99+
def string_add_sql(self, add_to: str, value: str, location="append") -> str:
100+
"""+ is T-SQL's string concatenation operator"""
101+
if location == "append":
102+
return f"{add_to} + '{value}'"
103+
elif location == "prepend":
104+
return f"'{value}' + {add_to}"
105+
else:
106+
raise ValueError(f'Got an unexpected location value of "{location}"')
107+
108+
def get_rows_different_sql(
109+
self,
110+
relation_a: BaseRelation,
111+
relation_b: BaseRelation,
112+
column_names: Optional[List[str]] = None,
113+
except_operator: str = "EXCEPT",
114+
) -> str:
115+
names: List[str]
116+
if column_names is None:
117+
columns = self.get_columns_in_relation(relation_a)
118+
names = sorted((self.quote(c.name) for c in columns))
119+
else:
120+
names = sorted((self.quote(n) for n in column_names))
121+
122+
columns_csv = ", ".join(names)
123+
124+
sql = COLUMNS_EQUAL_SQL.format(
125+
columns=columns_csv,
126+
relation_a=str(relation_a),
127+
relation_b=str(relation_b),
128+
except_op=except_operator,
129+
)
130+
return sql
131+
132+
# -- Constraint rendering --
133+
134+
@available
135+
@classmethod
136+
def render_column_constraint(cls, constraint: ColumnLevelConstraint) -> Optional[str]:
137+
rendered_column_constraint = None
138+
if constraint.type == ConstraintType.not_null:
139+
rendered_column_constraint = "not null"
140+
else:
141+
rendered_column_constraint = ""
142+
143+
if rendered_column_constraint:
144+
rendered_column_constraint = rendered_column_constraint.strip()
145+
146+
return rendered_column_constraint
147+
30148
@classmethod
31149
def render_model_constraint(cls, constraint) -> Optional[str]:
32150
constraint_prefix = "add constraint "
@@ -42,7 +160,10 @@ def render_model_constraint(cls, constraint) -> Optional[str]:
42160
if constraint.type == ConstraintType.unique:
43161
return constraint_prefix + f"{constraint.name} unique nonclustered({column_list})"
44162
elif constraint.type == ConstraintType.primary_key:
45-
return constraint_prefix + f"{constraint.name} primary key nonclustered({column_list})"
163+
return (
164+
constraint_prefix
165+
+ f"{constraint.name} primary key nonclustered({column_list})"
166+
)
46167
elif constraint.type == ConstraintType.foreign_key and constraint.expression:
47168
return (
48169
constraint_prefix
@@ -56,10 +177,6 @@ def render_model_constraint(cls, constraint) -> Optional[str]:
56177
else:
57178
return None
58179

59-
@classmethod
60-
def date_function(cls):
61-
return "getdate()"
62-
63180
def valid_incremental_strategies(self):
64181
"""The set of standard builtin strategies which this adapter supports out-of-the-box.
65182
Not used to validate custom strategies defined by end users.
Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,96 @@
1-
from dbt.adapters.fabric import FabricColumn
1+
from typing import Any, ClassVar, Dict
22

3+
from dbt.adapters.base import Column
4+
from dbt_common.exceptions import DbtRuntimeError
5+
6+
7+
class SQLServerColumn(Column):
8+
TYPE_LABELS: ClassVar[Dict[str, str]] = {
9+
"STRING": "VARCHAR(8000)",
10+
"VARCHAR": "VARCHAR(8000)",
11+
"CHAR": "CHAR(1)",
12+
"NCHAR": "CHAR(1)",
13+
"NVARCHAR": "VARCHAR(8000)",
14+
"TIMESTAMP": "DATETIME2(6)",
15+
"DATETIME2": "DATETIME2(6)",
16+
"DATETIME2(6)": "DATETIME2(6)",
17+
"DATE": "DATE",
18+
"TIME": "TIME(6)",
19+
"FLOAT": "FLOAT",
20+
"REAL": "REAL",
21+
"INT": "INT",
22+
"INTEGER": "INT",
23+
"BIGINT": "BIGINT",
24+
"SMALLINT": "SMALLINT",
25+
"TINYINT": "SMALLINT",
26+
"BIT": "BIT",
27+
"BOOLEAN": "BIT",
28+
"DECIMAL": "DECIMAL",
29+
"NUMERIC": "NUMERIC",
30+
"MONEY": "DECIMAL",
31+
"SMALLMONEY": "DECIMAL",
32+
"UNIQUEIDENTIFIER": "UNIQUEIDENTIFIER",
33+
"VARBINARY": "VARBINARY(MAX)",
34+
"BINARY": "BINARY(1)",
35+
}
36+
37+
@classmethod
38+
def string_type(cls, size: int) -> str:
39+
return f"varchar({size if size > 0 else '8000'})"
40+
41+
def literal(self, value: Any) -> str:
42+
return "cast('{}' as {})".format(value, self.data_type)
43+
44+
@property
45+
def data_type(self) -> str:
46+
if self.dtype.lower() == "datetime2":
47+
return "datetime2(6)"
48+
if self.is_string():
49+
return self.string_type(self.string_size())
50+
elif self.is_numeric():
51+
return self.numeric_type(self.dtype, self.numeric_precision, self.numeric_scale)
52+
else:
53+
return self.dtype
54+
55+
def is_string(self) -> bool:
56+
return self.dtype.lower() in ["varchar", "char"]
57+
58+
def is_number(self):
59+
return any([self.is_integer(), self.is_numeric(), self.is_float()])
60+
61+
def is_float(self):
62+
return self.dtype.lower() in ["float", "real"]
363

4-
class SQLServerColumn(FabricColumn):
564
def is_integer(self) -> bool:
665
return self.dtype.lower() in [
7-
# real types
866
"smallint",
967
"integer",
1068
"bigint",
1169
"smallserial",
1270
"serial",
1371
"bigserial",
14-
# aliases
1572
"int2",
1673
"int4",
1774
"int8",
1875
"serial2",
1976
"serial4",
2077
"serial8",
2178
"int",
79+
"tinyint",
2280
]
81+
82+
def is_numeric(self) -> bool:
83+
return self.dtype.lower() in ["numeric", "decimal", "money", "smallmoney"]
84+
85+
def string_size(self) -> int:
86+
if not self.is_string():
87+
raise DbtRuntimeError("Called string_size() on non-string field!")
88+
if self.char_size is None:
89+
return 8000
90+
else:
91+
return int(self.char_size)
92+
93+
def can_expand_to(self, other_column: "SQLServerColumn") -> bool:
94+
if not self.is_string() or not other_column.is_string():
95+
return False
96+
return other_column.string_size() > self.string_size()
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from dataclasses import dataclass
22

3-
from dbt.adapters.fabric import FabricConfigs
3+
from dbt.adapters.protocol import AdapterConfig
44

55

66
@dataclass
7-
class SQLServerConfigs(FabricConfigs):
7+
class SQLServerConfigs(AdapterConfig):
88
pass

0 commit comments

Comments
 (0)