Skip to main content
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.
Every generic above has a default in the shipped types, so each method is usable without an explicit type argument — all() and run() hand back rows typed as plain records, and raw() hands back arrays of values. Key behaviors:
  • prepare is synchronous; every terminal is async. prepare builds a statement locally — nothing crosses the wire until first, run, all, raw, batch, or exec is awaited.
  • bind returns 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 for all(). Both send the same request and return the same SqlQueryResult, rows included — run() on a SELECT returns 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.
  • success is always true on a resolved promise. Failures reject; they are never reported through the flag.
  • Errors throw plain Error objects. There is no typed error class and no error-code taxonomy. See Errors.
  • Transaction control statements are rejected. SQL containing BEGIN, COMMIT, ROLLBACK, or SAVEPOINT fails when it runs — through a prepared statement or through exec() — with SqlTransactionControlError in the message. Transaction boundaries belong to the runtime; use batch() 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.
SQLite accepts 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.
A query string may hold several ;-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.
Placeholders are positional: anonymous ? 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 — what bind() 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).
One bound value is capped at 2 MiB (2,097,152 bytes). A larger ArrayBuffer or string is rejected with SqlParameterTooLargeError before the statement runs. This is a ceiling of the storage engine SQL Databases share with per-actor SQLite, and it applies only to values sent through .bind() — a value produced inside SQL, such as randomblob(), is not bound and is not checked. Split larger payloads, or keep them in object storage and bind the key.
A bound number arrives with REAL affinitySELECT 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.
Reading an integer larger than 2^53 − 1 throws RangeError: Value is too large to be represented as a JavaScript number. SQLite stores 64-bit integers, but result columns come back as JavaScript numbers — a value past Number.MAX_SAFE_INTEGER (9007199254740991) can be written and compared in SQL, but the read fails loudly rather than losing precision. Store large ids, snowflake keys, and nanosecond timestamps as TEXT.

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.
The whole batch commits together: if any statement fails, every earlier statement in the same batch is rolled back and the promise rejects. A batch whose middle statement violates a 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 plain Error. 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:
Match on the SQLite text as a substring. Do not parse the envelope — its shape is not part of the contract, and the surrounding transport detail changes.
Long-running statements fail differently: no SQLite message comes back at all. Statements running for tens of seconds complete, but past the caller’s own budget — a function invocation tops out around 60 seconds — the call was observed to hang rather than fail with anything identifying the statement. The database stays healthy afterwards. See Limits.
  • 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