{"record":{"id":"78d24b055924de89","repo":"tursodatabase/turso","slug":"batch-statement-index-failed-keyword-is-not","errorCode":null,"errorMessage":"batch statement ${index} failed: ${keyword} is not allowed in an atomic batch","messagePattern":"batch statement (.+?) failed: (.+?) is not allowed in an atomic batch","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"bindings/javascript/packages/common/promise.ts","lineNumber":808,"sourceCode":"    }\n    try {\n      const args = Array.isArray(statement.args)\n        ? statement.args.map(normalizeBatchBindValue)\n        : Object.fromEntries(\n          Object.entries(statement.args).map(([name, value]) => [name, normalizeBatchBindValue(value)]),\n        );\n      return { sql: statement.sql, args };\n    } catch (error) {\n      throw batchInputError(index, error);\n    }\n  });\n  if (wrap) {\n    for (let index = 0; index < normalizedStatements.length; index++) {\n      const statement = normalizedStatements[index];\n      const sql = typeof statement === \"string\" ? statement : statement.sql;\n      const keyword = firstSqlKeyword(sql);\n      if (keyword !== undefined && TRANSACTION_CONTROL_KEYWORDS.has(keyword)) {\n        throw batchInputError(index, new Error(`${keyword} is not allowed in an atomic batch`));\n      }\n    }\n  }\n  if (wrap) {\n    await runRawSql(`BEGIN ${normalizeBatchMode(mode!)}`);\n  }\n\n  const results: ResultSet[] = [];\n  const executeStatement = async (\n    statement: BatchStatement,\n  ): Promise<ResultSet> => {\n    const sql = typeof statement === \"string\" ? statement : statement.sql;\n    const args = typeof statement === \"string\" ? undefined : statement.args;\n\n    let nativeStmt: NativeStatement;\n    try {\n      nativeStmt = native.prepare(sql);\n    } catch (err) {","sourceCodeStart":790,"sourceCodeEnd":826,"githubUrl":"https://github.com/tursodatabase/turso/blob/c1e59287258d99b309e362a63f48822256e2f65f/bindings/javascript/packages/common/promise.ts#L790-L826","documentation":"In atomic batch mode the library itself wraps statements in BEGIN/COMMIT, so any user statement starting with a transaction-control keyword (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, etc.) is rejected up front with batchInputError(index, ...) naming the offending keyword. Nested or interleaved transaction control would break the atomicity guarantee the mode promises.","triggerScenarios":"Calling batch(statements, 'write'|'read'|<BatchMode>) where a statement's SQL starts with BEGIN/COMMIT/END/ROLLBACK/SAVEPOINT/RELEASE and the library is not already inside a transaction (so it adds its own wrap).","commonSituations":"Copy-pasting a SQL script (with BEGIN...COMMIT) into an atomic batch; dynamically generated SQL that includes transaction statements; switching an existing execute() script to batch() without removing transaction statements.","solutions":["Remove BEGIN/COMMIT/ROLLBACK/SAVEPOINT statements from the batch; the library handles the transaction when a mode is given.","If you need manual transaction control, call batch() without a mode (no wrap) or use execute() for the transaction statements separately.","Split the script so each batch contains only DML/DDL statements."],"exampleFix":"// before\nawait db.batch([\"BEGIN\", \"INSERT INTO t VALUES (1)\", \"COMMIT\"], \"write\");\n// after\nawait db.batch([\"INSERT INTO t VALUES (1)\"], \"write\"); // library wraps in BEGIN/COMMIT","handlingStrategy":"validation","validationCode":"const TXN_KEYWORDS = new Set([\"BEGIN\",\"COMMIT\",\"END\",\"ROLLBACK\",\"SAVEPOINT\",\"RELEASE\"]);\nconst assertNoTxnControl = (statements) => statements.forEach((s, i) => {\n  const sql = typeof s === \"string\" ? s : s.sql;\n  const kw = sql.trim().split(/\\s+/)[0]?.toUpperCase();\n  if (TXN_KEYWORDS.has(kw)) throw new Error(`statement ${i}: ${kw} not allowed in atomic batch`);\n});","typeGuard":"const isTransactionControl = (sql: string): boolean =>\n  TXN_KEYWORDS.has(sql.trim().split(/\\s+/)[0]?.toUpperCase() ?? \"\");","tryCatchPattern":"try {\n  await db.batch(statements, \"write\");\n} catch (e) {\n  if (/not allowed in an atomic batch/.test(e.message ?? \"\")) {\n    // strip txn statements and retry without a mode, or drop them\n    await db.batch(statements.filter(s => !isTransactionControl(typeof s === 'string' ? s : s.sql)), \"write\");\n  } else throw e;\n}","preventionTips":["Never include BEGIN/COMMIT/ROLLBACK in batch statements — the mode adds them","Use a mode-less batch() or execute() when you need manual transaction control","Lint SQL-building code for hardcoded transaction keywords","Filter transaction statements out of imported SQL scripts before batching"],"tags":["javascript","typescript","batch","transactions"],"backgroundTag":"transaction-control-in-batch","analyzedSha":"c1e59287258d99b309e362a63f48822256e2f65f","analyzedAt":"2026-08-31T11:17:35.598Z","contentChangedAt":"2026-08-31T11:17:35.598Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}