@wringer/db
v0.1.2
Published
The db driver for wringer: SQL against a target database, read-only unless asked to write
Readme
The db driver for wringer: it runs SQL on a target's SQLite or Postgres database.
Install
bun add @wringer/core @wringer/dbThe package runs on Bun 1.3 or newer.
Usage
import { createWringer, local, ssh } from '@wringer/core';
import { db } from '@wringer/db';
export const registry = createWringer({ name: 'shop' })
.use(db({}))
.target('local', {
mode: 'attach',
access: 'read-write',
host: local(),
drivers: { db: () => ({ via: 'sql', adapter: 'sqlite', url: '/srv/shop/shop.db' }) },
})
.target('prod', {
mode: 'attach',
access: 'read-only',
host: ssh({ destination: '[email protected]' }),
drivers: { db: () => ({ via: 'exec', dialect: 'postgres', database: 'shop' }) },
})
.build();An action calls ctx.drivers.db.query({ sql, params, write }). The CLI adds db query "<sql>",
with --write for a statement that writes, and the doctor check db.connect.
Connections
via: 'sql'connects from the CLI withbun:sqliteor Bun.SQL to the database inurl. Postgres stops a query at astatement_timeoutof the time left, and the driver cancels it at the deadline. Abun:sqlitecall is synchronous, so a SQLite write can commit after the timeout.via: 'exec'runs the database's own client on the target's host, with the SQL on stdin:sqlite3 -json, orpsqlwith the rows turned into JSON byjson_agg.programreplaces the client's command, such as['docker', 'exec', '-i', 'db', 'psql'].
A query through via: 'exec' has these limits:
- It takes no
params. - sqlite3 runs in
-safemode, which turns off the dot-commands that reach past the database. - A statement for psql, or a read for sqlite3, runs alone, and one for psql holds no backslash.
- A Postgres write returns no rows. Connect with
via: 'sql'to read itsRETURNINGrows. - An
explainread, or ashowthrough psql, fails with a usage error. sqlite3 runs apragmaread as its table-valued function, with no schema, so run a pragma that sets a value as a write. - At the deadline, the host stops the client's process group. A client that
programstarts in a container, such as withdocker exec, runs on, so a sqlite3 write there can commit after it. - psql gets a
statement_timeoutof the time left when it starts, by the database host's clock, which must agree with the CLI's. It refuses to start once no time is left.
Results
A query returns { columns, rows, rowCount }.
rowCountcounts the rows a read returned, or the rows a write changed. It is null for a write through sqlite3, and for a psql write whose command tag holds no count, such asCREATE TABLE.columnsholds a read's columns in order, also with no rows: from the statement on SQLitevia: 'sql', else from a wrapper, or from the first row of a Postgresshoworexplain.
Reads and writes
- A query is a read unless it passes
write: true, and a read runs read-only in the database. On SQLite,via: 'sql'opens the file read-only withquery_onlyon, andvia: 'exec'runs sqlite3 with-readonly. Postgres runs a read in a read-only transaction. - A query through
via: 'sql'holds one statement, so a read cannot end its transaction early. - On SQLite, a read starts with
select,with,values,explainorpragma, and one that runsattach,vacuumorquery_onlycounts as a write. A read-only handle still runsVACUUM INTO, which copies the database to any path. - On Postgres, a read starts with
select,with,values,table,showorexplain, and holds none ofinsert,update,delete,merge,into,create,executeordeclare. Any other read counts as a write: a read-only transaction still runsNOTIFY,SETorPREPARE. - On Postgres, a read that holds the word
copyorlo_exportcounts as a write, inside a string or a quoted name too, but never inside a comment: a read-only transaction still lets both write server files. ADOblock, which runs SQL from a string, and aU&"…"name count as writes. - On Postgres, a read calls only the listed functions, such as
count. Any other call counts as a write: a read-only transaction still runsset_config,pg_sleeporpg_terminate_backend. So do a schema other thanpg_catalogbefore a function name, and a column list after a name, such aswith totals(day, amount). - The driver catches only these statements, so give a read-only target a role with no file rights.
- A statement that writes without asking fails with a usage error. A read-only target refuses each
write with
read-onlyand exit code 3, unlessreadOnly.allowlists it verbatim. - A database that the driver cannot reach or open, or that refuses a declared write, such as a
replica, fails with exit code 4 (
environment). A statement stopped at the deadline exits 5.
