Skip to content

Commit d7214f2

Browse files
committed
libsql-client: Report batch statement index on error
If a batch statement fails, the SQL over HTTP protocol does report back exactly what step failed. However, we also need to propagate that information to the caller.
1 parent e0b105f commit d7214f2

File tree

3 files changed

+154
-44
lines changed

3 files changed

+154
-44
lines changed

packages/libsql-client/src/hrana.ts

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
TransactionMode,
77
InArgs,
88
} from "@libsql/core/api";
9-
import { LibsqlError } from "@libsql/core/api";
9+
import { LibsqlError, LibsqlBatchError } from "@libsql/core/api";
1010
import type { SqlCache } from "./sql_cache.js";
1111
import { transactionModeToBegin, ResultSetImpl } from "@libsql/core/util";
1212

@@ -134,16 +134,33 @@ export abstract class HranaTransaction implements Transaction {
134134
}
135135

136136
const resultSets = [];
137-
for (const rowsPromise of rowsPromises) {
138-
const rows = await rowsPromise;
139-
if (rows === undefined) {
140-
throw new LibsqlError(
141-
"Statement in a transaction was not executed, " +
142-
"probably because the transaction has been rolled back",
143-
"TRANSACTION_CLOSED",
144-
);
137+
for (let i = 0; i < rowsPromises.length; i++) {
138+
try {
139+
const rows = await rowsPromises[i];
140+
if (rows === undefined) {
141+
throw new LibsqlBatchError(
142+
"Statement in a transaction was not executed, " +
143+
"probably because the transaction has been rolled back",
144+
i,
145+
"TRANSACTION_CLOSED",
146+
);
147+
}
148+
resultSets.push(resultSetFromHrana(rows));
149+
} catch (e) {
150+
if (e instanceof LibsqlBatchError) {
151+
throw e;
152+
}
153+
if (e instanceof LibsqlError) {
154+
throw new LibsqlBatchError(
155+
e.message,
156+
i,
157+
e.code,
158+
e.rawCode,
159+
e.cause,
160+
);
161+
}
162+
throw e;
145163
}
146-
resultSets.push(resultSetFromHrana(rows));
147164
}
148165
return resultSets;
149166
} catch (e) {
@@ -295,15 +312,32 @@ export async function executeHranaBatch(
295312

296313
const resultSets = [];
297314
await beginPromise;
298-
for (const stmtPromise of stmtPromises) {
299-
const hranaRows = await stmtPromise;
300-
if (hranaRows === undefined) {
301-
throw new LibsqlError(
302-
"Statement in a batch was not executed, probably because the transaction has been rolled back",
303-
"TRANSACTION_CLOSED",
304-
);
315+
for (let i = 0; i < stmtPromises.length; i++) {
316+
try {
317+
const hranaRows = await stmtPromises[i];
318+
if (hranaRows === undefined) {
319+
throw new LibsqlBatchError(
320+
"Statement in a batch was not executed, probably because the transaction has been rolled back",
321+
i,
322+
"TRANSACTION_CLOSED",
323+
);
324+
}
325+
resultSets.push(resultSetFromHrana(hranaRows));
326+
} catch (e) {
327+
if (e instanceof LibsqlBatchError) {
328+
throw e;
329+
}
330+
if (e instanceof LibsqlError) {
331+
throw new LibsqlBatchError(
332+
e.message,
333+
i,
334+
e.code,
335+
e.rawCode,
336+
e.cause,
337+
);
338+
}
339+
throw e;
305340
}
306-
resultSets.push(resultSetFromHrana(hranaRows));
307341
}
308342
await commitPromise;
309343

packages/libsql-client/src/sqlite3.ts

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
InArgs,
1616
Replicated,
1717
} from "@libsql/core/api";
18-
import { LibsqlError } from "@libsql/core/api";
18+
import { LibsqlError, LibsqlBatchError } from "@libsql/core/api";
1919
import type { ExpandedConfig } from "@libsql/core/config";
2020
import { expandConfig, isInMemoryConfig } from "@libsql/core/config";
2121
import {
@@ -148,18 +148,38 @@ export class Sqlite3Client implements Client {
148148
const db = this.#getDb();
149149
try {
150150
executeStmt(db, transactionModeToBegin(mode), this.#intMode);
151-
const resultSets = stmts.map((stmt) => {
152-
if (!db.inTransaction) {
153-
throw new LibsqlError(
154-
"The transaction has been rolled back",
155-
"TRANSACTION_CLOSED",
151+
const resultSets = [];
152+
for (let i = 0; i < stmts.length; i++) {
153+
try {
154+
if (!db.inTransaction) {
155+
throw new LibsqlBatchError(
156+
"The transaction has been rolled back",
157+
i,
158+
"TRANSACTION_CLOSED",
159+
);
160+
}
161+
const normalizedStmt: InStatement = Array.isArray(stmts[i])
162+
? { sql: stmts[i][0], args: stmts[i][1] || [] }
163+
: stmts[i];
164+
resultSets.push(
165+
executeStmt(db, normalizedStmt, this.#intMode),
156166
);
167+
} catch (e) {
168+
if (e instanceof LibsqlBatchError) {
169+
throw e;
170+
}
171+
if (e instanceof LibsqlError) {
172+
throw new LibsqlBatchError(
173+
e.message,
174+
i,
175+
e.code,
176+
e.rawCode,
177+
e.cause,
178+
);
179+
}
180+
throw e;
157181
}
158-
const normalizedStmt: InStatement = Array.isArray(stmt)
159-
? { sql: stmt[0], args: stmt[1] || [] }
160-
: stmt;
161-
return executeStmt(db, normalizedStmt, this.#intMode);
162-
});
182+
}
163183
executeStmt(db, "COMMIT", this.#intMode);
164184
return resultSets;
165185
} finally {
@@ -175,15 +195,33 @@ export class Sqlite3Client implements Client {
175195
try {
176196
executeStmt(db, "PRAGMA foreign_keys=off", this.#intMode);
177197
executeStmt(db, transactionModeToBegin("deferred"), this.#intMode);
178-
const resultSets = stmts.map((stmt) => {
179-
if (!db.inTransaction) {
180-
throw new LibsqlError(
181-
"The transaction has been rolled back",
182-
"TRANSACTION_CLOSED",
183-
);
198+
const resultSets = [];
199+
for (let i = 0; i < stmts.length; i++) {
200+
try {
201+
if (!db.inTransaction) {
202+
throw new LibsqlBatchError(
203+
"The transaction has been rolled back",
204+
i,
205+
"TRANSACTION_CLOSED",
206+
);
207+
}
208+
resultSets.push(executeStmt(db, stmts[i], this.#intMode));
209+
} catch (e) {
210+
if (e instanceof LibsqlBatchError) {
211+
throw e;
212+
}
213+
if (e instanceof LibsqlError) {
214+
throw new LibsqlBatchError(
215+
e.message,
216+
i,
217+
e.code,
218+
e.rawCode,
219+
e.cause,
220+
);
221+
}
222+
throw e;
184223
}
185-
return executeStmt(db, stmt, this.#intMode);
186-
});
224+
}
187225
executeStmt(db, "COMMIT", this.#intMode);
188226
return resultSets;
189227
} finally {
@@ -291,13 +329,33 @@ export class Sqlite3Transaction implements Transaction {
291329
async batch(
292330
stmts: Array<InStatement | [string, InArgs?]>,
293331
): Promise<Array<ResultSet>> {
294-
return stmts.map((stmt) => {
295-
this.#checkNotClosed();
296-
const normalizedStmt: InStatement = Array.isArray(stmt)
297-
? { sql: stmt[0], args: stmt[1] || [] }
298-
: stmt;
299-
return executeStmt(this.#database, normalizedStmt, this.#intMode);
300-
});
332+
const resultSets = [];
333+
for (let i = 0; i < stmts.length; i++) {
334+
try {
335+
this.#checkNotClosed();
336+
const normalizedStmt: InStatement = Array.isArray(stmts[i])
337+
? { sql: stmts[i][0], args: stmts[i][1] || [] }
338+
: stmts[i];
339+
resultSets.push(
340+
executeStmt(this.#database, normalizedStmt, this.#intMode),
341+
);
342+
} catch (e) {
343+
if (e instanceof LibsqlBatchError) {
344+
throw e;
345+
}
346+
if (e instanceof LibsqlError) {
347+
throw new LibsqlBatchError(
348+
e.message,
349+
i,
350+
e.code,
351+
e.rawCode,
352+
e.cause,
353+
);
354+
}
355+
throw e;
356+
}
357+
}
358+
return resultSets;
301359
}
302360

303361
async executeMultiple(sql: string): Promise<void> {

packages/libsql-core/src/api.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,21 @@ export class LibsqlError extends Error {
504504
this.name = "LibsqlError";
505505
}
506506
}
507+
508+
/** Error thrown by the client during batch operations. */
509+
export class LibsqlBatchError extends LibsqlError {
510+
/** The zero-based index of the statement that failed in the batch. */
511+
statementIndex: number;
512+
513+
constructor(
514+
message: string,
515+
statementIndex: number,
516+
code: string,
517+
rawCode?: number,
518+
cause?: Error,
519+
) {
520+
super(message, code, rawCode, cause);
521+
this.statementIndex = statementIndex;
522+
this.name = "LibsqlBatchError";
523+
}
524+
}

0 commit comments

Comments
 (0)