@tursodatabase/serverless 1.2.0-pre.1 → 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/README.md CHANGED
@@ -60,6 +60,14 @@ await conn.batch([
60
60
  "INSERT INTO users (email) VALUES ('user@example.com')",
61
61
  "INSERT INTO users (email) VALUES ('admin@example.com')",
62
62
  ]);
63
+
64
+ // Parameterized batch statements also work
65
+ await conn.transaction(async () => {
66
+ await conn.batch([
67
+ { sql: "INSERT INTO users (email) VALUES (?)", args: ["alice@example.com"] },
68
+ { sql: "INSERT INTO users (email) VALUES (?)", args: ["bob@example.com"] },
69
+ ]);
70
+ }).concurrent();
63
71
  ```
64
72
 
65
73
  ### libSQL Compatibility Layer
@@ -212,6 +212,38 @@ async function executePipeline(url, authToken, request, remoteEncryptionKey, sig
212
212
  return response.json();
213
213
  }
214
214
 
215
+ // src/args.ts
216
+ function encodeSqlArgs(args = []) {
217
+ let positionalArgs = [];
218
+ let namedArgs = [];
219
+ if (Array.isArray(args)) {
220
+ positionalArgs = args.map(encodeValue);
221
+ } else {
222
+ const keys = Object.keys(args);
223
+ const isNumericKeys = keys.length > 0 && keys.every((key) => /^\d+$/.test(key));
224
+ if (isNumericKeys) {
225
+ const sortedKeys = keys.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
226
+ const maxIndex = parseInt(sortedKeys[sortedKeys.length - 1], 10);
227
+ positionalArgs = new Array(maxIndex);
228
+ for (const key of sortedKeys) {
229
+ const index = parseInt(key, 10) - 1;
230
+ positionalArgs[index] = encodeValue(args[key]);
231
+ }
232
+ for (let i = 0; i < positionalArgs.length; i++) {
233
+ if (positionalArgs[i] === void 0) {
234
+ positionalArgs[i] = { type: "null" };
235
+ }
236
+ }
237
+ } else {
238
+ namedArgs = Object.entries(args).map(([name, value]) => ({
239
+ name,
240
+ value: encodeValue(value)
241
+ }));
242
+ }
243
+ }
244
+ return { args: positionalArgs, namedArgs };
245
+ }
246
+
215
247
  // src/session.ts
216
248
  function normalizeUrl(url) {
217
249
  return url.replace(/^libsql:\/\//, "https://");
@@ -289,41 +321,15 @@ var Session = class {
289
321
  * @returns Promise resolving to the raw response and cursor entries
290
322
  */
291
323
  async executeRaw(sql, args = [], queryOptions) {
292
- let positionalArgs = [];
293
- let namedArgs = [];
294
- if (Array.isArray(args)) {
295
- positionalArgs = args.map(encodeValue);
296
- } else {
297
- const keys = Object.keys(args);
298
- const isNumericKeys = keys.length > 0 && keys.every((key) => /^\d+$/.test(key));
299
- if (isNumericKeys) {
300
- const sortedKeys = keys.sort((a, b) => parseInt(a) - parseInt(b));
301
- const maxIndex = parseInt(sortedKeys[sortedKeys.length - 1]);
302
- positionalArgs = new Array(maxIndex);
303
- for (const key of sortedKeys) {
304
- const index = parseInt(key) - 1;
305
- positionalArgs[index] = encodeValue(args[key]);
306
- }
307
- for (let i = 0; i < positionalArgs.length; i++) {
308
- if (positionalArgs[i] === void 0) {
309
- positionalArgs[i] = { type: "null" };
310
- }
311
- }
312
- } else {
313
- namedArgs = Object.entries(args).map(([name, value]) => ({
314
- name,
315
- value: encodeValue(value)
316
- }));
317
- }
318
- }
324
+ const encodedArgs = encodeSqlArgs(args);
319
325
  const request = {
320
326
  baton: this.baton,
321
327
  batch: {
322
328
  steps: [{
323
329
  stmt: {
324
330
  sql,
325
- args: positionalArgs,
326
- named_args: namedArgs,
331
+ args: encodedArgs.args,
332
+ named_args: encodedArgs.namedArgs,
327
333
  want_rows: true
328
334
  }
329
335
  }]
@@ -414,23 +420,76 @@ var Session = class {
414
420
  }
415
421
  /**
416
422
  * Execute multiple SQL statements in a batch.
417
- *
418
- * @param statements - Array of SQL statements to execute
419
- * @returns Promise resolving to batch execution results
423
+ *
424
+ * When `mode` is set, the batch is sent as a single Hrana request that
425
+ * also carries `BEGIN <mode>` / `COMMIT` / `ROLLBACK` steps using the
426
+ * server-side condition chain, giving atomic execution in one round-trip.
427
+ * When `mode` is omitted, the user statements are sent as-is and run
428
+ * under autocommit (or whatever transaction is already active on this
429
+ * stream).
430
+ *
431
+ * @param statements - Array of SQL statements to execute.
432
+ * @param mode - Optional locking mode; when set, the batch executes
433
+ * atomically. Accepts the same values as `Database.transaction(...)`
434
+ * variants: `"deferred"`, `"immediate"`, `"exclusive"`, `"concurrent"`.
435
+ * @returns Promise resolving to batch execution results.
420
436
  */
421
- async batch(statements, queryOptions) {
437
+ async batch(statements, mode, queryOptions) {
438
+ const userSteps = statements.map((statement) => {
439
+ if (typeof statement === "string") {
440
+ return {
441
+ stmt: { sql: statement, args: [], named_args: [], want_rows: false }
442
+ };
443
+ }
444
+ const encodedArgs = encodeSqlArgs(statement.args ?? []);
445
+ return {
446
+ stmt: {
447
+ sql: statement.sql,
448
+ args: encodedArgs.args,
449
+ named_args: encodedArgs.namedArgs,
450
+ want_rows: false
451
+ }
452
+ };
453
+ });
454
+ let steps;
455
+ let firstUserStepIdx = 0;
456
+ let lastUserStepIdx = userSteps.length - 1;
457
+ let beginIdx = -1;
458
+ let commitIdx = -1;
459
+ let rollbackIdx = -1;
460
+ if (mode === void 0) {
461
+ steps = userSteps;
462
+ } else {
463
+ beginIdx = 0;
464
+ firstUserStepIdx = 1;
465
+ lastUserStepIdx = userSteps.length;
466
+ commitIdx = lastUserStepIdx + 1;
467
+ rollbackIdx = commitIdx + 1;
468
+ steps = [
469
+ { stmt: { sql: `BEGIN ${mode.toUpperCase()}`, args: [], named_args: [], want_rows: false } },
470
+ ...userSteps.map((step, i) => ({
471
+ ...step,
472
+ condition: { type: "ok", step: i === 0 ? beginIdx : firstUserStepIdx + i - 1 }
473
+ })),
474
+ {
475
+ stmt: { sql: "COMMIT", args: [], named_args: [], want_rows: false },
476
+ condition: { type: "ok", step: lastUserStepIdx }
477
+ },
478
+ {
479
+ stmt: { sql: "ROLLBACK", args: [], named_args: [], want_rows: false },
480
+ condition: {
481
+ type: "and",
482
+ conds: [
483
+ { type: "ok", step: beginIdx },
484
+ { type: "not", cond: { type: "ok", step: commitIdx } }
485
+ ]
486
+ }
487
+ }
488
+ ];
489
+ }
422
490
  const request = {
423
491
  baton: this.baton,
424
- batch: {
425
- steps: statements.map((sql) => ({
426
- stmt: {
427
- sql,
428
- args: [],
429
- named_args: [],
430
- want_rows: false
431
- }
432
- }))
433
- }
492
+ batch: { steps }
434
493
  };
435
494
  let batchResult;
436
495
  try {
@@ -446,21 +505,46 @@ var Session = class {
446
505
  }
447
506
  let totalRowsAffected = 0;
448
507
  let lastInsertRowid;
508
+ let deferredError = null;
509
+ let currentStep;
510
+ const isUserStep = (step) => {
511
+ if (mode === void 0) {
512
+ return true;
513
+ }
514
+ return step !== void 0 && step >= firstUserStepIdx && step <= lastUserStepIdx;
515
+ };
449
516
  for await (const entry of entries) {
450
517
  switch (entry.type) {
518
+ case "step_begin":
519
+ currentStep = entry.step;
520
+ break;
451
521
  case "step_end":
452
- if (entry.affected_row_count !== void 0) {
453
- totalRowsAffected += entry.affected_row_count;
454
- }
455
- if (entry.last_insert_rowid !== void 0 && entry.last_insert_rowid !== null) {
456
- lastInsertRowid = typeof entry.last_insert_rowid === "number" ? entry.last_insert_rowid : parseInt(entry.last_insert_rowid, 10);
522
+ if (isUserStep(currentStep)) {
523
+ if (entry.affected_row_count !== void 0) {
524
+ totalRowsAffected += entry.affected_row_count;
525
+ }
526
+ if (entry.last_insert_rowid !== void 0 && entry.last_insert_rowid !== null) {
527
+ lastInsertRowid = typeof entry.last_insert_rowid === "number" ? entry.last_insert_rowid : parseInt(entry.last_insert_rowid, 10);
528
+ }
457
529
  }
530
+ currentStep = void 0;
458
531
  break;
459
532
  case "step_error":
533
+ if (mode === void 0) {
534
+ throw new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
535
+ }
536
+ if (deferredError === null && entry.step !== rollbackIdx) {
537
+ deferredError = new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
538
+ }
539
+ currentStep = void 0;
540
+ break;
460
541
  case "error":
461
542
  throw new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
462
543
  }
463
544
  }
545
+ if (deferredError !== null) {
546
+ throw deferredError;
547
+ }
464
548
  return {
465
549
  rowsAffected: totalRowsAffected,
466
550
  lastInsertRowid
@@ -210,6 +210,38 @@ async function executePipeline(url, authToken, request, remoteEncryptionKey, sig
210
210
  return response.json();
211
211
  }
212
212
 
213
+ // src/args.ts
214
+ function encodeSqlArgs(args = []) {
215
+ let positionalArgs = [];
216
+ let namedArgs = [];
217
+ if (Array.isArray(args)) {
218
+ positionalArgs = args.map(encodeValue);
219
+ } else {
220
+ const keys = Object.keys(args);
221
+ const isNumericKeys = keys.length > 0 && keys.every((key) => /^\d+$/.test(key));
222
+ if (isNumericKeys) {
223
+ const sortedKeys = keys.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
224
+ const maxIndex = parseInt(sortedKeys[sortedKeys.length - 1], 10);
225
+ positionalArgs = new Array(maxIndex);
226
+ for (const key of sortedKeys) {
227
+ const index = parseInt(key, 10) - 1;
228
+ positionalArgs[index] = encodeValue(args[key]);
229
+ }
230
+ for (let i = 0; i < positionalArgs.length; i++) {
231
+ if (positionalArgs[i] === void 0) {
232
+ positionalArgs[i] = { type: "null" };
233
+ }
234
+ }
235
+ } else {
236
+ namedArgs = Object.entries(args).map(([name, value]) => ({
237
+ name,
238
+ value: encodeValue(value)
239
+ }));
240
+ }
241
+ }
242
+ return { args: positionalArgs, namedArgs };
243
+ }
244
+
213
245
  // src/session.ts
214
246
  function normalizeUrl(url) {
215
247
  return url.replace(/^libsql:\/\//, "https://");
@@ -287,41 +319,15 @@ var Session = class {
287
319
  * @returns Promise resolving to the raw response and cursor entries
288
320
  */
289
321
  async executeRaw(sql, args = [], queryOptions) {
290
- let positionalArgs = [];
291
- let namedArgs = [];
292
- if (Array.isArray(args)) {
293
- positionalArgs = args.map(encodeValue);
294
- } else {
295
- const keys = Object.keys(args);
296
- const isNumericKeys = keys.length > 0 && keys.every((key) => /^\d+$/.test(key));
297
- if (isNumericKeys) {
298
- const sortedKeys = keys.sort((a, b) => parseInt(a) - parseInt(b));
299
- const maxIndex = parseInt(sortedKeys[sortedKeys.length - 1]);
300
- positionalArgs = new Array(maxIndex);
301
- for (const key of sortedKeys) {
302
- const index = parseInt(key) - 1;
303
- positionalArgs[index] = encodeValue(args[key]);
304
- }
305
- for (let i = 0; i < positionalArgs.length; i++) {
306
- if (positionalArgs[i] === void 0) {
307
- positionalArgs[i] = { type: "null" };
308
- }
309
- }
310
- } else {
311
- namedArgs = Object.entries(args).map(([name, value]) => ({
312
- name,
313
- value: encodeValue(value)
314
- }));
315
- }
316
- }
322
+ const encodedArgs = encodeSqlArgs(args);
317
323
  const request = {
318
324
  baton: this.baton,
319
325
  batch: {
320
326
  steps: [{
321
327
  stmt: {
322
328
  sql,
323
- args: positionalArgs,
324
- named_args: namedArgs,
329
+ args: encodedArgs.args,
330
+ named_args: encodedArgs.namedArgs,
325
331
  want_rows: true
326
332
  }
327
333
  }]
@@ -412,23 +418,76 @@ var Session = class {
412
418
  }
413
419
  /**
414
420
  * Execute multiple SQL statements in a batch.
415
- *
416
- * @param statements - Array of SQL statements to execute
417
- * @returns Promise resolving to batch execution results
421
+ *
422
+ * When `mode` is set, the batch is sent as a single Hrana request that
423
+ * also carries `BEGIN <mode>` / `COMMIT` / `ROLLBACK` steps using the
424
+ * server-side condition chain, giving atomic execution in one round-trip.
425
+ * When `mode` is omitted, the user statements are sent as-is and run
426
+ * under autocommit (or whatever transaction is already active on this
427
+ * stream).
428
+ *
429
+ * @param statements - Array of SQL statements to execute.
430
+ * @param mode - Optional locking mode; when set, the batch executes
431
+ * atomically. Accepts the same values as `Database.transaction(...)`
432
+ * variants: `"deferred"`, `"immediate"`, `"exclusive"`, `"concurrent"`.
433
+ * @returns Promise resolving to batch execution results.
418
434
  */
419
- async batch(statements, queryOptions) {
435
+ async batch(statements, mode, queryOptions) {
436
+ const userSteps = statements.map((statement) => {
437
+ if (typeof statement === "string") {
438
+ return {
439
+ stmt: { sql: statement, args: [], named_args: [], want_rows: false }
440
+ };
441
+ }
442
+ const encodedArgs = encodeSqlArgs(statement.args ?? []);
443
+ return {
444
+ stmt: {
445
+ sql: statement.sql,
446
+ args: encodedArgs.args,
447
+ named_args: encodedArgs.namedArgs,
448
+ want_rows: false
449
+ }
450
+ };
451
+ });
452
+ let steps;
453
+ let firstUserStepIdx = 0;
454
+ let lastUserStepIdx = userSteps.length - 1;
455
+ let beginIdx = -1;
456
+ let commitIdx = -1;
457
+ let rollbackIdx = -1;
458
+ if (mode === void 0) {
459
+ steps = userSteps;
460
+ } else {
461
+ beginIdx = 0;
462
+ firstUserStepIdx = 1;
463
+ lastUserStepIdx = userSteps.length;
464
+ commitIdx = lastUserStepIdx + 1;
465
+ rollbackIdx = commitIdx + 1;
466
+ steps = [
467
+ { stmt: { sql: `BEGIN ${mode.toUpperCase()}`, args: [], named_args: [], want_rows: false } },
468
+ ...userSteps.map((step, i) => ({
469
+ ...step,
470
+ condition: { type: "ok", step: i === 0 ? beginIdx : firstUserStepIdx + i - 1 }
471
+ })),
472
+ {
473
+ stmt: { sql: "COMMIT", args: [], named_args: [], want_rows: false },
474
+ condition: { type: "ok", step: lastUserStepIdx }
475
+ },
476
+ {
477
+ stmt: { sql: "ROLLBACK", args: [], named_args: [], want_rows: false },
478
+ condition: {
479
+ type: "and",
480
+ conds: [
481
+ { type: "ok", step: beginIdx },
482
+ { type: "not", cond: { type: "ok", step: commitIdx } }
483
+ ]
484
+ }
485
+ }
486
+ ];
487
+ }
420
488
  const request = {
421
489
  baton: this.baton,
422
- batch: {
423
- steps: statements.map((sql) => ({
424
- stmt: {
425
- sql,
426
- args: [],
427
- named_args: [],
428
- want_rows: false
429
- }
430
- }))
431
- }
490
+ batch: { steps }
432
491
  };
433
492
  let batchResult;
434
493
  try {
@@ -444,21 +503,46 @@ var Session = class {
444
503
  }
445
504
  let totalRowsAffected = 0;
446
505
  let lastInsertRowid;
506
+ let deferredError = null;
507
+ let currentStep;
508
+ const isUserStep = (step) => {
509
+ if (mode === void 0) {
510
+ return true;
511
+ }
512
+ return step !== void 0 && step >= firstUserStepIdx && step <= lastUserStepIdx;
513
+ };
447
514
  for await (const entry of entries) {
448
515
  switch (entry.type) {
516
+ case "step_begin":
517
+ currentStep = entry.step;
518
+ break;
449
519
  case "step_end":
450
- if (entry.affected_row_count !== void 0) {
451
- totalRowsAffected += entry.affected_row_count;
452
- }
453
- if (entry.last_insert_rowid !== void 0 && entry.last_insert_rowid !== null) {
454
- lastInsertRowid = typeof entry.last_insert_rowid === "number" ? entry.last_insert_rowid : parseInt(entry.last_insert_rowid, 10);
520
+ if (isUserStep(currentStep)) {
521
+ if (entry.affected_row_count !== void 0) {
522
+ totalRowsAffected += entry.affected_row_count;
523
+ }
524
+ if (entry.last_insert_rowid !== void 0 && entry.last_insert_rowid !== null) {
525
+ lastInsertRowid = typeof entry.last_insert_rowid === "number" ? entry.last_insert_rowid : parseInt(entry.last_insert_rowid, 10);
526
+ }
455
527
  }
528
+ currentStep = void 0;
456
529
  break;
457
530
  case "step_error":
531
+ if (mode === void 0) {
532
+ throw new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
533
+ }
534
+ if (deferredError === null && entry.step !== rollbackIdx) {
535
+ deferredError = new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
536
+ }
537
+ currentStep = void 0;
538
+ break;
458
539
  case "error":
459
540
  throw new DatabaseError(entry.error?.message || "Batch execution failed", entry.error?.code);
460
541
  }
461
542
  }
543
+ if (deferredError !== null) {
544
+ throw deferredError;
545
+ }
462
546
  return {
463
547
  rowsAffected: totalRowsAffected,
464
548
  lastInsertRowid