duckdb-sqlalchemy 1.5.2__tar.gz → 1.5.2.2__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.2 → duckdb_sqlalchemy-1.5.2.2}/.pre-commit-config.yaml +2 -2
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/CHANGELOG.md +35 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/PKG-INFO +6 -6
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/connection-urls.md +11 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/motherduck.md +19 -2
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/olap.md +19 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/__init__.py +103 -74
- duckdb_sqlalchemy-1.5.2.2/duckdb_sqlalchemy/_bulk_insert.py +50 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/_validation.py +17 -13
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/bulk.py +22 -14
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/motherduck.py +43 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/olap.py +23 -1
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/conftest.py +55 -55
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/test_core_units.py +304 -1
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/test_execution_options.py +18 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/test_helpers.py +55 -0
- duckdb_sqlalchemy-1.5.2.2/duckdb_sqlalchemy/tests/test_validation.py +46 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/noxfile.py +0 -2
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/pyproject.toml +6 -6
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/uv.lock +180 -182
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.codex/environments/environment.toml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.github/dependabot.yml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.github/workflows/lint.yml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.github/workflows/pages.yml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.github/workflows/publish.yaml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.github/workflows/pythonapp.yaml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/.gitignore +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/AGENTS.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/ARCHITECTURE.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/CLAUDE.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/CODE_OF_CONDUCT.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/LICENSE.txt +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/PLANS.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/README.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/ROADMAP.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/codecov.yml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/README.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/_config.yml +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/alembic.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/configuration.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/exec-plans/active/2026-03-27-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/exec-plans/active/2026-03-30-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/exec-plans/active/2026-03-31-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/getting-started.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/index.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/migration-from-duckdb-engine.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/overview.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/pandas-jupyter.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/robots.txt +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/seo-checklist.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/docs/types-and-caveats.md +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/_query.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/_supports.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/capabilities.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/config.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/conftest.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/datatypes.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/py.typed +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/requirements.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/__init__.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/snapshots/test_datatypes/test_interval/schema.sql +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/sqlalchemy_suite/conftest.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/sqlalchemy_suite/test_suite.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/test_basic.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/test_datatypes.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/test_integration.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/test_pandas.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/test_pyarrow.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/tests/util.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/duckdb_sqlalchemy/url.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/examples/motherduck_arrow_reads.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/examples/motherduck_attach_modes.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/examples/motherduck_multi_instance_pool.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/examples/motherduck_queuepool_high_concurrency.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/examples/motherduck_read_scaling_per_user.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/examples/sqlalchemy_example.py +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/llms.txt +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/renovate.json +0 -0
- {duckdb_sqlalchemy-1.5.2 → duckdb_sqlalchemy-1.5.2.2}/test.cfg +0 -0
|
@@ -19,11 +19,11 @@ repos:
|
|
|
19
19
|
hooks:
|
|
20
20
|
- id: ty
|
|
21
21
|
name: ty
|
|
22
|
-
entry: ty check --ignore unresolved-import
|
|
22
|
+
entry: ty check --ignore unresolved-import
|
|
23
23
|
language: python
|
|
24
24
|
files: ^duckdb_sqlalchemy/(?!tests/).*\.py$
|
|
25
25
|
additional_dependencies:
|
|
26
|
-
- "ty==0.0.
|
|
26
|
+
- "ty==0.0.34"
|
|
27
27
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
28
28
|
# Ruff version.
|
|
29
29
|
rev: 'v0.15.11'
|
|
@@ -8,6 +8,41 @@ preserved from the upstream project for historical context.
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## [1.5.2.2](https://github.com/leonardovida/duckdb-sqlalchemy/compare/v1.5.2.1...v1.5.2.2) (2026-05-11)
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- add `md_user_info()` for querying MotherDuck user and organization metadata through SQLAlchemy table-valued functions
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
- prefer the documented `MOTHERDUCK_TOKEN` environment variable over the lowercase fallback when both are set
|
|
20
|
+
- reject MotherDuck database names containing commas before building SQLAlchemy URLs
|
|
21
|
+
- ignore common PostgreSQL session setup statements and transaction isolation declarations emitted by PostgreSQL-oriented clients
|
|
22
|
+
|
|
23
|
+
### Tooling
|
|
24
|
+
|
|
25
|
+
- extract bulk insert builder logic into a focused helper module without changing the public execution option behavior
|
|
26
|
+
- revert unreleased PostgreSQL compatibility-mode aliases before publishing them
|
|
27
|
+
- bump the local `ty` pin to `0.0.34` after validating the current checks against the latest non-breaking release
|
|
28
|
+
- refresh the locked development dependency set to the latest non-breaking releases within existing version caps
|
|
29
|
+
- deduplicate reflection fallback wrapper handling
|
|
30
|
+
|
|
31
|
+
## [1.5.2.1](https://github.com/leonardovida/duckdb-sqlalchemy/compare/v1.5.2...v1.5.2.1) (2026-04-25)
|
|
32
|
+
|
|
33
|
+
### Bug Fixes
|
|
34
|
+
|
|
35
|
+
- treat MotherDuck transport overrides such as `host`, `region_host`, `port`, `tls`, and `grpc_local_subchannel_pool` as startup URL parameters so SQLAlchemy URLs and `connect_args["config"]` keep working with current MotherDuck routing behavior
|
|
36
|
+
- derive `duckdb_sqlalchemy.__version__` from installed package metadata so the runtime version matches published package metadata and generated user agents
|
|
37
|
+
- normalize legacy MotherDuck transport aliases such as `motherduck_host`, `motherduck_port`, `motherduck_use_tls`, `motherduck_region_host`, and `motherduck_grpc_local_subchannel_pool` into the startup URL parameters the current extension expects, while emitting deprecation warnings
|
|
38
|
+
- restore the exported package version to `1.5.2` so the runtime version matches published package metadata
|
|
39
|
+
|
|
40
|
+
### Tooling
|
|
41
|
+
|
|
42
|
+
- bump the local `ty` pin to `0.0.32` after validating the current checks against the latest non-breaking release
|
|
43
|
+
- allow `pre-commit` 4.6.x in the devtools extra
|
|
44
|
+
- refresh validated dev minimums for `nox` and `pytest-cov`
|
|
45
|
+
|
|
11
46
|
## [1.5.2](https://github.com/leonardovida/duckdb-sqlalchemy/compare/v1.5.1.4...v1.5.2) (2026-04-17)
|
|
12
47
|
|
|
13
48
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duckdb-sqlalchemy
|
|
3
|
-
Version: 1.5.2
|
|
3
|
+
Version: 1.5.2.2
|
|
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
|
|
@@ -33,24 +33,24 @@ Requires-Dist: fsspec<2027.0.0,>=2025.2.0; extra == 'dev'
|
|
|
33
33
|
Requires-Dist: github-action-utils<2.0.0,>=1.1.0; extra == 'dev'
|
|
34
34
|
Requires-Dist: hypothesis<7.0.0,>=6.75.2; extra == 'dev'
|
|
35
35
|
Requires-Dist: jupysql<0.12.0,>=0.11.1; extra == 'dev'
|
|
36
|
-
Requires-Dist: nox<2027.0.0,>=2026.
|
|
36
|
+
Requires-Dist: nox<2027.0.0,>=2026.4.10; extra == 'dev'
|
|
37
37
|
Requires-Dist: numpy<2.0,>=1.24; (python_version < '3.12') and extra == 'dev'
|
|
38
38
|
Requires-Dist: numpy<3.0,>=1.26; (python_version >= '3.12' and python_version < '3.13') and extra == 'dev'
|
|
39
39
|
Requires-Dist: numpy<3.0,>=2.0; (python_version >= '3.13') and extra == 'dev'
|
|
40
40
|
Requires-Dist: pandas<2.0,>=1; (python_version < '3.12') and extra == 'dev'
|
|
41
41
|
Requires-Dist: pandas<4.0,>=2.2; (python_version >= '3.12') and extra == 'dev'
|
|
42
42
|
Requires-Dist: pyarrow>=22.0.0; (python_version >= '3.10') and extra == 'dev'
|
|
43
|
-
Requires-Dist: pytest-cov<8.0.0,>=
|
|
43
|
+
Requires-Dist: pytest-cov<8.0.0,>=7.1.0; extra == 'dev'
|
|
44
44
|
Requires-Dist: pytest-remotedata<0.5.0,>=0.4.0; extra == 'dev'
|
|
45
45
|
Requires-Dist: pytest-snapshot<1.0.0,>=0.9.0; extra == 'dev'
|
|
46
46
|
Requires-Dist: pytest<10.0.0,>=8.0.0; extra == 'dev'
|
|
47
47
|
Requires-Dist: pytz>=2024.2; (python_version >= '3.12') and extra == 'dev'
|
|
48
48
|
Requires-Dist: toml<0.11.0,>=0.10.2; extra == 'dev'
|
|
49
|
-
Requires-Dist: ty==0.0.
|
|
49
|
+
Requires-Dist: ty==0.0.34; extra == 'dev'
|
|
50
50
|
Provides-Extra: devtools
|
|
51
|
-
Requires-Dist: nox<2027.0.0,>=2026.
|
|
51
|
+
Requires-Dist: nox<2027.0.0,>=2026.4.10; extra == 'devtools'
|
|
52
52
|
Requires-Dist: pdbpp<0.13.0,>=0.11.0; extra == 'devtools'
|
|
53
|
-
Requires-Dist: pre-commit<4.
|
|
53
|
+
Requires-Dist: pre-commit<4.7.0,>=4.6.0; (python_version >= '3.10') and extra == 'devtools'
|
|
54
54
|
Description-Content-Type: text/markdown
|
|
55
55
|
|
|
56
56
|
# duckdb-sqlalchemy
|
|
@@ -82,6 +82,17 @@ url = MotherDuckURL(
|
|
|
82
82
|
engine = create_engine(url)
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
Local or staging endpoint overrides belong in the same path-query bucket:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
url = MotherDuckURL(
|
|
89
|
+
database="md:my_db",
|
|
90
|
+
host="localhost",
|
|
91
|
+
port=1984,
|
|
92
|
+
tls="off",
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
85
96
|
## Manual escaping
|
|
86
97
|
|
|
87
98
|
If you build URLs manually and your token contains special characters, escape it:
|
|
@@ -6,6 +6,7 @@ title: MotherDuck
|
|
|
6
6
|
# MotherDuck
|
|
7
7
|
|
|
8
8
|
MotherDuck connections use the `md:` database prefix.
|
|
9
|
+
Database names may not contain commas.
|
|
9
10
|
|
|
10
11
|
```python
|
|
11
12
|
from sqlalchemy import create_engine
|
|
@@ -62,12 +63,13 @@ string for MotherDuck connections.
|
|
|
62
63
|
Parameters that are treated as part of the database string:
|
|
63
64
|
|
|
64
65
|
- `user`
|
|
66
|
+
- `host`, `region_host`, `port`, `tls`, `grpc_local_subchannel_pool`
|
|
65
67
|
- `session_name` (read-scaling affinity)
|
|
66
68
|
- `attach_mode` (`workspace` or `single`)
|
|
67
69
|
- `access_mode` (`read_only` for read-scaling tokens)
|
|
68
70
|
- `dbinstance_inactivity_ttl` (preferred; `motherduck_dbinstance_inactivity_ttl`
|
|
69
71
|
remains supported but is deprecated)
|
|
70
|
-
- `saas_mode`
|
|
72
|
+
- `saas_mode` (`pgendpoint` is supported when you need PG endpoint-compatible routing)
|
|
71
73
|
- `cache_buster`
|
|
72
74
|
|
|
73
75
|
For backward compatibility the dialect also accepts `session_hint`,
|
|
@@ -81,6 +83,13 @@ Example:
|
|
|
81
83
|
duckdb:///md:my_db?attach_mode=single&access_mode=read_only&session_name=team-a
|
|
82
84
|
```
|
|
83
85
|
|
|
86
|
+
For local or staging routing, keep the endpoint override in the database string
|
|
87
|
+
as well:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
duckdb:///md:my_db?host=localhost&port=1984&tls=off
|
|
91
|
+
```
|
|
92
|
+
|
|
84
93
|
If you pass these in `connect_args["config"]`, the dialect will move them into the database string automatically.
|
|
85
94
|
|
|
86
95
|
### Config parameters
|
|
@@ -88,7 +97,15 @@ If you pass these in `connect_args["config"]`, the dialect will move them into t
|
|
|
88
97
|
Other DuckDB settings can be passed as URL query params or via `connect_args["config"]`:
|
|
89
98
|
|
|
90
99
|
- Any DuckDB `SET`-table config option (for example `memory_limit`, `threads`)
|
|
91
|
-
- MotherDuck startup auth keys such as `motherduck_token` and `
|
|
100
|
+
- MotherDuck startup auth keys such as `motherduck_token`, `token`, `motherduck_oauth_token`, and `oauth_token`
|
|
101
|
+
|
|
102
|
+
Example with a MotherDuck host override / PG endpoint-style routing:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
engine = create_engine(
|
|
106
|
+
"duckdb:///md:my_db?host=custom.motherduck.com&port=443&tls=true&saas_mode=pgendpoint"
|
|
107
|
+
)
|
|
108
|
+
```
|
|
92
109
|
|
|
93
110
|
## Recommended defaults for apps/BI
|
|
94
111
|
|
|
@@ -50,6 +50,25 @@ parquet = table_function(
|
|
|
50
50
|
stmt = select(parquet.c.event_id, parquet.c.ts)
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
## MotherDuck user metadata
|
|
54
|
+
|
|
55
|
+
MotherDuck exposes `md_user_info()` for the current user and organization.
|
|
56
|
+
The helper names the released columns so they are available through SQLAlchemy:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sqlalchemy import select
|
|
60
|
+
from duckdb_sqlalchemy import md_user_info
|
|
61
|
+
|
|
62
|
+
user_info = md_user_info()
|
|
63
|
+
stmt = select(
|
|
64
|
+
user_info.c.user_id,
|
|
65
|
+
user_info.c.username,
|
|
66
|
+
user_info.c.org_id,
|
|
67
|
+
user_info.c.org_name,
|
|
68
|
+
user_info.c.org_type,
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
53
72
|
## Arrow results
|
|
54
73
|
|
|
55
74
|
For large reads, you can request Arrow tables directly:
|
|
@@ -5,9 +5,12 @@ import uuid
|
|
|
5
5
|
import warnings
|
|
6
6
|
from collections import defaultdict
|
|
7
7
|
from functools import lru_cache
|
|
8
|
+
from importlib.metadata import PackageNotFoundError
|
|
9
|
+
from importlib.metadata import version as package_version
|
|
8
10
|
from typing import (
|
|
9
11
|
TYPE_CHECKING,
|
|
10
12
|
Any,
|
|
13
|
+
Callable,
|
|
11
14
|
Collection,
|
|
12
15
|
Dict,
|
|
13
16
|
Iterable,
|
|
@@ -38,6 +41,7 @@ from sqlalchemy.ext.compiler import compiles
|
|
|
38
41
|
from sqlalchemy.sql import bindparam
|
|
39
42
|
from sqlalchemy.sql.selectable import Select
|
|
40
43
|
|
|
44
|
+
from ._bulk_insert import build_bulk_insert_data as _build_bulk_insert_data
|
|
41
45
|
from ._supports import has_comment_support
|
|
42
46
|
from ._validation import validate_extension_name
|
|
43
47
|
from .bulk import copy_from_csv, copy_from_parquet, copy_from_rows
|
|
@@ -56,8 +60,9 @@ from .motherduck import (
|
|
|
56
60
|
split_url_query,
|
|
57
61
|
stable_session_hint,
|
|
58
62
|
stable_session_name,
|
|
63
|
+
validate_motherduck_database_name,
|
|
59
64
|
)
|
|
60
|
-
from .olap import read_csv, read_csv_auto, read_parquet, table_function
|
|
65
|
+
from .olap import md_user_info, read_csv, read_csv_auto, read_parquet, table_function
|
|
61
66
|
from .url import URL, make_url
|
|
62
67
|
|
|
63
68
|
try:
|
|
@@ -69,7 +74,10 @@ else:
|
|
|
69
74
|
_pg_base, "PGExecutionContext", DefaultExecutionContext
|
|
70
75
|
)
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
try:
|
|
78
|
+
__version__ = package_version("duckdb-sqlalchemy")
|
|
79
|
+
except PackageNotFoundError: # pragma: no cover - source tree import fallback
|
|
80
|
+
__version__ = "1.5.2.2"
|
|
73
81
|
sqlalchemy_version = sqlalchemy.__version__
|
|
74
82
|
SQLALCHEMY_VERSION = Version(sqlalchemy_version)
|
|
75
83
|
SQLALCHEMY_2 = SQLALCHEMY_VERSION >= Version("2.0.0")
|
|
@@ -111,6 +119,7 @@ __all__ = [
|
|
|
111
119
|
"read_parquet",
|
|
112
120
|
"read_csv",
|
|
113
121
|
"read_csv_auto",
|
|
122
|
+
"md_user_info",
|
|
114
123
|
"copy_from_parquet",
|
|
115
124
|
"copy_from_csv",
|
|
116
125
|
"copy_from_rows",
|
|
@@ -202,6 +211,26 @@ class ConnectionWrapper:
|
|
|
202
211
|
|
|
203
212
|
_REGISTER_NAME_KEYS = ("name", "view_name", "table")
|
|
204
213
|
_REGISTER_DATA_KEYS = ("df", "dataframe", "relation", "data")
|
|
214
|
+
_IGNORED_POSTGRES_CONFIG_SETTINGS = frozenset(
|
|
215
|
+
{
|
|
216
|
+
"extra_float_digits",
|
|
217
|
+
"application_name",
|
|
218
|
+
"standard_conforming_strings",
|
|
219
|
+
"client_min_messages",
|
|
220
|
+
"datestyle",
|
|
221
|
+
"ssl_renegotiation_limit",
|
|
222
|
+
"statement_timeout",
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
_SET_POSTGRES_CONFIG_RE = re.compile(r"^set\s+(?:session\s+|local\s+)?(\w+)\b")
|
|
226
|
+
_SET_TRANSACTION_ISOLATION_RE = re.compile(
|
|
227
|
+
r"^set\s+(?:session\s+characteristics\s+as\s+)?transaction\s+isolation\s+level\s+"
|
|
228
|
+
r"(?:serializable|repeatable\s+read|read\s+committed|read\s+uncommitted)$"
|
|
229
|
+
)
|
|
230
|
+
_BEGIN_TRANSACTION_ISOLATION_RE = re.compile(
|
|
231
|
+
r"^begin\s+(?:transaction\s+)?isolation\s+level\s+"
|
|
232
|
+
r"(?:serializable|repeatable\s+read|read\s+committed|read\s+uncommitted)$"
|
|
233
|
+
)
|
|
205
234
|
|
|
206
235
|
|
|
207
236
|
def _parse_register_params(parameters: Optional[Any]) -> Tuple[str, Any]:
|
|
@@ -236,6 +265,9 @@ class CursorWrapper:
|
|
|
236
265
|
self.__c = c
|
|
237
266
|
self.__connection_wrapper = connection_wrapper
|
|
238
267
|
|
|
268
|
+
def _clear_result(self) -> None:
|
|
269
|
+
self.__c.execute("")
|
|
270
|
+
|
|
239
271
|
def executemany(
|
|
240
272
|
self,
|
|
241
273
|
statement: str,
|
|
@@ -260,11 +292,19 @@ class CursorWrapper:
|
|
|
260
292
|
norm = statement.strip().lower().rstrip(";")
|
|
261
293
|
if norm == "commit": # this is largely for ipython-sql
|
|
262
294
|
self.__c.commit()
|
|
295
|
+
elif _BEGIN_TRANSACTION_ISOLATION_RE.fullmatch(norm):
|
|
296
|
+
self.__c.begin()
|
|
297
|
+
elif _SET_TRANSACTION_ISOLATION_RE.fullmatch(norm):
|
|
298
|
+
self._clear_result()
|
|
299
|
+
elif _is_ignored_postgres_config_set(norm):
|
|
300
|
+
self._clear_result()
|
|
263
301
|
elif norm.startswith("register"):
|
|
264
302
|
view_name, df = _parse_register_params(parameters)
|
|
265
303
|
self.__c.register(view_name, df)
|
|
266
304
|
elif norm == "show transaction isolation level":
|
|
267
305
|
self.__c.execute("select 'read committed' as transaction_isolation")
|
|
306
|
+
elif norm == "show standard_conforming_strings":
|
|
307
|
+
self.__c.execute("select 'on' as standard_conforming_strings")
|
|
268
308
|
elif parameters is None:
|
|
269
309
|
self.__c.execute(statement)
|
|
270
310
|
else:
|
|
@@ -316,6 +356,13 @@ class CursorWrapper:
|
|
|
316
356
|
return self.__c.fetchmany(size)
|
|
317
357
|
|
|
318
358
|
|
|
359
|
+
def _is_ignored_postgres_config_set(statement: str) -> bool:
|
|
360
|
+
match = _SET_POSTGRES_CONFIG_RE.match(statement)
|
|
361
|
+
if match is None:
|
|
362
|
+
return False
|
|
363
|
+
return match.group(1) in _IGNORED_POSTGRES_CONFIG_SETTINGS
|
|
364
|
+
|
|
365
|
+
|
|
319
366
|
class DuckDBEngineWarning(Warning):
|
|
320
367
|
pass
|
|
321
368
|
|
|
@@ -350,49 +397,6 @@ def _normalize_execution_options(execution_options: Dict[str, Any]) -> Dict[str,
|
|
|
350
397
|
return execution_options
|
|
351
398
|
|
|
352
399
|
|
|
353
|
-
def _build_bulk_insert_dataframe(
|
|
354
|
-
rows: Sequence[Any], column_names: Sequence[str]
|
|
355
|
-
) -> Any:
|
|
356
|
-
try:
|
|
357
|
-
import pandas as pd # type: ignore[import-not-found]
|
|
358
|
-
except Exception:
|
|
359
|
-
return None
|
|
360
|
-
|
|
361
|
-
try:
|
|
362
|
-
if isinstance(rows[0], dict):
|
|
363
|
-
return pd.DataFrame.from_records(rows, columns=column_names)
|
|
364
|
-
return pd.DataFrame(rows, columns=cast(Any, column_names))
|
|
365
|
-
except Exception:
|
|
366
|
-
return None
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
def _build_bulk_insert_arrow_table(
|
|
370
|
-
rows: Sequence[Any], column_names: Sequence[str]
|
|
371
|
-
) -> Any:
|
|
372
|
-
try:
|
|
373
|
-
import pyarrow as pa # type: ignore[import-not-found]
|
|
374
|
-
except Exception:
|
|
375
|
-
return None
|
|
376
|
-
|
|
377
|
-
try:
|
|
378
|
-
if isinstance(rows[0], dict):
|
|
379
|
-
table = pa.Table.from_pylist(rows)
|
|
380
|
-
if column_names:
|
|
381
|
-
return table.select(column_names)
|
|
382
|
-
return table
|
|
383
|
-
columns = list(zip(*rows)) if rows else [[] for _ in column_names]
|
|
384
|
-
return pa.Table.from_arrays(columns, names=column_names)
|
|
385
|
-
except Exception:
|
|
386
|
-
return None
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def _build_bulk_insert_data(rows: Sequence[Any], column_names: Sequence[str]) -> Any:
|
|
390
|
-
data = _build_bulk_insert_dataframe(rows, column_names)
|
|
391
|
-
if data is not None:
|
|
392
|
-
return data
|
|
393
|
-
return _build_bulk_insert_arrow_table(rows, column_names)
|
|
394
|
-
|
|
395
|
-
|
|
396
400
|
class DuckDBArrowResult:
|
|
397
401
|
def __init__(self, result: Any) -> None:
|
|
398
402
|
self._result = result
|
|
@@ -622,7 +626,7 @@ def _default_pool_class_for_database(
|
|
|
622
626
|
|
|
623
627
|
def _apply_motherduck_defaults(config: Dict[str, Any], database: Optional[str]) -> None:
|
|
624
628
|
if "motherduck_token" not in config:
|
|
625
|
-
token = os.getenv("
|
|
629
|
+
token = os.getenv("MOTHERDUCK_TOKEN") or os.getenv("motherduck_token")
|
|
626
630
|
if token and _looks_like_motherduck(database, config):
|
|
627
631
|
config["motherduck_token"] = token
|
|
628
632
|
|
|
@@ -751,6 +755,7 @@ class Dialect(PGDialect_psycopg2):
|
|
|
751
755
|
cparams["database"] = append_query_to_database(
|
|
752
756
|
cparams.get("database"), path_query
|
|
753
757
|
)
|
|
758
|
+
validate_motherduck_database_name(cparams.get("database"))
|
|
754
759
|
_normalize_motherduck_config(config)
|
|
755
760
|
|
|
756
761
|
ext = {k: config.pop(k) for k in list(config) if k not in core_keys}
|
|
@@ -966,6 +971,35 @@ class Dialect(PGDialect_psycopg2):
|
|
|
966
971
|
sql += where_sql
|
|
967
972
|
return connection.execute(text(sql), params).first() is not None
|
|
968
973
|
|
|
974
|
+
def _get_reflection_or_empty_for_existing_table(
|
|
975
|
+
self,
|
|
976
|
+
getter: Callable[[], List[Any]],
|
|
977
|
+
connection: "Connection",
|
|
978
|
+
table_name: str,
|
|
979
|
+
schema: Optional[str],
|
|
980
|
+
) -> List[Any]:
|
|
981
|
+
try:
|
|
982
|
+
return getter()
|
|
983
|
+
except NoSuchTableError:
|
|
984
|
+
if self._duckdb_table_exists(connection, table_name, schema):
|
|
985
|
+
return []
|
|
986
|
+
raise
|
|
987
|
+
|
|
988
|
+
def _get_super_reflection_or_empty(
|
|
989
|
+
self,
|
|
990
|
+
getter: Callable[..., List[Any]],
|
|
991
|
+
connection: "Connection",
|
|
992
|
+
table_name: str,
|
|
993
|
+
schema: Optional[str],
|
|
994
|
+
**kw: Any,
|
|
995
|
+
) -> List[Any]:
|
|
996
|
+
return self._get_reflection_or_empty_for_existing_table(
|
|
997
|
+
lambda: getter(connection, table_name, schema=schema, **kw),
|
|
998
|
+
connection,
|
|
999
|
+
table_name,
|
|
1000
|
+
schema,
|
|
1001
|
+
)
|
|
1002
|
+
|
|
969
1003
|
def _duckdb_columns(
|
|
970
1004
|
self, connection: "Connection", table_name: str, schema: Optional[str]
|
|
971
1005
|
) -> Optional[List[Dict[str, Any]]]:
|
|
@@ -1352,18 +1386,14 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1352
1386
|
postgresql_ignore_search_path: bool = False,
|
|
1353
1387
|
**kw: Any,
|
|
1354
1388
|
) -> List["ReflectedForeignKeyConstraint"]:
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
except NoSuchTableError:
|
|
1364
|
-
if self._duckdb_table_exists(connection, table_name, schema):
|
|
1365
|
-
return []
|
|
1366
|
-
raise
|
|
1389
|
+
return self._get_super_reflection_or_empty(
|
|
1390
|
+
super().get_foreign_keys,
|
|
1391
|
+
connection,
|
|
1392
|
+
table_name,
|
|
1393
|
+
schema,
|
|
1394
|
+
postgresql_ignore_search_path=postgresql_ignore_search_path,
|
|
1395
|
+
**kw,
|
|
1396
|
+
)
|
|
1367
1397
|
|
|
1368
1398
|
@cache # type: ignore[call-arg]
|
|
1369
1399
|
def get_unique_constraints(
|
|
@@ -1373,14 +1403,13 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1373
1403
|
schema: Optional[str] = None,
|
|
1374
1404
|
**kw: Any,
|
|
1375
1405
|
) -> List["ReflectedUniqueConstraint"]:
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
raise
|
|
1406
|
+
return self._get_super_reflection_or_empty(
|
|
1407
|
+
super().get_unique_constraints,
|
|
1408
|
+
connection,
|
|
1409
|
+
table_name,
|
|
1410
|
+
schema,
|
|
1411
|
+
**kw,
|
|
1412
|
+
)
|
|
1384
1413
|
|
|
1385
1414
|
@cache # type: ignore[call-arg]
|
|
1386
1415
|
def get_check_constraints(
|
|
@@ -1390,14 +1419,13 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1390
1419
|
schema: Optional[str] = None,
|
|
1391
1420
|
**kw: Any,
|
|
1392
1421
|
) -> List["ReflectedCheckConstraint"]:
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
raise
|
|
1422
|
+
return self._get_super_reflection_or_empty(
|
|
1423
|
+
super().get_check_constraints,
|
|
1424
|
+
connection,
|
|
1425
|
+
table_name,
|
|
1426
|
+
schema,
|
|
1427
|
+
**kw,
|
|
1428
|
+
)
|
|
1401
1429
|
|
|
1402
1430
|
def get_indexes(
|
|
1403
1431
|
self,
|
|
@@ -1462,6 +1490,7 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1462
1490
|
database = opts.get("database")
|
|
1463
1491
|
if database in {None, ""}:
|
|
1464
1492
|
database = ":memory:"
|
|
1493
|
+
validate_motherduck_database_name(database)
|
|
1465
1494
|
opts["database"] = append_query_to_database(database, path_query)
|
|
1466
1495
|
return (), opts
|
|
1467
1496
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Sequence, cast
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _rows_use_mapping_shape(rows: Sequence[Any]) -> bool:
|
|
7
|
+
return bool(rows) and isinstance(rows[0], dict)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_bulk_insert_dataframe(
|
|
11
|
+
rows: Sequence[Any], column_names: Sequence[str]
|
|
12
|
+
) -> Any:
|
|
13
|
+
try:
|
|
14
|
+
import pandas as pd # type: ignore[import-not-found]
|
|
15
|
+
except Exception:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
if _rows_use_mapping_shape(rows):
|
|
20
|
+
return pd.DataFrame.from_records(rows, columns=column_names)
|
|
21
|
+
return pd.DataFrame(rows, columns=cast(Any, column_names))
|
|
22
|
+
except Exception:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_bulk_insert_arrow_table(
|
|
27
|
+
rows: Sequence[Any], column_names: Sequence[str]
|
|
28
|
+
) -> Any:
|
|
29
|
+
try:
|
|
30
|
+
import pyarrow as pa # type: ignore[import-not-found]
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
if _rows_use_mapping_shape(rows):
|
|
36
|
+
table = pa.Table.from_pylist(rows)
|
|
37
|
+
if column_names:
|
|
38
|
+
return table.select(column_names)
|
|
39
|
+
return table
|
|
40
|
+
columns = list(zip(*rows)) if rows else [[] for _ in column_names]
|
|
41
|
+
return pa.Table.from_arrays(columns, names=column_names)
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_bulk_insert_data(rows: Sequence[Any], column_names: Sequence[str]) -> Any:
|
|
47
|
+
data = build_bulk_insert_dataframe(rows, column_names)
|
|
48
|
+
if data is not None:
|
|
49
|
+
return data
|
|
50
|
+
return build_bulk_insert_arrow_table(rows, column_names)
|
|
@@ -5,31 +5,35 @@ IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
|
5
5
|
EXTENSION_RE = re.compile(r"^[A-Za-z0-9_]+$")
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def
|
|
8
|
+
def _require_string(value: str, *, kind: str) -> str:
|
|
9
9
|
if not isinstance(value, str):
|
|
10
10
|
raise ValueError(f"{kind} must be a string")
|
|
11
|
-
if not IDENTIFIER_RE.fullmatch(value):
|
|
12
|
-
raise ValueError(f"invalid {kind}: {value!r}")
|
|
13
11
|
return value
|
|
14
12
|
|
|
15
13
|
|
|
14
|
+
def _validate_pattern(value: str, *, kind: str, pattern: re.Pattern[str]) -> str:
|
|
15
|
+
validated = _require_string(value, kind=kind)
|
|
16
|
+
if not pattern.fullmatch(validated):
|
|
17
|
+
raise ValueError(f"invalid {kind}: {validated!r}")
|
|
18
|
+
return validated
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_identifier(value: str, *, kind: str = "identifier") -> str:
|
|
22
|
+
return _validate_pattern(value, kind=kind, pattern=IDENTIFIER_RE)
|
|
23
|
+
|
|
24
|
+
|
|
16
25
|
def validate_dotted_identifier(value: str, *, kind: str = "identifier") -> str:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
parts = value.split(".")
|
|
26
|
+
validated = _require_string(value, kind=kind)
|
|
27
|
+
parts = validated.split(".")
|
|
20
28
|
if not parts or any(not part for part in parts):
|
|
21
|
-
raise ValueError(f"invalid {kind}: {
|
|
29
|
+
raise ValueError(f"invalid {kind}: {validated!r}")
|
|
22
30
|
for part in parts:
|
|
23
31
|
validate_identifier(part, kind=kind)
|
|
24
|
-
return
|
|
32
|
+
return validated
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
def validate_extension_name(value: str) -> str:
|
|
28
|
-
|
|
29
|
-
raise ValueError("extension name must be a string")
|
|
30
|
-
if not EXTENSION_RE.fullmatch(value):
|
|
31
|
-
raise ValueError(f"invalid extension name: {value!r}")
|
|
32
|
-
return value
|
|
36
|
+
return _validate_pattern(value, kind="extension name", pattern=EXTENSION_RE)
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
def validate_identifier_list(
|
|
@@ -155,6 +155,27 @@ def _copy_rows_as_csv_chunks(
|
|
|
155
155
|
_close_and_unlink_tempfile(tmp)
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def _copy_rows_as_sequences(
|
|
159
|
+
first: Union[Mapping[str, Any], Sequence[Any]],
|
|
160
|
+
rows: Iterable[Union[Mapping[str, Any], Sequence[Any]]],
|
|
161
|
+
columns: Optional[Sequence[str]],
|
|
162
|
+
) -> Tuple[Iterable[Sequence[Any]], Optional[Sequence[str]]]:
|
|
163
|
+
if isinstance(first, Mapping):
|
|
164
|
+
if columns is None:
|
|
165
|
+
columns = [str(col) for col in cast(Mapping[str, Any], first).keys()]
|
|
166
|
+
|
|
167
|
+
def row_to_seq(row: Mapping[str, Any]) -> Sequence[Any]:
|
|
168
|
+
return [row.get(col) for col in columns or []]
|
|
169
|
+
|
|
170
|
+
first_row = row_to_seq(cast(Mapping[str, Any], first))
|
|
171
|
+
remaining_rows = (row_to_seq(cast(Mapping[str, Any], row)) for row in rows)
|
|
172
|
+
return chain((first_row,), remaining_rows), columns
|
|
173
|
+
|
|
174
|
+
first_row = cast(Sequence[Any], first)
|
|
175
|
+
remaining_rows = (cast(Sequence[Any], row) for row in rows)
|
|
176
|
+
return chain((first_row,), remaining_rows), columns
|
|
177
|
+
|
|
178
|
+
|
|
158
179
|
def copy_from_parquet(
|
|
159
180
|
connection: Any,
|
|
160
181
|
table: TableLike,
|
|
@@ -227,20 +248,7 @@ def copy_from_rows(
|
|
|
227
248
|
|
|
228
249
|
copy_options = {"header": include_header, **copy_options}
|
|
229
250
|
|
|
230
|
-
|
|
231
|
-
if columns is None:
|
|
232
|
-
columns = [str(col) for col in cast(Mapping[str, Any], first).keys()]
|
|
233
|
-
|
|
234
|
-
def row_to_seq(row: Mapping[str, Any]) -> Sequence[Any]:
|
|
235
|
-
return [row.get(col) for col in columns or []]
|
|
236
|
-
|
|
237
|
-
first_row = row_to_seq(cast(Mapping[str, Any], first))
|
|
238
|
-
remaining_rows = (row_to_seq(cast(Mapping[str, Any], row)) for row in iterator)
|
|
239
|
-
else:
|
|
240
|
-
first_row = cast(Sequence[Any], first)
|
|
241
|
-
remaining_rows = (cast(Sequence[Any], row) for row in iterator)
|
|
242
|
-
|
|
243
|
-
chunked_rows = chain((first_row,), remaining_rows)
|
|
251
|
+
chunked_rows, columns = _copy_rows_as_sequences(first, iterator, columns)
|
|
244
252
|
_copy_rows_as_csv_chunks(
|
|
245
253
|
connection,
|
|
246
254
|
table,
|