Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions packages/libsql-client/src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,110 @@ describe("batch()", () => {
}),
);

test(
"batch error reports statement index - error at index 0",
withClient(async (c) => {
try {
await c.batch(
["SELECT invalid_column", "SELECT 1", "SELECT 2"],
"read",
);
throw new Error("Expected batch to fail");
} catch (e: any) {
expect(e.name).toBe("LibsqlBatchError");
expect(e.statementIndex).toBe(0);
expect(e.code).toBeDefined();
}
}),
);

test(
"batch error reports statement index - error at index 1",
withClient(async (c) => {
try {
await c.batch(
["SELECT 1", "SELECT invalid_column", "SELECT 2"],
"read",
);
throw new Error("Expected batch to fail");
} catch (e: any) {
expect(e.name).toBe("LibsqlBatchError");
expect(e.statementIndex).toBe(1);
expect(e.code).toBeDefined();
}
}),
);

test(
"batch error reports statement index - error at index 2",
withClient(async (c) => {
try {
await c.batch(
["SELECT 1", "SELECT 2", "SELECT invalid_column"],
"read",
);
throw new Error("Expected batch to fail");
} catch (e: any) {
expect(e.name).toBe("LibsqlBatchError");
expect(e.statementIndex).toBe(2);
expect(e.code).toBeDefined();
}
}),
);

test(
"batch error with write mode reports statement index",
withClient(async (c) => {
await c.execute("DROP TABLE IF EXISTS t");
await c.execute("CREATE TABLE t (a UNIQUE)");
await c.execute("INSERT INTO t VALUES (1)");

try {
await c.batch(
[
"INSERT INTO t VALUES (2)",
"INSERT INTO t VALUES (3)",
"INSERT INTO t VALUES (1)", // Duplicate, will fail
"INSERT INTO t VALUES (4)",
],
"write",
);
throw new Error("Expected batch to fail");
} catch (e: any) {
expect(e.name).toBe("LibsqlBatchError");
expect(e.statementIndex).toBe(2);
expect(e.code).toBeDefined();
}

// Verify rollback happened
const rs = await c.execute("SELECT COUNT(*) FROM t");
expect(rs.rows[0][0]).toBe(1);
}),
);

test(
"batch error in in-memory database reports statement index",
withInMemoryClient(async (c) => {
await c.execute("CREATE TABLE t (a)");

try {
await c.batch(
[
"INSERT INTO t VALUES (1)",
"SELECT invalid_column FROM t",
"INSERT INTO t VALUES (2)",
],
"write",
);
throw new Error("Expected batch to fail");
} catch (e: any) {
expect(e.name).toBe("LibsqlBatchError");
expect(e.statementIndex).toBe(1);
expect(e.code).toBeDefined();
}
}),
);

test(
"batch with a lot of different statements",
withClient(async (c) => {
Expand Down Expand Up @@ -1216,6 +1320,79 @@ describe("transaction()", () => {
await txn.commit();
}),
);

test(
"batch error reports statement index in transaction",
withClient(async (c) => {
const txn = await c.transaction("write");

try {
await txn.batch([
"DROP TABLE IF EXISTS t",
"CREATE TABLE t (a UNIQUE)",
"INSERT INTO t VALUES (1), (2), (3)",
"INSERT INTO t VALUES (1)", // Duplicate, will fail at index 3
"INSERT INTO t VALUES (4), (5)",
]);
throw new Error("Expected batch to fail");
} catch (e: any) {
expect(e.name).toBe("LibsqlBatchError");
expect(e.statementIndex).toBe(3);
expect(e.code).toBeDefined();
}

// Transaction should still be usable after batch error
const rs = await txn.execute("SELECT SUM(a) FROM t");
expect(rs.rows[0][0]).toBe(6);

await txn.commit();
}),
);

test(
"batch error reports statement index - error at first statement in transaction",
withClient(async (c) => {
const txn = await c.transaction("read");

try {
await txn.batch([
"SELECT invalid_column",
"SELECT 1",
"SELECT 2",
]);
throw new Error("Expected batch to fail");
} catch (e: any) {
expect(e.name).toBe("LibsqlBatchError");
expect(e.statementIndex).toBe(0);
expect(e.code).toBeDefined();
}

txn.close();
}),
);

test(
"batch error reports statement index - error at middle statement in transaction",
withClient(async (c) => {
const txn = await c.transaction("read");

try {
await txn.batch([
"SELECT 1",
"SELECT 2",
"SELECT invalid_column",
"SELECT 3",
]);
throw new Error("Expected batch to fail");
} catch (e: any) {
expect(e.name).toBe("LibsqlBatchError");
expect(e.statementIndex).toBe(2);
expect(e.code).toBeDefined();
}

txn.close();
}),
);
});

(hasHrana2 ? describe : describe.skip)("executeMultiple()", () => {
Expand Down
78 changes: 60 additions & 18 deletions packages/libsql-client/src/hrana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
TransactionMode,
InArgs,
} from "@libsql/core/api";
import { LibsqlError } from "@libsql/core/api";
import { LibsqlError, LibsqlBatchError } from "@libsql/core/api";
import type { SqlCache } from "./sql_cache.js";
import { transactionModeToBegin, ResultSetImpl } from "@libsql/core/util";

Expand Down Expand Up @@ -134,16 +134,37 @@ export abstract class HranaTransaction implements Transaction {
}

const resultSets = [];
for (const rowsPromise of rowsPromises) {
const rows = await rowsPromise;
if (rows === undefined) {
throw new LibsqlError(
"Statement in a transaction was not executed, " +
"probably because the transaction has been rolled back",
"TRANSACTION_CLOSED",
);
for (let i = 0; i < rowsPromises.length; i++) {
try {
const rows = await rowsPromises[i];
if (rows === undefined) {
throw new LibsqlBatchError(
"Statement in a transaction was not executed, " +
"probably because the transaction has been rolled back",
i,
"TRANSACTION_CLOSED",
);
}
resultSets.push(resultSetFromHrana(rows));
} catch (e) {
if (e instanceof LibsqlBatchError) {
throw e;
}
// Map hrana errors to LibsqlError first, then wrap in LibsqlBatchError
const mappedError = mapHranaError(e);
if (mappedError instanceof LibsqlError) {
throw new LibsqlBatchError(
mappedError.message,
i,
mappedError.code,
mappedError.rawCode,
mappedError.cause instanceof Error
? mappedError.cause
: undefined,
);
}
throw mappedError;
}
resultSets.push(resultSetFromHrana(rows));
}
return resultSets;
} catch (e) {
Expand Down Expand Up @@ -295,15 +316,36 @@ export async function executeHranaBatch(

const resultSets = [];
await beginPromise;
for (const stmtPromise of stmtPromises) {
const hranaRows = await stmtPromise;
if (hranaRows === undefined) {
throw new LibsqlError(
"Statement in a batch was not executed, probably because the transaction has been rolled back",
"TRANSACTION_CLOSED",
);
for (let i = 0; i < stmtPromises.length; i++) {
try {
const hranaRows = await stmtPromises[i];
if (hranaRows === undefined) {
throw new LibsqlBatchError(
"Statement in a batch was not executed, probably because the transaction has been rolled back",
i,
"TRANSACTION_CLOSED",
);
}
resultSets.push(resultSetFromHrana(hranaRows));
} catch (e) {
if (e instanceof LibsqlBatchError) {
throw e;
}
// Map hrana errors to LibsqlError first, then wrap in LibsqlBatchError
const mappedError = mapHranaError(e);
if (mappedError instanceof LibsqlError) {
throw new LibsqlBatchError(
mappedError.message,
i,
mappedError.code,
mappedError.rawCode,
mappedError.cause instanceof Error
? mappedError.cause
: undefined,
);
}
throw mappedError;
}
resultSets.push(resultSetFromHrana(hranaRows));
}
await commitPromise;

Expand Down
Loading
Loading