Source Code:
src/gaia/database/mixin.pyComponent: DatabaseMixin
Module:
gaia.database.mixin
Import: from gaia.database.mixin import DatabaseMixinOverview
DatabaseMixin is a zero-dependency SQLite mixin for agents that need
persistent, queryable state (user intake records, chat sessions, job queues,
etc.). It wraps Python’s stdlib sqlite3 with a small ergonomic API and an
optional transaction context manager.
Design goals:
- No extra dependencies. Uses only
sqlite3from the standard library. - Dict-oriented API. Every query returns dicts (via
sqlite3.Rowunder the hood) so tools can serialize results without transformation. - LLM-friendly SQL. Explicit parameterised SQL with
:nameplaceholders and a consistent(table, data, where, params)signature for CRUD operations. - Safe concurrency for agents. Single connection with
check_same_thread=False; usetransaction()for atomic writes.
API Reference
All methods are instance methods on any class that mixes inDatabaseMixin.
Connection lifecycle
init_db(path)— open (or re-open) a SQLite connection. Accepts:memory:or a filesystem path; parent directories are auto-created. Enables foreign keys (PRAGMA foreign_keys = ON) and usessqlite3.Rowfor dict-style access.close_db()— close the connection. Safe to call multiple times; safe to call beforeinit_db.db_ready—Trueif a connection is open.
RuntimeError("Database not initialized. Call init_db() first.") if the connection isn’t ready.
Reads
:name placeholders. When one=True, returns either a
single row dict or None; otherwise returns a list of row dicts (possibly
empty).
query() is a bare execute() — it does not verify the statement is a
SELECT, so it will happily run a DELETE. It is for SQL you wrote yourself.
query(), but installs a SQLite
authorizer for the duration of the call. Permitted actions are SQLITE_SELECT,
SQLITE_READ, SQLITE_FUNCTION, SQLITE_RECURSIVE, and SQLITE_PRAGMA
restricted to table_info, table_xinfo, index_list, index_info, and
foreign_key_list. Everything else — writes, DDL, ATTACH, any other pragma —
is denied by SQLite at statement-prepare time and re-raised as
PermissionError. A genuine SQL error (missing table, corrupt file) propagates
as its original sqlite3 exception rather than being relabelled.
Use this for any SQL that originates outside trusted Python code; db_query on
DatabaseAgent is built on it.
Writes
insert(table, data)— buildsINSERT INTO table (...) VALUES (...)from the data dict and returns the new row’slastrowid.update(table, data, where, params)— the implementation prefixes the data keys with__set_internally to keep them distinct from theparamsused in theWHEREclause; aparamskey that collides with that prefix raisesValueErrorrather than silently overriding the column value. Returns the affected row count.delete(table, where, params)— returns the deleted row count.
transaction()
block.
Validation contract. The table name, and for insert/update every key of
data, are interpolated into the statement rather than bound, so each must be
a bare identifier matching [A-Za-z_][A-Za-z0-9_]*. Quoted or non-ASCII
identifiers that SQLite would otherwise accept are rejected. ValueError is
raised for an invalid identifier, empty data, or an empty/whitespace where.
where is not parsed — beyond a non-blank-string check it is a trusted SQL
fragment, interpolated verbatim so callers keep access to the full grammar
(expires_at < :now, mailbox IS NULL, 1 = 1). It must therefore be a
literal written by the developer; never assemble it from LLM output or
end-user input. Bind untrusted values through :name placeholders inside a
literal fragment, or use the structured-condition tools on DatabaseAgent.
Transactions
execute() (raw DDL below) cannot be called inside a transaction — it will
raise RuntimeError.
Raw SQL / schema
execute(sql)— runs arbitrary SQL viaexecutescript(). Use forCREATE TABLE/CREATE INDEXbootstrap. Auto-commits any pending work.table_exists(name)— returnsTrueif a table of that name exists.
Example agent
Caveats
- SQLite only — there is no PostgreSQL/MySQL support. If you need a different backend, you’ll need a different mixin.
- Single connection per agent instance; the mixin assumes single-process
ownership. For multi-process scenarios, either use a shared DB file with
WAL-mode via
execute("PRAGMA journal_mode=WAL")or route writes through a single coordinator agent. - There is no ORM layer. You work directly with SQL and dict results, which is intentional — it keeps the surface small and LLM-friendly.
- Identifiers must be bare — see the validation contract under Writes. Table or column names that need quoting are not supported.
query_readonly()’s authorizer is connection-scoped, not call-scoped. Concurrent and nested read-only calls are reference-counted, so one finishing never disarms another still in flight. But the window applies to the whole connection: becauseinit_db()opens it withcheck_same_thread=False, a writer on another thread sharing that connection is denied for as long as any read-only call is open. Don’t share one mixin instance between a read-only caller and a background writer.
Related
- EMR Agent uses this mixin as its persistence layer.
- FileWatcher is commonly paired with
DatabaseMixinto record ingestion events.