jed

Queries & parameters

Running a query means two things: binding parameters into the SQL, and reading the rows back out. jed gives each language an ergonomic layer for both — and deliberately not the same shape in every language. Each core adopts its ecosystem’s de facto embedded-SQL idiom, so the code feels native rather than translated:

  • Rust — rusqlite’s traits: run, query_row, query_map, with ToValue / FromValue doing the conversions and row.get::<T>(…) reading a typed column.
  • Go — database/sql / pgx: Exec, Query, QueryRow taking ...any args, with Scan(&dest) and struct mapping.
  • TypeScript — better-sqlite3: db.prepare(sql) returns a Statement with run / get / all / iterate, and rows come back as plain objects.

Use the language selector in the top bar to switch this example between the three.

use jed::Database;

fn main() -> jed::Result<()> {
    let mut db = Database::open("app.jed")?;

    // run() binds native params — a tuple here — to $1, $2, … and returns the affected-row count.
    // No hand-built Value::Int / Value::Text: the rusqlite-style ToValue/Params traits do the
    // conversion (and a raw &[Value] still works — it implements Params too).
    let affected = db.run(
        "INSERT INTO account (id, name, balance) VALUES ($1, $2, $3)",
        (1, "Ada", 100_i64),
    )?;
    println!("inserted {affected} row");

    // query_row maps the FIRST row through a closure, returning Option<T> (None when nothing
    // matched). row.get::<T>(i) converts column i to a native type (FromValue); Option<T> is the
    // only target that accepts SQL NULL — a bare scalar rejects it with 22004.
    let balance: Option<i64> =
        db.query_row("SELECT balance FROM account WHERE id = $1", (1,), |row| row.get(0))?;
    println!("balance = {balance:?}");

    // query_map maps every row; read columns by index or by name.
    let names: Vec<String> = db.query_map("SELECT name FROM account ORDER BY id", (), |row| {
        row.get_by_name("name")
    })?;
    println!("{} account(s): {names:?}", names.len());

    Ok(())
}

Binding parameters

Parameters are positional $1, $2, … placeholders, bound left to right from the values you pass. You pass native values, not engine Values — the ergonomic layer converts them: integers, floats, booleans, strings, byte arrays, and NULL all map across. This keeps user data out of the SQL string, so there is no string-interpolation injection surface.

A note on integers, because it is the one place the type systems differ. jed integers are 64-bit and exact. In Rust and Go that is the natural integer type. In TypeScript a number is a float, so jed uses bigint for integer values — an integer-valued number like 1 still binds as an integer (you write run(1), not run(1n)), but values read back come as bigint so a large i64 never loses precision.

Reading rows

How a row arrives depends on the idiom:

  • Rust hands each row to a closure as a Row; call row.get::<T>(index) or row.get_by_name::<T>(name) to pull a typed column. query_row returns Option<T> (None when nothing matched); query_map maps every row.
  • Go scans columns into pointers with Scan(&a, &b), or maps a whole row into a struct by column name with RowToStructByName. QueryRow(...).Scan(...) returns jed.ErrNoRows on an empty result.
  • TypeScript returns each row as an object keyed by output column name — get gives the first (or undefined), all gives an array, iterate yields them lazily.

NULL

SQL NULL needs an explicit home, so it can’t silently become a zero. Each layer has a nullable target: Rust’s Option<T> (a bare T rejects NULL with 22004), Go’s *jed.Null[T] (or *any), and TypeScript’s null in the result object. A column you expect to be nullable should be read into one of these.

The raw Value path is still there

These ergonomic methods are additive — a thin, idiomatic layer over the lower-level path that speaks jed Values directly. Rust and TypeScript expose that path as dedicated methods: execute / query taking &[Value] in Rust and Value[] in TypeScript. Go needs no separate method — a raw jed.Value passes straight through the same variadic Query / Exec / QueryRow args (mixed freely with native Go values), and Rows.Value(i) reads a column back as its engine Value. Either way the raw path stays available for full fidelity: a rich type with no clean native counterpart in your language (a range, a jsonb, a composite) round-trips losslessly as a Value, where the ergonomic layer renders it to its canonical text. Reach for the raw Value when you need the engine value itself; reach for the native ergonomic form — the recommended default — for everything else.

Prepared statements

For a hot statement — a point lookup run thousands of times — jed offers an explicit prepared statement: the SQL is parsed once, and its resolved query plan is cached and reused across executes (planning dominates the latency of a trivial lookup, so this is the single biggest win for high-frequency queries). A prepared statement is a standalone value bound to no session: you run it by handing it to whichever handle should execute it — a Database, a durable Session, or an open Transaction — and that handle supplies the privileges, snapshot, and transaction state the run observes. Prepare once at startup, run it from every request:

  • Rust — db.prepare(sql) → session.query_prepared(&stmt, ¶ms) / session.execute_prepared(&stmt, ¶ms) (also on Database and Transaction).
  • Go — db.Prepare(sql) → session.QueryPrepared(ctx, stmt, args...) / ExecPrepared / QueryRowPrepared (also on Database and Transaction; the statement is safe to share across goroutines, each running it on its own session).
  • TypeScript — db.prepareStatement(sql) → session.queryPrepared(stmt, params) / executePrepared(stmt, params) (also on Database and Transaction). The better-sqlite3-style db.prepare(sql) Statement above uses the same machinery internally — its plan is cached too.

The cache is transparent and always correct: a schema change (CREATE/DROP/ALTER) invalidates it and the next execute re-plans; reusing a cached plan is by construction result- and cost-identical to planning fresh, so a prepared statement never changes what a query returns — only how fast it gets there.

jed — an embeddable, strictly-typed SQL database.