Skip to content

Commit dbc42e4

Browse files
dzsquaredMaxteabag
andauthored
begin pyodbc to mssql-python swap (#35)
* begin pyodbc to mssql-python swap * correct non-optional dep blunder * Remove ODBC driver selection infrastructure - Delete sqlit/drivers.py (ODBC driver detection) - Delete sqlit/ui/screens/driver_setup.py (ODBC driver setup screen) - Remove DriverSetupScreen exports from sqlit/ui/__init__.py - Remove DriverSetupScreen exports from sqlit/ui/screens/__init__.py mssql-python doesn't require external ODBC drivers, so this infrastructure is no longer needed. * Remove MissingODBCDriverError and handler - Remove MissingODBCDriverError class from sqlit/db/exceptions.py - Remove MissingOdbcDriverHandler from connection_error_handlers.py ODBC driver errors are no longer possible with mssql-python. * Remove ODBC driver field from MSSQL schema - Remove driver field from MSSQL_SCHEMA in sqlit/db/schema.py - Remove SUPPORTED_DRIVERS import - Remove _get_default_driver function from sqlit/config.py mssql-python handles connections directly without driver selection. * Remove Advanced tab from connection screen - Remove Advanced TabPane from connection dialog - Remove _split_groups_by_advanced method - Remove _set_advanced_tab_enabled method - Remove _show_advanced property - Remove all tab-advanced references from navigation and validation - Simplify field rendering (no longer splits general/advanced) The Advanced tab only contained the ODBC driver selector which is no longer needed with mssql-python. * Clean up MSSQL adapter for mssql-python - Remove driver selection logic from normalize_config - Update docstring for driver_setup_kind property - Update comment about sql_variant columns * Update Arch Linux package mapping for mssql-python Replace pyodbc -> python-pyodbc with mssql-python -> python-mssql in the Arch Linux package name mapping. * Update docs: pyodbc -> mssql-python - Update driver reference table in README.md - Remove note about ODBC driver requirement - Remove pyodbc mypy override from pyproject.toml * Remove ODBC driver integration tests Delete tests/integration/drivers/ directory containing: - ODBC driver installation tests - ODBC driver UI flow tests - Docker test infrastructure for driver testing - Test artifacts and screenshots These tests are obsolete with mssql-python which doesn't require ODBC driver installation. * Update tests for mssql-python migration - Update test_mssql_datetimeoffset.py docstrings - Remove driver field from test fixtures in test_credentials_service.py - Remove pyodbc reference from test_database_base.py - Remove Advanced tab tests from test_connection_screen.py * Update CI to use mssql-python instead of pyodbc - Remove ODBC driver installation step (no longer needed) - Change pip install pyodbc to pip install mssql-python --------- Co-authored-by: Peter Adams <18162810+Maxteabag@users.noreply.github.com>
1 parent e78edfc commit dbc42e4

69 files changed

Lines changed: 181 additions & 5644 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,11 @@ jobs:
120120
with:
121121
python-version: "3.12"
122122

123-
- name: Install ODBC driver
124-
run: |
125-
curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc
126-
curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
127-
sudo apt-get update
128-
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
129-
130123
- name: Install dependencies
131124
run: |
132125
python -m pip install --upgrade pip
133126
pip install -e ".[test]"
134-
pip install pyodbc
127+
pip install mssql-python
135128
136129
- name: Wait for SQL Server to be ready
137130
run: |

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ Most of the time you can just run `sqlit` and connect. If a Python driver is mis
239239
| :---------------------------------- | :--------------------------- | :------------------------------------------------- | :------------------------------------------------- |
240240
| SQLite | *(built-in)* | *(built-in)* | *(built-in)* |
241241
| PostgreSQL / CockroachDB / Supabase | `psycopg2-binary` | `pipx inject sqlit-tui psycopg2-binary` | `python -m pip install psycopg2-binary` |
242-
| SQL Server | `pyodbc` | `pipx inject sqlit-tui pyodbc` | `python -m pip install pyodbc` |
242+
| SQL Server | `mssql-python` | `pipx inject sqlit-tui mssql-python` | `python -m pip install mssql-python` |
243243
| MySQL | `mysql-connector-python` | `pipx inject sqlit-tui mysql-connector-python` | `python -m pip install mysql-connector-python` |
244244
| MariaDB | `mariadb` | `pipx inject sqlit-tui mariadb` | `python -m pip install mariadb` |
245245
| Oracle | `oracledb` | `pipx inject sqlit-tui oracledb` | `python -m pip install oracledb` |
@@ -250,8 +250,6 @@ Most of the time you can just run `sqlit` and connect. If a Python driver is mis
250250
| Snowflake | `snowflake-connector-python` | `pipx inject sqlit-tui snowflake-connector-python` | `python -m pip install snowflake-connector-python` |
251251
| Firebird | `firebirdsql` | `pipx inject sqlit-tui firebirdsql` | `python -m pip install firebirdsql` |
252252

253-
**Note:** SQL Server also requires the platform-specific ODBC driver. On your first connection attempt, `sqlit` can help you install it if it's missing.
254-
255253
### SSH Tunnel Support
256254

257255
SSH tunnel functionality requires additional dependencies. Install with the `ssh` extra:

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ dependencies = [
3030
"textual-fastdatatable>=0.14.0",
3131
"pyperclip>=1.8.2",
3232
"keyring>=24.0.0",
33-
"docker>=7.0.0", # Docker container auto-detection (lazy loaded)
33+
"docker>=7.0.0",
3434
]
3535
dynamic = ["version"]
3636

3737
[project.optional-dependencies]
3838
all = [
3939
"psycopg2-binary>=2.9.0",
40-
"pyodbc>=5.0.0",
40+
"mssql-python>=1.1.0",
4141
"mysql-connector-python>=9.1.0", # min avoids known CVEs
4242
"mariadb>=1.1.0",
4343
"oracledb>=2.0.0",
@@ -52,7 +52,7 @@ all = [
5252
]
5353
postgres = ["psycopg2-binary>=2.9.0"]
5454
cockroachdb = ["psycopg2-binary>=2.9.0"]
55-
mssql = ["pyodbc>=5.0.0"]
55+
mssql = ["mssql-python>=1.1.0"]
5656
mysql = ["mysql-connector-python>=9.1.0"] # min avoids known CVEs
5757
mariadb = ["mariadb>=1.1.0"]
5858
oracle = ["oracledb>=2.0.0"]
@@ -169,5 +169,5 @@ module = [
169169
ignore_missing_imports = true
170170

171171
[[tool.mypy.overrides]]
172-
module = "pyodbc"
172+
module = "mssql_python"
173173
ignore_missing_imports = true

sqlit/config.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,6 @@ class AuthType(Enum):
144144
}
145145

146146

147-
def _get_default_driver() -> str:
148-
"""Get default ODBC driver (lazy import)."""
149-
from .drivers import SUPPORTED_DRIVERS
150-
return SUPPORTED_DRIVERS[0]
151-
152-
153147
@dataclass
154148
class ConnectionConfig:
155149
"""Database connection configuration."""

sqlit/db/adapters/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def execute_test_query(self, conn: Any) -> None:
239239

240240
@property
241241
def driver_setup_kind(self) -> str | None:
242-
"""Optional driver setup type for UI workflows (e.g., 'odbc')."""
242+
"""Optional driver setup type for UI workflows."""
243243
return None
244244

245245
def normalize_config(self, config: ConnectionConfig) -> ConnectionConfig:

sqlit/db/adapters/mssql.py

Lines changed: 17 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Microsoft SQL Server adapter using pyodbc."""
1+
"""Microsoft SQL Server adapter using mssql-python."""
22

33
from __future__ import annotations
44

@@ -10,24 +10,8 @@
1010
from ...config import ConnectionConfig
1111

1212

13-
def _convert_datetimeoffset(value: bytes) -> str:
14-
"""Convert SQL Server datetimeoffset binary to ISO 8601 string.
15-
16-
The binary format is 20 bytes: year(2), month(2), day(2), hour(2),
17-
minute(2), second(2), nanoseconds(4), tz_hour(2), tz_minute(2).
18-
See: https://github.com/mkleehammer/pyodbc/issues/134
19-
"""
20-
import struct
21-
22-
tup = struct.unpack("<6hI2h", value)
23-
year, month, day, hour, minute, second, ns, tz_hour, tz_min = tup
24-
microseconds = ns // 1000
25-
tz_sign = "+" if tz_hour >= 0 else "-"
26-
return f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}.{microseconds:06d} {tz_sign}{abs(tz_hour):02d}:{abs(tz_min):02d}"
27-
28-
2913
class SQLServerAdapter(DatabaseAdapter):
30-
"""Adapter for Microsoft SQL Server using pyodbc."""
14+
"""Adapter for Microsoft SQL Server using the mssql-python driver."""
3115

3216
@classmethod
3317
def badge_label(cls) -> str:
@@ -47,11 +31,13 @@ def install_extra(self) -> str:
4731

4832
@property
4933
def install_package(self) -> str:
50-
return "pyodbc"
34+
# Package providing the SQL Server driver (no external ODBC manager required)
35+
return "mssql-python"
5136

5237
@property
5338
def driver_import_names(self) -> tuple[str, ...]:
54-
return ("pyodbc",)
39+
# DB-API 2.0 compatible driver
40+
return ("mssql_python",)
5541

5642
@property
5743
def supports_multiple_databases(self) -> bool:
@@ -89,7 +75,9 @@ def supports_sequences(self) -> bool:
8975

9076
@property
9177
def driver_setup_kind(self) -> str | None:
92-
return "odbc"
78+
# mssql-python uses Direct Database Connectivity and does not require
79+
# a separate ODBC driver manager.
80+
return None
9381

9482
@classmethod
9583
def docker_image_patterns(cls) -> tuple[str, ...]:
@@ -123,36 +111,24 @@ def normalize_config(self, config: ConnectionConfig) -> ConnectionConfig:
123111
config.set_option("auth_type", "sql")
124112
config.set_option("trusted_connection", False)
125113

126-
driver = config.get_option("driver")
127-
if not driver:
128-
from ...config import _get_default_driver
129-
130-
config.set_option("driver", _get_default_driver())
131-
132114
return config
133115

134116
def _build_connection_string(self, config: ConnectionConfig) -> str:
135-
"""Build ODBC connection string from config.
117+
"""Build mssql-python connection string from config.
136118
137119
Args:
138120
config: Connection configuration.
139121
140122
Returns:
141-
ODBC connection string for pyodbc.
123+
semicolon-delimited key=value connection string.
142124
"""
143125
from ...config import AuthType
144126

145127
server_with_port = config.server
146128
if config.port and config.port != "1433":
147129
server_with_port = f"{config.server},{config.port}"
148130

149-
driver = config.get_option("driver")
150-
if not driver:
151-
from ...config import _get_default_driver
152-
153-
driver = _get_default_driver()
154131
base = (
155-
f"DRIVER={{{driver}}};"
156132
f"SERVER={server_with_port};"
157133
f"DATABASE={config.database or 'master'};"
158134
f"TrustServerCertificate=yes;"
@@ -163,7 +139,7 @@ def _build_connection_string(self, config: ConnectionConfig) -> str:
163139
if auth == AuthType.WINDOWS:
164140
return base + "Trusted_Connection=yes;"
165141
elif auth == AuthType.SQL_SERVER:
166-
return base + f"UID={config.username};PWD={config.password};"
142+
return base + f"Authentication=SqlPassword;" f"UID={config.username};PWD={config.password};"
167143
elif auth == AuthType.AD_PASSWORD:
168144
return base + f"Authentication=ActiveDirectoryPassword;" f"UID={config.username};PWD={config.password};"
169145
elif auth == AuthType.AD_INTERACTIVE:
@@ -174,30 +150,16 @@ def _build_connection_string(self, config: ConnectionConfig) -> str:
174150
return base + "Trusted_Connection=yes;"
175151

176152
def connect(self, config: ConnectionConfig) -> Any:
177-
"""Connect to SQL Server using pyodbc."""
178-
pyodbc = import_driver_module(
179-
"pyodbc",
153+
"""Connect to SQL Server using the mssql-python driver."""
154+
mssql_python = import_driver_module(
155+
"mssql_python",
180156
driver_name=self.name,
181157
extra_name=self.install_extra,
182158
package_name=self.install_package,
183159
)
184160

185-
installed = list(pyodbc.drivers())
186-
driver = config.get_option("driver")
187-
if not driver:
188-
from ...config import _get_default_driver
189-
190-
driver = _get_default_driver()
191-
if driver not in installed:
192-
from ...db.exceptions import MissingODBCDriverError
193-
194-
raise MissingODBCDriverError(driver, installed)
195-
196161
conn_str = self._build_connection_string(config)
197-
conn = pyodbc.connect(conn_str, timeout=10)
198-
199-
# Register converter for datetimeoffset (ODBC type -155) which pyodbc doesn't support natively
200-
conn.add_output_converter(-155, _convert_datetimeoffset)
162+
conn = mssql_python.connect(conn_str)
201163

202164
return conn
203165

@@ -451,7 +413,7 @@ def get_sequence_definition(
451413
) -> dict[str, Any]:
452414
"""Get detailed information about a SQL Server sequence."""
453415
cursor = conn.cursor()
454-
# Cast sql_variant columns to BIGINT to avoid pyodbc type -25 error
416+
# Cast sql_variant columns to BIGINT to avoid driver type conversion errors
455417
if database:
456418
cursor.execute(
457419
f"SELECT CAST(start_value AS BIGINT), CAST(increment AS BIGINT), "

sqlit/db/exceptions.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,3 @@ def __init__(
1919
self.module_name = module_name
2020
self.import_error = import_error
2121
super().__init__(f"Missing driver for {driver_name}")
22-
23-
24-
class MissingODBCDriverError(ConnectionError):
25-
"""Exception raised when a required ODBC driver is not installed (SQL Server)."""
26-
27-
def __init__(self, selected_driver: str, installed_drivers: list[str]):
28-
self.selected_driver = selected_driver
29-
self.installed_drivers = installed_drivers
30-
super().__init__(f"Missing ODBC driver: {selected_driver}")

sqlit/db/schema.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
from dataclasses import dataclass
1111
from enum import Enum
1212

13-
from ..drivers import SUPPORTED_DRIVERS
14-
1513

1614
class FieldType(Enum):
1715
TEXT = "text"
@@ -201,10 +199,6 @@ def _get_ssh_fields() -> tuple[SchemaField, ...]:
201199
SSH_FIELDS = _get_ssh_fields()
202200

203201

204-
def _get_mssql_driver_options() -> tuple[SelectOption, ...]:
205-
return tuple(SelectOption(d, d) for d in SUPPORTED_DRIVERS)
206-
207-
208202
def _get_mssql_auth_options() -> tuple[SelectOption, ...]:
209203
return (
210204
SelectOption("sql", "SQL Server Authentication"),
@@ -234,14 +228,6 @@ def _get_mssql_auth_options() -> tuple[SelectOption, ...]:
234228
),
235229
_port_field("1433"),
236230
_database_field(),
237-
SchemaField(
238-
name="driver",
239-
label="Driver",
240-
field_type=FieldType.SELECT,
241-
options=_get_mssql_driver_options(),
242-
default=SUPPORTED_DRIVERS[0],
243-
advanced=True,
244-
),
245231
SchemaField(
246232
name="auth_type",
247233
label="Authentication",

0 commit comments

Comments
 (0)