Skip to content

Commit eb98b14

Browse files
committed
Merge remote-tracking branch 'origin/master' into 753-binary-decimal
# Conflicts: # src/connection.cpp # src/pyodbc.pyi
2 parents 90c5999 + e8b69a2 commit eb98b14

25 files changed

Lines changed: 1000 additions & 83 deletions

.github/workflows/ubuntu_build.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
15+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1616

1717
services:
1818

@@ -75,10 +75,9 @@ jobs:
7575
cat /etc/odbc.ini
7676
7777
- name: Install ODBC driver for SQL Server
78-
# version 18.6.1.1-1 causes a segmentation fault in _test_tvp() for MS SQL Server, hence use the earlier version
7978
run: |
8079
echo "*** apt-get install the driver"
81-
sudo ACCEPT_EULA=Y apt-get install --yes msodbcsql18=18.5.1.1-1
80+
sudo ACCEPT_EULA=Y apt-get install --yes msodbcsql18
8281
echo '*** ls -l /opt/microsoft/msodbcsql18/lib64'
8382
ls -l /opt/microsoft/msodbcsql18/lib64 || true
8483

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ pyodbc.conf
5858
tmp
5959
tags
6060
xxx_*
61+
.DS_Store
6162

6263
# The Access unit tests copy empty.accdb and empty.mdb to these names and use them.
6364
test.accdb
6465
test.mdb
66+
67+
# Claude Code scratch space (throwaway repro scripts, experiments) — never committed
68+
/.claude/scratch/

CLAUDE.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this
4+
repository.
5+
6+
## What this is
7+
8+
pyodbc is a Python module implementing the DB API 2.0 spec on top of ODBC. It is a **C++
9+
extension** (everything in `src/` compiles into a single `pyodbc` shared library); there is no
10+
Python source for the runtime — `src/pyodbc.pyi` is only a type stub. The package targets
11+
Python 3.10+ and links against an ODBC driver manager (unixODBC/iODBC on Unix, built-in on
12+
Windows, via `odbc_config`).
13+
14+
## Build & test
15+
16+
Fast development loop (build in place, then run pytest from the repo root so the freshly built
17+
library is on the path):
18+
19+
```sh
20+
python setup.py build_ext --inplace
21+
pytest tests/sqlite_test.py -vxk test_text # single test by name substring
22+
```
23+
24+
To debug a crash, build with tracing and pass `-s` (pytest otherwise swallows output on a
25+
segfault):
26+
27+
```sh
28+
python setup.py build_ext --inplace -D PYODBC_TRACE
29+
pytest tests/sqlite_test.py -vxs -k test_text
30+
```
31+
32+
Full multi-version test matrix uses tox (`pipx install tox`), covering py310–py314:
33+
34+
```sh
35+
tox # all interpreters + all databases
36+
tox -e py312 # one interpreter
37+
tox -e py312 -- -rA # pass pytest args after --
38+
```
39+
40+
Lint: `flake8` (max line length 95; see `.flake8`) and `pylint`. Install dev deps with `pip
41+
install -r requirements-dev.txt`.
42+
43+
### Test database configuration
44+
45+
There is one test file per backend (`tests/sqlite_test.py`, `sqlserver_test.py`,
46+
`postgresql_test.py`, `mysql_test.py`). Each reads its connection string from an environment
47+
variable, falling back to a DSN default:
48+
49+
- `PYODBC_SQLITE`, `PYODBC_SQLSERVER`, `PYODBC_POSTGRESQL`, `PYODBC_MYSQL`
50+
51+
Set these in the shell or in a `pytest.ini` (with `pytest-env`); `tox.ini`'s header has an
52+
example. SQLite needs no server (`driver={SQLite3 ODBC Driver};Database=:memory:`) and is the
53+
easiest target for quick iteration. The real database must be running and the matching ODBC
54+
driver installed before tests pass — see `.github/workflows/ubuntu_build.yml` for the exact
55+
driver names and connection strings CI uses. `tests/old/` holds legacy, unmaintained test
56+
scripts; ignore them.
57+
58+
Setting `PYODBC_TESTLOCAL=1` makes `tests/conftest.py` add the `build/` directory to
59+
`sys.path`, so you can test a `build_ext` output without pip-installing (do not have pyodbc
60+
installed in that environment if you use this).
61+
62+
## Scratch scripts vs. project tooling
63+
64+
Decide by **audience**, not by who happens to type the command:
65+
66+
- **Scripts only Claude runs** — issue-repro scripts, one-off experiments, debug helpers — go
67+
in **`.claude/scratch/`**. That directory is git-ignored (`/.claude/scratch/` in
68+
`.gitignore`) and must **never** be committed. It is local scratch space, not part of the
69+
project.
70+
- **Scripts a human would ever run** (e.g. a "build + test across Python versions" convenience
71+
wrapper) are ordinary project tooling. They belong in **`utils/`** (alongside the existing
72+
`build-releases.sh`/`.cmd`), committed and reviewed like any other code with a neutral name —
73+
not in an "AI" directory. The moment a script has to be readable/maintained for a human, it
74+
is bucket two.
75+
76+
Before writing a human-facing test runner, note that **`tox` already builds and tests across
77+
all supported Python versions** (see "Build & test"); a custom runner is only a thin
78+
convenience and often unnecessary.
79+
80+
## Architecture
81+
82+
Each major ODBC object is a `PyTypeObject` defined in its own `.cpp`/`.h` pair. The object
83+
lifecycle is **Connection → Cursor → Row**:
84+
85+
- **`pyodbcmodule.cpp`** — module entry point (`PyInit_pyodbc`), the `connect()` factory,
86+
global state (the shared `HENV`, pooling, `lowercase`, ODBC version), exception class
87+
definitions, and per-thread caching of exception classes via `GetClassForThread`.
88+
- **`connection.cpp`** — the `Connection` type. Owns the `HDBC`, autocommit/transaction
89+
control, and the four `TextEnc` encodings used for the connection (read SQL_CHAR, read
90+
SQL_WCHAR, write unicode, read metadata — see "text encoding" below). Output-converter
91+
registration lives here.
92+
- **`cursor.cpp`** — the `Cursor` type (the largest/most central file). Drives
93+
`execute`/`executemany`/`fetch*`, builds the DB API `description`, the column-name→index map
94+
shared with rows, and `messages`.
95+
- **`params.cpp`** — binding Python parameters *into* SQL statements (`SQLBindParameter`), type
96+
detection, NULL handling via `SQLDescribeParam`, table-valued parameters (TVPs), and the
97+
array-binding "fast executemany" path. (Note: a header comment flags fast-executemany as
98+
being re-ported across the 4.x→5.x rewrite.)
99+
- **`getdata.cpp`** — the reverse direction: converting fetched SQL column data *out* into
100+
Python objects, including user-defined output converters (`GetUserConvIndex`).
101+
- **`row.cpp`** — the `Row` type: tuple-like, also supports access by column name via the
102+
shared name→index map, usable after the cursor/connection closes.
103+
- **`cnxninfo.cpp`** — caches per-connection-string driver capabilities (`CnxnInfo`: ODBC
104+
version, whether `SQLDescribeParam` is supported, datetime precision, type max-lengths,
105+
`need_long_data_len`) so they aren't re-probed on every connect.
106+
- **`errors.cpp`** — maps ODBC `SQLSTATE` codes to the DB API exception hierarchy
107+
(`RaiseErrorFromHandle`, `RaiseErrorV`).
108+
- **`textenc.cpp`** / **`decimal.cpp`** — text encode/decode helpers and `Decimal` support.
109+
- **`dbspecific.h`** — constants for non-standard driver types (SQL Server variant/XML/TIME2,
110+
DB2 DECFLOAT). **`wrapper.h`**`Object`, an RAII wrapper for `PyObject*` refcounts;
111+
**`pyodbc.h`** — the umbrella header (platform shims, ODBC headers, `TRACE`).
112+
113+
### Text encoding is the subtle part
114+
115+
Drivers disagree wildly about Unicode, so encoding is *not* uniform. A separate `TextEnc` is
116+
configured for reading SQL_CHAR, reading SQL_WCHAR, writing unicode strings, and **reading
117+
metadata** (column names). Metadata gets its own encoding because PostgreSQL/MySQL return
118+
column names as UTF-16LE from `SQLDescribeCol` regardless of connection
119+
settings. `setencoding()`/`setdecoding()` on the Connection adjust these. The code deliberately
120+
uses `uint16_t`/`SQLWCHAR` rather than `wchar_t` because unixODBC may define `SQLWCHAR` as
121+
32-bit `wchar_t` while the buffer data is still 16-bit (see `HACKING.md`). When touching
122+
encoding code, consult `notes.txt` for the ODBC length-argument rules (count-of-characters
123+
vs. count-of-bytes).
124+
125+
## Versioning
126+
127+
The single source of truth for the version is the `version = "..."` line in
128+
`pyproject.toml`. `setup.py` parses it with a regex (to avoid a TOML dependency on old Pythons)
129+
and passes it to the compiler as the `PYODBC_VERSION` macro, which becomes
130+
`pyodbc.version`. cibuildwheel also reads `pyproject.toml` directly. Bump the version there
131+
only.

appveyor.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ environment:
5656
- PYTHON_HOME: "C:\\Python311"
5757
- PYTHON_HOME: "C:\\Python310-x64"
5858
- PYTHON_HOME: "C:\\Python310"
59-
- PYTHON_HOME: "C:\\Python39-x64"
60-
- PYTHON_HOME: "C:\\Python39"
6159

6260
# ref: https://www.appveyor.com/docs/services-databases/
6361
init:

appveyor/install.ps1

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,13 @@ CheckAndInstallMsiFromUrl `
220220
# 18.5 : https://download.microsoft.com/download/26bc9eb1-ba24-4b62-8274-bff0f935bb75/amd64/1033/msodbcsql.msi (2025-03-17)
221221
# 18.5.2 : https://download.microsoft.com/download/48a8e0c3-556b-4012-ba65-fcea935447f2/amd64/1033/msodbcsql.msi (2025-09-26)
222222
# 18.6 : https://download.microsoft.com/download/8d6e3acc-bf5b-41fe-ad51-a9ad406a780f/amd64/1033/msodbcsql.msi (2025-12-17)
223-
# NOTE 2026-02-14: version 18.6.1.1 causes the MS SQL _test_tvp() unit test to raise an access violation fault, so using 18.5.2 for the time being
223+
# 18.6.2 : https://download.microsoft.com/download/7bf9fad4-0f21-486d-a750-fc990ded5624/amd64/1033/msodbcsql.msi (2026-03-31)
224+
# NOTE: 18.6.1.1 caused an access violation in the MS SQL _test_tvp() unit test (#1459); fixed in 18.6.2.1
224225
CheckAndInstallMsiFromUrl `
225226
-driver_name "ODBC Driver 18 for SQL Server" `
226227
-driver_bitness "64-bit" `
227-
-driver_url "https://download.microsoft.com/download/48a8e0c3-556b-4012-ba65-fcea935447f2/amd64/1033/msodbcsql.msi" `
228-
-msifile_path "$cache_dir\msodbcsql_18.5.2.1_x64.msi" `
228+
-driver_url "https://download.microsoft.com/download/7bf9fad4-0f21-486d-a750-fc990ded5624/amd64/1033/msodbcsql.msi" `
229+
-msifile_path "$cache_dir\msodbcsql_18.6.2.1_x64.msi" `
229230
-msiexec_paras @("IACCEPTMSODBCSQLLICENSETERMS=YES", "ADDLOCAL=ALL");
230231

231232
# some drivers must be installed in alignment with Python's bitness

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[project]
22
name = "pyodbc"
3-
version = "5.3.0"
3+
version = "5.4.0"
44

5-
requires-python = ">=3.9"
5+
requires-python = ">=3.10"
66
# This is used by the GitHub action that builds release artifacts using cibuildwheel.
77
# cibuildwheel reads this directly:
88
#

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# https://peps.python.org/pep-0508/
33
# https://peps.python.org/pep-0440/
44

5-
pytest ~= 7.3
5+
pytest >= 9.0.3 # 9.0.3 fixes GHSA-6w46-j5rx-g56g (tmpdir handling); requires Python 3.10+
66

77
# I'm going to try leaving the versions off since we're supporting drastically different Python
88
# versions. I want the most up to date I can get in each, at least until one of them makes a

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def main():
5656
package_dir={'': 'src'},
5757
package_data={'': ['pyodbc.pyi']}, # places pyodbc.pyi alongside pyodbc.{platform}.{pyd|so} in site-packages
5858
license='MIT-0',
59-
python_requires='>=3.9',
59+
python_requires='>=3.10',
6060
classifiers=['Development Status :: 5 - Production/Stable',
6161
'Intended Audience :: Developers',
6262
'Intended Audience :: System Administrators',
@@ -116,6 +116,7 @@ def get_compiler_settings():
116116

117117
settings['libraries'].append('odbc32')
118118
settings['libraries'].append('advapi32')
119+
settings['libraries'].append('user32')
119120

120121
elif os.environ.get("OS", '').lower().startswith('windows'):
121122
# Windows Cygwin (posix on windows)

0 commit comments

Comments
 (0)