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,1411 @@
1
+ import os
2
+ import re
3
+ import time
4
+ import uuid
5
+ import warnings
6
+ from functools import lru_cache
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Collection,
11
+ Dict,
12
+ Iterable,
13
+ List,
14
+ Optional,
15
+ Sequence,
16
+ Tuple,
17
+ Type,
18
+ )
19
+
20
+ import duckdb
21
+ import sqlalchemy
22
+ from packaging.version import Version
23
+ from sqlalchemy import pool, select, sql, text, util
24
+ from sqlalchemy import types as sqltypes
25
+ from sqlalchemy.dialects.postgresql import UUID, insert
26
+ from sqlalchemy.dialects.postgresql.base import (
27
+ PGDialect,
28
+ PGIdentifierPreparer,
29
+ PGInspector,
30
+ )
31
+ from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
32
+ from sqlalchemy.engine.default import DefaultDialect, DefaultExecutionContext
33
+ from sqlalchemy.engine.reflection import cache
34
+ from sqlalchemy.engine.url import URL as SAURL
35
+ from sqlalchemy.exc import NoSuchTableError
36
+ from sqlalchemy.ext.compiler import compiles
37
+ from sqlalchemy.sql import bindparam
38
+ from sqlalchemy.sql.selectable import Select
39
+
40
+ from ._supports import has_comment_support
41
+ from .bulk import copy_from_csv, copy_from_parquet, copy_from_rows
42
+ from .capabilities import get_capabilities
43
+ from .config import apply_config, get_core_config
44
+ from .datatypes import ISCHEMA_NAMES, register_extension_types
45
+ from .motherduck import (
46
+ DIALECT_QUERY_KEYS,
47
+ MotherDuckURL,
48
+ append_query_to_database,
49
+ create_engine_from_paths,
50
+ create_motherduck_engine,
51
+ extract_path_query_from_config,
52
+ split_url_query,
53
+ stable_session_hint,
54
+ )
55
+ from .olap import read_csv, read_csv_auto, read_parquet, table_function
56
+ from .url import URL, make_url
57
+
58
+ try:
59
+ from sqlalchemy.dialects.postgresql.base import PGExecutionContext
60
+ except ImportError: # pragma: no cover - fallback for older SQLAlchemy
61
+ PGExecutionContext = DefaultExecutionContext
62
+
63
+ __version__ = "0.19.0"
64
+ sqlalchemy_version = sqlalchemy.__version__
65
+ SQLALCHEMY_VERSION = Version(sqlalchemy_version)
66
+ SQLALCHEMY_2 = SQLALCHEMY_VERSION >= Version("2.0.0")
67
+ duckdb_version: str = duckdb.__version__
68
+ _capabilities = get_capabilities(duckdb_version)
69
+ supports_attach: bool = _capabilities.supports_attach
70
+ supports_user_agent: bool = _capabilities.supports_user_agent
71
+
72
+ if TYPE_CHECKING:
73
+ from sqlalchemy.engine import Connection
74
+ from sqlalchemy.engine.reflection import ReflectedCheckConstraint, ReflectedIndex
75
+
76
+ from .capabilities import DuckDBCapabilities
77
+
78
+ register_extension_types()
79
+
80
+
81
+ __all__ = [
82
+ "Dialect",
83
+ "ConnectionWrapper",
84
+ "CursorWrapper",
85
+ "DBAPI",
86
+ "DuckDBEngineWarning",
87
+ "insert", # reexport of sqlalchemy.dialects.postgresql.insert
88
+ "MotherDuckURL",
89
+ "URL",
90
+ "create_engine_from_paths",
91
+ "create_motherduck_engine",
92
+ "make_url",
93
+ "stable_session_hint",
94
+ "table_function",
95
+ "read_parquet",
96
+ "read_csv",
97
+ "read_csv_auto",
98
+ "copy_from_parquet",
99
+ "copy_from_csv",
100
+ "copy_from_rows",
101
+ ]
102
+
103
+
104
+ class DBAPI:
105
+ paramstyle = "numeric_dollar" if SQLALCHEMY_2 else "qmark"
106
+ apilevel = duckdb.apilevel
107
+ threadsafety = duckdb.threadsafety
108
+
109
+ # this is being fixed upstream to add a proper exception hierarchy
110
+ Error = getattr(duckdb, "Error", RuntimeError)
111
+ TransactionException = getattr(duckdb, "TransactionException", Error)
112
+ ParserException = getattr(duckdb, "ParserException", Error)
113
+
114
+ @staticmethod
115
+ def Binary(x: Any) -> Any:
116
+ return x
117
+
118
+
119
+ class DuckDBInspector(PGInspector):
120
+ def get_check_constraints(
121
+ self, table_name: str, schema: Optional[str] = None, **kw: Any
122
+ ) -> List["ReflectedCheckConstraint"]:
123
+ try:
124
+ return super().get_check_constraints(table_name, schema, **kw)
125
+ except Exception as e:
126
+ raise NotImplementedError() from e
127
+
128
+
129
+ class ConnectionWrapper:
130
+ __c: duckdb.DuckDBPyConnection
131
+ notices: List[str]
132
+ autocommit = None # duckdb doesn't support setting autocommit
133
+ closed = False
134
+
135
+ def __init__(self, c: duckdb.DuckDBPyConnection) -> None:
136
+ self.__c = c
137
+ self.notices = list()
138
+
139
+ def cursor(self) -> "CursorWrapper":
140
+ return CursorWrapper(self.__c, self)
141
+
142
+ def __getattr__(self, name: str) -> Any:
143
+ return getattr(self.__c, name)
144
+
145
+ def close(self) -> None:
146
+ self.__c.close()
147
+ self.closed = True
148
+
149
+
150
+ _REGISTER_NAME_KEYS = ("name", "view_name", "table")
151
+ _REGISTER_DATA_KEYS = ("df", "dataframe", "relation", "data")
152
+
153
+
154
+ def _parse_register_params(parameters: Optional[Any]) -> Tuple[str, Any]:
155
+ if parameters is None:
156
+ raise ValueError("register requires a view name and data")
157
+ if isinstance(parameters, dict):
158
+ view_name = None
159
+ for key in _REGISTER_NAME_KEYS:
160
+ if key in parameters:
161
+ view_name = parameters[key]
162
+ break
163
+ df = None
164
+ for key in _REGISTER_DATA_KEYS:
165
+ if key in parameters:
166
+ df = parameters[key]
167
+ break
168
+ if view_name is None or df is None:
169
+ raise ValueError("register requires a view name and data (tuple or dict)")
170
+ return view_name, df
171
+ if isinstance(parameters, (list, tuple)) and len(parameters) == 2:
172
+ return parameters[0], parameters[1]
173
+ raise ValueError("register requires a view name and data (tuple or dict)")
174
+
175
+
176
+ class CursorWrapper:
177
+ __c: duckdb.DuckDBPyConnection
178
+ __connection_wrapper: "ConnectionWrapper"
179
+
180
+ def __init__(
181
+ self, c: duckdb.DuckDBPyConnection, connection_wrapper: "ConnectionWrapper"
182
+ ) -> None:
183
+ self.__c = c
184
+ self.__connection_wrapper = connection_wrapper
185
+
186
+ def executemany(
187
+ self,
188
+ statement: str,
189
+ parameters: Optional[List[Dict]] = None,
190
+ context: Optional[Any] = None,
191
+ ) -> None:
192
+ if not parameters:
193
+ params = []
194
+ elif isinstance(parameters, list):
195
+ params = parameters
196
+ else:
197
+ params = list(parameters)
198
+ self.__c.executemany(statement, params)
199
+
200
+ def execute(
201
+ self,
202
+ statement: str,
203
+ parameters: Optional[Tuple] = None,
204
+ context: Optional[Any] = None,
205
+ ) -> None:
206
+ try:
207
+ norm = statement.strip().lower().rstrip(";")
208
+ if norm == "commit": # this is largely for ipython-sql
209
+ self.__c.commit()
210
+ elif norm.startswith("register"):
211
+ view_name, df = _parse_register_params(parameters)
212
+ self.__c.register(view_name, df)
213
+ elif norm == "show transaction isolation level":
214
+ self.__c.execute("select 'read committed' as transaction_isolation")
215
+ elif parameters is None:
216
+ self.__c.execute(statement)
217
+ else:
218
+ self.__c.execute(statement, parameters)
219
+ except RuntimeError as e:
220
+ if e.args[0].startswith("Not implemented Error"):
221
+ raise NotImplementedError(*e.args) from e
222
+ elif (
223
+ e.args[0]
224
+ == "TransactionContext Error: cannot commit - no transaction is active"
225
+ ):
226
+ return
227
+ else:
228
+ raise e
229
+
230
+ @property
231
+ def connection(self) -> Any:
232
+ return self.__connection_wrapper
233
+
234
+ def close(self) -> None:
235
+ pass # closing cursors is not supported in duckdb
236
+
237
+ @property
238
+ def description(self) -> Any:
239
+ desc = self.__c.description
240
+ if desc is None:
241
+ return None
242
+ fixed = []
243
+ for col in desc:
244
+ if len(col) >= 2:
245
+ type_code = col[1]
246
+ try:
247
+ hash(type_code)
248
+ fixed.append(col)
249
+ except TypeError:
250
+ fixed.append((col[0], str(type_code), *col[2:]))
251
+ else:
252
+ fixed.append(col)
253
+ return fixed
254
+
255
+ def __getattr__(self, name: str) -> Any:
256
+ return getattr(self.__c, name)
257
+
258
+ def fetchmany(self, size: Optional[int] = None) -> List:
259
+ if size is None:
260
+ return self.__c.fetchmany()
261
+ else:
262
+ return self.__c.fetchmany(size)
263
+
264
+
265
+ class DuckDBEngineWarning(Warning):
266
+ pass
267
+
268
+
269
+ def index_warning() -> None:
270
+ warnings.warn(
271
+ "duckdb-sqlalchemy doesn't yet support reflection on indices",
272
+ DuckDBEngineWarning,
273
+ )
274
+
275
+
276
+ def _normalize_execution_options(execution_options: Dict[str, Any]) -> Dict[str, Any]:
277
+ if (
278
+ "duckdb_insertmanyvalues_page_size" in execution_options
279
+ and "insertmanyvalues_page_size" not in execution_options
280
+ ):
281
+ execution_options = dict(execution_options)
282
+ execution_options["insertmanyvalues_page_size"] = execution_options[
283
+ "duckdb_insertmanyvalues_page_size"
284
+ ]
285
+ return execution_options
286
+
287
+
288
+ class DuckDBArrowResult:
289
+ def __init__(self, result: Any) -> None:
290
+ self._result = result
291
+ self._arrow = None
292
+
293
+ def _fetch_arrow(self) -> Any:
294
+ if self._arrow is not None:
295
+ return self._arrow
296
+ cursor = getattr(self._result, "cursor", None)
297
+ if cursor is None:
298
+ cursor = getattr(self._result, "_cursor", None)
299
+ if cursor is None or not hasattr(cursor, "fetch_arrow_table"):
300
+ raise NotImplementedError("Arrow results are not available on this cursor")
301
+ self._arrow = cursor.fetch_arrow_table()
302
+ return self._arrow
303
+
304
+ @property
305
+ def arrow(self) -> Any:
306
+ return self._fetch_arrow()
307
+
308
+ def all(self) -> Any:
309
+ return self._fetch_arrow()
310
+
311
+ def fetchall(self) -> Any:
312
+ return self._fetch_arrow()
313
+
314
+ def __getattr__(self, name: str) -> Any:
315
+ return getattr(self._result, name)
316
+
317
+ def __iter__(self) -> Any:
318
+ return iter(self._result)
319
+
320
+
321
+ class DuckDBExecutionContext(PGExecutionContext):
322
+ @classmethod
323
+ def _init_compiled(
324
+ cls,
325
+ dialect: "Dialect",
326
+ connection: Any,
327
+ dbapi_connection: Any,
328
+ execution_options: Dict[str, Any],
329
+ compiled: Any,
330
+ parameters: Any,
331
+ invoked_statement: Any,
332
+ extracted_parameters: Any,
333
+ cache_hit: Any = None,
334
+ ) -> Any:
335
+ execution_options = _normalize_execution_options(execution_options)
336
+ return super()._init_compiled(
337
+ dialect,
338
+ connection,
339
+ dbapi_connection,
340
+ execution_options,
341
+ compiled,
342
+ parameters,
343
+ invoked_statement,
344
+ extracted_parameters,
345
+ cache_hit,
346
+ )
347
+
348
+ @classmethod
349
+ def _init_statement(
350
+ cls,
351
+ dialect: "Dialect",
352
+ connection: Any,
353
+ dbapi_connection: Any,
354
+ execution_options: Dict[str, Any],
355
+ statement: str,
356
+ parameters: Any,
357
+ ) -> Any:
358
+ execution_options = _normalize_execution_options(execution_options)
359
+ return super()._init_statement(
360
+ dialect,
361
+ connection,
362
+ dbapi_connection,
363
+ execution_options,
364
+ statement,
365
+ parameters,
366
+ )
367
+
368
+ def _setup_result_proxy(self) -> Any:
369
+ arraysize = self.execution_options.get("duckdb_arraysize")
370
+ if arraysize is None:
371
+ arraysize = self.execution_options.get("arraysize")
372
+ if arraysize is not None and hasattr(self.cursor, "arraysize"):
373
+ self.cursor.arraysize = arraysize
374
+ result = super()._setup_result_proxy()
375
+ if self.execution_options.get("duckdb_arrow") and getattr(
376
+ result, "returns_rows", False
377
+ ):
378
+ return DuckDBArrowResult(result)
379
+ return result
380
+
381
+
382
+ def _looks_like_motherduck(database: Optional[str], config: Dict[str, Any]) -> bool:
383
+ if database is not None and (
384
+ database.startswith("md:") or database.startswith("motherduck:")
385
+ ):
386
+ return True
387
+ motherduck_keys = {
388
+ "motherduck_token",
389
+ "attach_mode",
390
+ "saas_mode",
391
+ "session_hint",
392
+ "access_mode",
393
+ "dbinstance_inactivity_ttl",
394
+ "motherduck_dbinstance_inactivity_ttl",
395
+ }
396
+ return any(k in config for k in motherduck_keys)
397
+
398
+
399
+ DISCONNECT_ERROR_PATTERNS = (
400
+ "connection closed",
401
+ "connection reset",
402
+ "connection refused",
403
+ "broken pipe",
404
+ "socket",
405
+ "network is unreachable",
406
+ "timed out",
407
+ "timeout",
408
+ "could not connect",
409
+ "failed to connect",
410
+ )
411
+
412
+ TRANSIENT_ERROR_PATTERNS = (
413
+ "temporarily unavailable",
414
+ "service unavailable",
415
+ "http error: 429",
416
+ "http error: 503",
417
+ "http error: 504",
418
+ "rate limit",
419
+ )
420
+
421
+
422
+ def _is_idempotent_statement(statement: str) -> bool:
423
+ normalized = statement.strip().lower()
424
+ return normalized.startswith(("select", "show", "describe", "pragma", "explain"))
425
+
426
+
427
+ def _is_transient_error(error: BaseException) -> bool:
428
+ message = str(error).lower()
429
+ if any(pattern in message for pattern in DISCONNECT_ERROR_PATTERNS):
430
+ return False
431
+ return any(pattern in message for pattern in TRANSIENT_ERROR_PATTERNS)
432
+
433
+
434
+ def _pool_override_from_url(url: SAURL) -> Optional[str]:
435
+ value = None
436
+ if "duckdb_sqlalchemy_pool" in url.query:
437
+ value = url.query.get("duckdb_sqlalchemy_pool")
438
+ elif "pool" in url.query:
439
+ value = url.query.get("pool")
440
+ if value is None:
441
+ value = os.getenv("DUCKDB_SQLALCHEMY_POOL")
442
+ if isinstance(value, (list, tuple)):
443
+ value = value[0] if value else None
444
+ if value is None:
445
+ return None
446
+ return str(value).lower()
447
+
448
+
449
+ def _apply_motherduck_defaults(config: Dict[str, Any], database: Optional[str]) -> None:
450
+ if "motherduck_token" not in config:
451
+ token = os.getenv("motherduck_token") or os.getenv("MOTHERDUCK_TOKEN")
452
+ if token and _looks_like_motherduck(database, config):
453
+ config["motherduck_token"] = token
454
+
455
+ if "motherduck_token" in config and not isinstance(config["motherduck_token"], str):
456
+ raise TypeError("motherduck_token must be a string")
457
+
458
+
459
+ def _normalize_motherduck_config(config: Dict[str, Any]) -> None:
460
+ if (
461
+ "dbinstance_inactivity_ttl" in config
462
+ and "motherduck_dbinstance_inactivity_ttl" not in config
463
+ ):
464
+ config["motherduck_dbinstance_inactivity_ttl"] = config[
465
+ "dbinstance_inactivity_ttl"
466
+ ]
467
+
468
+
469
+ class DuckDBIdentifierPreparer(PGIdentifierPreparer):
470
+ def __init__(self, dialect: "Dialect", **kwargs: Any) -> None:
471
+ super().__init__(dialect, **kwargs)
472
+
473
+ self.reserved_words.update(
474
+ {
475
+ keyword_name
476
+ for (keyword_name,) in duckdb.cursor()
477
+ .execute(
478
+ "select keyword_name from duckdb_keywords() where keyword_category == 'reserved'"
479
+ )
480
+ .fetchall()
481
+ }
482
+ )
483
+
484
+ def _separate(self, name: Optional[str]) -> Tuple[Optional[Any], Optional[str]]:
485
+ """
486
+ Get database name and schema name from schema if it contains a database name
487
+ Format:
488
+ <db_name>.<schema_name>
489
+ db_name and schema_name are double quoted if contains spaces or double quotes
490
+ """
491
+ database_name, schema_name = None, name
492
+ if name is not None and "." in name:
493
+ database_name, schema_name = (
494
+ max(s) for s in re.findall(r'"([^.]+)"|([^.]+)', name)
495
+ )
496
+ return database_name, schema_name
497
+
498
+ def format_schema(self, name: str) -> str:
499
+ """Prepare a quoted schema name."""
500
+ database_name, schema_name = self._separate(name)
501
+ if database_name is None or schema_name is None:
502
+ return self.quote(name)
503
+ return ".".join(self.quote(str(_n)) for _n in [database_name, schema_name])
504
+
505
+ def quote_schema(self, schema: str, force: Any = None) -> str:
506
+ """
507
+ Conditionally quote a schema name.
508
+
509
+ :param schema: string schema name
510
+ :param force: unused
511
+ """
512
+ return self.format_schema(schema)
513
+
514
+
515
+ class DuckDBNullType(sqltypes.NullType):
516
+ def result_processor(self, dialect: Any, coltype: object) -> Any:
517
+ if coltype == "JSON":
518
+ return sqltypes.JSON().result_processor(dialect, coltype)
519
+ else:
520
+ return super().result_processor(dialect, coltype)
521
+
522
+
523
+ class Dialect(PGDialect_psycopg2):
524
+ name = "duckdb"
525
+ driver = "duckdb_sqlalchemy"
526
+ _has_events = False
527
+ supports_statement_cache = True
528
+ supports_comments = False
529
+ supports_sane_rowcount = False
530
+ supports_server_side_cursors = False
531
+ execution_ctx_cls = DuckDBExecutionContext
532
+ div_is_floordiv = False # TODO: tweak this to be based on DuckDB version
533
+ inspector = DuckDBInspector
534
+ insertmanyvalues_page_size = 1000
535
+ use_insertmanyvalues = SQLALCHEMY_2
536
+ use_insertmanyvalues_wo_returning = SQLALCHEMY_2
537
+ duckdb_copy_threshold = 10000
538
+ _capabilities: "DuckDBCapabilities"
539
+ colspecs = util.update_copy(
540
+ PGDialect.colspecs,
541
+ {
542
+ # the psycopg2 driver registers a _PGNumeric with custom logic for
543
+ # postgres type_codes (such as 701 for float) that duckdb doesn't have
544
+ sqltypes.Numeric: sqltypes.Numeric,
545
+ sqltypes.JSON: sqltypes.JSON,
546
+ UUID: UUID,
547
+ },
548
+ )
549
+ ischema_names = util.update_copy(
550
+ PGDialect.ischema_names,
551
+ ISCHEMA_NAMES,
552
+ )
553
+ preparer = DuckDBIdentifierPreparer
554
+ identifier_preparer: DuckDBIdentifierPreparer
555
+
556
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
557
+ kwargs["use_native_hstore"] = False
558
+ super().__init__(*args, **kwargs)
559
+ self._capabilities = get_capabilities(duckdb.__version__)
560
+
561
+ def initialize(self, connection: "Connection") -> None:
562
+ DefaultDialect.initialize(self, connection)
563
+ self._capabilities = get_capabilities(duckdb.__version__)
564
+ self.supports_comments = has_comment_support()
565
+
566
+ @property
567
+ def capabilities(self) -> "DuckDBCapabilities":
568
+ return self._capabilities
569
+
570
+ def type_descriptor(self, typeobj: Type[sqltypes.TypeEngine]) -> Any: # type: ignore[override]
571
+ res = super().type_descriptor(typeobj)
572
+
573
+ if isinstance(res, sqltypes.NullType):
574
+ return DuckDBNullType()
575
+
576
+ return res
577
+
578
+ def connect(self, *cargs: Any, **cparams: Any) -> Any:
579
+ core_keys = get_core_config()
580
+ preload_extensions = cparams.pop("preload_extensions", [])
581
+ config = dict(cparams.get("config", {}))
582
+ cparams["config"] = config
583
+ config.update(cparams.pop("url_config", {}))
584
+ for key in DIALECT_QUERY_KEYS:
585
+ config.pop(key, None)
586
+ _apply_motherduck_defaults(config, cparams.get("database"))
587
+ path_query = extract_path_query_from_config(config)
588
+ if path_query:
589
+ cparams["database"] = append_query_to_database(
590
+ cparams.get("database"), path_query
591
+ )
592
+ _normalize_motherduck_config(config)
593
+
594
+ ext = {k: config.pop(k) for k in list(config) if k not in core_keys}
595
+ if supports_user_agent:
596
+ user_agent = (
597
+ f"duckdb-sqlalchemy/{__version__}(sqlalchemy/{sqlalchemy_version})"
598
+ )
599
+ if "custom_user_agent" in config:
600
+ user_agent = f"{user_agent} {config['custom_user_agent']}"
601
+ config["custom_user_agent"] = user_agent
602
+
603
+ filesystems = cparams.pop("register_filesystems", [])
604
+
605
+ conn = duckdb.connect(*cargs, **cparams)
606
+
607
+ for extension in preload_extensions:
608
+ conn.execute(f"LOAD {extension}")
609
+
610
+ for filesystem in filesystems:
611
+ conn.register_filesystem(filesystem)
612
+
613
+ apply_config(self, conn, ext)
614
+
615
+ return ConnectionWrapper(conn)
616
+
617
+ def on_connect(self) -> None:
618
+ pass
619
+
620
+ @classmethod
621
+ def get_pool_class(cls, url: SAURL) -> Type[pool.Pool]:
622
+ pool_override = _pool_override_from_url(url)
623
+ if pool_override == "queue":
624
+ return pool.QueuePool
625
+ if pool_override in {"singleton", "singletonthreadpool"}:
626
+ return pool.SingletonThreadPool
627
+ if pool_override in {"null", "nullpool"}:
628
+ return pool.NullPool
629
+ if url.database and url.database.startswith(":memory:"):
630
+ return pool.SingletonThreadPool
631
+ if _looks_like_motherduck(url.database, dict(url.query)):
632
+ return pool.NullPool
633
+ return pool.NullPool
634
+
635
+ @staticmethod
636
+ def dbapi(**kwargs: Any) -> Type[DBAPI]:
637
+ return DBAPI
638
+
639
+ def _get_server_version_info(self, connection: "Connection") -> Tuple[int, int]:
640
+ return (8, 0)
641
+
642
+ def get_default_isolation_level(self, dbapi_conn):
643
+ return self.get_isolation_level(dbapi_conn)
644
+
645
+ def do_rollback(self, dbapi_connection: Any) -> None:
646
+ try:
647
+ super().do_rollback(dbapi_connection)
648
+ except DBAPI.TransactionException as e:
649
+ if (
650
+ e.args[0]
651
+ != "TransactionContext Error: cannot rollback - no transaction is active"
652
+ ):
653
+ raise e
654
+
655
+ def do_begin(self, dbapi_connection: Any) -> None:
656
+ dbapi_connection.begin()
657
+
658
+ def get_view_names(
659
+ self,
660
+ connection: Any,
661
+ schema: Optional[Any] = None,
662
+ include: Optional[Any] = None,
663
+ **kw: Any,
664
+ ) -> Any:
665
+ s = """
666
+ SELECT table_name
667
+ FROM information_schema.tables
668
+ WHERE
669
+ table_type='VIEW'
670
+ AND table_schema = :schema_name
671
+ """
672
+ params = {}
673
+ database_name = None
674
+
675
+ if schema is not None:
676
+ database_name, schema = self.identifier_preparer._separate(schema)
677
+ else:
678
+ schema = "main"
679
+
680
+ params.update({"schema_name": schema})
681
+
682
+ if database_name is not None:
683
+ s += "AND table_catalog = :database_name\n"
684
+ params.update({"database_name": database_name})
685
+
686
+ rs = connection.execute(text(s), params)
687
+ return [view for (view,) in rs]
688
+
689
+ @cache # type: ignore[call-arg]
690
+ def get_schema_names(self, connection: "Connection", **kw: "Any"): # type: ignore[no-untyped-def]
691
+ """
692
+ Return unquoted database_name.schema_name unless either contains spaces or double quotes.
693
+ In that case, escape double quotes and then wrap in double quotes.
694
+ SQLAlchemy definition of a schema includes database name for databases like SQL Server (Ex: databasename.dbo)
695
+ (see https://docs.sqlalchemy.org/en/20/dialects/mssql.html#multipart-schema-names)
696
+ """
697
+
698
+ if not supports_attach:
699
+ return super().get_schema_names(connection, **kw)
700
+
701
+ s = """
702
+ SELECT database_name, schema_name AS nspname
703
+ FROM duckdb_schemas()
704
+ WHERE schema_name NOT LIKE 'pg\\_%' ESCAPE '\\'
705
+ ORDER BY database_name, nspname
706
+ """
707
+ rs = connection.execute(text(s))
708
+
709
+ qs = self.identifier_preparer.quote_schema
710
+ return [qs(".".join(nspname)) for nspname in rs]
711
+
712
+ def _build_query_where(
713
+ self,
714
+ table_name: Optional[str] = None,
715
+ schema_name: Optional[str] = None,
716
+ database_name: Optional[str] = None,
717
+ ) -> Tuple[str, Dict[str, str]]:
718
+ sql = ""
719
+ params = {}
720
+
721
+ # If no database name is provided, try to get it from the schema name
722
+ # specified as "<db name>.<schema name>"
723
+ # If only a schema name is found, database_name will return None
724
+ if database_name is None and schema_name is not None:
725
+ database_name, schema_name = self.identifier_preparer._separate(schema_name)
726
+
727
+ if table_name is not None:
728
+ sql += "AND table_name = :table_name\n"
729
+ params.update({"table_name": table_name})
730
+
731
+ if schema_name is not None:
732
+ sql += "AND schema_name = :schema_name\n"
733
+ params.update({"schema_name": schema_name})
734
+
735
+ if database_name is not None:
736
+ sql += "AND database_name = :database_name\n"
737
+ params.update({"database_name": database_name})
738
+
739
+ return sql, params
740
+
741
+ @cache # type: ignore[call-arg]
742
+ def get_table_names(self, connection: "Connection", schema=None, **kw: "Any"): # type: ignore[no-untyped-def]
743
+ """
744
+ Return unquoted database_name.schema_name unless either contains spaces or double quotes.
745
+ In that case, escape double quotes and then wrap in double quotes.
746
+ SQLAlchemy definition of a schema includes database name for databases like SQL Server (Ex: databasename.dbo)
747
+ (see https://docs.sqlalchemy.org/en/20/dialects/mssql.html#multipart-schema-names)
748
+ """
749
+
750
+ if not supports_attach:
751
+ return super().get_table_names(connection, schema, **kw)
752
+
753
+ s = """
754
+ SELECT database_name, schema_name, table_name
755
+ FROM duckdb_tables()
756
+ WHERE schema_name NOT LIKE 'pg\\_%' ESCAPE '\\'
757
+ """
758
+ sql, params = self._build_query_where(schema_name=schema)
759
+ s += sql
760
+ rs = connection.execute(text(s), params)
761
+
762
+ return [
763
+ table
764
+ for (
765
+ db,
766
+ sc,
767
+ table,
768
+ ) in rs
769
+ ]
770
+
771
+ @cache # type: ignore[call-arg]
772
+ def get_table_oid( # type: ignore[no-untyped-def]
773
+ self,
774
+ connection: "Connection",
775
+ table_name: str,
776
+ schema: "Optional[str]" = None,
777
+ **kw: "Any",
778
+ ):
779
+ """Fetch the oid for (database.)schema.table_name.
780
+ The schema name can be formatted either as database.schema or just the schema name.
781
+ In the latter scenario the schema associated with the default database is used.
782
+ """
783
+ s = """
784
+ SELECT oid, table_name
785
+ FROM (
786
+ SELECT table_oid AS oid, table_name, database_name, schema_name FROM duckdb_tables()
787
+ UNION ALL BY NAME
788
+ SELECT view_oid AS oid , view_name AS table_name, database_name, schema_name FROM duckdb_views()
789
+ )
790
+ WHERE schema_name NOT LIKE 'pg\\_%' ESCAPE '\\'
791
+ """
792
+ sql, params = self._build_query_where(table_name=table_name, schema_name=schema)
793
+ s += sql
794
+
795
+ rs = connection.execute(text(s), params)
796
+ table_oid = rs.scalar()
797
+ if table_oid is None:
798
+ raise NoSuchTableError(table_name)
799
+ return table_oid
800
+
801
+ def _duckdb_table_exists(
802
+ self, connection: "Connection", table_name: str, schema: Optional[str]
803
+ ) -> bool:
804
+ sql = """
805
+ SELECT 1
806
+ FROM duckdb_tables()
807
+ WHERE table_name = :table_name
808
+ """
809
+ params: Dict[str, Any] = {"table_name": table_name}
810
+ if schema is not None:
811
+ database_name, schema_name = self.identifier_preparer._separate(schema)
812
+ sql += "AND schema_name = :schema_name\n"
813
+ params["schema_name"] = schema_name
814
+ if database_name is not None:
815
+ sql += "AND database_name = :database_name\n"
816
+ params["database_name"] = database_name
817
+ return connection.execute(text(sql), params).first() is not None
818
+
819
+ def _duckdb_columns(
820
+ self, connection: "Connection", table_name: str, schema: Optional[str]
821
+ ) -> Optional[List[Dict[str, Any]]]:
822
+ sql = """
823
+ SELECT column_name, column_default, is_nullable, data_type, comment, column_index
824
+ FROM duckdb_columns()
825
+ WHERE table_name = :table_name
826
+ """
827
+ params: Dict[str, Any] = {"table_name": table_name}
828
+ if schema is not None:
829
+ database_name, schema_name = self.identifier_preparer._separate(schema)
830
+ sql += "AND schema_name = :schema_name\n"
831
+ params["schema_name"] = schema_name
832
+ if database_name is not None:
833
+ sql += "AND database_name = :database_name\n"
834
+ params["database_name"] = database_name
835
+ sql += "ORDER BY column_index"
836
+ rows = list(connection.execute(text(sql), params).mappings())
837
+ if not rows:
838
+ return None
839
+ columns: List[Dict[str, Any]] = []
840
+ for row in rows:
841
+ coltype = self._reflect_type( # type: ignore[attr-defined]
842
+ row["data_type"],
843
+ {},
844
+ {},
845
+ type_description=f"column '{row['column_name']}'",
846
+ collation=None,
847
+ )
848
+ columns.append(
849
+ {
850
+ "name": row["column_name"],
851
+ "type": coltype,
852
+ "nullable": bool(row["is_nullable"]),
853
+ "default": row["column_default"],
854
+ "autoincrement": False,
855
+ "comment": row["comment"],
856
+ }
857
+ )
858
+ return columns
859
+
860
+ def has_table(
861
+ self,
862
+ connection: "Connection",
863
+ table_name: str,
864
+ schema: Optional[str] = None,
865
+ **kw: Any,
866
+ ) -> bool:
867
+ try:
868
+ return self.get_table_oid(connection, table_name, schema) is not None
869
+ except NoSuchTableError:
870
+ return False
871
+
872
+ @cache # type: ignore[call-arg]
873
+ def get_columns( # type: ignore[no-untyped-def]
874
+ self, connection: "Connection", table_name: str, schema=None, **kw: Any
875
+ ):
876
+ try:
877
+ return super().get_columns(connection, table_name, schema=schema, **kw)
878
+ except NoSuchTableError:
879
+ columns = self._duckdb_columns(connection, table_name, schema)
880
+ if columns is None:
881
+ raise
882
+ return columns
883
+
884
+ @cache # type: ignore[call-arg]
885
+ def get_foreign_keys( # type: ignore[no-untyped-def]
886
+ self, connection: "Connection", table_name: str, schema=None, **kw: Any
887
+ ):
888
+ try:
889
+ return super().get_foreign_keys(connection, table_name, schema=schema, **kw)
890
+ except NoSuchTableError:
891
+ if self._duckdb_table_exists(connection, table_name, schema):
892
+ return []
893
+ raise
894
+
895
+ @cache # type: ignore[call-arg]
896
+ def get_unique_constraints( # type: ignore[no-untyped-def]
897
+ self, connection: "Connection", table_name: str, schema=None, **kw: Any
898
+ ):
899
+ try:
900
+ return super().get_unique_constraints(
901
+ connection, table_name, schema=schema, **kw
902
+ )
903
+ except NoSuchTableError:
904
+ if self._duckdb_table_exists(connection, table_name, schema):
905
+ return []
906
+ raise
907
+
908
+ @cache # type: ignore[call-arg]
909
+ def get_check_constraints( # type: ignore[no-untyped-def]
910
+ self, connection: "Connection", table_name: str, schema=None, **kw: Any
911
+ ):
912
+ try:
913
+ return super().get_check_constraints(
914
+ connection, table_name, schema=schema, **kw
915
+ )
916
+ except NoSuchTableError:
917
+ if self._duckdb_table_exists(connection, table_name, schema):
918
+ return []
919
+ raise
920
+
921
+ def get_indexes(
922
+ self,
923
+ connection: "Connection",
924
+ table_name: str,
925
+ schema: Optional[str] = None,
926
+ **kw: Any,
927
+ ) -> List["ReflectedIndex"]:
928
+ index_warning()
929
+ return []
930
+
931
+ # the following methods are for SQLA2 compatibility
932
+ def get_multi_indexes(
933
+ self,
934
+ connection: "Connection",
935
+ schema: Optional[str] = None,
936
+ filter_names: Optional[Collection[str]] = None,
937
+ scope: Any = None,
938
+ kind: Any = None,
939
+ **kw: Any,
940
+ ) -> Iterable[Tuple[Any, Any]]:
941
+ index_warning()
942
+ return []
943
+
944
+ def create_connect_args(self, url: SAURL) -> Tuple[tuple, dict]:
945
+ opts = url.translate_connect_args(database="database")
946
+ path_query, url_config = split_url_query(dict(url.query))
947
+ opts["url_config"] = url_config
948
+ opts["database"] = append_query_to_database(opts.get("database"), path_query)
949
+ return (), opts
950
+
951
+ @classmethod
952
+ def import_dbapi(cls) -> Any:
953
+ return cls.dbapi()
954
+
955
+ def _get_execution_options(self, context: Optional[Any]) -> Dict[str, Any]:
956
+ if context is None:
957
+ return {}
958
+ return getattr(context, "execution_options", {}) or {}
959
+
960
+ def _bulk_insert_via_register(
961
+ self,
962
+ cursor: Any,
963
+ context: Any,
964
+ parameters: Sequence[Any],
965
+ ) -> bool:
966
+ if not parameters:
967
+ return False
968
+ compiled = getattr(context, "compiled", None)
969
+ if compiled is None:
970
+ return False
971
+ if getattr(compiled, "effective_returning", None):
972
+ return False
973
+ stmt = getattr(compiled, "statement", None)
974
+ table = getattr(stmt, "table", None)
975
+ if table is None:
976
+ return False
977
+ if getattr(stmt, "_post_values_clause", None) is not None:
978
+ return False
979
+
980
+ column_keys = getattr(compiled, "column_keys", None)
981
+ if not column_keys:
982
+ column_keys = getattr(compiled, "positiontup", None)
983
+ if not column_keys and isinstance(parameters[0], dict):
984
+ column_keys = list(parameters[0].keys())
985
+ if not column_keys:
986
+ return False
987
+
988
+ column_names = [
989
+ getattr(column_key, "key", column_key) for column_key in column_keys
990
+ ]
991
+
992
+ data = None
993
+ if isinstance(parameters[0], dict):
994
+ try:
995
+ import pandas as pd # type: ignore[import-not-found]
996
+
997
+ rows = parameters if isinstance(parameters, list) else list(parameters)
998
+ data = pd.DataFrame.from_records(rows, columns=column_names)
999
+ except Exception:
1000
+ data = None
1001
+ if data is None:
1002
+ try:
1003
+ import pyarrow as pa # type: ignore[import-not-found]
1004
+
1005
+ rows = (
1006
+ parameters if isinstance(parameters, list) else list(parameters)
1007
+ )
1008
+ data = pa.Table.from_pylist(rows)
1009
+ if column_names:
1010
+ data = data.select(column_names)
1011
+ except Exception:
1012
+ return False
1013
+ else:
1014
+ try:
1015
+ import pandas as pd # type: ignore[import-not-found]
1016
+
1017
+ rows = parameters if isinstance(parameters, list) else list(parameters)
1018
+ data = pd.DataFrame(rows, columns=column_names)
1019
+ except Exception:
1020
+ data = None
1021
+ if data is None:
1022
+ try:
1023
+ import pyarrow as pa # type: ignore[import-not-found]
1024
+
1025
+ rows = (
1026
+ parameters if isinstance(parameters, list) else list(parameters)
1027
+ )
1028
+ if rows:
1029
+ cols = list(zip(*rows))
1030
+ else:
1031
+ cols = [[] for _ in column_names]
1032
+ data = pa.Table.from_arrays(cols, names=column_names)
1033
+ except Exception:
1034
+ return False
1035
+
1036
+ view_name = f"__duckdb_sa_bulk_{uuid.uuid4().hex}"
1037
+ dbapi_conn = cursor.connection
1038
+ dbapi_conn.register(view_name, data)
1039
+ preparer = getattr(context, "identifier_preparer", self.identifier_preparer)
1040
+ target = preparer.format_table(table)
1041
+ columns = ", ".join(preparer.quote(col) for col in column_names)
1042
+ insert_sql = (
1043
+ f"INSERT INTO {target} ({columns}) SELECT {columns} FROM {view_name}"
1044
+ )
1045
+ try:
1046
+ cursor.execute(insert_sql)
1047
+ finally:
1048
+ try:
1049
+ dbapi_conn.unregister(view_name)
1050
+ except Exception:
1051
+ pass
1052
+ return True
1053
+
1054
+ def do_executemany(
1055
+ self, cursor: Any, statement: Any, parameters: Any, context: Optional[Any] = ...
1056
+ ) -> None:
1057
+ if (
1058
+ context is not None
1059
+ and getattr(context, "isinsert", False)
1060
+ and parameters
1061
+ and isinstance(parameters, (list, tuple))
1062
+ ):
1063
+ options = self._get_execution_options(context)
1064
+ copy_threshold = options.get(
1065
+ "duckdb_copy_threshold", self.duckdb_copy_threshold
1066
+ )
1067
+ if copy_threshold and len(parameters) >= copy_threshold:
1068
+ if self._bulk_insert_via_register(cursor, context, parameters):
1069
+ return None
1070
+ return DefaultDialect.do_executemany(
1071
+ self, cursor, statement, parameters, context
1072
+ )
1073
+
1074
+ def is_disconnect(self, e: Exception, connection: Any, cursor: Any) -> bool:
1075
+ message = str(e).lower()
1076
+ return any(pattern in message for pattern in DISCONNECT_ERROR_PATTERNS)
1077
+
1078
+ def _execute_with_retry(
1079
+ self,
1080
+ cursor: Any,
1081
+ statement: str,
1082
+ parameters: Any,
1083
+ context: Optional[Any],
1084
+ executor: Any,
1085
+ ) -> Any:
1086
+ options = self._get_execution_options(context)
1087
+ retry_count = int(options.get("duckdb_retry_count", 0) or 0)
1088
+ if options.get("duckdb_retry_on_transient") and retry_count == 0:
1089
+ retry_count = 1
1090
+ if retry_count <= 0 or not _is_idempotent_statement(statement):
1091
+ return executor()
1092
+ backoff = options.get("duckdb_retry_backoff")
1093
+ attempt = 0
1094
+ while True:
1095
+ try:
1096
+ return executor()
1097
+ except Exception as exc:
1098
+ if attempt >= retry_count or not _is_transient_error(exc):
1099
+ raise
1100
+ attempt += 1
1101
+ if backoff:
1102
+ time.sleep(float(backoff))
1103
+
1104
+ def do_execute(
1105
+ self,
1106
+ cursor: Any,
1107
+ statement: str,
1108
+ parameters: Any,
1109
+ context: Optional[Any] = None,
1110
+ ) -> None:
1111
+ def executor() -> Any:
1112
+ return DefaultDialect.do_execute(
1113
+ self, cursor, statement, parameters, context
1114
+ )
1115
+
1116
+ self._execute_with_retry(cursor, statement, parameters, context, executor)
1117
+
1118
+ def do_execute_no_params(
1119
+ self,
1120
+ cursor: Any,
1121
+ statement: str,
1122
+ context: Optional[Any] = None,
1123
+ ) -> None:
1124
+ def executor() -> Any:
1125
+ return DefaultDialect.do_execute_no_params(self, cursor, statement, context)
1126
+
1127
+ self._execute_with_retry(cursor, statement, None, context, executor)
1128
+
1129
+ def _pg_class_filter_scope_schema(
1130
+ self,
1131
+ query: Select,
1132
+ schema: Optional[str],
1133
+ scope: Any,
1134
+ pg_class_table: Any = None,
1135
+ ) -> Any:
1136
+ # Scope by schema, but strip any database prefix (DuckDB uses db.schema).
1137
+ # This will not work if a schema or table name is not unique!
1138
+ if hasattr(super(), "_pg_class_filter_scope_schema"):
1139
+ schema_arg = schema
1140
+ if schema is not None:
1141
+ _, schema_name = self.identifier_preparer._separate(schema)
1142
+ schema_arg = schema_name
1143
+ return getattr(super(), "_pg_class_filter_scope_schema")(
1144
+ query,
1145
+ schema=schema_arg,
1146
+ scope=scope,
1147
+ pg_class_table=pg_class_table,
1148
+ )
1149
+
1150
+ @lru_cache()
1151
+ def _columns_query(self, schema, has_filter_names, scope, kind): # type: ignore[no-untyped-def]
1152
+ if not SQLALCHEMY_2:
1153
+ return super()._columns_query(schema, has_filter_names, scope, kind) # type: ignore[misc]
1154
+
1155
+ # DuckDB versions before 1.4 don't expose pg_collation; skip collation
1156
+ # reflection to avoid Catalog Errors during SQLAlchemy 2.x reflection.
1157
+ from sqlalchemy.dialects.postgresql import base as pg_base
1158
+
1159
+ pg_catalog = pg_base.pg_catalog
1160
+ REGCLASS = pg_base.REGCLASS
1161
+ TEXT = pg_base.TEXT
1162
+ OID = pg_base.OID
1163
+
1164
+ server_version_info = self.server_version_info or (0,)
1165
+
1166
+ generated = (
1167
+ pg_catalog.pg_attribute.c.attgenerated.label("generated")
1168
+ if server_version_info >= (12,)
1169
+ else sql.null().label("generated")
1170
+ )
1171
+ if server_version_info >= (10,):
1172
+ identity = (
1173
+ select(
1174
+ sql.func.json_build_object(
1175
+ "always",
1176
+ pg_catalog.pg_attribute.c.attidentity == "a",
1177
+ "start",
1178
+ pg_catalog.pg_sequence.c.seqstart,
1179
+ "increment",
1180
+ pg_catalog.pg_sequence.c.seqincrement,
1181
+ "minvalue",
1182
+ pg_catalog.pg_sequence.c.seqmin,
1183
+ "maxvalue",
1184
+ pg_catalog.pg_sequence.c.seqmax,
1185
+ "cache",
1186
+ pg_catalog.pg_sequence.c.seqcache,
1187
+ "cycle",
1188
+ pg_catalog.pg_sequence.c.seqcycle,
1189
+ type_=sqltypes.JSON(),
1190
+ )
1191
+ )
1192
+ .select_from(pg_catalog.pg_sequence)
1193
+ .where(
1194
+ pg_catalog.pg_attribute.c.attidentity != "",
1195
+ pg_catalog.pg_sequence.c.seqrelid
1196
+ == sql.cast(
1197
+ sql.cast(
1198
+ pg_catalog.pg_get_serial_sequence(
1199
+ sql.cast(
1200
+ sql.cast(
1201
+ pg_catalog.pg_attribute.c.attrelid,
1202
+ REGCLASS,
1203
+ ),
1204
+ TEXT,
1205
+ ),
1206
+ pg_catalog.pg_attribute.c.attname,
1207
+ ),
1208
+ REGCLASS,
1209
+ ),
1210
+ OID,
1211
+ ),
1212
+ )
1213
+ .correlate(pg_catalog.pg_attribute)
1214
+ .scalar_subquery()
1215
+ .label("identity_options")
1216
+ )
1217
+ else:
1218
+ identity = sql.null().label("identity_options")
1219
+
1220
+ default = (
1221
+ select(
1222
+ pg_catalog.pg_get_expr(
1223
+ pg_catalog.pg_attrdef.c.adbin,
1224
+ pg_catalog.pg_attrdef.c.adrelid,
1225
+ )
1226
+ )
1227
+ .select_from(pg_catalog.pg_attrdef)
1228
+ .where(
1229
+ pg_catalog.pg_attrdef.c.adrelid == pg_catalog.pg_attribute.c.attrelid,
1230
+ pg_catalog.pg_attrdef.c.adnum == pg_catalog.pg_attribute.c.attnum,
1231
+ pg_catalog.pg_attribute.c.atthasdef,
1232
+ )
1233
+ .correlate(pg_catalog.pg_attribute)
1234
+ .scalar_subquery()
1235
+ .label("default")
1236
+ )
1237
+
1238
+ collate = sql.null().label("collation")
1239
+
1240
+ relkinds = self._kind_to_relkinds(kind)
1241
+ query = (
1242
+ select(
1243
+ pg_catalog.pg_attribute.c.attname.label("name"),
1244
+ pg_catalog.format_type(
1245
+ pg_catalog.pg_attribute.c.atttypid,
1246
+ pg_catalog.pg_attribute.c.atttypmod,
1247
+ ).label("format_type"),
1248
+ default,
1249
+ pg_catalog.pg_attribute.c.attnotnull.label("not_null"),
1250
+ pg_catalog.pg_class.c.relname.label("table_name"),
1251
+ pg_catalog.pg_description.c.description.label("comment"),
1252
+ generated,
1253
+ identity,
1254
+ collate,
1255
+ )
1256
+ .select_from(pg_catalog.pg_class)
1257
+ .outerjoin(
1258
+ pg_catalog.pg_attribute,
1259
+ sql.and_(
1260
+ pg_catalog.pg_class.c.oid == pg_catalog.pg_attribute.c.attrelid,
1261
+ pg_catalog.pg_attribute.c.attnum > 0,
1262
+ ~pg_catalog.pg_attribute.c.attisdropped,
1263
+ ),
1264
+ )
1265
+ .outerjoin(
1266
+ pg_catalog.pg_description,
1267
+ sql.and_(
1268
+ pg_catalog.pg_description.c.objoid
1269
+ == pg_catalog.pg_attribute.c.attrelid,
1270
+ pg_catalog.pg_description.c.objsubid
1271
+ == pg_catalog.pg_attribute.c.attnum,
1272
+ ),
1273
+ )
1274
+ .where(self._pg_class_relkind_condition(relkinds))
1275
+ .order_by(pg_catalog.pg_class.c.relname, pg_catalog.pg_attribute.c.attnum)
1276
+ )
1277
+ query = self._pg_class_filter_scope_schema(query, schema, scope=scope)
1278
+ if has_filter_names:
1279
+ query = query.where(
1280
+ pg_catalog.pg_class.c.relname.in_(bindparam("filter_names"))
1281
+ )
1282
+ return query
1283
+
1284
+ # FIXME: this method is a hack around the fact that we use a single cursor for all queries inside a connection,
1285
+ # and this is required to fix get_multi_columns
1286
+ def get_multi_columns(
1287
+ self,
1288
+ connection: "Connection",
1289
+ schema: Optional[str] = None,
1290
+ filter_names: Optional[Collection[str]] = None,
1291
+ scope: Any = None,
1292
+ kind: Any = None,
1293
+ **kw: Any,
1294
+ ) -> Any:
1295
+ """
1296
+ Copyright 2005-2023 SQLAlchemy authors and contributors <see AUTHORS file>.
1297
+
1298
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
1299
+ this software and associated documentation files (the "Software"), to deal in
1300
+ the Software without restriction, including without limitation the rights to
1301
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
1302
+ of the Software, and to permit persons to whom the Software is furnished to do
1303
+ so, subject to the following conditions:
1304
+
1305
+ The above copyright notice and this permission notice shall be included in all
1306
+ copies or substantial portions of the Software.
1307
+
1308
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1309
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1310
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1311
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1312
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1313
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1314
+ SOFTWARE.
1315
+ """
1316
+
1317
+ scope = kw.get("scope", scope)
1318
+ kind = kw.get("kind", kind)
1319
+ has_filter_names, params = self._prepare_filter_names(filter_names) # type: ignore[attr-defined]
1320
+ query = self._columns_query(schema, has_filter_names, scope, kind) # type: ignore[attr-defined]
1321
+ rows = list(connection.execute(query, params).mappings())
1322
+
1323
+ # dictionary with (name, ) if default search path or (schema, name)
1324
+ # as keys
1325
+ domains: Dict[tuple, dict] = {}
1326
+ """
1327
+ TODO: fix these pg_collation errors in SQLA2
1328
+ domains = {
1329
+ ((d["schema"], d["name"]) if not d["visible"] else (d["name"],)): d
1330
+ for d in self._load_domains( # type: ignore[attr-defined]
1331
+ connection, schema="*", info_cache=kw.get("info_cache")
1332
+ )
1333
+ }
1334
+ """
1335
+
1336
+ # dictionary with (name, ) if default search path or (schema, name)
1337
+ # as keys
1338
+ enums = dict(
1339
+ (
1340
+ ((rec["name"],), rec)
1341
+ if rec["visible"]
1342
+ else ((rec["schema"], rec["name"]), rec)
1343
+ )
1344
+ for rec in self._load_enums( # type: ignore[attr-defined]
1345
+ connection, schema="*", info_cache=kw.get("info_cache")
1346
+ )
1347
+ )
1348
+
1349
+ columns = self._get_columns_info(rows, domains, enums, schema) # type: ignore[attr-defined]
1350
+
1351
+ return columns.items()
1352
+
1353
+ # fix for https://github.com/leonardovida/duckdb-sqlalchemy/issues/1128
1354
+ # (Overrides sqlalchemy method)
1355
+ @lru_cache()
1356
+ def _comment_query( # type: ignore[no-untyped-def]
1357
+ self, schema: str, has_filter_names: bool, scope: Any, kind: Any
1358
+ ):
1359
+ if SQLALCHEMY_VERSION >= Version("2.0.36"):
1360
+ from sqlalchemy.dialects.postgresql import ( # type: ignore[attr-defined]
1361
+ pg_catalog,
1362
+ )
1363
+
1364
+ if (
1365
+ hasattr(super(), "_kind_to_relkinds")
1366
+ and hasattr(super(), "_pg_class_filter_scope_schema")
1367
+ and hasattr(super(), "_pg_class_relkind_condition")
1368
+ ):
1369
+ relkinds = getattr(super(), "_kind_to_relkinds")(kind)
1370
+ query = (
1371
+ select(
1372
+ pg_catalog.pg_class.c.relname,
1373
+ pg_catalog.pg_description.c.description,
1374
+ )
1375
+ .select_from(pg_catalog.pg_class)
1376
+ .outerjoin(
1377
+ pg_catalog.pg_description,
1378
+ sql.and_(
1379
+ pg_catalog.pg_class.c.oid
1380
+ == pg_catalog.pg_description.c.objoid,
1381
+ pg_catalog.pg_description.c.objsubid == 0,
1382
+ ),
1383
+ )
1384
+ .where(getattr(super(), "_pg_class_relkind_condition")(relkinds))
1385
+ )
1386
+ query = self._pg_class_filter_scope_schema(query, schema, scope)
1387
+ if has_filter_names:
1388
+ query = query.where(
1389
+ pg_catalog.pg_class.c.relname.in_(bindparam("filter_names"))
1390
+ )
1391
+ return query
1392
+ else:
1393
+ if hasattr(super(), "_comment_query"):
1394
+ return getattr(super(), "_comment_query")(
1395
+ schema, has_filter_names, scope, kind
1396
+ )
1397
+
1398
+
1399
+ if SQLALCHEMY_VERSION >= Version("2.0.14"):
1400
+ from sqlalchemy import TryCast # type: ignore[attr-defined]
1401
+
1402
+ @compiles(TryCast, "duckdb") # type: ignore[misc]
1403
+ def visit_try_cast(
1404
+ instance: TryCast,
1405
+ compiler: Any,
1406
+ **kw: Any,
1407
+ ) -> str:
1408
+ return "TRY_CAST({} AS {})".format(
1409
+ compiler.process(instance.clause, **kw),
1410
+ compiler.process(instance.typeclause, **kw),
1411
+ )