sqlalchemy-adbc 0.3.0__tar.gz → 0.4.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_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/CHANGELOG.md +34 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/PKG-INFO +23 -18
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/README.md +21 -16
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/pyproject.toml +9 -2
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/__init__.py +4 -4
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/base.py +13 -1
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/bigquery.py +1 -1
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/flightsql.py +2 -2
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/postgresql.py +16 -11
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/snowflake.py +1 -1
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_alembic_autogenerate.py +1 -1
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_bigquery_url.py +6 -6
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_flightsql_url.py +9 -9
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_postgresql_types.py +1 -1
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_postgresql_url.py +26 -13
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_reflection.py +7 -5
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_snowflake_url.py +9 -9
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_sqlite.py +5 -5
- sqlalchemy_adbc-0.4.0/tests/test_url_schemes.py +55 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/.github/workflows/ci.yml +0 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/.github/workflows/publish.yml +0 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/.gitignore +0 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/LICENSE +0 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/postgresql_types.py +0 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/reflection.py +0 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/src/sqlalchemy_adbc/sqlite.py +0 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/__init__.py +0 -0
- {sqlalchemy_adbc-0.3.0 → sqlalchemy_adbc-0.4.0}/tests/test_base.py +0 -0
|
@@ -6,6 +6,40 @@ this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] — 2026-11-07
|
|
10
|
+
|
|
11
|
+
URL schemes now follow SQLAlchemy's `<backend>+adbc://` convention,
|
|
12
|
+
plus compatibility fixes for SQLAlchemy 2.1 and ADBC 1.13. Existing
|
|
13
|
+
`adbc+<backend>://` URLs keep working and emit a `DeprecationWarning`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **URL schemes are now `<backend>+adbc://`** — `postgresql+adbc://`,
|
|
18
|
+
`sqlite+adbc://`, `flightsql+adbc://`, `snowflake+adbc://`,
|
|
19
|
+
`bigquery+adbc://`. This follows SQLAlchemy's `dialect+driver`
|
|
20
|
+
convention: ADBC is the driver. `URL.get_backend_name()` now agrees
|
|
21
|
+
with `dialect.name`, so tools that pick behavior from the URL (for
|
|
22
|
+
example Superset's engine specs) see the real backend.
|
|
23
|
+
([#15](https://github.com/drls-io/sqlalchemy-adbc/issues/15))
|
|
24
|
+
|
|
25
|
+
### Deprecated
|
|
26
|
+
|
|
27
|
+
- **`adbc+<backend>://` URL spellings** — still registered and
|
|
28
|
+
functional, but `create_engine` emits a `DeprecationWarning`. They
|
|
29
|
+
will be removed in 1.0.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **PostgreSQL database names with SQLAlchemy 2.1** — SQLAlchemy 2.1
|
|
34
|
+
percent-decodes `URL.database`, so the libpq URI lost its escapes
|
|
35
|
+
(`my%20db` became `my db`). The dialect now detects the parser's
|
|
36
|
+
behavior and quotes the database path exactly once on both 2.0 and
|
|
37
|
+
2.1. ([#19](https://github.com/drls-io/sqlalchemy-adbc/issues/19))
|
|
38
|
+
- **CI on ADBC 1.13** — `adbc_driver_sqlite` 1.13.0 reports NOT NULL
|
|
39
|
+
correctly, which turned the strict xfail for column nullability into
|
|
40
|
+
a hard failure. The xfail now applies only to drivers older than
|
|
41
|
+
1.13. ([#13](https://github.com/drls-io/sqlalchemy-adbc/issues/13))
|
|
42
|
+
|
|
9
43
|
## [0.3.0] — 2026-04-21
|
|
10
44
|
|
|
11
45
|
Largest release so far: table reflection, Alembic autogenerate,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlalchemy-adbc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Generic SQLAlchemy dialect for any ADBC driver (Flight SQL, PostgreSQL, SQLite, Snowflake, BigQuery).
|
|
5
5
|
Project-URL: Homepage, https://github.com/drls-io/sqlalchemy-adbc
|
|
6
6
|
Project-URL: Repository, https://github.com/drls-io/sqlalchemy-adbc
|
|
@@ -81,18 +81,18 @@ import sqlalchemy
|
|
|
81
81
|
|
|
82
82
|
# Flight SQL
|
|
83
83
|
engine = sqlalchemy.create_engine(
|
|
84
|
-
"adbc
|
|
84
|
+
"flightsql+adbc://flight-sql.example.com:443?tls=true",
|
|
85
85
|
connect_args={"db_kwargs": {
|
|
86
86
|
"adbc.flight.sql.authorization_header": f"Bearer {token}",
|
|
87
87
|
}},
|
|
88
88
|
)
|
|
89
89
|
|
|
90
90
|
# SQLite
|
|
91
|
-
engine = sqlalchemy.create_engine("adbc
|
|
91
|
+
engine = sqlalchemy.create_engine("sqlite+adbc:///path/to/db.sqlite")
|
|
92
92
|
|
|
93
93
|
# PostgreSQL
|
|
94
94
|
engine = sqlalchemy.create_engine(
|
|
95
|
-
"adbc
|
|
95
|
+
"postgresql+adbc://user:pass@host:5432/mydb"
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
# Works with pandas, Polars, marimo SQL cells, etc.
|
|
@@ -102,14 +102,18 @@ df = pd.read_sql("SELECT 1 AS n", engine)
|
|
|
102
102
|
|
|
103
103
|
## URL forms
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
URLs follow SQLAlchemy's `<backend>+<driver>://` convention, with `adbc`
|
|
106
|
+
as the driver. The pre-0.4 `adbc+<backend>://` spelling still works but
|
|
107
|
+
emits a `DeprecationWarning` and will be removed in 1.0.
|
|
108
|
+
|
|
109
|
+
| Backend | URL |
|
|
106
110
|
|--------------|------------------------------------------------------------|
|
|
107
|
-
| Flight SQL | `adbc
|
|
108
|
-
| Flight SQL | `adbc
|
|
109
|
-
| PostgreSQL | `adbc
|
|
110
|
-
| SQLite | `adbc
|
|
111
|
-
| Snowflake | `adbc
|
|
112
|
-
| BigQuery | `adbc
|
|
111
|
+
| Flight SQL | `flightsql+adbc://host:port?tls=true&authorization=...` |
|
|
112
|
+
| Flight SQL | `flightsql+adbc://user:TOKEN@host:port?tls=true` (bearer) |
|
|
113
|
+
| PostgreSQL | `postgresql+adbc://user:pass@host:port/database` |
|
|
114
|
+
| SQLite | `sqlite+adbc:///path/to/db` or `sqlite+adbc:///:memory:` |
|
|
115
|
+
| Snowflake | `snowflake+adbc://user:pass@account/db/schema?warehouse=W` |
|
|
116
|
+
| BigQuery | `bigquery+adbc:///my-project?dataset=my_dataset` |
|
|
113
117
|
|
|
114
118
|
### Flight SQL query-string options
|
|
115
119
|
|
|
@@ -151,17 +155,18 @@ published wheels on [PyPI](https://pypi.org/project/sqlalchemy-adbc/).
|
|
|
151
155
|
…) and **hstore** — not yet mapped to typed decorators; returned
|
|
152
156
|
as raw strings. JSONB / JSON / UUID / INET / CIDR / MACADDR /
|
|
153
157
|
MACADDR8 all round-trip through typed decorators as of 0.3.0
|
|
154
|
-
([#
|
|
155
|
-
JSONB/UUID/INET portion closed)
|
|
158
|
+
([#11](https://github.com/drls-io/sqlalchemy-adbc/issues/11))
|
|
156
159
|
- **Drivers exercised in CI** — SQLite fully, PostgreSQL
|
|
157
160
|
integration-tested via service container. Snowflake / BigQuery /
|
|
158
161
|
Flight SQL inherit the shared reflection path but have no
|
|
159
162
|
per-driver CI
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
(
|
|
163
|
+
([#12](https://github.com/drls-io/sqlalchemy-adbc/issues/12))
|
|
164
|
+
- **ADBC SQLite NOT NULL reporting** — fixed upstream in
|
|
165
|
+
`adbc_driver_sqlite` 1.13 (Python ≥ 3.11). On older drivers every
|
|
166
|
+
column reflects as nullable=True
|
|
167
|
+
([#13](https://github.com/drls-io/sqlalchemy-adbc/issues/13))
|
|
163
168
|
|
|
164
|
-
Tests:
|
|
169
|
+
Tests: 121 passing on Python 3.10–3.13, including 17 PostgreSQL
|
|
165
170
|
integration tests against a service-container Postgres.
|
|
166
171
|
|
|
167
172
|
## Development
|
|
@@ -37,18 +37,18 @@ import sqlalchemy
|
|
|
37
37
|
|
|
38
38
|
# Flight SQL
|
|
39
39
|
engine = sqlalchemy.create_engine(
|
|
40
|
-
"adbc
|
|
40
|
+
"flightsql+adbc://flight-sql.example.com:443?tls=true",
|
|
41
41
|
connect_args={"db_kwargs": {
|
|
42
42
|
"adbc.flight.sql.authorization_header": f"Bearer {token}",
|
|
43
43
|
}},
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
# SQLite
|
|
47
|
-
engine = sqlalchemy.create_engine("adbc
|
|
47
|
+
engine = sqlalchemy.create_engine("sqlite+adbc:///path/to/db.sqlite")
|
|
48
48
|
|
|
49
49
|
# PostgreSQL
|
|
50
50
|
engine = sqlalchemy.create_engine(
|
|
51
|
-
"adbc
|
|
51
|
+
"postgresql+adbc://user:pass@host:5432/mydb"
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
# Works with pandas, Polars, marimo SQL cells, etc.
|
|
@@ -58,14 +58,18 @@ df = pd.read_sql("SELECT 1 AS n", engine)
|
|
|
58
58
|
|
|
59
59
|
## URL forms
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
URLs follow SQLAlchemy's `<backend>+<driver>://` convention, with `adbc`
|
|
62
|
+
as the driver. The pre-0.4 `adbc+<backend>://` spelling still works but
|
|
63
|
+
emits a `DeprecationWarning` and will be removed in 1.0.
|
|
64
|
+
|
|
65
|
+
| Backend | URL |
|
|
62
66
|
|--------------|------------------------------------------------------------|
|
|
63
|
-
| Flight SQL | `adbc
|
|
64
|
-
| Flight SQL | `adbc
|
|
65
|
-
| PostgreSQL | `adbc
|
|
66
|
-
| SQLite | `adbc
|
|
67
|
-
| Snowflake | `adbc
|
|
68
|
-
| BigQuery | `adbc
|
|
67
|
+
| Flight SQL | `flightsql+adbc://host:port?tls=true&authorization=...` |
|
|
68
|
+
| Flight SQL | `flightsql+adbc://user:TOKEN@host:port?tls=true` (bearer) |
|
|
69
|
+
| PostgreSQL | `postgresql+adbc://user:pass@host:port/database` |
|
|
70
|
+
| SQLite | `sqlite+adbc:///path/to/db` or `sqlite+adbc:///:memory:` |
|
|
71
|
+
| Snowflake | `snowflake+adbc://user:pass@account/db/schema?warehouse=W` |
|
|
72
|
+
| BigQuery | `bigquery+adbc:///my-project?dataset=my_dataset` |
|
|
69
73
|
|
|
70
74
|
### Flight SQL query-string options
|
|
71
75
|
|
|
@@ -107,17 +111,18 @@ published wheels on [PyPI](https://pypi.org/project/sqlalchemy-adbc/).
|
|
|
107
111
|
…) and **hstore** — not yet mapped to typed decorators; returned
|
|
108
112
|
as raw strings. JSONB / JSON / UUID / INET / CIDR / MACADDR /
|
|
109
113
|
MACADDR8 all round-trip through typed decorators as of 0.3.0
|
|
110
|
-
([#
|
|
111
|
-
JSONB/UUID/INET portion closed)
|
|
114
|
+
([#11](https://github.com/drls-io/sqlalchemy-adbc/issues/11))
|
|
112
115
|
- **Drivers exercised in CI** — SQLite fully, PostgreSQL
|
|
113
116
|
integration-tested via service container. Snowflake / BigQuery /
|
|
114
117
|
Flight SQL inherit the shared reflection path but have no
|
|
115
118
|
per-driver CI
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
(
|
|
119
|
+
([#12](https://github.com/drls-io/sqlalchemy-adbc/issues/12))
|
|
120
|
+
- **ADBC SQLite NOT NULL reporting** — fixed upstream in
|
|
121
|
+
`adbc_driver_sqlite` 1.13 (Python ≥ 3.11). On older drivers every
|
|
122
|
+
column reflects as nullable=True
|
|
123
|
+
([#13](https://github.com/drls-io/sqlalchemy-adbc/issues/13))
|
|
119
124
|
|
|
120
|
-
Tests:
|
|
125
|
+
Tests: 121 passing on Python 3.10–3.13, including 17 PostgreSQL
|
|
121
126
|
integration tests against a service-container Postgres.
|
|
122
127
|
|
|
123
128
|
## Development
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlalchemy-adbc"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Generic SQLAlchemy dialect for any ADBC driver (Flight SQL, PostgreSQL, SQLite, Snowflake, BigQuery)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -58,8 +58,15 @@ Issues = "https://github.com/drls-io/sqlalchemy-adbc/issues"
|
|
|
58
58
|
|
|
59
59
|
# SQLAlchemy discovers dialects via the `sqlalchemy.dialects` entry point
|
|
60
60
|
# group. The name form is `<name>` or `<name>.<driver>` — the second is what
|
|
61
|
-
# lets us write `adbc
|
|
61
|
+
# lets us write `flightsql+adbc://` etc.
|
|
62
62
|
[project.entry-points."sqlalchemy.dialects"]
|
|
63
|
+
# SQLAlchemy URLs are <backend>+<driver>; ADBC is the driver (#15).
|
|
64
|
+
"flightsql.adbc" = "sqlalchemy_adbc.flightsql:ADBCFlightSQLDialect"
|
|
65
|
+
"sqlite.adbc" = "sqlalchemy_adbc.sqlite:ADBCSQLiteDialect"
|
|
66
|
+
"postgresql.adbc" = "sqlalchemy_adbc.postgresql:ADBCPostgreSQLDialect"
|
|
67
|
+
"snowflake.adbc" = "sqlalchemy_adbc.snowflake:ADBCSnowflakeDialect"
|
|
68
|
+
"bigquery.adbc" = "sqlalchemy_adbc.bigquery:ADBCBigQueryDialect"
|
|
69
|
+
# Deprecated adbc+<backend> spellings; removed in 1.0.
|
|
63
70
|
"adbc" = "sqlalchemy_adbc.base:ADBCDialect"
|
|
64
71
|
"adbc.flightsql" = "sqlalchemy_adbc.flightsql:ADBCFlightSQLDialect"
|
|
65
72
|
"adbc.sqlite" = "sqlalchemy_adbc.sqlite:ADBCSQLiteDialect"
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
Register dialects via the ``sqlalchemy.dialects`` entry-point group (see
|
|
4
4
|
pyproject.toml). URL forms::
|
|
5
5
|
|
|
6
|
-
adbc
|
|
7
|
-
adbc
|
|
8
|
-
adbc
|
|
9
|
-
adbc
|
|
6
|
+
flightsql+adbc://host:port/...?tls=true
|
|
7
|
+
sqlite+adbc:///path/to/db.sqlite
|
|
8
|
+
postgresql+adbc://user:pass@host:port/db
|
|
9
|
+
snowflake+adbc://user:pass@account/db/schema?warehouse=WH
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from sqlalchemy_adbc.base import ADBCDialect
|
|
@@ -16,6 +16,7 @@ Subclasses implement:
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import importlib
|
|
19
|
+
import warnings
|
|
19
20
|
from typing import Any
|
|
20
21
|
|
|
21
22
|
from sqlalchemy.engine.default import DefaultDialect
|
|
@@ -33,8 +34,10 @@ class ADBCDialect(DefaultDialect):
|
|
|
33
34
|
"""Base class for SQLAlchemy dialects backed by ADBC drivers.
|
|
34
35
|
|
|
35
36
|
This class is registered as the plain ``adbc://`` scheme. In practice
|
|
36
|
-
callers should use a
|
|
37
|
+
callers should use a backend-specific variant like ``flightsql+adbc://``;
|
|
37
38
|
the plain scheme requires ``adbc_driver_name`` in the URL query string.
|
|
39
|
+
The pre-0.4 ``adbc+<backend>://`` spellings remain registered as
|
|
40
|
+
deprecated aliases.
|
|
38
41
|
"""
|
|
39
42
|
|
|
40
43
|
# SQLAlchemy convention: `name` = backend identity (used by
|
|
@@ -76,6 +79,15 @@ class ADBCDialect(DefaultDialect):
|
|
|
76
79
|
|
|
77
80
|
Subclasses should override ``build_connect_args`` instead of this.
|
|
78
81
|
"""
|
|
82
|
+
if url.drivername.startswith("adbc+"):
|
|
83
|
+
backend = url.get_driver_name()
|
|
84
|
+
warnings.warn(
|
|
85
|
+
f"URL scheme 'adbc+{backend}://' is deprecated; use "
|
|
86
|
+
f"'{backend}+adbc://' (SQLAlchemy's backend+driver order). "
|
|
87
|
+
"The old spelling will be removed in 1.0.",
|
|
88
|
+
DeprecationWarning,
|
|
89
|
+
stacklevel=2,
|
|
90
|
+
)
|
|
79
91
|
return self.build_connect_args(url)
|
|
80
92
|
|
|
81
93
|
def build_connect_args(self, url: URL) -> tuple[list[Any], dict[str, Any]]:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
URL form::
|
|
4
4
|
|
|
5
|
-
adbc
|
|
5
|
+
bigquery+adbc:///<project>?dataset=<dataset>&credentials_path=/path/to/sa.json
|
|
6
6
|
|
|
7
7
|
adbc_driver_bigquery reads the GCP project and optional dataset via
|
|
8
8
|
db_kwargs; we pass the URL query string through verbatim.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
URL form::
|
|
4
4
|
|
|
5
|
-
adbc
|
|
5
|
+
flightsql+adbc://host:port?tls=true&authorization=Bearer%20...
|
|
6
6
|
|
|
7
7
|
All query-string keys are forwarded to the driver as ``db_kwargs``. The
|
|
8
8
|
dialect also understands a few shorthands:
|
|
@@ -69,7 +69,7 @@ class ADBCFlightSQLDialect(ADBCDialect):
|
|
|
69
69
|
db_kwargs[auth_key] = query.pop("authorization")
|
|
70
70
|
|
|
71
71
|
# Caller can also set the bearer via SQLAlchemy URL password:
|
|
72
|
-
# adbc
|
|
72
|
+
# flightsql+adbc://user:<token>@host:443?tls=true
|
|
73
73
|
# The "user" part is ignored by Flight SQL; we just read the
|
|
74
74
|
# password into the Authorization header.
|
|
75
75
|
if url.password:
|
|
@@ -10,14 +10,19 @@ corrupt the DSN or change its parse. Without quoting, a password like
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
|
13
|
-
from urllib.parse import quote
|
|
13
|
+
from urllib.parse import quote, unquote
|
|
14
14
|
|
|
15
15
|
from sqlalchemy.engine.interfaces import ReflectedColumn, ReflectedIndex
|
|
16
|
-
from sqlalchemy.engine.url import URL
|
|
16
|
+
from sqlalchemy.engine.url import URL, make_url
|
|
17
17
|
|
|
18
18
|
from sqlalchemy_adbc.base import ADBCDialect
|
|
19
19
|
from sqlalchemy_adbc.postgresql_types import pg_ischema_names
|
|
20
20
|
|
|
21
|
+
# SQLAlchemy 2.0 leaves ``URL.database`` percent-encoded; 2.1 decodes it
|
|
22
|
+
# like userinfo. Probe once so the libpq URI is quoted exactly once on
|
|
23
|
+
# either version.
|
|
24
|
+
_URL_DECODES_DATABASE = make_url("postgresql://h/a%20b").database == "a b"
|
|
25
|
+
|
|
21
26
|
|
|
22
27
|
class ADBCPostgreSQLDialect(ADBCDialect):
|
|
23
28
|
# `name = "postgresql"` makes Alembic pick the standard Postgres
|
|
@@ -45,20 +50,20 @@ class ADBCPostgreSQLDialect(ADBCDialect):
|
|
|
45
50
|
super().__init__(**kwargs)
|
|
46
51
|
|
|
47
52
|
def build_connect_args(self, url: URL) -> tuple[list[Any], dict[str, Any]]:
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# database through as-is. Re-quoting the database would
|
|
54
|
-
# double-encode already-percent-escaped names (``my%20db`` →
|
|
55
|
-
# ``my%2520db``).
|
|
53
|
+
# Every user-supplied component is reduced to its raw value and
|
|
54
|
+
# then quoted once, so a password like ``p@ss`` cannot re-route
|
|
55
|
+
# libpq to another host and a database named ``my%20db`` is not
|
|
56
|
+
# collapsed to ``my db``. Userinfo is always decoded by SQLAlchemy;
|
|
57
|
+
# the database path only from 2.1 (``_URL_DECODES_DATABASE``).
|
|
56
58
|
user = quote(url.username, safe="") if url.username else ""
|
|
57
59
|
password = f":{quote(url.password, safe='')}" if url.password else ""
|
|
58
60
|
auth = f"{user}{password}@" if user or url.password else ""
|
|
59
61
|
host = url.host or "localhost"
|
|
60
62
|
port = f":{url.port}" if url.port else ""
|
|
61
|
-
database =
|
|
63
|
+
database = ""
|
|
64
|
+
if url.database:
|
|
65
|
+
raw = url.database if _URL_DECODES_DATABASE else unquote(url.database)
|
|
66
|
+
database = f"/{quote(raw, safe='')}"
|
|
62
67
|
uri = f"postgresql://{auth}{host}{port}{database}"
|
|
63
68
|
|
|
64
69
|
kwargs: dict[str, Any] = {}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
URL form::
|
|
4
4
|
|
|
5
|
-
adbc
|
|
5
|
+
snowflake+adbc://user:password@account/database/schema?warehouse=WH&role=R
|
|
6
6
|
|
|
7
7
|
adbc_driver_snowflake expects a DSN of the form
|
|
8
8
|
``user:password@account/database/schema?warehouse=...``. We URL-quote
|
|
@@ -25,7 +25,7 @@ from alembic.migration import MigrationContext # noqa: E402
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _make_engine() -> sa.Engine:
|
|
28
|
-
return sa.create_engine("adbc
|
|
28
|
+
return sa.create_engine("sqlite+adbc:///:memory:")
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def _diff(engine: sa.Engine, target: sa.MetaData, **opts: Any) -> tuple[str, str]:
|
|
@@ -12,21 +12,21 @@ def _build(url_str: str):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def test_project_from_database_segment():
|
|
15
|
-
"""``adbc
|
|
16
|
-
args, kwargs = _build("adbc
|
|
15
|
+
"""``bigquery+adbc:///my-project`` → project_id set."""
|
|
16
|
+
args, kwargs = _build("bigquery+adbc:///my-project")
|
|
17
17
|
assert args == []
|
|
18
18
|
assert kwargs["db_kwargs"]["adbc.bigquery.sql.project_id"] == "my-project"
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_project_plus_dataset():
|
|
22
|
-
_, kwargs = _build("adbc
|
|
22
|
+
_, kwargs = _build("bigquery+adbc:///my-project?dataset=events")
|
|
23
23
|
db = kwargs["db_kwargs"]
|
|
24
24
|
assert db["adbc.bigquery.sql.project_id"] == "my-project"
|
|
25
25
|
assert db["dataset"] == "events"
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def test_only_query_params():
|
|
29
|
-
_, kwargs = _build("adbc
|
|
29
|
+
_, kwargs = _build("bigquery+adbc://?adbc.bigquery.sql.project_id=p&x=y")
|
|
30
30
|
db = kwargs["db_kwargs"]
|
|
31
31
|
assert db["adbc.bigquery.sql.project_id"] == "p"
|
|
32
32
|
assert db["x"] == "y"
|
|
@@ -35,11 +35,11 @@ def test_only_query_params():
|
|
|
35
35
|
def test_explicit_project_kwarg_wins_over_url():
|
|
36
36
|
"""If both URL-database and ``?project_id=...`` are present, the
|
|
37
37
|
explicit query string must not be overwritten by the implicit default."""
|
|
38
|
-
_, kwargs = _build("adbc
|
|
38
|
+
_, kwargs = _build("bigquery+adbc:///implicit?adbc.bigquery.sql.project_id=explicit")
|
|
39
39
|
assert kwargs["db_kwargs"]["adbc.bigquery.sql.project_id"] == "explicit"
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def test_no_kwargs_at_all():
|
|
43
|
-
args, kwargs = _build("adbc
|
|
43
|
+
args, kwargs = _build("bigquery+adbc://")
|
|
44
44
|
assert args == []
|
|
45
45
|
assert kwargs == {}
|
|
@@ -18,35 +18,35 @@ def _build(url_str: str):
|
|
|
18
18
|
|
|
19
19
|
def test_tls_is_default():
|
|
20
20
|
"""No ``tls`` query param → scheme is grpc+tls (secure by default)."""
|
|
21
|
-
args, kwargs = _build("adbc
|
|
21
|
+
args, kwargs = _build("flightsql+adbc://flight-sql.example.com:443")
|
|
22
22
|
assert args == ["grpc+tls://flight-sql.example.com:443"]
|
|
23
23
|
assert "db_kwargs" not in kwargs
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def test_tls_explicit_opt_out():
|
|
27
27
|
"""``?tls=false`` drops back to plaintext gRPC — needed for local dev."""
|
|
28
|
-
args, kwargs = _build("adbc
|
|
28
|
+
args, kwargs = _build("flightsql+adbc://localhost:50051?tls=false")
|
|
29
29
|
assert args == ["grpc://localhost:50051"]
|
|
30
30
|
assert "db_kwargs" not in kwargs
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def test_default_port_tls():
|
|
34
|
-
args, _ = _build("adbc
|
|
34
|
+
args, _ = _build("flightsql+adbc://flight-sql.example.com")
|
|
35
35
|
assert args == ["grpc+tls://flight-sql.example.com:443"]
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def test_default_port_plaintext():
|
|
39
|
-
args, _ = _build("adbc
|
|
39
|
+
args, _ = _build("flightsql+adbc://localhost?tls=false")
|
|
40
40
|
assert args == ["grpc://localhost:50051"]
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def test_tls_scheme_explicit():
|
|
44
|
-
args, _ = _build("adbc
|
|
44
|
+
args, _ = _build("flightsql+adbc://flight-sql.example.com:443?tls=true")
|
|
45
45
|
assert args == ["grpc+tls://flight-sql.example.com:443"]
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def test_bearer_from_password():
|
|
49
|
-
args, kwargs = _build("adbc
|
|
49
|
+
args, kwargs = _build("flightsql+adbc://user:TOKEN@host:443?tls=true")
|
|
50
50
|
assert args == ["grpc+tls://host:443"]
|
|
51
51
|
# We can't assert on the exact key name without the driver installed —
|
|
52
52
|
# the flightsql dialect has a fallback constant for this case.
|
|
@@ -55,13 +55,13 @@ def test_bearer_from_password():
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def test_authorization_passthrough():
|
|
58
|
-
args, kwargs = _build("adbc
|
|
58
|
+
args, kwargs = _build("flightsql+adbc://host:443?tls=true&authorization=Bearer%20abc")
|
|
59
59
|
db = kwargs["db_kwargs"]
|
|
60
60
|
assert any(v == "Bearer abc" for v in db.values())
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def test_custom_headers():
|
|
64
|
-
_, kwargs = _build("adbc
|
|
64
|
+
_, kwargs = _build("flightsql+adbc://host:443?tls=true&header.x-workspace-id=ws-1")
|
|
65
65
|
db = kwargs["db_kwargs"]
|
|
66
66
|
# Header keys use the RPC_CALL_HEADER_PREFIX; just check the value made
|
|
67
67
|
# it through.
|
|
@@ -69,5 +69,5 @@ def test_custom_headers():
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def test_unknown_query_passthrough():
|
|
72
|
-
_, kwargs = _build("adbc
|
|
72
|
+
_, kwargs = _build("flightsql+adbc://host:443?tls=true&some_opt=value")
|
|
73
73
|
assert kwargs["db_kwargs"]["some_opt"] == "value"
|
|
@@ -29,7 +29,7 @@ from sqlalchemy import inspect
|
|
|
29
29
|
PG_URL = os.environ.get("DRLS_PG_URL")
|
|
30
30
|
pytestmark = pytest.mark.skipif(
|
|
31
31
|
not PG_URL,
|
|
32
|
-
reason="Requires DRLS_PG_URL (e.g. adbc
|
|
32
|
+
reason="Requires DRLS_PG_URL (e.g. postgresql+adbc://user:pw@host/db)",
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
# Lazy-imported so the pytest collection doesn't fail when the extra
|
|
@@ -12,72 +12,85 @@ def _build(url_str: str):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def test_full_url():
|
|
15
|
-
args, kwargs = _build("adbc
|
|
15
|
+
args, kwargs = _build("postgresql+adbc://user:pass@host:5432/mydb")
|
|
16
16
|
assert args == ["postgresql://user:pass@host:5432/mydb"]
|
|
17
17
|
assert kwargs == {}
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def test_no_credentials():
|
|
21
|
-
args, _ = _build("adbc
|
|
21
|
+
args, _ = _build("postgresql+adbc://host:5432/mydb")
|
|
22
22
|
assert args == ["postgresql://host:5432/mydb"]
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def test_no_port():
|
|
26
|
-
args, _ = _build("adbc
|
|
26
|
+
args, _ = _build("postgresql+adbc://user:pass@host/mydb")
|
|
27
27
|
assert args == ["postgresql://user:pass@host/mydb"]
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def test_query_options_in_db_kwargs():
|
|
31
|
-
_, kwargs = _build("adbc
|
|
31
|
+
_, kwargs = _build("postgresql+adbc://user:pass@host:5432/mydb?sslmode=require&app=x")
|
|
32
32
|
assert kwargs["db_kwargs"] == {"sslmode": "require", "app": "x"}
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def test_password_with_at_sign_quoted():
|
|
36
36
|
"""A password containing @ must not be interpreted as host delimiter."""
|
|
37
|
-
args, _ = _build("adbc
|
|
37
|
+
args, _ = _build("postgresql+adbc://user:p%40ss@host:5432/db")
|
|
38
38
|
# SQLAlchemy decodes %40 → @; we must re-encode for the DSN.
|
|
39
39
|
assert args == ["postgresql://user:p%40ss@host:5432/db"]
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def test_password_with_colon_quoted():
|
|
43
|
-
args, _ = _build("adbc
|
|
43
|
+
args, _ = _build("postgresql+adbc://user:a%3Ab@host/db")
|
|
44
44
|
assert args == ["postgresql://user:a%3Ab@host/db"]
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def test_password_with_slash_quoted():
|
|
48
|
-
args, _ = _build("adbc
|
|
48
|
+
args, _ = _build("postgresql+adbc://user:a%2Fb@host/db")
|
|
49
49
|
assert args == ["postgresql://user:a%2Fb@host/db"]
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def test_password_with_hash_quoted():
|
|
53
53
|
"""``#`` in a DSN starts a fragment; must be quoted."""
|
|
54
|
-
args, _ = _build("adbc
|
|
54
|
+
args, _ = _build("postgresql+adbc://user:a%23b@host/db")
|
|
55
55
|
assert args == ["postgresql://user:a%23b@host/db"]
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def test_password_with_percent_quoted():
|
|
59
|
-
args, _ = _build("adbc
|
|
59
|
+
args, _ = _build("postgresql+adbc://user:a%25b@host/db")
|
|
60
60
|
# SQLAlchemy decodes %25 → %; we must re-encode.
|
|
61
61
|
assert args == ["postgresql://user:a%25b@host/db"]
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def test_username_special_chars_quoted():
|
|
65
|
-
args, _ = _build("adbc
|
|
65
|
+
args, _ = _build("postgresql+adbc://u%40me:pw@host/db")
|
|
66
66
|
assert args == ["postgresql://u%40me:pw@host/db"]
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def test_database_with_spaces_quoted():
|
|
70
|
-
args, _ = _build("adbc
|
|
70
|
+
args, _ = _build("postgresql+adbc://host/my%20db")
|
|
71
71
|
assert args == ["postgresql://host/my%20db"]
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def test_database_with_encoded_percent_sequence_roundtrips():
|
|
75
|
+
# A database literally named ``my%20db`` arrives as ``%2520``. It must
|
|
76
|
+
# come out exactly as it went in, not collapsed to a space.
|
|
77
|
+
args, _ = _build("postgresql+adbc://host/my%2520db")
|
|
78
|
+
assert args == ["postgresql://host/my%2520db"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_database_with_encoded_slash_quoted():
|
|
82
|
+
# libpq reads ``/`` as a path separator, so it must stay escaped.
|
|
83
|
+
args, _ = _build("postgresql+adbc://host/a%2Fb")
|
|
84
|
+
assert args == ["postgresql://host/a%2Fb"]
|
|
85
|
+
|
|
86
|
+
|
|
74
87
|
def test_defaults_localhost():
|
|
75
|
-
args, _ = _build("adbc
|
|
88
|
+
args, _ = _build("postgresql+adbc:///mydb")
|
|
76
89
|
assert args == ["postgresql://localhost/mydb"]
|
|
77
90
|
|
|
78
91
|
|
|
79
92
|
def test_password_no_username_still_builds():
|
|
80
93
|
"""SQLAlchemy URL can have a password-only userinfo (edge case)."""
|
|
81
|
-
args, _ = _build("adbc
|
|
94
|
+
args, _ = _build("postgresql+adbc://:pw@host/db")
|
|
82
95
|
# We emit auth=":pw@" — libpq accepts this as password-only.
|
|
83
96
|
assert args[0].startswith("postgresql://:pw@host")
|
|
@@ -8,15 +8,18 @@ and are manually smoke-tested.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
from importlib.metadata import version
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
13
14
|
import pytest
|
|
14
15
|
import sqlalchemy as sa
|
|
15
16
|
from sqlalchemy import inspect
|
|
16
17
|
|
|
18
|
+
_ADBC_SQLITE_VERSION = tuple(int(part) for part in version("adbc-driver-sqlite").split(".")[:2])
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
def _make_engine():
|
|
19
|
-
return sa.create_engine("adbc
|
|
22
|
+
return sa.create_engine("sqlite+adbc:///:memory:")
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def _seed(engine) -> None:
|
|
@@ -362,10 +365,9 @@ def test_metadata_reflect_full_schema():
|
|
|
362
365
|
|
|
363
366
|
|
|
364
367
|
@pytest.mark.xfail(
|
|
365
|
-
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
"test unblocks automatically when the driver is fixed.",
|
|
368
|
+
_ADBC_SQLITE_VERSION < (1, 13),
|
|
369
|
+
reason="adbc_driver_sqlite < 1.13 reports xdbc_is_nullable='YES' for "
|
|
370
|
+
"every column, including NOT NULL ones (fixed upstream in 1.13.0).",
|
|
369
371
|
strict=True,
|
|
370
372
|
)
|
|
371
373
|
def test_metadata_reflect_column_nullability():
|
|
@@ -12,52 +12,52 @@ def _build(url_str: str):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def test_basic_dsn():
|
|
15
|
-
args, kwargs = _build("adbc
|
|
15
|
+
args, kwargs = _build("snowflake+adbc://user:pw@myacct/db/schema?warehouse=WH")
|
|
16
16
|
assert args == ["user:pw@myacct/db/schema?warehouse=WH"]
|
|
17
17
|
assert kwargs == {}
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def test_multiple_query_params_preserved():
|
|
21
|
-
args, _ = _build("adbc
|
|
21
|
+
args, _ = _build("snowflake+adbc://u:p@acct/db/s?warehouse=WH&role=R")
|
|
22
22
|
# Order of dict.items() in Python 3.7+ is insertion order; SQLAlchemy
|
|
23
23
|
# preserves URL order, so this is deterministic.
|
|
24
24
|
assert args[0].endswith("?warehouse=WH&role=R")
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def test_password_at_sign_escaped():
|
|
28
|
-
args, _ = _build("adbc
|
|
28
|
+
args, _ = _build("snowflake+adbc://u:p%40w@acct/db")
|
|
29
29
|
assert args == ["u:p%40w@acct/db"]
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def test_query_value_with_space_escaped():
|
|
33
33
|
"""warehouse names can legally contain spaces when quoted in SQL."""
|
|
34
|
-
args, _ = _build("adbc
|
|
34
|
+
args, _ = _build("snowflake+adbc://u:p@acct/db?warehouse=MY%20WH")
|
|
35
35
|
assert args[0].endswith("?warehouse=MY%20WH")
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def test_query_ampersand_in_value_escaped():
|
|
39
|
-
args, _ = _build("adbc
|
|
39
|
+
args, _ = _build("snowflake+adbc://u:p@acct/db?role=A%26B")
|
|
40
40
|
# & inside a value must be %26 or the parser will treat it as a separator
|
|
41
41
|
assert args[0].endswith("?role=A%26B")
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def test_no_database():
|
|
45
|
-
args, _ = _build("adbc
|
|
45
|
+
args, _ = _build("snowflake+adbc://u:p@acct")
|
|
46
46
|
assert args == ["u:p@acct"]
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def test_no_query():
|
|
50
|
-
args, _ = _build("adbc
|
|
50
|
+
args, _ = _build("snowflake+adbc://u:p@acct/db")
|
|
51
51
|
assert args == ["u:p@acct/db"]
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def test_username_only():
|
|
55
|
-
args, _ = _build("adbc
|
|
55
|
+
args, _ = _build("snowflake+adbc://u@acct/db")
|
|
56
56
|
assert args == ["u@acct/db"]
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def test_database_path_with_schema():
|
|
60
60
|
# SQLAlchemy treats everything after the host as `database`; we must
|
|
61
61
|
# NOT over-escape ``/`` between db and schema.
|
|
62
|
-
args, _ = _build("adbc
|
|
62
|
+
args, _ = _build("snowflake+adbc://u:p@acct/my_db/public")
|
|
63
63
|
assert args == ["u:p@acct/my_db/public"]
|
|
@@ -10,7 +10,7 @@ pytest.importorskip("adbc_driver_sqlite")
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def test_engine_created():
|
|
13
|
-
engine = sqlalchemy.create_engine("adbc
|
|
13
|
+
engine = sqlalchemy.create_engine("sqlite+adbc:///:memory:")
|
|
14
14
|
# `.name` is the backend identity (used by Alembic's DDL impl
|
|
15
15
|
# registry); `.driver` is the wire path. Reporting name="sqlite"
|
|
16
16
|
# lets Alembic autogenerate find the standard SQLite DDL impl.
|
|
@@ -19,7 +19,7 @@ def test_engine_created():
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_select_one():
|
|
22
|
-
engine = sqlalchemy.create_engine("adbc
|
|
22
|
+
engine = sqlalchemy.create_engine("sqlite+adbc:///:memory:")
|
|
23
23
|
with engine.connect() as conn:
|
|
24
24
|
result = conn.execute(text("SELECT 1 AS n"))
|
|
25
25
|
row = result.fetchone()
|
|
@@ -28,7 +28,7 @@ def test_select_one():
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def test_create_insert_select():
|
|
31
|
-
engine = sqlalchemy.create_engine("adbc
|
|
31
|
+
engine = sqlalchemy.create_engine("sqlite+adbc:///:memory:")
|
|
32
32
|
with engine.begin() as conn:
|
|
33
33
|
conn.execute(text("CREATE TABLE t (id INTEGER, name TEXT)"))
|
|
34
34
|
conn.execute(text("INSERT INTO t VALUES (1, 'a'), (2, 'b'), (3, 'c')"))
|
|
@@ -41,7 +41,7 @@ def test_create_insert_select():
|
|
|
41
41
|
|
|
42
42
|
def test_file_backed(tmp_path):
|
|
43
43
|
db_path = tmp_path / "test.db"
|
|
44
|
-
engine = sqlalchemy.create_engine(f"adbc
|
|
44
|
+
engine = sqlalchemy.create_engine(f"sqlite+adbc:///{db_path}")
|
|
45
45
|
with engine.begin() as conn:
|
|
46
46
|
conn.execute(text("CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT)"))
|
|
47
47
|
conn.execute(text("INSERT INTO kv VALUES ('hello', 'world')"))
|
|
@@ -52,7 +52,7 @@ def test_file_backed(tmp_path):
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def test_do_ping():
|
|
55
|
-
engine = sqlalchemy.create_engine("adbc
|
|
55
|
+
engine = sqlalchemy.create_engine("sqlite+adbc:///:memory:")
|
|
56
56
|
with engine.connect() as conn:
|
|
57
57
|
# pool_pre_ping path exercises dialect.do_ping
|
|
58
58
|
raw = conn.connection.dbapi_connection
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""URL scheme registration.
|
|
2
|
+
|
|
3
|
+
SQLAlchemy URLs are ``<backend>+<driver>://``. ADBC is the driver, so the
|
|
4
|
+
documented form is ``postgresql+adbc://``. The original ``adbc+postgresql://``
|
|
5
|
+
spelling stays registered as a deprecated alias (#15).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
from sqlalchemy.engine.url import make_url
|
|
15
|
+
|
|
16
|
+
from sqlalchemy_adbc.bigquery import ADBCBigQueryDialect
|
|
17
|
+
from sqlalchemy_adbc.flightsql import ADBCFlightSQLDialect
|
|
18
|
+
from sqlalchemy_adbc.postgresql import ADBCPostgreSQLDialect
|
|
19
|
+
from sqlalchemy_adbc.snowflake import ADBCSnowflakeDialect
|
|
20
|
+
from sqlalchemy_adbc.sqlite import ADBCSQLiteDialect
|
|
21
|
+
|
|
22
|
+
BACKENDS = [
|
|
23
|
+
("flightsql", ADBCFlightSQLDialect),
|
|
24
|
+
("sqlite", ADBCSQLiteDialect),
|
|
25
|
+
("postgresql", ADBCPostgreSQLDialect),
|
|
26
|
+
("snowflake", ADBCSnowflakeDialect),
|
|
27
|
+
("bigquery", ADBCBigQueryDialect),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize(("backend", "dialect_cls"), BACKENDS)
|
|
32
|
+
def test_backend_plus_adbc_resolves(backend, dialect_cls):
|
|
33
|
+
url = make_url(f"{backend}+adbc://host/db")
|
|
34
|
+
assert url.get_dialect() is dialect_cls
|
|
35
|
+
# URL and dialect agree on which side is the backend and which the driver.
|
|
36
|
+
assert url.get_backend_name() == dialect_cls.name
|
|
37
|
+
assert url.get_driver_name() == dialect_cls.driver == "adbc"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize(("backend", "dialect_cls"), BACKENDS)
|
|
41
|
+
def test_legacy_adbc_plus_backend_still_resolves(backend, dialect_cls):
|
|
42
|
+
assert make_url(f"adbc+{backend}://host/db").get_dialect() is dialect_cls
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_legacy_scheme_warns_on_engine_creation():
|
|
46
|
+
with pytest.warns(DeprecationWarning, match=r"sqlite\+adbc://"):
|
|
47
|
+
sa.create_engine("adbc+sqlite:///:memory:")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_documented_scheme_does_not_warn():
|
|
51
|
+
with warnings.catch_warnings():
|
|
52
|
+
warnings.simplefilter("error")
|
|
53
|
+
engine = sa.create_engine("sqlite+adbc:///:memory:")
|
|
54
|
+
with engine.connect() as conn:
|
|
55
|
+
assert conn.exec_driver_sql("SELECT 1").scalar() == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|