@tursodatabase/serverless 1.1.3 → 1.2.0-pre.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -237,6 +237,67 @@ async function executePipeline(url, authToken, request, remoteEncryptionKey, sig
237
237
  return response.json();
238
238
  }
239
239
 
240
+ // src/args.ts
241
+ function normalizeArgs(args) {
242
+ if (args === void 0) return [];
243
+ if (Array.isArray(args)) return args;
244
+ if (args !== null && typeof args === "object" && args.constructor === Object) {
245
+ return args;
246
+ }
247
+ return [args];
248
+ }
249
+ function isQueryOptions(value) {
250
+ return value != null && typeof value === "object" && !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "queryTimeout");
251
+ }
252
+ function splitBindParameters(bindParameters) {
253
+ if (bindParameters.length === 0) {
254
+ return { params: void 0, queryOptions: void 0 };
255
+ }
256
+ if (isQueryOptions(bindParameters[bindParameters.length - 1])) {
257
+ if (bindParameters.length === 1) {
258
+ return { params: void 0, queryOptions: bindParameters[0] };
259
+ }
260
+ return {
261
+ params: bindParameters.length === 2 ? bindParameters[0] : bindParameters.slice(0, -1),
262
+ queryOptions: bindParameters[bindParameters.length - 1]
263
+ };
264
+ }
265
+ return {
266
+ params: bindParameters.length === 1 ? bindParameters[0] : bindParameters,
267
+ queryOptions: void 0
268
+ };
269
+ }
270
+ function encodeSqlArgs(args = []) {
271
+ let positionalArgs = [];
272
+ let namedArgs = [];
273
+ if (Array.isArray(args)) {
274
+ positionalArgs = args.map(encodeValue);
275
+ } else {
276
+ const keys = Object.keys(args);
277
+ const isNumericKeys = keys.length > 0 && keys.every((key) => /^\d+$/.test(key));
278
+ if (isNumericKeys) {
279
+ const sortedKeys = keys.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
280
+ const maxIndex = parseInt(sortedKeys[sortedKeys.length - 1], 10);
281
+ positionalArgs = new Array(maxIndex);
282
+ for (const key of sortedKeys) {
283
+ const index = parseInt(key, 10) - 1;
284
+ positionalArgs[index] = encodeValue(args[key]);
285
+ }
286
+ for (let i = 0; i < positionalArgs.length; i++) {
287
+ if (positionalArgs[i] === void 0) {
288
+ positionalArgs[i] = { type: "null" };
289
+ }
290
+ }
291
+ } else {
292
+ namedArgs = Object.entries(args).map(([name, value]) => ({
293
+ name,
294
+ value: encodeValue(value)
295
+ }));
296
+ }
297
+ }
298
+ return { args: positionalArgs, namedArgs };
299
+ }
300
+
240
301
  // src/session.ts
241
302
  function normalizeUrl(url) {
242
303
  return url.replace(/^libsql:\/\//, "https://");
@@ -314,41 +375,15 @@ var Session = class {
314
375
  * @returns Promise resolving to the raw response and cursor entries
315
376
  */
316
377
  async executeRaw(sql, args = [], queryOptions) {
317
- let positionalArgs = [];
318
- let namedArgs = [];
319
- if (Array.isArray(args)) {
320
- positionalArgs = args.map(encodeValue);
321
- } else {
322
- const keys = Object.keys(args);
323
- const isNumericKeys = keys.length > 0 && keys.every((key) => /^\d+$/.test(key));
324
- if (isNumericKeys) {
325
- const sortedKeys = keys.sort((a, b) => parseInt(a) - parseInt(b));
326
- const maxIndex = parseInt(sortedKeys[sortedKeys.length - 1]);
327
- positionalArgs = new Array(maxIndex);
328
- for (const key of sortedKeys) {
329
- const index = parseInt(key) - 1;
330
- positionalArgs[index] = encodeValue(args[key]);
331
- }
332
- for (let i = 0; i < positionalArgs.length; i++) {
333
- if (positionalArgs[i] === void 0) {
334
- positionalArgs[i] = { type: "null" };
335
- }
336
- }
337
- } else {
338
- namedArgs = Object.entries(args).map(([name, value]) => ({
339
- name,
340
- value: encodeValue(value)
341
- }));
342
- }
343
- }
378
+ const encodedArgs = encodeSqlArgs(args);
344
379
  const request = {
345
380
  baton: this.baton,
346
381
  batch: {
347
382
  steps: [{
348
383
  stmt: {
349
384
  sql,
350
- args: positionalArgs,
351
- named_args: namedArgs,
385
+ args: encodedArgs.args,
386
+ named_args: encodedArgs.namedArgs,
352
387
  want_rows: true
353
388
  }
354
389
  }]
@@ -439,23 +474,76 @@ var Session = class {
439
474
  }
440
475
  /**
441
476
  * Execute multiple SQL statements in a batch.
442
- *
443
- * @param statements - Array of SQL statements to execute
444
- * @returns Promise resolving to batch execution results
477
+ *
478
+ * When `mode` is set, the batch is sent as a single Hrana request that
479
+ * also carries `BEGIN <mode>` / `COMMIT` / `ROLLBACK` steps using the
480
+ * server-side condition chain, giving atomic execution in one round-trip.
481
+ * When `mode` is omitted, the user statements are sent as-is and run
482
+ * under autocommit (or whatever transaction is already active on this
483
+ * stream).
484
+ *
485
+ * @param statements - Array of SQL statements to execute.
486
+ * @param mode - Optional locking mode; when set, the batch executes
487
+ * atomically. Accepts the same values as `Database.transaction(...)`
488
+ * variants: `"deferred"`, `"immediate"`, `"exclusive"`, `"concurrent"`.
489
+ * @returns Promise resolving to batch execution results.
445
490
  */
446
- async batch(statements, queryOptions) {
491
+ async batch(statements, mode, queryOptions) {
492
+ const userSteps = statements.map((statement) => {
493
+ if (typeof statement === "string") {
494
+ return {
495
+ stmt: { sql: statement, args: [], named_args: [], want_rows: false }
496
+ };
497
+ }
498
+ const encodedArgs = encodeSqlArgs(statement.args ?? []);
499
+ return {
500
+ stmt: {
501
+ sql: statement.sql,
502
+ args: encodedArgs.args,
503
+ named_args: encodedArgs.namedArgs,
504
+ want_rows: false
505
+ }
506
+ };
507
+ });
508
+ let steps;
509
+ let firstUserStepIdx = 0;
510
+ let lastUserStepIdx = userSteps.length - 1;
511
+ let beginIdx = -1;
512
+ let commitIdx = -1;
513
+ let rollbackIdx = -1;
514
+ if (mode === void 0) {
515
+ steps = userSteps;
516
+ } else {
517
+ beginIdx = 0;
518
+ firstUserStepIdx = 1;
519
+ lastUserStepIdx = userSteps.length;
520
+ commitIdx = lastUserStepIdx + 1;
521
+ rollbackIdx = commitIdx + 1;
522
+ steps = [
523
+ { stmt: { sql: `BEGIN ${mode.toUpperCase()}`, args: [], named_args: [], want_rows: false } },
524
+ ...userSteps.map((step, i) => ({
525
+ ...step,
526
+ condition: { type: "ok", step: i === 0 ? beginIdx : firstUserStepIdx + i - 1 }
527
+ })),
528
+ {
529
+ stmt: { sql: "COMMIT", args: [], named_args: [], want_rows: false },
530
+ condition: { type: "ok", step: lastUserStepIdx }
531
+ },
532
+ {
533
+ stmt: { sql: "ROLLBACK", args: [], named_args: [], want_rows: false },
534
+ condition: {
535
+ type: "and",
536
+ conds: [
537
+ { type: "ok", step: beginIdx },
538
+ { type: "not", cond: { type: "ok", step: commitIdx } }
539
+ ]
540
+ }
541
+ }
542
+ ];
543
+ }
447
544
  const request = {
448
545
  baton: this.baton,
449
- batch: {
450
- steps: statements.map((sql) => ({
451
- stmt: {
452
- sql,
453
- args: [],
454
- named_args: [],
455
- want_rows: false
456
- }
457
- }))
458
- }
546
+ batch: { steps }
459
547
  };
460
548
  let batchResult;
461
549
  try {
@@ -471,21 +559,46 @@ var Session = class {
471
559
  }
472
560
  let totalRowsAffected = 0;
473
561
  let lastInsertRowid;
562
+ let deferredError = null;
563
+ let currentStep;
564
+ const isUserStep = (step) => {
565
+ if (mode === void 0) {
566
+ return true;
567
+ }
568
+ return step !== void 0 && step >= firstUserStepIdx && step <= lastUserStepIdx;
569
+ };
474
570
  for await (const entry of entries) {
475
571
  switch (entry.type) {
572
+ case "step_begin":
573
+ currentStep = entry.step;
574
+ break;
476
575
  case "step_end":
477
- if (entry.affected_row_count !== void 0) {
478
- totalRowsAffected += entry.affected_row_count;
479
- }
480
- if (entry.last_insert_rowid !== void 0 && entry.last_insert_rowid !== null) {
481
- lastInsertRowid = typeof entry.last_insert_rowid === "number" ? entry.last_insert_rowid : parseInt(entry.last_insert_rowid, 10);
576
+ if (isUserStep(currentStep)) {
577
+ if (entry.affected_row_count !== void 0) {
578
+ totalRowsAffected += entry.affected_row_count;
579
+ }
580
+ if (entry.last_insert_rowid !== void 0 && entry.last_insert_rowid !== null) {
581
+ lastInsertRowid = typeof entry.last_insert_rowid === "number" ? entry.last_insert_rowid : parseInt(entry.last_insert_rowid, 10);
582
+ }
482
583
  }
584
+ currentStep = void 0;
483
585
  break;
484
586
  case "step_error":
587
+ if (mode === void 0) {
588
+ throw new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
589
+ }
590
+ if (deferredError === null && entry.step !== rollbackIdx) {
591
+ deferredError = new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
592
+ }
593
+ currentStep = void 0;
594
+ break;
485
595
  case "error":
486
596
  throw new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
487
597
  }
488
598
  }
599
+ if (deferredError !== null) {
600
+ throw deferredError;
601
+ }
489
602
  return {
490
603
  rowsAffected: totalRowsAffected,
491
604
  lastInsertRowid
@@ -547,37 +660,6 @@ var Session = class {
547
660
  }
548
661
  };
549
662
 
550
- // src/args.ts
551
- function normalizeArgs(args) {
552
- if (args === void 0) return [];
553
- if (Array.isArray(args)) return args;
554
- if (args !== null && typeof args === "object" && args.constructor === Object) {
555
- return args;
556
- }
557
- return [args];
558
- }
559
- function isQueryOptions(value) {
560
- return value != null && typeof value === "object" && !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "queryTimeout");
561
- }
562
- function splitBindParameters(bindParameters) {
563
- if (bindParameters.length === 0) {
564
- return { params: void 0, queryOptions: void 0 };
565
- }
566
- if (isQueryOptions(bindParameters[bindParameters.length - 1])) {
567
- if (bindParameters.length === 1) {
568
- return { params: void 0, queryOptions: bindParameters[0] };
569
- }
570
- return {
571
- params: bindParameters.length === 2 ? bindParameters[0] : bindParameters.slice(0, -1),
572
- queryOptions: bindParameters[bindParameters.length - 1]
573
- };
574
- }
575
- return {
576
- params: bindParameters.length === 1 ? bindParameters[0] : bindParameters,
577
- queryOptions: void 0
578
- };
579
- }
580
-
581
663
  // src/statement.ts
582
664
  var Statement = class _Statement {
583
665
  constructor(sessionConfig, sql, columns) {
@@ -1009,20 +1091,61 @@ var Connection = class {
1009
1091
  }
1010
1092
  }
1011
1093
  /**
1012
- * Execute multiple SQL statements in a batch.
1013
- *
1014
- * @param statements - Array of SQL statements to execute
1015
- * @param mode - Optional transaction mode (currently unused)
1016
- * @returns Promise resolving to batch execution results
1017
- *
1094
+ * Executes a batch of SQL statements over this connection.
1095
+ *
1096
+ * By default, batch() is not transactional: each statement runs in its
1097
+ * own autocommit step, so a failure mid-batch leaves earlier successful
1098
+ * statements committed. Pass a `mode` to make the batch atomic — the
1099
+ * statements are wrapped in `BEGIN <mode>` / `COMMIT` (with `ROLLBACK`
1100
+ * on failure) and dispatched as a single Hrana request, so the whole
1101
+ * batch completes in one round-trip. When called from inside a
1102
+ * `connection.transaction(...)` callback the `mode` argument is ignored
1103
+ * and the surrounding transaction is reused.
1104
+ *
1105
+ * When `mode` is set, `batch()` owns the surrounding
1106
+ * `BEGIN`/`COMMIT`/`ROLLBACK`, so the `statements` array must not
1107
+ * contain its own transaction-control SQL (`BEGIN`, `COMMIT`,
1108
+ * `ROLLBACK`, `SAVEPOINT`, `RELEASE`). The input is not validated
1109
+ * for that — a user-supplied `COMMIT` will close the wrapper
1110
+ * transaction mid-batch and leave earlier statements committed,
1111
+ * defeating the all-or-nothing contract.
1112
+ *
1113
+ * @param statements - An array of SQL strings or `{ sql, args }` objects.
1114
+ * @param mode - When set, makes the batch atomic. Accepts the same
1115
+ * values as `connection.transaction(...)` variants: `"deferred"`,
1116
+ * `"immediate"`, `"exclusive"`, `"concurrent"`. Ignored when already
1117
+ * inside a transaction.
1118
+ * @returns An object with `rowsAffected` (sum of affected rows) and
1119
+ * `lastInsertRowid` (rowid of the last successful insert).
1120
+ *
1018
1121
  * @example
1019
- * ```typescript
1020
- * await client.batch([
1021
- * "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
1022
- * "INSERT INTO users (name) VALUES ('Alice')",
1023
- * "INSERT INTO users (name) VALUES ('Bob')"
1122
+ * // Plain SQL strings (non-atomic).
1123
+ * await db.batch([
1124
+ * "INSERT INTO users(name) VALUES ('Alice')",
1125
+ * "INSERT INTO users(name) VALUES ('Bob')",
1024
1126
  * ]);
1025
- * ```
1127
+ *
1128
+ * @example
1129
+ * // Positional and named bind parameters.
1130
+ * await db.batch([
1131
+ * { sql: "INSERT INTO users(name, email) VALUES (?, ?)", args: ["Carol", "carol@example.net"] },
1132
+ * { sql: "INSERT INTO users(name, email) VALUES (:name, :email)", args: { name: "Dave", email: "dave@example.net" } },
1133
+ * ]);
1134
+ *
1135
+ * @example
1136
+ * // Atomic via the mode parameter.
1137
+ * await db.batch([
1138
+ * { sql: "INSERT INTO users(name) VALUES (?)", args: ["Eve"] },
1139
+ * { sql: "INSERT INTO users(name) VALUES (?)", args: ["Frank"] },
1140
+ * ], "immediate");
1141
+ *
1142
+ * @example
1143
+ * // Atomic via the transaction() API for mixed workloads.
1144
+ * const txn = db.transaction(async () => {
1145
+ * await db.batch([{ sql: "INSERT INTO users(name) VALUES (?)", args: ["Eve"] }]);
1146
+ * await db.execute("UPDATE counters SET n = n + 1");
1147
+ * });
1148
+ * await txn.immediate();
1026
1149
  */
1027
1150
  async batch(statements, mode, queryOptions) {
1028
1151
  if (!this.isOpen) {
@@ -1030,7 +1153,8 @@ var Connection = class {
1030
1153
  }
1031
1154
  await this.execLock.acquire();
1032
1155
  try {
1033
- return await this.session.batch(statements, queryOptions);
1156
+ const effectiveMode = this._inTransaction ? void 0 : mode;
1157
+ return await this.session.batch(statements, effectiveMode, queryOptions);
1034
1158
  } finally {
1035
1159
  this.execLock.release();
1036
1160
  }
@@ -1103,12 +1227,14 @@ var Connection = class {
1103
1227
  const properties = {
1104
1228
  default: { value: wrapTxn("") },
1105
1229
  deferred: { value: wrapTxn("DEFERRED") },
1230
+ concurrent: { value: wrapTxn("CONCURRENT") },
1106
1231
  immediate: { value: wrapTxn("IMMEDIATE") },
1107
1232
  exclusive: { value: wrapTxn("EXCLUSIVE") },
1108
1233
  database: { value: this, enumerable: true }
1109
1234
  };
1110
1235
  Object.defineProperties(properties.default.value, properties);
1111
1236
  Object.defineProperties(properties.deferred.value, properties);
1237
+ Object.defineProperties(properties.concurrent.value, properties);
1112
1238
  Object.defineProperties(properties.immediate.value, properties);
1113
1239
  Object.defineProperties(properties.exclusive.value, properties);
1114
1240
  return properties.default.value;
package/dist/index.d.cts CHANGED
@@ -39,6 +39,11 @@ interface QueryOptions {
39
39
  queryTimeout?: number;
40
40
  }
41
41
 
42
+ /**
43
+ * Locking mode for atomic `batch()` execution. Accepts the same values
44
+ * as the variants of `Connection.transaction(...)`.
45
+ */
46
+ type BatchMode = 'deferred' | 'immediate' | 'exclusive' | 'concurrent';
42
47
  /**
43
48
  * Configuration options for a session.
44
49
  */
@@ -112,10 +117,23 @@ declare class Session {
112
117
  /**
113
118
  * Execute multiple SQL statements in a batch.
114
119
  *
115
- * @param statements - Array of SQL statements to execute
116
- * @returns Promise resolving to batch execution results
120
+ * When `mode` is set, the batch is sent as a single Hrana request that
121
+ * also carries `BEGIN <mode>` / `COMMIT` / `ROLLBACK` steps using the
122
+ * server-side condition chain, giving atomic execution in one round-trip.
123
+ * When `mode` is omitted, the user statements are sent as-is and run
124
+ * under autocommit (or whatever transaction is already active on this
125
+ * stream).
126
+ *
127
+ * @param statements - Array of SQL statements to execute.
128
+ * @param mode - Optional locking mode; when set, the batch executes
129
+ * atomically. Accepts the same values as `Database.transaction(...)`
130
+ * variants: `"deferred"`, `"immediate"`, `"exclusive"`, `"concurrent"`.
131
+ * @returns Promise resolving to batch execution results.
117
132
  */
118
- batch(statements: string[], queryOptions?: QueryOptions): Promise<any>;
133
+ batch(statements: Array<string | {
134
+ sql: string;
135
+ args?: any[] | Record<string, any>;
136
+ }>, mode?: BatchMode, queryOptions?: QueryOptions): Promise<any>;
119
137
  /**
120
138
  * Execute a sequence of SQL statements separated by semicolons.
121
139
  *
@@ -298,6 +316,10 @@ declare class Statement {
298
316
  */
299
317
  interface Config extends SessionConfig {
300
318
  }
319
+ type BatchStatement = string | {
320
+ sql: string;
321
+ args?: any[] | Record<string, any>;
322
+ };
301
323
  /**
302
324
  * A connection to a Turso database.
303
325
  *
@@ -418,22 +440,63 @@ declare class Connection {
418
440
  */
419
441
  exec(sql: string, queryOptions?: QueryOptions): Promise<any>;
420
442
  /**
421
- * Execute multiple SQL statements in a batch.
443
+ * Executes a batch of SQL statements over this connection.
444
+ *
445
+ * By default, batch() is not transactional: each statement runs in its
446
+ * own autocommit step, so a failure mid-batch leaves earlier successful
447
+ * statements committed. Pass a `mode` to make the batch atomic — the
448
+ * statements are wrapped in `BEGIN <mode>` / `COMMIT` (with `ROLLBACK`
449
+ * on failure) and dispatched as a single Hrana request, so the whole
450
+ * batch completes in one round-trip. When called from inside a
451
+ * `connection.transaction(...)` callback the `mode` argument is ignored
452
+ * and the surrounding transaction is reused.
453
+ *
454
+ * When `mode` is set, `batch()` owns the surrounding
455
+ * `BEGIN`/`COMMIT`/`ROLLBACK`, so the `statements` array must not
456
+ * contain its own transaction-control SQL (`BEGIN`, `COMMIT`,
457
+ * `ROLLBACK`, `SAVEPOINT`, `RELEASE`). The input is not validated
458
+ * for that — a user-supplied `COMMIT` will close the wrapper
459
+ * transaction mid-batch and leave earlier statements committed,
460
+ * defeating the all-or-nothing contract.
461
+ *
462
+ * @param statements - An array of SQL strings or `{ sql, args }` objects.
463
+ * @param mode - When set, makes the batch atomic. Accepts the same
464
+ * values as `connection.transaction(...)` variants: `"deferred"`,
465
+ * `"immediate"`, `"exclusive"`, `"concurrent"`. Ignored when already
466
+ * inside a transaction.
467
+ * @returns An object with `rowsAffected` (sum of affected rows) and
468
+ * `lastInsertRowid` (rowid of the last successful insert).
422
469
  *
423
- * @param statements - Array of SQL statements to execute
424
- * @param mode - Optional transaction mode (currently unused)
425
- * @returns Promise resolving to batch execution results
470
+ * @example
471
+ * // Plain SQL strings (non-atomic).
472
+ * await db.batch([
473
+ * "INSERT INTO users(name) VALUES ('Alice')",
474
+ * "INSERT INTO users(name) VALUES ('Bob')",
475
+ * ]);
426
476
  *
427
477
  * @example
428
- * ```typescript
429
- * await client.batch([
430
- * "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
431
- * "INSERT INTO users (name) VALUES ('Alice')",
432
- * "INSERT INTO users (name) VALUES ('Bob')"
478
+ * // Positional and named bind parameters.
479
+ * await db.batch([
480
+ * { sql: "INSERT INTO users(name, email) VALUES (?, ?)", args: ["Carol", "carol@example.net"] },
481
+ * { sql: "INSERT INTO users(name, email) VALUES (:name, :email)", args: { name: "Dave", email: "dave@example.net" } },
433
482
  * ]);
434
- * ```
483
+ *
484
+ * @example
485
+ * // Atomic via the mode parameter.
486
+ * await db.batch([
487
+ * { sql: "INSERT INTO users(name) VALUES (?)", args: ["Eve"] },
488
+ * { sql: "INSERT INTO users(name) VALUES (?)", args: ["Frank"] },
489
+ * ], "immediate");
490
+ *
491
+ * @example
492
+ * // Atomic via the transaction() API for mixed workloads.
493
+ * const txn = db.transaction(async () => {
494
+ * await db.batch([{ sql: "INSERT INTO users(name) VALUES (?)", args: ["Eve"] }]);
495
+ * await db.execute("UPDATE counters SET n = n + 1");
496
+ * });
497
+ * await txn.immediate();
435
498
  */
436
- batch(statements: string[], mode?: string, queryOptions?: QueryOptions): Promise<any>;
499
+ batch(statements: BatchStatement[], mode?: BatchMode, queryOptions?: QueryOptions): Promise<any>;
437
500
  /**
438
501
  * Execute a pragma.
439
502
  *
@@ -527,4 +590,4 @@ declare class TimeoutError extends DatabaseError {
527
590
  constructor(message?: string, cause?: Error);
528
591
  }
529
592
 
530
- export { type Column, type Config, Connection, DatabaseError, ENCRYPTION_KEY_HEADER, type QueryOptions, Session, type SessionConfig, Statement, TimeoutError, connect };
593
+ export { type BatchStatement, type Column, type Config, Connection, DatabaseError, ENCRYPTION_KEY_HEADER, type QueryOptions, Session, type SessionConfig, Statement, TimeoutError, connect };
package/dist/index.d.ts CHANGED
@@ -39,6 +39,11 @@ interface QueryOptions {
39
39
  queryTimeout?: number;
40
40
  }
41
41
 
42
+ /**
43
+ * Locking mode for atomic `batch()` execution. Accepts the same values
44
+ * as the variants of `Connection.transaction(...)`.
45
+ */
46
+ type BatchMode = 'deferred' | 'immediate' | 'exclusive' | 'concurrent';
42
47
  /**
43
48
  * Configuration options for a session.
44
49
  */
@@ -112,10 +117,23 @@ declare class Session {
112
117
  /**
113
118
  * Execute multiple SQL statements in a batch.
114
119
  *
115
- * @param statements - Array of SQL statements to execute
116
- * @returns Promise resolving to batch execution results
120
+ * When `mode` is set, the batch is sent as a single Hrana request that
121
+ * also carries `BEGIN <mode>` / `COMMIT` / `ROLLBACK` steps using the
122
+ * server-side condition chain, giving atomic execution in one round-trip.
123
+ * When `mode` is omitted, the user statements are sent as-is and run
124
+ * under autocommit (or whatever transaction is already active on this
125
+ * stream).
126
+ *
127
+ * @param statements - Array of SQL statements to execute.
128
+ * @param mode - Optional locking mode; when set, the batch executes
129
+ * atomically. Accepts the same values as `Database.transaction(...)`
130
+ * variants: `"deferred"`, `"immediate"`, `"exclusive"`, `"concurrent"`.
131
+ * @returns Promise resolving to batch execution results.
117
132
  */
118
- batch(statements: string[], queryOptions?: QueryOptions): Promise<any>;
133
+ batch(statements: Array<string | {
134
+ sql: string;
135
+ args?: any[] | Record<string, any>;
136
+ }>, mode?: BatchMode, queryOptions?: QueryOptions): Promise<any>;
119
137
  /**
120
138
  * Execute a sequence of SQL statements separated by semicolons.
121
139
  *
@@ -298,6 +316,10 @@ declare class Statement {
298
316
  */
299
317
  interface Config extends SessionConfig {
300
318
  }
319
+ type BatchStatement = string | {
320
+ sql: string;
321
+ args?: any[] | Record<string, any>;
322
+ };
301
323
  /**
302
324
  * A connection to a Turso database.
303
325
  *
@@ -418,22 +440,63 @@ declare class Connection {
418
440
  */
419
441
  exec(sql: string, queryOptions?: QueryOptions): Promise<any>;
420
442
  /**
421
- * Execute multiple SQL statements in a batch.
443
+ * Executes a batch of SQL statements over this connection.
444
+ *
445
+ * By default, batch() is not transactional: each statement runs in its
446
+ * own autocommit step, so a failure mid-batch leaves earlier successful
447
+ * statements committed. Pass a `mode` to make the batch atomic — the
448
+ * statements are wrapped in `BEGIN <mode>` / `COMMIT` (with `ROLLBACK`
449
+ * on failure) and dispatched as a single Hrana request, so the whole
450
+ * batch completes in one round-trip. When called from inside a
451
+ * `connection.transaction(...)` callback the `mode` argument is ignored
452
+ * and the surrounding transaction is reused.
453
+ *
454
+ * When `mode` is set, `batch()` owns the surrounding
455
+ * `BEGIN`/`COMMIT`/`ROLLBACK`, so the `statements` array must not
456
+ * contain its own transaction-control SQL (`BEGIN`, `COMMIT`,
457
+ * `ROLLBACK`, `SAVEPOINT`, `RELEASE`). The input is not validated
458
+ * for that — a user-supplied `COMMIT` will close the wrapper
459
+ * transaction mid-batch and leave earlier statements committed,
460
+ * defeating the all-or-nothing contract.
461
+ *
462
+ * @param statements - An array of SQL strings or `{ sql, args }` objects.
463
+ * @param mode - When set, makes the batch atomic. Accepts the same
464
+ * values as `connection.transaction(...)` variants: `"deferred"`,
465
+ * `"immediate"`, `"exclusive"`, `"concurrent"`. Ignored when already
466
+ * inside a transaction.
467
+ * @returns An object with `rowsAffected` (sum of affected rows) and
468
+ * `lastInsertRowid` (rowid of the last successful insert).
422
469
  *
423
- * @param statements - Array of SQL statements to execute
424
- * @param mode - Optional transaction mode (currently unused)
425
- * @returns Promise resolving to batch execution results
470
+ * @example
471
+ * // Plain SQL strings (non-atomic).
472
+ * await db.batch([
473
+ * "INSERT INTO users(name) VALUES ('Alice')",
474
+ * "INSERT INTO users(name) VALUES ('Bob')",
475
+ * ]);
426
476
  *
427
477
  * @example
428
- * ```typescript
429
- * await client.batch([
430
- * "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
431
- * "INSERT INTO users (name) VALUES ('Alice')",
432
- * "INSERT INTO users (name) VALUES ('Bob')"
478
+ * // Positional and named bind parameters.
479
+ * await db.batch([
480
+ * { sql: "INSERT INTO users(name, email) VALUES (?, ?)", args: ["Carol", "carol@example.net"] },
481
+ * { sql: "INSERT INTO users(name, email) VALUES (:name, :email)", args: { name: "Dave", email: "dave@example.net" } },
433
482
  * ]);
434
- * ```
483
+ *
484
+ * @example
485
+ * // Atomic via the mode parameter.
486
+ * await db.batch([
487
+ * { sql: "INSERT INTO users(name) VALUES (?)", args: ["Eve"] },
488
+ * { sql: "INSERT INTO users(name) VALUES (?)", args: ["Frank"] },
489
+ * ], "immediate");
490
+ *
491
+ * @example
492
+ * // Atomic via the transaction() API for mixed workloads.
493
+ * const txn = db.transaction(async () => {
494
+ * await db.batch([{ sql: "INSERT INTO users(name) VALUES (?)", args: ["Eve"] }]);
495
+ * await db.execute("UPDATE counters SET n = n + 1");
496
+ * });
497
+ * await txn.immediate();
435
498
  */
436
- batch(statements: string[], mode?: string, queryOptions?: QueryOptions): Promise<any>;
499
+ batch(statements: BatchStatement[], mode?: BatchMode, queryOptions?: QueryOptions): Promise<any>;
437
500
  /**
438
501
  * Execute a pragma.
439
502
  *
@@ -527,4 +590,4 @@ declare class TimeoutError extends DatabaseError {
527
590
  constructor(message?: string, cause?: Error);
528
591
  }
529
592
 
530
- export { type Column, type Config, Connection, DatabaseError, ENCRYPTION_KEY_HEADER, type QueryOptions, Session, type SessionConfig, Statement, TimeoutError, connect };
593
+ export { type BatchStatement, type Column, type Config, Connection, DatabaseError, ENCRYPTION_KEY_HEADER, type QueryOptions, Session, type SessionConfig, Statement, TimeoutError, connect };