Skip to content

SQL, CRUD, and upserts

These methods are available on SyncBaseDB and AsyncBaseDB. Add await to the async calls. Values belong in params or row mappings; SQL identifiers and predicates are trusted SQL text.

execute(sql, params=None) -> Cursor

Execute one statement. params is a positional sequence, named mapping, or None; the returned cursor exposes rowcount and lastrowid:

cur = db.execute("INSERT INTO users(name) VALUES (?)", ("Alice",))
user_id = cur.lastrowid
cur.close()

await async_db.execute("UPDATE users SET name=:name WHERE id=:id", {
    "name": "Alicia", "id": user_id,
})

Writes commit automatically unless a transaction is active. Table and column identifiers accepted by CRUD helpers are quoted; raw SQL and where expressions remain the caller's responsibility.

execute_many(sql, seq_params) -> Cursor

Execute one statement for each positional sequence:

db.execute_many(
    "INSERT INTO users(name) VALUES (?)",
    [("Alice",), ("Bob",)],
)

seq_params is an iterable of sequences, not a mapping. The batch itself is atomic; use a transaction when it must also be atomic with other statements.

insert_one(table, row) -> Any

Insert one column/value mapping. Return the supplied primary key or generated lastrowid:

user_id = db.insert_one("users", {"name": "Alice"})
await async_db.insert_one("users", {"id": 10, "name": "Bob"})

The table must have a single-column primary key. The method never updates an existing row.

insert_many(table, rows) -> None

Insert multiple mappings with the same column shape:

db.insert_many("users", [{"name": "Alice"}, {"name": "Bob"}])

An empty list is a no-op. Different keys between rows raise ValueError, and any database error rolls back the complete batch.

upsert_one(table, row) -> Any

Insert a row or update an existing row selected by its single-column primary key:

db.upsert_one("users", {"id": 1, "name": "Alicia", "active": True})

If the key is omitted this behaves as an insert. Native upsert requires SQLite 3.24.0 or newer. Use legacy_sqlite_support=True when opening an old backend to enable the slower compatibility path.

upsert_many(table, rows) -> None

Apply several upserts with compatible column shapes:

await async_db.upsert_many(
    "users",
    [{"id": 1, "active": False}, {"id": 2, "name": "Bob"}],
)

An empty list is a no-op. Upserts are serialized by an internal lock and the complete batch is rolled back if any row fails.

update_one(table, pk, row) -> int

Update the columns in row where the primary key equals pk. Return the affected row count (0 or 1):

changed = db.update_one("users", 1, {"name": "Alicia", "active": False})

An empty mapping returns 0. The primary-key column is taken from the schema.

delete_one(table, pk) -> int

Delete one row by primary key and return the affected row count:

deleted = await async_db.delete_one("users", 1)

Deleting a missing key returns 0.

delete_many(table, where, params=None) -> int

Delete rows matching the SQL predicate in where:

deleted = db.delete_many(
    "users", "active = ? AND name LIKE ?", (False, "test:%")
)

where is inserted into the statement and must be trusted. Put external values in params.