velt/db
import { readRows, RowSource, CellKind, RowError } from "velt/db". What database drivers share:
typed rows from any source of tabular data, and a statement's parameters handed to a native
driver. The SQLite and PostgreSQL packages
read their rows this way, so a typed query there (stmt.all<User>()) fills its Users by the
rules on this page. Use
the module directly when you write a driver, or read a tabular format of your own into typed
rows.
Typed rows from any source
A driver written in Velt (a database's wire protocol parsed in place, a file format) implements
RowSource, and readRows<T> fills one T per row: each column fills the field of its name, as
JSON.parse<T> fills fields from keys, with the same conversions (an integer into an f64
field, an exact float into an integer field, null into a T | null field) and the same messages
for a row that doesn't fit.
import { CellKind, readRows, RowSource } from "velt/db";
// Rows of `key=value` pairs, one row per line.
class Pairs implements RowSource {
lines: string[];
row: i64 = -1;
constructor(text: string) {
this.lines = text.split("\n");
}
columnCount(): i64 {
return 2;
}
columnName(i: i64): string {
return i == 0 ? "key" : "value";
}
next(): bool {
this.row++;
return this.row < (this.lines.length as i64);
}
kind(i: i64): CellKind {
return i == 0 ? CellKind.Text : CellKind.Int;
}
bool(i: i64): bool {
return false;
}
int(i: i64): i64 {
return parseInt(this.lines[this.row].split("=")[1], 10) as i64;
}
float(i: i64): f64 {
return 0;
}
text(i: i64): string {
return this.lines[this.row].split("=")[0];
}
blob(i: i64): u8[] {
return [];
}
json(i: i64): string {
return "null";
}
}
struct Pair {
key: string;
value: i64;
}
for (const p of readRows<Pair, Pairs>(new Pairs("a=1\nb=2"))) {
console.log(p.key, p.value); // a 1, then b 2
}interface RowSource:columnCount(): i64,columnName(i): string,next(): bool(to the next row, the first on the first call; false when there is none),kind(i): CellKind(what columniof the current row holds), and one getter per kind:bool(i),int(i): i64,float(i): f64,text(i): string,blob(i): u8[],json(i): string(a nested value as JSON text). The reader asks a value's kind before taking it with that kind's getter.enum CellKind { Null, Bool, Int, Float, Text, Blob, Json }.readRows<T, S extends RowSource>(src: S): T[]: every remaining row.readRow<T, S extends RowSource>(src: S): T | null: the next row, ornull.
Errors
readRows and readRow throw RowError { message } for a row that doesn't fit T
(expected i64 at $[0].id, expected field "name" at $[1]). Extra columns are ignored. An error
the source's methods throw is thrown again as a RowError with its message.
Performance
For a row type of plain fields (numbers, bools, strings, T | null of those, u8[]), the rows
are read column by column from the source itself, by code generated for the row type at compile
time: each column is matched to its field once, and no text is made. Any other row type (nested
objects, arrays, unions) is read through the JSON text of the rows, with the same result.
Native drivers
A driver in a package with native code hands its rows and
parameters across without JSON text in between, through two objects its Rust library owns: a row
cursor over its live result and a parameter sink for a statement (crate
velt_text::rows_cursor on the Rust side). The library registers each one with the runtime and
gives its Velt side a handle (a u64), never an address: std looks the handle up, so a number
that names no open cursor or sink never reaches memory. The decoder and the binder are generated
for each type at compile time.
decodeRows<T>(cursor: u64): T[]: every row of the cursor as aT, each column filling the field of its name with the same conversions and messages asreadRows. The cursor stays the driver's: give it back afterwards, so the driver can release the result and report a failure it met while stepping. ThrowsRowErrorfor a row that doesn't fit, and for a handle that names no open row cursor.decodeRow<T>(cursor: u64): T: the row of a cursor the driver opened for a single row.bindParams<P>(sink: u64, params: P): bool: handsparamsto the sink value by value: an array, a tuple or a lone value by position, an object's fields by name, each value as its JSON text would read back (an integral number an integer,NaNnull, au8[]field a blob). Returnsfalse, having done nothing, for a type it can't take that way (a nested object, a union): the driver then takesJSON.stringify(params), which binds the same values.falsetoo for a handle that names no open parameter sink.
import { bindParams, decodeRows } from "velt/db";
declare function velt_mydb__bind_begin(stmt: u64): u64;
declare function velt_mydb__query_bound(sink: u64): u64;
declare function velt_mydb__query_json(stmt: u64, params: string): u64;
declare function velt_mydb__close_rows(cursor: u64): void;
export function query<T, P>(stmt: u64, params: P): T[] {
const sink = velt_mydb__bind_begin(stmt);
const cursor = bindParams<P>(sink, params)
? velt_mydb__query_bound(sink)
: velt_mydb__query_json(stmt, JSON.stringify(params));
try {
return decodeRows<T>(cursor);
} finally {
velt_mydb__close_rows(cursor);
}
}packages/sqlite and packages/postgres in the Velt repository are complete drivers built this
way.