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