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.
@@ -0,0 +1,718 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ import zlib
5
+ from datetime import datetime, timedelta
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Generic, Optional, Tuple, TypeVar, cast
8
+
9
+ import duckdb
10
+ import fsspec
11
+ import sqlalchemy
12
+ from hypothesis import assume, given, settings
13
+ from hypothesis.strategies import text as text_strat
14
+ from packaging.version import Version
15
+ from pytest import LogCaptureFixture, fixture, importorskip, mark, raises
16
+ from sqlalchemy import (
17
+ Column,
18
+ DateTime,
19
+ ForeignKey,
20
+ Integer,
21
+ Interval,
22
+ MetaData,
23
+ Sequence,
24
+ String,
25
+ Table,
26
+ column,
27
+ create_engine,
28
+ func,
29
+ inspect,
30
+ select,
31
+ text,
32
+ types,
33
+ )
34
+ from sqlalchemy.dialects import registry # type: ignore
35
+ from sqlalchemy.engine import Connection, Engine
36
+ from sqlalchemy.engine.reflection import Inspector
37
+ from sqlalchemy.exc import DBAPIError
38
+ from sqlalchemy.ext.declarative import declarative_base
39
+ from sqlalchemy.orm import Session, relationship, sessionmaker
40
+
41
+ from .. import Dialect, insert, supports_attach, supports_user_agent
42
+ from .._supports import has_comment_support
43
+
44
+ try:
45
+ # sqlalchemy 2
46
+ from sqlalchemy.engine import ObjectKind # type: ignore[attr-defined]
47
+ from sqlalchemy.orm import Mapped
48
+ except ImportError:
49
+ # sqlalchemy 1
50
+ T = TypeVar("T")
51
+
52
+ class Mapped(Generic[T]): # type: ignore[no-redef]
53
+ pass
54
+
55
+
56
+ @fixture
57
+ def engine() -> Engine:
58
+ registry.register("duckdb", "duckdb_sqlalchemy", "Dialect")
59
+
60
+ eng = create_engine("duckdb:///:memory:")
61
+ Base.metadata.create_all(eng)
62
+ return eng
63
+
64
+
65
+ Base = declarative_base()
66
+
67
+
68
+ class CompressedString(types.TypeDecorator):
69
+ """Custom Column Type"""
70
+
71
+ impl = types.BLOB
72
+
73
+ def process_bind_param(self, value: Optional[str], dialect: Any) -> Optional[bytes]:
74
+ if value is None:
75
+ return None
76
+ return zlib.compress(value.encode("utf-8"), level=9)
77
+
78
+ def process_result_value(
79
+ self, value: Optional[bytes], dialect: Any
80
+ ) -> Optional[str]:
81
+ if value is None:
82
+ return None
83
+ return zlib.decompress(value).decode("utf-8")
84
+
85
+
86
+ class TableWithBinary(Base):
87
+ __tablename__ = "table_with_binary"
88
+
89
+ id = Column(Integer(), Sequence("id_seq"), primary_key=True)
90
+
91
+ text = Column(CompressedString())
92
+
93
+
94
+ class FakeModel(Base):
95
+ __tablename__ = "fake"
96
+
97
+ id = Column(Integer, Sequence("fakemodel_id_sequence"), primary_key=True)
98
+ name = Column(String)
99
+
100
+ owner: Mapped["Owner"] = relationship("Owner")
101
+
102
+
103
+ class Owner(Base):
104
+ __tablename__ = "owner"
105
+ id = Column(Integer, Sequence("owner_id"), primary_key=True)
106
+
107
+ fake_id = Column(Integer, ForeignKey("fake.id"))
108
+ owned: Mapped[FakeModel] = relationship(FakeModel, back_populates="owner")
109
+
110
+
111
+ class IntervalModel(Base):
112
+ __tablename__ = "IntervalModel"
113
+
114
+ id = Column(Integer, Sequence("IntervalModel_id_sequence"), primary_key=True)
115
+
116
+ field = Column(Interval)
117
+
118
+
119
+ @fixture
120
+ def session(engine: Engine) -> Session:
121
+ return sessionmaker(bind=engine)()
122
+
123
+
124
+ def test_basic(session: Session) -> None:
125
+ session.add(FakeModel(name="Frank"))
126
+ session.commit()
127
+
128
+ frank = session.query(FakeModel).one() # act
129
+
130
+ assert frank.name == "Frank"
131
+
132
+
133
+ def test_foreign(session: Session) -> None:
134
+ model = FakeModel(name="Walter")
135
+ session.add(model)
136
+ session.add(Owner(owned=model))
137
+ session.commit()
138
+
139
+ owner = session.query(Owner).one() # act
140
+
141
+ owned = cast(FakeModel, owner.owned)
142
+ assert owned.name == "Walter"
143
+
144
+
145
+ def test_disabled_server_side_cursors(engine: Engine) -> None:
146
+ connection = engine.connect().execution_options(stream_results=True)
147
+
148
+ session = sessionmaker(bind=connection)()
149
+
150
+ session.add(FakeModel(name="Walter"))
151
+ session.commit()
152
+
153
+ assert list(session.query(FakeModel).yield_per(1))
154
+
155
+
156
+ @given(text_strat())
157
+ @settings(deadline=timedelta(seconds=1))
158
+ def test_simple_string(s: str) -> None:
159
+ assume("\x00" not in s)
160
+ eng = create_engine("duckdb:///:memory:")
161
+ Base.metadata.create_all(eng)
162
+ session = sessionmaker(bind=eng)()
163
+ model = FakeModel(name=s)
164
+ session.add(model)
165
+ session.add(Owner(owned=model))
166
+ session.commit()
167
+
168
+ owner = session.query(Owner).one() # act
169
+
170
+ owned = cast(FakeModel, owner.owned)
171
+ assert owned.name == s
172
+
173
+
174
+ def test_get_tables(inspector: Inspector) -> None:
175
+ for table_name in inspector.get_table_names():
176
+ assert inspector.has_table(table_name)
177
+ assert inspector.get_view_names() == []
178
+
179
+
180
+ @mark.skipif(
181
+ supports_attach is False,
182
+ reason="ATTACH is not supported for DuckDB version < 0.7.0",
183
+ )
184
+ def test_get_schema_names(inspector: Inspector, session: Session) -> None:
185
+ # Using multi-line strings because of all the single and double quotes flying around...
186
+ cmds = [
187
+ """CREATE SCHEMA "quack quack" """,
188
+ """CREATE SCHEMA "daffy duck"."you're "" despicable" """,
189
+ ]
190
+ for cmd in cmds:
191
+ session.execute(text(cmd))
192
+ session.commit()
193
+
194
+ # Deliberately excluding pg_catalog schema (to align with Postgres)
195
+ names = set(inspector.get_schema_names())
196
+ expected = {
197
+ '"daffy duck".main',
198
+ '"daffy duck"."quack quack"',
199
+ '"daffy duck"."you\'re "" despicable"',
200
+ "memory.main",
201
+ 'memory."quack quack"',
202
+ "system.information_schema",
203
+ "system.main",
204
+ "temp.main",
205
+ }
206
+ if Version(duckdb.__version__) <= Version("1.1.3"):
207
+ expected.update(
208
+ {
209
+ '"daffy duck".information_schema',
210
+ "memory.information_schema",
211
+ "temp.information_schema",
212
+ }
213
+ )
214
+ assert names == expected
215
+
216
+
217
+ @mark.skipif(
218
+ supports_attach is False,
219
+ reason="ATTACH is not supported for DuckDB version < 0.7.0",
220
+ )
221
+ def test_get_table_names(inspector: Inspector, session: Session) -> None:
222
+ # Using multi-line strings because of all the single and double quotes flying around...
223
+ cmds = [
224
+ """CREATE TABLE "daffy duck"."quack quack"."t2" (i INTEGER, j INTEGER);""",
225
+ """CREATE TABLE "t3" (i INTEGER, j INTEGER);""",
226
+ """CREATE SCHEMA "porky" """,
227
+ """CREATE TABLE "porky"."t4" (i INTEGER, j INTEGER);""",
228
+ ]
229
+ for cmd in cmds:
230
+ session.execute(text(cmd))
231
+ session.commit()
232
+
233
+ for schema, table_names in zip(
234
+ ['"daffy duck"."quack quack"', "main", "porky"], [["t1", "t2"], ["t3"], ["t4"]]
235
+ ):
236
+ _table_names = inspector.get_table_names(schema=schema)
237
+ assert set(_table_names).issuperset(set(table_names))
238
+ for _table_name in _table_names:
239
+ assert inspector.has_table(_table_name, schema)
240
+
241
+ table_names_all = inspector.get_table_names()
242
+ assert set(table_names_all).issuperset({"t1", "t2", "t3", "t4"})
243
+ for table_name in table_names_all:
244
+ assert inspector.has_table(table_name)
245
+
246
+
247
+ def test_get_views(conn: Connection, dialect: Dialect) -> None:
248
+ views = dialect.get_view_names(conn)
249
+ assert views == []
250
+
251
+ conn.execute(text("create view test as select 1"))
252
+ conn.execute(
253
+ text("create schema scheme; create view scheme.schema_test as select 1")
254
+ )
255
+
256
+ views = dialect.get_view_names(conn)
257
+ assert views == ["test"]
258
+
259
+ views = dialect.get_view_names(conn, schema="scheme")
260
+ assert views == ["schema_test"]
261
+
262
+ views = dialect.get_view_names(conn, schema="memory.scheme")
263
+ assert views == ["schema_test"]
264
+
265
+ assert dialect.has_table(conn, table_name="test")
266
+ assert dialect.has_table(conn, table_name="schema_test", schema="scheme")
267
+
268
+
269
+ @mark.skipif(os.uname().machine == "aarch64", reason="not supported on aarch64")
270
+ @mark.remote_data
271
+ def test_preload_extension() -> None:
272
+ duckdb.connect().execute("INSTALL httpfs")
273
+ engine = create_engine(
274
+ "duckdb:///",
275
+ connect_args={
276
+ "preload_extensions": ["httpfs"],
277
+ "config": {"s3_region": "ap-southeast-2", "s3_use_ssl": True},
278
+ },
279
+ )
280
+
281
+ # check that we get an error indicating that the extension was loaded
282
+ with engine.connect() as conn, raises(Exception, match="HTTP HEAD"):
283
+ conn.execute(
284
+ text("SELECT * FROM read_parquet('https://domain/path/to/file.parquet');")
285
+ )
286
+
287
+
288
+ @fixture
289
+ def inspector(engine: Engine, session: Session) -> Inspector:
290
+ cmds = [
291
+ """CREATE TABLE test (id INTEGER);""" """ATTACH ':memory:' AS "daffy duck" """,
292
+ """CREATE SCHEMA "daffy duck"."quack quack" """,
293
+ """CREATE TABLE "daffy duck"."quack quack"."t1" (i INTEGER, j INTEGER);""",
294
+ ]
295
+ for cmd in cmds:
296
+ session.execute(text(cmd))
297
+ session.commit()
298
+
299
+ meta = MetaData()
300
+ Table("test", meta)
301
+
302
+ return inspect(engine)
303
+
304
+
305
+ def test_get_columns(inspector: Inspector, session: Session) -> None:
306
+ cols = inspector.get_columns("test", None)
307
+ assert len(cols) == 1
308
+ assert cols[0]["name"] == "id"
309
+ assert inspector.has_table("t1", '"daffy duck"."quack quack"')
310
+ cols1 = inspector.get_columns("t1", None)
311
+ cols2 = inspector.get_columns("t1", '"daffy duck"."quack quack"')
312
+ cols3 = inspector.get_columns("t1", "daffy duck.quack quack")
313
+ assert len(cols1) == 2
314
+ assert cols1[0]["name"] == "i"
315
+ assert cols1[1]["name"] == "j"
316
+ assert cols1[0]["name"] == cols2[0]["name"] == cols3[0]["name"]
317
+ assert cols1[1]["name"] == cols2[1]["name"] == cols3[1]["name"]
318
+
319
+
320
+ def test_get_foreign_keys(inspector: Inspector) -> None:
321
+ assert inspector.get_foreign_keys("test", None) == []
322
+ assert inspector.get_foreign_keys("t1", '"daffy duck"."quack quack"') == []
323
+
324
+
325
+ @mark.skipif(
326
+ Version(sqlalchemy.__version__) < Version("2.0.0"),
327
+ reason="2-arg pg_getconstraintdef not yet supported in duckdb",
328
+ )
329
+ def test_get_check_constraints(inspector: Inspector) -> None:
330
+ assert inspector.get_check_constraints("test", None) == []
331
+ assert inspector.get_check_constraints("t1", '"daffy duck"."quack quack"') == []
332
+
333
+
334
+ def test_get_unique_constraints(inspector: Inspector) -> None:
335
+ inspector.get_unique_constraints("test", None)
336
+ inspector.get_unique_constraints("t1", '"daffy duck"."quack quack"')
337
+
338
+
339
+ def test_reflect(session: Session, engine: Engine) -> None:
340
+ session.execute(text("create table test (id int);"))
341
+ session.commit()
342
+
343
+ meta = MetaData()
344
+ meta.reflect(only=["test"], bind=engine)
345
+
346
+
347
+ def test_get_multi_columns(engine: Engine) -> None:
348
+ importorskip("sqlalchemy", "2.0.0-rc1")
349
+ with engine.connect() as conn:
350
+ assert cast(Dialect, engine.dialect).get_multi_columns(
351
+ connection=conn,
352
+ schema=None,
353
+ filter_names=set(),
354
+ scope=None,
355
+ kind=cast(Tuple[str, ...], (ObjectKind.TABLE,)),
356
+ )
357
+
358
+
359
+ def test_commit(session: Session, engine: Engine) -> None:
360
+ importorskip("sqlalchemy", "1.4.0")
361
+ session.execute(text("commit;"))
362
+
363
+ InteractiveShell = importorskip("IPython.core.interactiveshell").InteractiveShell
364
+
365
+ shell = InteractiveShell()
366
+ assert not shell.run_line_magic("load_ext", "sql")
367
+ assert not shell.run_line_magic("sql", "duckdb:///:memory:")
368
+ assert shell.run_line_magic("sql", "select 42;") == [(42,)]
369
+
370
+
371
+ def test_table_reflect(session: Session, engine: Engine) -> None:
372
+ session.execute(text("create table test (id int);"))
373
+ session.commit()
374
+
375
+ meta = MetaData()
376
+ user_table = Table("test", meta)
377
+ insp = inspect(engine)
378
+
379
+ reflect_table = (
380
+ insp.reflecttable if hasattr(insp, "reflecttable") else insp.reflect_table
381
+ )
382
+ reflect_table = cast(Callable[..., Any], reflect_table)
383
+ reflect_table(user_table, None)
384
+
385
+
386
+ def test_fetch_df_chunks() -> None:
387
+ import duckdb
388
+
389
+ duckdb.connect(":memory:").execute("select 1").fetch_df_chunk(1)
390
+
391
+
392
+ def test_fetchmany(engine: Engine) -> None:
393
+ with engine.connect() as conn:
394
+ res = conn.execute(text("select 1"))
395
+ assert res.fetchmany(1) == [(1,)]
396
+
397
+
398
+ def test_description() -> None:
399
+ import duckdb
400
+
401
+ duckdb.connect("").description
402
+
403
+
404
+ def test_intervals(session: Session) -> None:
405
+ session.add(IntervalModel(field=timedelta(days=1)))
406
+ session.commit()
407
+
408
+ owner = session.query(IntervalModel).one() # act
409
+
410
+ assert owner.field == timedelta(days=1)
411
+
412
+
413
+ def test_binary(session: Session) -> None:
414
+ a = TableWithBinary(text="Hello World!")
415
+ session.add(a)
416
+ session.commit()
417
+
418
+ b: TableWithBinary = session.query(TableWithBinary).one()
419
+ assert b.text == "Hello World!"
420
+
421
+
422
+ @mark.skipif(
423
+ not has_comment_support(), reason="comments not supported by duckdb version"
424
+ )
425
+ def test_comment_support(engine: Engine) -> None:
426
+ "comments not yet supported by duckdb"
427
+
428
+ class Notice(Base):
429
+ __tablename__ = "tb_notice"
430
+ __table_args__ = {"comment": "Notice table"}
431
+
432
+ seqno = Column(
433
+ Integer,
434
+ Sequence("seqno_sequence"),
435
+ primary_key=True,
436
+ comment="Integer representing the sequence number",
437
+ )
438
+ title = Column(
439
+ String(200),
440
+ nullable=False,
441
+ comment="Title of the notice, represented as a string",
442
+ )
443
+
444
+ Base.metadata.create_all(bind=engine)
445
+
446
+ inspector = inspect(engine)
447
+
448
+ assert inspector.get_table_comment("tb_notice")["text"] == "Notice table"
449
+
450
+ columns = inspector.get_columns("tb_notice", None)
451
+ assert columns[0]["comment"] == "Integer representing the sequence number"
452
+ assert columns[1]["comment"] == "Title of the notice, represented as a string"
453
+
454
+
455
+ def test_rowcount() -> None:
456
+ import duckdb
457
+
458
+ assert duckdb.connect().rowcount == -1
459
+
460
+
461
+ def test_sessions(session: Session) -> None:
462
+ c = IntervalModel(field=timedelta(seconds=5))
463
+ session.add(c)
464
+ session.commit()
465
+
466
+ c2 = session.query(IntervalModel).get(1)
467
+ assert c2
468
+ c2.field = timedelta(days=5)
469
+ session.flush()
470
+ session.commit()
471
+
472
+
473
+ def test_inmemory() -> None:
474
+ InteractiveShell = importorskip("IPython.core.interactiveshell").InteractiveShell
475
+
476
+ shell = InteractiveShell()
477
+ shell.run_cell("""import sqlalchemy as sa""")
478
+ shell.run_cell("""eng = sa.create_engine("duckdb:///:memory:")""")
479
+ shell.run_cell("""conn = eng.connect()""")
480
+ shell.run_cell("""conn.execute(sa.text("CREATE TABLE t (x int)"))""")
481
+ res = shell.run_cell("""conn.execute(sa.text("SHOW TABLES")).fetchall()""")
482
+
483
+ assert res.result == [("t",)]
484
+
485
+
486
+ def test_config(tmp_path: Path) -> None:
487
+ db_path = tmp_path / "test.db"
488
+
489
+ db = duckdb.connect(str(db_path))
490
+ db.execute("create table hello1 (i int)")
491
+ db.close()
492
+
493
+ eng = create_engine(
494
+ f"duckdb:///{db_path}",
495
+ connect_args={"read_only": True, "config": {"memory_limit": "500mb"}},
496
+ )
497
+
498
+ with raises(
499
+ DBAPIError,
500
+ match='Cannot execute statement of type "CREATE" (on database "test" which is attached )?in read-only mode!',
501
+ ):
502
+ with eng.connect() as conn:
503
+ conn.execute(text("create table hello2 (i int)"))
504
+
505
+
506
+ def test_url_config() -> None:
507
+ eng = create_engine("duckdb:///:memory:?worker_threads=123")
508
+
509
+ with eng.connect() as conn:
510
+ res = conn.execute(text("select current_setting('worker_threads')"))
511
+ row = res.first()
512
+ assert row is not None
513
+ assert row[0] == 123
514
+
515
+
516
+ def test_url_config_and_dict_config() -> None:
517
+ eng = create_engine(
518
+ "duckdb:///:memory:?worker_threads=123",
519
+ connect_args={"config": {"memory_limit": "500mb"}},
520
+ )
521
+
522
+ with eng.connect() as conn:
523
+ res = conn.execute(
524
+ text(
525
+ "select current_setting('worker_threads'), current_setting('memory_limit')"
526
+ )
527
+ )
528
+ row = res.first()
529
+ assert row is not None
530
+ worker_threads, memory_limit = row
531
+ assert worker_threads == 123
532
+ assert memory_limit in ("500.0MB", "476.8 MiB")
533
+
534
+
535
+ user_agent_re = r"duckdb/.*(.*) python(/.*)? duckdb-sqlalchemy/.*(sqlalchemy/.*)"
536
+
537
+
538
+ @mark.skipif(
539
+ supports_user_agent is False,
540
+ reason="custom_user_agent is not supported for DuckDB version < 0.9.2",
541
+ )
542
+ def test_user_agent() -> None:
543
+ eng = create_engine("duckdb:///:memory:")
544
+
545
+ with eng.connect() as conn:
546
+ res = conn.execute(text("PRAGMA USER_AGENT"))
547
+ row = res.first()
548
+ assert row is not None
549
+ assert re.match(user_agent_re, row[0])
550
+
551
+
552
+ @mark.skipif(
553
+ supports_user_agent is False,
554
+ reason="custom_user_agent is not supported for DuckDB version < 0.9.2",
555
+ )
556
+ def test_user_agent_with_custom_user_agent() -> None:
557
+ connect_args = {"config": {"custom_user_agent": "custom"}}
558
+ eng = create_engine("duckdb:///:memory:", connect_args=connect_args)
559
+
560
+ with eng.connect() as conn:
561
+ res = conn.execute(text("PRAGMA USER_AGENT"))
562
+ row = res.first()
563
+ assert row is not None
564
+ assert re.match(user_agent_re + " custom", row[0])
565
+
566
+ # Check that connect hasn't mutated the caller's data.
567
+ assert connect_args["config"]["custom_user_agent"] == "custom"
568
+
569
+
570
+ def test_do_ping(tmp_path: Path, caplog: LogCaptureFixture) -> None:
571
+ engine = create_engine(
572
+ "duckdb:///" + str(tmp_path / "db"),
573
+ pool_pre_ping=True,
574
+ pool_size=1,
575
+ poolclass=sqlalchemy.pool.QueuePool,
576
+ )
577
+
578
+ logger = cast(logging.Logger, engine.pool.logger) # type: ignore
579
+ logger.setLevel(logging.DEBUG)
580
+
581
+ with caplog.at_level(logging.DEBUG, logger=logger.name):
582
+ engine.connect() # create a connection in the pool
583
+ assert (
584
+ engine.connect() is not None
585
+ ) # grab the "stale" connection, which will cause a ping
586
+
587
+ assert any(
588
+ "Pool pre-ping on connection" in message for message in caplog.messages
589
+ )
590
+
591
+
592
+ def test_try_cast(engine: Engine) -> None:
593
+ try_cast = importorskip("sqlalchemy", "2.0.14").try_cast
594
+
595
+ with engine.connect() as conn:
596
+ query = select(try_cast("2022-01-01", DateTime))
597
+ assert conn.execute(query).one() == (datetime(2022, 1, 1),)
598
+
599
+ query = select(try_cast("not a date", DateTime))
600
+ assert conn.execute(query).one() == (None,)
601
+
602
+
603
+ def test_params(engine: Engine) -> None:
604
+ s = text("SELECT :x")
605
+ with engine.connect() as conn:
606
+ assert ("m",) == conn.execute(s, {"x": "m"}).fetchone()
607
+
608
+
609
+ def test_361(engine: Engine) -> None:
610
+ importorskip("sqlalchemy", "2.0.0")
611
+
612
+ with engine.connect() as conn:
613
+ conn.execute(text("create table test (dt date);"))
614
+ conn.execute(text("insert into test values ('2022-01-01');"))
615
+
616
+ metadata = MetaData()
617
+ metadata.reflect(bind=conn)
618
+ test = metadata.tables["test"]
619
+ part = "year"
620
+ date_part = func.date_part(part, test.c.dt)
621
+
622
+ stmt = select(date_part).select_from(test).group_by(date_part)
623
+ assert conn.execute(stmt).fetchall() == [(2022,)]
624
+
625
+
626
+ def test_close(engine: Engine) -> None:
627
+ with engine.connect() as conn:
628
+ res = conn.execute(text("select 1"))
629
+ res.close()
630
+
631
+
632
+ def test_with_cache(tmp_path: Path) -> None:
633
+ importorskip("duckdb", "1.0.0")
634
+ tmp_db_path = str(tmp_path / "db_cached")
635
+ engine1 = create_engine(f"duckdb:///{tmp_db_path}?threads=1")
636
+ engine2 = create_engine(f"duckdb:///{tmp_db_path}?threads=1")
637
+ with engine1.connect() as conn1:
638
+ with engine2.connect() as conn2:
639
+ res1 = conn1.execute(
640
+ text("select value from duckdb_settings() where name = 'threads'")
641
+ ).fetchall()
642
+ res2 = conn2.execute(
643
+ text("select value from duckdb_settings() where name = 'threads'")
644
+ ).fetchall()
645
+ assert res1 == res2
646
+ # TODO: how do we validate that both connections point to the same database instance?
647
+ assert res1[0][0] == "1"
648
+
649
+
650
+ def test_no_cache(tmp_path: Path) -> None:
651
+ tmp_db_path = str(tmp_path / "db_no_cache")
652
+ engine1 = create_engine(f"duckdb:///{tmp_db_path}?threads=1&user=1")
653
+ engine2 = create_engine(f"duckdb:///{tmp_db_path}?threads=2&user=2")
654
+ with engine1.connect() as conn1:
655
+ with engine2.connect() as conn2:
656
+ res1 = conn1.execute(
657
+ text("select value from duckdb_settings() where name = 'threads'")
658
+ ).fetchall()
659
+ res2 = conn2.execute(
660
+ text("select value from duckdb_settings() where name = 'threads'")
661
+ ).fetchall()
662
+ assert res1 != res2
663
+ assert res1[0][0] == "1"
664
+ assert res2[0][0] == "2"
665
+
666
+
667
+ def test_reflection(engine: Engine) -> None:
668
+ metadata = MetaData()
669
+ with engine.connect() as conn:
670
+ conn.execute(text("CREATE TABLE tbl(col1 INTEGER)"))
671
+ metadata.reflect(engine)
672
+
673
+
674
+ def test_upsert(session: Session) -> None:
675
+ class User(Base):
676
+ __tablename__ = "users"
677
+
678
+ id = Column(Integer(), Sequence("id_seq"), primary_key=True)
679
+ name = Column(String, unique=True)
680
+ fullname = Column(String)
681
+
682
+ Base.metadata.create_all(session.bind)
683
+ stmt = insert(User).values(
684
+ [
685
+ {"name": "spongebob", "fullname": "Spongebob Squarepants"},
686
+ {"name": "sandy", "fullname": "Sandy Cheeks"},
687
+ {"name": "patrick", "fullname": "Patrick Star"},
688
+ {"name": "squidward", "fullname": "Squidward Tentacles"},
689
+ {"name": "ehkrabs", "fullname": "Eugene H. Krabs"},
690
+ ]
691
+ )
692
+ stmt = stmt.on_conflict_do_update(
693
+ index_elements=[User.name], set_=dict(fullname=stmt.excluded.fullname)
694
+ )
695
+ session.execute(stmt)
696
+
697
+ assert session.query(User).count() == 5
698
+
699
+
700
+ def test_reserved_keywords(engine: Engine) -> None:
701
+ stmt = select(column("qualify"))
702
+
703
+ assert str(stmt.compile(engine)) == 'SELECT "qualify"'
704
+
705
+
706
+ def test_register_filesystem() -> None:
707
+ memory_fs = fsspec.filesystem("memory")
708
+ file_fs = fsspec.filesystem("file")
709
+ engine = create_engine(
710
+ "duckdb:///",
711
+ connect_args={
712
+ "register_filesystems": [memory_fs, file_fs],
713
+ },
714
+ )
715
+
716
+ with engine.connect() as conn:
717
+ duckdb_conn = getattr(conn.connection.dbapi_connection, "_ConnectionWrapper__c")
718
+ assert duckdb.list_filesystems(connection=duckdb_conn) == ["memory", "file"]