duckdb-sqlalchemy 0.19.1__py3-none-any.whl → 0.19.3__py3-none-any.whl

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.
@@ -60,7 +60,7 @@ try:
60
60
  except ImportError: # pragma: no cover - fallback for older SQLAlchemy
61
61
  PGExecutionContext = DefaultExecutionContext
62
62
 
63
- __version__ = "0.19.1"
63
+ __version__ = "0.19.3"
64
64
  sqlalchemy_version = sqlalchemy.__version__
65
65
  SQLALCHEMY_VERSION = Version(sqlalchemy_version)
66
66
  SQLALCHEMY_2 = SQLALCHEMY_VERSION >= Version("2.0.0")
@@ -583,6 +583,8 @@ class Dialect(PGDialect_psycopg2):
583
583
  config.update(cparams.pop("url_config", {}))
584
584
  for key in DIALECT_QUERY_KEYS:
585
585
  config.pop(key, None)
586
+ if cparams.get("database") in {None, ""}:
587
+ cparams["database"] = ":memory:"
586
588
  _apply_motherduck_defaults(config, cparams.get("database"))
587
589
  path_query = extract_path_query_from_config(config)
588
590
  if path_query:
@@ -626,6 +628,8 @@ class Dialect(PGDialect_psycopg2):
626
628
  return pool.SingletonThreadPool
627
629
  if pool_override in {"null", "nullpool"}:
628
630
  return pool.NullPool
631
+ if not url.database:
632
+ return pool.SingletonThreadPool
629
633
  if url.database and url.database.startswith(":memory:"):
630
634
  return pool.SingletonThreadPool
631
635
  if _looks_like_motherduck(url.database, dict(url.query)):
@@ -5,6 +5,7 @@ import duckdb
5
5
  import pytest
6
6
  from sqlalchemy import Integer, String, pool
7
7
  from sqlalchemy import exc as sa_exc
8
+ from sqlalchemy.engine import URL as SAURL
8
9
 
9
10
  from duckdb_sqlalchemy import (
10
11
  URL,
@@ -13,14 +14,20 @@ from duckdb_sqlalchemy import (
13
14
  Dialect,
14
15
  MotherDuckURL,
15
16
  _apply_motherduck_defaults,
17
+ _is_idempotent_statement,
18
+ _is_transient_error,
16
19
  _looks_like_motherduck,
20
+ _normalize_execution_options,
17
21
  _normalize_motherduck_config,
22
+ _parse_register_params,
23
+ _pool_override_from_url,
18
24
  _supports,
19
25
  create_engine_from_paths,
20
26
  olap,
21
27
  stable_session_hint,
22
28
  )
23
29
  from duckdb_sqlalchemy import datatypes as dt
30
+ from duckdb_sqlalchemy import motherduck as md
24
31
  from duckdb_sqlalchemy.config import TYPES, apply_config, get_core_config
25
32
 
26
33
 
@@ -472,3 +479,141 @@ def test_struct_or_union_requires_fields() -> None:
472
479
  assert rendered.startswith("(")
473
480
  assert rendered.endswith(")")
474
481
  assert '"first name"' in rendered
482
+
483
+
484
+ def test_parse_register_params_dict_and_tuple() -> None:
485
+ view_name, df = _parse_register_params({"view_name": "v", "df": "data"})
486
+ assert view_name == "v"
487
+ assert df == "data"
488
+
489
+ view_name, df = _parse_register_params(("v2", "data2"))
490
+ assert view_name == "v2"
491
+ assert df == "data2"
492
+
493
+
494
+ def test_parse_register_params_errors() -> None:
495
+ with pytest.raises(ValueError):
496
+ _parse_register_params(None)
497
+
498
+ with pytest.raises(ValueError):
499
+ _parse_register_params({"name": "v"})
500
+
501
+ with pytest.raises(ValueError):
502
+ _parse_register_params(("only-one",))
503
+
504
+
505
+ def test_normalize_execution_options_insertmanyvalues() -> None:
506
+ original = {"duckdb_insertmanyvalues_page_size": 123}
507
+ normalized = _normalize_execution_options(original)
508
+ assert normalized["insertmanyvalues_page_size"] == 123
509
+ assert "insertmanyvalues_page_size" not in original
510
+
511
+ already = {
512
+ "duckdb_insertmanyvalues_page_size": 5,
513
+ "insertmanyvalues_page_size": 10,
514
+ }
515
+ normalized = _normalize_execution_options(already)
516
+ assert normalized["insertmanyvalues_page_size"] == 10
517
+
518
+
519
+ def test_idempotent_statement_detection() -> None:
520
+ assert _is_idempotent_statement("SELECT 1")
521
+ assert _is_idempotent_statement(" show tables")
522
+ assert _is_idempotent_statement("pragma version")
523
+ assert not _is_idempotent_statement("insert into t values (1)")
524
+
525
+
526
+ def test_transient_error_detection() -> None:
527
+ assert _is_transient_error(RuntimeError("HTTP Error: 503 Service Unavailable"))
528
+ assert not _is_transient_error(RuntimeError("connection reset by peer"))
529
+ assert not _is_transient_error(RuntimeError("HTTP Error: 503 connection reset"))
530
+
531
+
532
+ def test_pool_override_from_url_and_env(monkeypatch: pytest.MonkeyPatch) -> None:
533
+ url = URL(database=":memory:", query={"duckdb_sqlalchemy_pool": ["Queue"]})
534
+ assert _pool_override_from_url(url) == "queue"
535
+
536
+ monkeypatch.setenv("DUCKDB_SQLALCHEMY_POOL", "Null")
537
+ url = URL(database=":memory:")
538
+ assert _pool_override_from_url(url) == "null"
539
+
540
+
541
+ def test_pool_class_for_empty_database() -> None:
542
+ url = SAURL.create("duckdb")
543
+ assert Dialect.get_pool_class(url) is pool.SingletonThreadPool
544
+
545
+
546
+ def test_apply_config_handles_none_path_decimal() -> None:
547
+ import decimal
548
+ from pathlib import Path
549
+
550
+ dialect = Dialect()
551
+
552
+ class DummyConn:
553
+ def __init__(self) -> None:
554
+ self.executed = []
555
+
556
+ def execute(self, statement: str) -> None:
557
+ self.executed.append(statement)
558
+
559
+ conn = DummyConn()
560
+ ext = {
561
+ "memory_limit": None,
562
+ "data_path": Path("/tmp/data"),
563
+ "ratio": decimal.Decimal("1.5"),
564
+ }
565
+
566
+ apply_config(
567
+ dialect,
568
+ conn,
569
+ cast(dict[str, str | int | bool | float | None], ext),
570
+ )
571
+
572
+ string_processor = String().literal_processor(dialect=dialect)
573
+ expected = [
574
+ "SET memory_limit = NULL",
575
+ f"SET data_path = {string_processor(str(Path('/tmp/data')))}",
576
+ f"SET ratio = {string_processor(str(decimal.Decimal('1.5')))}",
577
+ ]
578
+ assert conn.executed == expected
579
+
580
+
581
+ def test_motherduck_helpers() -> None:
582
+ url = md.MotherDuckURL(
583
+ database="md:db",
584
+ query={"memory_limit": "1GB"},
585
+ path_query={"user": "alice", "session_hint": "team"},
586
+ )
587
+ assert url.database.startswith("md:db?")
588
+ assert url.query == {"memory_limit": "1GB"}
589
+
590
+ appended = md.append_query_to_database("md:db?user=alice", {"session_hint": "s"})
591
+ assert appended == "md:db?user=alice&session_hint=s"
592
+
593
+ normalized = md._normalize_path_item("duckdb:///tmp.db")
594
+ assert normalized.drivername == "duckdb"
595
+
596
+ normalized = md._normalize_path_item("md:db")
597
+ assert normalized.database == "md:db"
598
+
599
+
600
+ def test_merge_and_copy_connect_args() -> None:
601
+ base = {"config": {"threads": 2}, "url_config": {"memory_limit": "1GB"}}
602
+ extra = {"config": {"threads": 4}, "url_config": {"s3_region": "us-east-1"}}
603
+ merged = md._merge_connect_args(base, extra)
604
+
605
+ assert merged["config"] == {"threads": 4}
606
+ assert merged["url_config"] == {"memory_limit": "1GB", "s3_region": "us-east-1"}
607
+
608
+ copied = md._copy_connect_params(merged)
609
+ copied["config"]["threads"] = 1
610
+ copied["url_config"]["memory_limit"] = "2GB"
611
+ assert merged["config"]["threads"] == 4
612
+ assert merged["url_config"]["memory_limit"] == "1GB"
613
+
614
+
615
+ def test_create_engine_from_paths_driver_mismatch() -> None:
616
+ url1 = SAURL.create("duckdb", database=":memory:")
617
+ url2 = SAURL.create("sqlite", database=":memory:")
618
+ with pytest.raises(ValueError):
619
+ create_engine_from_paths([url1, url2])
@@ -1,14 +1,29 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duckdb-sqlalchemy
3
- Version: 0.19.1
4
- Summary: SQLAlchemy driver for duckdb
3
+ Version: 0.19.3
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
7
+ Project-URL: Documentation, https://leonardovida.github.io/duckdb-sqlalchemy/
7
8
  Project-URL: repository, https://github.com/leonardovida/duckdb-sqlalchemy
8
9
  Project-URL: Upstream, https://github.com/Mause/duckdb_engine
9
- Author-email: Leonardo Vida <leonardo@motherduck.com>
10
+ Author-email: Leonardo Vida <lleonardovida@gmail.com>
10
11
  License-Expression: MIT
11
12
  License-File: LICENSE.txt
13
+ Keywords: analytics,database,dialect,duckdb,motherduck,olap,sqlalchemy
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Database
26
+ Classifier: Topic :: Database :: Front-Ends
12
27
  Requires-Python: <4,>=3.9
13
28
  Requires-Dist: duckdb>=0.5.0
14
29
  Requires-Dist: packaging>=21
@@ -41,16 +56,16 @@ Description-Content-Type: text/markdown
41
56
  [![PyPI Downloads](https://img.shields.io/pypi/dm/duckdb-sqlalchemy.svg)](https://pypi.org/project/duckdb-sqlalchemy/)
42
57
  [![codecov](https://codecov.io/gh/leonardovida/duckdb-sqlalchemy/graph/badge.svg)](https://codecov.io/gh/leonardovida/duckdb-sqlalchemy)
43
58
 
44
- The production-grade SQLAlchemy dialect for DuckDB and MotherDuck. Use the full SQLAlchemy Core and ORM APIs with DuckDB's analytical engine, locally or in the cloud via MotherDuck.
59
+ duckdb-sqlalchemy is a DuckDB SQLAlchemy dialect for DuckDB and MotherDuck. It supports SQLAlchemy Core and ORM APIs for local DuckDB and MotherDuck connections.
45
60
 
46
- This dialect handles connection pooling, bulk inserts, type mappings, and cloud-specific configuration so you can focus on queries instead of driver quirks.
61
+ The dialect handles pooling defaults, bulk inserts, type mappings, and cloud-specific configuration.
47
62
 
48
63
  ## Why this dialect
49
64
 
50
- - **Full SQLAlchemy compatibility**: Core, ORM, Alembic migrations, and reflection work out of the box.
51
- - **MotherDuck support**: Automatic token handling, attach modes, session hints, and read scaling helpers.
52
- - **Production defaults**: Sensible pooling, transient retry for reads, and bulk insert optimization via Arrow/DataFrame registration.
53
- - **Actively maintained**: Tracks current DuckDB releases with long-term support commitment.
65
+ - **SQLAlchemy compatibility**: Core, ORM, Alembic, and reflection.
66
+ - **MotherDuck support**: Token handling, attach modes, session hints, and read scaling helpers.
67
+ - **Operational defaults**: Pooling defaults, transient retry for reads, and bulk insert optimization via Arrow/DataFrame registration.
68
+ - **Maintained**: Tracks current DuckDB releases with a long-term support posture.
54
69
 
55
70
  ## Compatibility
56
71
 
@@ -127,19 +142,23 @@ Use the URL helpers to build connection strings safely:
127
142
  ```python
128
143
  from duckdb_sqlalchemy import URL, MotherDuckURL
129
144
 
130
- local_url = URL(database=":memory:", read_only=False)
145
+ local_url = URL(database=":memory:", memory_limit="1GB")
131
146
  md_url = MotherDuckURL(database="md:my_db", attach_mode="single")
132
147
  ```
133
148
 
134
149
  ## Configuration and pooling
135
150
 
136
- This dialect ships with sensible defaults (NullPool for file/MotherDuck connections, SingletonThreadPool for `:memory:`) and lets you override pooling explicitly. For production services, use the MotherDuck performance helper or configure `QueuePool`, `pool_pre_ping`, and `pool_recycle`.
151
+ This dialect defaults to `NullPool` for file/MotherDuck connections and `SingletonThreadPool` for `:memory:`. You can override pooling explicitly. For long-lived MotherDuck pools, use the performance helper or configure `QueuePool`, `pool_pre_ping`, and `pool_recycle`.
137
152
 
138
153
  See `docs/configuration.md` and `docs/motherduck.md` for detailed guidance.
139
154
 
140
155
  ## Documentation
141
156
 
157
+ - `docs/index.md` - GitHub Pages entrypoint
142
158
  - `docs/README.md` - Docs index
159
+ - `docs/overview.md` - Overview and quick start
160
+ - `docs/getting-started.md` - Minimal install + setup walkthrough
161
+ - `docs/migration-from-duckdb-engine.md` - Migration guide from older dialects
143
162
  - `docs/connection-urls.md` - URL formats and helpers
144
163
  - `docs/motherduck.md` - MotherDuck setup and options
145
164
  - `docs/configuration.md` - Connection configuration, extensions, filesystems
@@ -148,6 +167,12 @@ See `docs/configuration.md` and `docs/motherduck.md` for detailed guidance.
148
167
  - `docs/types-and-caveats.md` - Type support and known caveats
149
168
  - `docs/alembic.md` - Alembic integration
150
169
 
170
+ Docs site (GitHub Pages):
171
+
172
+ ```
173
+ https://leonardovida.github.io/duckdb-sqlalchemy/
174
+ ```
175
+
151
176
  ## Examples
152
177
 
153
178
  - `examples/sqlalchemy_example.py` - end-to-end example
@@ -159,10 +184,10 @@ See `docs/configuration.md` and `docs/motherduck.md` for detailed guidance.
159
184
 
160
185
  ## Release and support policy
161
186
 
162
- - Long-term maintenance: this project is intended to remain supported indefinitely.
163
- - Compatibility: we track current DuckDB and SQLAlchemy releases while preserving SQLAlchemy semantics.
187
+ - Long-term maintenance: intended to remain supported.
188
+ - Compatibility: track current DuckDB and SQLAlchemy releases while preserving SQLAlchemy semantics.
164
189
  - Breaking changes: only in major/minor releases with explicit notes in `CHANGELOG.md`.
165
- - Security: please open an issue with details; we will prioritize fixes.
190
+ - Security: open an issue with details; fixes are prioritized.
166
191
 
167
192
  ## Changelog and roadmap
168
193
 
@@ -1,4 +1,4 @@
1
- duckdb_sqlalchemy/__init__.py,sha256=78p0lVyEZVA_Zl9xnSmJx-00qRPX2G867Grqcs8hc24,49824
1
+ duckdb_sqlalchemy/__init__.py,sha256=jQTzFo4TvnZJOS2-dU60hQsfK68iBSkQuf00_hkEnI8,49992
2
2
  duckdb_sqlalchemy/_supports.py,sha256=GCOH9nFB4MitnjYKx5V4BsDSCxIfTyXqm6W-BDkgbfE,598
3
3
  duckdb_sqlalchemy/bulk.py,sha256=lc6T258-BYQ8fp8xwNVePIrJorjFhg_1FGiEROHKqb8,5209
4
4
  duckdb_sqlalchemy/capabilities.py,sha256=Y9l-FaVPMw9CTpsG-42tiqltXECFXqIeTQdXPfSuxPY,719
@@ -13,7 +13,7 @@ duckdb_sqlalchemy/url.py,sha256=y2rtgiHXXcgVpQt22eEIeP8MAeVKwBNC9tsh1i3j3OE,1539
13
13
  duckdb_sqlalchemy/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  duckdb_sqlalchemy/tests/conftest.py,sha256=GldGGf9wrY1hZvcl4hmzKmdirQYCltpZWfM3-WyOKqc,1498
15
15
  duckdb_sqlalchemy/tests/test_basic.py,sha256=CGTHkQfXIm07fxXnSb6jDXI8s9B0E_lCN6q5loDkgrI,21985
16
- duckdb_sqlalchemy/tests/test_core_units.py,sha256=gqMf4FKyMqNflQlvhoxAX-_WPUKqaJ72jDlnHhJEuOE,13740
16
+ duckdb_sqlalchemy/tests/test_core_units.py,sha256=rcG48lCNCB-qMXxDoUZSWQ5RjDpW8QvoMj9v_74-88A,18584
17
17
  duckdb_sqlalchemy/tests/test_datatypes.py,sha256=g7WwxP6Kq6rhhWdpFUs1g6NA0jNYuaJMiolsRpG0qI8,7144
18
18
  duckdb_sqlalchemy/tests/test_execution_options.py,sha256=ov0YVVQLdKdw1K8grdzkpIQMD863dsyB0SG4rkevGks,964
19
19
  duckdb_sqlalchemy/tests/test_helpers.py,sha256=9KGRmNVvTVPvcEN3mHCwXIIKhdFe_GXmyBnfWisYiCs,2216
@@ -24,8 +24,8 @@ duckdb_sqlalchemy/tests/util.py,sha256=YHTtB19mxQO--nq1tCnQELcKL_Qh73T9mvdnH6rVl
24
24
  duckdb_sqlalchemy/tests/snapshots/test_datatypes/test_interval/schema.sql,sha256=ZXscZo4xepli7WSjbhWqTufIciscCDLoRznaA6KGiOI,47
25
25
  duckdb_sqlalchemy/tests/sqlalchemy_suite/conftest.py,sha256=BVvwaWDIXobKa-ziFyhmjkIkCd5vz0TbT77AFOPCHHc,263
26
26
  duckdb_sqlalchemy/tests/sqlalchemy_suite/test_suite.py,sha256=O2O52uLfENDAU_xl2_iZZgigLP1DB8IYaULqCEOnIA8,58
27
- duckdb_sqlalchemy-0.19.1.dist-info/METADATA,sha256=U4WaQ-EvMa7ua2iRLIXD8bI1ZanrrfvQWosyocSYpUs,6740
28
- duckdb_sqlalchemy-0.19.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
29
- duckdb_sqlalchemy-0.19.1.dist-info/entry_points.txt,sha256=MyXbmaqEhyBLIL2NnHrweY6EJ_Rke2HnVZR1wCz08cM,57
30
- duckdb_sqlalchemy-0.19.1.dist-info/licenses/LICENSE.txt,sha256=nhRQcy_ZV2R-xzl3MPltQuQ53bcURavT0N6mC3VdDE8,1076
31
- duckdb_sqlalchemy-0.19.1.dist-info/RECORD,,
27
+ duckdb_sqlalchemy-0.19.3.dist-info/METADATA,sha256=SSjKpjGqQV1FqjqEOSXlH9UMQj1-B4FQH-88KwlLJ48,7680
28
+ duckdb_sqlalchemy-0.19.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
29
+ duckdb_sqlalchemy-0.19.3.dist-info/entry_points.txt,sha256=MyXbmaqEhyBLIL2NnHrweY6EJ_Rke2HnVZR1wCz08cM,57
30
+ duckdb_sqlalchemy-0.19.3.dist-info/licenses/LICENSE.txt,sha256=nhRQcy_ZV2R-xzl3MPltQuQ53bcURavT0N6mC3VdDE8,1076
31
+ duckdb_sqlalchemy-0.19.3.dist-info/RECORD,,