Source code for tortoise.backends.base.client

from __future__ import annotations

import asyncio
from typing import (
    Any,
    Generic,
    List,
    Optional,
    Sequence,
    Tuple,
    Type,
    TypeVar,
    Union,
    cast,
)

from pypika import Query

from tortoise.backends.base.executor import BaseExecutor
from tortoise.backends.base.schema_generator import BaseSchemaGenerator
from tortoise.connection import connections
from tortoise.exceptions import TransactionManagementError
from tortoise.log import db_client_logger

T_conn = TypeVar("T_conn")  # Instance of client connection, such as: asyncpg.Connection()


[docs]class Capabilities: """ DB Client Capabilities indicates the supported feature-set, and is also used to note common workarounds to deficiencies. Defaults are set with the following standard: * Deficiencies: assume it is working right. * Features: assume it doesn't have it. :param dialect: Dialect name of the DB Client driver. :param daemon: Is the DB an external Daemon we connect to? :param requires_limit: Indicates that this DB requires a ``LIMIT`` statement for an ``OFFSET`` statement to work. :param inline_comment: Indicates that comments should be rendered in line with the DDL statement, and not as a separate statement. :param supports_transactions: Indicates that this DB supports transactions. :param support_for_update: Indicates that this DB supports SELECT ... FOR UPDATE SQL statement. :param support_index_hint: Support force index or use index. :param support_update_limit_order_by: support update/delete with limit and order by. """ def __init__( self, dialect: str, *, # Is the connection a Daemon? daemon: bool = True, # Deficiencies to work around: requires_limit: bool = False, inline_comment: bool = False, supports_transactions: bool = True, support_for_update: bool = True, # Support force index or use index? support_index_hint: bool = False, # support update/delete with limit and order by support_update_limit_order_by: bool = True, ) -> None: super().__setattr__("_mutable", True) self.dialect = dialect self.daemon = daemon self.requires_limit = requires_limit self.inline_comment = inline_comment self.supports_transactions = supports_transactions self.support_for_update = support_for_update self.support_index_hint = support_index_hint self.support_update_limit_order_by = support_update_limit_order_by super().__setattr__("_mutable", False) def __setattr__(self, attr: str, value: Any) -> None: if not getattr(self, "_mutable", False): raise AttributeError(attr) super().__setattr__(attr, value) def __str__(self) -> str: return str(self.__dict__)
[docs]class BaseDBAsyncClient: """ Base class for containing a DB connection. Parameters get passed as kwargs, and is mostly driver specific. .. attribute:: query_class :annotation: Type[pypika.Query] The PyPika Query dialect (low level dialect) .. attribute:: executor_class :annotation: Type[BaseExecutor] The executor dialect class (high level dialect) .. attribute:: schema_generator :annotation: Type[BaseSchemaGenerator] The DDL schema generator .. attribute:: capabilities :annotation: Capabilities Contains the connection capabilities """ query_class: Type[Query] = Query executor_class: Type[BaseExecutor] = BaseExecutor schema_generator: Type[BaseSchemaGenerator] = BaseSchemaGenerator capabilities: Capabilities = Capabilities("") def __init__(self, connection_name: str, fetch_inserted: bool = True, **kwargs: Any) -> None: self.log = db_client_logger self.connection_name = connection_name self.fetch_inserted = fetch_inserted
[docs] async def create_connection(self, with_db: bool) -> None: """ Establish a DB connection. :param with_db: If True, then select the DB to use, else use default. Use case for this is to create/drop a database. """ raise NotImplementedError() # pragma: nocoverage
[docs] async def close(self) -> None: """ Closes the DB connection. """ raise NotImplementedError() # pragma: nocoverage
[docs] async def db_create(self) -> None: """ Created the database in the server. Typically only called by the test runner. Need to have called ``create_connection()``` with parameter ``with_db=False`` set to use the default connection instead of the configured one, else you would get errors indicating the database doesn't exist. """ raise NotImplementedError() # pragma: nocoverage
[docs] async def db_delete(self) -> None: """ Delete the database from the Server. Typically only called by the test runner. Need to have called ``create_connection()``` with parameter ``with_db=False`` set to use the default connection instead of the configured one, else you would get errors indicating the database is in use. """ raise NotImplementedError() # pragma: nocoverage
[docs] def acquire_connection(self) -> Union["ConnectionWrapper", "PoolConnectionWrapper"]: """ Acquires a connection from the pool. Will return the current context connection if already in a transaction. """ raise NotImplementedError() # pragma: nocoverage
def _in_transaction(self) -> "TransactionContext": raise NotImplementedError() # pragma: nocoverage
[docs] async def execute_insert(self, query: str, values: list) -> Any: """ Executes a RAW SQL insert statement, with provided parameters. :param query: The SQL string, pre-parametrized for the target DB dialect. :param values: A sequence of positional DB parameters. :return: The primary key if it is generated by the DB. (Currently only integer autonumber PK's) """ raise NotImplementedError() # pragma: nocoverage
[docs] async def execute_query( self, query: str, values: Optional[list] = None ) -> Tuple[int, Sequence[dict]]: """ Executes a RAW SQL query statement, and returns the resultset. :param query: The SQL string, pre-parametrized for the target DB dialect. :param values: A sequence of positional DB parameters. :return: A tuple of: (The number of rows affected, The resultset) """ raise NotImplementedError() # pragma: nocoverage
[docs] async def execute_script(self, query: str) -> None: """ Executes a RAW SQL script with multiple statements, and returns nothing. :param query: The SQL string, which will be passed on verbatim. Semicolons is supported here. """ raise NotImplementedError() # pragma: nocoverage
[docs] async def execute_many(self, query: str, values: List[list]) -> None: """ Executes a RAW bulk insert statement, like execute_insert, but returns no data. :param query: The SQL string, pre-parametrized for the target DB dialect. :param values: A sequence of positional DB parameters. """ raise NotImplementedError() # pragma: nocoverage
[docs] async def execute_query_dict(self, query: str, values: Optional[list] = None) -> List[dict]: """ Executes a RAW SQL query statement, and returns the resultset as a list of dicts. :param query: The SQL string, pre-parametrized for the target DB dialect. :param values: A sequence of positional DB parameters. """ raise NotImplementedError() # pragma: nocoverage
class ConnectionWrapper(Generic[T_conn]): __slots__ = ("connection", "lock", "client") def __init__(self, lock: asyncio.Lock, client: Any) -> None: """Wraps the connections with a lock to facilitate safe concurrent access.""" self.lock: asyncio.Lock = lock self.client = client self.connection: T_conn = client._connection async def ensure_connection(self) -> None: if not self.connection: await self.client.create_connection(with_db=True) self.connection = self.client._connection async def __aenter__(self) -> T_conn: await self.lock.acquire() await self.ensure_connection() return self.connection async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.lock.release() class TransactionContext(Generic[T_conn]): __slots__ = ("connection", "connection_name", "token", "lock") def __init__(self, connection: Any) -> None: self.connection = connection self.connection_name = connection.connection_name self.lock = getattr(connection, "_trxlock", None) async def ensure_connection(self) -> None: if not self.connection._connection: await self.connection._parent.create_connection(with_db=True) self.connection._connection = self.connection._parent._connection async def __aenter__(self) -> T_conn: await self.ensure_connection() await self.lock.acquire() # type:ignore self.token = connections.set(self.connection_name, self.connection) await self.connection.start() return self.connection async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if not self.connection._finalized: if exc_type: # Can't rollback a transaction that already failed. if exc_type is not TransactionManagementError: await self.connection.rollback() else: await self.connection.commit() connections.reset(self.token) self.lock.release() # type:ignore class TransactionContextPooled(TransactionContext): __slots__ = ("conn_wrapper", "connection", "connection_name", "token") async def ensure_connection(self) -> None: if not self.connection._parent._pool: await self.connection._parent.create_connection(with_db=True) async def __aenter__(self) -> T_conn: await self.ensure_connection() self.token = connections.set(self.connection_name, self.connection) self.connection._connection = await self.connection._parent._pool.acquire() await self.connection.start() return self.connection async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if not self.connection._finalized: if exc_type: # Can't rollback a transaction that already failed. if exc_type is not TransactionManagementError: await self.connection.rollback() else: await self.connection.commit() if self.connection._parent._pool: await self.connection._parent._pool.release(self.connection._connection) connections.reset(self.token) class NestedTransactionContext(TransactionContext): async def __aenter__(self) -> T_conn: return self.connection async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if not self.connection._finalized: if exc_type: # Can't rollback a transaction that already failed. if exc_type is not TransactionManagementError: await self.connection.rollback() class NestedTransactionPooledContext(TransactionContext): async def __aenter__(self) -> T_conn: await self.lock.acquire() # type:ignore return self.connection async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.lock.release() # type:ignore if not self.connection._finalized: if exc_type: # Can't rollback a transaction that already failed. if exc_type is not TransactionManagementError: await self.connection.rollback() class PoolConnectionWrapper(Generic[T_conn]): def __init__(self, client: Any) -> None: """Class to manage acquiring from and releasing connections to a pool.""" self.pool = client._pool self.client = client self.connection: Optional[T_conn] = None async def ensure_connection(self) -> None: if not self.pool: await self.client.create_connection(with_db=True) self.pool = self.client._pool async def __aenter__(self) -> T_conn: await self.ensure_connection() # get first available connection self.connection = await self.pool.acquire() return cast(T_conn, self.connection) async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: # release the connection back to the pool await self.pool.release(self.connection) class BaseTransactionWrapper: async def start(self) -> None: raise NotImplementedError() # pragma: nocoverage def release(self) -> None: raise NotImplementedError() # pragma: nocoverage async def rollback(self) -> None: raise NotImplementedError() # pragma: nocoverage async def commit(self) -> None: raise NotImplementedError() # pragma: nocoverage