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.
Files changed (83) hide show
  1. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/workflows/pythonapp.yaml +1 -0
  2. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.pre-commit-config.yaml +2 -2
  3. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/CHANGELOG.md +20 -0
  4. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/PKG-INFO +6 -5
  5. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/README.md +2 -2
  6. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/configuration.md +2 -1
  7. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/motherduck.md +1 -0
  8. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/olap.md +22 -13
  9. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/__init__.py +138 -68
  10. duckdb_sqlalchemy-1.5.4/duckdb_sqlalchemy/_arrow.py +39 -0
  11. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/datatypes.py +27 -20
  12. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/motherduck.py +12 -4
  13. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/olap.py +321 -24
  14. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_core_units.py +301 -28
  15. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_arrow_reads.py +2 -1
  16. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/llms.txt +1 -1
  17. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/noxfile.py +5 -2
  18. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/pyproject.toml +4 -3
  19. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/uv.lock +289 -287
  20. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.codex/environments/environment.toml +0 -0
  21. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  22. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  23. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/dependabot.yml +0 -0
  24. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/workflows/lint.yml +0 -0
  25. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/workflows/pages.yml +0 -0
  26. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.github/workflows/publish.yaml +0 -0
  27. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/.gitignore +0 -0
  28. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/AGENTS.md +0 -0
  29. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/ARCHITECTURE.md +0 -0
  30. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/CLAUDE.md +0 -0
  31. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/CODE_OF_CONDUCT.md +0 -0
  32. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/LICENSE.txt +0 -0
  33. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/PLANS.md +0 -0
  34. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/ROADMAP.md +0 -0
  35. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/codecov.yml +0 -0
  36. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/README.md +0 -0
  37. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/_config.yml +0 -0
  38. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/alembic.md +0 -0
  39. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/connection-urls.md +0 -0
  40. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/exec-plans/active/2026-03-27-maintenance-review.md +0 -0
  41. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/exec-plans/active/2026-03-30-maintenance-review.md +0 -0
  42. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/exec-plans/active/2026-03-31-maintenance-review.md +0 -0
  43. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/getting-started.md +0 -0
  44. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/index.md +0 -0
  45. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/migration-from-duckdb-engine.md +0 -0
  46. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/overview.md +0 -0
  47. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/pandas-jupyter.md +0 -0
  48. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/robots.txt +0 -0
  49. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/seo-checklist.md +0 -0
  50. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/docs/types-and-caveats.md +0 -0
  51. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_bulk_insert.py +0 -0
  52. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_query.py +0 -0
  53. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_statements.py +0 -0
  54. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_supports.py +0 -0
  55. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/_validation.py +0 -0
  56. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/bulk.py +0 -0
  57. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/capabilities.py +0 -0
  58. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/config.py +0 -0
  59. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/conftest.py +0 -0
  60. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/py.typed +0 -0
  61. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/requirements.py +0 -0
  62. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/__init__.py +0 -0
  63. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/conftest.py +0 -0
  64. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/snapshots/test_datatypes/test_interval/schema.sql +0 -0
  65. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/sqlalchemy_suite/conftest.py +0 -0
  66. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/sqlalchemy_suite/test_suite.py +0 -0
  67. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_basic.py +0 -0
  68. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_datatypes.py +0 -0
  69. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_execution_options.py +0 -0
  70. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_helpers.py +0 -0
  71. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_integration.py +0 -0
  72. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_pandas.py +0 -0
  73. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_pyarrow.py +0 -0
  74. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/test_validation.py +0 -0
  75. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/tests/util.py +0 -0
  76. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/duckdb_sqlalchemy/url.py +0 -0
  77. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_attach_modes.py +0 -0
  78. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_multi_instance_pool.py +0 -0
  79. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_queuepool_high_concurrency.py +0 -0
  80. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/motherduck_read_scaling_per_user.py +0 -0
  81. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/examples/sqlalchemy_example.py +0 -0
  82. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/renovate.json +0 -0
  83. {duckdb_sqlalchemy-1.5.3 → duckdb_sqlalchemy-1.5.4}/test.cfg +0 -0
@@ -66,4 +66,5 @@ jobs:
66
66
  token: ${{ secrets.CODECOV_TOKEN }}
67
67
  env_vars: PYTHON
68
68
  fail_ci_if_error: true
69
+ use_pypi: true
69
70
  verbose: true
@@ -23,10 +23,10 @@ repos:
23
23
  language: python
24
24
  files: ^duckdb_sqlalchemy/(?!tests/).*\.py$
25
25
  additional_dependencies:
26
- - "ty==0.0.38"
26
+ - "ty==0.0.54"
27
27
  - repo: https://github.com/astral-sh/ruff-pre-commit
28
28
  # Ruff version.
29
- rev: 'v0.15.11'
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
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.45
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.38; extra == 'dev'
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.45, 2.0.50) |
110
- | DuckDB | 0.5.0+ (CI-tested currently: 1.3.0 to 1.5.3) |
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.45, 2.0.50) |
55
- | DuckDB | 0.5.0+ (CI-tested currently: 1.3.0 to 1.5.3) |
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 specially for `md:` URLs. See
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 jobs
147
+ ## MotherDuck Flights
147
148
 
148
- MotherDuck also exposes preview table functions for job metadata. The read-only
149
- helpers are useful for listing jobs, runs, logs, and versions:
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 md_job_runs, md_job_versions, md_jobs
154
+ from duckdb_sqlalchemy import md_flight_runs, md_flight_versions, md_flights
154
155
 
155
- jobs = md_jobs(limit=10)
156
- jobs_stmt = select(jobs.c.job_id, jobs.c.job_name, jobs.c.status)
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 = md_job_runs(job_id="00000000-0000-0000-0000-000000000000", limit=10)
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 = md_job_versions(job_id="00000000-0000-0000-0000-000000000000")
162
- versions_stmt = select(versions.c.version, versions.c.requirements_txt)
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 job functions are available as helpers too:
166
- `md_create_job`, `md_update_job`, `md_delete_job`, `md_run_job`, and
167
- `md_cancel_job_run`. They only execute when the SQLAlchemy statement is run.
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.3"
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
- config = dict(cparams.get("config", {}))
729
- cparams["config"] = 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._reflect_type( # type: ignore[attr-defined]
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
- validate_motherduck_database_name(database)
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(context, "identifier_preparer", self.identifier_preparer)
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(cursor, statement, parameters, context, executor)
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(cursor, statement, None, context, executor)
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 PGIdentifierPreparer, PGTypeCompiler
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, identifier_preparer, **kw)
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, identifier_preparer, **kw)
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: PGIdentifierPreparer,
344
+ identifier_preparer: Optional[IdentifierPreparer] = None,
346
345
  **kw: Any,
347
346
  ) -> str:
348
- fields = getattr(instance, "_fields", None)
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
- return "({})".format(
354
- ", ".join(
355
- "{} {}".format(
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(