js/database

js/database/index.ts

fino:database — generic relational database facade.

This module provides the common application-facing database shape and routes connection strings to concrete engines. Database.open() inspects the target: postgres:// and postgresql:// URLs connect through fino:database/postgres, while everything else — filesystem paths, ':memory:', and sqlite:// URLs — opens through fino:database/sqlite. Both engines are wrapped in the same DatabaseConnection interface, so application code written against this module stays portable across drivers.

The shared API intentionally covers only portable connection, statement, query, transaction, and disposal behavior. Engine-specific features such as SQLite VFS/extension loading or Postgres LISTEN/NOTIFY remain on the concrete connection classes, which this module re-exports as PostgresDatabase and the sqlite namespace.

Two layers smooth over placeholder differences between the engines. The sql template tag builds SqlFragment values that render ? placeholders for SQLite and $n for Postgres while carrying bound values separately from the SQL text. Plain string queries prepared on a Postgres connection are additionally normalized: ? positional and :name named placeholders are rewritten to $n (quoted regions and :: casts are left untouched), so the same query string works against both engines.

Connections implement Symbol.asyncDispose, so await using closes them automatically at scope exit.

import { Database, sql } from 'fino:database';

await using db = await Database.open(process.env.DATABASE_URL ?? ':memory:');
await db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
await db.prepare('INSERT INTO users (name) VALUES (:name)').run({ name: 'Ada' });
const rows = await db.prepare(sql`SELECT * FROM users WHERE id = ${1}`).all();
console.log(rows);

Types

type DatabaseDriver = 'sqlite' | 'postgres'

Identifier for the concrete engine behind a connection.

Database.open() selects the driver from the connection target and exposes the choice as DatabaseConnection.driver, letting portable code branch into dialect-specific SQL only where it must.

type DbValue = null | undefined | bigint | number | string | boolean | Uint8Array | Date

Portable values accepted for parameter binding and returned in result rows.

null and undefined bind as SQL NULL. bigint and integral number values bind as integers, other numbers as floating point, strings as text, and Uint8Array as BLOB/bytea. Date is serialized to an ISO-8601 string by the Postgres engine; SQLite has no native date binding, so convert dates to strings or numbers explicitly in portable code.

type DbParams = DbValue | Record<string, DbValue>

One argument to statement execution: a positional value or a record of named parameters.

Positional values fill ? (or $n) placeholders in order. A record binds named placeholders — write :name in the SQL and key the record by the bare name. Prefer either positional values or a single named record per call: SQLite rejects mixing a named record with positional placeholders, while the Postgres placeholder rewrite tolerates the combination.

type DbRow = Record<string, DbValue>

A single result row: column names mapped to portable values.

Value representation follows the engine's type mapping — for example SQLite integers arrive as number by default (or bigint when the connection was opened with the engine's safeIntegers option).

type SqlDialect = DatabaseDriver

Placeholder dialect used when rendering a SqlFragment to text.

Identical to DatabaseDriver: 'postgres' renders $1, $2, … placeholders while 'sqlite' renders ?.

type SqlInput = string | SqlFragment

Query forms accepted by exec() and prepare(): a raw SQL string or a SqlFragment built with the sql tag.

Interfaces

interface QueryResult {

Outcome of a statement executed with DatabaseStatement.run().

changes is always present; the other fields depend on the engine that produced the result.

Properties

changes: number

Number of rows inserted, updated, or deleted by the statement.

lastInsertRowid?: bigint

Rowid of the most recently inserted row. Present on SQLite results only; Postgres omits it.

command?: string

Server command tag such as INSERT 0 1. Present on Postgres results only; SQLite omits it.

interface DatabaseStatement {

Prepared statement handle shared by both engines.

Statements come from DatabaseConnection.prepare() and may be executed repeatedly with different parameters. Each execution method accepts positional values or a single record of named parameters (see DbParams). When the statement was prepared from a SqlFragment, the fragment's interpolated values are bound first and call-time parameters follow them.

import { Database } from 'fino:database';

await using db = await Database.open(':memory:');
await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');

const insert = db.prepare('INSERT INTO users (name) VALUES (?)');
await insert.run('Ada');
await insert.run('Grace');
insert.finalize();

const byId = db.prepare('SELECT * FROM users WHERE id = :id');
console.log(await byId.get({ id: 1 }));

for await (const row of db.prepare('SELECT name FROM users').iterate()) {
  console.log(row.name);
}

Methods

run(...params: DbParams[]): Promise<QueryResult>

Execute the statement for its side effects and resolve with the change count plus engine-specific metadata. Use for INSERT/UPDATE/DELETE and DDL; result rows, if any, are not returned.

get(...params: DbParams[]): Promise<DbRow | undefined>

Execute the statement and resolve with the first result row, or undefined when the query produces no rows.

all(...params: DbParams[]): Promise<DbRow[]>

Execute the statement and resolve with every result row as an array.

iterate(...params: DbParams[]): AsyncGenerator<DbRow>

Execute the statement and yield result rows one at a time, avoiding buffering the full result set in memory.

finalize(): void

Release the engine's underlying prepared statement. Call once the statement will not be executed again; executing after finalization is invalid.

interface DatabaseConnection {

Engine-agnostic connection contract returned by Database.open().

Both the SQLite and Postgres engines are wrapped in this shape: exec() for fire-and-forget statements, prepare() for reusable parameterized statements, transaction() for atomic groups, and close() (aliased as Symbol.asyncDispose) for teardown. Anything beyond this portable core lives on the concrete engine classes.

import { Database, sql } from 'fino:database';

await using db = await Database.open(process.env.DATABASE_URL ?? ':memory:');
await db.exec('CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY, balance REAL)');

await db.transaction(async () => {
  await db.prepare(sql`UPDATE accounts SET balance = balance - ${25} WHERE id = ${1}`).run();
  await db.prepare(sql`UPDATE accounts SET balance = balance + ${25} WHERE id = ${2}`).run();
});

console.log(db.driver, await db.prepare('SELECT * FROM accounts').all());

Readonly Properties

readonly driver: DatabaseDriver

The engine this connection routes to. Branch on it when a query needs dialect-specific SQL.

Methods

exec(query: SqlInput): Promise<void>

Run a query for its side effects, discarding any result rows.

Plain strings and value-free fragments go through the engine's exec path, which typically permits multiple ;-separated statements. A fragment carrying interpolated values is instead prepared and run once so the values bind as parameters.

prepare(query: SqlInput): DatabaseStatement

Create a reusable DatabaseStatement from a string or SqlFragment.

Plain strings prepared on Postgres are placeholder-normalized: ? and :name become $n, skipping quoted text and :: casts. SQLite strings pass through untouched because SQLite natively understands both forms. Fragments render dialect-native placeholder text with their interpolated values pre-bound ahead of any call-time parameters.

transaction<T>(fn: () => Promise<T>): Promise<T>

Run fn inside a transaction: BEGIN first, COMMIT when it resolves, and ROLLBACK when it throws (the original error is rethrown).

close(): Promise<void>

Close the connection and release the engine's resources.

interface SqlFragment {

Portable SQL fragment produced by the sql tag.

A fragment keeps the SQL text and its interpolated values separate so the values can be bound as statement parameters instead of being spliced into the query string. Fragments compose: interpolating one fragment inside another splices its text and renumbers its value slots.

Detection is structural — anything with kind: 'sql', a values array, and a text function is treated as a fragment — so fragments do not need to come from this module's own sql tag to compose with it.

import { sql, type SqlFragment } from 'fino:database';

const where: SqlFragment = sql`WHERE id = ${42}`;
const query = sql`SELECT * FROM users ${where}`;
console.log(query.text('postgres')); // SELECT * FROM users WHERE id = $1
console.log(query.text('sqlite'));   // SELECT * FROM users WHERE id = ?
console.log(query.values);           // [42]

Readonly Properties

readonly kind: 'sql'

Discriminant marking a value as a SQL fragment; always 'sql'.

readonly values: DbValue[]

Interpolated values in placeholder order, ready for statement binding.

Methods

text(dialect: SqlDialect): string

Render the fragment as SQL text with dialect-native placeholders: $1, $2, … for 'postgres' and ? for 'sqlite'.

interface SqlTag {

Shape of the sql template tag: callable as a tag, with helpers for the positions placeholders cannot express.

import { sql, type SqlTag } from 'fino:database';

const tag: SqlTag = sql;
const fragment = tag`SELECT ${1} AS answer`;
console.log(fragment.values); // [1]

Methods

raw(text: string): SqlFragment

Insert text verbatim, with no escaping or binding. Only pass trusted, program-controlled SQL — never user input.

identifier(name: string): SqlFragment

Quote name as a double-quoted SQL identifier for table or column positions. Throws TypeError unless the name starts with a letter or underscore and contains only letters, digits, underscores, and spaces.

join(values: unknown[], separator?: SqlFragment | string): SqlFragment

Join values with separator (', ' by default). Plain values become bound parameters and fragments are spliced; a string separator is inserted as raw SQL.

Constants

const sql: SqlTag

Template tag for building parameterized, dialect-portable SQL.

Interpolated values never enter the SQL text: each ${value} becomes a placeholder and the value travels in SqlFragment.values for statement binding, which makes value positions safe from SQL injection. Interpolating another fragment splices it inline with its placeholders renumbered, so queries can be assembled from reusable pieces.

The helpers cover what placeholders cannot: sql.identifier() validates and quotes table or column names, sql.join() expands lists (for example IN (...) membership tests), and sql.raw() inserts trusted SQL text verbatim.

import { Database, sql } from 'fino:database';

await using db = await Database.open(':memory:');
const table = sql.identifier('users');
const names = ['Ada', 'Grace'];
const query = sql`SELECT * FROM ${table} WHERE name IN (${sql.join(names)})`;
// postgres: SELECT * FROM "users" WHERE name IN ($1, $2)
// sqlite:   SELECT * FROM "users" WHERE name IN (?, ?)
const rows = await db.prepare(query).all();
console.log(rows);

Classes

class Database {

Entry point that opens a connection on whichever engine matches the target.

This is the portable front door of the module: hand it a connection string (typically from configuration such as DATABASE_URL) and it returns a DatabaseConnection backed by the right engine. Code that needs engine-specific features should open PostgresDatabase or sqlite.Database directly instead.

import { Database } from 'fino:database';

await using pg = await Database.open('postgres://app@localhost/app');
await using file = await Database.open('./data/app.db');
await using mem = await Database.open(':memory:');
console.log(pg.driver, file.driver, mem.driver); // postgres sqlite sqlite

Static Methods

static async open( target: string | URL, options: Record<string, unknown> = { } ): Promise<DatabaseConnection>

Open a connection to target and resolve with a DatabaseConnection.

Targets matching postgres:// or postgresql:// (case-insensitive) connect over the Postgres wire protocol. Anything else is treated as SQLite: a filesystem path, ':memory:', or a sqlite:// URL whose decoded path names the database file (an empty path means in-memory). options passes through to the selected engine's open(), so engine-specific settings — SQLite safeIntegers or fs, Postgres credentials or tls — remain available.

Throws if the engine's shared library is unavailable or the connection cannot be established.