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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2022 DjDeveloperr
Copyright 2026 DjDeveloperr

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![License](https://img.shields.io/github/license/denodrivers/sqlite3)](https://github.com/denodrivers/sqlite3/blob/master/LICENSE)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/DjDeveloperr)

The fastest and correct module for SQLite3 in Deno.
The fastest and correct SQLite3 client for Deno.

## Example

Expand Down Expand Up @@ -92,4 +92,4 @@ DENO_SQLITE_LOCAL=1 deno task bench

Apache-2.0. Check [LICENSE](./LICENSE) for details.

Copyright © 2023 DjDeveloperr
Copyright © 2026 DjDeveloperr
90 changes: 90 additions & 0 deletions doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,46 @@ const db = new Database("test.db", { create: false });
a dynamic library, this needs to be set to true for the method `loadExtension`
to work. Defaults to `false`.

## Exporting Database Bytes

Use `db.export()` to serialize a schema into a `Uint8Array`.

For on-disk databases, this is equivalent to the database file bytes. For
in-memory databases, it is the byte sequence that would be written to disk.

```ts
const db = new Database(":memory:");
db.exec("create table test (id integer primary key, value text)");
db.exec("insert into test (value) values (?)", "hello");

const bytes = db.export();
console.log(bytes.byteLength);
```

You can optionally pass a schema name, such as `"main"` or an attached database
name:

```ts
const mainBytes = db.export("main");
```

## Getting Database Size

Use `db.size()` to get the serialized size of a schema in bytes.

```ts
const db = new Database(":memory:");
db.exec("create table test (id integer primary key, value text)");

console.log(db.size()); // serialized byte size
```

As with `export()`, you may pass a schema name:

```ts
const mainSize = db.size("main");
```

## Loading extensions

Loading SQLite3 extensions is enabled through the `enableLoadExtension` property
Expand Down Expand Up @@ -197,6 +237,9 @@ execute the statement, and return an array of rows as objects.
const rows = stmt.all(...params);
```

`all()` loads the entire result set into memory. For large result sets, prefer
iterating the statement row by row instead.

To get rows in array form, use `values()` method.

```ts
Expand Down Expand Up @@ -305,11 +348,31 @@ memory at once. Since it does not accept any parameters, you must bind the
parameters before iterating using `bind` method.

```ts
const stmt = db.prepare("SELECT * FROM logs WHERE created_at >= ?");
stmt.bind("2026-01-01");

for (const row of stmt) {
console.log(row);
}
```

This processes rows lazily, one row at a time, without building a large array
first.

You can also call `iter(...params)` directly when you want to provide parameters
for that specific iteration.

```ts
const stmt = db.prepare("SELECT * FROM logs WHERE created_at >= ?");

for (const row of stmt.iter("2026-01-01")) {
console.log(row);
}
```

Use `all()` when you explicitly want an in-memory array. Use `for...of` or
`iter()` when you want to stream through a large dataset.

## Transactions

To start a transaction, use the `transaction()` method. This method takes a
Expand Down Expand Up @@ -349,6 +412,33 @@ runTransaction.deferred([
]);
```

## Update hooks

Use `setUpdateHook()` to observe row-level `INSERT`, `UPDATE`, and `DELETE`
operations on the current database connection.

Pass a callback to enable the hook:

```ts
db.setUpdateHook((type, dbName, tableName, rowId) => {
console.log({ type, dbName, tableName, rowId });
});
```

The callback receives:

- `type: number` - SQLite update type. Insert is `18`, delete is `9`, and update
is `23`.
- `dbName: string` - Database name, usually `"main"`.
- `tableName: string` - Name of the table that changed.
- `rowId: bigint` - Row ID of the affected row.

Pass `null` to remove the current hook:

```ts
db.setUpdateHook(null);
```

## Binding Parameters

Parameters can be bound both by name and positiion. To bind by name, just pass
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export const SQLITE3_PREPARE_PERSISTENT = 0x00000001;
export const SQLITE3_PREPARE_NORMALIZE = 0x00000002;
export const SQLITE3_PREPARE_NO_VTAB = 0x00000004;

// Serialize Flags
export const SQLITE_SERIALIZE_NOCOPY = 0x001;

// Fundamental Datatypes
export const SQLITE_INTEGER = 1;
export const SQLITE_FLOAT = 2;
Expand Down
129 changes: 129 additions & 0 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SQLITE_FLOAT,
SQLITE_INTEGER,
SQLITE_NULL,
SQLITE_SERIALIZE_NOCOPY,
SQLITE_TEXT,
} from "./constants.ts";
import { readCstr, toCString, unwrap } from "./util.ts";
Expand Down Expand Up @@ -87,6 +88,7 @@ const {
sqlite3_free,
sqlite3_libversion,
sqlite3_sourceid,
sqlite3_serialize,
sqlite3_complete,
sqlite3_finalize,
sqlite3_result_blob,
Expand All @@ -110,6 +112,7 @@ const {
sqlite3_backup_step,
sqlite3_backup_finish,
sqlite3_errcode,
sqlite3_update_hook,
} = ffi;

/** SQLite version string */
Expand All @@ -126,6 +129,12 @@ export function isComplete(statement: string): boolean {
return Boolean(sqlite3_complete(toCString(statement)));
}

export enum SqliteUpdateType {
SQLITE_INSERT = 18,
SQLITE_DELETE = 9,
SQLITE_UPDATE = 23,
}

const BIG_MAX = BigInt(Number.MAX_SAFE_INTEGER);

/**
Expand Down Expand Up @@ -778,6 +787,68 @@ export class Database {
unwrap(result, this.#handle);
}

#updateHook?: Deno.UnsafeCallback<{
readonly parameters: readonly [
"pointer",
"i32",
"pointer",
"pointer",
"i64",
];
readonly result: "void";
}>;

/**
* Sets a callback function that is invoked whenever a row is updated, inserted or deleted.
*
* The callback function receives the type of update (insert, update, or delete), the database name, the table name, and the row ID of the row being modified.
*
* Example:
* ```ts
* db.setUpdateHook((type, dbName, tableName, rowId) => {
* console.log(`Row with ID ${rowId} in table ${tableName} was modified in database ${dbName}. Update type: ${type}`);
* });
* ```
*/
setUpdateHook(
hook:
| ((
type: SqliteUpdateType,
dbName: string,
tableName: string,
rowId: bigint,
) => void)
| null,
): void {
if (hook === null) {
sqlite3_update_hook(this.#handle, null, null);
if (this.#updateHook) {
this.#updateHook.close();
this.#updateHook = undefined;
}
return;
}

const updateHook = new Deno.UnsafeCallback(
{
parameters: ["pointer", "i32", "pointer", "pointer", "i64"],
result: "void",
} as const,
(_, type, pDbName, pTableName, rowId) => {
const dbName = readCstr(pDbName!);
const tableName = readCstr(pTableName!);
hook(type, dbName, tableName, rowId);
},
);

sqlite3_update_hook(this.#handle, updateHook.pointer, null);

if (this.#updateHook) {
this.#updateHook.close();
}
this.#updateHook = updateHook;
}

/**
* Closes the database connection.
*
Expand All @@ -794,6 +865,9 @@ export class Database {
for (const cb of this.#callbacks) {
cb.close();
}
if (this.#updateHook) {
this.#updateHook.close();
}
unwrap(sqlite3_close_v2(this.#handle));
this.#open = false;
}
Expand All @@ -818,6 +892,61 @@ export class Database {
}
}

#serialize(name: string, flags: number): [Deno.PointerValue, number] {
if (sqlite3_serialize === null) {
throw new Error(
"Database serialization is not supported by the shared library that was used.",
);
}

const size = new BigInt64Array(1);
const ptr = sqlite3_serialize(this.#handle, toCString(name), size, flags);
const bytes = size[0];

if (bytes < 0) {
throw new Error("Failed to serialize database");
}

if (bytes > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new RangeError("Database is too large to represent in JavaScript");
}

return [ptr, Number(bytes)];
}

/**
* Export a database schema as serialized bytes.
*
* For on-disk databases this is equivalent to the database file contents.
* For in-memory databases this is the same byte sequence that would be
* written if the database were backed up to disk.
*
* @param name Schema name to export. Defaults to "main".
*/
export(name = "main"): Uint8Array {
const [ptr, size] = this.#serialize(name, 0);
if (ptr === null) {
throw new Error("Failed to serialize database");
}

try {
return new Uint8Array(
Deno.UnsafePointerView.getArrayBuffer(ptr, size).slice(0),
);
} finally {
sqlite3_free(ptr);
}
}

/**
* Get the serialized size of a database schema in bytes.
*
* @param name Schema name to measure. Defaults to "main".
*/
size(name = "main"): number {
return this.#serialize(name, SQLITE_SERIALIZE_NOCOPY)[1];
}

[Symbol.for("Deno.customInspect")](): string {
return `SQLite3.Database { path: ${this.path} }`;
}
Expand Down
Loading
Loading