env.<BINDING> (a SqlDatabase) is the in-function handle to a SQL database. The runtime routes the call for you, so function code holds no API key. Every database is served by one primary that serializes writes, and the binding, the CLI, and the REST query endpoint all funnel through that same primary — a row written from a function is immediately readable from the CLI, and the reverse.
all() and run() hand back rows typed as plain records, and raw()
hands back arrays of values.
Key behaviors:
prepareis synchronous; every terminal is async.preparebuilds a statement locally — nothing crosses the wire untilfirst,run,all,raw,batch, orexecis awaited.bindreturns a new statement. It never mutates the statement it was called on, so a prepared statement can be reused with different values.run()is an alias forall(). Both send the same request and return the sameSqlQueryResult, rows included —run()on aSELECTreturns rows. This matches Cloudflare D1.batch()is atomic. All statements commit together or none do.exec()returns no rows — only a statement count. Use it for schema scripts.successis alwaystrueon a resolved promise. Failures reject; they are never reported through the flag.- Errors throw plain
Errorobjects. There is no typed error class and no error-code taxonomy. See Errors. - Transaction control statements are rejected. SQL containing
BEGIN,COMMIT,ROLLBACK, orSAVEPOINTfails when it runs — through a prepared statement or throughexec()— withSqlTransactionControlErrorin the message. Transaction boundaries belong to the runtime; usebatch()for all-or-nothing writes.
Reading What a Write Touched
meta reports the statement’s own accounting — last_row_id, changes, rows_read, rows_written, and duration.
RETURNING is the more precise option, and worth preferring for anything you branch on. It names the rows the statement actually touched, so it survives concurrency: every caller of a database shares one connection, and meta describes the statement that most recently ran on it.
RETURNING on INSERT, UPDATE, and DELETE, and the rows it hands back are exactly the rows the statement touched. SELECT changes() reports the count instead, but it reads whichever statement most recently ran on the shared connection, so a concurrent write can land in between. When a bare count is all you need, issue it in the same batch() as the write so nothing can interleave:
prepare(query)
Build a statement. Synchronous, and safe to call once and reuse.
;-separated statements. All of them execute in order, and the rows that come back are the final statement’s — the same rule Cloudflare’s Durable Object SQL storage follows. Bound parameters apply to that final statement only: place placeholders nowhere else, because the earlier statements run without bindings and a ? in one of them fails.
bind(...values)
Attach values to the placeholders in the query and get back a new statement. Always bind user input — never build SQL by string concatenation.
? and ordered ?NNN (1-indexed) both work, and values are consumed in argument order.
Type Conversion
How values cross between JavaScript and SQLite, in both directions — whatbind() accepts on the way in, and what the terminal methods hand back on the way out.
Four JavaScript types reach SQLite — null, number, string, and ArrayBuffer — plus undefined, which is accepted and stored as NULL. Every other value throws, and it throws when the statement runs rather than at the bind() call, because bind() performs no validation.
Store a boolean as
0/1 and binary as an ArrayBuffer:
.buffer is the whole backing store, not the view. For a view that does not span its buffer,
slice first: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength).
A bound number arrives with REAL affinity — SELECT typeof(?) on a bound 42 returns real. Inserted into a column declared INTEGER, it still lands as an integer (typeof(col) reads back integer); the affinity only shows in untyped or expression contexts. In a STRICT table the check is exact, so binding 3.5 into an INTEGER column correctly fails with cannot store REAL value in INTEGER column.
first() and first(column)
Return the first row, or a single column of it. No meta.
first() resolves to null when there are no rows. first(column) returns the value at that key; if the column is not in the result set it also resolves to null rather than throwing — a divergence from D1, which raises D1_COLUMN_NOTFOUND.
run() and all()
Run the statement and return the full envelope. The two methods are identical.
results holds the rows as objects keyed by column name. For an INSERT, UPDATE, or DELETE it is an empty array unless the statement carries a RETURNING clause. The generic is a compile-time assertion only — it is not validated against the columns the query actually returns.
raw(options?)
Return rows as arrays of values instead of objects, for callers that already know the column order or that feed a columnar consumer. The rows are converted after they arrive, so this is a shape convenience, not a smaller response.
{ columnNames: true } prepends one row of column names. An empty result set returns [] with no header row, so check the length before treating the first entry as headers. Rows are converted from the same objects all() returns, so two result columns with the same name collapse into one — alias them (SELECT a.id AS a_id, b.id AS b_id) when joining.
batch(statements)
Run several prepared statements as one atomic unit. Results come back in the order the statements were passed.
UNIQUE constraint leaves the table exactly as it was.
Every statement must come from the same database’s prepare(). Passing a statement built by a different binding throws env.<DB>.batch() accepts only statements created by this database's prepare().
batch() is the transaction primitive for parameterized, all-or-nothing writes: explicit BEGIN is rejected, so a transaction cannot be opened by hand.
exec(query)
Run a raw SQL script. Takes no bound parameters and returns no rows.
count is the number of statements in the script. Passing bind arguments throws env.<DB>.exec() takes no bound params — use prepare().bind() for parameterized statements. Use exec for schema and one-shot maintenance work, and prepare().bind() for anything that takes user input.
A script is atomic: if any statement in it fails, every statement in that script rolls back. A script that creates a table, inserts a row, and then hits an error leaves no table behind. So exec() either applies a whole script or changes nothing.
Errors
Every failure — syntax error, missing table, constraint violation, bad bind — rejects with a plainError. There is no typed error class, no cause chain, and no error-code taxonomy equivalent to D1’s D1_ERROR / D1_TYPE_ERROR / D1_COLUMN_NOTFOUND.
error.message is a long transport string with the SQLite message embedded near the end:
Related
- Overview — the SQL Databases Runtime API surface at a glance
- Quick Start — declare the binding and type it
- How SQL Databases Work — one primary per database, and where the data lives
- Migrations — versioned schema changes from the CLI
- Limits — statement size, parameter, and duration ceilings
- Stateful Actor SQL — the per-actor embedded SQLite at
ctx.storage.sql, private to one instance and synchronous