db.sql("CREATE TABLE IF NOT EXISTS todos (id INT PRIMARY KEY AUTO_INCREMENT, title TEXT, done BOOLEAN)");
const r = db.sql("INSERT INTO todos (title, done) VALUES (?, ?)", ["write docs", false]);
r.changes; // 1
r.lastInsertId; // 1 (a BigInt past 2^53)
db.sql("SELECT * FROM todos WHERE done = ?", [false]); // [{ id: 1, title: "write docs", done: 0 }]
db.sql(text, params?) answers rows for a SELECT and { changes, lastInsertId }
for anything else.
Parameters and types
Parameters are an array, one per ?. A parameter is null, undefined
(NULL), a boolean, a number, a BigInt, a string, a Uint8Array or an
ArrayBuffer. Anything else throws a TypeError naming the parameter.
- An integral number inside int64 binds as an integer; any other number (a fraction, NaN, infinity, -0) as a double.
- A BigInt binds as int64, or as unsigned past INT64_MAX up to UINT64_MAX; outside that range it throws.
- Binary values bind as BLOB.
- One parameter is at most 16 MiB; a larger one throws an error naming the limit.
Result columns: integers past 2^53 come back as BigInt; DECIMAL, DATE, TIME and
DATETIME as their SQL text; BLOB as Uint8Array. A TEXT or BLOB column holds
65535 bytes; a longer value is refused (error 1406, Data too long), never cut: declare MEDIUMTEXT or LONGTEXT for more.
Prepared statements
const byDone = db.prepare("SELECT id, title FROM todos WHERE done = ?");
byDone.all(false); // every row
byDone.first(false); // the first row or null
Transactions
Transaction text (BEGIN, COMMIT, ROLLBACK, SAVEPOINT) is refused, as every statement the
engine does not run is: an Error named SQL_FAILED whose message says why. Use db.transaction.
const moved = db.transaction((tx) => {
tx.sql("UPDATE accounts SET balance = balance - ? WHERE id = ?", [10, 1]);
tx.sql("UPDATE accounts SET balance = balance + ? WHERE id = ?", [10, 2]);
return tx.sql("SELECT balance FROM accounts WHERE id = ?", [1])[0].balance;
});
db.transaction(fn) runs fn(tx) as one SQL transaction and answers fn's
return value. Rules:
fnis synchronous. A returned promise rolls back and throws a TypeError.- A throw rolls back and rethrows the same error.
- The app's write lock is held until
fnreturns: other writers wait, so keep it short. A call killed at its CPU limit insidefnis rolled back. - Collection writes join the transaction:
tx.<collection>isdb.<collection>, and an insert, update, upsert, delete,insertMany,updateWhere, a chain'supdate/delete, a row'ssave()and the rest commit with it and are taken back by a rollback (a throw, a returned promise, a nested transaction's own rollback, a failed commit), together with thetx.sqlwrites. Reads through collections and chains, andtx.sqlover a collection, see the transaction's own writes. A collection's change events are published when the outermost transaction commits, in the order of the writes, and its oplog entries are written then (one per collection); a rolled-back transaction publishes and records nothing. The transaction holds the write lock, but another thread's collection write is not held off by it yet. - Refused inside the callback, with an error naming the problem: kv and graph
writes ("kv and graph writes are not transactional yet: use tx.sql or
collection writes, or write outside db.transaction"), a
db.transactionfrom script that runs inside a collection upsert, and the services that write outside the transaction or run another function (substrio.apiwrites,substrio.functions.<name>calls,substrio.webhooks.create/remove/emit,substrio.plans.set,substrio.usage.consume,substrio.lua.eval,substrio.dbwrites).substrio.requestis allowed for GET and HEAD only. - A nested
db.transactionis a savepoint: it rolls back alone.
db.transaction((tx) => {
tx.sql("INSERT INTO todos (title) VALUES (?)", ["outer"]);
try {
db.transaction((inner) => {
inner.sql("INSERT INTO todos (title) VALUES (?)", ["inner"]);
throw new Error("undo only the inner insert");
});
} catch (e) { /* the outer insert is kept */ }
});
SQL over documents
A document collection is also a SQL table of the same name. SQL reads it and never writes it: write documents through the collection (collections-and-chains.md).
db.sql("SELECT name FROM people WHERE age < ?", [30]); // [{ name: "bob" }]
db.sql("SELECT p.name, r.rank FROM people p JOIN ranks r ON r.who = p.name"); // joins a SQL table
db.people.query("SELECT name WHERE age > ?", 30); // no FROM: reads the collection
A field reads as a typed column, a missing field is NULL and a nested value is
JSON text. INSERT, REPLACE, UPDATE, DELETE, DROP TABLE, ALTER TABLE
and CREATE TABLE of a collection's name are refused, the documents stay as
they were, and the error reads:
people is a document collection: SQL reads it; write documents with db.people.insert/update/delete
db.<collection>.query(sql, ...args) and .prepare(sql) are described in
collections-and-chains.md, "SQL over a collection".
A collection is a SQL table of its documents' fields, and its text compares
byte for byte in SQL, as it does everywhere else on documents: equality, IN,
GROUP BY, DISTINCT, ORDER BY and LIKE keep case apart (Paid, paid
and PAID are three values), ignore no trailing spaces ('paid ' is not
'paid') and order by bytes. lower(col) gives a folded comparison:
WHERE lower(status) = 'paid' (an expression compares under the default
collation, so it also ignores trailing spaces). A text equality or IN on a collection field
that holds only text is answered from the collection's own value keys, with no
scan. A chain query (eq, in, startsWith) gives the same answer through
SQL as through the chain.
D1 shim
env.<D1> has prepare(sql).bind(...).first/all/run/raw, batch and exec,
all returning promises. batch runs the statements one after another, not as
a transaction: use db.transaction when the writes must be atomic.