Session lifecycle (internals)¶
How OntoSession / AsyncOntoSession manage SQL transactions, the unit-of-work queue, and optional graph sync. For hybrid deployments see HYBRID.md.
Context manager contract¶
Always use sessions as context managers:
On normal exit (__exit__ with no exception):
flush()— drainSessionState.pending(all queued saves/deletes execute SQL)commit()— commit the SQLAlchemy transactionflush_graph_sync()— apply queued graph pushes/removes (after SQL is durable)
On exception exit:
clear_graph_sync()— discard queued graph updatesrollback()— SQLAlchemy transaction rollback- Pending queue is not auto-cleared (see Rollback)
Save decision tree¶
save(instance) calls resolve_save_is_new_and_snapshot in session/_ops.py:
sequenceDiagram
participant App
participant Session
participant DB
App->>Session: save(instance)
Session->>Session: identity unset?
alt insert path
Session->>DB: optional SELECT if identity set but row may exist
Session->>Session: compile insert or update plan
else update path
Session->>Session: use snapshot / model_fields_set for partial update
end
Session->>Session: queue or flush plan
Session->>Session: queue graph push if graph_sync configured
- Insert vs update: Determined by whether mapper identity is unset (
_identity_unset) and whether a DB row already exists for that identity (hidden SELECT on save when identity is set). - Partial updates: On update, only fields in
instance.model_fields_setthat map to columns are written (Pydantic “touched fields” semantics). flush_now=False: Plan stays inpendinguntilflush()or context exit.
Delete¶
delete(instance) compiles a root-table delete plan, queues graph remove (if configured), and optionally flushes immediately.
Flush¶
flush() processes pending sequentially:
- Each successful item is removed from the queue and executed against the current SQLAlchemy transaction.
- If item N fails, items N+1… remain queued; graph sync pushes added during the failed flush are restored to the pre-flush queue length.
- Not an atomic multi-item transaction unless the database driver/session already wraps them.
Rollback¶
session.rollback():
- Rolls back the SQLAlchemy transaction.
- Always clears graph sync queues (even when
clear_uow=False). - Clears
pendingand pending-delete tombstones whenclear_uow=True(default).
Pass clear_uow=False only when you intentionally want queued SQL writes to flush on context exit after rollback. A UserWarning is emitted if the pending queue remains non-empty.
After rollback(), call expire_all() or expire(...) if identity map state is stale relative to the database.
Graph sync timing¶
Graph updates are queued during save() / delete() and applied after SQL commit:
| Event | SQL | Graph |
|---|---|---|
save() |
Queued or flushed per flush_now |
Queued push (after successful flush in deferred path) |
delete() |
Queued or flushed | Queued remove (uses pre-delete snapshot for nested IRIs) |
commit() |
Durable | Not yet updated |
flush_graph_sync() |
— | Push/remove applied |
If graph sync fails after commit, SQL remains committed. GraphSyncError is raised; use retry_graph_sync() after fixing the target. See HYBRID.md — split-brain.
Async sessions: flush_graph_sync() is synchronous and may block the event loop for remote graph I/O. Prefer in-memory StoreSyncTarget for tests or wrap remote sync in a worker.
clear_pending vs rollback¶
| Call | SQL transaction | pending queue |
Graph queue |
|---|---|---|---|
rollback() (default) |
Rolled back | Cleared | Cleared |
rollback(clear_uow=False) |
Rolled back | Unchanged | Unchanged |
clear_pending() |
Unchanged | Cleared | Cleared |
Identity map¶
SessionState caches instances by (entity_type, identity_value) using entity_type.identity_field. Mappers must use the same identity field name as the entity (MapperRegistry validates on register).
Related¶
- guides/cascade-policies.md — nested write behavior
- guides/bridge-tables.md — collection bridge wipe-and-reinsert
- TROUBLESHOOTING.md — common errors