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
38 changes: 17 additions & 21 deletions cpp/libsql/OPLibsqlBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -750,27 +750,23 @@ opsqlite_libsql_execute_batch(DB const &db,
throw std::runtime_error("No SQL commands provided");
}

try {
int affectedRows = 0;
// opsqlite_libsql_execute(db, "BEGIN EXCLUSIVE TRANSACTION", nullptr);
for (int i = 0; i < commandCount; i++) {
auto command = commands->at(i);
// We do not provide a datastructure to receive query data because
// we don't need/want to handle this results in a batch execution
auto result = opsqlite_libsql_execute(db, command.sql, &command.params);
affectedRows += result.affectedRows;
}
// opsqlite_libsql_execute(db, "COMMIT", nullptr);
return BatchResult{
.affectedRows = affectedRows,
.commands = static_cast<int>(commandCount),
};
} catch (std::exception &exc) {
// opsqlite_libsql_execute(db, "ROLLBACK", nullptr);
return BatchResult{
.message = exc.what(),
};
}
int affectedRows = 0;
// Transaction control (BEGIN/COMMIT/ROLLBACK) is left to the JS side, so
// any exception here must propagate to reject the JS promise instead of
// being swallowed - otherwise the wrapping COMMIT would persist a partial
// batch instead of the ROLLBACK the caller expects.
for (int i = 0; i < commandCount; i++) {
auto command = commands->at(i);
// We do not provide a datastructure to receive query data because
// we don't need/want to handle this results in a batch execution
auto result = opsqlite_libsql_execute(db, command.sql, &command.params);
affectedRows += result.affectedRows;
}

return BatchResult{
.affectedRows = affectedRows,
.commands = static_cast<int>(commandCount),
};
}

} // namespace opsqlite
8 changes: 8 additions & 0 deletions docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ const res = await db.executeBatch(commands);
console.log(`Batch affected ${result.rowsAffected} rows`);
```

`executeBatch` runs the `BEGIN`/`COMMIT`/`ROLLBACK` statements that wrap the batch asynchronously, off the JS thread. For very large batches the `COMMIT` is where SQLite actually writes the WAL frames/fsyncs, so this keeps the JS thread free while that happens.

If you need those transaction boundaries to run synchronously on the JS thread instead, use `executeBatchSync`, which has the same signature and behavior otherwise:

```tsx
const res = await db.executeBatchSync(commands);
```

In some scenarios, dynamic applications may need to get some metadata information about the returned result set.

## Blob support
Expand Down
90 changes: 90 additions & 0 deletions example/src/tests/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,96 @@ describe("Queries tests", () => {
]);
});

it("executeBatchSync", async () => {
const id1 = chance.integer();
const name1 = chance.name();
const age1 = chance.integer();
const networth1 = chance.floating();

const id2 = chance.integer();
const name2 = chance.name();
const age2 = chance.integer();
const networth2 = chance.floating();

const commands: SQLBatchTuple[] = [
['SELECT * FROM "User"', []],
['SELECT * FROM "User"'],
[
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
[id1, name1, age1, networth1],
],
[
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
[[id2, name2, age2, networth2]],
],
];

await db.executeBatchSync(commands);

const res = await db.execute("SELECT * FROM User");

expect(res.rows).toDeepEqual([
{ id: id1, name: name1, age: age1, networth: networth1, nickname: null },
{
id: id2,
name: name2,
age: age2,
networth: networth2,
nickname: null,
},
]);
});

it("executeBatch rolls back on error", async () => {
const id1 = chance.integer();
const name1 = chance.name();
const age1 = chance.integer();
const networth1 = chance.floating();

const commands: SQLBatchTuple[] = [
[
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
[id1, name1, age1, networth1],
],
["INSERT INTO [tableThatDoesNotExist] (id) VALUES(1)"],
];

try {
await db.executeBatch(commands);
throw new Error("Should not resolve");
} catch (e) {
expect(((e as Error)?.message?.length ?? 0) > 0).toBe(true);
}

const res = await db.execute("SELECT * FROM User");
expect(res.rows).toDeepEqual([]);
});

it("executeBatchSync rolls back on error", async () => {
const id1 = chance.integer();
const name1 = chance.name();
const age1 = chance.integer();
const networth1 = chance.floating();

const commands: SQLBatchTuple[] = [
[
'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)',
[id1, name1, age1, networth1],
],
["INSERT INTO [tableThatDoesNotExist] (id) VALUES(1)"],
];

try {
await db.executeBatchSync(commands);
throw new Error("Should not resolve");
} catch (e) {
expect(((e as Error)?.message?.length ?? 0) > 0).toBe(true);
}

const res = await db.execute("SELECT * FROM User");
expect(res.rows).toDeepEqual([]);
});

it("Batch execute with BLOB", async () => {
const db = open({
name: "queries.sqlite",
Expand Down
33 changes: 33 additions & 0 deletions src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,39 @@ function enhanceDB(db: _InternalDB, options: DBParams): DB {
},
flushPendingReactiveQueries: db.flushPendingReactiveQueries,
executeBatch: async (commands: SQLBatchTuple[]): Promise<BatchQueryResult> => {
async function run() {
try {
await enhancedDb.execute("BEGIN TRANSACTION;");

const res = await db.executeBatch(commands as any[]);

await enhancedDb.execute("COMMIT;");

await db.flushPendingReactiveQueries();

return res;
} catch (executionError) {
await enhancedDb.execute("ROLLBACK;");

throw executionError;
} finally {
lock.inProgress = false;
startNextTransaction();
}
}

return await new Promise((resolve, reject) => {
const tx: _PendingTransaction = {
start: () => {
run().then(resolve).catch(reject);
},
};

lock.queue.push(tx);
startNextTransaction();
});
},
executeBatchSync: async (commands: SQLBatchTuple[]): Promise<BatchQueryResult> => {
async function run() {
try {
enhancedDb.executeSync("BEGIN TRANSACTION;");
Expand Down
4 changes: 4 additions & 0 deletions src/functions.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ function enhanceWebDb(db: _InternalDB, options: { name?: string; location?: stri
rowsAffected: 0,
};
},
// Web has no synchronous native APIs, so there is no distinct blocking behavior to offer.
executeBatchSync: async (commands: SQLBatchTuple[]): Promise<BatchQueryResult> => {
return enhancedDb.executeBatch(commands);
},
loadFile: async (_location: string): Promise<FileLoadResult> => {
throw new Error("[op-sqlite] loadFile() is not supported on web.");
},
Expand Down
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,24 @@ export type DB = {
* Executes all the queries in the params inside a single transaction
*
* It's faster than executing single queries as data is sent to the native side only once
*
* The BEGIN/COMMIT/ROLLBACK statements that wrap the batch are executed asynchronously,
* off the JS thread. Use this over `executeBatchSync` unless you specifically need the
* transaction boundaries to block the JS thread.
* @param commands
* @returns Promise<BatchQueryResult>
*/
executeBatch: (commands: SQLBatchTuple[]) => Promise<BatchQueryResult>;
/**
* Same as `executeBatch` but the BEGIN/COMMIT/ROLLBACK statements that wrap the batch
* are executed synchronously on the JS thread. For large batches this can block the JS
* thread for a noticeable amount of time (the COMMIT is where SQLite writes the WAL
* frames/fsyncs), so prefer `executeBatch` unless you have a specific reason to need
* synchronous transaction boundaries.
* @param commands
* @returns Promise<BatchQueryResult>
*/
executeBatchSync: (commands: SQLBatchTuple[]) => Promise<BatchQueryResult>;
/**
* Loads a SQLite Dump from disk. It will be the fastest way to execute a large set of queries as no JS is involved
*/
Expand Down
Loading