tursodatabase/turso · error · DatabaseError

batch statement ${index} failed: ${keyword} is not allowed i

Error message

batch statement ${index} failed: ${keyword} is not allowed in an atomic batch

What it means

session.batch() with an explicit BatchMode runs atomically: the library emits its own BEGIN <mode> ... COMMIT ... ROLLBACK steps. Statements starting with a transaction-control keyword (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE, END) would corrupt that structure, so they are rejected before the request is sent, reported as batchInputError(index, ...) naming the keyword.

Source

Thrown at serverless/javascript/src/session.ts:539

        throw batchInputError(index, e?.message ?? String(e));
      }
      return {
        stmt: {
          sql: statement.sql,
          args: encodedArgs.args,
          named_args: encodedArgs.namedArgs,
          want_rows: true,
        },
      };
    });

    if (mode !== undefined) {
      for (let index = 0; index < statements.length; index++) {
        const statement = statements[index];
        const sql = typeof statement === 'string' ? statement : statement.sql;
        const keyword = firstSqlKeyword(sql);
        if (keyword !== undefined && TRANSACTION_CONTROL_KEYWORDS.has(keyword)) {
          throw batchInputError(index, `${keyword} is not allowed in an atomic batch`);
        }
      }
    }

    let steps: BatchStep[];
    let firstUserStepIdx = 0;
    let beginIdx = -1;
    let commitIdx = -1;
    let rollbackIdx = -1;
    if (mode === undefined) {
      // Each statement is gated on its predecessor succeeding, so
      // execution stops at the first failure (matching the Rust and
      // Python drivers and the `sequence` request).
      steps = userSteps.map((step, i) =>
        i === 0 ? step : { ...step, condition: { type: 'ok' as const, step: i - 1 } },
      );
    } else {
      // Atomic batch: BEGIN <mode>, then each user step gated on its

View on GitHub (pinned to c1e5928725)

Solutions

  1. Strip transaction-control statements; pass the batch mode and let the library manage BEGIN/COMMIT/ROLLBACK.
  2. If manual control is required, omit the mode argument so no atomic wrap is added, or send those statements via execute().
  3. Pre-process SQL scripts to split on statement and filter out keywords before batching.

Example fix

// before
await session.batch(["BEGIN", "UPDATE t SET x=1", "COMMIT"], "write");
// after
await session.batch(["UPDATE t SET x=1"], "write");
Defensive patterns

Strategy: validation

Validate before calling

const TXN = new Set(["BEGIN","COMMIT","END","ROLLBACK","SAVEPOINT","RELEASE"]);
const rejectTxnInBatch = (statements) => statements.forEach((s, i) => {
  const sql = typeof s === 'string' ? s : s.sql;
  const kw = sql.trim().split(/\s+/)[0]?.toUpperCase();
  if (TXN.has(kw)) throw new Error(`statement ${i}: ${kw} not allowed in atomic batch`);
});

Type guard

const isTxnControl = (sql: string): boolean =>
  TXN.has(sql.trim().split(/\s+/)[0]?.toUpperCase() ?? "");

Try / catch

try {
  await session.batch(statements, mode);
} catch (e) {
  if (/not allowed in an atomic batch/.test(e?.message ?? "")) {
    const filtered = statements.filter(s => !isTxnControl(typeof s === 'string' ? s : s.sql));
    return session.batch(filtered, mode);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling session.batch(statements, mode) where any statement string (or statement.sql) begins with a TRANSACTION_CONTROL_KEYWORDS entry — e.g. a SQL dump containing BEGIN;...COMMIT; passed as batch statements with mode 'write'.

Common situations: Importing .sql dump files via batch(); converting imperative execute('BEGIN') code to batch with a mode; template-generated SQL that includes transaction frames; applying a mode parameter where previously none was passed.

Related errors


AI-assisted analysis of tursodatabase/turso@c1e5928725 (2026-08-31). Data as JSON: /api/errors/44503c956087fec8. Report an issue: GitHub.