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,474 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
from urllib.parse import parse_qs
|
|
3
|
+
|
|
4
|
+
import duckdb
|
|
5
|
+
import pytest
|
|
6
|
+
from sqlalchemy import Integer, String, pool
|
|
7
|
+
from sqlalchemy import exc as sa_exc
|
|
8
|
+
|
|
9
|
+
from duckdb_sqlalchemy import (
|
|
10
|
+
URL,
|
|
11
|
+
ConnectionWrapper,
|
|
12
|
+
CursorWrapper,
|
|
13
|
+
Dialect,
|
|
14
|
+
MotherDuckURL,
|
|
15
|
+
_apply_motherduck_defaults,
|
|
16
|
+
_looks_like_motherduck,
|
|
17
|
+
_normalize_motherduck_config,
|
|
18
|
+
_supports,
|
|
19
|
+
create_engine_from_paths,
|
|
20
|
+
olap,
|
|
21
|
+
stable_session_hint,
|
|
22
|
+
)
|
|
23
|
+
from duckdb_sqlalchemy import datatypes as dt
|
|
24
|
+
from duckdb_sqlalchemy.config import TYPES, apply_config, get_core_config
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cursor(conn: object) -> CursorWrapper:
|
|
28
|
+
wrapper = ConnectionWrapper(cast(duckdb.DuckDBPyConnection, conn))
|
|
29
|
+
return CursorWrapper(cast(duckdb.DuckDBPyConnection, conn), wrapper)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_url_coerces_types_and_overrides() -> None:
|
|
33
|
+
url = URL(
|
|
34
|
+
database=":memory:",
|
|
35
|
+
query={"access_mode": "read_only", "flag": True, "drop": None},
|
|
36
|
+
access_mode="read_write",
|
|
37
|
+
enabled=False,
|
|
38
|
+
list_val=[1, "two"],
|
|
39
|
+
tuple_val=("a", 2),
|
|
40
|
+
empty=None,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert url.query["access_mode"] == "read_write"
|
|
44
|
+
assert url.query["flag"] == "true"
|
|
45
|
+
assert url.query["enabled"] == "false"
|
|
46
|
+
assert url.query["list_val"] == ("1", "two")
|
|
47
|
+
assert url.query["tuple_val"] == ("a", "2")
|
|
48
|
+
assert "drop" not in url.query
|
|
49
|
+
assert "empty" not in url.query
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_create_connect_args_moves_user_query_param() -> None:
|
|
53
|
+
dialect = Dialect()
|
|
54
|
+
url = URL(
|
|
55
|
+
database="md:my_db",
|
|
56
|
+
query={
|
|
57
|
+
"user": "alice",
|
|
58
|
+
"session_hint": "hint",
|
|
59
|
+
"attach_mode": "single",
|
|
60
|
+
"cache_buster": "123",
|
|
61
|
+
"motherduck_dbinstance_inactivity_ttl": "15m",
|
|
62
|
+
"memory_limit": "1GB",
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
args, kwargs = dialect.create_connect_args(url)
|
|
67
|
+
|
|
68
|
+
assert args == ()
|
|
69
|
+
database, query = kwargs["database"].split("?", 1)
|
|
70
|
+
assert database == "md:my_db"
|
|
71
|
+
assert parse_qs(query) == {
|
|
72
|
+
"user": ["alice"],
|
|
73
|
+
"session_hint": ["hint"],
|
|
74
|
+
"attach_mode": ["single"],
|
|
75
|
+
"cache_buster": ["123"],
|
|
76
|
+
"dbinstance_inactivity_ttl": ["15m"],
|
|
77
|
+
}
|
|
78
|
+
assert kwargs["url_config"] == {"memory_limit": "1GB"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_create_connect_args_strips_pool_override() -> None:
|
|
82
|
+
dialect = Dialect()
|
|
83
|
+
url = URL(
|
|
84
|
+
database=":memory:",
|
|
85
|
+
query={"duckdb_sqlalchemy_pool": "queue", "threads": "4"},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
_, kwargs = dialect.create_connect_args(url)
|
|
89
|
+
|
|
90
|
+
assert kwargs["url_config"] == {"threads": "4"}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_pool_override_from_url() -> None:
|
|
94
|
+
url = URL(database="md:my_db", query={"duckdb_sqlalchemy_pool": "queue"})
|
|
95
|
+
assert Dialect.get_pool_class(url) is pool.QueuePool
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_create_engine_from_paths_requires_paths() -> None:
|
|
99
|
+
with pytest.raises(ValueError):
|
|
100
|
+
create_engine_from_paths([])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_is_disconnect_matches_patterns() -> None:
|
|
104
|
+
dialect = Dialect()
|
|
105
|
+
assert dialect.is_disconnect(RuntimeError("connection closed"), None, None)
|
|
106
|
+
assert not dialect.is_disconnect(RuntimeError("syntax error"), None, None)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_retry_on_transient_select() -> None:
|
|
110
|
+
dialect = Dialect()
|
|
111
|
+
|
|
112
|
+
class DummyCursor:
|
|
113
|
+
def __init__(self) -> None:
|
|
114
|
+
self.calls = 0
|
|
115
|
+
|
|
116
|
+
def execute(self, statement, parameters=None):
|
|
117
|
+
self.calls += 1
|
|
118
|
+
if self.calls == 1:
|
|
119
|
+
raise RuntimeError("HTTP Error: 503 Service Unavailable")
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
class DummyContext:
|
|
123
|
+
execution_options = {
|
|
124
|
+
"duckdb_retry_on_transient": True,
|
|
125
|
+
"duckdb_retry_count": 1,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
cursor = DummyCursor()
|
|
129
|
+
dialect.do_execute(cursor, "select 1", (), context=DummyContext())
|
|
130
|
+
assert cursor.calls == 2
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_motherduck_url_builder_moves_path_params() -> None:
|
|
134
|
+
url = MotherDuckURL(
|
|
135
|
+
database="md:my_db",
|
|
136
|
+
session_hint="team-a",
|
|
137
|
+
attach_mode="single",
|
|
138
|
+
motherduck_dbinstance_inactivity_ttl="15m",
|
|
139
|
+
query={"memory_limit": "1GB"},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
database, query = url.database.split("?", 1)
|
|
143
|
+
assert database == "md:my_db"
|
|
144
|
+
assert parse_qs(query) == {
|
|
145
|
+
"session_hint": ["team-a"],
|
|
146
|
+
"attach_mode": ["single"],
|
|
147
|
+
"dbinstance_inactivity_ttl": ["15m"],
|
|
148
|
+
}
|
|
149
|
+
assert url.query == {"memory_limit": "1GB"}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_stable_session_hint_is_deterministic() -> None:
|
|
153
|
+
hint1 = stable_session_hint("user-123", salt="salt", length=8)
|
|
154
|
+
hint2 = stable_session_hint("user-123", salt="salt", length=8)
|
|
155
|
+
assert hint1 == hint2
|
|
156
|
+
assert len(hint1) == 8
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_apply_config_uses_literal_processors() -> None:
|
|
160
|
+
dialect = Dialect()
|
|
161
|
+
|
|
162
|
+
class DummyConn:
|
|
163
|
+
def __init__(self) -> None:
|
|
164
|
+
self.executed = []
|
|
165
|
+
|
|
166
|
+
def execute(self, statement: str) -> None:
|
|
167
|
+
self.executed.append(statement)
|
|
168
|
+
|
|
169
|
+
conn = DummyConn()
|
|
170
|
+
ext = {"memory_limit": "1GB", "threads": 4, "enable_profiling": True}
|
|
171
|
+
|
|
172
|
+
apply_config(dialect, conn, ext)
|
|
173
|
+
|
|
174
|
+
processors = {k: v.literal_processor(dialect=dialect) for k, v in TYPES.items()}
|
|
175
|
+
expected = [
|
|
176
|
+
f"SET {key} = {processors[type(value)](value)}" for key, value in ext.items()
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
assert conn.executed == expected
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_get_core_config_includes_motherduck_keys() -> None:
|
|
183
|
+
core = get_core_config()
|
|
184
|
+
|
|
185
|
+
expected = {
|
|
186
|
+
"motherduck_token",
|
|
187
|
+
"attach_mode",
|
|
188
|
+
"saas_mode",
|
|
189
|
+
"session_hint",
|
|
190
|
+
"access_mode",
|
|
191
|
+
"dbinstance_inactivity_ttl",
|
|
192
|
+
"motherduck_dbinstance_inactivity_ttl",
|
|
193
|
+
}
|
|
194
|
+
assert expected.issubset(core)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_looks_like_motherduck_detection() -> None:
|
|
198
|
+
assert _looks_like_motherduck("md:db", {}) is True
|
|
199
|
+
assert _looks_like_motherduck("motherduck:db", {}) is True
|
|
200
|
+
assert _looks_like_motherduck("local.db", {"motherduck_token": "x"}) is True
|
|
201
|
+
assert _looks_like_motherduck("local.db", {}) is False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_apply_motherduck_defaults_env_token(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
205
|
+
config = {}
|
|
206
|
+
monkeypatch.setenv("MOTHERDUCK_TOKEN", "token123")
|
|
207
|
+
monkeypatch.delenv("motherduck_token", raising=False)
|
|
208
|
+
|
|
209
|
+
_apply_motherduck_defaults(config, "md:my_db")
|
|
210
|
+
|
|
211
|
+
assert config["motherduck_token"] == "token123"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_apply_motherduck_defaults_skips_non_md(
|
|
215
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
216
|
+
) -> None:
|
|
217
|
+
config = {}
|
|
218
|
+
monkeypatch.setenv("MOTHERDUCK_TOKEN", "token123")
|
|
219
|
+
monkeypatch.delenv("motherduck_token", raising=False)
|
|
220
|
+
|
|
221
|
+
_apply_motherduck_defaults(config, "local.db")
|
|
222
|
+
|
|
223
|
+
assert "motherduck_token" not in config
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_apply_motherduck_defaults_requires_string() -> None:
|
|
227
|
+
with pytest.raises(TypeError):
|
|
228
|
+
_apply_motherduck_defaults({"motherduck_token": 123}, "md:db")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_normalize_motherduck_config_alias() -> None:
|
|
232
|
+
config = {"dbinstance_inactivity_ttl": "1h"}
|
|
233
|
+
_normalize_motherduck_config(config)
|
|
234
|
+
assert config["motherduck_dbinstance_inactivity_ttl"] == "1h"
|
|
235
|
+
|
|
236
|
+
config = {
|
|
237
|
+
"dbinstance_inactivity_ttl": "1h",
|
|
238
|
+
"motherduck_dbinstance_inactivity_ttl": "2h",
|
|
239
|
+
}
|
|
240
|
+
_normalize_motherduck_config(config)
|
|
241
|
+
assert config["motherduck_dbinstance_inactivity_ttl"] == "2h"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_has_comment_support_false_on_parser_exception(
|
|
245
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
246
|
+
) -> None:
|
|
247
|
+
_supports.has_comment_support.cache_clear()
|
|
248
|
+
|
|
249
|
+
class DummyConn:
|
|
250
|
+
def __enter__(self):
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def __exit__(self, exc_type, exc, tb):
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def execute(self, *args, **kwargs):
|
|
257
|
+
raise _supports.duckdb.ParserException("nope")
|
|
258
|
+
|
|
259
|
+
monkeypatch.setattr(_supports.duckdb, "connect", lambda *_: DummyConn())
|
|
260
|
+
|
|
261
|
+
assert _supports.has_comment_support() is False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_has_comment_support_true_when_no_error(
|
|
265
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
266
|
+
) -> None:
|
|
267
|
+
_supports.has_comment_support.cache_clear()
|
|
268
|
+
|
|
269
|
+
class DummyConn:
|
|
270
|
+
def __enter__(self):
|
|
271
|
+
return self
|
|
272
|
+
|
|
273
|
+
def __exit__(self, exc_type, exc, tb):
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
def execute(self, *args, **kwargs):
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
monkeypatch.setattr(_supports.duckdb, "connect", lambda *_: DummyConn())
|
|
280
|
+
|
|
281
|
+
assert _supports.has_comment_support() is True
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_identifier_preparer_separate_and_format_schema() -> None:
|
|
285
|
+
preparer = Dialect().identifier_preparer
|
|
286
|
+
|
|
287
|
+
assert preparer._separate("db.schema") == ("db", "schema")
|
|
288
|
+
assert preparer._separate('"my db"."my schema"') == ("my db", "my schema")
|
|
289
|
+
assert preparer._separate("schema") == (None, "schema")
|
|
290
|
+
|
|
291
|
+
formatted = preparer.format_schema('"my db".main')
|
|
292
|
+
assert formatted == '"my db".main'
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_table_function_error_without_table_valued(
|
|
296
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
297
|
+
) -> None:
|
|
298
|
+
class DummyFunc:
|
|
299
|
+
def __getattr__(self, name):
|
|
300
|
+
def _fn(*args, **kwargs):
|
|
301
|
+
return object()
|
|
302
|
+
|
|
303
|
+
return _fn
|
|
304
|
+
|
|
305
|
+
monkeypatch.setattr(olap, "func", DummyFunc())
|
|
306
|
+
|
|
307
|
+
with pytest.raises(NotImplementedError):
|
|
308
|
+
olap.table_function("read_parquet", "path", columns=["col"])
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_table_function_returns_function_call(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
312
|
+
called = {}
|
|
313
|
+
|
|
314
|
+
class DummyFunc:
|
|
315
|
+
def __getattr__(self, name):
|
|
316
|
+
def _fn(*args, **kwargs):
|
|
317
|
+
called["name"] = name
|
|
318
|
+
called["args"] = args
|
|
319
|
+
called["kwargs"] = kwargs
|
|
320
|
+
return "ok"
|
|
321
|
+
|
|
322
|
+
return _fn
|
|
323
|
+
|
|
324
|
+
monkeypatch.setattr(olap, "func", DummyFunc())
|
|
325
|
+
|
|
326
|
+
result = olap.table_function("read_csv", "file.csv", header=True)
|
|
327
|
+
|
|
328
|
+
assert result == "ok"
|
|
329
|
+
assert called["name"] == "read_csv"
|
|
330
|
+
assert called["args"] == ("file.csv",)
|
|
331
|
+
assert called["kwargs"] == {"header": True}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_cursorwrapper_execute_basic_paths() -> None:
|
|
335
|
+
class DummyConn:
|
|
336
|
+
def __init__(self) -> None:
|
|
337
|
+
self.calls = []
|
|
338
|
+
|
|
339
|
+
def commit(self) -> None:
|
|
340
|
+
self.calls.append(("commit",))
|
|
341
|
+
|
|
342
|
+
def register(self, name, df) -> None:
|
|
343
|
+
self.calls.append(("register", name, df))
|
|
344
|
+
|
|
345
|
+
def execute(self, *args):
|
|
346
|
+
self.calls.append(("execute", args))
|
|
347
|
+
|
|
348
|
+
conn = DummyConn()
|
|
349
|
+
cursor = _cursor(conn)
|
|
350
|
+
df = object()
|
|
351
|
+
|
|
352
|
+
cursor.execute("COMMIT")
|
|
353
|
+
cursor.execute("register", ("view", df))
|
|
354
|
+
cursor.execute("select 1")
|
|
355
|
+
cursor.execute("select ?", (1,))
|
|
356
|
+
|
|
357
|
+
assert ("commit",) in conn.calls
|
|
358
|
+
assert ("register", "view", df) in conn.calls
|
|
359
|
+
assert ("execute", ("select 1",)) in conn.calls
|
|
360
|
+
assert ("execute", ("select ?", (1,))) in conn.calls
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_cursorwrapper_execute_show_isolation_level() -> None:
|
|
364
|
+
class DummyConn:
|
|
365
|
+
def __init__(self) -> None:
|
|
366
|
+
self.calls = []
|
|
367
|
+
|
|
368
|
+
def execute(self, statement: str) -> None:
|
|
369
|
+
self.calls.append(statement)
|
|
370
|
+
|
|
371
|
+
conn = DummyConn()
|
|
372
|
+
cursor = _cursor(conn)
|
|
373
|
+
|
|
374
|
+
cursor.execute("show transaction isolation level")
|
|
375
|
+
|
|
376
|
+
assert conn.calls == ["select 'read committed' as transaction_isolation"]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_cursorwrapper_executemany_coerces_to_list() -> None:
|
|
380
|
+
class DummyConn:
|
|
381
|
+
def __init__(self) -> None:
|
|
382
|
+
self.calls = []
|
|
383
|
+
|
|
384
|
+
def executemany(self, *args):
|
|
385
|
+
self.calls.append(args)
|
|
386
|
+
|
|
387
|
+
conn = DummyConn()
|
|
388
|
+
cursor = _cursor(conn)
|
|
389
|
+
params = ({"a": 1}, {"a": 2})
|
|
390
|
+
|
|
391
|
+
cursor.executemany("insert", params) # type: ignore[arg-type]
|
|
392
|
+
|
|
393
|
+
assert conn.calls[0][0] == "insert"
|
|
394
|
+
assert conn.calls[0][1] == list(params)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_cursorwrapper_execute_handles_specific_runtime_errors() -> None:
|
|
398
|
+
class CommitConn:
|
|
399
|
+
def commit(self) -> None:
|
|
400
|
+
raise RuntimeError(
|
|
401
|
+
"TransactionContext Error: cannot commit - no transaction is active"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
cursor = _cursor(CommitConn())
|
|
405
|
+
cursor.execute("commit")
|
|
406
|
+
|
|
407
|
+
class NotImplementedConn:
|
|
408
|
+
def execute(self, *args, **kwargs):
|
|
409
|
+
raise RuntimeError("Not implemented Error: nope")
|
|
410
|
+
|
|
411
|
+
cursor = _cursor(NotImplementedConn())
|
|
412
|
+
with pytest.raises(NotImplementedError):
|
|
413
|
+
cursor.execute("select 1")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_cursorwrapper_description_handles_unhashable_type_code() -> None:
|
|
417
|
+
class DummyConn:
|
|
418
|
+
description = [("col", ["complex"])]
|
|
419
|
+
|
|
420
|
+
cursor = _cursor(DummyConn())
|
|
421
|
+
|
|
422
|
+
assert cursor.description == [("col", "['complex']")]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_connectionwrapper_close_marks_closed() -> None:
|
|
426
|
+
class DummyConn:
|
|
427
|
+
def __init__(self) -> None:
|
|
428
|
+
self.closed = False
|
|
429
|
+
|
|
430
|
+
def close(self) -> None:
|
|
431
|
+
self.closed = True
|
|
432
|
+
|
|
433
|
+
conn = DummyConn()
|
|
434
|
+
wrapper = ConnectionWrapper(cast(duckdb.DuckDBPyConnection, conn))
|
|
435
|
+
assert wrapper.closed is False
|
|
436
|
+
|
|
437
|
+
wrapper.close()
|
|
438
|
+
|
|
439
|
+
assert wrapper.closed is True
|
|
440
|
+
assert conn.closed is True
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def test_map_processors(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
444
|
+
map_type = dt.Map(String, Integer)
|
|
445
|
+
bind = map_type.bind_processor(Dialect())
|
|
446
|
+
|
|
447
|
+
assert bind({"a": 1, "b": 2}) == {"key": ["a", "b"], "value": [1, 2]}
|
|
448
|
+
assert bind(None) is None
|
|
449
|
+
|
|
450
|
+
result = map_type.result_processor(Dialect(), None)({"key": ["a"], "value": [1]})
|
|
451
|
+
if dt.IS_GT_1:
|
|
452
|
+
assert result == {"key": ["a"], "value": [1]}
|
|
453
|
+
else:
|
|
454
|
+
assert result == {"a": 1}
|
|
455
|
+
|
|
456
|
+
monkeypatch.setattr(dt, "IS_GT_1", False)
|
|
457
|
+
legacy = dt.Map(String, Integer).result_processor(Dialect(), None)
|
|
458
|
+
assert legacy({"key": ["a"], "value": [1]}) == {"a": 1}
|
|
459
|
+
assert legacy(None) == {}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_struct_or_union_requires_fields() -> None:
|
|
463
|
+
dialect = Dialect()
|
|
464
|
+
compiler = dialect.type_compiler
|
|
465
|
+
preparer = dialect.identifier_preparer
|
|
466
|
+
|
|
467
|
+
with pytest.raises(sa_exc.CompileError):
|
|
468
|
+
dt.struct_or_union(dt.Struct(), compiler, preparer)
|
|
469
|
+
|
|
470
|
+
struct = dt.Struct({"first name": String, "age": Integer})
|
|
471
|
+
rendered = dt.struct_or_union(struct, compiler, preparer)
|
|
472
|
+
assert rendered.startswith("(")
|
|
473
|
+
assert rendered.endswith(")")
|
|
474
|
+
assert '"first name"' in rendered
|