sqlalchemy-monetdb-adbc 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. sqlalchemy_monetdb_adbc-0.1.0/.gitattributes +2 -0
  2. sqlalchemy_monetdb_adbc-0.1.0/.github/dependabot.yml +16 -0
  3. sqlalchemy_monetdb_adbc-0.1.0/.github/workflows/ci.yml +167 -0
  4. sqlalchemy_monetdb_adbc-0.1.0/.gitignore +15 -0
  5. sqlalchemy_monetdb_adbc-0.1.0/AGENTS.md +47 -0
  6. sqlalchemy_monetdb_adbc-0.1.0/CONTRIBUTING.md +37 -0
  7. sqlalchemy_monetdb_adbc-0.1.0/LICENSE +21 -0
  8. sqlalchemy_monetdb_adbc-0.1.0/PKG-INFO +349 -0
  9. sqlalchemy_monetdb_adbc-0.1.0/README.md +319 -0
  10. sqlalchemy_monetdb_adbc-0.1.0/compose.yaml +15 -0
  11. sqlalchemy_monetdb_adbc-0.1.0/packaging/release_checks.py +97 -0
  12. sqlalchemy_monetdb_adbc-0.1.0/pyproject.toml +137 -0
  13. sqlalchemy_monetdb_adbc-0.1.0/setup.cfg +9 -0
  14. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/__init__.py +37 -0
  15. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/_alembic.py +74 -0
  16. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/_convert.py +107 -0
  17. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/arrow.py +99 -0
  18. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/base.py +623 -0
  19. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/compiler.py +410 -0
  20. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/connection.py +62 -0
  21. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/constants.py +39 -0
  22. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/ddl.py +33 -0
  23. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/dialect.py +252 -0
  24. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/multi_reflection.py +539 -0
  25. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/provision.py +119 -0
  26. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/py.typed +1 -0
  27. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/reflection.py +195 -0
  28. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/requirements.py +248 -0
  29. sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/types.py +275 -0
  30. sqlalchemy_monetdb_adbc-0.1.0/suite/conftest.py +34 -0
  31. sqlalchemy_monetdb_adbc-0.1.0/suite/known_failures.py +45 -0
  32. sqlalchemy_monetdb_adbc-0.1.0/suite/test_suite.py +1 -0
  33. sqlalchemy_monetdb_adbc-0.1.0/tests/conftest.py +24 -0
  34. sqlalchemy_monetdb_adbc-0.1.0/tests/test_alembic.py +190 -0
  35. sqlalchemy_monetdb_adbc-0.1.0/tests/test_convert.py +135 -0
  36. sqlalchemy_monetdb_adbc-0.1.0/tests/test_dialect.py +330 -0
  37. sqlalchemy_monetdb_adbc-0.1.0/tests/test_integration.py +969 -0
  38. sqlalchemy_monetdb_adbc-0.1.0/tests/test_local_benchmark.py +108 -0
  39. sqlalchemy_monetdb_adbc-0.1.0/uv.lock +546 -0
@@ -0,0 +1,2 @@
1
+ * text=auto eol=lf
2
+ *.bat text eol=crlf
@@ -0,0 +1,16 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ groups:
8
+ actions:
9
+ patterns: ["*"]
10
+ - package-ecosystem: uv
11
+ directory: /
12
+ schedule:
13
+ interval: weekly
14
+ groups:
15
+ python:
16
+ patterns: ["*"]
@@ -0,0 +1,167 @@
1
+ name: CI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ push:
7
+ branches: [main]
8
+ tags: ["v*"]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ concurrency:
14
+ group: ${{ github.workflow }}-${{ github.ref }}
15
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
16
+
17
+ jobs:
18
+ test:
19
+ name: Python ${{ matrix.python-version }}
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ python-version: ["3.13", "3.14"]
25
+ steps:
26
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+ - run: uv sync --locked --all-groups
31
+ - run: uv run --no-sync ruff check .
32
+ - run: uv run --no-sync ruff format --check .
33
+ - run: uv run --no-sync pyright
34
+ - run: uv run --no-sync pytest
35
+
36
+ platform:
37
+ name: Platform (${{ matrix.os }}, Python ${{ matrix.python-version }})
38
+ runs-on: ${{ matrix.os }}
39
+ strategy:
40
+ fail-fast: false
41
+ matrix:
42
+ os: [macos-14, windows-latest]
43
+ python-version: ["3.13", "3.14"]
44
+ steps:
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
47
+ with:
48
+ python-version: ${{ matrix.python-version }}
49
+ - run: uv sync --locked --all-groups
50
+ - run: uv run --no-sync pytest
51
+
52
+ integration:
53
+ name: Integration (MonetDB ${{ matrix.monetdb-version }})
54
+ runs-on: ubuntu-24.04-arm
55
+ strategy:
56
+ fail-fast: false
57
+ matrix:
58
+ monetdb-version: ["11.55.7-2"]
59
+ services:
60
+ monetdb:
61
+ image: wlaur/monetdb-container:${{ matrix.monetdb-version }}@sha256:22dd41bd10fd7136ca1ef6afaec5cf0c1670f16a84b80785a5fee12aac480eb4
62
+ ports:
63
+ - 50000:50000
64
+ env:
65
+ MDB_DB_ADMIN_PASS: monetdb
66
+ MDB_CREATE_DBS: test
67
+ env:
68
+ MONETDB_TEST_URI: monetdb://monetdb:monetdb@localhost:50000/test
69
+ steps:
70
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
71
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
72
+ with:
73
+ python-version: "3.13"
74
+ - run: uv sync --locked --all-groups
75
+ - name: Wait for MonetDB
76
+ run: |
77
+ for _ in $(seq 1 60); do
78
+ if uv run --no-sync python -c "
79
+ from adbc_driver_monetdb import dbapi
80
+ dbapi.connect('$MONETDB_TEST_URI').close()
81
+ "; then exit 0; fi
82
+ sleep 2
83
+ done
84
+ echo "MonetDB did not become ready" >&2
85
+ exit 1
86
+ - name: Integration tests
87
+ run: uv run --no-sync pytest tests -m integration
88
+ - name: SQLAlchemy dialect compliance suite
89
+ run: |
90
+ uv run --no-sync pytest suite -o addopts="" \
91
+ --dburi "$MONETDB_TEST_URI"
92
+
93
+ build:
94
+ name: Build distributions
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
98
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
99
+ with:
100
+ python-version: "3.13"
101
+ - name: Build and validate the release artifact set
102
+ shell: bash
103
+ run: |
104
+ uv build --out-dir dist
105
+ python packaging/release_checks.py artifacts dist --version "$(uv version --short)"
106
+ - name: Smoke-test wheel
107
+ shell: bash
108
+ run: |
109
+ wheel=$(find dist -maxdepth 1 -name '*.whl' -print -quit)
110
+ wheel=$(realpath "$wheel")
111
+ cd "$RUNNER_TEMP"
112
+ uv run --isolated --no-project --python 3.13 --with "$wheel" \
113
+ python -c "from sqlalchemy.dialects import registry; from sqlalchemy_monetdb_adbc.constants import DIALECT_NAMES; assert all(registry.load(name).name == 'monetdb' for name in DIALECT_NAMES)"
114
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
115
+ with:
116
+ name: distributions
117
+ path: dist/*
118
+ if-no-files-found: error
119
+
120
+ publish:
121
+ name: Publish to PyPI
122
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
123
+ needs: [test, platform, integration, build]
124
+ runs-on: ubuntu-latest
125
+ environment:
126
+ name: pypi
127
+ url: https://pypi.org/project/sqlalchemy-monetdb-adbc/
128
+ permissions:
129
+ id-token: write
130
+ contents: read
131
+ steps:
132
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
133
+ with:
134
+ fetch-depth: 0
135
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
136
+ with:
137
+ name: distributions
138
+ path: dist
139
+ - name: Verify protected-main provenance and release artifacts
140
+ run: |
141
+ git fetch origin main --no-tags
142
+ python packaging/release_checks.py provenance \
143
+ --tag "$GITHUB_REF_NAME" --commit "$GITHUB_SHA" --main-ref origin/main
144
+ python packaging/release_checks.py artifacts dist --version "${GITHUB_REF_NAME#v}"
145
+ - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
146
+
147
+ github-release:
148
+ name: Create GitHub release
149
+ needs: publish
150
+ runs-on: ubuntu-latest
151
+ permissions:
152
+ contents: write
153
+ steps:
154
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
155
+ with:
156
+ name: distributions
157
+ path: dist
158
+ - name: Create release with checksums and generated notes
159
+ env:
160
+ GH_TOKEN: ${{ github.token }}
161
+ shell: bash
162
+ run: |
163
+ cd dist
164
+ sha256sum sqlalchemy_monetdb_adbc-* > SHA256SUMS
165
+ gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \
166
+ --verify-tag --generate-notes \
167
+ sqlalchemy_monetdb_adbc-* SHA256SUMS
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .pyright/
10
+ .coverage
11
+ htmlcov/
12
+
13
+ .DS_Store
14
+ .idea/
15
+ .vscode/
@@ -0,0 +1,47 @@
1
+ # Working on sqlalchemy-monetdb-adbc
2
+
3
+ This repository provides the SQLAlchemy dialect for `adbc-driver-monetdb`. It is a
4
+ pure-Python integration package: native protocol and Arrow behavior belong in the
5
+ driver, while SQLAlchemy compilation, connection adaptation, and reflection belong
6
+ here.
7
+
8
+ ## Python
9
+
10
+ - Support Python 3.13 and newer.
11
+ - Use `uv` only, never `pip`, and commit `uv.lock`.
12
+ - Use full typing. Strict pyright, Ruff check/format, and pytest must pass.
13
+ - Do not add `pymonetdb` or `sqlalchemy-monetdb` as runtime dependencies.
14
+ - Do not add unnecessary comments, docstrings, or compatibility fallbacks.
15
+
16
+ ## Connection and transaction boundaries
17
+
18
+ - `monetdb://`, `monetdb+adbc://`, `monetdbs://`, and `monetdbs+adbc://` must
19
+ create one ADBC DBAPI connection per SQLAlchemy connection. The secure schemes must
20
+ remain secure when translated to driver URIs.
21
+ - DDL, SQL execution, Arrow reads, and `adbc_ingest` participating in one unit of work
22
+ must use that same physical connection. Do not introduce a second MonetDB session.
23
+ - Preserve SQLAlchemy's transaction contract while exposing a documented way to reach
24
+ the raw ADBC connection for Arrow-native operations.
25
+ - The driver owns ADBC behavior and MonetDB wire semantics. Do not duplicate or patch
26
+ those layers in this package.
27
+
28
+ ## Testing
29
+
30
+ - Unit gates: `uv run ruff check .`, `uv run ruff format --check .`,
31
+ `uv run pyright`, and `uv run pytest`.
32
+ - Integration tests use the pinned native ARM64 MonetDB image documented by
33
+ `adbc-driver-monetdb` and skip when no test URI is configured.
34
+ - Add regression tests for transaction ownership before changing connection or pool
35
+ behavior.
36
+
37
+ ## Packaging
38
+
39
+ - The distribution is `sqlalchemy-monetdb-adbc`, the import package is
40
+ `sqlalchemy_monetdb_adbc`, and the SQLAlchemy entry points are `monetdb`,
41
+ `monetdb.adbc`, `monetdbs`, and `monetdbs.adbc`.
42
+ - Do not install this package alongside `sqlalchemy-monetdb`; both distributions
43
+ register the bare `monetdb` entry point.
44
+ - Build both wheel and sdist with `uv build` and smoke-test the built wheel outside the
45
+ repository before release.
46
+ - Keep the public repository self-contained. Do not commit private planning notes,
47
+ benchmark results, credentials, or local infrastructure details.
@@ -0,0 +1,37 @@
1
+ # Contributing
2
+
3
+ Create a branch from `main`, keep changes focused, and include tests for behavior
4
+ changes.
5
+
6
+ Set up the environment and run every local gate:
7
+
8
+ ```console
9
+ uv sync --all-groups
10
+ uv run ruff check .
11
+ uv run ruff format --check .
12
+ uv run pyright
13
+ uv run pytest
14
+ uv build
15
+ ```
16
+
17
+ Integration work must use the MonetDB image and connection rules documented in
18
+ `AGENTS.md`. Do not run live tests against an unrelated development container.
19
+
20
+ ## Review matrix
21
+
22
+ A passing happy-path test is not evidence for adjacent states. Reflection work
23
+ must cover `schema=None`, explicit and missing schemas; absent, empty and
24
+ non-empty filters; default, temporary and combined scopes; permanent/temporary
25
+ name collisions; and same-schema, cross-schema and self-referential foreign
26
+ keys.
27
+
28
+ Connection work must cover the default transaction mode and autocommit,
29
+ commit, rollback and pool reset, complete and partial result consumption, and
30
+ raw ADBC operations sharing the SQLAlchemy session. Packaging changes must
31
+ exercise every declared dialect entry point from the built wheel outside the
32
+ checkout.
33
+
34
+ Treat test names, documentation claims, support metadata and repository
35
+ settings as assertions that need executable gates. A dependency release must
36
+ be available from its public registry before this lockfile adopts it, and the
37
+ driver must run this dialect against its candidate wheel before publishing.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 William Laurén
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,349 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-monetdb-adbc
3
+ Version: 0.1.0
4
+ Summary: SQLAlchemy dialect for MonetDB over ADBC
5
+ Project-URL: Homepage, https://github.com/wlaur/sqlalchemy-monetdb-adbc
6
+ Project-URL: Repository, https://github.com/wlaur/sqlalchemy-monetdb-adbc
7
+ Project-URL: Issues, https://github.com/wlaur/sqlalchemy-monetdb-adbc/issues
8
+ Author-email: William Laurén <lauren.william.a@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: adbc,arrow,database,dialect,monetdb,sqlalchemy
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.13
22
+ Requires-Dist: adbc-driver-manager>=1.11
23
+ Requires-Dist: adbc-driver-monetdb>=0.8.5
24
+ Requires-Dist: numpy>=2.1
25
+ Requires-Dist: pyarrow>=25
26
+ Requires-Dist: pydantic>=2
27
+ Requires-Dist: sqlalchemy>=2.0.34
28
+ Requires-Dist: tzdata>=2025.2; sys_platform == 'win32'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # sqlalchemy-monetdb-adbc
32
+
33
+ A SQLAlchemy dialect for MonetDB backed by
34
+ [`adbc-driver-monetdb`](https://github.com/wlaur/adbc-driver-monetdb).
35
+
36
+ The package is pure Python. It gives SQLAlchemy and Arrow-native ADBC operations a
37
+ single MonetDB connection and transaction boundary, without depending on `pymonetdb`
38
+ or `sqlalchemy-monetdb`.
39
+
40
+ ## Status
41
+
42
+ The dialect covers SQL compilation, types, reflection, transactions, and the ORM,
43
+ and is validated against a live MonetDB server.
44
+
45
+ ### Dialect compliance suite
46
+
47
+ SQLAlchemy's own dialect suite runs from `suite/` (see Development) and reports
48
+ no failures: 1237 passed, 16 xfailed, 388 skipped. Each xfail is a MonetDB or
49
+ driver limitation, listed with its reason in `suite/known_failures.py`. They are
50
+ marked xfail rather than skipped so that the tests still run, and report XPASS
51
+ if a future release gains the behaviour:
52
+
53
+ - MonetDB infers no type for a bare parameter, so `WHERE ? = ?` is rejected
54
+ outright. A cast resolves it, but SQLAlchemy emits the parameter alone. This
55
+ is most of the remainder.
56
+ - Untyped parameters also change arithmetic: `SELECT ? / ?` with `(15, 10)`
57
+ returns `1`, not `1.5`, and dividing an untyped parameter by a decimal is
58
+ read as interval arithmetic.
59
+ - No lastrowid, which is why generated values come back through `RETURNING`.
60
+ - `RETURNING *` needs an explicit column list.
61
+ - JSON is normalized on input, so a document does not round-trip byte for byte.
62
+ - Some identifiers legal elsewhere are rejected, such as one containing `%`.
63
+
64
+ Common table expressions are fully supported, including recursive CTEs, CTEs
65
+ over `VALUES`, and CTEs driving `UPDATE`/`DELETE`. MonetDB's `WITH` accepts only
66
+ `SELECT` or `VALUES`, so a CTE cannot itself be an `INSERT`/`UPDATE`/`DELETE`.
67
+
68
+ Regular expression matching is not available: MonetDB's `~` is `mbr_contains`,
69
+ a geometry operator, so `regexp_match()` raises rather than producing SQL that
70
+ means something else. `regexp_replace()` works.
71
+
72
+ The dialect requires `adbc-driver-monetdb` 0.8.5 or newer, which reports
73
+ truthful row counts, exports the PEP 249 `Binary` constructor, caches
74
+ prepared statements per connection, executes one-row bound DML without a
75
+ savepoint, and returns small results from MonetDB's initial reply. One
76
+ `DRIVER-WORKAROUND` remains in the source for upstream Apache arrow-adbc
77
+ behavior: ADBC always returns an Arrow stream, so the DB-API layer reports an
78
+ empty `description` rather than `None` for statements that produce no result
79
+ set, and SQLAlchemy needs `None` to decide that a statement returned no rows.
80
+
81
+ ### MonetDB behaviors worth knowing
82
+
83
+ - A stock login lands in the `sys` schema, where system views already occupy
84
+ ordinary table names such as `users` and `columns`. Create and use a schema of
85
+ your own.
86
+ - Self-referential foreign keys are added by `ALTER TABLE` after the table
87
+ exists, because MonetDB cannot declare them inline. MonetDB then enforces
88
+ them one statement at a time rather than at statement end, so on such a table
89
+ `DELETE FROM t` and `TRUNCATE t` are both rejected, and a multi-row `INSERT`
90
+ cannot reference a row added by the same statement. Declare the foreign key
91
+ with `ondelete="CASCADE"` if you need bulk deletes, or clear the referencing
92
+ column first:
93
+
94
+ ```python
95
+ connection.execute(update(tree).values(parent_id=None))
96
+ connection.execute(delete(tree))
97
+ ```
98
+ - Indexes carry no ordering, so `ASC`/`DESC` on an index expression is dropped.
99
+ - MonetDB rejects the all-zero (nil) UUID, `00000000-0000-0000-0000-000000000000`,
100
+ even as a plain literal, because it uses that value as its internal `NULL` for
101
+ the `uuid` type. A `Uuid` column therefore cannot store `uuid.UUID(int=0)`.
102
+ - A `DECIMAL` declared without precision means `DECIMAL(18, 3)`. The dialect
103
+ always renders that explicitly: MonetDB drops the connection when a
104
+ *parameter* is cast to an unsized `DECIMAL`, which is what SQLAlchemy emits
105
+ for true division.
106
+ - A `Numeric` column accepts `float`, `int` and `Decimal` in the same
107
+ `executemany`. ADBC would otherwise take the column's Arrow type from the
108
+ first value and reject the rest, so the dialect normalises the column first.
109
+ - A `UNIQUE` constraint is backed by an index of the same name, so it is
110
+ reflected both by `get_unique_constraints()` and by `get_indexes()`.
111
+
112
+ ## Installation
113
+
114
+ ```console
115
+ uv add sqlalchemy-monetdb-adbc
116
+ ```
117
+
118
+ Or with Python's standard package installer:
119
+
120
+ ```console
121
+ python -m pip install sqlalchemy-monetdb-adbc
122
+ ```
123
+
124
+ ## Usage
125
+
126
+ Existing SQLAlchemy MonetDB URLs work unchanged:
127
+
128
+ ```python
129
+ from sqlalchemy import create_engine
130
+
131
+ engine = create_engine(
132
+ "monetdb://monetdb:monetdb@localhost:50000/demo",
133
+ )
134
+
135
+ with engine.connect() as connection:
136
+ rows = connection.exec_driver_sql("SELECT 1").all()
137
+ ```
138
+
139
+ The explicit `monetdb+adbc://` form resolves to the same dialect. Do not install
140
+ `sqlalchemy-monetdb-adbc` and `sqlalchemy-monetdb` in the same environment: both
141
+ packages register the bare `monetdb://` SQLAlchemy entry point.
142
+
143
+ TLS URLs work with either the unchanged `monetdbs://` form or the explicit
144
+ `monetdbs+adbc://` form. Both preserve the secure driver URI.
145
+
146
+ The driver accepts the same URI query options after the database name:
147
+
148
+ ```python
149
+ engine = create_engine(
150
+ "monetdb://monetdb:monetdb@localhost:50000/demo?client_application=my_app",
151
+ )
152
+ ```
153
+
154
+ ## Arrow and polars
155
+
156
+ Rows are converted to Python objects only if you ask for them, and that
157
+ conversion is done a column at a time through numpy rather than element by
158
+ element. Ordinary row-returning queries with meaningful result sizes avoid
159
+ per-cell Arrow scalar boxing.
160
+
161
+ Driver 0.8.5 removes the avoidable fixed costs found in the release review:
162
+ one-row parameterized DML no longer adds a savepoint pair, and results of up to
163
+ 100 rows no longer force a second fetch. The driver also batches `executemany`
164
+ parameters into one multi-row statement when MonetDB can preserve the DB-API
165
+ semantics. Tiny row-oriented SELECTs still carry the fixed cost of receiving a
166
+ canonical Arrow stream across the native boundary; use the Arrow helpers below
167
+ when keeping the result columnar matters.
168
+
169
+ To skip the conversion entirely and keep data columnar, run the query on the
170
+ same connection and take Arrow back:
171
+
172
+ ```python
173
+ from sqlalchemy_monetdb_adbc import fetch_arrow_table, fetch_record_batches, ingest_arrow
174
+
175
+ statement = select(trades.c.id, trades.c.symbol).where(trades.c.symbol == "AAPL")
176
+
177
+ with Session(engine) as session:
178
+ table = fetch_arrow_table(session.connection(), statement) # pyarrow.Table
179
+ frame = polars.from_arrow(table)
180
+
181
+ # streaming, for results that should not be materialized at once
182
+ with fetch_record_batches(session.connection(), statement) as reader:
183
+ for batch in reader:
184
+ ...
185
+
186
+ ingest_arrow(session.connection(), "trades", table, mode="append")
187
+ ```
188
+
189
+ These run on the ADBC session backing the SQLAlchemy connection, so they see
190
+ that connection's uncommitted work, take part in its transaction, and roll back
191
+ with it. Use them rather than opening a second connection with
192
+ `polars.read_database`, which would be a separate session and transaction.
193
+
194
+ A SQLAlchemy statement is compiled for you, bind parameters included; a plain
195
+ SQL string works too. `raw_adbc_connection()` returns the underlying ADBC
196
+ connection if you need the driver API directly.
197
+
198
+ The driver prefetches large results by default. Closing a partially consumed
199
+ result—such as calling `.first()`—waits briefly and then lets an in-flight fetch
200
+ finish without killing the pooled session. The next statement on that
201
+ connection can wait for that fetch or its configured timeout. Set
202
+ `read_prefetch=false` in the connection URI when prompt pool reuse matters more
203
+ than fetch/decode overlap:
204
+
205
+ ```python
206
+ engine = create_engine("monetdb://localhost/demo?read_prefetch=false")
207
+ ```
208
+
209
+ ## JSON
210
+
211
+ A `JSON` column round-trips Python objects, as SQLAlchemy specifies: the driver
212
+ returns the document as a string and SQLAlchemy deserializes it, honouring
213
+ `json_serializer` and `json_deserializer` on `create_engine`.
214
+
215
+ ```python
216
+ engine = create_engine("monetdb://...", json_deserializer=orjson.loads)
217
+ ```
218
+
219
+ MonetDB validates and normalizes JSON on input, so a round-tripped document
220
+ keeps its values but not its original whitespace or key order.
221
+
222
+ Path indexing works as on other backends, including nested keys, array indexes
223
+ and the `as_string()`/`as_integer()` accessors. A path that matches nothing
224
+ returns `None`:
225
+
226
+ ```python
227
+ select(t.c.doc["title"]) # 'hello'
228
+ select(t.c.doc[("sub", "k")]) # 'v'
229
+ select(t.c.doc[("arr", 1)]) # 20
230
+ select(t.c.doc["missing"]) # None
231
+ ```
232
+
233
+ Whatever Python object you store is serialized, and you get that same object
234
+ back. Note that a `str` is itself a valid JSON value, so passing pre-serialized
235
+ text to a `JSON` column stores a JSON *string*, not an object:
236
+
237
+ ```python
238
+ connection.execute(insert(t), [{"payload": {"a": 1}}]) # stored as {"a":1}
239
+ connection.execute(insert(t), [{"payload": '{"a": 1}'}]) # stored as "{\"a\": 1}"
240
+ ```
241
+
242
+ This is SQLAlchemy's behavior on every backend, not a MonetDB quirk. To store
243
+ JSON text you already hold, use a `Text` column, or a type that controls the
244
+ codec as below.
245
+
246
+ ### Storing a Pydantic model
247
+
248
+ `PydanticJSON` stores a Pydantic model in a MonetDB `JSON` column. The model is
249
+ serialized straight to JSON text and parsed straight back with
250
+ `model_validate_json`, so no intermediate `dict` is built in either direction:
251
+
252
+ ```python
253
+ from pydantic import BaseModel, ConfigDict
254
+ from sqlalchemy import Integer, select
255
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
256
+
257
+ from sqlalchemy_monetdb_adbc import PydanticJSON
258
+
259
+
260
+ class Content(BaseModel):
261
+ model_config = ConfigDict(frozen=True)
262
+
263
+ title: str
264
+ views: int
265
+
266
+
267
+ class Base(DeclarativeBase):
268
+ pass
269
+
270
+
271
+ class Article(Base):
272
+ __tablename__ = "article"
273
+
274
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
275
+ content: Mapped[Content] = mapped_column(PydanticJSON(Content))
276
+ ```
277
+
278
+ The attribute is the model itself, with no conversion at the call site:
279
+
280
+ ```python
281
+ article: Article = session.scalars(select(Article)).one()
282
+ article.content.title # Content instance, never a dict
283
+ ```
284
+
285
+ This avoids deserializing to an intermediate `dict` before validation. A plain
286
+ `JSON` column cannot avoid the dict: by the time the attribute is read,
287
+ SQLAlchemy has already deserialized and the original text is gone.
288
+
289
+ #### Declare the model frozen
290
+
291
+ As with any `JSON` column, SQLAlchemy does not track mutation *inside* the
292
+ value, so an in-place edit is silently not persisted. Declaring the model
293
+ `frozen=True`, as above, turns that silent loss into a `ValidationError`, and
294
+ makes the model hashable. Persist a change by assigning a new value:
295
+
296
+ ```python
297
+ article.content = article.content.model_copy(update={"views": 2})
298
+ ```
299
+
300
+ Freezing is a property of your model, so the type cannot impose it; if you need
301
+ mutation to be tracked instead, use `sqlalchemy.ext.mutable`.
302
+
303
+ ## Alembic
304
+
305
+ Importing the dialect registers a MonetDB migration implementation, so Alembic
306
+ works without further configuration. Schema operations, autogenerate, and
307
+ `PydanticJSON` columns are covered by the test suite.
308
+
309
+ No configuration is needed: importing the dialect, which `create_engine` does
310
+ for you, registers the implementation. This works in offline mode too.
311
+
312
+ Changing a column's type is supported. MonetDB spells it
313
+ `ALTER TABLE t ALTER COLUMN c <type>`, without the `TYPE` keyword most backends
314
+ use. It refuses to alter a column that other objects depend on, such as one
315
+ carrying a primary key, and says so.
316
+
317
+ A `PydanticJSON` column autogenerates as `sa.JSON()`, since a migration
318
+ describes the database schema. Rendering the model instead would make
319
+ migrations import application code and break as soon as that model moved.
320
+
321
+ ## Development
322
+
323
+ Python 3.13 or newer and `uv` are required.
324
+
325
+ ```console
326
+ uv sync --all-groups
327
+ uv run ruff check .
328
+ uv run ruff format --check .
329
+ uv run pyright
330
+ uv run pytest
331
+ uv build
332
+ ```
333
+
334
+ Integration tests and the dialect compliance suite need a server. `compose.yaml`
335
+ pins the same native ARM64 MonetDB image the driver is developed against:
336
+
337
+ ```console
338
+ docker compose up --wait
339
+ MONETDB_TEST_URI=monetdb://monetdb:monetdb@localhost:50001/test uv run pytest tests
340
+ uv run pytest suite -o addopts=""
341
+ docker compose down -v
342
+ ```
343
+
344
+ `uv run pytest` alone skips every test that needs a server, so the unit gate
345
+ runs without one.
346
+
347
+ ## License
348
+
349
+ MIT