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.Databasefor registry ownership and ORM execution - registry lifecycle usually controls
connect()anddisconnect() - advanced code can inspect the native SQLAlchemy async engine through
database.engineafter 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 | |
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 | |
_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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
_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 | |
_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 | |
_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 | |
_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 | |
_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 | |
_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 | |
_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 | |
_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 | |
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 | |
_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 | |
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 | |
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 | |
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 | |
_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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |