Skip to content

Surqlize for TypeScript

Surqlize transactions and batches

Surqlize has two ways to run several queries as a unit. A batch sends all queries in one request and applies them atomically, but gives you no results until it finishes. A transaction stays open on the server, so you can read the result of each query and decide what to do next before you commit or cancel.

BatchTransaction
Round tripsOneOne to begin, one for each query and one to commit
Intermediate resultsNoYes
Can be retried as a unitYesNo

The examples on this page use the tables below, with db created as described in connections.

Example schema
import { edge, orm, t, table } from "surqlize";

const user = table("user", {
    name: t.object({ first: t.string(), last: t.string() }),
    email: t.string(),
    age: t.number(),
    role: t.union([t.literal("admin"), t.literal("moderator"), t.literal("member")]).default("member"),
});

const post = table("post", {
    title: t.string(),
    content: t.string(),
    author: t.record("user"),
});

const authored = edge("user", "authored", "post", {
    created: t.date(),
});

const db = orm(surreal, user, post, authored);

db.batch() takes any number of queries and resolves to a tuple of their results, typed in order. Either every query takes effect or none does.

Run queries in a batch
const [created, updated, users] = await db.batch(
    db.create("user").set({
        name: { first: "Alice", last: "Smith" },
        email: "alice@example.com",
        age: 30,
    }),
    db.update("user", "bob").set({ age: 31 }),
    db.select("user"),
);
// each result is typed according to its own query

A batch is sent as one request using the SDK's stateless transaction(), so it works over HTTP as well as WebSockets. When it fails, the promise rejects with the error that made it fail, and none of its statements take effect.

Because a batch is a single request, it is safe to replay. Use .retry() to send it again after a conflict with a concurrent transaction. A batch also takes .signal() and .requestTimeout(). Aborting a batch or reaching its timeout stops the wait only. The server may still have committed the batch. See cancellation, timeouts and retries.

Retry a batch
const [saved] = await db
    .batch(
        db.create("user").set({
            name: { first: "Alice", last: "Smith" },
            email: "alice@example.com",
            age: 30,
        }),
    )
    .retry()
    .requestTimeout(5_000);

To see the SurrealQL of a batch before you run it, call toString():

Inspect a batch
const batch = db.batch(
    db.update("user", "bob").set({ age: 31 }),
    db.delete("user", "carol"),
);

console.log(batch.toString());
Output
BEGIN TRANSACTION; UPDATE $_v0 SET age = $_v1; DELETE $_v2; COMMIT TRANSACTION;

db.transaction() opens a server-side transaction through the SDK's beginTransaction(). It needs a connection that supports open transactions, such as a WebSocket connection. Over HTTP it rejects with the SDK's UnsupportedFeatureError, so use a batch there. The SDK documents the limits of open transactions in transactions.

With a callback, the transaction commits when the callback returns and cancels when it throws. The result of the callback is the result of db.transaction().

Run a transaction with a callback
const result = await db.transaction(async (tx) => {
    const alice = await tx.create("user").only().set({
        name: { first: "Alice", last: "Smith" },
        email: "alice@example.com",
        age: 30,
    });

    // Use the result to decide what to do next
    if (alice.age > 25) {
        await tx.update(alice.id).set({ role: "moderator" });
    }

    return alice;
});

Without a callback, db.transaction() returns the transaction, and you commit or cancel it yourself.

Commit or cancel a transaction
import { RecordId } from "surrealdb";

const tx = await db.transaction();

try {
    const alice = await tx.create("user").only().set({
        name: { first: "Alice", last: "Smith" },
        email: "alice@example.com",
        age: 30,
    });
    await tx
        .relate("authored", alice.id, new RecordId("post", "hello"))
        .set({ created: new Date() });
    await tx.commit();
} catch (error) {
    await tx.cancel();
    throw error;
}

The transaction object has the query builders of db: select, create, insert, update, upsert, delete, relate and run. A batch is already atomic, and a transaction cannot be nested. So tx.batch() throws an OrmError. Run the queries on tx instead.

Note

Do not retry a query inside a transaction. A conflict has already failed the transaction. To retry statements together, use a batch, which is atomic and can be retried as a unit.

tx.withSignal(signal) returns a handle on the same transaction in which every query is abandoned when the signal aborts. commit() and cancel() work on either handle. They are not tied to the signal, so an aborted request cannot leave the outcome of a commit uncertain.

A transaction keeps the validation setting of the ORM that opened it. tx.validated() returns a handle on the same transaction with validation switched on or off. See runtime validation.

Was this page helpful?