database
v0.5.4Talk to PostgreSQL, MySQL, and SQLite from iii — query, execute, transactions, prepared statements, and change feeds.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64 · x86
exact versions are immutable; binary and bundle artifacts are digest-pinned.
skill doc
database
The database worker connects to PostgreSQL, MySQL, and SQLite through a
managed per-database connection pool. Every callable surface lives under
the database::* namespace. The driver is chosen from each database URL
scheme (sqlite:, postgres://, postgresql://, mysql://).
Runtime settings live in the configuration worker under id database;
pools hot-reload when the value changes. SQLite is the recommended starting
point. Placeholder syntax: ? for SQLite and MySQL, $1/$2/… for Postgres.
When to Use
- You need to read rows from a configured database (
database::query). - You need to insert, update, delete, or run DDL and read affected-row
counts or autoincrement ids (
database::execute). - Several statements must commit or roll back together as one unit
(
database::transaction,database::executeBatch, or the interactive transaction surface). - The same parameterized SQL will run many times and you want to skip
per-call parse/plan cost (
database::prepareStatement+database::runStatement). - You need read-your-writes across round-trips with logic between steps
(
database::beginTransaction…commitTransaction/rollbackTransaction).
Boundaries
- Not a migration tool, ORM, or schema designer — pass raw SQL only.
- Not a general pub/sub bus.
database::row-changedreports only what THIS worker wrote, on commit — not change data capture; a write from psql or another worker is invisible to it. database::queryis read-oriented; usedatabase::executefor writes. Running a SELECT throughexecutediscards rows.- Prepared handles pin a pool connection until TTL expiry — not transactions.
Batch
database::transaction/database::executeBatchneed every statement up front; use the interactive surface when code must branch between steps. - MySQL ignores the
returningoption onexecute(warn-once). SQLite degradesread_committed/repeatable_readisolation to serializable. - For filesystem or shell operations, use the
shellworker instead.
Functions
database::query— run read-only SQL and return rows, row count, and column metadata.database::execute— run write SQL (INSERT/UPDATE/DELETE/DDL) and return affected rows, optional last insert id, and optional RETURNING rows.database::executeBatch— convenience form oftransaction: statements may be bare SQL strings or{sql, params}objects (preferparamsfor dynamic values). Same atomic semantics, envelope, andfailed_indexreporting astransaction.database::prepareStatement— parse and plan SQL once; return a handle that pins a pool connection until TTL expiry.database::runStatement— re-execute a prepared handle with new bind params; response shape matchesquery.database::transaction— run an ordered batch of statements atomically; rolls back on first failure and reportsfailed_index.database::beginTransaction— open an interactive transaction and return an id plus expiry deadline.database::transactionQuery— read SQL inside an open interactive transaction; same envelope asquery.database::transactionExecute— write SQL inside an open interactive transaction; same envelope asexecute. Rejects bare transaction-control SQL — finalize viacommitTransactionorrollbackTransaction.database::commitTransaction— commit and finalize an interactive transaction.database::rollbackTransaction— roll back and finalize an interactive transaction.database::listDatabases— every configured database with its driver, credential-redacted URL, pool settings and TLS mode. Config only; usedatabase::healthfor live state.
Interactive transactions auto-roll back when timeout_ms elapses (default
30 s, max 5 min). Prepared handles default to a 1 h TTL (max 24 h) with no
explicit release call — let them expire or stop using them when done.
Reading the schema
One shape across all three drivers — prefer these over hand-writing
sqlite_master / information_schema / PRAGMA.
database::listTables— tables and views, with kind and (postgres) schema.database::describeTable— columns with type, nullability, default, primary key and a structuredforeign_keyof{ schema, table, column }; plus indexes and a planner row estimate.database::describeSchema— the same for every table in one pass. Use this rather than loopingdescribeTable.database::schemaDiagram— positioned nodes, routed foreign-key edges, hubdegreeandisolatedtables. For reasoning about a schema's shape, not only for drawing it.
Reading data
database::browseTable— paged, sorted, filtered reads with no SQL. Filters are{ column, op, value }andtotalhonours them. Follow a foreign key with an equality filter atpage_size: 1.database::explain— the plan as a tree with costs and warnings.analyzeruns the statement, so it defaults to false and is refused for anything that is not a single read.database::columnStats— planner statistics by default (approximate, labelledsource: planner);exact: truescans. To profile rows you already hold, use thefpworker on abrowseTableresult instead.
Operations and reuse
database::health— pool occupancy, active queries, table sizes, locks, cache ratio. Each section isavailable,unsupportedordenied, so a driver gap is never mistaken for an empty result.database::terminateQuery— end a session, or cancel its statement withcancel_only. Takes an id fromdatabase::health.database::saveQuery,database::listSavedQueries,database::deleteSavedQuery— named queries per database, kept in thestateworker.database::history— recent queries, newest first. Best effort, not an audit log; binddatabase::row-changedfor that.
Reacting to writes
Register a database::row-changed trigger to be told when this worker commits
a change, instead of polling:
{ "trigger_type": "database::row-changed", "config": { "db": "primary", "table": "orders", "ops": ["insert"] } }The event is { db, table, op, affected_rows, returning?, at }. It fires on
commit — an interactive transaction's writes are announced by
commitTransaction, and a rollback announces nothing. table is null when the
statement's table cannot be read off the SQL (a CTE-wrapped write), and
runStatement does not fire because it has no affected-row count to report.
Delivery is best-effort: it is not durable with the commit and has no replay or
exactly-once guarantee.