duckdb-sqlalchemy 0.19.2__py3-none-any.whl → 1.4.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.2"
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")
@@ -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.2
4
- Summary: SQLAlchemy driver for duckdb
3
+ Version: 1.4.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,7 +56,7 @@ 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
- A production-ready SQLAlchemy dialect for DuckDB and MotherDuck. It supports SQLAlchemy Core and ORM APIs against DuckDB locally or in 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
61
  The dialect handles pooling defaults, bulk inserts, type mappings, and cloud-specific configuration.
47
62
 
@@ -127,7 +142,7 @@ 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
 
@@ -139,7 +154,11 @@ 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
@@ -1,4 +1,4 @@
1
- duckdb_sqlalchemy/__init__.py,sha256=ozIZSz_zrMeg7r742If64RY_f946gF_IW_U2n0oUmdc,49992
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.2.dist-info/METADATA,sha256=SjwhxoPg7_bVEd-sBcg22_noGTK_35_Y_9q_hipeMsI,6533
28
- duckdb_sqlalchemy-0.19.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
29
- duckdb_sqlalchemy-0.19.2.dist-info/entry_points.txt,sha256=MyXbmaqEhyBLIL2NnHrweY6EJ_Rke2HnVZR1wCz08cM,57
30
- duckdb_sqlalchemy-0.19.2.dist-info/licenses/LICENSE.txt,sha256=nhRQcy_ZV2R-xzl3MPltQuQ53bcURavT0N6mC3VdDE8,1076
31
- duckdb_sqlalchemy-0.19.2.dist-info/RECORD,,
27
+ duckdb_sqlalchemy-1.4.3.dist-info/METADATA,sha256=ByDFR-JMvrys8xd4jW7Hk_cVaz_YvFAQSOyo-2Bqw4E,7679
28
+ duckdb_sqlalchemy-1.4.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
29
+ duckdb_sqlalchemy-1.4.3.dist-info/entry_points.txt,sha256=MyXbmaqEhyBLIL2NnHrweY6EJ_Rke2HnVZR1wCz08cM,57
30
+ duckdb_sqlalchemy-1.4.3.dist-info/licenses/LICENSE.txt,sha256=nhRQcy_ZV2R-xzl3MPltQuQ53bcURavT0N6mC3VdDE8,1076
31
+ duckdb_sqlalchemy-1.4.3.dist-info/RECORD,,