Compare commits

..

No commits in common. "main" and "v3.0.1" have entirely different histories.
main ... v3.0.1

12 changed files with 16 additions and 411 deletions

View File

@ -40,7 +40,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
version: 10.18.1
version: 10.3.0
- name: Get Node version from .nvmrc
id: get-nvm-version
@ -90,7 +90,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
version: 10.18.1
version: 10.3.0
- name: Get Node version from .nvmrc
id: get-nvm-version
@ -137,7 +137,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
version: 10.18.1
version: 10.3.0
- name: Setup node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:

View File

@ -30,7 +30,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
version: 10.18.1
version: 10.3.0
- name: Setup node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:

1
.gitignore vendored
View File

@ -7,4 +7,3 @@ docs/
.tmp/
.eslintcache
todo.md
.vscode

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
#
FROM ubuntu:jammy-20250714@sha256:1ec65b2719518e27d4d25f104d93f9fac60dc437f81452302406825c46fcc9cb
FROM ubuntu:focal-20240530@sha256:fa17826afb526a9fc7250e0fbcbfd18d03fe7a54849472f86879d8bf562c629e
# Avoid getting prompted to configure things during installation.
ENV DEBIAN_FRONTEND=noninteractive
@ -15,15 +15,13 @@ COPY docker/apt.conf docker/sources.list /etc/apt/
# But we can't install it because it doesn't trust our mirror!
# Temporarily disables APT's certificate signature checking
# to download the certificates.
RUN apt update -oAcquire::https::Verify-Peer=false
RUN apt install -oAcquire::https::Verify-Peer=false -y ca-certificates
RUN apt-get update -oAcquire::https::Verify-Peer=false \
&& apt-get install -oAcquire::https::Verify-Peer=false -y ca-certificates
# Back to normal, verification back on
# Install only what's needed to set up Rust and Node.
# We'll install additional tools at the end to take advantage of Docker's caching of earlier steps.
RUN apt update
RUN apt install -y apt-transport-https xz-utils unzip
RUN apt-get update && apt-get install -y apt-transport-https xz-utils unzip
# User-specific setup!
@ -67,9 +65,6 @@ RUN tar -xf node.tar.xz \
ENV PATH="/home/sqlcipher/node/bin:${PATH}"
# Install pnpm
RUN npm install -g pnpm@10.18.1
# And finally any bonus packages we're going to need
# Note that we jump back to root for this.
USER root

View File

@ -1,3 +1,3 @@
deb [snapshot=20250811T060900Z] http://archive.ubuntu.com/ubuntu/ jammy main universe
deb [snapshot=20250811T060900Z] http://archive.ubuntu.com/ubuntu/ jammy-updates main universe
deb [snapshot=20250811T060900Z] http://security.ubuntu.com/ubuntu jammy-security main universe
deb [snapshot=20240829T060900Z] http://archive.ubuntu.com/ubuntu/ focal main universe
deb [snapshot=20240829T060900Z] http://archive.ubuntu.com/ubuntu/ focal-updates main universe
deb [snapshot=20240829T060900Z] http://security.ubuntu.com/ubuntu focal-security main universe

View File

@ -47,16 +47,6 @@ const addon = bindings<{
databaseInitTokenizer(db: NativeDatabase): void;
databaseExec(db: NativeDatabase, query: string): void;
databaseClose(db: NativeDatabase): void;
databaseCreateFunction(
db: NativeDatabase,
name: string,
fn: (...args: ReadonlyArray<unknown>) => void,
bigint: boolean,
): void;
databaseSetWalHook(
db: NativeDatabase,
fn: (dbName: string, pageCount: number) => void,
): void;
signalTokenize(value: string): Array<string>;
@ -112,7 +102,7 @@ export type StatementParameters<Options extends StatementOptions> =
*/
export type SqliteValue<Options extends StatementOptions> =
| string
| Uint8Array<ArrayBuffer>
| Uint8Array
| number
| null
| (Options extends { bigint: true } ? bigint : never);
@ -126,14 +116,6 @@ export type RowType<Options extends StatementOptions> = Options extends {
? SqliteValue<Options>
: Record<string, SqliteValue<Options>>;
export type FunctionOptions = Readonly<{
/**
* If `true` - all integers passed to the fucntion will be big
* integers instead of regular (floating-point) numbers.
*/
bigint?: boolean;
}>;
/**
* A compiled SQL statement class.
*/
@ -413,13 +395,6 @@ export type DatabaseOptions = Readonly<{
cacheStatements?: boolean;
}>;
/**
* @param dbName - The name of the database that was written to.
* @param pageCount - The number of pages currently in the write-ahead log file,
* including those that were just committed.
*/
export type WalHook = (dbName: string, pageCount: number) => void;
/**
* A sqlite database class.
*/
@ -477,51 +452,6 @@ export default class Database {
addon.databaseExec(this.#native, sql);
}
/**
* Create custom SQL function with a given `name`.
*
* @param name - name of the function
* @param fn - function implementation
* @param options - function options.
*/
public createFunction(
name: string,
fn: (...args: ReadonlyArray<unknown>) => void,
options: FunctionOptions = {},
): void {
if (this.#native === undefined) {
throw new Error('Database closed');
}
if (typeof name !== 'string') {
throw new TypeError('Invalid name argument');
}
if (typeof fn !== 'function') {
throw new TypeError('Invalid fn argument');
}
addon.databaseCreateFunction(
this.#native,
name,
fn,
options.bigint === true,
);
}
/**
* Register a callback to be invoked each time data is commited to a database
* in WAL mode.
*
* @param fn - function implementation
*/
public setWalHook(fn: WalHook): void {
if (this.#native === undefined) {
throw new Error('Database closed');
}
if (typeof fn !== 'function') {
throw new TypeError('Invalid fn argument');
}
addon.databaseSetWalHook(this.#native, fn);
}
/**
* Compile a single SQL statement.
*

View File

@ -1,7 +1,6 @@
{
"packageManager": "pnpm@10.18.1",
"name": "@signalapp/sqlcipher",
"version": "3.3.5",
"version": "3.0.1",
"description": "A fast N-API-based Node.js addon wrapping sqlcipher and FTS5 segmenting APIs",
"homepage": "http://github.com/signalapp/node-sqlcipher.git",
"license": "AGPL-3.0-only",

View File

@ -1 +1 @@
nightly-2025-09-24
nightly-2025-02-25

View File

@ -74,122 +74,6 @@ static Napi::Value SignalTokenize(const Napi::CallbackInfo& info) {
return result;
}
// Functions
class FunctionWrap {
public:
FunctionWrap(Napi::Function fn, bool is_bigint) : is_bigint_(is_bigint) {
fn_.Reset(fn, 1);
}
static void Run(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
auto wrap = static_cast<FunctionWrap*>(sqlite3_user_data(ctx));
wrap->Call(ctx, argc, argv);
}
static void Final(void* p_app) { delete static_cast<FunctionWrap*>(p_app); }
protected:
void Call(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
auto env = fn_.Env();
Napi::HandleScope scope(env);
assert(argc >= 0);
auto args = std::vector<Napi::Value>(static_cast<size_t>(argc));
for (int i = 0; i < argc; i++) {
args[i] = TranslateValue(argv[i]);
}
auto result = fn_.Value().Call(args);
// Ignore exceptions
if (result.IsEmpty()) {
auto e = env.GetAndClearPendingException();
sqlite3_result_error(ctx, e.Message().c_str(), SQLITE_ERROR);
} else if (result.IsUndefined()) {
sqlite3_result_null(ctx);
} else {
sqlite3_result_error(ctx, "Function must not return a value",
SQLITE_ERROR);
}
}
Napi::Value TranslateValue(sqlite3_value* value) {
auto env = fn_.Env();
int type = sqlite3_value_type(value);
switch (type) {
case SQLITE_INTEGER: {
auto val = sqlite3_value_int64(value);
if (is_bigint_) {
return Napi::BigInt::New(env, static_cast<int64_t>(val));
}
if (static_cast<int64_t>(INT32_MIN) <= val &&
val <= static_cast<int64_t>(INT32_MAX)) {
napi_value n_value;
NAPI_THROW_IF_FAILED(
env, napi_create_int32(env, static_cast<int32_t>(val), &n_value),
Napi::Value());
return Napi::Value(env, n_value);
} else {
return Napi::Number::New(env, val);
}
}
case SQLITE_TEXT:
return Napi::String::New(
env, reinterpret_cast<const char*>(sqlite3_value_text(value)),
sqlite3_value_bytes(value));
case SQLITE_FLOAT:
return Napi::Number::New(env, sqlite3_value_double(value));
case SQLITE_BLOB:
return Napi::Buffer<uint8_t>::Copy(
env, reinterpret_cast<const uint8_t*>(sqlite3_value_blob(value)),
sqlite3_value_bytes(value));
case SQLITE_NULL:
return env.Null();
}
return Napi::Value();
}
private:
Napi::Reference<Napi::Function> fn_;
bool is_bigint_;
};
// WAL Hook
class WalHookWrap {
public:
explicit WalHookWrap(Napi::Function fn) { fn_.Reset(fn, 1); }
static int Run(void* p_app, sqlite3* _db, const char* db_name, int n_pages) {
auto wrap = static_cast<WalHookWrap*>(p_app);
wrap->Call(db_name, n_pages);
return SQLITE_OK;
}
protected:
void Call(const char* db_name, int n_pages) {
auto env = fn_.Env();
Napi::HandleScope scope(env);
auto result = fn_.Value().Call({
Napi::String::New(env, db_name),
Napi::Number::New(env, n_pages),
});
// Ignore exceptions
if (result.IsEmpty()) {
env.GetAndClearPendingException();
}
}
private:
Napi::Reference<Napi::Function> fn_;
};
// Global Settings
thread_local Napi::Reference<Napi::Function> logger_fn_;
@ -266,10 +150,6 @@ Napi::Object Database::Init(Napi::Env env, Napi::Object exports) {
Napi::Function::New(env, &Database::InitTokenizer);
exports["databaseClose"] = Napi::Function::New(env, &Database::Close);
exports["databaseExec"] = Napi::Function::New(env, &Database::Exec);
exports["databaseCreateFunction"] =
Napi::Function::New(env, &Database::CreateFunction);
exports["databaseSetWalHook"] =
Napi::Function::New(env, &Database::SetWalHook);
return exports;
}
@ -285,9 +165,6 @@ Database::~Database() {
return;
}
delete wal_hook_wrap_;
wal_hook_wrap_ = nullptr;
int r = sqlite3_close(handle_);
if (r != SQLITE_OK) {
fprintf(stderr, "Cleanup: sqlite3_close failure\n");
@ -379,9 +256,6 @@ Napi::Value Database::Close(const Napi::CallbackInfo& info) {
}
db->statements_.clear();
delete db->wal_hook_wrap_;
db->wal_hook_wrap_ = nullptr;
int r = sqlite3_close(db->handle_);
if (r != SQLITE_OK) {
return db->ThrowSqliteError(env, r);
@ -415,68 +289,6 @@ Napi::Value Database::Exec(const Napi::CallbackInfo& info) {
return Napi::Value();
}
Napi::Value Database::CreateFunction(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto db = FromExternal(info[0]);
auto name = info[1].As<Napi::String>();
auto fn = info[2].As<Napi::Function>();
auto is_bigint = info[3].As<Napi::Boolean>();
assert(name.IsString());
assert(fn.IsFunction());
assert(is_bigint.IsBoolean());
if (db == nullptr) {
return Napi::Value();
}
if (db->handle_ == nullptr) {
NAPI_THROW(Napi::Error::New(env, "Database closed"), Napi::Value());
}
auto name_utf8 = name.Utf8Value();
auto fn_wrap = new FunctionWrap(fn, is_bigint);
int r = sqlite3_create_function_v2(db->handle_, name_utf8.c_str(), -1,
SQLITE_UTF8, // TODO(indutny): or UTF16?
fn_wrap, FunctionWrap::Run, nullptr,
nullptr, FunctionWrap::Final);
if (r != SQLITE_OK) {
delete fn_wrap;
return db->ThrowSqliteError(env, r);
}
return Napi::Value();
}
Napi::Value Database::SetWalHook(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto db = FromExternal(info[0]);
auto fn = info[1].As<Napi::Function>();
assert(fn.IsFunction());
if (db == nullptr) {
return Napi::Value();
}
if (db->handle_ == nullptr) {
NAPI_THROW(Napi::Error::New(env, "Database closed"), Napi::Value());
}
auto wal_wrap = new WalHookWrap(fn);
delete db->wal_hook_wrap_;
db->wal_hook_wrap_ = wal_wrap;
sqlite3_wal_hook(db->handle_, WalHookWrap::Run, wal_wrap);
return Napi::Value();
}
Napi::Value Database::ThrowSqliteError(Napi::Env env, int error) {
assert(handle_ != nullptr);
const char* msg = sqlite3_errmsg(handle_);

View File

@ -9,7 +9,6 @@
#include "sqlite3.h"
class Statement;
class WalHookWrap;
class Database {
public:
@ -31,8 +30,6 @@ class Database {
static Napi::Value InitTokenizer(const Napi::CallbackInfo& info);
static Napi::Value Close(const Napi::CallbackInfo& info);
static Napi::Value Exec(const Napi::CallbackInfo& info);
static Napi::Value CreateFunction(const Napi::CallbackInfo& info);
static Napi::Value SetWalHook(const Napi::CallbackInfo& info);
fts5_api* GetFTS5API(Napi::Env env);
@ -47,8 +44,6 @@ class Database {
// All currently open statements for this database. Used to close all open
// statements when closing the database.
std::list<Statement*> statements_;
WalHookWrap* wal_hook_wrap_ = nullptr;
};
class AutoResetStatement {

View File

@ -1,7 +1,7 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { expect, test, describe, beforeEach, afterEach, vi } from 'vitest';
import { expect, test, beforeEach, afterEach } from 'vitest';
import Database from '../lib/index.js';
@ -65,80 +65,3 @@ test.each([[false], [true]])('ciphertext=%j', (ciphertext) => {
expect(row).toEqual({ name: 'Adam', value: 'Sandler' });
});
describe('setWalHook', () => {
beforeEach(() => {
db.pragma('journal_mode = WAL');
db.exec('CREATE TABLE t (a INTEGER)');
});
test('calls hook after WAL commit', () => {
const hook = vi.fn();
db.setWalHook(hook);
db.prepare('INSERT INTO t (a) VALUES (1)').run();
expect(hook).toHaveBeenCalledOnce();
expect(hook).toHaveBeenCalledWith('main', expect.any(Number));
});
test('hook receives page count > 0', () => {
let pageCount: number | null = null;
db.setWalHook((_dbName, n) => {
pageCount = n;
});
db.prepare('INSERT INTO t (a) VALUES (1)').run();
expect(pageCount).toBeGreaterThan(0);
});
test('hook fires once per commit', () => {
const hook = vi.fn();
db.setWalHook(hook);
db.transaction(() => {
db.prepare('INSERT INTO t (a) VALUES (1)').run();
db.prepare('INSERT INTO t (a) VALUES (2)').run();
})();
expect(hook).toHaveBeenCalledOnce();
});
test('replaces previous hook', () => {
const first = vi.fn();
const second = vi.fn();
db.setWalHook(first);
db.setWalHook(second);
db.prepare('INSERT INTO t (a) VALUES (1)').run();
expect(first).not.toHaveBeenCalled();
expect(second).toHaveBeenCalledOnce();
});
test('silently ignores exceptions thrown by hook', () => {
let called = false;
db.setWalHook(() => {
called = true;
throw new Error('hook error');
});
expect(() =>
db.prepare('INSERT INTO t (a) VALUES (1)').run(),
).not.toThrow();
expect(called).toBe(true);
});
test('throws when database is closed', () => {
db.close();
expect(() => db.setWalHook(vi.fn())).toThrowError('Database closed');
db = new Database(join(dir, 'db2.sqlite'));
});
test('throws for invalid argument', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => db.setWalHook(123 as any)).toThrowError('Invalid fn argument');
});
});

View File

@ -1,4 +1,4 @@
import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest';
import { describe, expect, test, beforeEach, afterEach } from 'vitest';
import Database, { setLogger } from '../lib/index.js';
@ -497,51 +497,3 @@ describe('statement cache', () => {
);
});
});
describe('custom function', () => {
let fnDb: Database;
let fn: ReturnType<typeof vi.fn>;
let bigFn: ReturnType<typeof vi.fn>;
beforeEach(() => {
fnDb = new Database(':memory:');
fn = vi.fn();
fnDb.createFunction('fn', fn);
bigFn = vi.fn();
fnDb.createFunction('bigFn', bigFn, {
bigint: true,
});
});
afterEach(() => {
fnDb.close();
});
test('it calls the function without args', () => {
fnDb.exec(`SELECT fn()`);
expect(fn).toHaveBeenCalledWith();
});
test('it calls the function with multiple args', () => {
fnDb.exec(`SELECT fn(1, '123', NULL)`);
expect(fn).toHaveBeenCalledWith(1, '123', null);
});
test('it calls the function with blob', () => {
fnDb.exec(`SELECT fn(x'abba')`);
expect(fn).toHaveBeenCalledWith(Buffer.from('abba', 'hex'));
});
test('it uses bigints when configured', () => {
fnDb.exec(`SELECT bigFn(123456)`);
expect(bigFn).toHaveBeenCalledWith(123456n);
});
test('it throws when function returns a value', () => {
fnDb.createFunction('intFn', () => {
return 1;
});
expect(() => fnDb.exec(`SELECT intFn()`)).toThrowError('SQLITE_ERROR');
});
});