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.
- duckdb_sqlalchemy/__init__.py +1 -1
- duckdb_sqlalchemy/tests/test_core_units.py +145 -0
- {duckdb_sqlalchemy-0.19.2.dist-info → duckdb_sqlalchemy-1.4.3.dist-info}/METADATA +30 -5
- {duckdb_sqlalchemy-0.19.2.dist-info → duckdb_sqlalchemy-1.4.3.dist-info}/RECORD +7 -7
- {duckdb_sqlalchemy-0.19.2.dist-info → duckdb_sqlalchemy-1.4.3.dist-info}/WHEEL +0 -0
- {duckdb_sqlalchemy-0.19.2.dist-info → duckdb_sqlalchemy-1.4.3.dist-info}/entry_points.txt +0 -0
- {duckdb_sqlalchemy-0.19.2.dist-info → duckdb_sqlalchemy-1.4.3.dist-info}/licenses/LICENSE.txt +0 -0
duckdb_sqlalchemy/__init__.py
CHANGED
|
@@ -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.
|
|
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:
|
|
4
|
-
Summary: SQLAlchemy
|
|
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 <
|
|
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
|
[](https://pypi.org/project/duckdb-sqlalchemy/)
|
|
42
57
|
[](https://codecov.io/gh/leonardovida/duckdb-sqlalchemy)
|
|
43
58
|
|
|
44
|
-
|
|
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:",
|
|
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=
|
|
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=
|
|
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-
|
|
28
|
-
duckdb_sqlalchemy-
|
|
29
|
-
duckdb_sqlalchemy-
|
|
30
|
-
duckdb_sqlalchemy-
|
|
31
|
-
duckdb_sqlalchemy-
|
|
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,,
|
|
File without changes
|
|
File without changes
|
{duckdb_sqlalchemy-0.19.2.dist-info → duckdb_sqlalchemy-1.4.3.dist-info}/licenses/LICENSE.txt
RENAMED
|
File without changes
|