duckdb-sqlalchemy 1.5.3__tar.gz → 1.5.4__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.
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/workflows/pythonapp.yaml +1 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.pre-commit-config.yaml +2 -2
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/CHANGELOG.md +20 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/PKG-INFO +6 -5
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/README.md +2 -2
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/configuration.md +2 -1
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/motherduck.md +1 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/olap.md +22 -13
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/__init__.py +138 -68
- duckdb_sqlalchemy-1.5.4/duckdb_sqlalchemy/_arrow.py +39 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/datatypes.py +27 -20
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/motherduck.py +12 -4
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/olap.py +321 -24
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_core_units.py +301 -28
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_arrow_reads.py +2 -1
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/llms.txt +1 -1
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/noxfile.py +5 -2
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/pyproject.toml +4 -3
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/uv.lock +289 -287
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.codex/environments/environment.toml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/dependabot.yml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/workflows/lint.yml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/workflows/pages.yml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/workflows/publish.yaml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.gitignore +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/AGENTS.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/ARCHITECTURE.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/CLAUDE.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/CODE_OF_CONDUCT.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/LICENSE.txt +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/PLANS.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/ROADMAP.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/codecov.yml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/README.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/_config.yml +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/alembic.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/connection-urls.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/exec-plans/active/2026-03-27-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/exec-plans/active/2026-03-30-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/exec-plans/active/2026-03-31-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/getting-started.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/index.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/migration-from-duckdb-engine.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/overview.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/pandas-jupyter.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/robots.txt +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/seo-checklist.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/types-and-caveats.md +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_bulk_insert.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_query.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_statements.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_supports.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_validation.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/bulk.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/capabilities.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/config.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/conftest.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/py.typed +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/requirements.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/__init__.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/conftest.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/snapshots/test_datatypes/test_interval/schema.sql +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/sqlalchemy_suite/conftest.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/sqlalchemy_suite/test_suite.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_basic.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_datatypes.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_execution_options.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_helpers.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_integration.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_pandas.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_pyarrow.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_validation.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/util.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/url.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_attach_modes.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_multi_instance_pool.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_queuepool_high_concurrency.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_read_scaling_per_user.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/sqlalchemy_example.py +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/renovate.json +0 -0
- {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/test.cfg +0 -0
|
@@ -23,10 +23,10 @@ repos:
|
|
|
23
23
|
language: python
|
|
24
24
|
files: ^duckdb_sqlalchemy/(?!tests/).*\.py$
|
|
25
25
|
additional_dependencies:
|
|
26
|
-
- "ty==0.0.
|
|
26
|
+
- "ty==0.0.54"
|
|
27
27
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
28
28
|
# Ruff version.
|
|
29
|
-
rev: 'v0.15.
|
|
29
|
+
rev: 'v0.15.20'
|
|
30
30
|
hooks:
|
|
31
31
|
- id: ruff
|
|
32
32
|
args:
|
|
@@ -8,6 +8,26 @@ preserved from the upstream project for historical context.
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## [1.5.4](https://github.com/leonardovida/duckdb-sqlalchemy/compare/v1.5.3...v1.5.4) (2026-06-26)
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- expose MotherDuck `md_user_info().c.region` and Flight run `config` columns in the SQLAlchemy table-function helpers
|
|
16
|
+
- document one-off `md_run_flight(config=...)` overrides and selecting effective Flight run config
|
|
17
|
+
- route `motherduck_enable_server_side_temp_tables` through the MotherDuck startup URL parameter bucket
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
- restore SQLAlchemy 2.0.0+ compatibility by using a reflection fallback on early 2.0 releases
|
|
22
|
+
- reject empty MotherDuck Flight config keys before compiling Flight helper calls
|
|
23
|
+
- reject invalid MotherDuck Flight config map keys and NUL-containing values before compiling Flight helper calls
|
|
24
|
+
|
|
25
|
+
### Tooling
|
|
26
|
+
|
|
27
|
+
- extend the validated compatibility matrix to DuckDB 1.5.4 and SQLAlchemy 2.0.51
|
|
28
|
+
- bump the local `ty` pin to `0.0.54` and the Ruff pre-commit hook to `0.15.20` after validating the current checks against the latest non-breaking releases
|
|
29
|
+
- refresh the locked development dependency set to the latest non-breaking transitive releases within existing version caps
|
|
30
|
+
|
|
11
31
|
## [1.5.3](https://github.com/leonardovida/duckdb-sqlalchemy/compare/v1.5.2.2...v1.5.3) (2026-06-02)
|
|
12
32
|
|
|
13
33
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duckdb-sqlalchemy
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4
|
|
4
4
|
Summary: DuckDB SQLAlchemy dialect for DuckDB and MotherDuck
|
|
5
5
|
Project-URL: Bug Tracker, https://github.com/leonardovida/duckdb-sqlalchemy/issues
|
|
6
6
|
Project-URL: Changelog, https://github.com/leonardovida/duckdb-sqlalchemy/releases
|
|
@@ -27,7 +27,8 @@ Classifier: Topic :: Database :: Front-Ends
|
|
|
27
27
|
Requires-Python: <4,>=3.9
|
|
28
28
|
Requires-Dist: duckdb>=0.5.0
|
|
29
29
|
Requires-Dist: packaging>=21
|
|
30
|
-
Requires-Dist: sqlalchemy>=2.0.
|
|
30
|
+
Requires-Dist: sqlalchemy>=2.0.0; python_version < '3.14'
|
|
31
|
+
Requires-Dist: sqlalchemy>=2.0.45; python_version >= '3.14'
|
|
31
32
|
Provides-Extra: dev
|
|
32
33
|
Requires-Dist: fsspec<2027.0.0,>=2025.2.0; extra == 'dev'
|
|
33
34
|
Requires-Dist: github-action-utils<2.0.0,>=1.1.0; extra == 'dev'
|
|
@@ -46,7 +47,7 @@ Requires-Dist: pytest-snapshot<1.0.0,>=0.9.0; extra == 'dev'
|
|
|
46
47
|
Requires-Dist: pytest<10.0.0,>=8.0.0; extra == 'dev'
|
|
47
48
|
Requires-Dist: pytz>=2024.2; (python_version >= '3.12') and extra == 'dev'
|
|
48
49
|
Requires-Dist: toml<0.11.0,>=0.10.2; extra == 'dev'
|
|
49
|
-
Requires-Dist: ty==0.0.
|
|
50
|
+
Requires-Dist: ty==0.0.54; extra == 'dev'
|
|
50
51
|
Provides-Extra: devtools
|
|
51
52
|
Requires-Dist: nox<2027.0.0,>=2026.4.10; extra == 'devtools'
|
|
52
53
|
Requires-Dist: pdbpp<0.13.0,>=0.11.0; extra == 'devtools'
|
|
@@ -106,8 +107,8 @@ Current direction in this repository:
|
|
|
106
107
|
| Component | Supported versions |
|
|
107
108
|
| --- | --- |
|
|
108
109
|
| Python | 3.9+ |
|
|
109
|
-
| SQLAlchemy | 2.0.45+ (CI-tested: 2.0.
|
|
110
|
-
| DuckDB | 0.5.0+ (CI-tested currently: 1.3.0 to 1.5.
|
|
110
|
+
| SQLAlchemy | 2.0.0+ on Python <3.14; 2.0.45+ on Python 3.14+ (CI-tested: 2.0.0, 2.0.51) |
|
|
111
|
+
| DuckDB | 0.5.0+ (CI-tested currently: 1.3.0 to 1.5.4) |
|
|
111
112
|
|
|
112
113
|
## Install
|
|
113
114
|
|
|
@@ -51,8 +51,8 @@ Current direction in this repository:
|
|
|
51
51
|
| Component | Supported versions |
|
|
52
52
|
| --- | --- |
|
|
53
53
|
| Python | 3.9+ |
|
|
54
|
-
| SQLAlchemy | 2.0.45+ (CI-tested: 2.0.
|
|
55
|
-
| DuckDB | 0.5.0+ (CI-tested currently: 1.3.0 to 1.5.
|
|
54
|
+
| SQLAlchemy | 2.0.0+ on Python <3.14; 2.0.45+ on Python 3.14+ (CI-tested: 2.0.0, 2.0.51) |
|
|
55
|
+
| DuckDB | 0.5.0+ (CI-tested currently: 1.3.0 to 1.5.4) |
|
|
56
56
|
|
|
57
57
|
## Install
|
|
58
58
|
|
|
@@ -50,7 +50,8 @@ engine = create_engine("duckdb:///analytics.db", connect_args={"read_only": True
|
|
|
50
50
|
- Unknown but syntactically valid keys are forwarded to DuckDB and may fail with
|
|
51
51
|
DuckDB's native "unrecognized configuration parameter" error.
|
|
52
52
|
- MotherDuck path-query options (`attach_mode`, `session_name`, `access_mode`,
|
|
53
|
-
and related keys) are handled
|
|
53
|
+
`motherduck_enable_server_side_temp_tables`, and related keys) are handled
|
|
54
|
+
specially for `md:` URLs. See
|
|
54
55
|
[motherduck.md](motherduck) and [connection-urls.md](connection-urls).
|
|
55
56
|
|
|
56
57
|
## Preload extensions
|
|
@@ -70,6 +70,7 @@ Parameters that are treated as part of the database string:
|
|
|
70
70
|
- `dbinstance_inactivity_ttl` (preferred; `motherduck_dbinstance_inactivity_ttl`
|
|
71
71
|
remains supported but is deprecated)
|
|
72
72
|
- `saas_mode` (`pgendpoint` is supported when you need PG endpoint-compatible routing)
|
|
73
|
+
- `motherduck_enable_server_side_temp_tables`
|
|
73
74
|
- `cache_buster`
|
|
74
75
|
|
|
75
76
|
For backward compatibility the dialect also accepts `session_hint`,
|
|
@@ -99,6 +99,7 @@ user_stmt = select(
|
|
|
99
99
|
user_info.c.org_id,
|
|
100
100
|
user_info.c.org_name,
|
|
101
101
|
user_info.c.org_type,
|
|
102
|
+
user_info.c.region,
|
|
102
103
|
)
|
|
103
104
|
|
|
104
105
|
dives = md_list_dives()
|
|
@@ -143,28 +144,36 @@ versions = md_list_dive_versions(id="00000000-0000-0000-0000-000000000000")
|
|
|
143
144
|
versions_stmt = select(versions.c.version, versions.c.created_at)
|
|
144
145
|
```
|
|
145
146
|
|
|
146
|
-
## MotherDuck
|
|
147
|
+
## MotherDuck Flights
|
|
147
148
|
|
|
148
|
-
MotherDuck also exposes preview table functions for
|
|
149
|
-
helpers are useful for listing
|
|
149
|
+
MotherDuck also exposes preview table functions for Flight metadata. The
|
|
150
|
+
read-only helpers are useful for listing Flights, runs, logs, and versions:
|
|
150
151
|
|
|
151
152
|
```python
|
|
152
153
|
from sqlalchemy import select
|
|
153
|
-
from duckdb_sqlalchemy import
|
|
154
|
+
from duckdb_sqlalchemy import md_flight_runs, md_flight_versions, md_flights
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
flights = md_flights(limit=10)
|
|
157
|
+
flights_stmt = select(flights.c.flight_id, flights.c.flight_name, flights.c.status)
|
|
157
158
|
|
|
158
|
-
runs =
|
|
159
|
-
runs_stmt = select(runs.c.run_number, runs.c.status, runs.c.started_at)
|
|
159
|
+
runs = md_flight_runs(flight_id="00000000-0000-0000-0000-000000000000", limit=10)
|
|
160
|
+
runs_stmt = select(runs.c.run_number, runs.c.status, runs.c.config, runs.c.started_at)
|
|
160
161
|
|
|
161
|
-
versions =
|
|
162
|
-
versions_stmt = select(versions.c.
|
|
162
|
+
versions = md_flight_versions(flight_id="00000000-0000-0000-0000-000000000000")
|
|
163
|
+
versions_stmt = select(versions.c.flight_version, versions.c.requirements_txt)
|
|
163
164
|
```
|
|
164
165
|
|
|
165
|
-
Mutating
|
|
166
|
-
`
|
|
167
|
-
`
|
|
166
|
+
Mutating Flight functions are available as helpers too:
|
|
167
|
+
`md_create_flight`, `md_update_flight`, `md_delete_flight`, `md_run_flight`,
|
|
168
|
+
and `md_cancel_flight_run`. They only execute when the SQLAlchemy statement is
|
|
169
|
+
run. `md_run_flight` accepts a `config` map to override a Flight's config for
|
|
170
|
+
that run only; run result helpers expose the effective `config` column. Config
|
|
171
|
+
map keys must be strings, non-empty, and cannot contain `=` or NUL bytes.
|
|
172
|
+
Config values must be strings or `None`, and cannot contain NUL bytes.
|
|
173
|
+
|
|
174
|
+
The older `md_*job*` helper names remain as deprecated compatibility aliases.
|
|
175
|
+
They compile through the current Flight functions while preserving legacy
|
|
176
|
+
`job_*` column access where possible.
|
|
168
177
|
|
|
169
178
|
## Arrow results
|
|
170
179
|
|
|
@@ -41,6 +41,7 @@ from sqlalchemy.ext.compiler import compiles
|
|
|
41
41
|
from sqlalchemy.sql import bindparam
|
|
42
42
|
from sqlalchemy.sql.selectable import Select
|
|
43
43
|
|
|
44
|
+
from ._arrow import DuckDBArrowResult
|
|
44
45
|
from ._bulk_insert import build_bulk_insert_data as _build_bulk_insert_data
|
|
45
46
|
from ._bulk_insert import (
|
|
46
47
|
infer_bulk_insert_column_keys as _infer_bulk_insert_column_keys,
|
|
@@ -60,6 +61,7 @@ from .motherduck import (
|
|
|
60
61
|
DIALECT_QUERY_KEYS,
|
|
61
62
|
MOTHERDUCK_CONFIG_KEYS,
|
|
62
63
|
MotherDuckURL,
|
|
64
|
+
_database_with_path_query,
|
|
63
65
|
_normalize_config_aliases,
|
|
64
66
|
append_query_to_database,
|
|
65
67
|
create_engine_from_paths,
|
|
@@ -72,13 +74,22 @@ from .motherduck import (
|
|
|
72
74
|
)
|
|
73
75
|
from .olap import (
|
|
74
76
|
md_access_tokens,
|
|
77
|
+
md_cancel_flight_run,
|
|
75
78
|
md_cancel_job_run,
|
|
76
79
|
md_create_dive,
|
|
80
|
+
md_create_flight,
|
|
77
81
|
md_create_job,
|
|
78
82
|
md_delete_dive,
|
|
83
|
+
md_delete_flight,
|
|
79
84
|
md_delete_job,
|
|
85
|
+
md_flight_logs,
|
|
86
|
+
md_flight_runs,
|
|
87
|
+
md_flight_versions,
|
|
88
|
+
md_flights,
|
|
80
89
|
md_get_dive,
|
|
81
90
|
md_get_dive_version,
|
|
91
|
+
md_get_flight,
|
|
92
|
+
md_get_flight_version,
|
|
82
93
|
md_get_job,
|
|
83
94
|
md_get_job_version,
|
|
84
95
|
md_job_run_logs,
|
|
@@ -87,9 +98,11 @@ from .olap import (
|
|
|
87
98
|
md_jobs,
|
|
88
99
|
md_list_dive_versions,
|
|
89
100
|
md_list_dives,
|
|
101
|
+
md_run_flight,
|
|
90
102
|
md_run_job,
|
|
91
103
|
md_update_dive_content,
|
|
92
104
|
md_update_dive_metadata,
|
|
105
|
+
md_update_flight,
|
|
93
106
|
md_update_job,
|
|
94
107
|
md_user_info,
|
|
95
108
|
pragma_storage_info,
|
|
@@ -113,7 +126,7 @@ else:
|
|
|
113
126
|
try:
|
|
114
127
|
__version__ = package_version("duckdb-sqlalchemy")
|
|
115
128
|
except PackageNotFoundError: # pragma: no cover - source tree import fallback
|
|
116
|
-
__version__ = "1.5.
|
|
129
|
+
__version__ = "1.5.4"
|
|
117
130
|
sqlalchemy_version = sqlalchemy.__version__
|
|
118
131
|
SQLALCHEMY_VERSION = Version(sqlalchemy_version)
|
|
119
132
|
SQLALCHEMY_2 = SQLALCHEMY_VERSION >= Version("2.0.0")
|
|
@@ -146,12 +159,14 @@ __all__ = [
|
|
|
146
159
|
"insert", # reexport of sqlalchemy.dialects.postgresql.insert
|
|
147
160
|
"MotherDuckURL",
|
|
148
161
|
"URL",
|
|
162
|
+
"append_query_to_database",
|
|
149
163
|
"create_engine_from_paths",
|
|
150
164
|
"create_motherduck_engine",
|
|
151
165
|
"make_url",
|
|
152
166
|
"stable_session_hint",
|
|
153
167
|
"stable_session_name",
|
|
154
168
|
"table_function",
|
|
169
|
+
"validate_motherduck_database_name",
|
|
155
170
|
"read_parquet",
|
|
156
171
|
"read_csv",
|
|
157
172
|
"read_csv_auto",
|
|
@@ -160,6 +175,17 @@ __all__ = [
|
|
|
160
175
|
"md_user_info",
|
|
161
176
|
"md_list_dives",
|
|
162
177
|
"md_access_tokens",
|
|
178
|
+
"md_create_flight",
|
|
179
|
+
"md_flights",
|
|
180
|
+
"md_get_flight",
|
|
181
|
+
"md_update_flight",
|
|
182
|
+
"md_delete_flight",
|
|
183
|
+
"md_run_flight",
|
|
184
|
+
"md_cancel_flight_run",
|
|
185
|
+
"md_flight_runs",
|
|
186
|
+
"md_flight_logs",
|
|
187
|
+
"md_flight_versions",
|
|
188
|
+
"md_get_flight_version",
|
|
163
189
|
"md_create_job",
|
|
164
190
|
"md_jobs",
|
|
165
191
|
"md_get_job",
|
|
@@ -455,44 +481,6 @@ def _normalize_execution_options(execution_options: Dict[str, Any]) -> Dict[str,
|
|
|
455
481
|
return execution_options
|
|
456
482
|
|
|
457
483
|
|
|
458
|
-
class DuckDBArrowResult:
|
|
459
|
-
def __init__(self, result: Any) -> None:
|
|
460
|
-
self._result = result
|
|
461
|
-
self._arrow = None
|
|
462
|
-
|
|
463
|
-
def _fetch_arrow(self) -> Any:
|
|
464
|
-
if self._arrow is not None:
|
|
465
|
-
return self._arrow
|
|
466
|
-
cursor = getattr(self._result, "cursor", None)
|
|
467
|
-
if cursor is None:
|
|
468
|
-
cursor = getattr(self._result, "_cursor", None)
|
|
469
|
-
if cursor is None:
|
|
470
|
-
raise NotImplementedError("Arrow results are not available on this cursor")
|
|
471
|
-
fetch_arrow_table = getattr(cursor, "to_arrow_table", None)
|
|
472
|
-
if fetch_arrow_table is None:
|
|
473
|
-
fetch_arrow_table = getattr(cursor, "fetch_arrow_table", None)
|
|
474
|
-
if fetch_arrow_table is None:
|
|
475
|
-
raise NotImplementedError("Arrow results are not available on this cursor")
|
|
476
|
-
self._arrow = fetch_arrow_table()
|
|
477
|
-
return self._arrow
|
|
478
|
-
|
|
479
|
-
@property
|
|
480
|
-
def arrow(self) -> Any:
|
|
481
|
-
return self._fetch_arrow()
|
|
482
|
-
|
|
483
|
-
def all(self) -> Any:
|
|
484
|
-
return self._fetch_arrow()
|
|
485
|
-
|
|
486
|
-
def fetchall(self) -> Any:
|
|
487
|
-
return self._fetch_arrow()
|
|
488
|
-
|
|
489
|
-
def __getattr__(self, name: str) -> Any:
|
|
490
|
-
return getattr(self._result, name)
|
|
491
|
-
|
|
492
|
-
def __iter__(self) -> Any:
|
|
493
|
-
return iter(self._result)
|
|
494
|
-
|
|
495
|
-
|
|
496
484
|
class DuckDBExecutionContext(_PGExecutionContext):
|
|
497
485
|
@classmethod
|
|
498
486
|
def _init_compiled(
|
|
@@ -621,6 +609,32 @@ def _normalize_motherduck_config(config: Dict[str, Any]) -> None:
|
|
|
621
609
|
_normalize_config_aliases(config)
|
|
622
610
|
|
|
623
611
|
|
|
612
|
+
def _pop_application_name(config: Dict[str, Any]) -> Optional[str]:
|
|
613
|
+
application_name = config.pop("application_name", None)
|
|
614
|
+
if application_name is None:
|
|
615
|
+
return None
|
|
616
|
+
return str(application_name)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _prepare_connection_params(
|
|
620
|
+
cparams: Dict[str, Any], core_keys: Collection[str]
|
|
621
|
+
) -> Tuple[Dict[str, Any], Optional[str]]:
|
|
622
|
+
config = dict(cparams.get("config", {}))
|
|
623
|
+
cparams["config"] = config
|
|
624
|
+
config.update(cparams.pop("url_config", {}))
|
|
625
|
+
for key in DIALECT_QUERY_KEYS:
|
|
626
|
+
config.pop(key, None)
|
|
627
|
+
if cparams.get("database") in {None, ""}:
|
|
628
|
+
cparams["database"] = ":memory:"
|
|
629
|
+
_apply_motherduck_defaults(config, cparams.get("database"))
|
|
630
|
+
path_query = extract_path_query_from_config(config)
|
|
631
|
+
cparams["database"] = _database_with_path_query(cparams.get("database"), path_query)
|
|
632
|
+
_normalize_motherduck_config(config)
|
|
633
|
+
application_name = _pop_application_name(config)
|
|
634
|
+
ext = {k: config.pop(k) for k in list(config) if k not in core_keys}
|
|
635
|
+
return ext, application_name
|
|
636
|
+
|
|
637
|
+
|
|
624
638
|
class DuckDBIdentifierPreparer(PGIdentifierPreparer):
|
|
625
639
|
def __init__(self, dialect: "Dialect", **kwargs: Any) -> None:
|
|
626
640
|
super().__init__(dialect, **kwargs)
|
|
@@ -725,27 +739,14 @@ class Dialect(PGDialect_psycopg2):
|
|
|
725
739
|
def connect(self, *cargs: Any, **cparams: Any) -> Any:
|
|
726
740
|
core_keys = get_core_config()
|
|
727
741
|
preload_extensions = cparams.pop("preload_extensions", [])
|
|
728
|
-
|
|
729
|
-
cparams["config"]
|
|
730
|
-
config.update(cparams.pop("url_config", {}))
|
|
731
|
-
for key in DIALECT_QUERY_KEYS:
|
|
732
|
-
config.pop(key, None)
|
|
733
|
-
if cparams.get("database") in {None, ""}:
|
|
734
|
-
cparams["database"] = ":memory:"
|
|
735
|
-
_apply_motherduck_defaults(config, cparams.get("database"))
|
|
736
|
-
path_query = extract_path_query_from_config(config)
|
|
737
|
-
if path_query:
|
|
738
|
-
cparams["database"] = append_query_to_database(
|
|
739
|
-
cparams.get("database"), path_query
|
|
740
|
-
)
|
|
741
|
-
validate_motherduck_database_name(cparams.get("database"))
|
|
742
|
-
_normalize_motherduck_config(config)
|
|
743
|
-
|
|
744
|
-
ext = {k: config.pop(k) for k in list(config) if k not in core_keys}
|
|
742
|
+
ext, application_name = _prepare_connection_params(cparams, core_keys)
|
|
743
|
+
config = cparams["config"]
|
|
745
744
|
if supports_user_agent:
|
|
746
745
|
user_agent = (
|
|
747
746
|
f"duckdb-sqlalchemy/{__version__}(sqlalchemy/{sqlalchemy_version})"
|
|
748
747
|
)
|
|
748
|
+
if application_name:
|
|
749
|
+
user_agent = f"{user_agent} application_name({application_name})"
|
|
749
750
|
if "custom_user_agent" in config:
|
|
750
751
|
user_agent = f"{user_agent} {config['custom_user_agent']}"
|
|
751
752
|
config["custom_user_agent"] = user_agent
|
|
@@ -1176,6 +1177,79 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1176
1177
|
column_names.append(None)
|
|
1177
1178
|
return expressions, column_names
|
|
1178
1179
|
|
|
1180
|
+
def _reflect_pg_type_compat(
|
|
1181
|
+
self,
|
|
1182
|
+
format_type: Optional[str],
|
|
1183
|
+
*,
|
|
1184
|
+
type_description: str,
|
|
1185
|
+
) -> Any:
|
|
1186
|
+
reflect_type = getattr(super(), "_reflect_type", None)
|
|
1187
|
+
if reflect_type is not None:
|
|
1188
|
+
return reflect_type(
|
|
1189
|
+
format_type,
|
|
1190
|
+
{},
|
|
1191
|
+
{},
|
|
1192
|
+
type_description=type_description,
|
|
1193
|
+
collation=None,
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
if format_type is None:
|
|
1197
|
+
util.warn(
|
|
1198
|
+
"PostgreSQL format_type() returned NULL for %s" % type_description
|
|
1199
|
+
)
|
|
1200
|
+
return sqltypes.NULLTYPE
|
|
1201
|
+
|
|
1202
|
+
args_match = re.search(r"\((.*)\)$", format_type)
|
|
1203
|
+
type_args: List[str]
|
|
1204
|
+
if args_match and args_match.group(1):
|
|
1205
|
+
type_args = [part.strip() for part in args_match.group(1).split(",")]
|
|
1206
|
+
else:
|
|
1207
|
+
type_args = []
|
|
1208
|
+
|
|
1209
|
+
type_name = re.sub(r"\(.*\)$", "", format_type).strip().lower()
|
|
1210
|
+
schema_type = self.ischema_names.get(type_name)
|
|
1211
|
+
args: Tuple[Any, ...] = ()
|
|
1212
|
+
kwargs: Dict[str, Any] = {}
|
|
1213
|
+
|
|
1214
|
+
if type_name == "numeric" and len(type_args) == 2:
|
|
1215
|
+
args = (int(type_args[0]), int(type_args[1]))
|
|
1216
|
+
elif type_name == "double precision":
|
|
1217
|
+
args = (53,)
|
|
1218
|
+
elif type_name in (
|
|
1219
|
+
"timestamp with time zone",
|
|
1220
|
+
"time with time zone",
|
|
1221
|
+
):
|
|
1222
|
+
kwargs["timezone"] = True
|
|
1223
|
+
if len(type_args) == 1:
|
|
1224
|
+
kwargs["precision"] = int(type_args[0])
|
|
1225
|
+
elif type_name in (
|
|
1226
|
+
"timestamp without time zone",
|
|
1227
|
+
"time without time zone",
|
|
1228
|
+
"time",
|
|
1229
|
+
):
|
|
1230
|
+
kwargs["timezone"] = False
|
|
1231
|
+
if len(type_args) == 1:
|
|
1232
|
+
kwargs["precision"] = int(type_args[0])
|
|
1233
|
+
elif type_name == "bit varying":
|
|
1234
|
+
kwargs["varying"] = True
|
|
1235
|
+
if len(type_args) == 1:
|
|
1236
|
+
args = (int(type_args[0]),)
|
|
1237
|
+
else:
|
|
1238
|
+
try:
|
|
1239
|
+
charlen = int(type_args[0])
|
|
1240
|
+
except (IndexError, ValueError):
|
|
1241
|
+
args = tuple(type_args)
|
|
1242
|
+
else:
|
|
1243
|
+
args = (charlen, *type_args[1:])
|
|
1244
|
+
|
|
1245
|
+
if schema_type is None:
|
|
1246
|
+
util.warn(
|
|
1247
|
+
"Did not recognize type '%s' of %s" % (type_name, type_description)
|
|
1248
|
+
)
|
|
1249
|
+
return sqltypes.NULLTYPE
|
|
1250
|
+
|
|
1251
|
+
return schema_type(*args, **kwargs)
|
|
1252
|
+
|
|
1179
1253
|
def _reflect_duckdb_data_type(
|
|
1180
1254
|
self,
|
|
1181
1255
|
data_type: str,
|
|
@@ -1213,12 +1287,9 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1213
1287
|
elif upper.startswith(("STRUCT(", "MAP(", "UNION(")):
|
|
1214
1288
|
reflected = sqltypes.NULLTYPE
|
|
1215
1289
|
else:
|
|
1216
|
-
reflected = self.
|
|
1290
|
+
reflected = self._reflect_pg_type_compat(
|
|
1217
1291
|
normalized,
|
|
1218
|
-
{},
|
|
1219
|
-
{},
|
|
1220
1292
|
type_description=type_description,
|
|
1221
|
-
collation=None,
|
|
1222
1293
|
)
|
|
1223
1294
|
|
|
1224
1295
|
if dimensions:
|
|
@@ -1473,8 +1544,7 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1473
1544
|
database = opts.get("database")
|
|
1474
1545
|
if database in {None, ""}:
|
|
1475
1546
|
database = ":memory:"
|
|
1476
|
-
|
|
1477
|
-
opts["database"] = append_query_to_database(database, path_query)
|
|
1547
|
+
opts["database"] = _database_with_path_query(database, path_query)
|
|
1478
1548
|
return (), opts
|
|
1479
1549
|
|
|
1480
1550
|
@classmethod
|
|
@@ -1525,7 +1595,9 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1525
1595
|
view_name = f"__duckdb_sa_bulk_{uuid.uuid4().hex}"
|
|
1526
1596
|
dbapi_conn = cursor.connection
|
|
1527
1597
|
dbapi_conn.register(view_name, data)
|
|
1528
|
-
preparer = getattr(
|
|
1598
|
+
preparer: Any = getattr(
|
|
1599
|
+
context, "identifier_preparer", self.identifier_preparer
|
|
1600
|
+
)
|
|
1529
1601
|
target = preparer.format_table(table)
|
|
1530
1602
|
columns = ", ".join(preparer.quote(col) for col in column_names)
|
|
1531
1603
|
insert_sql = (
|
|
@@ -1566,11 +1638,9 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1566
1638
|
|
|
1567
1639
|
def _execute_with_retry(
|
|
1568
1640
|
self,
|
|
1569
|
-
cursor: Any,
|
|
1570
1641
|
statement: str,
|
|
1571
|
-
parameters: Any,
|
|
1572
1642
|
context: Optional[Any],
|
|
1573
|
-
executor: Any,
|
|
1643
|
+
executor: Callable[[], Any],
|
|
1574
1644
|
) -> Any:
|
|
1575
1645
|
options = self._get_execution_options(context)
|
|
1576
1646
|
retry_count = int(options.get("duckdb_retry_count", 0) or 0)
|
|
@@ -1602,7 +1672,7 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1602
1672
|
self, cursor, statement, parameters, context
|
|
1603
1673
|
)
|
|
1604
1674
|
|
|
1605
|
-
self._execute_with_retry(
|
|
1675
|
+
self._execute_with_retry(statement, context, executor)
|
|
1606
1676
|
|
|
1607
1677
|
def do_execute_no_params(
|
|
1608
1678
|
self, cursor: Any, statement: str, context: Optional[Any] = None
|
|
@@ -1610,7 +1680,7 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1610
1680
|
def executor() -> Any:
|
|
1611
1681
|
return DefaultDialect.do_execute_no_params(self, cursor, statement, context)
|
|
1612
1682
|
|
|
1613
|
-
self._execute_with_retry(
|
|
1683
|
+
self._execute_with_retry(statement, context, executor)
|
|
1614
1684
|
|
|
1615
1685
|
def _pg_class_filter_scope_schema(
|
|
1616
1686
|
self,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DuckDBArrowResult:
|
|
5
|
+
def __init__(self, result: Any) -> None:
|
|
6
|
+
self._result = result
|
|
7
|
+
self._arrow = None
|
|
8
|
+
|
|
9
|
+
def _fetch_arrow(self) -> Any:
|
|
10
|
+
if self._arrow is not None:
|
|
11
|
+
return self._arrow
|
|
12
|
+
cursor = getattr(self._result, "cursor", None)
|
|
13
|
+
if cursor is None:
|
|
14
|
+
cursor = getattr(self._result, "_cursor", None)
|
|
15
|
+
if cursor is None:
|
|
16
|
+
raise NotImplementedError("Arrow results are not available on this cursor")
|
|
17
|
+
fetch_arrow_table = getattr(cursor, "to_arrow_table", None)
|
|
18
|
+
if fetch_arrow_table is None:
|
|
19
|
+
fetch_arrow_table = getattr(cursor, "fetch_arrow_table", None)
|
|
20
|
+
if fetch_arrow_table is None:
|
|
21
|
+
raise NotImplementedError("Arrow results are not available on this cursor")
|
|
22
|
+
self._arrow = fetch_arrow_table()
|
|
23
|
+
return self._arrow
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def arrow(self) -> Any:
|
|
27
|
+
return self._fetch_arrow()
|
|
28
|
+
|
|
29
|
+
def all(self) -> Any:
|
|
30
|
+
return self._fetch_arrow()
|
|
31
|
+
|
|
32
|
+
def fetchall(self) -> Any:
|
|
33
|
+
return self._fetch_arrow()
|
|
34
|
+
|
|
35
|
+
def __getattr__(self, name: str) -> Any:
|
|
36
|
+
return getattr(self._result, name)
|
|
37
|
+
|
|
38
|
+
def __iter__(self) -> Any:
|
|
39
|
+
return iter(self._result)
|
|
@@ -13,10 +13,11 @@ from typing import Any, Dict, Optional, Tuple, Type
|
|
|
13
13
|
import duckdb
|
|
14
14
|
from packaging.version import Version
|
|
15
15
|
from sqlalchemy import exc, util
|
|
16
|
-
from sqlalchemy.dialects.postgresql.base import
|
|
16
|
+
from sqlalchemy.dialects.postgresql.base import PGTypeCompiler
|
|
17
17
|
from sqlalchemy.engine import Dialect
|
|
18
18
|
from sqlalchemy.ext.compiler import compiles
|
|
19
19
|
from sqlalchemy.sql import sqltypes, type_api
|
|
20
|
+
from sqlalchemy.sql.compiler import IdentifierPreparer
|
|
20
21
|
from sqlalchemy.sql.type_api import TypeEngine
|
|
21
22
|
from sqlalchemy.types import BigInteger, Integer, SmallInteger, String
|
|
22
23
|
|
|
@@ -305,20 +306,18 @@ def register_extension_types() -> None:
|
|
|
305
306
|
def visit_struct(
|
|
306
307
|
instance: Struct,
|
|
307
308
|
compiler: PGTypeCompiler,
|
|
308
|
-
identifier_preparer: PGIdentifierPreparer,
|
|
309
309
|
**kw: Any,
|
|
310
310
|
) -> str:
|
|
311
|
-
return "STRUCT" + struct_or_union(instance, compiler,
|
|
311
|
+
return "STRUCT" + struct_or_union(instance, compiler, **kw)
|
|
312
312
|
|
|
313
313
|
|
|
314
314
|
@compiles(Union, "duckdb") # type: ignore[misc]
|
|
315
315
|
def visit_union(
|
|
316
316
|
instance: Union,
|
|
317
317
|
compiler: PGTypeCompiler,
|
|
318
|
-
identifier_preparer: PGIdentifierPreparer,
|
|
319
318
|
**kw: Any,
|
|
320
319
|
) -> str:
|
|
321
|
-
return "UNION" + struct_or_union(instance, compiler,
|
|
320
|
+
return "UNION" + struct_or_union(instance, compiler, **kw)
|
|
322
321
|
|
|
323
322
|
|
|
324
323
|
@compiles(Variant, "duckdb") # type: ignore[misc]
|
|
@@ -342,25 +341,15 @@ def visit_geometry(
|
|
|
342
341
|
def struct_or_union(
|
|
343
342
|
instance: typing.Union[Union, Struct],
|
|
344
343
|
compiler: PGTypeCompiler,
|
|
345
|
-
identifier_preparer:
|
|
344
|
+
identifier_preparer: Optional[IdentifierPreparer] = None,
|
|
346
345
|
**kw: Any,
|
|
347
346
|
) -> str:
|
|
348
|
-
fields =
|
|
349
|
-
if fields is None:
|
|
350
|
-
fields = _normalize_fields(instance.fields)
|
|
347
|
+
fields = instance._fields
|
|
351
348
|
if fields is None:
|
|
352
349
|
raise exc.CompileError(f"DuckDB {repr(instance)} type requires fields")
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
identifier_preparer.quote_identifier(key),
|
|
357
|
-
process_type(
|
|
358
|
-
value, compiler, identifier_preparer=identifier_preparer, **kw
|
|
359
|
-
),
|
|
360
|
-
)
|
|
361
|
-
for key, value in (fields.items() if isinstance(fields, dict) else fields)
|
|
362
|
-
)
|
|
363
|
-
)
|
|
350
|
+
if identifier_preparer is None:
|
|
351
|
+
identifier_preparer = compiler.dialect.identifier_preparer
|
|
352
|
+
return _render_struct_or_union_fields(fields, compiler, identifier_preparer, **kw)
|
|
364
353
|
|
|
365
354
|
|
|
366
355
|
def process_type(
|
|
@@ -371,6 +360,24 @@ def process_type(
|
|
|
371
360
|
return compiler.process(type_api.to_instance(value), **kw)
|
|
372
361
|
|
|
373
362
|
|
|
363
|
+
def _render_struct_or_union_fields(
|
|
364
|
+
fields: Tuple[Tuple[str, TypeEngine], ...],
|
|
365
|
+
compiler: PGTypeCompiler,
|
|
366
|
+
identifier_preparer: IdentifierPreparer,
|
|
367
|
+
**kw: Any,
|
|
368
|
+
) -> str:
|
|
369
|
+
rendered_fields = (
|
|
370
|
+
"{} {}".format(
|
|
371
|
+
identifier_preparer.quote_identifier(key),
|
|
372
|
+
process_type(
|
|
373
|
+
value, compiler, identifier_preparer=identifier_preparer, **kw
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
for key, value in fields
|
|
377
|
+
)
|
|
378
|
+
return "({})".format(", ".join(rendered_fields))
|
|
379
|
+
|
|
380
|
+
|
|
374
381
|
@compiles(Map, "duckdb") # type: ignore[misc]
|
|
375
382
|
def visit_map(instance: Map, compiler: PGTypeCompiler, **kw: Any) -> str:
|
|
376
383
|
return "MAP({}, {})".format(
|