UrsulaConnection

Last updated 3 November 2026

View .md

UrsulaConnection is the connection open() returns: what it adds to sqlite3.Connection, synced(), asynced(), status() and the reset callbacks, and what changes in the members it keeps.

It is a sqlite3.Connection subclass, so isinstance holds and any library that takes a sqlite3 connection takes it. Made with sqlite3.connect(..., factory=UrsulaConnection) outside open(), it binds nothing and works as a plain sqlite3.Connection.

UrsulaConnection

class UrsulaConnection(sqlite3.Connection):
    name: str
    path: str
    async def asynced(self, timeout: float | None = None) -> None: ...
    def off_reset(self, callback: ResetCallback) -> None: ...
    def on_reset(self, callback: ResetCallback) -> ResetCallback: ...
    def status(self) -> dict[str, Any]: ...
    def synced(self, timeout: float | None = None) -> None: ...

name

The database’s path on Tonbo, such as /acme/notes, or "" for a connection open() did not make.

path

The local database file the database is bound to, as status()["path"] reports it. A framework that opens its own sqlite3 connection by path uses the same database through it: its commits replicate too and synced() covers them. Keep db open while it does, since the binding retires once its last connection closes.

other = sqlite3.connect(db.path)  # replicates too, and db.synced() covers it

synced()

def synced(self, timeout: float | None = None) -> None: ...
db.execute("INSERT INTO notes(body) VALUES (?)", (body,))
db.commit()
db.synced()  # returns once Tonbo holds the commit
send(reply)

Returns once Tonbo holds every commit this process made on the database before the call, and a round trip that started after the call found this process still its writer. Call it before anything outside the process acts on what you wrote or read, such as a reply or a queue acknowledgement (see When a commit is durable). It covers every connection of the process on this database, a framework’s included, and no other database: call it on each one an output depends on, or use gate().

ParameterTypeDefaultWhat it does
timeoutfloat | NoneNone: no limitHow many seconds to wait, zero or more, at most threading.TIMEOUT_MAX. Past it, TimeoutError, while the commits keep replicating: call it again to keep waiting. Any other value raises invalid_argument.
  • Network failures never raise: it waits until Tonbo is reachable again. Ctrl-C interrupts it.
  • It raises TimeoutError after timeout, UrsulaResetError once the database is reset, and forked in a forked child.
  • On a reader cache, from a read_only open in a process that does not own the database, it returns at once.
  • It is safe to call any number of times, from any thread: each call waits for the commits made before it. After a TimeoutError, call it again to keep waiting.

asynced()

async def asynced(self, timeout: float | None = None) -> None: ...
db.execute("INSERT INTO notes(body) VALUES (?)", (body,))
db.commit()
await db.asynced(timeout=5)  # waits on the event loop
await send(reply)

synced() for asyncio: the same timeout, the same guarantee and the same errors, and it waits on the event loop instead of blocking it. Cancelling it ends the wait, while the commits keep replicating.

status()

def status(self) -> dict[str, Any]: ...
status = db.status()
if status["commits"]["pending"]:
    print("waiting for Tonbo:", status["replication"]["condition"])

The database’s state, read locally with no network, as a dict (dict[str, Any]). It is the JSON the extension’s ursula_status returns to every host, so its keys are the fields Status lists, nested as there: such as commits.pending, the commits on this machine Tonbo does not hold yet, and replication.condition, why they wait. It raises forked in a forked child, and not_bound once the database retired.

on_reset()

def on_reset(self, callback: ResetCallback) -> ResetCallback: ...
@db.on_reset
def reset(error: sqlite_ursula.UrsulaResetError) -> None:
    log.warning("notes was reset: %s (%s)", error.kind, error.action)

Runs callback once when the database is reset, after its pending synced() calls were rejected, on a thread of the package’s. It returns callback, so it serves as a decorator. A connection keeps its callbacks until it closes; a closed one runs none. What to do after a reset depends on its kind (see Troubleshooting).

ParameterTypeDefaultWhat it does
callbackResetCallbackRequiredCalled once, with the UrsulaResetError, when the database is reset. What it returns is ignored, and an exception it raises is logged to the sqlite_ursula logger.

off_reset()

def off_reset(self, callback: ResetCallback) -> None: ...

Removes a callback on_reset() added. Both raise forked in a forked child.

ParameterTypeDefaultWhat it does
callbackResetCallbackRequiredA callback on_reset() added to this connection. Any other does nothing.

The sqlite3 members

The members it keeps from sqlite3.Connection take the same parameters and return the same values. What changes:

MemberWhat changes
cursorReturns an UrsulaCursor unless given a factory.
executeAs sqlite3’s. A failure of the database raises an UrsulaError, a refused write or a reset; every other error is sqlite3’s, unchanged.
executemanyAs execute.
executescriptAs execute.
commitAs sqlite3’s. A refused COMMIT raises UrsulaWriteRefusedError with nothing committed. It returns once the commit is on this machine: call synced() before anything outside the process acts on it.
rollbackAs sqlite3’s; after a refused write or a reset there is nothing to roll back, and it does nothing (see After a refused write).
closeCloses the connection. Once the database’s last connection closes, the commits left keep replicating in the background and the binding retires; open() it again to use it. In a child forked after the parent made the connection it closes nothing, since the file’s locks are the parent’s.
autocommitOn Python 3.12 or later, as sqlite3’s, except that setting it to False raises invalid_argument: in that mode the connection is always in a transaction, which would hide a refused commit.

with db: commits or rolls back as sqlite3’s does, and raises as commit does. Its other members, such as backup, blobopen and serialize, are sqlite3’s, unchanged. Keep the default transaction control, isolation_level=None or autocommit=True.

After a refused write or a reset

A refused write and a reset are I/O errors on which SQLite rolls the whole transaction back, so no transaction is open after one. The connection then keeps the error until a statement succeeds: a ROLLBACK run through execute succeeds, since nothing is left to roll back, while a COMMIT or END that finds no transaction, a ROLLBACK TO or RELEASE that finds no savepoint, and a ROLLBACK inside executescript raise the kept error again. So a transaction helper’s own ROLLBACK or COMMIT never replaces the error you need. Run the whole transaction again after backlog_full, once synced() returned.

UrsulaCursor

class UrsulaCursor(sqlite3.Cursor):
    def execute(self, sql: str, parameters: Any = (), /) -> UrsulaCursor: ...
    def executemany(self, sql: str, parameters: Any, /) -> UrsulaCursor: ...
    def executescript(self, script: str, /) -> UrsulaCursor: ...
    def fetchall(self) -> list[Any]: ...
    def fetchmany(self, size: int | None = None) -> list[Any]: ...
    def fetchone(self) -> Any: ...

The sqlite3.Cursor subclass cursor() returns. Its execute, executemany, executescript, fetchall, fetchmany and fetchone, and iterating it, take what sqlite3.Cursor’s take and raise as execute does. A cursor of a connection open() did not make works as a plain sqlite3.Cursor. For a first open, see Connect from Python.