25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

712 satır
22KB

  1. # ext/asyncio/engine.py
  2. # Copyright (C) 2020-2021 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: http://www.opensource.org/licenses/mit-license.php
  7. from . import exc as async_exc
  8. from .base import ProxyComparable
  9. from .base import StartableContext
  10. from .result import AsyncResult
  11. from ... import exc
  12. from ... import util
  13. from ...engine import create_engine as _create_engine
  14. from ...engine.base import NestedTransaction
  15. from ...future import Connection
  16. from ...future import Engine
  17. from ...util.concurrency import greenlet_spawn
  18. def create_async_engine(*arg, **kw):
  19. """Create a new async engine instance.
  20. Arguments passed to :func:`_asyncio.create_async_engine` are mostly
  21. identical to those passed to the :func:`_sa.create_engine` function.
  22. The specified dialect must be an asyncio-compatible dialect
  23. such as :ref:`dialect-postgresql-asyncpg`.
  24. .. versionadded:: 1.4
  25. """
  26. if kw.get("server_side_cursors", False):
  27. raise async_exc.AsyncMethodRequired(
  28. "Can't set server_side_cursors for async engine globally; "
  29. "use the connection.stream() method for an async "
  30. "streaming result set"
  31. )
  32. kw["future"] = True
  33. sync_engine = _create_engine(*arg, **kw)
  34. return AsyncEngine(sync_engine)
  35. class AsyncConnectable:
  36. __slots__ = "_slots_dispatch", "__weakref__"
  37. @util.create_proxy_methods(
  38. Connection,
  39. ":class:`_future.Connection`",
  40. ":class:`_asyncio.AsyncConnection`",
  41. classmethods=[],
  42. methods=[],
  43. attributes=[
  44. "closed",
  45. "invalidated",
  46. "dialect",
  47. "default_isolation_level",
  48. ],
  49. )
  50. class AsyncConnection(ProxyComparable, StartableContext, AsyncConnectable):
  51. """An asyncio proxy for a :class:`_engine.Connection`.
  52. :class:`_asyncio.AsyncConnection` is acquired using the
  53. :meth:`_asyncio.AsyncEngine.connect`
  54. method of :class:`_asyncio.AsyncEngine`::
  55. from sqlalchemy.ext.asyncio import create_async_engine
  56. engine = create_async_engine("postgresql+asyncpg://user:pass@host/dbname")
  57. async with engine.connect() as conn:
  58. result = await conn.execute(select(table))
  59. .. versionadded:: 1.4
  60. """ # noqa
  61. # AsyncConnection is a thin proxy; no state should be added here
  62. # that is not retrievable from the "sync" engine / connection, e.g.
  63. # current transaction, info, etc. It should be possible to
  64. # create a new AsyncConnection that matches this one given only the
  65. # "sync" elements.
  66. __slots__ = (
  67. "sync_engine",
  68. "sync_connection",
  69. )
  70. def __init__(self, async_engine, sync_connection=None):
  71. self.engine = async_engine
  72. self.sync_engine = async_engine.sync_engine
  73. self.sync_connection = self._assign_proxied(sync_connection)
  74. @classmethod
  75. def _regenerate_proxy_for_target(cls, target):
  76. return AsyncConnection(
  77. AsyncEngine._retrieve_proxy_for_target(target.engine), target
  78. )
  79. async def start(self, is_ctxmanager=False):
  80. """Start this :class:`_asyncio.AsyncConnection` object's context
  81. outside of using a Python ``with:`` block.
  82. """
  83. if self.sync_connection:
  84. raise exc.InvalidRequestError("connection is already started")
  85. self.sync_connection = self._assign_proxied(
  86. await (greenlet_spawn(self.sync_engine.connect))
  87. )
  88. return self
  89. @property
  90. def connection(self):
  91. """Not implemented for async; call
  92. :meth:`_asyncio.AsyncConnection.get_raw_connection`.
  93. """
  94. raise exc.InvalidRequestError(
  95. "AsyncConnection.connection accessor is not implemented as the "
  96. "attribute may need to reconnect on an invalidated connection. "
  97. "Use the get_raw_connection() method."
  98. )
  99. async def get_raw_connection(self):
  100. """Return the pooled DBAPI-level connection in use by this
  101. :class:`_asyncio.AsyncConnection`.
  102. This is typically the SQLAlchemy connection-pool proxied connection
  103. which then has an attribute .connection that refers to the actual
  104. DBAPI-level connection.
  105. """
  106. conn = self._sync_connection()
  107. return await greenlet_spawn(getattr, conn, "connection")
  108. @property
  109. def _proxied(self):
  110. return self.sync_connection
  111. @property
  112. def info(self):
  113. """Return the :attr:`_engine.Connection.info` dictionary of the
  114. underlying :class:`_engine.Connection`.
  115. This dictionary is freely writable for user-defined state to be
  116. associated with the database connection.
  117. This attribute is only available if the :class:`.AsyncConnection` is
  118. currently connected. If the :attr:`.AsyncConnection.closed` attribute
  119. is ``True``, then accessing this attribute will raise
  120. :class:`.ResourceClosedError`.
  121. .. versionadded:: 1.4.0b2
  122. """
  123. return self.sync_connection.info
  124. def _sync_connection(self):
  125. if not self.sync_connection:
  126. self._raise_for_not_started()
  127. return self.sync_connection
  128. def begin(self):
  129. """Begin a transaction prior to autobegin occurring."""
  130. self._sync_connection()
  131. return AsyncTransaction(self)
  132. def begin_nested(self):
  133. """Begin a nested transaction and return a transaction handle."""
  134. self._sync_connection()
  135. return AsyncTransaction(self, nested=True)
  136. async def invalidate(self, exception=None):
  137. """Invalidate the underlying DBAPI connection associated with
  138. this :class:`_engine.Connection`.
  139. See the method :meth:`_engine.Connection.invalidate` for full
  140. detail on this method.
  141. """
  142. conn = self._sync_connection()
  143. return await greenlet_spawn(conn.invalidate, exception=exception)
  144. async def get_isolation_level(self):
  145. conn = self._sync_connection()
  146. return await greenlet_spawn(conn.get_isolation_level)
  147. async def set_isolation_level(self):
  148. conn = self._sync_connection()
  149. return await greenlet_spawn(conn.get_isolation_level)
  150. def in_transaction(self):
  151. """Return True if a transaction is in progress.
  152. .. versionadded:: 1.4.0b2
  153. """
  154. conn = self._sync_connection()
  155. return conn.in_transaction()
  156. def in_nested_transaction(self):
  157. """Return True if a transaction is in progress.
  158. .. versionadded:: 1.4.0b2
  159. """
  160. conn = self._sync_connection()
  161. return conn.in_nested_transaction()
  162. def get_transaction(self):
  163. """Return an :class:`.AsyncTransaction` representing the current
  164. transaction, if any.
  165. This makes use of the underlying synchronous connection's
  166. :meth:`_engine.Connection.get_transaction` method to get the current
  167. :class:`_engine.Transaction`, which is then proxied in a new
  168. :class:`.AsyncTransaction` object.
  169. .. versionadded:: 1.4.0b2
  170. """
  171. conn = self._sync_connection()
  172. trans = conn.get_transaction()
  173. if trans is not None:
  174. return AsyncTransaction._retrieve_proxy_for_target(trans)
  175. else:
  176. return None
  177. def get_nested_transaction(self):
  178. """Return an :class:`.AsyncTransaction` representing the current
  179. nested (savepoint) transaction, if any.
  180. This makes use of the underlying synchronous connection's
  181. :meth:`_engine.Connection.get_nested_transaction` method to get the
  182. current :class:`_engine.Transaction`, which is then proxied in a new
  183. :class:`.AsyncTransaction` object.
  184. .. versionadded:: 1.4.0b2
  185. """
  186. conn = self._sync_connection()
  187. trans = conn.get_nested_transaction()
  188. if trans is not None:
  189. return AsyncTransaction._retrieve_proxy_for_target(trans)
  190. else:
  191. return None
  192. async def execution_options(self, **opt):
  193. r"""Set non-SQL options for the connection which take effect
  194. during execution.
  195. This returns this :class:`_asyncio.AsyncConnection` object with
  196. the new options added.
  197. See :meth:`_future.Connection.execution_options` for full details
  198. on this method.
  199. """
  200. conn = self._sync_connection()
  201. c2 = await greenlet_spawn(conn.execution_options, **opt)
  202. assert c2 is conn
  203. return self
  204. async def commit(self):
  205. """Commit the transaction that is currently in progress.
  206. This method commits the current transaction if one has been started.
  207. If no transaction was started, the method has no effect, assuming
  208. the connection is in a non-invalidated state.
  209. A transaction is begun on a :class:`_future.Connection` automatically
  210. whenever a statement is first executed, or when the
  211. :meth:`_future.Connection.begin` method is called.
  212. """
  213. conn = self._sync_connection()
  214. await greenlet_spawn(conn.commit)
  215. async def rollback(self):
  216. """Roll back the transaction that is currently in progress.
  217. This method rolls back the current transaction if one has been started.
  218. If no transaction was started, the method has no effect. If a
  219. transaction was started and the connection is in an invalidated state,
  220. the transaction is cleared using this method.
  221. A transaction is begun on a :class:`_future.Connection` automatically
  222. whenever a statement is first executed, or when the
  223. :meth:`_future.Connection.begin` method is called.
  224. """
  225. conn = self._sync_connection()
  226. await greenlet_spawn(conn.rollback)
  227. async def close(self):
  228. """Close this :class:`_asyncio.AsyncConnection`.
  229. This has the effect of also rolling back the transaction if one
  230. is in place.
  231. """
  232. conn = self._sync_connection()
  233. await greenlet_spawn(conn.close)
  234. async def exec_driver_sql(
  235. self,
  236. statement,
  237. parameters=None,
  238. execution_options=util.EMPTY_DICT,
  239. ):
  240. r"""Executes a driver-level SQL string and return buffered
  241. :class:`_engine.Result`.
  242. """
  243. conn = self._sync_connection()
  244. result = await greenlet_spawn(
  245. conn.exec_driver_sql,
  246. statement,
  247. parameters,
  248. execution_options,
  249. _require_await=True,
  250. )
  251. if result.context._is_server_side:
  252. raise async_exc.AsyncMethodRequired(
  253. "Can't use the connection.exec_driver_sql() method with a "
  254. "server-side cursor."
  255. "Use the connection.stream() method for an async "
  256. "streaming result set."
  257. )
  258. return result
  259. async def stream(
  260. self,
  261. statement,
  262. parameters=None,
  263. execution_options=util.EMPTY_DICT,
  264. ):
  265. """Execute a statement and return a streaming
  266. :class:`_asyncio.AsyncResult` object."""
  267. conn = self._sync_connection()
  268. result = await greenlet_spawn(
  269. conn._execute_20,
  270. statement,
  271. parameters,
  272. util.EMPTY_DICT.merge_with(
  273. execution_options, {"stream_results": True}
  274. ),
  275. _require_await=True,
  276. )
  277. if not result.context._is_server_side:
  278. # TODO: real exception here
  279. assert False, "server side result expected"
  280. return AsyncResult(result)
  281. async def execute(
  282. self,
  283. statement,
  284. parameters=None,
  285. execution_options=util.EMPTY_DICT,
  286. ):
  287. r"""Executes a SQL statement construct and return a buffered
  288. :class:`_engine.Result`.
  289. :param object: The statement to be executed. This is always
  290. an object that is in both the :class:`_expression.ClauseElement` and
  291. :class:`_expression.Executable` hierarchies, including:
  292. * :class:`_expression.Select`
  293. * :class:`_expression.Insert`, :class:`_expression.Update`,
  294. :class:`_expression.Delete`
  295. * :class:`_expression.TextClause` and
  296. :class:`_expression.TextualSelect`
  297. * :class:`_schema.DDL` and objects which inherit from
  298. :class:`_schema.DDLElement`
  299. :param parameters: parameters which will be bound into the statement.
  300. This may be either a dictionary of parameter names to values,
  301. or a mutable sequence (e.g. a list) of dictionaries. When a
  302. list of dictionaries is passed, the underlying statement execution
  303. will make use of the DBAPI ``cursor.executemany()`` method.
  304. When a single dictionary is passed, the DBAPI ``cursor.execute()``
  305. method will be used.
  306. :param execution_options: optional dictionary of execution options,
  307. which will be associated with the statement execution. This
  308. dictionary can provide a subset of the options that are accepted
  309. by :meth:`_future.Connection.execution_options`.
  310. :return: a :class:`_engine.Result` object.
  311. """
  312. conn = self._sync_connection()
  313. result = await greenlet_spawn(
  314. conn._execute_20,
  315. statement,
  316. parameters,
  317. execution_options,
  318. _require_await=True,
  319. )
  320. if result.context._is_server_side:
  321. raise async_exc.AsyncMethodRequired(
  322. "Can't use the connection.execute() method with a "
  323. "server-side cursor."
  324. "Use the connection.stream() method for an async "
  325. "streaming result set."
  326. )
  327. return result
  328. async def scalar(
  329. self,
  330. statement,
  331. parameters=None,
  332. execution_options=util.EMPTY_DICT,
  333. ):
  334. r"""Executes a SQL statement construct and returns a scalar object.
  335. This method is shorthand for invoking the
  336. :meth:`_engine.Result.scalar` method after invoking the
  337. :meth:`_future.Connection.execute` method. Parameters are equivalent.
  338. :return: a scalar Python value representing the first column of the
  339. first row returned.
  340. """
  341. result = await self.execute(statement, parameters, execution_options)
  342. return result.scalar()
  343. async def run_sync(self, fn, *arg, **kw):
  344. """Invoke the given sync callable passing self as the first argument.
  345. This method maintains the asyncio event loop all the way through
  346. to the database connection by running the given callable in a
  347. specially instrumented greenlet.
  348. E.g.::
  349. with async_engine.begin() as conn:
  350. await conn.run_sync(metadata.create_all)
  351. .. note::
  352. The provided callable is invoked inline within the asyncio event
  353. loop, and will block on traditional IO calls. IO within this
  354. callable should only call into SQLAlchemy's asyncio database
  355. APIs which will be properly adapted to the greenlet context.
  356. .. seealso::
  357. :ref:`session_run_sync`
  358. """
  359. conn = self._sync_connection()
  360. return await greenlet_spawn(fn, conn, *arg, **kw)
  361. def __await__(self):
  362. return self.start().__await__()
  363. async def __aexit__(self, type_, value, traceback):
  364. await self.close()
  365. @util.create_proxy_methods(
  366. Engine,
  367. ":class:`_future.Engine`",
  368. ":class:`_asyncio.AsyncEngine`",
  369. classmethods=[],
  370. methods=[
  371. "clear_compiled_cache",
  372. "update_execution_options",
  373. "get_execution_options",
  374. ],
  375. attributes=["url", "pool", "dialect", "engine", "name", "driver", "echo"],
  376. )
  377. class AsyncEngine(ProxyComparable, AsyncConnectable):
  378. """An asyncio proxy for a :class:`_engine.Engine`.
  379. :class:`_asyncio.AsyncEngine` is acquired using the
  380. :func:`_asyncio.create_async_engine` function::
  381. from sqlalchemy.ext.asyncio import create_async_engine
  382. engine = create_async_engine("postgresql+asyncpg://user:pass@host/dbname")
  383. .. versionadded:: 1.4
  384. """ # noqa
  385. # AsyncEngine is a thin proxy; no state should be added here
  386. # that is not retrievable from the "sync" engine / connection, e.g.
  387. # current transaction, info, etc. It should be possible to
  388. # create a new AsyncEngine that matches this one given only the
  389. # "sync" elements.
  390. __slots__ = ("sync_engine", "_proxied")
  391. _connection_cls = AsyncConnection
  392. _option_cls: type
  393. class _trans_ctx(StartableContext):
  394. def __init__(self, conn):
  395. self.conn = conn
  396. async def start(self, is_ctxmanager=False):
  397. await self.conn.start(is_ctxmanager=is_ctxmanager)
  398. self.transaction = self.conn.begin()
  399. await self.transaction.__aenter__()
  400. return self.conn
  401. async def __aexit__(self, type_, value, traceback):
  402. await self.transaction.__aexit__(type_, value, traceback)
  403. await self.conn.close()
  404. def __init__(self, sync_engine):
  405. if not sync_engine.dialect.is_async:
  406. raise exc.InvalidRequestError(
  407. "The asyncio extension requires an async driver to be used. "
  408. f"The loaded {sync_engine.dialect.driver!r} is not async."
  409. )
  410. self.sync_engine = self._proxied = self._assign_proxied(sync_engine)
  411. @classmethod
  412. def _regenerate_proxy_for_target(cls, target):
  413. return AsyncEngine(target)
  414. def begin(self):
  415. """Return a context manager which when entered will deliver an
  416. :class:`_asyncio.AsyncConnection` with an
  417. :class:`_asyncio.AsyncTransaction` established.
  418. E.g.::
  419. async with async_engine.begin() as conn:
  420. await conn.execute(
  421. text("insert into table (x, y, z) values (1, 2, 3)")
  422. )
  423. await conn.execute(text("my_special_procedure(5)"))
  424. """
  425. conn = self.connect()
  426. return self._trans_ctx(conn)
  427. def connect(self):
  428. """Return an :class:`_asyncio.AsyncConnection` object.
  429. The :class:`_asyncio.AsyncConnection` will procure a database
  430. connection from the underlying connection pool when it is entered
  431. as an async context manager::
  432. async with async_engine.connect() as conn:
  433. result = await conn.execute(select(user_table))
  434. The :class:`_asyncio.AsyncConnection` may also be started outside of a
  435. context manager by invoking its :meth:`_asyncio.AsyncConnection.start`
  436. method.
  437. """
  438. return self._connection_cls(self)
  439. async def raw_connection(self):
  440. """Return a "raw" DBAPI connection from the connection pool.
  441. .. seealso::
  442. :ref:`dbapi_connections`
  443. """
  444. return await greenlet_spawn(self.sync_engine.raw_connection)
  445. def execution_options(self, **opt):
  446. """Return a new :class:`_asyncio.AsyncEngine` that will provide
  447. :class:`_asyncio.AsyncConnection` objects with the given execution
  448. options.
  449. Proxied from :meth:`_future.Engine.execution_options`. See that
  450. method for details.
  451. """
  452. return AsyncEngine(self.sync_engine.execution_options(**opt))
  453. async def dispose(self):
  454. """Dispose of the connection pool used by this
  455. :class:`_asyncio.AsyncEngine`.
  456. This will close all connection pool connections that are
  457. **currently checked in**. See the documentation for the underlying
  458. :meth:`_future.Engine.dispose` method for further notes.
  459. .. seealso::
  460. :meth:`_future.Engine.dispose`
  461. """
  462. return await greenlet_spawn(self.sync_engine.dispose)
  463. class AsyncTransaction(ProxyComparable, StartableContext):
  464. """An asyncio proxy for a :class:`_engine.Transaction`."""
  465. __slots__ = ("connection", "sync_transaction", "nested")
  466. def __init__(self, connection, nested=False):
  467. self.connection = connection # AsyncConnection
  468. self.sync_transaction = None # sqlalchemy.engine.Transaction
  469. self.nested = nested
  470. @classmethod
  471. def _regenerate_proxy_for_target(cls, target):
  472. sync_connection = target.connection
  473. sync_transaction = target
  474. nested = isinstance(target, NestedTransaction)
  475. async_connection = AsyncConnection._retrieve_proxy_for_target(
  476. sync_connection
  477. )
  478. assert async_connection is not None
  479. obj = cls.__new__(cls)
  480. obj.connection = async_connection
  481. obj.sync_transaction = obj._assign_proxied(sync_transaction)
  482. obj.nested = nested
  483. return obj
  484. def _sync_transaction(self):
  485. if not self.sync_transaction:
  486. self._raise_for_not_started()
  487. return self.sync_transaction
  488. @property
  489. def _proxied(self):
  490. return self.sync_transaction
  491. @property
  492. def is_valid(self):
  493. return self._sync_transaction().is_valid
  494. @property
  495. def is_active(self):
  496. return self._sync_transaction().is_active
  497. async def close(self):
  498. """Close this :class:`.Transaction`.
  499. If this transaction is the base transaction in a begin/commit
  500. nesting, the transaction will rollback(). Otherwise, the
  501. method returns.
  502. This is used to cancel a Transaction without affecting the scope of
  503. an enclosing transaction.
  504. """
  505. await greenlet_spawn(self._sync_transaction().close)
  506. async def rollback(self):
  507. """Roll back this :class:`.Transaction`."""
  508. await greenlet_spawn(self._sync_transaction().rollback)
  509. async def commit(self):
  510. """Commit this :class:`.Transaction`."""
  511. await greenlet_spawn(self._sync_transaction().commit)
  512. async def start(self, is_ctxmanager=False):
  513. """Start this :class:`_asyncio.AsyncTransaction` object's context
  514. outside of using a Python ``with:`` block.
  515. """
  516. self.sync_transaction = self._assign_proxied(
  517. await greenlet_spawn(
  518. self.connection._sync_connection().begin_nested
  519. if self.nested
  520. else self.connection._sync_connection().begin
  521. )
  522. )
  523. if is_ctxmanager:
  524. self.sync_transaction.__enter__()
  525. return self
  526. async def __aexit__(self, type_, value, traceback):
  527. await greenlet_spawn(
  528. self._sync_transaction().__exit__, type_, value, traceback
  529. )
  530. def _get_sync_engine_or_connection(async_engine):
  531. if isinstance(async_engine, AsyncConnection):
  532. return async_engine.sync_connection
  533. try:
  534. return async_engine.sync_engine
  535. except AttributeError as e:
  536. raise exc.ArgumentError(
  537. "AsyncEngine expected, got %r" % async_engine
  538. ) from e