Guide

Database

db is built in: SQLite inside the server, or PostgreSQL, MySQL and MariaDB over the network, all with the same functions. Values go in as parameters, never as SQL text, and what goes wrong comes back as an error value, so ? and ?? handle a failed query as they handle anything else. This page goes through it in the order an app needs it; the details are also in Standard library.

db is a library for SQL databases, not syntax. Another kind of store (Redis, MongoDB, a graph database) comes in through its Rust crate: see Other stores.

Opening a database

db.open("app.db")                                   // SQLite, a file next to the programdb.open(":memory:")                                 // SQLite, in memorydb.open("postgres://user:pass@host:5432/app")       // PostgreSQLdb.open("mysql://user:pass@host:3306/app")          // MySQL or MariaDB (also mariadb://)db.open(env("DATABASE_URL"))                        // whatever the environment says

Call it once, at the top of the program. Without it, the database is $QUILLO_DB (a path or a URL), or else SQLite in memory, which is handy for trying things out and for tests.

When the database cannot be reached, db.open gives an error value, as every db function does, and db stays what it was: SQLite in memory, empty. At the top of a program nothing looks at that value, so pass it to unwrap, which stops the program with the reason instead of letting it start on the wrong database:

db.open(env("DATABASE_URL")) | unwrap

Queries

Three functions do the work:

CallGives
db.query(sql, ...values)every row, as a list of maps
db.one(sql, ...values)the first row as a map, or null
db.exec(sql, ...values){ changes, last_id } for statements that change the database
db.exec("create table if not exists todos (    id integer primary key,    text text not null,    done int not null default 0)")let r = db.exec("insert into todos (text) values (?)", "Write the docs")      // { changes: 1, last_id: 1 }let open = db.query("select id, text from todos where done = ? order by id", 0)let todo = db.one("select * from todos where id = :id", { id: r.last_id })let missing = db.one("select * from todos where id = ?", 999)               // null

Values are given after the SQL, one for each ?, or as a map for :name placeholders. PostgreSQL also takes its own $1, $2. Every :name needs a value, but keys the SQL does not use are ignored, so one map can serve a query put together from optional parts; given to SQL with no names it is no value, and given for a lone ? it is that value, stored as JSON. Placeholders given no values are an error. SQLite also reads @name, $name and #name, and ?1 as the first value; on MySQL @name is a variable of the session, so a map with that name is refused: write :name. Rows are maps from column names to values, so todo.text reads a column, and a list of rows goes straight into for in a page or out as JSON.

changes and last_id mean the same on every database. changes is the rows the statement matched: an update that sets a value a row already has counts that row (MySQL's own count, of rows changed, is ?client_found_rows=false in its URL), an upsert's or a replace's row counts once, and a read or a schema statement counts 0. last_id is the id of the one row an insert made, on SQLite and MySQL, and null after anything else: an insert of several rows, an upsert that updated, an update. PostgreSQL has no such id, so it is always null there: write insert ... returning id with db.one.

Five things that surprise people:

A query that fails (a syntax error, a constraint, the server unreachable) is an error value with the database's message:

fn add(text: str) -> int with db = db.exec("insert into todos (text) values (?)", text)?.last_idroute POST /todos { form: { text: str } } with db {    let id = add(text) ?? reply 422 "Could not add that"    redirect(`/todos/${id}`)}

db.error_kind(e) says what kind of failure it was, the same on every database: "unique", "foreign_key", "not_null", "check", "conflict" (another transaction), or null:

let r = db.exec("insert into seats (show, seat) values (?, ?)", show, seat)if db.error_kind(r) == "unique" { return err("Someone has just taken that seat") }

On PostgreSQL, the error's text ends in its SQLSTATE, as [23505], and db.error_kind reads that code first. So it gives the same word on a server whose messages are in another language.

sql`...` writes a query over several lines and tells editors to highlight it as SQL. It is a template string like any other, so it should hold no ${...} values: those go in as parameters.

fn recent(limit: int) -> list with db = db.query(sql`    select id, text    from todos    order by id desc    limit ?`, limit)

Safe SQL

Statements are prepared and values are sent apart from the SQL, so a value can never change what a query does. quillo check warns where SQL is put together from values (${x} or +): pass them as ? parameters instead.

Names cannot be parameters. When a table or a column comes from a choice (a column to sort by, say), put it in with sql_name, which takes only letters and digits of any script (with the marks written on a letter) and _, at most 63 bytes, so a name is the same name on every database, or a list of such names:

fn sorted(by: "text" | "id") -> list with db = db.query(`select id, text from todos order by ${sql_name(by)}`)

SQL built only from text as written, sql_name and sql_param, and choices between those, is not warned about, nor is a map over such pieces joined with join, which quillo check follows through every value its function returns. So an update of the columns the program chose passes:

fn save(id: int, data: map) with db {    let cols = ["title", "body"]                       // chosen here, never from the form    let set = cols | map((c) => `${sql_name(c)} = ${sql_param(c)}`) | join(", ")    db.exec(`update notes set ${set} where id = :id`, { ...data, id })}

while map((c) => `${c} = 1`) warns. Only the builtins count: a sql_name the program or the function defines for itself is followed as any other value.

sql_name quotes for the program's database: "title", or `title` on MySQL. For SQL sent to another database, quote with that connection's own: analytics.sql_name(col). MySQL reads "title" as text, so a name quoted for SQLite gives the word title in every row there.

Rows as records

A type names the shape of a row, and an annotation checks rows against it as they come back. A User(row) call makes one, keeping only its fields, which is the way to turn a row into what the rest of the program expects:

type Todo = { id: int, text: str }fn todos() -> list<Todo> with db = db.query("select id, text from todos order by id")

See Records for what a record checks and how it reports what is wrong.

Transactions

db.transaction(f) runs f and commits what it did, all or nothing:

fn transfer(from: int, to: int, amount: int) -> map with db =    db.transaction(() => {        db.exec("update accounts set balance = balance - ? where id = ?", amount, from)?        db.exec("update accounts set balance = balance + ? where id = ?", amount, to)?        { moved: amount }    })

Migrations

db.migrate() brings the schema up to date from SQL files in a folder, migrations unless another is given, applied in name order:

migrations/    001_todos.sql    002_tags.sql    003_todo_due_date.sql
db.open(env("DATABASE_URL"))db.migrate()

Live data

A page can follow the database. live(load) in a component runs a server fn again whenever a table it read changes, through Quillo, for anyone:

server fn load_todos() -> list = db.query("select id, text from todos order by id")server fn add_todo(text: str) -> map = db.exec("insert into todos (text) values (?)", text)component Todos() {    state todos = live(load_todos)      // every open page sees new todos    state draft = ""    <div>        <ul>{for t in todos => <li key={t.id}>{t.text}</li>}</ul>        <form onsubmit={() => { add_todo(draft); draft = "" }}>            <input value={draft} placeholder="Add a todo"/>        </form>    </div>}route GET / = <html><body><Todos/></body></html>

The form calls add_todo and does not touch todos itself: the insert announces the table, and live runs load_todos again, on this page and on every other page showing the list.

The tables a loader reads are found in its SQL (from, join), and every insert, update and delete through db announces the tables it writes. This works the same on SQLite, PostgreSQL and MySQL. Changes made by other programs (psql, a script) are not seen, nor what a trigger or a stored procedure writes, nor writes to the tables behind a view a loader reads: publish a topic for those.

A write wakes every loader that read the table, whatever rows it read: a reply in one thread runs load_posts again on every open thread page. To follow one thread only, read the table inside unwatched and watch a topic of your own, which the writer publishes:

server fn load_posts(thread: int) -> list {    watch(`thread:${thread}`)    unwatched(() => db.query("select * from posts where thread = ?", thread))}server fn reply_to(thread: int, text: str) {    db.exec("insert into posts (thread, text) values (?, ?)", thread, text)    publish(`thread:${thread}`)}

A topic is any text of 1 to 256 bytes, commas included; watch and publish stop on an empty or a longer one. One that ends in :* is a wildcard: publish("thread:*") wakes every page watching a thread: topic. So a topic built from what a visitor typed must not end in :*; one built from an int, as here, cannot.

What a loader's helpers read counts too: a me() that looks up sessions makes every loader calling it run again when anyone signs in, so call it as unwatched(() => me()). Keep what changes often but is not shown (view counts, last-seen times) in a table of its own. See Live data, and Several servers for apps on more than one server.

Several databases

db is the program's database; db.connect(target) opens another, of any kind, with the same functions and its own transactions, migrations and live updates:

db.open("app.db")let analytics = db.connect(env("ANALYTICS_URL"))analytics.migrate("migrations/analytics")let sales = analytics.query("select day, sum(total) as total from sales group by day")

Read replicas

A PostgreSQL or MySQL database's replicas can take its reads:

db.open(env("DATABASE_URL"), { replicas: [env("REPLICA_1"), env("REPLICA_2")] }) | unwrap

Shards

When one database is not enough, db.shards splits data over several by a key, such as a customer or an account:

let users = db.shards([env("SHARD_1"), env("SHARD_2"), env("SHARD_3")])users.migrate()                                            // each shardusers.on(id).exec("insert into users (id, name) values (?, ?)", id, name)users.on(id).one("select * from users where id = ?", id)  // the shard for the keylet total = users.all("select count(*) as n from users") | map((r) => r.n) | sum

Joining across databases

One SQL query cannot join tables in two databases. join_on puts their rows together in the program:

let orders = db.query("select * from orders where day = ?", today)let people = accounts.query("select * from users where id = any(?)", orders | map((o) => o.user_id))join_on(orders, people, "user_id", "id")    // each order with `user`: its user, or null    | filter((o) => o.user != null)          // only orders whose user was found

join_on(rows, others, key, other_key = key, as) gives each row its first match in others (the same value, of the same type) under as. Unless given, as is key without _id (user here), else joined.

One transaction over several databases

db.transaction is all or nothing on one database. db.atomic is all or nothing over several:

fn pay(from: int, n: int) -> int =    db.atomic([db, ledger], () => {        db.exec("update accounts set balance = balance - ? where id = ?", n, from)?        ledger.exec("insert into entries (account, amount) values (?, ?)", from, -n)?        n    })

Quillo's own tables

Quillo keeps a few tables of its own in the program's database: scheduled jobs (_quillo_jobs, each run once, whichever server it is due on), the decisions of db.atomic (_quillo_atomic) and the migrations applied (_quillo_migrations).

With a database per tenant there is no one program database to keep them in, so the first two go to a system database: db.system(target) (one every server of the program shares, when there are several), else $QUILLO_SYSTEM_DB, else a SQLite file quillo-system.db next to the program. db.system also keeps them out of the program's own tables when there is one database.

Calls made later (_quillo_later, see Microservices) stay with the work wherever the others go, in the program's database or the tenant's, so they commit or roll back with it. With a database per tenant, the system database only notes which tenants have some.

A database per tenant

One program can serve many tenants (customers, teams, sites), each with a database of its own:

tenant_from("example.com")          // acme.example.com is tenant "acme"db.open("data/{tenant}.db")         // a SQLite file per tenantdb.migrate()                        // run for each tenant's database when it is maderoute POST /signup { form: { name: str } } with db {    let made = new_tenant(name, () => db.exec("insert into settings (plan) values ('free')"))   // makes acme.db    if is_err(made) { reply 409 "That name is taken" }    redirect(`https://${name}.example.com/`)}
TargetOne per tenant
data/{tenant}.dbSQLite file
postgres://host/app?schema=t_{tenant}PostgreSQL schema, in one database
postgres://host/app_{tenant}PostgreSQL database, made by you beforehand
mysql://host/app_{tenant}MySQL or MariaDB database
a function of the tenant's namewhatever it gives, or null for no such tenant

Many tenants has the rest: tenant names, schema prefixes, and how many databases stay open at once.

Engines

SQLite

Built in, nothing to install: the database runs inside the server process, in a file next to the program or in memory, with foreign keys on and WAL mode. It is SQLite 3.46, with JSON and FTS5 full-text search, and without math functions (sin, radians). Booleans are stored as 0 and 1, and a column declared boolean reads them back as true and false. Queries run on the server's own thread: a long one holds up the other requests of its instance. Several processes may share the file: readers do not wait, writers take turns, for up to 5 seconds before a write fails as a conflict.

PostgreSQL

MySQL and MariaDB

Reference

FunctionDoes
db.open(target)Opens the program's database: a path, :memory:, a URL, a template with {tenant}, or a function of the tenant
db.connect(target)Opens another database, with these same functions
db.query(sql, ...values)Every row, as a list of maps
db.one(sql, ...values)The first row, or null
db.exec(sql, ...values){ changes, last_id }
db.error_kind(e)"unique", "foreign_key", "not_null", "check", "conflict" or null
db.transaction(f)Runs f; commits, or rolls back on failure, error value or early reply
db.migrate(folder = "migrations")Applies the SQL files not yet applied, in name order
db.open(url, { replicas })Sends reads to replicas (also db.connect)
db.shards([target, ...])Data split by key: .on(key), .all(sql, ...), .migrate(), .count
db.atomic([db, ...], f)Runs f as one transaction over several databases
db.system(target)Where Quillo keeps its own tables
db.tenants()The tenants that have a database
db.tenant_exists(name)Whether a tenant has a database, without making one
sql_name(name)A table or column name, safe to put into SQL
conn.sql_name(name), db.sql_name(name)The same, quoted for that database
sql_param(name)A :name placeholder, safe to put into SQL
join_on(rows, others, key, other_key, as)Each row with its first match from another list of rows

A function declared with effects may use the database only if db is among them: one declared with http cannot query, on db or on a connection, and quillo check says so before the program runs. Functions without with may do anything.

What it does not do

The reference has the rest: parameters and their traps, type conversions in full, pools and connections, tenants' schemas.

Thread
29Useful? Like it, or add to its thread below.

Thread 0 posts

On Database: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.