duckdb-sqlalchemy 0.19.0__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 +1411 -0
- duckdb_sqlalchemy/_supports.py +23 -0
- duckdb_sqlalchemy/bulk.py +192 -0
- duckdb_sqlalchemy/capabilities.py +24 -0
- duckdb_sqlalchemy/config.py +70 -0
- duckdb_sqlalchemy/conftest.py +0 -0
- duckdb_sqlalchemy/datatypes.py +332 -0
- duckdb_sqlalchemy/motherduck.py +237 -0
- duckdb_sqlalchemy/olap.py +39 -0
- duckdb_sqlalchemy/py.typed +0 -0
- duckdb_sqlalchemy/requirements.py +5 -0
- duckdb_sqlalchemy/tests/__init__.py +0 -0
- duckdb_sqlalchemy/tests/conftest.py +55 -0
- duckdb_sqlalchemy/tests/snapshots/test_datatypes/test_interval/schema.sql +3 -0
- duckdb_sqlalchemy/tests/sqlalchemy_suite/conftest.py +11 -0
- duckdb_sqlalchemy/tests/sqlalchemy_suite/test_suite.py +1 -0
- duckdb_sqlalchemy/tests/test_basic.py +718 -0
- duckdb_sqlalchemy/tests/test_core_units.py +474 -0
- duckdb_sqlalchemy/tests/test_datatypes.py +251 -0
- duckdb_sqlalchemy/tests/test_execution_options.py +32 -0
- duckdb_sqlalchemy/tests/test_helpers.py +71 -0
- duckdb_sqlalchemy/tests/test_integration.py +46 -0
- duckdb_sqlalchemy/tests/test_pandas.py +146 -0
- duckdb_sqlalchemy/tests/test_pyarrow.py +61 -0
- duckdb_sqlalchemy/tests/util.py +9 -0
- duckdb_sqlalchemy/url.py +59 -0
- duckdb_sqlalchemy-0.19.0.dist-info/METADATA +182 -0
- duckdb_sqlalchemy-0.19.0.dist-info/RECORD +31 -0
- duckdb_sqlalchemy-0.19.0.dist-info/WHEEL +4 -0
- duckdb_sqlalchemy-0.19.0.dist-info/entry_points.txt +2 -0
- duckdb_sqlalchemy-0.19.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
|
|
3
|
+
import duckdb
|
|
4
|
+
|
|
5
|
+
from .capabilities import get_capabilities
|
|
6
|
+
|
|
7
|
+
_capabilities = get_capabilities(duckdb.__version__)
|
|
8
|
+
duckdb_version = _capabilities.version
|
|
9
|
+
has_uhugeint_support = _capabilities.supports_uhugeint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@lru_cache()
|
|
13
|
+
def has_comment_support() -> bool:
|
|
14
|
+
"""
|
|
15
|
+
See https://github.com/duckdb/duckdb/pull/10372
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
with duckdb.connect(":memory:") as con:
|
|
19
|
+
con.execute("CREATE TABLE t (i INTEGER);")
|
|
20
|
+
con.execute("COMMENT ON TABLE t IS 'test';")
|
|
21
|
+
except duckdb.ParserException:
|
|
22
|
+
return False
|
|
23
|
+
return True
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Iterable, Mapping, Optional, Sequence, Tuple, Union
|
|
5
|
+
|
|
6
|
+
TableLike = Union[str, Any]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _quote_literal(value: Any) -> str:
|
|
10
|
+
if isinstance(value, Path):
|
|
11
|
+
value = str(value)
|
|
12
|
+
if isinstance(value, bool):
|
|
13
|
+
return "TRUE" if value else "FALSE"
|
|
14
|
+
if isinstance(value, (int, float)):
|
|
15
|
+
return str(value)
|
|
16
|
+
if value is None:
|
|
17
|
+
return "NULL"
|
|
18
|
+
return "'" + str(value).replace("'", "''") + "'"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _format_copy_options(options: Mapping[str, Any]) -> str:
|
|
22
|
+
if not options:
|
|
23
|
+
return ""
|
|
24
|
+
parts = []
|
|
25
|
+
for key, value in options.items():
|
|
26
|
+
if value is None:
|
|
27
|
+
continue
|
|
28
|
+
opt_key = str(key).upper()
|
|
29
|
+
if isinstance(value, (list, tuple)):
|
|
30
|
+
inner = ", ".join(_quote_literal(v) for v in value)
|
|
31
|
+
parts.append(f"{opt_key} ({inner})")
|
|
32
|
+
else:
|
|
33
|
+
parts.append(f"{opt_key} {_quote_literal(value)}")
|
|
34
|
+
if not parts:
|
|
35
|
+
return ""
|
|
36
|
+
return " (" + ", ".join(parts) + ")"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _format_table(connection: Any, table: TableLike) -> str:
|
|
40
|
+
if hasattr(table, "name"):
|
|
41
|
+
preparer = getattr(
|
|
42
|
+
getattr(connection, "dialect", None), "identifier_preparer", None
|
|
43
|
+
)
|
|
44
|
+
if preparer is not None:
|
|
45
|
+
return preparer.format_table(table)
|
|
46
|
+
schema = getattr(table, "schema", None)
|
|
47
|
+
name = getattr(table, "name", None)
|
|
48
|
+
if schema:
|
|
49
|
+
return f"{schema}.{name}"
|
|
50
|
+
return str(name)
|
|
51
|
+
return str(table)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_columns(connection: Any, columns: Optional[Sequence[str]]) -> str:
|
|
55
|
+
if not columns:
|
|
56
|
+
return ""
|
|
57
|
+
preparer = getattr(
|
|
58
|
+
getattr(connection, "dialect", None), "identifier_preparer", None
|
|
59
|
+
)
|
|
60
|
+
if preparer is None:
|
|
61
|
+
cols = ", ".join(columns)
|
|
62
|
+
else:
|
|
63
|
+
cols = ", ".join(preparer.quote_identifier(col) for col in columns)
|
|
64
|
+
return f" ({cols})"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _execute_sql(connection: Any, statement: str) -> Any:
|
|
68
|
+
if hasattr(connection, "exec_driver_sql"):
|
|
69
|
+
return connection.exec_driver_sql(statement)
|
|
70
|
+
return connection.execute(statement)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def copy_from_parquet(
|
|
74
|
+
connection: Any,
|
|
75
|
+
table: TableLike,
|
|
76
|
+
path: Union[str, Path],
|
|
77
|
+
*,
|
|
78
|
+
columns: Optional[Sequence[str]] = None,
|
|
79
|
+
**options: Any,
|
|
80
|
+
) -> Any:
|
|
81
|
+
return _copy_from_file(
|
|
82
|
+
connection,
|
|
83
|
+
table,
|
|
84
|
+
path,
|
|
85
|
+
format_name="parquet",
|
|
86
|
+
columns=columns,
|
|
87
|
+
**options,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def copy_from_csv(
|
|
92
|
+
connection: Any,
|
|
93
|
+
table: TableLike,
|
|
94
|
+
path: Union[str, Path],
|
|
95
|
+
*,
|
|
96
|
+
columns: Optional[Sequence[str]] = None,
|
|
97
|
+
**options: Any,
|
|
98
|
+
) -> Any:
|
|
99
|
+
return _copy_from_file(
|
|
100
|
+
connection,
|
|
101
|
+
table,
|
|
102
|
+
path,
|
|
103
|
+
format_name="csv",
|
|
104
|
+
columns=columns,
|
|
105
|
+
**options,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _copy_from_file(
|
|
110
|
+
connection: Any,
|
|
111
|
+
table: TableLike,
|
|
112
|
+
path: Union[str, Path],
|
|
113
|
+
*,
|
|
114
|
+
format_name: str,
|
|
115
|
+
columns: Optional[Sequence[str]] = None,
|
|
116
|
+
**options: Any,
|
|
117
|
+
) -> Any:
|
|
118
|
+
table_name = _format_table(connection, table)
|
|
119
|
+
column_clause = _format_columns(connection, columns)
|
|
120
|
+
path_literal = _quote_literal(path)
|
|
121
|
+
copy_options = {"format": format_name, **options}
|
|
122
|
+
options_clause = _format_copy_options(copy_options)
|
|
123
|
+
statement = f"COPY {table_name}{column_clause} FROM {path_literal}{options_clause}"
|
|
124
|
+
return _execute_sql(connection, statement)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def copy_from_rows(
|
|
128
|
+
connection: Any,
|
|
129
|
+
table: TableLike,
|
|
130
|
+
rows: Iterable[Union[Mapping[str, Any], Sequence[Any]]],
|
|
131
|
+
*,
|
|
132
|
+
columns: Optional[Sequence[str]] = None,
|
|
133
|
+
chunk_size: int = 100000,
|
|
134
|
+
include_header: bool = False,
|
|
135
|
+
**copy_options: Any,
|
|
136
|
+
) -> Any:
|
|
137
|
+
iterator = iter(rows)
|
|
138
|
+
first = next(iterator, None)
|
|
139
|
+
if first is None:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
if isinstance(first, Mapping):
|
|
143
|
+
if columns is None:
|
|
144
|
+
columns = list(first.keys())
|
|
145
|
+
|
|
146
|
+
def row_to_seq(row: Mapping[str, Any]) -> Sequence[Any]:
|
|
147
|
+
return [row.get(col) for col in columns or []]
|
|
148
|
+
else:
|
|
149
|
+
|
|
150
|
+
def row_to_seq(row: Sequence[Any]) -> Sequence[Any]:
|
|
151
|
+
return row
|
|
152
|
+
|
|
153
|
+
def open_writer() -> Tuple[Any, Any, int]:
|
|
154
|
+
tmp = tempfile.NamedTemporaryFile("w", newline="", suffix=".csv", delete=False)
|
|
155
|
+
writer = csv.writer(tmp)
|
|
156
|
+
if include_header and columns:
|
|
157
|
+
writer.writerow(columns)
|
|
158
|
+
return tmp, writer, 0
|
|
159
|
+
|
|
160
|
+
def flush_chunk(tmp: Any) -> None:
|
|
161
|
+
tmp.flush()
|
|
162
|
+
path = tmp.name
|
|
163
|
+
tmp.close()
|
|
164
|
+
try:
|
|
165
|
+
copy_from_csv(
|
|
166
|
+
connection,
|
|
167
|
+
table,
|
|
168
|
+
path,
|
|
169
|
+
columns=columns if columns else None,
|
|
170
|
+
**copy_options,
|
|
171
|
+
)
|
|
172
|
+
finally:
|
|
173
|
+
try:
|
|
174
|
+
Path(path).unlink()
|
|
175
|
+
except FileNotFoundError:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
copy_options = {"header": include_header, **copy_options}
|
|
179
|
+
|
|
180
|
+
tmp, writer, count = open_writer()
|
|
181
|
+
writer.writerow(row_to_seq(first))
|
|
182
|
+
count += 1
|
|
183
|
+
|
|
184
|
+
for row in iterator:
|
|
185
|
+
writer.writerow(row_to_seq(row))
|
|
186
|
+
count += 1
|
|
187
|
+
if chunk_size and count >= chunk_size:
|
|
188
|
+
flush_chunk(tmp)
|
|
189
|
+
tmp, writer, count = open_writer()
|
|
190
|
+
|
|
191
|
+
if count:
|
|
192
|
+
flush_chunk(tmp)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from packaging.version import Version
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class DuckDBCapabilities:
|
|
9
|
+
version: Version
|
|
10
|
+
supports_attach: bool
|
|
11
|
+
supports_user_agent: bool
|
|
12
|
+
supports_uhugeint: bool
|
|
13
|
+
supports_varint: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_capabilities(version: Union[str, Version]) -> DuckDBCapabilities:
|
|
17
|
+
resolved = version if isinstance(version, Version) else Version(version)
|
|
18
|
+
return DuckDBCapabilities(
|
|
19
|
+
version=resolved,
|
|
20
|
+
supports_attach=resolved >= Version("0.7.0"),
|
|
21
|
+
supports_user_agent=resolved >= Version("0.9.2"),
|
|
22
|
+
supports_uhugeint=resolved >= Version("0.10.0"),
|
|
23
|
+
supports_varint=resolved > Version("1.0.0"),
|
|
24
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import Dict, Set, Type, Union
|
|
5
|
+
|
|
6
|
+
import duckdb
|
|
7
|
+
from sqlalchemy import Boolean, Float, Integer, String
|
|
8
|
+
from sqlalchemy.engine import Dialect
|
|
9
|
+
from sqlalchemy.sql.type_api import TypeEngine
|
|
10
|
+
|
|
11
|
+
TYPES: Dict[Type, TypeEngine] = {
|
|
12
|
+
bool: Boolean(),
|
|
13
|
+
int: Integer(),
|
|
14
|
+
float: Float(),
|
|
15
|
+
str: String(),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache()
|
|
20
|
+
def get_core_config() -> Set[str]:
|
|
21
|
+
# List of connection string parameters that are supported by MotherDuck
|
|
22
|
+
# See: https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/
|
|
23
|
+
motherduck_config_keys = {
|
|
24
|
+
"motherduck_token",
|
|
25
|
+
"attach_mode",
|
|
26
|
+
"saas_mode",
|
|
27
|
+
"session_hint",
|
|
28
|
+
"access_mode",
|
|
29
|
+
"dbinstance_inactivity_ttl",
|
|
30
|
+
"motherduck_dbinstance_inactivity_ttl",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
with duckdb.connect(":memory:") as conn:
|
|
34
|
+
rows = conn.execute("SELECT name FROM duckdb_settings()").fetchall()
|
|
35
|
+
return {name for (name,) in rows} | motherduck_config_keys
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def apply_config(
|
|
39
|
+
dialect: Dialect,
|
|
40
|
+
conn: duckdb.DuckDBPyConnection,
|
|
41
|
+
ext: Dict[str, Union[str, int, bool, float, None]],
|
|
42
|
+
) -> None:
|
|
43
|
+
# TODO: does sqlalchemy have something that could do this for us?
|
|
44
|
+
processors = [
|
|
45
|
+
(typ, type_engine.literal_processor(dialect=dialect))
|
|
46
|
+
for typ, type_engine in TYPES.items()
|
|
47
|
+
]
|
|
48
|
+
string_processor = String().literal_processor(dialect=dialect)
|
|
49
|
+
|
|
50
|
+
for k, v in ext.items():
|
|
51
|
+
if v is None:
|
|
52
|
+
conn.execute(f"SET {k} = NULL")
|
|
53
|
+
continue
|
|
54
|
+
if isinstance(v, os.PathLike):
|
|
55
|
+
v = os.fspath(v)
|
|
56
|
+
process = string_processor
|
|
57
|
+
elif isinstance(v, Decimal):
|
|
58
|
+
v = str(v)
|
|
59
|
+
process = string_processor
|
|
60
|
+
else:
|
|
61
|
+
process = None
|
|
62
|
+
for typ, processor in processors:
|
|
63
|
+
if isinstance(v, typ):
|
|
64
|
+
process = processor
|
|
65
|
+
break
|
|
66
|
+
if process is None:
|
|
67
|
+
v = str(v)
|
|
68
|
+
process = string_processor
|
|
69
|
+
assert process, f"Not able to configure {k} with {v}"
|
|
70
|
+
conn.execute(f"SET {k} = {process(v)}")
|
|
File without changes
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
See https://duckdb.org/docs/sql/data_types/numeric for more information
|
|
3
|
+
|
|
4
|
+
Also
|
|
5
|
+
```sql
|
|
6
|
+
select * from duckdb_types where type_category = 'NUMERIC';
|
|
7
|
+
```
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import typing
|
|
11
|
+
from typing import Any, Dict, Optional, Tuple, Type
|
|
12
|
+
|
|
13
|
+
import duckdb
|
|
14
|
+
from packaging.version import Version
|
|
15
|
+
from sqlalchemy import exc, util
|
|
16
|
+
from sqlalchemy.dialects.postgresql.base import PGIdentifierPreparer, PGTypeCompiler
|
|
17
|
+
from sqlalchemy.engine import Dialect
|
|
18
|
+
from sqlalchemy.ext.compiler import compiles
|
|
19
|
+
from sqlalchemy.sql import sqltypes, type_api
|
|
20
|
+
from sqlalchemy.sql.type_api import TypeEngine
|
|
21
|
+
from sqlalchemy.types import BigInteger, Integer, SmallInteger, String
|
|
22
|
+
|
|
23
|
+
# INTEGER INT4, INT, SIGNED -2147483648 2147483647
|
|
24
|
+
# SMALLINT INT2, SHORT -32768 32767
|
|
25
|
+
# BIGINT INT8, LONG -9223372036854775808 9223372036854775807
|
|
26
|
+
(BigInteger, SmallInteger) # pure reexport
|
|
27
|
+
|
|
28
|
+
duckdb_version = duckdb.__version__
|
|
29
|
+
|
|
30
|
+
IS_GT_1 = Version(duckdb_version) > Version("1.0.0")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DuckDBInteger(Integer):
|
|
34
|
+
cache_ok = True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UInt64(DuckDBInteger):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UInt32(DuckDBInteger):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UInt16(DuckDBInteger):
|
|
46
|
+
"AKA USMALLINT"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UInt8(DuckDBInteger):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UTinyInteger(DuckDBInteger):
|
|
54
|
+
"AKA UInt1"
|
|
55
|
+
|
|
56
|
+
name = "UTinyInt"
|
|
57
|
+
# UTINYINT - 0 255
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TinyInteger(DuckDBInteger):
|
|
61
|
+
"AKA Int1"
|
|
62
|
+
|
|
63
|
+
name = "TinyInt"
|
|
64
|
+
# TINYINT INT1 -128 127
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class USmallInteger(DuckDBInteger):
|
|
68
|
+
name = "usmallint"
|
|
69
|
+
"AKA UInt2"
|
|
70
|
+
# USMALLINT - 0 65535
|
|
71
|
+
min = 0
|
|
72
|
+
max = 2**15
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class UBigInteger(DuckDBInteger):
|
|
76
|
+
name = "UBigInt"
|
|
77
|
+
min = 0
|
|
78
|
+
max = 18446744073709551615
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class HugeInteger(DuckDBInteger):
|
|
82
|
+
name = "HugeInt"
|
|
83
|
+
# HUGEINT -170141183460469231731687303715884105727* 170141183460469231731687303715884105727
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UHugeInteger(DuckDBInteger):
|
|
87
|
+
name = "UHugeInt"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UInteger(DuckDBInteger):
|
|
91
|
+
# UINTEGER - 0 4294967295
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if IS_GT_1:
|
|
96
|
+
|
|
97
|
+
class VarInt(DuckDBInteger):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def compile_uint(element: Integer, compiler: PGTypeCompiler, **kw: Any) -> str:
|
|
102
|
+
return getattr(element, "name", type(element).__name__)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
types = [
|
|
106
|
+
subclass
|
|
107
|
+
for subclass in DuckDBInteger.__subclasses__()
|
|
108
|
+
if subclass.__module__ == UInt64.__module__
|
|
109
|
+
]
|
|
110
|
+
assert types
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
TV = typing.Union[Type[TypeEngine], TypeEngine]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _normalize_fields(
|
|
117
|
+
fields: Optional[Dict[str, TV]],
|
|
118
|
+
) -> Optional[Tuple[Tuple[str, TypeEngine], ...]]:
|
|
119
|
+
if fields is None:
|
|
120
|
+
return None
|
|
121
|
+
if isinstance(fields, dict):
|
|
122
|
+
items = fields.items()
|
|
123
|
+
else:
|
|
124
|
+
items = fields
|
|
125
|
+
return tuple(
|
|
126
|
+
(str(key), type_api.to_instance(value))
|
|
127
|
+
for key, value in items # type: ignore[arg-type]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _normalize_fields_cache_key(
|
|
132
|
+
fields: Optional[Tuple[Tuple[str, TypeEngine], ...]],
|
|
133
|
+
) -> Optional[Tuple[Tuple[str, Any], ...]]:
|
|
134
|
+
if fields is None:
|
|
135
|
+
return None
|
|
136
|
+
return tuple(
|
|
137
|
+
(key, value._static_cache_key if isinstance(value, TypeEngine) else value)
|
|
138
|
+
for key, value in fields
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Struct(TypeEngine):
|
|
143
|
+
"""
|
|
144
|
+
Represents a STRUCT type in DuckDB
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from duckdb_sqlalchemy.datatypes import Struct
|
|
148
|
+
from sqlalchemy import Table, Column, String
|
|
149
|
+
|
|
150
|
+
Table(
|
|
151
|
+
'hello',
|
|
152
|
+
Column('name', Struct({'first': String, 'last': String})
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
:param fields: only optional due to limitations with how much type information DuckDB returns to us in the description field
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
__visit_name__ = "struct"
|
|
160
|
+
cache_ok = True
|
|
161
|
+
|
|
162
|
+
def __init__(self, fields: Optional[Dict[str, TV]] = None):
|
|
163
|
+
self.fields = fields
|
|
164
|
+
self._fields = _normalize_fields(fields)
|
|
165
|
+
self._fields_cache_key = _normalize_fields_cache_key(self._fields)
|
|
166
|
+
|
|
167
|
+
@util.memoized_property
|
|
168
|
+
def _static_cache_key(self): # type: ignore[override]
|
|
169
|
+
if self._fields_cache_key is None:
|
|
170
|
+
return (self.__class__,)
|
|
171
|
+
return (self.__class__, ("fields", self._fields_cache_key))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Map(TypeEngine):
|
|
175
|
+
"""
|
|
176
|
+
Represents a MAP type in DuckDB
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from duckdb_sqlalchemy.datatypes import Map
|
|
180
|
+
from sqlalchemy import Table, Column, String
|
|
181
|
+
|
|
182
|
+
Table(
|
|
183
|
+
'hello',
|
|
184
|
+
Column('name', Map(String, String)
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
__visit_name__ = "map"
|
|
190
|
+
cache_ok = True
|
|
191
|
+
key_type: TV
|
|
192
|
+
value_type: TV
|
|
193
|
+
|
|
194
|
+
def __init__(self, key_type: TV, value_type: TV):
|
|
195
|
+
self.key_type = type_api.to_instance(key_type)
|
|
196
|
+
self.value_type = type_api.to_instance(value_type)
|
|
197
|
+
|
|
198
|
+
def bind_processor(self, dialect: Dialect) -> Any:
|
|
199
|
+
return lambda value: (
|
|
200
|
+
{"key": list(value), "value": list(value.values())} if value else None
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def result_processor(self, dialect: Dialect, coltype: object) -> Any:
|
|
204
|
+
if IS_GT_1:
|
|
205
|
+
return lambda value: value
|
|
206
|
+
else:
|
|
207
|
+
return (
|
|
208
|
+
lambda value: dict(zip(value["key"], value["value"])) if value else {}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Union(TypeEngine):
|
|
213
|
+
"""
|
|
214
|
+
Represents a UNION type in DuckDB
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from duckdb_sqlalchemy.datatypes import Union
|
|
218
|
+
from sqlalchemy import Table, Column, String
|
|
219
|
+
|
|
220
|
+
Table(
|
|
221
|
+
'hello',
|
|
222
|
+
Column('name', Union({"name": String, "age": String})
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
__visit_name__ = "union"
|
|
228
|
+
cache_ok = True
|
|
229
|
+
fields: Dict[str, TV]
|
|
230
|
+
|
|
231
|
+
def __init__(self, fields: Dict[str, TV]):
|
|
232
|
+
self.fields = fields
|
|
233
|
+
self._fields = _normalize_fields(fields)
|
|
234
|
+
self._fields_cache_key = _normalize_fields_cache_key(self._fields)
|
|
235
|
+
|
|
236
|
+
@util.memoized_property
|
|
237
|
+
def _static_cache_key(self): # type: ignore[override]
|
|
238
|
+
if self._fields_cache_key is None:
|
|
239
|
+
return (self.__class__,)
|
|
240
|
+
return (self.__class__, ("fields", self._fields_cache_key))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
ISCHEMA_NAMES = {
|
|
244
|
+
"bignum": sqltypes.Numeric,
|
|
245
|
+
"hugeint": HugeInteger,
|
|
246
|
+
"uhugeint": UHugeInteger,
|
|
247
|
+
"tinyint": TinyInteger,
|
|
248
|
+
"utinyint": UTinyInteger,
|
|
249
|
+
"int8": BigInteger,
|
|
250
|
+
"int4": Integer,
|
|
251
|
+
"int2": SmallInteger,
|
|
252
|
+
"timetz": sqltypes.TIME,
|
|
253
|
+
"timestamptz": sqltypes.TIMESTAMP,
|
|
254
|
+
"float4": sqltypes.FLOAT,
|
|
255
|
+
"float8": sqltypes.FLOAT,
|
|
256
|
+
"usmallint": USmallInteger,
|
|
257
|
+
"uinteger": UInteger,
|
|
258
|
+
"ubigint": UBigInteger,
|
|
259
|
+
"timestamp_s": sqltypes.TIMESTAMP,
|
|
260
|
+
"timestamp_ms": sqltypes.TIMESTAMP,
|
|
261
|
+
"timestamp_ns": sqltypes.TIMESTAMP,
|
|
262
|
+
"enum": sqltypes.Enum,
|
|
263
|
+
"bool": sqltypes.BOOLEAN,
|
|
264
|
+
"varchar": String,
|
|
265
|
+
}
|
|
266
|
+
if IS_GT_1:
|
|
267
|
+
ISCHEMA_NAMES["varint"] = VarInt
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def register_extension_types() -> None:
|
|
271
|
+
for subclass in types:
|
|
272
|
+
compiles(subclass, "duckdb")(compile_uint)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@compiles(Struct, "duckdb") # type: ignore[misc]
|
|
276
|
+
def visit_struct(
|
|
277
|
+
instance: Struct,
|
|
278
|
+
compiler: PGTypeCompiler,
|
|
279
|
+
identifier_preparer: PGIdentifierPreparer,
|
|
280
|
+
**kw: Any,
|
|
281
|
+
) -> str:
|
|
282
|
+
return "STRUCT" + struct_or_union(instance, compiler, identifier_preparer, **kw)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@compiles(Union, "duckdb") # type: ignore[misc]
|
|
286
|
+
def visit_union(
|
|
287
|
+
instance: Union,
|
|
288
|
+
compiler: PGTypeCompiler,
|
|
289
|
+
identifier_preparer: PGIdentifierPreparer,
|
|
290
|
+
**kw: Any,
|
|
291
|
+
) -> str:
|
|
292
|
+
return "UNION" + struct_or_union(instance, compiler, identifier_preparer, **kw)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def struct_or_union(
|
|
296
|
+
instance: typing.Union[Union, Struct],
|
|
297
|
+
compiler: PGTypeCompiler,
|
|
298
|
+
identifier_preparer: PGIdentifierPreparer,
|
|
299
|
+
**kw: Any,
|
|
300
|
+
) -> str:
|
|
301
|
+
fields = getattr(instance, "_fields", None)
|
|
302
|
+
if fields is None:
|
|
303
|
+
fields = instance.fields
|
|
304
|
+
if fields is None:
|
|
305
|
+
raise exc.CompileError(f"DuckDB {repr(instance)} type requires fields")
|
|
306
|
+
return "({})".format(
|
|
307
|
+
", ".join(
|
|
308
|
+
"{} {}".format(
|
|
309
|
+
identifier_preparer.quote_identifier(key),
|
|
310
|
+
process_type(
|
|
311
|
+
value, compiler, identifier_preparer=identifier_preparer, **kw
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
for key, value in (fields.items() if isinstance(fields, dict) else fields)
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def process_type(
|
|
320
|
+
value: typing.Union[TypeEngine, Type[TypeEngine]],
|
|
321
|
+
compiler: PGTypeCompiler,
|
|
322
|
+
**kw: Any,
|
|
323
|
+
) -> str:
|
|
324
|
+
return compiler.process(type_api.to_instance(value), **kw)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@compiles(Map, "duckdb") # type: ignore[misc]
|
|
328
|
+
def visit_map(instance: Map, compiler: PGTypeCompiler, **kw: Any) -> str:
|
|
329
|
+
return "MAP({}, {})".format(
|
|
330
|
+
process_type(instance.key_type, compiler, **kw),
|
|
331
|
+
process_type(instance.value_type, compiler, **kw),
|
|
332
|
+
)
|