Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Some of the big supported features:
- Custom tokenizers
- Load runtime extensions
- JSONB support
- Native query interruption via `db.interrupt()`

It also contains a simple [Key-Value store](https://op-engineering.github.io/op-sqlite/docs/key_value_storage) you can use without adding one more dependency to your app.

Expand Down
18 changes: 18 additions & 0 deletions cpp/DBHostObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,24 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) {
return {};
});

function_map["interrupt"] = HFN(this) {
if (invalidated) {
throw std::runtime_error("[op-sqlite][interrupt] database is closed");
}

#ifdef OP_SQLITE_USE_LIBSQL
throw std::runtime_error("[op-sqlite][interrupt] sqlite3_interrupt is not "
"supported with libsql");
#else
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are missing turso

if (db == nullptr) {
throw std::runtime_error("[op-sqlite][interrupt] database is null");
}

sqlite3_interrupt(db);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also probably want to call interrupt on close() and delete()?

return {};
#endif
});

function_map["delete"] = HFN(this) {
invalidated = true;

Expand Down
20 changes: 20 additions & 0 deletions docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ On web, `execute()` runs the full SQL string passed to it.
On native, `execute()` currently runs only the first prepared statement.
If you need identical behavior across platforms, avoid multi-statement SQL strings.

## Interrupting a Query

On native, `interrupt()` aborts any pending database operation on this connection. It is safe to call from a thread different from the one running the operation. The interrupted query returns `SQLITE_INTERRUPT`; any in-flight transaction is rolled back. This calls SQLite's native [`sqlite3_interrupt()`](https://sqlite.org/c3ref/interrupt.html).

```tsx
const query = db.execute(longRunningQuery);

setTimeout(() => {
db.interrupt();
}, 100);

try {
await query;
} catch (error) {
// SQLITE_INTERRUPT
}
```

On web, `interrupt()` is not supported.

### Execute with Host Objects

It’s possible to return HostObjects when using a query. The benefit is that HostObjects are only created in C++ and only when you try to access a value inside of them a C++ value → JS value conversion happens. This means creation is fast, property access is slow. The use case is clear if you are returning **massive** amount of objects but only displaying/accessing a few of them at the time.
Expand Down
50 changes: 50 additions & 0 deletions example/src/tests/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,56 @@ describe("Queries tests", () => {
}
});

it("interrupt is safe to call with no in-flight query", () => {
if (isLibsql() || isTurso()) {
return;
}

let threw = false;
try {
db.interrupt();
} catch (_e) {
threw = true;
}

expect(threw).toEqual(false);
});

it("interrupt aborts an in-flight query and rolls back the transaction", async () => {
if (isLibsql() || isTurso()) {
return;
}

await db.execute("DROP TABLE IF EXISTS InterruptTest;");
await db.execute("CREATE TABLE InterruptTest (n INTEGER);");

const longQuery = `
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 100000000
)
INSERT INTO InterruptTest SELECT n FROM seq;
`;

const queryPromise = db.execute(longQuery);

await new Promise((resolve) => setTimeout(resolve, 50));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a sleep function somewhere on the test suite, please use that

db.interrupt();

let interrupted = false;
try {
await queryPromise;
} catch (e: any) {
interrupted = /interrupt|interrupted|abort|code 9|SQLITE_INTERRUPT/i.test(
String(e?.message ?? e),
);
}

expect(interrupted).toEqual(true);

const count = await db.execute("SELECT COUNT(*) AS n FROM InterruptTest;");
expect(count.rows[0]!.n).toEqual(0);
});

it("executeSync", () => {
const res = db.executeSync("SELECT 1");
expect(res.rowsAffected).toEqual(0);
Expand Down
1 change: 1 addition & 0 deletions src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function enhanceDB(db: _InternalDB, options: DBParams): DB {
setReservedBytes: db.setReservedBytes,
getReservedBytes: db.getReservedBytes,
close: db.close,
interrupt: db.interrupt,
closeAsync: async () => {
db.close();
},
Expand Down
4 changes: 4 additions & 0 deletions src/functions.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ function enhanceWebDb(
closeAsync: async () => {
await db.closeAsync?.();
},
interrupt: unsupported("interrupt"),
delete: unsupported("delete"),
attach: unsupported("attach"),
detach: unsupported("detach"),
Expand Down Expand Up @@ -352,6 +353,9 @@ async function createWebDb(params: {
dbId,
});
},
interrupt: () => {
throwSyncApiError("interrupt");
},
delete: () => {
throwSyncApiError("delete");
},
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export type PreparedStatement = {
export type _InternalDB = {
close: () => void;
closeAsync?: () => Promise<void>;
interrupt: () => void;
delete: (location?: string) => void;
attach: (params: {
secondaryDbFileName: string;
Expand Down Expand Up @@ -153,6 +154,14 @@ export type _InternalDB = {
export type DB = {
close: () => void;
closeAsync: () => Promise<void>;
/**
* Aborts any pending database operation on this connection.
*
* Calls SQLite's native sqlite3_interrupt(). Safe to call from a thread
* different from the one running the operation. An interrupted operation
* returns SQLITE_INTERRUPT and any in-flight transaction is rolled back.
*/
interrupt: () => void;
delete: (location?: string) => void;
attach: (params: {
secondaryDbFileName: string;
Expand Down