Skip to content

Database

Database is Saffier's SQLAlchemy Async database runtime.

Most applications interact with it indirectly through a Registry, but the class is still important because it defines connection lifecycle, transaction management, and the SQLAlchemy AsyncEngine, AsyncConnection, and async_sessionmaker used by queries and schema helpers.

Typical usage

database = saffier.Database(
    "postgresql+asyncpg://postgres:postgres@localhost:5432/app"
)
models = saffier.Registry(database=database)

What to know in practice

  • use saffier.Database for registry ownership and ORM execution
  • registry lifecycle usually controls connect() and disconnect()
  • advanced code can inspect the native SQLAlchemy async engine through database.engine after connection

saffier.Database

Database(
    url=None,
    *,
    force_rollback=None,
    config=None,
    full_isolation=None,
    poll_interval=None,
    **options,
)

Saffier database runtime backed directly by SQLAlchemy Async.

The class owns an AsyncEngine, an async_sessionmaker, and execution helpers used by Saffier's ORM internals. Every database operation delegates to SQLAlchemy's public async engine, connection, transaction, and result APIs.

Configure URL, engine options, and rollback behavior for the runtime.

Engine creation is intentionally delayed until connect() so registry setup remains lightweight. Copy construction preserves the source database's URL and options while still creating independent SQLAlchemy engine and transaction state.

Source code in saffier/core/connection/database.py
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
def __init__(
    self,
    url: str | DatabaseURL | URL | Database | None = None,
    *,
    force_rollback: bool | None = None,
    config: dict[str, Any] | None = None,
    full_isolation: bool | None = None,
    poll_interval: float | None = None,
    **options: Any,
) -> None:
    """Configure URL, engine options, and rollback behavior for the runtime.

    Engine creation is intentionally delayed until ``connect()`` so registry
    setup remains lightweight. Copy construction preserves the source
    database's URL and options while still creating independent SQLAlchemy
    engine and transaction state.
    """
    assert config is None or url is None, "Use either 'url' or 'config', not both."
    if isinstance(url, Database):
        assert not options, "Cannot specify options when copying a Database object."
        self.url = url.url
        self.options = dict(url.options)
        if force_rollback is None:
            force_rollback = bool(url.force_rollback)
        if full_isolation is None:
            full_isolation = url._full_isolation
        if poll_interval is None:
            poll_interval = url.poll_interval
    else:
        database_url = DatabaseURL(url)
        if config and "connection" in config:
            connection_config = config["connection"]
            if "credentials" in connection_config:
                database_url = database_url.replace(**connection_config["credentials"])
        self.url, self.options = self._extract_options(database_url, **options)
        if force_rollback is None:
            force_rollback = False
        if full_isolation is None:
            full_isolation = False
        if poll_interval is None:
            poll_interval = 0.01

    self.poll_interval = poll_interval
    self._full_isolation = full_isolation
    self._force_rollback = ForceRollback(force_rollback)
    self._engine: AsyncEngine | None = None
    self._sessionmaker: async_sessionmaker[AsyncSession] | None = None
    self._global_connection: AsyncConnection | None = None
    self._global_transaction: AsyncTransaction | None = None
    self.is_connected = False
    self.ref_counter = 0
    self.ref_lock = asyncio.Lock()

force_rollback class-attribute instance-attribute

force_rollback = ForceRollbackDescriptor()

default_batch_size class-attribute instance-attribute

default_batch_size = 100

url instance-attribute

url = url

options instance-attribute

options = dict(options)

poll_interval instance-attribute

poll_interval = poll_interval

_full_isolation instance-attribute

_full_isolation = full_isolation

_force_rollback instance-attribute

_force_rollback = ForceRollback(force_rollback)

_engine instance-attribute

_engine = None

_sessionmaker instance-attribute

_sessionmaker = None

_global_connection instance-attribute

_global_connection = None

_global_transaction instance-attribute

_global_transaction = None

is_connected instance-attribute

is_connected = False

ref_counter instance-attribute

ref_counter = 0

ref_lock instance-attribute

ref_lock = Lock()

engine property

engine

Return the live SQLAlchemy async engine, when connected.

The property exposes the native AsyncEngine for advanced callers without manufacturing another abstraction. None means lifecycle has not connected the database yet or has already disconnected it.

sessionmaker property

sessionmaker

Return the SQLAlchemy async session factory, when available.

Saffier creates this with async_sessionmaker beside the engine. Returning None before connection makes lifecycle state explicit rather than constructing a hidden engine on property access.

__copy__

__copy__()

Return a new runtime object with the same configuration.

The copy shares URL and option values but not live SQLAlchemy engines, connections, transactions, or context state. This is used by registry preparation paths that need an isolated runtime handle.

Source code in saffier/core/connection/database.py
800
801
802
803
804
805
806
807
def __copy__(self) -> Database:
    """Return a new runtime object with the same configuration.

    The copy shares URL and option values but not live SQLAlchemy engines,
    connections, transactions, or context state. This is used by registry
    preparation paths that need an isolated runtime handle.
    """
    return self.__class__(self)

_extract_options staticmethod

_extract_options(database_url, **options)

Separate SQLAlchemy engine options from URL query parameters.

Saffier accepts selected engine options in the URL for compatibility with existing configuration. Known values are removed from the URL and passed directly to create_async_engine() so SQLAlchemy owns pool, isolation, echo, and JSON behavior.

Source code in saffier/core/connection/database.py
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
@staticmethod
def _extract_options(
    database_url: DatabaseURL,
    **options: Any,
) -> tuple[DatabaseURL, dict[str, Any]]:
    """Separate SQLAlchemy engine options from URL query parameters.

    Saffier accepts selected engine options in the URL for compatibility
    with existing configuration. Known values are removed from the URL and
    passed directly to ``create_async_engine()`` so SQLAlchemy owns pool,
    isolation, echo, and JSON behavior.
    """
    options.setdefault("pool_reset_on_return", None)
    options.setdefault("json_serializer", _json_serializer)
    options.setdefault("json_deserializer", _json_deserializer)
    query_options = dict(database_url.options)
    for param in ("ssl", "echo", "echo_pool"):
        if param in query_options:
            assert param not in options
            value = cast("str", query_options.pop(param))
            options[param] = value.lower() in {"true", ""}
    if "isolation_level" in query_options:
        assert "isolation_level" not in options
        options["isolation_level"] = cast("str", query_options.pop("isolation_level"))
    for param in ("pool_size", "max_overflow"):
        if param in query_options:
            assert param not in options
            options[param] = int(cast("str", query_options.pop(param)))
    if "pool_recycle" in query_options:
        assert "pool_recycle" not in options
        options["pool_recycle"] = float(cast("str", query_options.pop("pool_recycle")))
    options.setdefault("isolation_level", "AUTOCOMMIT")
    return database_url.replace(options=query_options), options

session async

session()

Create a native SQLAlchemy AsyncSession.

The database must be connected so the session factory is bound to the current AsyncEngine. Sessions use expire_on_commit=False to keep ORM-facing values usable after transaction boundaries.

Source code in saffier/core/connection/database.py
863
864
865
866
867
868
869
870
871
872
async def session(self) -> AsyncSession:
    """Create a native SQLAlchemy ``AsyncSession``.

    The database must be connected so the session factory is bound to the
    current ``AsyncEngine``. Sessions use ``expire_on_commit=False`` to keep
    ORM-facing values usable after transaction boundaries.
    """
    await self._ensure_connected()
    assert self._sessionmaker is not None
    return self._sessionmaker()

connect_hook async

connect_hook()

Run subclass setup before the SQLAlchemy engine is created.

DatabaseTestClient uses this hook to create or verify the test database before normal connection proceeds. The base implementation is intentionally empty so regular runtimes connect directly.

Source code in saffier/core/connection/database.py
874
875
876
877
878
879
880
881
async def connect_hook(self) -> None:
    """Run subclass setup before the SQLAlchemy engine is created.

    ``DatabaseTestClient`` uses this hook to create or verify the test
    database before normal connection proceeds. The base implementation is
    intentionally empty so regular runtimes connect directly.
    """
    pass

disconnect_hook async

disconnect_hook()

Run subclass cleanup after the SQLAlchemy engine is disposed.

Hooks run after Saffier has closed its force-rollback transaction and disposed the engine, which lets test clients safely drop databases or release resources outside the live connection pool.

Source code in saffier/core/connection/database.py
883
884
885
886
887
888
889
890
async def disconnect_hook(self) -> None:
    """Run subclass cleanup after the SQLAlchemy engine is disposed.

    Hooks run after Saffier has closed its force-rollback transaction and
    disposed the engine, which lets test clients safely drop databases or
    release resources outside the live connection pool.
    """
    pass

connect async

connect()

Create the SQLAlchemy async engine for this database lifecycle.

Connection is reference-counted because registries and nested async contexts may ask for the same database to connect more than once. The first caller creates AsyncEngine and async_sessionmaker; later callers reuse that live SQLAlchemy runtime.

Source code in saffier/core/connection/database.py
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
async def connect(self) -> bool:
    """Create the SQLAlchemy async engine for this database lifecycle.

    Connection is reference-counted because registries and nested async
    contexts may ask for the same database to connect more than once. The
    first caller creates ``AsyncEngine`` and ``async_sessionmaker``; later
    callers reuse that live SQLAlchemy runtime.
    """
    async with self.ref_lock:
        self.ref_counter += 1
        if self.ref_counter > 1:
            if bool(self.force_rollback):
                await self._ensure_global_force_rollback()
            return False
        try:
            await self.connect_hook()
            self._engine = create_async_engine(_async_url(self.url), **self.options)
            self._sessionmaker = async_sessionmaker(
                self._engine,
                expire_on_commit=False,
            )
            self.is_connected = True
            if bool(self.force_rollback):
                await self._ensure_global_force_rollback()
        except BaseException:
            self.ref_counter = 0
            self.is_connected = False
            self._engine = None
            self._sessionmaker = None
            raise
        return True

disconnect async

disconnect(force=False)

Dispose the SQLAlchemy async engine when lifecycle ownership ends.

Normal disconnect decrements the reference counter and only disposes the engine for the last owner. force=True is reserved for teardown paths that must close the pool regardless of outstanding references.

Source code in saffier/core/connection/database.py
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
async def disconnect(self, force: bool = False) -> bool:
    """Dispose the SQLAlchemy async engine when lifecycle ownership ends.

    Normal disconnect decrements the reference counter and only disposes the
    engine for the last owner. ``force=True`` is reserved for teardown paths
    that must close the pool regardless of outstanding references.
    """
    async with self.ref_lock:
        if force:
            self.ref_counter = 0
        elif self.ref_counter > 0:
            self.ref_counter -= 1
        if self.ref_counter > 0:
            return False
        if not self.is_connected:
            return False
        try:
            await self._close_global_force_rollback()
        finally:
            engine, self._engine = self._engine, None
            self._sessionmaker = None
            self.is_connected = False
            if engine is not None:
                await engine.dispose(close=True)
            await self.disconnect_hook()
        return True

_ensure_connected async

_ensure_connected()

Validate that the database has an active SQLAlchemy engine.

Execution helpers call this before reaching for AsyncEngine. Raising here gives callers a clear lifecycle error instead of a later attribute failure on a missing connection pool.

Source code in saffier/core/connection/database.py
979
980
981
982
983
984
985
986
987
async def _ensure_connected(self) -> None:
    """Validate that the database has an active SQLAlchemy engine.

    Execution helpers call this before reaching for ``AsyncEngine``. Raising
    here gives callers a clear lifecycle error instead of a later attribute
    failure on a missing connection pool.
    """
    if not self.is_connected or self._engine is None:
        raise RuntimeError("Database is not connected")

_current_connection

_current_connection()

Return the connection currently bound to this execution context.

Transaction contexts take precedence because nested operations must use the same SQLAlchemy connection. If no transaction is active and forced rollback is enabled, the reusable rollback connection becomes the current connection for test isolation.

Source code in saffier/core/connection/database.py
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
def _current_connection(self) -> AsyncConnection | None:
    """Return the connection currently bound to this execution context.

    Transaction contexts take precedence because nested operations must use
    the same SQLAlchemy connection. If no transaction is active and forced
    rollback is enabled, the reusable rollback connection becomes the
    current connection for test isolation.
    """
    stack = _active_connections().get(id(self), ())
    if stack:
        return stack[-1]
    if bool(self.force_rollback):
        return self._global_connection
    return None

_push_connection

_push_connection(connection)

Bind a SQLAlchemy connection to this database in the current context.

Bindings are stored as a per-database stack so nested transactions can push savepoint connections and then restore the previous state exactly when they finish.

Source code in saffier/core/connection/database.py
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
def _push_connection(
    self,
    connection: AsyncConnection,
) -> Token[dict[int, tuple[AsyncConnection, ...]] | None]:
    """Bind a SQLAlchemy connection to this database in the current context.

    Bindings are stored as a per-database stack so nested transactions can
    push savepoint connections and then restore the previous state exactly
    when they finish.
    """
    connections = _active_connections()
    copied = dict(connections)
    copied[id(self)] = (*copied.get(id(self), ()), connection)
    return _ACTIVE_CONNECTIONS.set(copied)

_push_loop_bound_database

_push_loop_bound_database()

Mark this database as using loop-bound SQLAlchemy resources.

Force-rollback mode keeps one SQLAlchemy connection open across many ORM operations. The sync bridge reads this marker to avoid executing lazy loads on a different event loop from that connection.

Source code in saffier/core/connection/database.py
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
def _push_loop_bound_database(self) -> None:
    """Mark this database as using loop-bound SQLAlchemy resources.

    Force-rollback mode keeps one SQLAlchemy connection open across many ORM
    operations. The sync bridge reads this marker to avoid executing lazy
    loads on a different event loop from that connection.
    """
    databases = _LOOP_BOUND_DATABASES.get()
    token = _LOOP_BOUND_DATABASES.set((*databases, id(self)))
    token_stacks = dict(_loop_bound_token_stacks())
    token_stacks[id(self)] = (*token_stacks.get(id(self), ()), token)
    _LOOP_BOUND_TOKEN_STACKS.set(token_stacks)

_pop_loop_bound_database

_pop_loop_bound_database()

Remove this database's loop-bound marker for the current context.

The marker token must be reset in the same context that created it. Keeping the stack in a context variable prevents concurrent tasks using the same Database instance from popping and resetting each other's SQLAlchemy loop markers.

Source code in saffier/core/connection/database.py
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
def _pop_loop_bound_database(self) -> None:
    """Remove this database's loop-bound marker for the current context.

    The marker token must be reset in the same context that created it.
    Keeping the stack in a context variable prevents concurrent tasks using
    the same ``Database`` instance from popping and resetting each other's
    SQLAlchemy loop markers.
    """
    token_stacks = _loop_bound_token_stacks()
    stack = token_stacks.get(id(self), ())
    if not stack:
        return
    token = stack[-1]
    copied = dict(token_stacks)
    if len(stack) == 1:
        copied.pop(id(self), None)
    else:
        copied[id(self)] = stack[:-1]
    _LOOP_BOUND_TOKEN_STACKS.set(copied)
    _LOOP_BOUND_DATABASES.reset(token)

_ensure_global_force_rollback async

_ensure_global_force_rollback()

Open the reusable rollback transaction used for test isolation.

The connection and transaction are created directly through SQLAlchemy and held for the database lifecycle. ORM statements run against this connection, and disconnect rolls the outer transaction back.

Source code in saffier/core/connection/database.py
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
async def _ensure_global_force_rollback(self) -> AsyncConnection:
    """Open the reusable rollback transaction used for test isolation.

    The connection and transaction are created directly through SQLAlchemy
    and held for the database lifecycle. ORM statements run against this
    connection, and disconnect rolls the outer transaction back.
    """
    await self._ensure_connected()
    if self._global_connection is not None:
        return self._global_connection
    assert self._engine is not None
    connection = await self._engine.connect()
    await connection.execution_options(isolation_level="SERIALIZABLE")
    transaction = await connection.begin()
    self._global_connection = connection
    self._global_transaction = transaction
    return connection

_close_global_force_rollback async

_close_global_force_rollback()

Roll back and close the force-rollback connection.

Teardown suppresses rollback errors so disposal can still close the underlying SQLAlchemy connection. This mirrors test cleanup expectations where isolation should be best-effort during exceptional shutdown.

Source code in saffier/core/connection/database.py
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
async def _close_global_force_rollback(self) -> None:
    """Roll back and close the force-rollback connection.

    Teardown suppresses rollback errors so disposal can still close the
    underlying SQLAlchemy connection. This mirrors test cleanup expectations
    where isolation should be best-effort during exceptional shutdown.
    """
    transaction, self._global_transaction = self._global_transaction, None
    connection, self._global_connection = self._global_connection, None
    try:
        if transaction is not None:
            with contextlib.suppress(Exception):
                await transaction.rollback()
    finally:
        if connection is not None:
            await connection.close()

_transaction_connection async

_transaction_connection()

Choose the SQLAlchemy connection for a new transaction scope.

Existing context connections are reused for nesting, forced rollback scopes reuse the lifecycle-wide rollback connection, and ordinary transactions open a fresh connection from the async engine's pool.

Source code in saffier/core/connection/database.py
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
async def _transaction_connection(self) -> AsyncConnection:
    """Choose the SQLAlchemy connection for a new transaction scope.

    Existing context connections are reused for nesting, forced rollback
    scopes reuse the lifecycle-wide rollback connection, and ordinary
    transactions open a fresh connection from the async engine's pool.
    """
    current = self._current_connection()
    if current is not None:
        return current
    if bool(self.force_rollback):
        return await self._ensure_global_force_rollback()
    await self._ensure_connected()
    assert self._engine is not None
    return await self._engine.connect()

connection async

connection()

Yield a raw SQLAlchemy AsyncConnection for direct operations.

The yielded connection is the current transaction-bound connection when one exists; otherwise a short-lived connection is opened from the AsyncEngine pool. This keeps direct SQLAlchemy usage aligned with Saffier's transaction context instead of bypassing it.

Source code in saffier/core/connection/database.py
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
@contextlib.asynccontextmanager
async def connection(self) -> AsyncGenerator[AsyncConnection, None]:
    """Yield a raw SQLAlchemy ``AsyncConnection`` for direct operations.

    The yielded connection is the current transaction-bound connection when
    one exists; otherwise a short-lived connection is opened from the
    ``AsyncEngine`` pool. This keeps direct SQLAlchemy usage aligned with
    Saffier's transaction context instead of bypassing it.
    """
    current = self._current_connection()
    if current is not None:
        yield current
        return
    await self._ensure_connected()
    assert self._engine is not None
    async with self._engine.connect() as connection:
        yield connection

_execution_connection async

_execution_connection()

Yield the SQLAlchemy connection for one ORM execution helper.

ORM helpers should not decide whether they are inside a transaction, forced rollback block, or normal engine checkout. This helper centralizes that choice so fetch, execute, and iteration paths all share the same connection ownership rules.

Source code in saffier/core/connection/database.py
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
@contextlib.asynccontextmanager
async def _execution_connection(self) -> AsyncGenerator[AsyncConnection, None]:
    """Yield the SQLAlchemy connection for one ORM execution helper.

    ORM helpers should not decide whether they are inside a transaction,
    forced rollback block, or normal engine checkout. This helper centralizes
    that choice so fetch, execute, and iteration paths all share the same
    connection ownership rules.
    """
    current = self._current_connection()
    if current is not None:
        yield current
        return
    async with self.connection() as connection:
        yield connection

fetch_all async

fetch_all(query, values=None, timeout=None)

Execute a statement and return all rows from SQLAlchemy.

The method preserves Saffier's public fetch_all API while routing execution directly through AsyncConnection.execute(). Results are materialized before the cursor is closed so callers can inspect rows after the connection context exits.

Source code in saffier/core/connection/database.py
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
async def fetch_all(
    self,
    query: sqlalchemy.ClauseElement | str,
    values: dict[str, Any] | None = None,
    timeout: float | None = None,
) -> list[sqlalchemy.Row[Any]]:
    """Execute a statement and return all rows from SQLAlchemy.

    The method preserves Saffier's public ``fetch_all`` API while routing
    execution directly through ``AsyncConnection.execute()``. Results are
    materialized before the cursor is closed so callers can inspect rows
    after the connection context exits.
    """
    del timeout
    statement = _coerce_statement(query)
    async with self._execution_connection() as connection:
        result = await connection.execute(statement, values or {})
        try:
            return list(result.fetchall())
        finally:
            result.close()

fetch_one async

fetch_one(query, values=None, pos=0, timeout=None)

Execute a statement and return one row by logical position.

Positive positions are translated into SQLAlchemy offset and limit calls when the statement supports them. -1 keeps the historic "last row" behavior by materializing the result and returning the final entry.

Source code in saffier/core/connection/database.py
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
async def fetch_one(
    self,
    query: sqlalchemy.ClauseElement | str,
    values: dict[str, Any] | None = None,
    pos: int = 0,
    timeout: float | None = None,
) -> sqlalchemy.Row[Any] | None:
    """Execute a statement and return one row by logical position.

    Positive positions are translated into SQLAlchemy ``offset`` and
    ``limit`` calls when the statement supports them. ``-1`` keeps the
    historic "last row" behavior by materializing the result and returning
    the final entry.
    """
    del timeout
    statement = _coerce_statement(query)
    if pos > 0 and hasattr(statement, "offset"):
        statement = statement.offset(pos)  # type: ignore[assignment,union-attr]
    if pos >= 0 and hasattr(statement, "limit"):
        statement = statement.limit(1)  # type: ignore[assignment,union-attr]
    rows = await self.fetch_all(statement, values)
    if not rows:
        return None
    if pos == -1:
        return rows[-1]
    if pos < -1:
        raise NotImplementedError(
            f"Only positive numbers and -1 for the last result are currently supported: {pos}"
        )
    return rows[0]

fetch_val async

fetch_val(
    query, values=None, column=0, pos=0, timeout=None
)

Execute a statement and return a single value from one row.

The value can be selected by integer position or by row mapping key. Returning None for empty results matches the existing Saffier query helper contract.

Source code in saffier/core/connection/database.py
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
async def fetch_val(
    self,
    query: sqlalchemy.ClauseElement | str,
    values: dict[str, Any] | None = None,
    column: int | str = 0,
    pos: int = 0,
    timeout: float | None = None,
) -> Any:
    """Execute a statement and return a single value from one row.

    The value can be selected by integer position or by row mapping key.
    Returning ``None`` for empty results matches the existing Saffier query
    helper contract.
    """
    row = await self.fetch_one(query, values, pos=pos, timeout=timeout)
    if row is None:
        return None
    if isinstance(column, str):
        return row._mapping[column]
    return row[column]

_parse_execute_result staticmethod

_parse_execute_result(result)

Convert SQLAlchemy cursor metadata into Saffier's execute result.

Inserts historically return primary-key metadata when the dialect can provide it, while updates and deletes return rowcount. Keeping this translation in one helper lets execute() stay SQLAlchemy-native without leaking dialect-specific cursor details throughout the ORM.

Source code in saffier/core/connection/database.py
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
@staticmethod
def _parse_execute_result(result: sqlalchemy.CursorResult[Any]) -> Any:
    """Convert SQLAlchemy cursor metadata into Saffier's execute result.

    Inserts historically return primary-key metadata when the dialect can
    provide it, while updates and deletes return ``rowcount``. Keeping this
    translation in one helper lets ``execute()`` stay SQLAlchemy-native
    without leaking dialect-specific cursor details throughout the ORM.
    """
    if result.is_insert:
        with contextlib.suppress(AttributeError):
            inserted_primary_key = result.inserted_primary_key
            if inserted_primary_key:
                return inserted_primary_key
        with contextlib.suppress(AttributeError):
            return result.lastrowid
    return result.rowcount

execute async

execute(query, values=None, timeout=None)

Execute one SQLAlchemy statement and return the public result value.

Statements are run through the current execution connection, which means they participate in active Saffier transactions and force-rollback test contexts. The returned value follows the legacy Saffier contract: insert metadata when available, otherwise affected row count.

Source code in saffier/core/connection/database.py
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
async def execute(
    self,
    query: sqlalchemy.ClauseElement | str,
    values: Any = None,
    timeout: float | None = None,
) -> Any:
    """Execute one SQLAlchemy statement and return the public result value.

    Statements are run through the current execution connection, which means
    they participate in active Saffier transactions and force-rollback test
    contexts. The returned value follows the legacy Saffier contract:
    insert metadata when available, otherwise affected row count.
    """
    del timeout
    statement = _coerce_statement(query)
    async with self._execution_connection() as connection:
        result = (
            await connection.execute(statement, values)
            if values is not None
            else await connection.execute(statement)
        )
        try:
            return self._parse_execute_result(result)
        finally:
            result.close()

execute_many async

execute_many(query, values=None, timeout=None)

Execute one statement against many parameter dictionaries.

SQLAlchemy handles executemany behavior when a sequence of parameter dictionaries is passed to execute(). Saffier only adapts the cursor metadata back into the value expected by bulk insert and update code.

Source code in saffier/core/connection/database.py
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
async def execute_many(
    self,
    query: sqlalchemy.ClauseElement | str,
    values: Any = None,
    timeout: float | None = None,
) -> Any:
    """Execute one statement against many parameter dictionaries.

    SQLAlchemy handles executemany behavior when a sequence of parameter
    dictionaries is passed to ``execute()``. Saffier only adapts the cursor
    metadata back into the value expected by bulk insert and update code.
    """
    del timeout
    statement = _coerce_statement(query)
    async with self._execution_connection() as connection:
        result = (
            await connection.execute(statement, values)
            if values is not None
            else await connection.execute(statement)
        )
        try:
            if result.is_insert:
                with contextlib.suppress(AttributeError):
                    if result.inserted_primary_key_rows is not None:
                        return result.inserted_primary_key_rows
            return result.rowcount
        finally:
            result.close()

iterate async

iterate(query, values=None, chunk_size=None, timeout=None)

Yield rows from a SQLAlchemy statement without hiding result objects.

Dialects with server-side cursor support use AsyncConnection.stream and yield_per when a transaction is already active. Dialects without streaming support, or autocommit connections where asyncpg cannot open a cursor, fall back to a materialized result while preserving the async generator API.

Source code in saffier/core/connection/database.py
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
async def iterate(
    self,
    query: sqlalchemy.ClauseElement | str,
    values: dict[str, Any] | None = None,
    chunk_size: int | None = None,
    timeout: float | None = None,
) -> AsyncGenerator[sqlalchemy.Row[Any], None]:
    """Yield rows from a SQLAlchemy statement without hiding result objects.

    Dialects with server-side cursor support use ``AsyncConnection.stream``
    and ``yield_per`` when a transaction is already active. Dialects without
    streaming support, or autocommit connections where asyncpg cannot open a
    cursor, fall back to a materialized result while preserving the async
    generator API.
    """
    del timeout
    statement = _coerce_statement(query)
    chunk_size = chunk_size or self.default_batch_size
    async with self._execution_connection() as connection:
        if (
            not connection.dialect.supports_server_side_cursors
            or not connection.in_transaction()
        ):
            result = await connection.execute(statement, values or {})
            try:
                for row in result.fetchall():
                    yield row
            finally:
                result.close()
            return

        await connection.execution_options(yield_per=chunk_size)
        try:
            async with connection.stream(statement, values or {}) as result:
                async for row in result:
                    yield row
        finally:
            await connection.execution_options(yield_per=0)

batched_iterate async

batched_iterate(
    query,
    values=None,
    batch_size=None,
    batch_wrapper=tuple,
    timeout=None,
)

Yield result rows grouped into caller-configured batches.

This compatibility helper materializes rows with fetch_all and then wraps each batch with batch_wrapper. The execution itself still uses SQLAlchemy async connections and participates in active transactions.

Source code in saffier/core/connection/database.py
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
async def batched_iterate(
    self,
    query: sqlalchemy.ClauseElement | str,
    values: dict[str, Any] | None = None,
    batch_size: int | None = None,
    batch_wrapper: Callable[[Sequence[Any]], Any] = tuple,
    timeout: float | None = None,
) -> AsyncGenerator[Any, None]:
    """Yield result rows grouped into caller-configured batches.

    This compatibility helper materializes rows with ``fetch_all`` and then
    wraps each batch with ``batch_wrapper``. The execution itself still uses
    SQLAlchemy async connections and participates in active transactions.
    """
    del timeout
    batch_size = batch_size or self.default_batch_size
    rows = await self.fetch_all(query, values)
    for batch in _batch_rows(rows, batch_size):
        yield batch_wrapper(batch)

transaction

transaction(*, force_rollback=False, **kwargs)

Create a SQLAlchemy-backed Saffier transaction context.

Keyword arguments are passed through as SQLAlchemy execution options before the transaction begins. The returned object supports async with, manual await startup, and decorator usage.

Source code in saffier/core/connection/database.py
1344
1345
1346
1347
1348
1349
1350
1351
def transaction(self, *, force_rollback: bool = False, **kwargs: Any) -> Transaction:
    """Create a SQLAlchemy-backed Saffier transaction context.

    Keyword arguments are passed through as SQLAlchemy execution options
    before the transaction begins. The returned object supports ``async
    with``, manual ``await`` startup, and decorator usage.
    """
    return Transaction(self, force_rollback=force_rollback, **kwargs)

run_sync async

run_sync(fn, *args, timeout=None, **kwargs)

Run a synchronous SQLAlchemy callable against an async connection.

SQLAlchemy exposes AsyncConnection.run_sync() for operations such as metadata creation and reflection that still use synchronous Core APIs. Saffier forwards directly to that method so schema helpers remain idiomatic SQLAlchemy 2.x async code.

Source code in saffier/core/connection/database.py
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
async def run_sync(
    self,
    fn: Callable[..., Any],
    *args: Any,
    timeout: float | None = None,
    **kwargs: Any,
) -> Any:
    """Run a synchronous SQLAlchemy callable against an async connection.

    SQLAlchemy exposes ``AsyncConnection.run_sync()`` for operations such as
    metadata creation and reflection that still use synchronous Core APIs.
    Saffier forwards directly to that method so schema helpers remain
    idiomatic SQLAlchemy 2.x async code.
    """
    del timeout
    async with self._execution_connection() as connection:
        return await connection.run_sync(fn, *args, **kwargs)

create_all async

create_all(meta, timeout=None, **kwargs)

Create every table in a SQLAlchemy MetaData object.

Table creation is executed through AsyncConnection.run_sync() so the synchronous MetaData.create_all API runs on the connection paired with the active async engine or transaction.

Source code in saffier/core/connection/database.py
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
async def create_all(
    self,
    meta: sqlalchemy.MetaData,
    timeout: float | None = None,
    **kwargs: Any,
) -> None:
    """Create every table in a SQLAlchemy ``MetaData`` object.

    Table creation is executed through ``AsyncConnection.run_sync()`` so the
    synchronous ``MetaData.create_all`` API runs on the connection paired
    with the active async engine or transaction.
    """
    del timeout
    await self.run_sync(meta.create_all, **kwargs)

drop_all async

drop_all(meta, timeout=None, **kwargs)

Drop every table in a SQLAlchemy MetaData object.

The method mirrors create_all and keeps Saffier schema teardown on SQLAlchemy's public metadata API. Any active transaction or force-rollback connection is respected by the execution connection.

Source code in saffier/core/connection/database.py
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
async def drop_all(
    self,
    meta: sqlalchemy.MetaData,
    timeout: float | None = None,
    **kwargs: Any,
) -> None:
    """Drop every table in a SQLAlchemy ``MetaData`` object.

    The method mirrors ``create_all`` and keeps Saffier schema teardown on
    SQLAlchemy's public metadata API. Any active transaction or
    force-rollback connection is respected by the execution connection.
    """
    del timeout
    await self.run_sync(meta.drop_all, **kwargs)

asgi

asgi(app=None, handle_lifespan=False)

Wrap an ASGI application with SQLAlchemy engine lifecycle hooks.

The wrapper connects the database during ASGI startup and registers disconnect as cleanup. This keeps framework integration small while still ensuring SQLAlchemy pools are opened and disposed at application lifecycle boundaries.

Source code in saffier/core/connection/database.py
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
def asgi(
    self,
    app: ASGIApp | None = None,
    handle_lifespan: bool = False,
) -> ASGIApp | Callable[[ASGIApp], ASGIApp]:
    """Wrap an ASGI application with SQLAlchemy engine lifecycle hooks.

    The wrapper connects the database during ASGI startup and registers
    ``disconnect`` as cleanup. This keeps framework integration small while
    still ensuring SQLAlchemy pools are opened and disposed at application
    lifecycle boundaries.
    """
    if LifespanHook is None:
        raise RuntimeError("monkay.asgi is required for ASGI integration.")

    async def setup() -> contextlib.AsyncExitStack:
        """Connect the database and register ASGI lifespan cleanup.

        ``LifespanHook`` expects a stack-like object that owns async cleanup
        callbacks. Saffier connects the SQLAlchemy engine first and then
        pushes ``disconnect`` so pool disposal always runs on shutdown.
        """
        cleanupstack = contextlib.AsyncExitStack()
        await self.connect()
        cleanupstack.push_async_callback(self.disconnect)
        return cleanupstack

    return LifespanHook(app, setup=setup, do_forward=not handle_lifespan)