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:

  • Rustrusqlite’s traits: run, query_row, query_map, with ToValue / FromValue doing the conversions and row.get::<T>(…) reading a typed column.
  • Godatabase/sql / pgx: Exec, Query, QueryRow taking ...any args, with Scan(&dest) and struct mapping.
  • TypeScriptbetter-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 nameget 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:

  • Rustdb.prepare(sql)session.query_prepared(&stmt, ¶ms) / session.execute_prepared(&stmt, ¶ms) (also on Database and Transaction).
  • Godb.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).
  • TypeScriptdb.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.