duckdb-sqlalchemy 1.5.4__tar.gz → 1.5.4.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.4 → duckdb_sqlalchemy-1.5.4.2}/.gitignore +1 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/CHANGELOG.md +23 -1
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/PKG-INFO +1 -1
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/olap.md +8 -2
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/__init__.py +101 -13
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/_bulk_insert.py +4 -9
- duckdb_sqlalchemy-1.5.4.2/duckdb_sqlalchemy/_row_shape.py +42 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/bulk.py +10 -18
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/olap.py +4 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_basic.py +35 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_core_units.py +80 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/pyproject.toml +1 -1
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/uv.lock +260 -304
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.codex/environments/environment.toml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.github/dependabot.yml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.github/workflows/lint.yml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.github/workflows/pages.yml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.github/workflows/publish.yaml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.github/workflows/pythonapp.yaml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/.pre-commit-config.yaml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/AGENTS.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/ARCHITECTURE.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/CLAUDE.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/CODE_OF_CONDUCT.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/LICENSE.txt +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/PLANS.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/README.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/ROADMAP.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/codecov.yml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/README.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/_config.yml +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/alembic.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/configuration.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/connection-urls.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/exec-plans/active/2026-03-27-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/exec-plans/active/2026-03-30-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/exec-plans/active/2026-03-31-maintenance-review.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/getting-started.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/index.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/migration-from-duckdb-engine.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/motherduck.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/overview.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/pandas-jupyter.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/robots.txt +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/seo-checklist.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/docs/types-and-caveats.md +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/_arrow.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/_query.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/_statements.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/_supports.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/_validation.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/capabilities.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/config.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/conftest.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/datatypes.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/motherduck.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/py.typed +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/requirements.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/__init__.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/conftest.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/snapshots/test_datatypes/test_interval/schema.sql +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/sqlalchemy_suite/conftest.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/sqlalchemy_suite/test_suite.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_datatypes.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_execution_options.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_helpers.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_integration.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_pandas.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_pyarrow.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_validation.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/util.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/url.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/examples/motherduck_arrow_reads.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/examples/motherduck_attach_modes.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/examples/motherduck_multi_instance_pool.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/examples/motherduck_queuepool_high_concurrency.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/examples/motherduck_read_scaling_per_user.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/examples/sqlalchemy_example.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/llms.txt +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/noxfile.py +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/renovate.json +0 -0
- {duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/test.cfg +0 -0
|
@@ -6,7 +6,29 @@ preserved from the upstream project for historical context.
|
|
|
6
6
|
|
|
7
7
|
## Maintained in this fork
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## [1.5.4.2](https://github.com/leonardovida/duckdb-sqlalchemy/compare/v1.5.4.1...v1.5.4.2) (2026-07-03)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- expose MotherDuck Dive status metadata columns on `md_list_dives()`, `md_create_dive()`, `md_get_dive()`, and other summary-returning Dive helpers
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- reject `copy_from_rows(..., include_header=True)` for sequence rows without explicit `columns` instead of letting DuckDB treat the first data row as the header
|
|
18
|
+
|
|
19
|
+
### Maintenance
|
|
20
|
+
|
|
21
|
+
- centralize reflection result default handling for multi-reflection methods without changing public behavior
|
|
22
|
+
|
|
23
|
+
### Tooling
|
|
24
|
+
|
|
25
|
+
- refresh the locked development dependency set to the latest non-breaking transitive releases within existing version caps
|
|
26
|
+
|
|
27
|
+
## [1.5.4.1](https://github.com/leonardovida/duckdb-sqlalchemy/compare/v1.5.4...v1.5.4.1) (2026-07-01)
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
- render autoincrement integer primary keys with an implicit DuckDB sequence (`DEFAULT nextval(...)`) instead of PostgreSQL's unsupported `SERIAL`, so `Column(Integer, primary_key=True)` works without an explicit `Sequence` ([#128](https://github.com/leonardovida/duckdb-sqlalchemy/issues/128))
|
|
10
32
|
|
|
11
33
|
## [1.5.4](https://github.com/leonardovida/duckdb-sqlalchemy/compare/v1.5.3...v1.5.4) (2026-06-26)
|
|
12
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duckdb-sqlalchemy
|
|
3
|
-
Version: 1.5.4
|
|
3
|
+
Version: 1.5.4.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
|
|
@@ -103,7 +103,13 @@ user_stmt = select(
|
|
|
103
103
|
)
|
|
104
104
|
|
|
105
105
|
dives = md_list_dives()
|
|
106
|
-
dives_stmt = select(
|
|
106
|
+
dives_stmt = select(
|
|
107
|
+
dives.c.id,
|
|
108
|
+
dives.c.title,
|
|
109
|
+
dives.c.status,
|
|
110
|
+
dives.c.status_applies_to_version,
|
|
111
|
+
dives.c.required_resources,
|
|
112
|
+
)
|
|
107
113
|
|
|
108
114
|
tokens = md_access_tokens()
|
|
109
115
|
tokens_stmt = select(tokens.c.token_name, tokens.c.token_type, tokens.c.expire_at)
|
|
@@ -132,7 +138,7 @@ created = md_create_dive(
|
|
|
132
138
|
create_stmt = select(created.c.id, created.c.version_id)
|
|
133
139
|
|
|
134
140
|
dive = md_get_dive(id="00000000-0000-0000-0000-000000000000")
|
|
135
|
-
dive_stmt = select(dive.c.title, dive.c.content)
|
|
141
|
+
dive_stmt = select(dive.c.title, dive.c.status, dive.c.content)
|
|
136
142
|
|
|
137
143
|
updated = md_update_dive_content(
|
|
138
144
|
id="00000000-0000-0000-0000-000000000000",
|
|
@@ -15,6 +15,7 @@ from typing import (
|
|
|
15
15
|
Dict,
|
|
16
16
|
Iterable,
|
|
17
17
|
List,
|
|
18
|
+
Mapping,
|
|
18
19
|
Optional,
|
|
19
20
|
Sequence,
|
|
20
21
|
Tuple,
|
|
@@ -24,10 +25,12 @@ from typing import (
|
|
|
24
25
|
import duckdb
|
|
25
26
|
import sqlalchemy
|
|
26
27
|
from packaging.version import Version
|
|
27
|
-
from sqlalchemy import pool, select, sql, text, util
|
|
28
|
+
from sqlalchemy import event, pool, select, sql, text, util
|
|
29
|
+
from sqlalchemy import schema as sa_schema
|
|
28
30
|
from sqlalchemy import types as sqltypes
|
|
29
31
|
from sqlalchemy.dialects.postgresql import UUID, insert
|
|
30
32
|
from sqlalchemy.dialects.postgresql.base import (
|
|
33
|
+
PGDDLCompiler,
|
|
31
34
|
PGDialect,
|
|
32
35
|
PGIdentifierPreparer,
|
|
33
36
|
PGInspector,
|
|
@@ -39,6 +42,7 @@ from sqlalchemy.engine.url import URL as SAURL
|
|
|
39
42
|
from sqlalchemy.exc import InvalidRequestError, NoSuchTableError
|
|
40
43
|
from sqlalchemy.ext.compiler import compiles
|
|
41
44
|
from sqlalchemy.sql import bindparam
|
|
45
|
+
from sqlalchemy.sql.compiler import IdentifierPreparer
|
|
42
46
|
from sqlalchemy.sql.selectable import Select
|
|
43
47
|
|
|
44
48
|
from ._arrow import DuckDBArrowResult
|
|
@@ -126,7 +130,7 @@ else:
|
|
|
126
130
|
try:
|
|
127
131
|
__version__ = package_version("duckdb-sqlalchemy")
|
|
128
132
|
except PackageNotFoundError: # pragma: no cover - source tree import fallback
|
|
129
|
-
__version__ = "1.5.4"
|
|
133
|
+
__version__ = "1.5.4.2"
|
|
130
134
|
sqlalchemy_version = sqlalchemy.__version__
|
|
131
135
|
SQLALCHEMY_VERSION = Version(sqlalchemy_version)
|
|
132
136
|
SQLALCHEMY_2 = SQLALCHEMY_VERSION >= Version("2.0.0")
|
|
@@ -671,6 +675,82 @@ class DuckDBIdentifierPreparer(PGIdentifierPreparer):
|
|
|
671
675
|
return self.format_schema(schema)
|
|
672
676
|
|
|
673
677
|
|
|
678
|
+
def _column_needs_implicit_sequence(column: Any) -> bool:
|
|
679
|
+
"""Columns that PostgreSQL would render as SERIAL/BIGSERIAL.
|
|
680
|
+
|
|
681
|
+
DuckDB has no SERIAL type, so these columns are backed by an explicitly
|
|
682
|
+
created sequence instead (see :class:`DuckDBDDLCompiler` and the
|
|
683
|
+
``before_create``/``after_drop`` hooks below).
|
|
684
|
+
"""
|
|
685
|
+
table = getattr(column, "table", None)
|
|
686
|
+
if table is None:
|
|
687
|
+
return False
|
|
688
|
+
if not column.primary_key or column is not table._autoincrement_column:
|
|
689
|
+
return False
|
|
690
|
+
if getattr(column, "identity", None) is not None:
|
|
691
|
+
return False
|
|
692
|
+
if column.default is not None and not (
|
|
693
|
+
isinstance(column.default, sa_schema.Sequence) and column.default.optional
|
|
694
|
+
):
|
|
695
|
+
return False
|
|
696
|
+
if column.server_default is not None:
|
|
697
|
+
return False
|
|
698
|
+
return True
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _implicit_sequence_ddl_name(preparer: IdentifierPreparer, column: Any) -> str:
|
|
702
|
+
name = preparer.quote(f"{column.table.name}_{column.name}_seq")
|
|
703
|
+
schema = column.table.schema
|
|
704
|
+
if schema:
|
|
705
|
+
name = f"{preparer.quote_schema(schema)}.{name}"
|
|
706
|
+
return name
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _create_implicit_sequences(target: Any, connection: Any, **kw: Any) -> None:
|
|
710
|
+
if connection.dialect.name != "duckdb":
|
|
711
|
+
return
|
|
712
|
+
preparer = connection.dialect.identifier_preparer
|
|
713
|
+
for column in target.columns:
|
|
714
|
+
if _column_needs_implicit_sequence(column):
|
|
715
|
+
connection.exec_driver_sql(
|
|
716
|
+
"CREATE SEQUENCE IF NOT EXISTS "
|
|
717
|
+
f"{_implicit_sequence_ddl_name(preparer, column)}"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _drop_implicit_sequences(target: Any, connection: Any, **kw: Any) -> None:
|
|
722
|
+
if connection.dialect.name != "duckdb":
|
|
723
|
+
return
|
|
724
|
+
preparer = connection.dialect.identifier_preparer
|
|
725
|
+
for column in target.columns:
|
|
726
|
+
if _column_needs_implicit_sequence(column):
|
|
727
|
+
connection.exec_driver_sql(
|
|
728
|
+
"DROP SEQUENCE IF EXISTS "
|
|
729
|
+
f"{_implicit_sequence_ddl_name(preparer, column)}"
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
event.listen(sa_schema.Table, "before_create", _create_implicit_sequences)
|
|
734
|
+
event.listen(sa_schema.Table, "after_drop", _drop_implicit_sequences)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
class DuckDBDDLCompiler(PGDDLCompiler):
|
|
738
|
+
def get_column_specification(self, column: Any, **kwargs: Any) -> str:
|
|
739
|
+
if not _column_needs_implicit_sequence(column):
|
|
740
|
+
return super().get_column_specification(column, **kwargs)
|
|
741
|
+
colspec = self.preparer.format_column(column)
|
|
742
|
+
colspec += " " + self.dialect.type_compiler_instance.process(
|
|
743
|
+
column.type, type_expression=column
|
|
744
|
+
)
|
|
745
|
+
seq_name = _implicit_sequence_ddl_name(self.preparer, column)
|
|
746
|
+
colspec += " DEFAULT nextval(%s)" % self.sql_compiler.render_literal_value(
|
|
747
|
+
seq_name, sqltypes.String()
|
|
748
|
+
)
|
|
749
|
+
if not column.nullable:
|
|
750
|
+
colspec += " NOT NULL"
|
|
751
|
+
return colspec
|
|
752
|
+
|
|
753
|
+
|
|
674
754
|
class DuckDBNullType(sqltypes.NullType):
|
|
675
755
|
def result_processor(self, dialect: Any, coltype: object) -> Any:
|
|
676
756
|
if coltype == "JSON":
|
|
@@ -711,6 +791,7 @@ class Dialect(PGDialect_psycopg2):
|
|
|
711
791
|
)
|
|
712
792
|
preparer = DuckDBIdentifierPreparer
|
|
713
793
|
identifier_preparer: DuckDBIdentifierPreparer
|
|
794
|
+
ddl_compiler = DuckDBDDLCompiler
|
|
714
795
|
|
|
715
796
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
716
797
|
kwargs["use_native_hstore"] = False
|
|
@@ -1326,6 +1407,20 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1326
1407
|
def _reflection_schema_key(self, schema: Optional[str]) -> Optional[str]:
|
|
1327
1408
|
return schema
|
|
1328
1409
|
|
|
1410
|
+
def _iter_reflection_results(
|
|
1411
|
+
self,
|
|
1412
|
+
schema: Optional[str],
|
|
1413
|
+
table_names: Iterable[str],
|
|
1414
|
+
reflected: Mapping[str, Any],
|
|
1415
|
+
default_factory: Callable[[], Any],
|
|
1416
|
+
) -> Iterable[Tuple[Any, Any]]:
|
|
1417
|
+
schema_key = self._reflection_schema_key(schema)
|
|
1418
|
+
for table_name in table_names:
|
|
1419
|
+
value = reflected.get(table_name)
|
|
1420
|
+
if value is None:
|
|
1421
|
+
value = default_factory()
|
|
1422
|
+
yield (schema_key, table_name), value
|
|
1423
|
+
|
|
1329
1424
|
def _get_single_reflection_result(
|
|
1330
1425
|
self,
|
|
1331
1426
|
connection: "Connection",
|
|
@@ -1422,13 +1517,8 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1422
1517
|
}
|
|
1423
1518
|
for row in constraint_rows
|
|
1424
1519
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
(
|
|
1428
|
-
(schema_key, table_name),
|
|
1429
|
-
constraints.get(table_name, ReflectionDefaults.pk_constraint()),
|
|
1430
|
-
)
|
|
1431
|
-
for table_name in table_names
|
|
1520
|
+
return self._iter_reflection_results(
|
|
1521
|
+
schema, table_names, constraints, ReflectionDefaults.pk_constraint
|
|
1432
1522
|
)
|
|
1433
1523
|
|
|
1434
1524
|
@cache # type: ignore[call-arg]
|
|
@@ -1531,10 +1621,8 @@ class Dialect(PGDialect_psycopg2):
|
|
|
1531
1621
|
if any(column_name is None for column_name in column_names):
|
|
1532
1622
|
reflected_index["expressions"] = expressions
|
|
1533
1623
|
indexes[row["table_name"]].append(reflected_index)
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
((schema_key, table_name), indexes.get(table_name, []))
|
|
1537
|
-
for table_name in table_names
|
|
1624
|
+
return self._iter_reflection_results(
|
|
1625
|
+
schema, table_names, indexes, ReflectionDefaults.indexes
|
|
1538
1626
|
)
|
|
1539
1627
|
|
|
1540
1628
|
def create_connect_args(self, url: SAURL) -> Tuple[tuple, dict]:
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Mapping
|
|
4
3
|
from typing import Any, Optional, Sequence, cast
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
def _rows_use_mapping_shape(rows: Sequence[Any]) -> bool:
|
|
8
|
-
return bool(rows) and isinstance(rows[0], Mapping)
|
|
5
|
+
from ._row_shape import infer_mapping_column_keys, rows_use_mapping_shape
|
|
9
6
|
|
|
10
7
|
|
|
11
8
|
def infer_bulk_insert_column_keys(rows: Sequence[Any]) -> Optional[list[str]]:
|
|
12
|
-
|
|
13
|
-
return None
|
|
14
|
-
return [str(key) for key in cast(Mapping[str, Any], rows[0]).keys()]
|
|
9
|
+
return infer_mapping_column_keys(rows)
|
|
15
10
|
|
|
16
11
|
|
|
17
12
|
def build_bulk_insert_dataframe(
|
|
@@ -23,7 +18,7 @@ def build_bulk_insert_dataframe(
|
|
|
23
18
|
return None
|
|
24
19
|
|
|
25
20
|
try:
|
|
26
|
-
if
|
|
21
|
+
if rows_use_mapping_shape(rows):
|
|
27
22
|
return pd.DataFrame.from_records(rows, columns=column_names)
|
|
28
23
|
return pd.DataFrame(rows, columns=cast(Any, column_names))
|
|
29
24
|
except Exception:
|
|
@@ -39,7 +34,7 @@ def build_bulk_insert_arrow_table(
|
|
|
39
34
|
return None
|
|
40
35
|
|
|
41
36
|
try:
|
|
42
|
-
if
|
|
37
|
+
if rows_use_mapping_shape(rows):
|
|
43
38
|
table = pa.Table.from_pylist(rows)
|
|
44
39
|
if column_names:
|
|
45
40
|
return table.select(column_names)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
4
|
+
from itertools import chain
|
|
5
|
+
from typing import Any, Optional, Tuple, Union, cast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def rows_use_mapping_shape(rows: Sequence[Any]) -> bool:
|
|
9
|
+
return bool(rows) and isinstance(rows[0], Mapping)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def infer_mapping_column_keys(rows: Sequence[Any]) -> Optional[list[str]]:
|
|
13
|
+
if not rows_use_mapping_shape(rows):
|
|
14
|
+
return None
|
|
15
|
+
return [str(key) for key in cast(Mapping[str, Any], rows[0]).keys()]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def mapping_row_as_sequence(
|
|
19
|
+
row: Mapping[str, Any], columns: Sequence[str]
|
|
20
|
+
) -> Sequence[Any]:
|
|
21
|
+
return [row.get(col) for col in columns]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def rows_as_sequences(
|
|
25
|
+
first: Union[Mapping[str, Any], Sequence[Any]],
|
|
26
|
+
rows: Iterable[Union[Mapping[str, Any], Sequence[Any]]],
|
|
27
|
+
columns: Optional[Sequence[str]],
|
|
28
|
+
) -> Tuple[Iterable[Sequence[Any]], Optional[Sequence[str]]]:
|
|
29
|
+
if isinstance(first, Mapping):
|
|
30
|
+
if columns is None:
|
|
31
|
+
columns = [str(col) for col in cast(Mapping[str, Any], first).keys()]
|
|
32
|
+
|
|
33
|
+
first_row = mapping_row_as_sequence(cast(Mapping[str, Any], first), columns)
|
|
34
|
+
remaining_rows = (
|
|
35
|
+
mapping_row_as_sequence(cast(Mapping[str, Any], row), columns)
|
|
36
|
+
for row in rows
|
|
37
|
+
)
|
|
38
|
+
return chain((first_row,), remaining_rows), columns
|
|
39
|
+
|
|
40
|
+
first_row = cast(Sequence[Any], first)
|
|
41
|
+
remaining_rows = (cast(Sequence[Any], row) for row in rows)
|
|
42
|
+
return chain((first_row,), remaining_rows), columns
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import csv
|
|
2
2
|
import tempfile
|
|
3
|
-
from itertools import chain
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Iterable, Mapping, Optional, Sequence, Tuple, Union
|
|
4
|
+
from typing import Any, Iterable, Mapping, Optional, Sequence, Tuple, Union
|
|
6
5
|
|
|
6
|
+
from ._row_shape import rows_as_sequences
|
|
7
7
|
from ._validation import (
|
|
8
8
|
validate_dotted_identifier,
|
|
9
9
|
validate_identifier,
|
|
@@ -160,20 +160,7 @@ def _copy_rows_as_sequences(
|
|
|
160
160
|
rows: Iterable[Union[Mapping[str, Any], Sequence[Any]]],
|
|
161
161
|
columns: Optional[Sequence[str]],
|
|
162
162
|
) -> Tuple[Iterable[Sequence[Any]], Optional[Sequence[str]]]:
|
|
163
|
-
|
|
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
|
|
163
|
+
return rows_as_sequences(first, rows, columns)
|
|
177
164
|
|
|
178
165
|
|
|
179
166
|
def copy_from_parquet(
|
|
@@ -246,16 +233,21 @@ def copy_from_rows(
|
|
|
246
233
|
if first is None:
|
|
247
234
|
return None
|
|
248
235
|
|
|
249
|
-
|
|
236
|
+
header = copy_options.pop("header", include_header)
|
|
237
|
+
copy_options = {"header": header, **copy_options}
|
|
250
238
|
|
|
251
239
|
chunked_rows, columns = _copy_rows_as_sequences(first, iterator, columns)
|
|
240
|
+
if header and columns is None:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
"copy_from_rows header mode requires columns for sequence rows"
|
|
243
|
+
)
|
|
252
244
|
_copy_rows_as_csv_chunks(
|
|
253
245
|
connection,
|
|
254
246
|
table,
|
|
255
247
|
chunked_rows,
|
|
256
248
|
columns=columns,
|
|
257
249
|
chunk_size=chunk_size,
|
|
258
|
-
include_header=
|
|
250
|
+
include_header=header,
|
|
259
251
|
copy_options=copy_options,
|
|
260
252
|
)
|
|
261
253
|
return None
|
|
@@ -37,6 +37,7 @@ from sqlalchemy.engine.reflection import Inspector
|
|
|
37
37
|
from sqlalchemy.exc import DBAPIError, OperationalError
|
|
38
38
|
from sqlalchemy.orm import Session, declarative_base, relationship, sessionmaker
|
|
39
39
|
from sqlalchemy.pool import QueuePool, SingletonThreadPool
|
|
40
|
+
from sqlalchemy.schema import CreateTable
|
|
40
41
|
|
|
41
42
|
from .. import Dialect, checkpoint, insert, supports_attach, supports_user_agent
|
|
42
43
|
from .._supports import has_comment_support
|
|
@@ -771,3 +772,37 @@ def test_register_filesystem() -> None:
|
|
|
771
772
|
with engine.connect() as conn:
|
|
772
773
|
duckdb_conn = getattr(conn.connection.dbapi_connection, "_ConnectionWrapper__c")
|
|
773
774
|
assert duckdb.list_filesystems(connection=duckdb_conn) == ["memory", "file"]
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def test_autoincrement_pk_without_sequence() -> None:
|
|
778
|
+
"""Plain Integer primary keys are backed by an implicit sequence (GH-128).
|
|
779
|
+
|
|
780
|
+
PostgreSQL renders these columns as SERIAL, which DuckDB does not support.
|
|
781
|
+
"""
|
|
782
|
+
LocalBase = declarative_base()
|
|
783
|
+
|
|
784
|
+
class AutoUser(LocalBase):
|
|
785
|
+
__tablename__ = "auto_users"
|
|
786
|
+
|
|
787
|
+
id = Column(Integer, primary_key=True)
|
|
788
|
+
name = Column(String)
|
|
789
|
+
|
|
790
|
+
engine = create_engine("duckdb:///:memory:")
|
|
791
|
+
ddl = str(CreateTable(AutoUser.__table__).compile(engine))
|
|
792
|
+
assert "SERIAL" not in ddl
|
|
793
|
+
assert "DEFAULT nextval('auto_users_id_seq')" in ddl
|
|
794
|
+
|
|
795
|
+
LocalBase.metadata.create_all(engine)
|
|
796
|
+
with Session(engine) as session:
|
|
797
|
+
session.add(AutoUser(name="Ada"))
|
|
798
|
+
session.add(AutoUser(name="Grace"))
|
|
799
|
+
session.commit()
|
|
800
|
+
assert sorted(u.id for u in session.query(AutoUser).all()) == [1, 2]
|
|
801
|
+
|
|
802
|
+
# drop_all removes the implicit sequence so create_all starts fresh
|
|
803
|
+
LocalBase.metadata.drop_all(engine)
|
|
804
|
+
LocalBase.metadata.create_all(engine)
|
|
805
|
+
with Session(engine) as session:
|
|
806
|
+
session.add(AutoUser(name="Alan"))
|
|
807
|
+
session.commit()
|
|
808
|
+
assert session.query(AutoUser).one().id == 1
|
{duckdb_sqlalchemy-1.5.4 → duckdb_sqlalchemy-1.5.4.2}/duckdb_sqlalchemy/tests/test_core_units.py
RENAMED
|
@@ -720,6 +720,10 @@ def test_md_list_dives_uses_released_motherduck_columns() -> None:
|
|
|
720
720
|
"created_at",
|
|
721
721
|
"updated_at",
|
|
722
722
|
"owner_name",
|
|
723
|
+
"status",
|
|
724
|
+
"status_changed_at",
|
|
725
|
+
"status_set_by",
|
|
726
|
+
"status_applies_to_version",
|
|
723
727
|
"required_resources",
|
|
724
728
|
]
|
|
725
729
|
|
|
@@ -932,6 +936,10 @@ def test_motherduck_dive_helpers_use_released_columns() -> None:
|
|
|
932
936
|
"created_at",
|
|
933
937
|
"updated_at",
|
|
934
938
|
"owner_name",
|
|
939
|
+
"status",
|
|
940
|
+
"status_changed_at",
|
|
941
|
+
"status_set_by",
|
|
942
|
+
"status_applies_to_version",
|
|
935
943
|
"required_resources",
|
|
936
944
|
"version_id",
|
|
937
945
|
"version_storage_url",
|
|
@@ -989,6 +997,21 @@ def test_motherduck_dive_helpers_render_named_parameters() -> None:
|
|
|
989
997
|
]
|
|
990
998
|
|
|
991
999
|
|
|
1000
|
+
def test_motherduck_dive_status_columns_compile() -> None:
|
|
1001
|
+
dives = olap.md_list_dives()
|
|
1002
|
+
stmt = select(
|
|
1003
|
+
dives.c.id,
|
|
1004
|
+
dives.c.status,
|
|
1005
|
+
dives.c.status_changed_at,
|
|
1006
|
+
dives.c.status_set_by,
|
|
1007
|
+
dives.c.status_applies_to_version,
|
|
1008
|
+
).select_from(dives)
|
|
1009
|
+
sql = str(stmt.compile(dialect=Dialect()))
|
|
1010
|
+
|
|
1011
|
+
assert "md_list_dives" in sql
|
|
1012
|
+
assert "status_applies_to_version" in sql
|
|
1013
|
+
|
|
1014
|
+
|
|
992
1015
|
def test_cursorwrapper_execute_basic_paths() -> None:
|
|
993
1016
|
class DummyConn:
|
|
994
1017
|
def __init__(self) -> None:
|
|
@@ -1339,6 +1362,31 @@ def test_reflection_fallback_returns_empty_only_for_existing_tables(
|
|
|
1339
1362
|
)
|
|
1340
1363
|
|
|
1341
1364
|
|
|
1365
|
+
def test_iter_reflection_results_defaults_only_missing_tables() -> None:
|
|
1366
|
+
dialect = Dialect()
|
|
1367
|
+
default_calls = 0
|
|
1368
|
+
|
|
1369
|
+
def default_factory() -> list[Any]:
|
|
1370
|
+
nonlocal default_calls
|
|
1371
|
+
default_calls += 1
|
|
1372
|
+
return []
|
|
1373
|
+
|
|
1374
|
+
results = list(
|
|
1375
|
+
dialect._iter_reflection_results(
|
|
1376
|
+
"analytics",
|
|
1377
|
+
["orders", "customers"],
|
|
1378
|
+
{"orders": [{"name": "orders_pk"}]},
|
|
1379
|
+
default_factory,
|
|
1380
|
+
)
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
assert results == [
|
|
1384
|
+
(("analytics", "orders"), [{"name": "orders_pk"}]),
|
|
1385
|
+
(("analytics", "customers"), []),
|
|
1386
|
+
]
|
|
1387
|
+
assert default_calls == 1
|
|
1388
|
+
|
|
1389
|
+
|
|
1342
1390
|
def test_parse_duckdb_enum_labels_unquotes_escaped_strings() -> None:
|
|
1343
1391
|
labels, enum_name = Dialect()._parse_duckdb_enum_labels(
|
|
1344
1392
|
"ENUM('alpha', 'it''s fine')",
|
|
@@ -1473,6 +1521,38 @@ def test_copy_from_rows_sequence_uses_explicit_columns_and_chunks() -> None:
|
|
|
1473
1521
|
assert rows == [(1, "one"), (2, "two"), (3, "three")]
|
|
1474
1522
|
|
|
1475
1523
|
|
|
1524
|
+
def test_copy_from_rows_sequence_header_requires_columns() -> None:
|
|
1525
|
+
conn = duckdb.connect(":memory:")
|
|
1526
|
+
conn.execute("CREATE TABLE safe(i INTEGER, label VARCHAR)")
|
|
1527
|
+
|
|
1528
|
+
with pytest.raises(ValueError, match="header mode requires columns"):
|
|
1529
|
+
copy_from_rows(
|
|
1530
|
+
conn,
|
|
1531
|
+
"safe",
|
|
1532
|
+
[(1, "one"), (2, "two")],
|
|
1533
|
+
include_header=True,
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
rows = conn.execute("SELECT i, label FROM safe ORDER BY i").fetchall()
|
|
1537
|
+
assert rows == []
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def test_copy_from_rows_header_option_writes_header_with_columns() -> None:
|
|
1541
|
+
conn = duckdb.connect(":memory:")
|
|
1542
|
+
conn.execute("CREATE TABLE safe(i INTEGER, label VARCHAR)")
|
|
1543
|
+
|
|
1544
|
+
copy_from_rows(
|
|
1545
|
+
conn,
|
|
1546
|
+
"safe",
|
|
1547
|
+
[(1, "one"), (2, "two")],
|
|
1548
|
+
columns=["i", "label"],
|
|
1549
|
+
header=True,
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
rows = conn.execute("SELECT i, label FROM safe ORDER BY i").fetchall()
|
|
1553
|
+
assert rows == [(1, "one"), (2, "two")]
|
|
1554
|
+
|
|
1555
|
+
|
|
1476
1556
|
def test_copy_rows_as_sequences_infers_mapping_columns() -> None:
|
|
1477
1557
|
rows, columns = duckdb_sqlalchemy.bulk._copy_rows_as_sequences(
|
|
1478
1558
|
{"id": 1, "label": "one"},
|