Cloudflare D1
SQLite driver for Cloudflare D1 using Worker bindings
Requirements
- Cloudflare Workers environment (no
nodejs_compatflag needed)
Configuration
import { createClient } from "viborm/d1";
export default {
async fetch(request: Request, env: Env) {
const client = createClient({
database: env.DB, // D1 binding from wrangler.toml
schema,
});
const users = await client.user.findMany();
return Response.json(users);
},
};
Options
| Option | Type | Description |
|---|---|---|
database |
D1Database |
D1 database binding from Worker env |
Binary values
Blob fields bind Uint8Array values directly to D1, and D1’s byte-array
results are normalized back to a plain Uint8Array — no Node Buffer or
nodejs_compat flag involved.
wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Transactions and batching
D1 does not support an interactive callback transaction. VibORM uses the
binding’s native batch() primitive for work that can be planned up front.
Atomic array transactions
The array API is one atomic D1 batch:
const [user, post] = await client.$transaction([
client.user.create({ data: { name: "Alice", email: "alice@example.com" } }),
client.post.create({ data: { title: "Hello", authorId: "preset-id" } }),
]);
Every operation in this array must be statically batchable. A dynamic record
series is refused before the batch is submitted because $transaction([...])
promises one atomic commit.
Interactive transactions
Dynamic transactions (callback API) are unsupported and reject:
// Throws TransactionError: D1 does not support callback transactions
await client.$transaction(async (tx) => {
const user = await tx.user.create({ data: { name: "Alice" } });
await tx.post.create({ data: { title: "Hello", authorId: user.id } });
});
Relation-bearing bulk writes
Root createMany and updateMany calls whose records write relations need
left-to-right decisions. Nested relation-bearing createMany and updateMany
can need the same pause at their exact position in a record tree. Outside
$transaction([...]), D1 executes supported series as ordered committed
segments:
- one submitted segment batch is atomic;
- a later member observes earlier committed members;
- successful results match transaction-capable drivers;
- if a later segment fails, earlier segments stay committed;
- the error reports the exact durable prefix;
- VibORM never retries an already committed prefix.
import { isVibORMError } from "viborm";
try {
await client.post.createMany({
data: [
{ id: "p1", author: { create: { id: "a1", name: "first" } } },
{ id: "p2", author: { create: { id: "a2", name: "occupied" } } },
],
});
} catch (error) {
if (isVibORMError(error)) {
console.log(error.toJSON().meta.recordSeriesProgress);
// {
// atomicity: "segment",
// phase: "member",
// committedSegments: 1,
// completedMembers: 1,
// committedWriteMembers: 1,
// memberPath: [1],
// totalMembers: 2
// }
}
}
completedMembers is the completed input prefix.
committedWriteMembers counts members whose user-table writes are durable.
committedSegments counts committed D1 batches. The phase says whether the
failure happened during capture, planning, an enclosing prefix, a member, an
enclosing suffix, the final result read, or cache invalidation.
This progressive route applies to root relation-bearing createMany and
updateMany. A nested relation-bearing createMany or updateMany also runs
at its exact tree position when VibORM can re-assert the complete parent row or
relation membership inside every later write batch.
For one inserted row with a database-generated integer key, the D1 driver
normalizes the binding’s official meta.last_row_id as that statement’s
generated identity. A later segment can therefore use the concrete key. VibORM
never derives a range of generated keys from it and never assumes adjacent
values for a multi-row insert.
The following shapes remain unavailable:
- a relation-bearing
createManymember with a write or nested series before its skippable root, because a root conflict would leave that earlier effect committed. A root-first member is supported: VibORM isolates the root batch and dispatches no descendants when it skips; - a nested series whose compiler cannot provide an exact complete-parent and membership guard. It is refused before the containing member writes. Earlier progressive root members can already be committed and are reported;
- any dynamic record series inside explicit
$transaction([...]).
Scalar bulk operations and other statically planned nested writes continue to
use one atomic D1 batch. Inside that batch, a splittable createMany statement
is divided into the largest chunks that fit D1’s conservative 100-bound-value
budget. The same applies to a many-to-many connect/set junction insert over
an already captured target-key list. The complete operation still succeeds or
rolls back as one D1 batch.
Migrations
Migration V1 permits offline generation and read-only inspection on D1, but
refuses every effectful push, apply, down, reset, and verify command
before SQL. D1’s native batch alone does not prove table recreation, foreign-key
handling, migration locking, and marker compare-and-swap together. Apply live
schema changes with wrangler d1 migrations instead.
Capabilities
Interactive transactions reject. Static work uses one native atomic batch. Root dynamic relation-bearing bulk uses ordered segment-atomic batches with reported progress. See the feature matrix for the full comparison.
Limitations
- Operations inside
$transaction([...])cannot read sibling results. - Nested dynamic writes require an exact compiler-provided parent/membership guard; shapes that cannot provide one reject.
- Only available in Cloudflare Workers
- SQLite dialect - no
LATERALjoins, limitedFULL OUTER JOIN - JSON columns are parsed automatically
- Boolean values stored as integers (0/1)