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.
- sqlalchemy_monetdb_adbc-0.1.0/.gitattributes +2 -0
- sqlalchemy_monetdb_adbc-0.1.0/.github/dependabot.yml +16 -0
- sqlalchemy_monetdb_adbc-0.1.0/.github/workflows/ci.yml +167 -0
- sqlalchemy_monetdb_adbc-0.1.0/.gitignore +15 -0
- sqlalchemy_monetdb_adbc-0.1.0/AGENTS.md +47 -0
- sqlalchemy_monetdb_adbc-0.1.0/CONTRIBUTING.md +37 -0
- sqlalchemy_monetdb_adbc-0.1.0/LICENSE +21 -0
- sqlalchemy_monetdb_adbc-0.1.0/PKG-INFO +349 -0
- sqlalchemy_monetdb_adbc-0.1.0/README.md +319 -0
- sqlalchemy_monetdb_adbc-0.1.0/compose.yaml +15 -0
- sqlalchemy_monetdb_adbc-0.1.0/packaging/release_checks.py +97 -0
- sqlalchemy_monetdb_adbc-0.1.0/pyproject.toml +137 -0
- sqlalchemy_monetdb_adbc-0.1.0/setup.cfg +9 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/__init__.py +37 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/_alembic.py +74 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/_convert.py +107 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/arrow.py +99 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/base.py +623 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/compiler.py +410 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/connection.py +62 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/constants.py +39 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/ddl.py +33 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/dialect.py +252 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/multi_reflection.py +539 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/provision.py +119 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/py.typed +1 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/reflection.py +195 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/requirements.py +248 -0
- sqlalchemy_monetdb_adbc-0.1.0/sqlalchemy_monetdb_adbc/types.py +275 -0
- sqlalchemy_monetdb_adbc-0.1.0/suite/conftest.py +34 -0
- sqlalchemy_monetdb_adbc-0.1.0/suite/known_failures.py +45 -0
- sqlalchemy_monetdb_adbc-0.1.0/suite/test_suite.py +1 -0
- sqlalchemy_monetdb_adbc-0.1.0/tests/conftest.py +24 -0
- sqlalchemy_monetdb_adbc-0.1.0/tests/test_alembic.py +190 -0
- sqlalchemy_monetdb_adbc-0.1.0/tests/test_convert.py +135 -0
- sqlalchemy_monetdb_adbc-0.1.0/tests/test_dialect.py +330 -0
- sqlalchemy_monetdb_adbc-0.1.0/tests/test_integration.py +969 -0
- sqlalchemy_monetdb_adbc-0.1.0/tests/test_local_benchmark.py +108 -0
- sqlalchemy_monetdb_adbc-0.1.0/uv.lock +546 -0
|
@@ -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,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
|