Database drivers
For adding a backend or building an ORM.
| Export | Purpose |
|---|---|
defineDriver(spec) | Make a driver: name, schemes, dialect, open(url, options), and optionally pooled(url, options, poolOptions). |
PooledConnection | The pooled form pool: true builds. Subclass it to put your own surface on a pool. |
BaseConnection | Transactions, savepoints, the closed-connection check. Implement _query, _execute, _close; override _executeMany for a fast batch path, _cancel to really cancel, and the _*Transaction trio for a backend without SQL. |
Dialect | placeholder(i), quoteIdent(name), supports — including queryText, queryAst and transactions. |
defineRowShape(columns) / decodeBatch(bytes, shape, n) | The row decoder for a backend handed bytes. |
defineRecordShape(columns) / Rows.fromObjects(records) | The same Row contract for a backend whose values are already JavaScript. |
encodeParams / splitParams | Parameter encoding. |
ByteWriter | A growable buffer with length-prefix back-fill. |
mapError / asDbError | Map a backend's vocabulary onto the portable codes. |
runBackendConformance(open) | The suite that proves a backend behaves like the built-ins. |
import { defineDriver, BaseConnection, Dialect } from "runtime:db"; const dialect = new Dialect({ name: "mydb", placeholder: (i) => `$${i}` }); class MyConnection extends BaseConnection { constructor(socket) { super({ dialect, backend: "mydb" }); this.socket = socket; } async _query({ text, positional }) { /* … */ } async _execute({ text, positional }) { /* … */ } async _close() { /* … */ } // Optional. The default loops `_execute` — correct, and no faster than the // loop it replaces. Override it with whatever the backend does in one round // trip: a reused prepared statement, or pipelined protocol messages. async _executeMany(query, sets) { /* … */ } } export const driver = defineDriver({ name: "mydb", schemes: ["mydb"], dialect, open: async (url, options) => new MyConnection(await open(url, options)), });
Nothing is registered, and nothing has to be imported for its side effects: your package's export is the driver, and connect(url, { driver }) is how a caller reaches it. Export it as a named driver and nothing as a default, as the first-party packages do — one import shape for every backend, and a caller with two of them tells them apart with as.
If your rows are not bytes
The batch layout exists because a wire protocol hands over bytes and decoding them lazily is worth the machinery. A backend that never had bytes — a document store answering JSON, a graph or vector service over HTTP, an in-process engine holding objects — hands over records instead, and nothing downstream can tell the difference:
async _query(q) { const docs = await this.find(q.ast); // already objects return Rows.fromObjects(docs); // columns default to their keys }
For a driver that wants control of batching, a RowSource may answer { records, done } — each record an array of values in column order, paired with defineRecordShape(columns) — instead of { bytes, rows, done }. Batch either way: a caller who breaks out of the loop after three rows should not have paid for three thousand.
A backend that does not speak SQL declares it, and inherits the rest unchanged:
const dialect = new Dialect({ name: "mydb", placeholder: () => { throw new DbError("no placeholders here", { code: DbErrorCode.QueryForm }); }, supports: { queryText: false, queryAst: true, transactions: false }, });
supports also carries your own capabilities — a vector index, a full-text mode, a graph traversal. They survive on dialect.supports untouched, and they are how an ORM branches on a backend that did not exist when it was written. driver.dialect.supports answers before a connection is opened.
Most conformance checks are written in SQL. Against such a backend they are skipped with a reason rather than failed — a check you cannot express is not a finding — and what runs is the part that holds for every backend.
Adding a networked backend needs no new runtime code — a driver is JS over runtime:net. See Databases for the guide and Internals for why it is built this way.