> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SqlDatabase

> The SQL Databases binding runtime API — prepare, bind, first, run, all, raw, batch, and exec on a bound database, with parameter conversion rules, result shapes, and error behavior.

`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](/docs/edge-compute/sqldb/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.

```ts theme={null}
interface SqlDatabase {
  prepare(query: string): SqlPreparedStatement;
  batch<T>(statements: SqlPreparedStatement[]): Promise<SqlQueryResult<T>[]>;
  exec(query: string): Promise<SqlExecResult>;
}

interface SqlPreparedStatement {
  bind(...values: unknown[]): SqlPreparedStatement;
  first<T>(column: string): Promise<T | null>;
  first<T>(): Promise<T | null>;
  run<T>(): Promise<SqlQueryResult<T>>;
  all<T>(): Promise<SqlQueryResult<T>>;
  raw<T>(options?: { columnNames?: boolean }): Promise<T[]>;
}

interface SqlQueryResult<T> {
  results: T[];
  success: boolean;
  meta: SqlQueryResultMeta;
}

interface SqlQueryResultMeta {
  duration: number;
  rows_read: number;
  rows_written: number;
  last_row_id: number;
  changes: number;
}

interface SqlExecResult {
  count: number;
  duration: number;
}
```

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](#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.

```ts theme={null}
// The id of a new row
const row = await env.DB
  .prepare("INSERT INTO users(email) VALUES (?) RETURNING id")
  .bind("alice@example.com")
  .first<{ id: number }>();
const newId = row?.id;

// Exactly which rows an UPDATE touched, not just how many
const { results } = await env.DB
  .prepare("UPDATE users SET active = 0 WHERE last_seen < ? RETURNING id")
  .bind(cutoff)
  .run<{ id: number }>();
const changed = results.length;
```

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:

```ts theme={null}
const [, counted] = await env.DB.batch<{ n: number }>([
  env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(Date.now()),
  env.DB.prepare("SELECT changes() AS n"),
]);
const deleted = counted.results[0].n;
```

## `prepare(query)`

Build a statement. Synchronous, and safe to call once and reuse.

```ts theme={null}
const stmt = env.DB.prepare("SELECT id, email FROM users WHERE org = ?");
const acme = await stmt.bind("acme").all<{ id: number; email: string }>();
const globex = await stmt.bind("globex").all<{ id: number; email: string }>();
```

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.

```ts theme={null}
const stmt = env.DB
  .prepare("INSERT INTO events(name, at, payload) VALUES (?, ?, ?)")
  .bind("signup", Date.now(), null);
```

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.

| JavaScript value                         | Bound as                                                                 | Read back as                                                                      |
| ---------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `number`                                 | `REAL` affinity — see below                                              | `number`                                                                          |
| `string`                                 | `TEXT`                                                                   | `string`                                                                          |
| `null`                                   | `NULL`                                                                   | `null`                                                                            |
| `undefined`                              | `NULL` (accepted; D1 rejects it)                                         | `null`                                                                            |
| `ArrayBuffer`                            | `BLOB`                                                                   | `ArrayBuffer` over the binding; a JSON array of byte values over REST and the CLI |
| `boolean`                                | **throws** `Provided value cannot be bound to SQLite parameter N`        | —                                                                                 |
| `Uint8Array` and other typed-array views | **throws** `Unknown named parameter` — pass the underlying `ArrayBuffer` | —                                                                                 |
| `bigint`                                 | **throws** `Do not know how to serialize a BigInt`                       | —                                                                                 |
| plain object                             | **throws** `Unknown named parameter`                                     | —                                                                                 |

Store a boolean as `0`/`1` and binary as an `ArrayBuffer`:

```ts theme={null}
const bytes = new Uint8Array([9, 8, 7]);
await env.DB
  .prepare("INSERT INTO flags(active, blob) VALUES (?, ?)")
  .bind(isActive ? 1 : 0, bytes.buffer) // .buffer, not the view
  .run();
```

`.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)`.

<Warning>
  **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](/docs/edge-compute/stateful-actors/guides/storage/sql), 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](/docs/cloud-storage/bindings) and bind the key.
</Warning>

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`.

<Warning>
  **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`.
</Warning>

## `first()` and `first(column)`

Return the first row, or a single column of it. No `meta`.

```ts theme={null}
// Whole row, or null when the query matched nothing
const user = await env.DB
  .prepare("SELECT id, email FROM users WHERE id = ?")
  .bind(1)
  .first<{ id: number; email: string }>();

// One column, as a bare value
const total = await env.DB.prepare("SELECT COUNT(*) AS n FROM users").first<number>("n");
```

`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.

```ts theme={null}
const { results, success } = await env.DB
  .prepare("SELECT id, email FROM users WHERE org = ?")
  .bind("acme")
  .all<{ id: number; email: string }>();

// results -> [{ id: 1, email: "alice@example.com" }, ...]
```

`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.

```ts theme={null}
const rows = await env.DB.prepare("SELECT id, email FROM users").raw();
// [[1, "alice@example.com"], [2, "bob@example.com"]]

const withHeader = await env.DB.prepare("SELECT id, email FROM users").raw({ columnNames: true });
// [["id", "email"], [1, "alice@example.com"], [2, "bob@example.com"]]
```

`{ 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.

```ts theme={null}
const [debited] = await env.DB.batch<{ cents: number }>([
  env.DB
    .prepare("UPDATE accounts SET cents = cents - ? WHERE id = ? RETURNING cents")
    .bind(500, "a"),
  env.DB.prepare("UPDATE accounts SET cents = cents + ? WHERE id = ?").bind(500, "b"),
]);
const remaining = debited.results[0]?.cents;
```

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.

```ts theme={null}
const { count } = await env.DB.exec(`
  CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, email TEXT UNIQUE);
  CREATE INDEX IF NOT EXISTS users_by_email ON users(email);
`);
// count -> 2
```

`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:

```text theme={null}
actor invocation <org>__telnyx_sqldb/<database-id>.sql returned 500: error invoke actor
method: rpc error: code = Internal desc = ... (500) {"error":"method_failed",
"message":"Error: UNIQUE constraint failed: users.email","name":"Error"}
```

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.

```ts theme={null}
try {
  await env.DB
    .prepare("INSERT INTO users(email) VALUES (?)")
    .bind(email)
    .run();
} catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  if (message.includes("UNIQUE constraint failed")) {
    return new Response("email already registered", { status: 409 });
  }
  throw err;
}
```

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](/docs/edge-compute/sqldb/limits).

## Related

* [Overview](/docs/edge-compute/sqldb/reference) — the SQL Databases Runtime API surface at a glance
* [Quick Start](/docs/edge-compute/sqldb/quick-start) — declare the binding and type it
* [How SQL Databases Work](/docs/edge-compute/sqldb/concepts/how-sqldb-works) — one primary per database, and where the data lives
* [Migrations](/docs/edge-compute/sqldb/migrations) — versioned schema changes from the CLI
* [Limits](/docs/edge-compute/sqldb/limits) — statement size, parameter, and duration ceilings
* [Stateful Actor SQL](/docs/edge-compute/stateful-actors/guides/storage/sql) — the per-actor embedded SQLite at `ctx.storage.sql`, private to one instance and synchronous
