psycopg.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. # dialects/postgresql/psycopg.py
  2. # Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. # mypy: ignore-errors
  8. r"""
  9. .. dialect:: postgresql+psycopg
  10. :name: psycopg (a.k.a. psycopg 3)
  11. :dbapi: psycopg
  12. :connectstring: postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...]
  13. :url: https://pypi.org/project/psycopg/
  14. ``psycopg`` is the package and module name for version 3 of the ``psycopg``
  15. database driver, formerly known as ``psycopg2``. This driver is different
  16. enough from its ``psycopg2`` predecessor that SQLAlchemy supports it
  17. via a totally separate dialect; support for ``psycopg2`` is expected to remain
  18. for as long as that package continues to function for modern Python versions,
  19. and also remains the default dialect for the ``postgresql://`` dialect
  20. series.
  21. The SQLAlchemy ``psycopg`` dialect provides both a sync and an async
  22. implementation under the same dialect name. The proper version is
  23. selected depending on how the engine is created:
  24. * calling :func:`_sa.create_engine` with ``postgresql+psycopg://...`` will
  25. automatically select the sync version, e.g.::
  26. from sqlalchemy import create_engine
  27. sync_engine = create_engine("postgresql+psycopg://scott:tiger@localhost/test")
  28. * calling :func:`_asyncio.create_async_engine` with
  29. ``postgresql+psycopg://...`` will automatically select the async version,
  30. e.g.::
  31. from sqlalchemy.ext.asyncio import create_async_engine
  32. asyncio_engine = create_async_engine("postgresql+psycopg://scott:tiger@localhost/test")
  33. The asyncio version of the dialect may also be specified explicitly using the
  34. ``psycopg_async`` suffix, as::
  35. from sqlalchemy.ext.asyncio import create_async_engine
  36. asyncio_engine = create_async_engine("postgresql+psycopg_async://scott:tiger@localhost/test")
  37. .. seealso::
  38. :ref:`postgresql_psycopg2` - The SQLAlchemy ``psycopg``
  39. dialect shares most of its behavior with the ``psycopg2`` dialect.
  40. Further documentation is available there.
  41. """ # noqa
  42. from __future__ import annotations
  43. import logging
  44. import re
  45. from typing import cast
  46. from typing import TYPE_CHECKING
  47. from . import ranges
  48. from ._psycopg_common import _PGDialect_common_psycopg
  49. from ._psycopg_common import _PGExecutionContext_common_psycopg
  50. from .base import INTERVAL
  51. from .base import PGCompiler
  52. from .base import PGIdentifierPreparer
  53. from .base import REGCONFIG
  54. from .json import JSON
  55. from .json import JSONB
  56. from .json import JSONPathType
  57. from .types import CITEXT
  58. from ... import pool
  59. from ... import util
  60. from ...engine import AdaptedConnection
  61. from ...sql import sqltypes
  62. from ...util.concurrency import await_fallback
  63. from ...util.concurrency import await_only
  64. if TYPE_CHECKING:
  65. from typing import Iterable
  66. from psycopg import AsyncConnection
  67. logger = logging.getLogger("sqlalchemy.dialects.postgresql")
  68. class _PGString(sqltypes.String):
  69. render_bind_cast = True
  70. class _PGREGCONFIG(REGCONFIG):
  71. render_bind_cast = True
  72. class _PGJSON(JSON):
  73. render_bind_cast = True
  74. def bind_processor(self, dialect):
  75. return self._make_bind_processor(None, dialect._psycopg_Json)
  76. def result_processor(self, dialect, coltype):
  77. return None
  78. class _PGJSONB(JSONB):
  79. render_bind_cast = True
  80. def bind_processor(self, dialect):
  81. return self._make_bind_processor(None, dialect._psycopg_Jsonb)
  82. def result_processor(self, dialect, coltype):
  83. return None
  84. class _PGJSONIntIndexType(sqltypes.JSON.JSONIntIndexType):
  85. __visit_name__ = "json_int_index"
  86. render_bind_cast = True
  87. class _PGJSONStrIndexType(sqltypes.JSON.JSONStrIndexType):
  88. __visit_name__ = "json_str_index"
  89. render_bind_cast = True
  90. class _PGJSONPathType(JSONPathType):
  91. pass
  92. class _PGInterval(INTERVAL):
  93. render_bind_cast = True
  94. class _PGTimeStamp(sqltypes.DateTime):
  95. render_bind_cast = True
  96. class _PGDate(sqltypes.Date):
  97. render_bind_cast = True
  98. class _PGTime(sqltypes.Time):
  99. render_bind_cast = True
  100. class _PGInteger(sqltypes.Integer):
  101. render_bind_cast = True
  102. class _PGSmallInteger(sqltypes.SmallInteger):
  103. render_bind_cast = True
  104. class _PGNullType(sqltypes.NullType):
  105. render_bind_cast = True
  106. class _PGBigInteger(sqltypes.BigInteger):
  107. render_bind_cast = True
  108. class _PGBoolean(sqltypes.Boolean):
  109. render_bind_cast = True
  110. class _PsycopgRange(ranges.AbstractRangeImpl):
  111. def bind_processor(self, dialect):
  112. psycopg_Range = cast(PGDialect_psycopg, dialect)._psycopg_Range
  113. def to_range(value):
  114. if isinstance(value, ranges.Range):
  115. value = psycopg_Range(
  116. value.lower, value.upper, value.bounds, value.empty
  117. )
  118. return value
  119. return to_range
  120. def result_processor(self, dialect, coltype):
  121. def to_range(value):
  122. if value is not None:
  123. value = ranges.Range(
  124. value._lower,
  125. value._upper,
  126. bounds=value._bounds if value._bounds else "[)",
  127. empty=not value._bounds,
  128. )
  129. return value
  130. return to_range
  131. class _PsycopgMultiRange(ranges.AbstractMultiRangeImpl):
  132. def bind_processor(self, dialect):
  133. psycopg_Range = cast(PGDialect_psycopg, dialect)._psycopg_Range
  134. psycopg_Multirange = cast(
  135. PGDialect_psycopg, dialect
  136. )._psycopg_Multirange
  137. NoneType = type(None)
  138. def to_range(value):
  139. if isinstance(value, (str, NoneType, psycopg_Multirange)):
  140. return value
  141. return psycopg_Multirange(
  142. [
  143. psycopg_Range(
  144. element.lower,
  145. element.upper,
  146. element.bounds,
  147. element.empty,
  148. )
  149. for element in cast("Iterable[ranges.Range]", value)
  150. ]
  151. )
  152. return to_range
  153. def result_processor(self, dialect, coltype):
  154. def to_range(value):
  155. if value is not None:
  156. value = [
  157. ranges.Range(
  158. elem._lower,
  159. elem._upper,
  160. bounds=elem._bounds if elem._bounds else "[)",
  161. empty=not elem._bounds,
  162. )
  163. for elem in value
  164. ]
  165. return value
  166. return to_range
  167. class PGExecutionContext_psycopg(_PGExecutionContext_common_psycopg):
  168. pass
  169. class PGCompiler_psycopg(PGCompiler):
  170. pass
  171. class PGIdentifierPreparer_psycopg(PGIdentifierPreparer):
  172. pass
  173. def _log_notices(diagnostic):
  174. logger.info("%s: %s", diagnostic.severity, diagnostic.message_primary)
  175. class PGDialect_psycopg(_PGDialect_common_psycopg):
  176. driver = "psycopg"
  177. supports_statement_cache = True
  178. supports_server_side_cursors = True
  179. default_paramstyle = "pyformat"
  180. supports_sane_multi_rowcount = True
  181. execution_ctx_cls = PGExecutionContext_psycopg
  182. statement_compiler = PGCompiler_psycopg
  183. preparer = PGIdentifierPreparer_psycopg
  184. psycopg_version = (0, 0)
  185. _has_native_hstore = True
  186. _psycopg_adapters_map = None
  187. colspecs = util.update_copy(
  188. _PGDialect_common_psycopg.colspecs,
  189. {
  190. sqltypes.String: _PGString,
  191. REGCONFIG: _PGREGCONFIG,
  192. JSON: _PGJSON,
  193. CITEXT: CITEXT,
  194. sqltypes.JSON: _PGJSON,
  195. JSONB: _PGJSONB,
  196. sqltypes.JSON.JSONPathType: _PGJSONPathType,
  197. sqltypes.JSON.JSONIntIndexType: _PGJSONIntIndexType,
  198. sqltypes.JSON.JSONStrIndexType: _PGJSONStrIndexType,
  199. sqltypes.Interval: _PGInterval,
  200. INTERVAL: _PGInterval,
  201. sqltypes.Date: _PGDate,
  202. sqltypes.DateTime: _PGTimeStamp,
  203. sqltypes.Time: _PGTime,
  204. sqltypes.Integer: _PGInteger,
  205. sqltypes.SmallInteger: _PGSmallInteger,
  206. sqltypes.BigInteger: _PGBigInteger,
  207. ranges.AbstractRange: _PsycopgRange,
  208. ranges.AbstractMultiRange: _PsycopgMultiRange,
  209. },
  210. )
  211. def __init__(self, **kwargs):
  212. super().__init__(**kwargs)
  213. if self.dbapi:
  214. m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", self.dbapi.__version__)
  215. if m:
  216. self.psycopg_version = tuple(
  217. int(x) for x in m.group(1, 2, 3) if x is not None
  218. )
  219. if self.psycopg_version < (3, 0, 2):
  220. raise ImportError(
  221. "psycopg version 3.0.2 or higher is required."
  222. )
  223. from psycopg.adapt import AdaptersMap
  224. self._psycopg_adapters_map = adapters_map = AdaptersMap(
  225. self.dbapi.adapters
  226. )
  227. if self._native_inet_types is False:
  228. import psycopg.types.string
  229. adapters_map.register_loader(
  230. "inet", psycopg.types.string.TextLoader
  231. )
  232. adapters_map.register_loader(
  233. "cidr", psycopg.types.string.TextLoader
  234. )
  235. if self._json_deserializer:
  236. from psycopg.types.json import set_json_loads
  237. set_json_loads(self._json_deserializer, adapters_map)
  238. if self._json_serializer:
  239. from psycopg.types.json import set_json_dumps
  240. set_json_dumps(self._json_serializer, adapters_map)
  241. def create_connect_args(self, url):
  242. # see https://github.com/psycopg/psycopg/issues/83
  243. cargs, cparams = super().create_connect_args(url)
  244. if self._psycopg_adapters_map:
  245. cparams["context"] = self._psycopg_adapters_map
  246. if self.client_encoding is not None:
  247. cparams["client_encoding"] = self.client_encoding
  248. return cargs, cparams
  249. def _type_info_fetch(self, connection, name):
  250. from psycopg.types import TypeInfo
  251. return TypeInfo.fetch(connection.connection.driver_connection, name)
  252. def initialize(self, connection):
  253. super().initialize(connection)
  254. # PGDialect.initialize() checks server version for <= 8.2 and sets
  255. # this flag to False if so
  256. if not self.insert_returning:
  257. self.insert_executemany_returning = False
  258. # HSTORE can't be registered until we have a connection so that
  259. # we can look up its OID, so we set up this adapter in
  260. # initialize()
  261. if self.use_native_hstore:
  262. info = self._type_info_fetch(connection, "hstore")
  263. self._has_native_hstore = info is not None
  264. if self._has_native_hstore:
  265. from psycopg.types.hstore import register_hstore
  266. # register the adapter for connections made subsequent to
  267. # this one
  268. register_hstore(info, self._psycopg_adapters_map)
  269. # register the adapter for this connection
  270. register_hstore(info, connection.connection)
  271. @classmethod
  272. def import_dbapi(cls):
  273. import psycopg
  274. return psycopg
  275. @classmethod
  276. def get_async_dialect_cls(cls, url):
  277. return PGDialectAsync_psycopg
  278. @util.memoized_property
  279. def _isolation_lookup(self):
  280. return {
  281. "READ COMMITTED": self.dbapi.IsolationLevel.READ_COMMITTED,
  282. "READ UNCOMMITTED": self.dbapi.IsolationLevel.READ_UNCOMMITTED,
  283. "REPEATABLE READ": self.dbapi.IsolationLevel.REPEATABLE_READ,
  284. "SERIALIZABLE": self.dbapi.IsolationLevel.SERIALIZABLE,
  285. }
  286. @util.memoized_property
  287. def _psycopg_Json(self):
  288. from psycopg.types import json
  289. return json.Json
  290. @util.memoized_property
  291. def _psycopg_Jsonb(self):
  292. from psycopg.types import json
  293. return json.Jsonb
  294. @util.memoized_property
  295. def _psycopg_TransactionStatus(self):
  296. from psycopg.pq import TransactionStatus
  297. return TransactionStatus
  298. @util.memoized_property
  299. def _psycopg_Range(self):
  300. from psycopg.types.range import Range
  301. return Range
  302. @util.memoized_property
  303. def _psycopg_Multirange(self):
  304. from psycopg.types.multirange import Multirange
  305. return Multirange
  306. def _do_isolation_level(self, connection, autocommit, isolation_level):
  307. connection.autocommit = autocommit
  308. connection.isolation_level = isolation_level
  309. def get_isolation_level(self, dbapi_connection):
  310. status_before = dbapi_connection.info.transaction_status
  311. value = super().get_isolation_level(dbapi_connection)
  312. # don't rely on psycopg providing enum symbols, compare with
  313. # eq/ne
  314. if status_before == self._psycopg_TransactionStatus.IDLE:
  315. dbapi_connection.rollback()
  316. return value
  317. def set_isolation_level(self, dbapi_connection, level):
  318. if level == "AUTOCOMMIT":
  319. self._do_isolation_level(
  320. dbapi_connection, autocommit=True, isolation_level=None
  321. )
  322. else:
  323. self._do_isolation_level(
  324. dbapi_connection,
  325. autocommit=False,
  326. isolation_level=self._isolation_lookup[level],
  327. )
  328. def set_readonly(self, connection, value):
  329. connection.read_only = value
  330. def get_readonly(self, connection):
  331. return connection.read_only
  332. def on_connect(self):
  333. def notices(conn):
  334. conn.add_notice_handler(_log_notices)
  335. fns = [notices]
  336. if self.isolation_level is not None:
  337. def on_connect(conn):
  338. self.set_isolation_level(conn, self.isolation_level)
  339. fns.append(on_connect)
  340. # fns always has the notices function
  341. def on_connect(conn):
  342. for fn in fns:
  343. fn(conn)
  344. return on_connect
  345. def is_disconnect(self, e, connection, cursor):
  346. if isinstance(e, self.dbapi.Error) and connection is not None:
  347. if connection.closed or connection.broken:
  348. return True
  349. return False
  350. def _do_prepared_twophase(self, connection, command, recover=False):
  351. dbapi_conn = connection.connection.dbapi_connection
  352. if (
  353. recover
  354. # don't rely on psycopg providing enum symbols, compare with
  355. # eq/ne
  356. or dbapi_conn.info.transaction_status
  357. != self._psycopg_TransactionStatus.IDLE
  358. ):
  359. dbapi_conn.rollback()
  360. before_autocommit = dbapi_conn.autocommit
  361. try:
  362. if not before_autocommit:
  363. self._do_autocommit(dbapi_conn, True)
  364. dbapi_conn.execute(command)
  365. finally:
  366. if not before_autocommit:
  367. self._do_autocommit(dbapi_conn, before_autocommit)
  368. def do_rollback_twophase(
  369. self, connection, xid, is_prepared=True, recover=False
  370. ):
  371. if is_prepared:
  372. self._do_prepared_twophase(
  373. connection, f"ROLLBACK PREPARED '{xid}'", recover=recover
  374. )
  375. else:
  376. self.do_rollback(connection.connection)
  377. def do_commit_twophase(
  378. self, connection, xid, is_prepared=True, recover=False
  379. ):
  380. if is_prepared:
  381. self._do_prepared_twophase(
  382. connection, f"COMMIT PREPARED '{xid}'", recover=recover
  383. )
  384. else:
  385. self.do_commit(connection.connection)
  386. @util.memoized_property
  387. def _dialect_specific_select_one(self):
  388. return ";"
  389. class AsyncAdapt_psycopg_cursor:
  390. __slots__ = ("_cursor", "await_", "_rows")
  391. _psycopg_ExecStatus = None
  392. def __init__(self, cursor, await_) -> None:
  393. self._cursor = cursor
  394. self.await_ = await_
  395. self._rows = []
  396. def __getattr__(self, name):
  397. return getattr(self._cursor, name)
  398. @property
  399. def arraysize(self):
  400. return self._cursor.arraysize
  401. @arraysize.setter
  402. def arraysize(self, value):
  403. self._cursor.arraysize = value
  404. def close(self):
  405. self._rows.clear()
  406. # Normal cursor just call _close() in a non-sync way.
  407. self._cursor._close()
  408. def execute(self, query, params=None, **kw):
  409. result = self.await_(self._cursor.execute(query, params, **kw))
  410. # sqlalchemy result is not async, so need to pull all rows here
  411. res = self._cursor.pgresult
  412. # don't rely on psycopg providing enum symbols, compare with
  413. # eq/ne
  414. if res and res.status == self._psycopg_ExecStatus.TUPLES_OK:
  415. rows = self.await_(self._cursor.fetchall())
  416. if not isinstance(rows, list):
  417. self._rows = list(rows)
  418. else:
  419. self._rows = rows
  420. return result
  421. def executemany(self, query, params_seq):
  422. return self.await_(self._cursor.executemany(query, params_seq))
  423. def __iter__(self):
  424. # TODO: try to avoid pop(0) on a list
  425. while self._rows:
  426. yield self._rows.pop(0)
  427. def fetchone(self):
  428. if self._rows:
  429. # TODO: try to avoid pop(0) on a list
  430. return self._rows.pop(0)
  431. else:
  432. return None
  433. def fetchmany(self, size=None):
  434. if size is None:
  435. size = self._cursor.arraysize
  436. retval = self._rows[0:size]
  437. self._rows = self._rows[size:]
  438. return retval
  439. def fetchall(self):
  440. retval = self._rows
  441. self._rows = []
  442. return retval
  443. class AsyncAdapt_psycopg_ss_cursor(AsyncAdapt_psycopg_cursor):
  444. def execute(self, query, params=None, **kw):
  445. self.await_(self._cursor.execute(query, params, **kw))
  446. return self
  447. def close(self):
  448. self.await_(self._cursor.close())
  449. def fetchone(self):
  450. return self.await_(self._cursor.fetchone())
  451. def fetchmany(self, size=0):
  452. return self.await_(self._cursor.fetchmany(size))
  453. def fetchall(self):
  454. return self.await_(self._cursor.fetchall())
  455. def __iter__(self):
  456. iterator = self._cursor.__aiter__()
  457. while True:
  458. try:
  459. yield self.await_(iterator.__anext__())
  460. except StopAsyncIteration:
  461. break
  462. class AsyncAdapt_psycopg_connection(AdaptedConnection):
  463. _connection: AsyncConnection
  464. __slots__ = ()
  465. await_ = staticmethod(await_only)
  466. def __init__(self, connection) -> None:
  467. self._connection = connection
  468. def __getattr__(self, name):
  469. return getattr(self._connection, name)
  470. def execute(self, query, params=None, **kw):
  471. cursor = self.await_(self._connection.execute(query, params, **kw))
  472. return AsyncAdapt_psycopg_cursor(cursor, self.await_)
  473. def cursor(self, *args, **kw):
  474. cursor = self._connection.cursor(*args, **kw)
  475. if hasattr(cursor, "name"):
  476. return AsyncAdapt_psycopg_ss_cursor(cursor, self.await_)
  477. else:
  478. return AsyncAdapt_psycopg_cursor(cursor, self.await_)
  479. def commit(self):
  480. self.await_(self._connection.commit())
  481. def rollback(self):
  482. self.await_(self._connection.rollback())
  483. def close(self):
  484. self.await_(self._connection.close())
  485. @property
  486. def autocommit(self):
  487. return self._connection.autocommit
  488. @autocommit.setter
  489. def autocommit(self, value):
  490. self.set_autocommit(value)
  491. def set_autocommit(self, value):
  492. self.await_(self._connection.set_autocommit(value))
  493. def set_isolation_level(self, value):
  494. self.await_(self._connection.set_isolation_level(value))
  495. def set_read_only(self, value):
  496. self.await_(self._connection.set_read_only(value))
  497. def set_deferrable(self, value):
  498. self.await_(self._connection.set_deferrable(value))
  499. class AsyncAdaptFallback_psycopg_connection(AsyncAdapt_psycopg_connection):
  500. __slots__ = ()
  501. await_ = staticmethod(await_fallback)
  502. class PsycopgAdaptDBAPI:
  503. def __init__(self, psycopg) -> None:
  504. self.psycopg = psycopg
  505. for k, v in self.psycopg.__dict__.items():
  506. if k != "connect":
  507. self.__dict__[k] = v
  508. def connect(self, *arg, **kw):
  509. async_fallback = kw.pop("async_fallback", False)
  510. creator_fn = kw.pop(
  511. "async_creator_fn", self.psycopg.AsyncConnection.connect
  512. )
  513. if util.asbool(async_fallback):
  514. return AsyncAdaptFallback_psycopg_connection(
  515. await_fallback(creator_fn(*arg, **kw))
  516. )
  517. else:
  518. return AsyncAdapt_psycopg_connection(
  519. await_only(creator_fn(*arg, **kw))
  520. )
  521. class PGDialectAsync_psycopg(PGDialect_psycopg):
  522. is_async = True
  523. supports_statement_cache = True
  524. @classmethod
  525. def import_dbapi(cls):
  526. import psycopg
  527. from psycopg.pq import ExecStatus
  528. AsyncAdapt_psycopg_cursor._psycopg_ExecStatus = ExecStatus
  529. return PsycopgAdaptDBAPI(psycopg)
  530. @classmethod
  531. def get_pool_class(cls, url):
  532. async_fallback = url.query.get("async_fallback", False)
  533. if util.asbool(async_fallback):
  534. return pool.FallbackAsyncAdaptedQueuePool
  535. else:
  536. return pool.AsyncAdaptedQueuePool
  537. def _type_info_fetch(self, connection, name):
  538. from psycopg.types import TypeInfo
  539. adapted = connection.connection
  540. return adapted.await_(TypeInfo.fetch(adapted.driver_connection, name))
  541. def _do_isolation_level(self, connection, autocommit, isolation_level):
  542. connection.set_autocommit(autocommit)
  543. connection.set_isolation_level(isolation_level)
  544. def _do_autocommit(self, connection, value):
  545. connection.set_autocommit(value)
  546. def set_readonly(self, connection, value):
  547. connection.set_read_only(value)
  548. def set_deferrable(self, connection, value):
  549. connection.set_deferrable(value)
  550. def get_driver_connection(self, connection):
  551. return connection._connection
  552. dialect = PGDialect_psycopg
  553. dialect_async = PGDialectAsync_psycopg