Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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,
}
Comment thread
DjDeveloperr marked this conversation as resolved.

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);
Comment thread
DjDeveloperr marked this conversation as resolved.

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