Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

29 changed files with 7445 additions and 14604 deletions

View File

@ -20,7 +20,6 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [windows-latest, macos-latest] os: [windows-latest, macos-latest]
buildType: ['production', 'profiling']
include: include:
- os: macos-latest - os: macos-latest
target: arm64 target: arm64
@ -40,7 +39,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with: with:
version: 10.18.1 version: 10.3.0
- name: Get Node version from .nvmrc - name: Get Node version from .nvmrc
id: get-nvm-version id: get-nvm-version
@ -52,10 +51,6 @@ jobs:
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Update package.json
if: ${{ matrix.buildType == 'profiling' }}
run: pnpm version prerelease --no-git-tag-version --preid profiling
- name: Install node_modules - name: Install node_modules
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@ -73,54 +68,28 @@ jobs:
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with: with:
name: sqlcipher-${{matrix.buildType}}-${{matrix.os}} name: sqlcipher-${{matrix.os}}
path: prebuilds/* path: prebuilds/*
prebuild_linux: prebuild_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
strategy:
matrix:
buildType: ['production', 'profiling']
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
version: 10.18.1
- name: Get Node version from .nvmrc
id: get-nvm-version
shell: bash
run: echo "node-version=$(cat .nvmrc)" >> $GITHUB_OUTPUT
- name: Setup node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.nvmrc'
- name: Update package.json
if: ${{ matrix.buildType == 'profiling' }}
run: pnpm version prerelease --no-git-tag-version --preid profiling
- name: Build in docker container - name: Build in docker container
run: ./docker-prebuildify.sh run: ./docker-prebuildify.sh
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with: with:
name: sqlcipher-${{matrix.buildType}}-linux-latest name: sqlcipher-linux-latest
path: prebuilds/* path: prebuilds/*
publish: publish:
name: Publish name: Publish
permissions: permissions:
# Required for OIDC
id-token: 'write'
# Needed for ncipollo/release-action. # Needed for ncipollo/release-action.
contents: 'write' contents: 'write'
@ -137,20 +106,17 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with: with:
version: 10.18.1 version: 10.3.0
- name: Setup node.js - name: Setup node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org/' registry-url: 'https://registry.npmjs.org/'
- name: Update npm - name: Download built libraries
run: npm install -g npm@latest
- name: Download built production libraries
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.19.1 uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.19.1
with: with:
pattern: sqlcipher-production-* pattern: sqlcipher-*
path: prebuilds path: prebuilds
merge-multiple: true merge-multiple: true
@ -163,13 +129,14 @@ jobs:
- name: Lint - name: Lint
run: pnpm lint run: pnpm lint
- name: Production Tests - run: pnpm test
run: pnpm test
env: env:
PREBUILDS_ONLY: 1 PREBUILDS_ONLY: 1
- name: Publish production - name: Publish
run: pnpm publish --tag '${{ github.event.inputs.npm_tag }}' --access public --no-git-checks ${{ inputs.dry_run && '--dry-run' || ''}} run: pnpm publish --tag '${{ github.event.inputs.npm_tag }}' --access public --no-git-checks ${{ inputs.dry_run && '--dry-run' || ''}}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Rename symbols - name: Rename symbols
run: | run: |
@ -183,28 +150,8 @@ jobs:
# This step is expected to fail if not run on a tag. # This step is expected to fail if not run on a tag.
- name: Upload debug info to release - name: Upload debug info to release
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0 uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0
if: ${{ !inputs.dry_run }}
with: with:
allowUpdates: true allowUpdates: true
artifactErrorsFailBuild: true artifactErrorsFailBuild: true
artifacts: prebuilds/node_sqlcipher_*.sym artifacts: prebuilds/node_sqlcipher_*.sym
- name: Update package.json version to profiling
run: pnpm version prerelease --no-git-tag-version --preid profiling
- name: Remove production prebuilds
run: rm -rf prebuilds
- name: Download built profiling libraries
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.19.1
with:
pattern: sqlcipher-profiling-*
path: prebuilds
merge-multiple: true
- name: Profiling build tests
run: pnpm test
env:
PREBUILDS_ONLY: 1
- name: Publish profiling
run: pnpm publish --tag 'profiling' --access public --no-git-checks ${{ inputs.dry_run && '--dry-run' || ''}}

View File

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

1
.gitignore vendored
View File

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

2
.nvmrc
View File

@ -1 +1 @@
22.18.0 20.18.2

View File

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

View File

@ -37,7 +37,7 @@ cd deps/sqlcipher
export OPENSSL_PREFIX=`brew --prefix openssl` export OPENSSL_PREFIX=`brew --prefix openssl`
export CFLAGS="-I $OPENSSL_PREFIX/include" export CFLAGS="-I $OPENSSL_PREFIX/include"
export LIBRARY_PATH="$LIBRARY_PATH:$OPENSSL_PREFIX/lib" export LIBRARY_PATH="$LIBRARY_PATH:$OPENSSL_PREFIX/lib"
./update.sh v4.10.0 ./update.sh v4.6.1
cd - cd -
``` ```

View File

@ -2,8 +2,7 @@ import { Buffer } from 'node:buffer';
import { bench, describe } from 'vitest'; import { bench, describe } from 'vitest';
import BDatabase from '@signalapp/better-sqlite3'; import BDatabase from '@signalapp/better-sqlite3';
import { DatabaseSync as NDatabase } from 'node:sqlite'; import Database from '../lib/index.js';
import Database from '../dist/index.mjs';
const PREPARE = ` const PREPARE = `
CREATE TABLE t ( CREATE TABLE t (
@ -22,15 +21,12 @@ const DELETE = 'DELETE FROM t';
describe('INSERT INTO t', () => { describe('INSERT INTO t', () => {
const sdb = new Database(':memory:', { cacheStatements: true }); const sdb = new Database(':memory:', { cacheStatements: true });
const bdb = new BDatabase(':memory:'); const bdb = new BDatabase(':memory:');
const ndb = new NDatabase(':memory:');
sdb.exec(PREPARE); sdb.exec(PREPARE);
bdb.exec(PREPARE); bdb.exec(PREPARE);
ndb.exec(PREPARE);
const sinsert = sdb.prepare(INSERT); const sinsert = sdb.prepare(INSERT);
const binsert = bdb.prepare(INSERT); const binsert = bdb.prepare(INSERT);
const ninsert = ndb.prepare(INSERT);
bench( bench(
'@signalapp/sqlcipher', '@signalapp/sqlcipher',
@ -55,16 +51,4 @@ describe('INSERT INTO t', () => {
}, },
}, },
); );
bench(
'node:sqlite',
() => {
ninsert.run({ b: BLOB });
},
{
teardown: () => {
ndb.exec(DELETE);
},
},
);
}); });

View File

@ -1,8 +1,7 @@
import { bench, describe } from 'vitest'; import { bench, describe } from 'vitest';
import BDatabase from '@signalapp/better-sqlite3'; import BDatabase from '@signalapp/better-sqlite3';
import { DatabaseSync as NDatabase } from 'node:sqlite'; import Database from '../lib/index.js';
import Database from '../dist/index.mjs';
const PREPARE = ` const PREPARE = `
CREATE TABLE t ( CREATE TABLE t (
@ -25,15 +24,12 @@ const DELETE = 'DELETE FROM t';
describe('INSERT INTO t', () => { describe('INSERT INTO t', () => {
const sdb = new Database(':memory:', { cacheStatements: true }); const sdb = new Database(':memory:', { cacheStatements: true });
const bdb = new BDatabase(':memory:'); const bdb = new BDatabase(':memory:');
const ndb = new NDatabase(':memory:');
sdb.exec(PREPARE); sdb.exec(PREPARE);
bdb.exec(PREPARE); bdb.exec(PREPARE);
ndb.exec(PREPARE);
const sinsert = sdb.prepare(INSERT); const sinsert = sdb.prepare(INSERT);
const binsert = bdb.prepare(INSERT); const binsert = bdb.prepare(INSERT);
const ninsert = ndb.prepare(INSERT);
bench( bench(
'@signalapp/sqlcipher', '@signalapp/sqlcipher',
@ -58,16 +54,4 @@ describe('INSERT INTO t', () => {
}, },
}, },
); );
bench(
'node:sqlite',
() => {
ninsert.run({ a1: 1, a2: 2, a3: 3, b1: 'b1', b2: 'b2', b3: 'b3' });
},
{
teardown: () => {
ndb.exec(DELETE);
},
},
);
}); });

View File

@ -1,8 +1,7 @@
import { bench, describe } from 'vitest'; import { bench, describe } from 'vitest';
import BDatabase from '@signalapp/better-sqlite3'; import BDatabase from '@signalapp/better-sqlite3';
import { DatabaseSync as NDatabase } from 'node:sqlite'; import Database from '../lib/index.js';
import Database from '../dist/index.mjs';
const PREPARE = ` const PREPARE = `
CREATE TABLE t ( CREATE TABLE t (
@ -37,15 +36,12 @@ const SELECT = 'SELECT * FROM t LIMIT 1000';
describe('SELECT * FROM t', () => { describe('SELECT * FROM t', () => {
const sdb = new Database(':memory:', { cacheStatements: true }); const sdb = new Database(':memory:', { cacheStatements: true });
const bdb = new BDatabase(':memory:'); const bdb = new BDatabase(':memory:');
const ndb = new NDatabase(':memory:');
sdb.exec(PREPARE); sdb.exec(PREPARE);
bdb.exec(PREPARE); bdb.exec(PREPARE);
ndb.exec(PREPARE);
const sinsert = sdb.prepare(INSERT); const sinsert = sdb.prepare(INSERT);
const binsert = bdb.prepare(INSERT); const binsert = bdb.prepare(INSERT);
const ninsert = ndb.prepare(INSERT);
sdb.transaction(() => { sdb.transaction(() => {
for (const value of VALUES) { for (const value of VALUES) {
@ -59,12 +55,6 @@ describe('SELECT * FROM t', () => {
} }
})(); })();
ndb.exec('BEGIN');
for (const value of VALUES) {
ninsert.run(value);
}
ndb.exec('COMMIT');
const sselect = sdb.prepare(SELECT); const sselect = sdb.prepare(SELECT);
const bselect = bdb.prepare(SELECT); const bselect = bdb.prepare(SELECT);
@ -75,10 +65,4 @@ describe('SELECT * FROM t', () => {
bench('@signalapp/better-sqlite', () => { bench('@signalapp/better-sqlite', () => {
bselect.all(); bselect.all();
}); });
bench('node:sqlite', () => {
// Node.js seems to finalize the statement after `.all()`
const nselect = ndb.prepare(SELECT);
nselect.all();
});
}); });

14
deps/extension/Cargo.lock generated vendored
View File

@ -425,17 +425,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.8" version = "0.10.8"
@ -465,7 +454,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-sqlcipher-extension" name = "signal-sqlcipher-extension"
version = "0.2.2" version = "0.2.1"
dependencies = [ dependencies = [
"aes", "aes",
"cbc", "cbc",
@ -473,7 +462,6 @@ dependencies = [
"hmac", "hmac",
"pbkdf2", "pbkdf2",
"rand_core", "rand_core",
"sha1",
"sha2", "sha2",
"signal-tokenizer", "signal-tokenizer",
] ]

View File

@ -5,7 +5,7 @@
[package] [package]
name = "signal-sqlcipher-extension" name = "signal-sqlcipher-extension"
version = "0.2.2" version = "0.2.1"
edition = "2021" edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
@ -23,7 +23,6 @@ cbc = "0.1.2"
hmac = "0.12.1" hmac = "0.12.1"
pbkdf2 = "0.12.2" pbkdf2 = "0.12.2"
rand_core = { version = "0.6.4", "default-features" = false, features = ["getrandom"] } rand_core = { version = "0.6.4", "default-features" = false, features = ["getrandom"] }
sha1 = { version = "0.10.6", "default-features" = false }
sha2 = { version = "0.10.8", "default-features" = false } sha2 = { version = "0.10.8", "default-features" = false }
signal-tokenizer = { git = "https://github.com/signalapp/Signal-FTS5-Extension" } signal-tokenizer = { git = "https://github.com/signalapp/Signal-FTS5-Extension" }

View File

@ -10,7 +10,6 @@ use core::ffi::{c_char, c_int, c_uchar, c_void};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use pbkdf2::pbkdf2_hmac; use pbkdf2::pbkdf2_hmac;
use rand_core::{OsRng, RngCore}; use rand_core::{OsRng, RngCore};
use sha1::Sha1;
use sha2::Sha512; use sha2::Sha512;
pub use signal_tokenizer; pub use signal_tokenizer;
@ -71,7 +70,6 @@ extern "C" fn random(_ctx: *mut c_void, buf: *mut c_void, length: c_int) -> c_in
extern "C" fn get_hmac_sz(_ctx: *mut c_void, algorithm: c_int) -> c_int { extern "C" fn get_hmac_sz(_ctx: *mut c_void, algorithm: c_int) -> c_int {
match algorithm { match algorithm {
SQLCIPHER_HMAC_SHA512 => 64, SQLCIPHER_HMAC_SHA512 => 64,
SQLCIPHER_HMAC_SHA1 => 20,
_ => 0, _ => 0,
} }
} }
@ -87,6 +85,9 @@ extern "C" fn hmac(
in2_sz: c_int, in2_sz: c_int,
out: *mut c_uchar, out: *mut c_uchar,
) -> c_int { ) -> c_int {
if algorithm != SQLCIPHER_HMAC_SHA512 {
return SQLITE_ERROR;
}
if hmac_key.is_null() || in1.is_null() || out.is_null() { if hmac_key.is_null() || in1.is_null() || out.is_null() {
return SQLITE_ERROR; return SQLITE_ERROR;
} }
@ -98,34 +99,16 @@ extern "C" fn hmac(
Some(unsafe { core::slice::from_raw_parts(in2 as *mut c_uchar, in2_sz as usize) }) Some(unsafe { core::slice::from_raw_parts(in2 as *mut c_uchar, in2_sz as usize) })
}; };
match algorithm { let Ok(mut mac) = Hmac::<Sha512>::new_from_slice(key) else {
SQLCIPHER_HMAC_SHA512 => { return SQLITE_ERROR;
let Ok(mut mac) = Hmac::<Sha512>::new_from_slice(key) else { };
return SQLITE_ERROR; mac.update(in1);
}; if let Some(in2) = in2 {
mac.update(in1); mac.update(in2);
if let Some(in2) = in2 { }
mac.update(in2); let digest = mac.finalize().into_bytes();
} unsafe {
let digest = mac.finalize().into_bytes(); out.copy_from(digest.as_ptr(), digest.len());
unsafe {
out.copy_from(digest.as_ptr(), digest.len());
};
}
SQLCIPHER_HMAC_SHA1 => {
let Ok(mut mac) = Hmac::<Sha1>::new_from_slice(key) else {
return SQLITE_ERROR;
};
mac.update(in1);
if let Some(in2) = in2 {
mac.update(in2);
}
let digest = mac.finalize().into_bytes();
unsafe {
out.copy_from(digest.as_ptr(), digest.len());
};
}
_ => return SQLITE_ERROR,
}; };
SQLITE_OK SQLITE_OK
} }
@ -141,21 +124,16 @@ extern "C" fn pbkdf(
key_sz: c_int, key_sz: c_int,
key: *mut c_uchar, key: *mut c_uchar,
) -> c_int { ) -> c_int {
if algorithm != SQLCIPHER_PBKDF2_HMAC_SHA512 {
return SQLITE_ERROR;
}
if pass.is_null() || salt.is_null() || key.is_null() { if pass.is_null() || salt.is_null() || key.is_null() {
return SQLITE_ERROR; return SQLITE_ERROR;
} }
let password = unsafe { core::slice::from_raw_parts(pass as *const c_uchar, pass_sz as usize) }; let password = unsafe { core::slice::from_raw_parts(pass as *const c_uchar, pass_sz as usize) };
let salt = unsafe { core::slice::from_raw_parts(salt as *const c_uchar, salt_sz as usize) }; let salt = unsafe { core::slice::from_raw_parts(salt as *const c_uchar, salt_sz as usize) };
let buf = unsafe { core::slice::from_raw_parts_mut(key as *mut c_uchar, key_sz as usize) }; let buf = unsafe { core::slice::from_raw_parts_mut(key as *mut c_uchar, key_sz as usize) };
match algorithm { pbkdf2_hmac::<Sha512>(password, salt, workfactor as u32, buf);
SQLCIPHER_PBKDF2_HMAC_SHA512 => {
pbkdf2_hmac::<Sha512>(password, salt, workfactor as u32, buf);
}
SQLCIPHER_PBKDF2_HMAC_SHA1 => {
pbkdf2_hmac::<Sha1>(password, salt, workfactor as u32, buf);
}
_ => return SQLITE_ERROR,
};
SQLITE_OK SQLITE_OK
} }

View File

@ -10,10 +10,6 @@ use core::ffi::{c_char, c_int, c_uchar, c_void};
pub const SQLCIPHER_HMAC_SHA512: c_int = 2; pub const SQLCIPHER_HMAC_SHA512: c_int = 2;
pub const SQLCIPHER_PBKDF2_HMAC_SHA512: c_int = 2; pub const SQLCIPHER_PBKDF2_HMAC_SHA512: c_int = 2;
// Legacy encryption primitives
pub const SQLCIPHER_HMAC_SHA1: c_int = 0;
pub const SQLCIPHER_PBKDF2_HMAC_SHA1: c_int = 0;
pub const CIPHER_ENCRYPT: c_int = 1; pub const CIPHER_ENCRYPT: c_int = 1;
#[repr(C)] #[repr(C)]

View File

@ -1,4 +1,4 @@
Copyright (c) 2025, ZETETIC LLC Copyright (c) 2008-2023, ZETETIC LLC
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View File

@ -0,0 +1,24 @@
diff --git a/src/sqlcipher.c b/src/sqlcipher.c
index 8be4bc92..9cafc366 100644
--- a/src/sqlcipher.c
+++ b/src/sqlcipher.c
@@ -76,7 +76,8 @@ void sqlite3pager_reset(Pager *pPager);
#if !defined (SQLCIPHER_CRYPTO_CC) \
&& !defined (SQLCIPHER_CRYPTO_LIBTOMCRYPT) \
&& !defined (SQLCIPHER_CRYPTO_NSS) \
- && !defined (SQLCIPHER_CRYPTO_OPENSSL)
+ && !defined (SQLCIPHER_CRYPTO_OPENSSL) \
+ && !defined (SQLCIPHER_CRYPTO_CUSTOM)
#define SQLCIPHER_CRYPTO_OPENSSL
#endif
@@ -540,6 +541,9 @@ static void sqlcipher_activate() {
#elif defined (SQLCIPHER_CRYPTO_OSSL3)
extern int sqlcipher_ossl3_setup(sqlcipher_provider *p);
sqlcipher_ossl3_setup(p);
+#elif defined (SQLCIPHER_CRYPTO_CUSTOM)
+ extern int SQLCIPHER_CRYPTO_CUSTOM(sqlcipher_provider *p);
+ SQLCIPHER_CRYPTO_CUSTOM(p);
#else
#error "NO DEFAULT SQLCIPHER CRYPTO PROVIDER DEFINED"
#endif

View File

@ -0,0 +1,15 @@
diff --git a/src/date.c b/src/date.c
index d74cecb..8a609ae 100644
--- a/src/date.c
+++ b/src/date.c
@@ -667,8 +667,8 @@ static const struct {
/* 1 */ { 6, "minute", 7.7379e+12, 60.0 },
/* 2 */ { 4, "hour", 1.2897e+11, 3600.0 },
/* 3 */ { 3, "day", 5373485.0, 86400.0 },
- /* 4 */ { 5, "month", 176546.0, 30.0*86400.0 },
- /* 5 */ { 4, "year", 14713.0, 365.0*86400.0 },
+ /* 4 */ { 5, "month", 176546.0, 2592000.0 },
+ /* 5 */ { 4, "year", 14713.0, 31536000.0 },
};
/*

View File

@ -21,16 +21,13 @@
'xcode_settings': { 'xcode_settings': {
'OTHER_CFLAGS': ['-std=c99'], 'OTHER_CFLAGS': ['-std=c99'],
'WARNING_CFLAGS': ['-w'], 'WARNING_CFLAGS': ['-w'],
'DEAD_CODE_STRIPPING': 'YES',
'LLVM_LTO': 'YES',
}, },
'defines': [ 'defines': [
'SQLITE_LIKE_DOESNT_MATCH_BLOBS', 'SQLITE_LIKE_DOESNT_MATCH_BLOBS',
'SQLITE_THREADSAFE=2', 'SQLITE_THREADSAFE=2',
'SQLITE_USE_URI=0', 'SQLITE_USE_URI=0',
'SQLITE_USE_ALLOCA', 'SQLITE_DEFAULT_MEMSTATUS=0',
'SQLITE_OMIT_AUTOINIT', 'SQLITE_OMIT_AUTOINIT',
'SQLITE_OMIT_DECLTYPE',
'SQLITE_OMIT_DEPRECATED', 'SQLITE_OMIT_DEPRECATED',
'SQLITE_OMIT_DESERIALIZE', 'SQLITE_OMIT_DESERIALIZE',
'SQLITE_OMIT_GET_TABLE', 'SQLITE_OMIT_GET_TABLE',
@ -39,22 +36,22 @@
'SQLITE_OMIT_SHARED_CACHE', 'SQLITE_OMIT_SHARED_CACHE',
'SQLITE_OMIT_UTF16', 'SQLITE_OMIT_UTF16',
'SQLITE_OMIT_COMPLETE', 'SQLITE_OMIT_COMPLETE',
'SQLITE_OMIT_GET_TABLE',
'SQLITE_OMIT_AUTHORIZATION', 'SQLITE_OMIT_AUTHORIZATION',
'SQLITE_OMIT_LOAD_EXTENSION', 'SQLITE_OMIT_LOAD_EXTENSION',
'SQLITE_OMIT_INTROSPECTION_PRAGMAS', 'SQLITE_TRACE_SIZE_LIMIT=32',
'SQLITE_OMIT_TRACE',
'SQLITE_DEFAULT_CACHE_SIZE=-16000', 'SQLITE_DEFAULT_CACHE_SIZE=-16000',
'SQLITE_DEFAULT_FOREIGN_KEYS=1', 'SQLITE_DEFAULT_FOREIGN_KEYS=1',
'SQLITE_DEFAULT_WAL_SYNCHRONOUS=1', 'SQLITE_DEFAULT_WAL_SYNCHRONOUS=1',
'SQLITE_DEFAULT_MEMSTATUS=0',
'SQLITE_LIKE_DOESNT_MATCH_BLOBS',
'SQLITE_MAX_EXPR_DEPTH=0',
'SQLITE_DQS=0', 'SQLITE_DQS=0',
'SQLITE_ENABLE_MATH_FUNCTIONS', 'SQLITE_ENABLE_MATH_FUNCTIONS',
'SQLITE_ENABLE_DESERIALIZE',
'SQLITE_ENABLE_COLUMN_METADATA',
'SQLITE_ENABLE_UPDATE_DELETE_LIMIT', 'SQLITE_ENABLE_UPDATE_DELETE_LIMIT',
'SQLITE_ENABLE_STAT4', 'SQLITE_ENABLE_STAT4',
'SQLITE_ENABLE_FTS5', 'SQLITE_ENABLE_FTS5',
'SQLITE_ENABLE_JSON1', 'SQLITE_ENABLE_JSON1',
'SQLITE_INTROSPECTION_PRAGMAS',
'SQLCIPHER_CRYPTO_CUSTOM=signal_crypto_provider_setup', 'SQLCIPHER_CRYPTO_CUSTOM=signal_crypto_provider_setup',
@ -72,11 +69,8 @@
'SQLITE_HAS_CODEC', 'SQLITE_HAS_CODEC',
'SQLITE_TEMP_STORE=2', 'SQLITE_TEMP_STORE=2',
'SQLITE_SECURE_DELETE', 'SQLITE_SECURE_DELETE',
'SQLITE_EXTRA_INIT=sqlcipher_extra_init',
'SQLITE_EXTRA_SHUTDOWN=sqlcipher_extra_shutdown',
], ],
'conditions': [ 'conditions': [
# Link with extension
['OS == "win"', { ['OS == "win"', {
'defines': [ 'defines': [
'WIN32' 'WIN32'
@ -100,25 +94,6 @@
] ]
}, },
}], }],
# LTO on Linux
['OS == "linux"', {
# GCC only for now
'cflags': ['-flto=4', '-fuse-linker-plugin', '-ffat-lto-objects'],
'ldflags': ['-flto=4', '-fuse-linker-plugin', '-ffat-lto-objects'],
}],
# Profiling
["\"-profiling.\" in \"<!(node -p \"require('../../package.json').version\")\"", {
'defines': [
'SQLITE_ENABLE_STMT_SCANSTATUS'
],
'direct_dependent_settings': {
'defines': [
'SQLITE_ENABLE_STMT_SCANSTATUS'
],
},
}],
], ],
'configurations': { 'configurations': {
'Debug': { 'Debug': {

20035
deps/sqlcipher/sqlite3.c vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,8 @@ tag=${1?Pass a valid sqlcipher version as an argument}
rm -rf .tmp/sqlcipher rm -rf .tmp/sqlcipher
git clone --branch $tag --filter=blob:none git@github.com:sqlcipher/sqlcipher.git .tmp/sqlcipher git clone --branch $tag --filter=blob:none git@github.com:sqlcipher/sqlcipher.git .tmp/sqlcipher
cd .tmp/sqlcipher cd .tmp/sqlcipher
git apply ../../patches/sqlcipher/custom-crypto-provider.diff
git apply ../../patches/sqlcipher/fix-constant-expression-for-msvc-arm64-6c103aee6f146869.diff
./configure --enable-update-limit ./configure --enable-update-limit
make sqlite3.h sqlite3.c sqlite3ext.h shell.c make sqlite3.h sqlite3.c sqlite3ext.h shell.c
cd - cd -

View File

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

View File

@ -27,40 +27,25 @@ const addon = bindings<{
persistent: boolean, persistent: boolean,
pluck: boolean, pluck: boolean,
bigint: boolean, bigint: boolean,
paramNames: Array<string | null>,
): NativeStatement; ): NativeStatement;
statementRun<Options extends StatementOptions>( statementRun<Options extends StatementOptions>(
stmt: NativeStatement, stmt: NativeStatement,
params: NativeParameters<Options> | undefined, params: StatementParameters<Options> | undefined,
result: [number, number], result: [number, number],
): void; ): void;
statementStep<Options extends StatementOptions>( statementStep<Options extends StatementOptions>(
stmt: NativeStatement, stmt: NativeStatement,
params: NativeParameters<Options> | null | undefined, params: StatementParameters<Options> | null | undefined,
cache: Array<SqliteValue<Options>> | undefined, cache: Array<SqliteValue<Options>> | undefined,
isGet: boolean, isGet: boolean,
): Array<SqliteValue<Options>>; ): Array<SqliteValue<Options>>;
statementScanStats(stmt: NativeStatement): Array<ScanStats>;
statementClose(stmt: NativeStatement): void; statementClose(stmt: NativeStatement): void;
databaseOpen(path: string): NativeDatabase; databaseOpen(path: string): NativeDatabase;
databaseInitTokenizer(db: NativeDatabase): void;
databaseExec(db: NativeDatabase, query: string): void; databaseExec(db: NativeDatabase, query: string): void;
databaseClose(db: NativeDatabase): 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>; signalTokenize(value: string): Array<string>;
setLogger(fn: (code: string, message: string) => void): void;
}>(ROOT_DIR); }>(ROOT_DIR);
export type RunResult = { export type RunResult = {
@ -96,15 +81,11 @@ export type StatementOptions = Readonly<{
bigint?: true; bigint?: true;
}>; }>;
export type NativeParameters<Options extends StatementOptions> = ReadonlyArray<
SqliteValue<Options>
>;
/** /**
* Parameters accepted by `.run()`/`.get()`/`.all()` methods of the statement. * Parameters accepted by `.run()`/`.get()`/`.all()` methods of the statement.
*/ */
export type StatementParameters<Options extends StatementOptions> = export type StatementParameters<Options extends StatementOptions> =
| NativeParameters<Options> | ReadonlyArray<SqliteValue<Options>>
| Readonly<Record<string, SqliteValue<Options>>>; | Readonly<Record<string, SqliteValue<Options>>>;
/** /**
@ -112,7 +93,7 @@ export type StatementParameters<Options extends StatementOptions> =
*/ */
export type SqliteValue<Options extends StatementOptions> = export type SqliteValue<Options extends StatementOptions> =
| string | string
| Uint8Array<ArrayBuffer> | Uint8Array
| number | number
| null | null
| (Options extends { bigint: true } ? bigint : never); | (Options extends { bigint: true } ? bigint : never);
@ -126,14 +107,6 @@ export type RowType<Options extends StatementOptions> = Options extends {
? SqliteValue<Options> ? SqliteValue<Options>
: Record<string, 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. * A compiled SQL statement class.
*/ */
@ -142,9 +115,6 @@ class Statement<Options extends StatementOptions = object> {
#cache: Array<SqliteValue<Options>> | undefined; #cache: Array<SqliteValue<Options>> | undefined;
#createRow: undefined | ((result: unknown) => RowType<Options>); #createRow: undefined | ((result: unknown) => RowType<Options>);
#translateParams: (
params: StatementParameters<Options>,
) => NativeParameters<Options>;
#native: NativeStatement | undefined; #native: NativeStatement | undefined;
#onClose: (() => void) | undefined; #onClose: (() => void) | undefined;
@ -157,47 +127,14 @@ class Statement<Options extends StatementOptions = object> {
) { ) {
this.#needsTranslation = persistent === true && !pluck; this.#needsTranslation = persistent === true && !pluck;
const paramNames = new Array<string | null>();
this.#native = addon.statementNew( this.#native = addon.statementNew(
db, db,
query, query,
persistent === true, persistent === true,
pluck === true, pluck === true,
bigint === true, bigint === true,
paramNames,
); );
const isArrayParams = paramNames.every((name) => name === null);
const isObjectParams =
!isArrayParams && paramNames.every((name) => typeof name === 'string');
if (!isArrayParams && !isObjectParams) {
throw new TypeError('Cannot mix named and anonymous params in query');
}
if (isArrayParams) {
this.#translateParams = (params) => {
if (!Array.isArray(params)) {
throw new TypeError('Query requires an array of anonymous params');
}
return params;
};
} else {
this.#translateParams = runInThisContext(`
(function translateParams(params) {
if (Array.isArray(params)) {
throw new TypeError('Query requires an object of named params');
}
return [
${paramNames
.map((name) => `params[${JSON.stringify(name)}]`)
.join(',\n')}
];
})
`);
}
this.#onClose = onClose; this.#onClose = onClose;
} }
@ -213,8 +150,8 @@ class Statement<Options extends StatementOptions = object> {
throw new Error('Statement closed'); throw new Error('Statement closed');
} }
const result: [number, number] = [0, 0]; const result: [number, number] = [0, 0];
const nativeParams = this.#checkParams(params); this.#checkParams(params);
addon.statementRun(this.#native, nativeParams, result); addon.statementRun(this.#native, params, result);
return { changes: result[0], lastInsertRowid: result[1] }; return { changes: result[0], lastInsertRowid: result[1] };
} }
@ -233,13 +170,8 @@ class Statement<Options extends StatementOptions = object> {
if (this.#native === undefined) { if (this.#native === undefined) {
throw new Error('Statement closed'); throw new Error('Statement closed');
} }
const nativeParams = this.#checkParams(params); this.#checkParams(params);
const result = addon.statementStep( const result = addon.statementStep(this.#native, params, this.#cache, true);
this.#native,
nativeParams,
this.#cache,
true,
);
if (result === undefined) { if (result === undefined) {
return undefined; return undefined;
} }
@ -266,8 +198,9 @@ class Statement<Options extends StatementOptions = object> {
throw new Error('Statement closed'); throw new Error('Statement closed');
} }
const result = []; const result = [];
const nativeParams = this.#checkParams(params); this.#checkParams(params);
let singleUseParams: typeof nativeParams | undefined | null = nativeParams; let singleUseParams: StatementParameters<Options> | undefined | null =
params;
while (true) { while (true) {
const single = addon.statementStep( const single = addon.statementStep(
this.#native, this.#native,
@ -291,20 +224,6 @@ class Statement<Options extends StatementOptions = object> {
return result as unknown as Array<Row>; return result as unknown as Array<Row>;
} }
/**
* Report collected performance statics for the statement.
*
* @returns A list of objects describing the performance of the query.
*
* @see {@link https://www.sqlite.org/profile.html}
*/
public scanStats(): Array<ScanStats> {
if (this.#native === undefined) {
throw new Error('Statement closed');
}
return addon.statementScanStats(this.#native);
}
/** /**
* Close the statement and release the used memory. * Close the statement and release the used memory.
*/ */
@ -345,11 +264,9 @@ class Statement<Options extends StatementOptions = object> {
} }
/** @internal */ /** @internal */
#checkParams( #checkParams(params: StatementParameters<Options> | undefined): void {
params: StatementParameters<Options> | undefined,
): NativeParameters<Options> | undefined {
if (params === undefined) { if (params === undefined) {
return undefined; return;
} }
if (typeof params !== 'object') { if (typeof params !== 'object') {
throw new TypeError('Params must be either object or array'); throw new TypeError('Params must be either object or array');
@ -357,7 +274,6 @@ class Statement<Options extends StatementOptions = object> {
if (params === null) { if (params === null) {
throw new TypeError('Params cannot be null'); throw new TypeError('Params cannot be null');
} }
return this.#translateParams(params);
} }
} }
@ -385,20 +301,6 @@ export type PragmaResult<Options extends PragmaOptions> = Options extends {
? RowType<{ pluck: true }> | undefined ? RowType<{ pluck: true }> | undefined
: Array<RowType<object>>; : Array<RowType<object>>;
/**
* An entry of result array of `stmt.scanStats()` method.
*
* Value of `-1` indicates that the field is not available for a given entry.
*/
export type ScanStats = Readonly<{
id: number;
parent: number;
cycles: number;
loops: number;
rows: number;
explain: string | null;
}>;
/** @internal */ /** @internal */
type TransactionStatement = Statement<{ persistent: true; pluck: true }>; type TransactionStatement = Statement<{ persistent: true; pluck: true }>;
@ -413,13 +315,6 @@ export type DatabaseOptions = Readonly<{
cacheStatements?: boolean; 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. * A sqlite database class.
*/ */
@ -455,13 +350,6 @@ export default class Database {
this.#isCacheEnabled = cacheStatements === true; this.#isCacheEnabled = cacheStatements === true;
} }
public initTokenizer(): void {
if (this.#native === undefined) {
throw new Error('Database closed');
}
addon.databaseInitTokenizer(this.#native);
}
/** /**
* Execute one or multiple SQL statements in a given `sql` string. * Execute one or multiple SQL statements in a given `sql` string.
* *
@ -477,51 +365,6 @@ export default class Database {
addon.databaseExec(this.#native, sql); 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. * Compile a single SQL statement.
* *
@ -675,14 +518,7 @@ export default class Database {
commit.run(); commit.run();
return result; return result;
} catch (error) { } catch (error) {
try { rollback.run();
rollback.run();
} catch (rollbackError) {
if (rollbackError instanceof Error) {
rollbackError.cause = error;
}
throw rollbackError;
}
throw error; throw error;
} finally { } finally {
this.#transactionDepth -= 1; this.#transactionDepth -= 1;
@ -707,12 +543,4 @@ export default class Database {
} }
} }
function setLogger(fn: (code: string, message: string) => void): void { export { Database };
if (typeof fn !== 'function') {
throw new TypeError('Invalid value');
}
return addon.setLogger(fn);
}
export { Database, setLogger };

View File

@ -1,7 +1,6 @@
{ {
"packageManager": "pnpm@10.18.1",
"name": "@signalapp/sqlcipher", "name": "@signalapp/sqlcipher",
"version": "3.3.5", "version": "1.0.0",
"description": "A fast N-API-based Node.js addon wrapping sqlcipher and FTS5 segmenting APIs", "description": "A fast N-API-based Node.js addon wrapping sqlcipher and FTS5 segmenting APIs",
"homepage": "http://github.com/signalapp/node-sqlcipher.git", "homepage": "http://github.com/signalapp/node-sqlcipher.git",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
@ -17,7 +16,6 @@
"main": "dist/index.cjs", "main": "dist/index.cjs",
"module": "dist/index.mjs", "module": "dist/index.mjs",
"exports": { "exports": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/index.mjs", "import": "./dist/index.mjs",
"require": "./dist/index.cjs" "require": "./dist/index.cjs"
}, },
@ -45,7 +43,7 @@
"prebuildify": "prebuildify --strip --napi", "prebuildify": "prebuildify --strip --napi",
"test": "vitest --coverage --pool threads", "test": "vitest --coverage --pool threads",
"format": "run-p --print-label format:c format:js", "format": "run-p --print-label format:c format:js",
"format:c": "xcrun clang-format --style=chromium -Werror --verbose -i src/*.cc src/*.h", "format:c": "xcrun clang-format --style=chromium -Werror --verbose -i src/*.cc",
"format:js": "prettier --cache --write .", "format:js": "prettier --cache --write .",
"lint": "run-p --print-label check:eslint check:format", "lint": "run-p --print-label check:eslint check:format",
"check:eslint": "eslint --cache .", "check:eslint": "eslint --cache .",

View File

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

View File

@ -5,7 +5,6 @@
#include <list> #include <list>
#include "addon.h" #include "addon.h"
#include "errors.h"
#include "napi.h" #include "napi.h"
#include "signal-tokenizer.h" #include "signal-tokenizer.h"
@ -74,169 +73,6 @@ static Napi::Value SignalTokenize(const Napi::CallbackInfo& info) {
return result; 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_;
static void LoggerWrapper(void* _ctx, int code, const char* msg) {
if (logger_fn_.IsEmpty()) {
return;
}
auto env = logger_fn_.Env();
Napi::HandleScope scope(env);
#define CODE_STR(NAME) \
case NAME: \
code_name = #NAME; \
break;
const char* code_name;
switch (code) {
SQLITE_ERROR_ENUM(CODE_STR)
default:
code_name = "unknown";
break;
}
#undef CODE_STR
auto result = logger_fn_.Value().Call({
Napi::String::New(env, code_name),
Napi::String::New(env, msg),
});
// Ignore exceptions
if (result.IsEmpty()) {
env.GetAndClearPendingException();
}
}
static void SetLogger(const Napi::CallbackInfo& info) {
auto callback = info[0].As<Napi::Function>();
assert(callback.IsFunction());
logger_fn_.Reset(callback, 1);
sqlite3_config(SQLITE_CONFIG_LOG, LoggerWrapper);
}
// Utils // Utils
Napi::Error FormatError(Napi::Env env, const char* format, ...) { Napi::Error FormatError(Napi::Env env, const char* format, ...) {
@ -262,14 +98,8 @@ Napi::Error FormatError(Napi::Env env, const char* format, ...) {
Napi::Object Database::Init(Napi::Env env, Napi::Object exports) { Napi::Object Database::Init(Napi::Env env, Napi::Object exports) {
exports["databaseOpen"] = Napi::Function::New(env, &Database::Open); exports["databaseOpen"] = Napi::Function::New(env, &Database::Open);
exports["databaseInitTokenizer"] =
Napi::Function::New(env, &Database::InitTokenizer);
exports["databaseClose"] = Napi::Function::New(env, &Database::Close); exports["databaseClose"] = Napi::Function::New(env, &Database::Close);
exports["databaseExec"] = Napi::Function::New(env, &Database::Exec); 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; return exports;
} }
@ -285,9 +115,6 @@ Database::~Database() {
return; return;
} }
delete wal_hook_wrap_;
wal_hook_wrap_ = nullptr;
int r = sqlite3_close(handle_); int r = sqlite3_close(handle_);
if (r != SQLITE_OK) { if (r != SQLITE_OK) {
fprintf(stderr, "Cleanup: sqlite3_close failure\n"); fprintf(stderr, "Cleanup: sqlite3_close failure\n");
@ -332,32 +159,20 @@ Napi::Value Database::Open(const Napi::CallbackInfo& info) {
return db->ThrowSqliteError(env, r); return db->ThrowSqliteError(env, r);
} }
return db->self_ref_.Value();
}
Napi::Value Database::InitTokenizer(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto db = FromExternal(info[0]);
if (db == nullptr) {
return Napi::Value();
}
fts5_api* fts5 = db->GetFTS5API(env); fts5_api* fts5 = db->GetFTS5API(env);
if (fts5 == nullptr) { if (fts5 == nullptr) {
return Napi::Value(); return Napi::Value();
} }
SignalTokenizerModule* icu = new SignalTokenizerModule(); SignalTokenizerModule* icu = new SignalTokenizerModule();
int r = r = fts5->xCreateTokenizer(fts5, "signal_tokenizer", icu, &icu->api_object,
fts5->xCreateTokenizer(fts5, "signal_tokenizer", icu, &icu->api_object,
&SignalTokenizerModule::Destroy); &SignalTokenizerModule::Destroy);
if (r != SQLITE_OK) { if (r != SQLITE_OK) {
delete icu; delete icu;
return db->ThrowSqliteError(env, r); return db->ThrowSqliteError(env, r);
} }
return Napi::Value(); return db->self_ref_.Value();
} }
Napi::Value Database::Close(const Napi::CallbackInfo& info) { Napi::Value Database::Close(const Napi::CallbackInfo& info) {
@ -379,9 +194,6 @@ Napi::Value Database::Close(const Napi::CallbackInfo& info) {
} }
db->statements_.clear(); db->statements_.clear();
delete db->wal_hook_wrap_;
db->wal_hook_wrap_ = nullptr;
int r = sqlite3_close(db->handle_); int r = sqlite3_close(db->handle_);
if (r != SQLITE_OK) { if (r != SQLITE_OK) {
return db->ThrowSqliteError(env, r); return db->ThrowSqliteError(env, r);
@ -415,99 +227,19 @@ Napi::Value Database::Exec(const Napi::CallbackInfo& info) {
return Napi::Value(); 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) { Napi::Value Database::ThrowSqliteError(Napi::Env env, int error) {
assert(handle_ != nullptr); assert(handle_ != nullptr);
const char* msg = sqlite3_errmsg(handle_); const char* msg = sqlite3_errmsg(handle_);
int offset = sqlite3_error_offset(handle_); int offset = sqlite3_error_offset(handle_);
int extended = sqlite3_extended_errcode(handle_); int extended = sqlite3_extended_errcode(handle_);
#define EXTENDED_STR(NAME) \
case NAME: \
extended_name = #NAME; \
break;
const char* extended_name;
switch (extended) {
SQLITE_ERROR_ENUM(EXTENDED_STR)
default:
extended_name = "unknown";
break;
}
#undef EXTENDED_STR
Napi::Error err;
if (offset == -1) { if (offset == -1) {
err = FormatError(env, "sqlite error(%s): %s", extended_name, msg); NAPI_THROW(FormatError(env, "sqlite error(%d): %s", extended, msg),
Napi::Value());
} else { } else {
err = FormatError(env, "sqlite error(%s): %s, offset: %d", extended_name, NAPI_THROW(FormatError(env, "sqlite error(%d): %s, offset: %d", extended,
msg, offset); msg, offset),
Napi::Value());
} }
err.Set("code", extended_name);
NAPI_THROW(err, Napi::Value());
} }
fts5_api* Database::GetFTS5API(Napi::Env env) { fts5_api* Database::GetFTS5API(Napi::Env env) {
@ -556,8 +288,6 @@ Napi::Object Statement::Init(Napi::Env env, Napi::Object exports) {
exports["statementClose"] = Napi::Function::New(env, &Statement::Close); exports["statementClose"] = Napi::Function::New(env, &Statement::Close);
exports["statementRun"] = Napi::Function::New(env, &Statement::Run); exports["statementRun"] = Napi::Function::New(env, &Statement::Run);
exports["statementStep"] = Napi::Function::New(env, &Statement::Step); exports["statementStep"] = Napi::Function::New(env, &Statement::Step);
exports["statementScanStats"] =
Napi::Function::New(env, &Statement::ScanStats);
return exports; return exports;
} }
@ -599,14 +329,12 @@ Napi::Value Statement::New(const Napi::CallbackInfo& info) {
auto is_persistent = info[2].As<Napi::Boolean>(); auto is_persistent = info[2].As<Napi::Boolean>();
auto is_pluck = info[3].As<Napi::Boolean>(); auto is_pluck = info[3].As<Napi::Boolean>();
auto is_bigint = info[4].As<Napi::Boolean>(); auto is_bigint = info[4].As<Napi::Boolean>();
auto param_names = info[5].As<Napi::Array>();
assert(db_external.IsExternal()); assert(db_external.IsExternal());
assert(query.IsString()); assert(query.IsString());
assert(is_persistent.IsBoolean()); assert(is_persistent.IsBoolean());
assert(is_pluck.IsBoolean()); assert(is_pluck.IsBoolean());
assert(is_bigint.IsBoolean()); assert(is_bigint.IsBoolean());
assert(param_names.IsArray());
auto db = db_external.Data(); auto db = db_external.Data();
@ -635,18 +363,6 @@ Napi::Value Statement::New(const Napi::CallbackInfo& info) {
auto stmt = new Statement(db, db_external, handle, is_persistent, is_pluck, auto stmt = new Statement(db, db_external, handle, is_persistent, is_pluck,
is_bigint); is_bigint);
int key_count = sqlite3_bind_parameter_count(handle);
for (int i = 1; i <= key_count; i++) {
auto name = sqlite3_bind_parameter_name(handle, i);
if (name == nullptr) {
param_names[i - 1] = env.Null();
} else {
// Skip "$"
param_names[i - 1] = name + 1;
}
}
return Napi::External<Statement>::New( return Napi::External<Statement>::New(
env, stmt, [](Napi::Env env, Statement* stmt) { delete stmt; }); env, stmt, [](Napi::Env env, Statement* stmt) { delete stmt; });
} }
@ -668,9 +384,6 @@ Napi::Value Statement::Close(const Napi::CallbackInfo& info) {
auto env = info.Env(); auto env = info.Env();
auto stmt = FromExternal(info[0]); auto stmt = FromExternal(info[0]);
if (stmt == nullptr) {
return Napi::Value();
}
int r = sqlite3_finalize(stmt->handle_); int r = sqlite3_finalize(stmt->handle_);
if (r != SQLITE_OK) { if (r != SQLITE_OK) {
@ -686,10 +399,6 @@ Napi::Value Statement::Run(const Napi::CallbackInfo& info) {
auto env = info.Env(); auto env = info.Env();
auto stmt = FromExternal(info[0]); auto stmt = FromExternal(info[0]);
if (stmt == nullptr) {
return Napi::Value();
}
auto params = info[1]; auto params = info[1];
auto result = info[2].As<Napi::Array>(); auto result = info[2].As<Napi::Array>();
@ -728,10 +437,6 @@ Napi::Value Statement::Step(const Napi::CallbackInfo& info) {
auto env = info.Env(); auto env = info.Env();
auto stmt = FromExternal(info[0]); auto stmt = FromExternal(info[0]);
if (stmt == nullptr) {
return Napi::Value();
}
auto params = info[1]; auto params = info[1];
auto cache = info[2]; auto cache = info[2];
auto is_get = info[3].As<Napi::Boolean>(); auto is_get = info[3].As<Napi::Boolean>();
@ -752,16 +457,14 @@ Napi::Value Statement::Step(const Napi::CallbackInfo& info) {
int r = sqlite3_step(stmt->handle_); int r = sqlite3_step(stmt->handle_);
AutoResetStatement auto_reset(stmt, is_get.Value());
// No more rows // No more rows
if (r == SQLITE_DONE) { if (r == SQLITE_DONE) {
auto_reset.Reset(); stmt->Reset();
return Napi::Value(); return Napi::Value();
} }
AutoResetStatement _(stmt, is_get.Value());
if (r != SQLITE_ROW) { if (r != SQLITE_ROW) {
auto_reset.Reset();
return stmt->db_->ThrowSqliteError(env, r); return stmt->db_->ThrowSqliteError(env, r);
} }
@ -770,7 +473,6 @@ Napi::Value Statement::Step(const Napi::CallbackInfo& info) {
// In pluck mode - return the value of the first column // In pluck mode - return the value of the first column
if (stmt->is_pluck_) { if (stmt->is_pluck_) {
if (column_count != 1) { if (column_count != 1) {
auto_reset.Reset();
NAPI_THROW(Napi::Error::New(env, "Invalid column count for pluck"), NAPI_THROW(Napi::Error::New(env, "Invalid column count for pluck"),
Napi::Value()); Napi::Value());
} }
@ -813,108 +515,6 @@ Napi::Value Statement::Step(const Napi::CallbackInfo& info) {
return result; return result;
} }
// Only enabled on `-profiling` npm package versions
#ifdef SQLITE_ENABLE_STMT_SCANSTATUS
Napi::Value Statement::ScanStats(const Napi::CallbackInfo& info) {
auto env = info.Env();
auto stmt = FromExternal(info[0]);
if (stmt == nullptr) {
return Napi::Value();
}
sqlite3_int64 total_cycles = 0;
int r = sqlite3_stmt_scanstatus_v2(stmt->handle_, -1, SQLITE_SCANSTAT_NCYCLE,
SQLITE_SCANSTAT_COMPLEX, &total_cycles);
if (r != SQLITE_OK) {
return stmt->db_->ThrowSqliteError(env, r);
}
auto results = Napi::Array::New(env, 1);
auto root = Napi::Object::New(env);
root["id"] = 0;
root["parent"] = -1;
root["cycles"] = total_cycles;
root["loops"] = -1;
root["rows"] = -1;
root["explain"] = env.Null();
results[static_cast<uint32_t>(0)] = root;
for (int idx = 0; r == SQLITE_OK; idx++) {
int id = 0;
int parent = 0;
sqlite3_int64 cycles = 0;
sqlite3_int64 loops = 0;
sqlite3_int64 rows = 0;
const char* explain = nullptr;
r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_SELECTID,
SQLITE_SCANSTAT_COMPLEX, &id);
if (r != SQLITE_OK) {
break;
}
r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_PARENTID,
SQLITE_SCANSTAT_COMPLEX, &parent);
if (r != SQLITE_OK) {
break;
}
r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_NCYCLE,
SQLITE_SCANSTAT_COMPLEX, &cycles);
if (r != SQLITE_OK) {
break;
}
r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_NLOOP,
SQLITE_SCANSTAT_COMPLEX, &loops);
if (r != SQLITE_OK) {
break;
}
r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_NVISIT,
SQLITE_SCANSTAT_COMPLEX, &rows);
if (r != SQLITE_OK) {
break;
}
r = sqlite3_stmt_scanstatus_v2(stmt->handle_, idx, SQLITE_SCANSTAT_EXPLAIN,
SQLITE_SCANSTAT_COMPLEX, &explain);
if (r != SQLITE_OK) {
break;
}
auto result = Napi::Object::New(env);
result["id"] = id;
result["parent"] = parent;
result["cycles"] = cycles;
result["loops"] = loops;
result["rows"] = rows;
if (explain == nullptr) {
result["explain"] = env.Null();
} else {
result["explain"] = explain;
}
results[static_cast<uint32_t>(idx + 1)] = result;
}
// SQLITE_ERROR is returned when `idx` is out of range
if (r != SQLITE_ERROR) {
return stmt->db_->ThrowSqliteError(env, r);
}
return results;
}
#else // !SQLITE_ENABLE_STMT_SCANSTATUS
Napi::Value Statement::ScanStats(const Napi::CallbackInfo& info) {
auto env = info.Env();
NAPI_THROW(Napi::Error::New(env, "Not available in production builds"),
Napi::Value());
}
#endif // !SQLITE_ENABLE_STMT_SCANSTATUS
bool Statement::BindParams(Napi::Env env, Napi::Value params) { bool Statement::BindParams(Napi::Env env, Napi::Value params) {
int key_count = sqlite3_bind_parameter_count(handle_); int key_count = sqlite3_bind_parameter_count(handle_);
@ -940,18 +540,36 @@ bool Statement::BindParams(Napi::Env env, Napi::Value params) {
for (int i = 1; i <= list_len; i++) { for (int i = 1; i <= list_len; i++) {
auto name = sqlite3_bind_parameter_name(handle_, i); auto name = sqlite3_bind_parameter_name(handle_, i);
if (name != nullptr) {
NAPI_THROW(FormatError(env, "Unexpected named param %s at %d", name, i),
false);
}
auto error = BindParam(env, i, list[i - 1]); auto error = BindParam(env, i, list[i - 1]);
if (error != nullptr) { if (error != nullptr) {
if (name == nullptr) { NAPI_THROW(
NAPI_THROW( FormatError(env, "Failed to bind param %d, error %s", i, error),
FormatError(env, "Failed to bind param %d, error %s", i, error), false);
false); }
} else { }
NAPI_THROW(FormatError(env, "Failed to bind param %s, error %s", } else {
name + 1, error), auto obj = params.As<Napi::Object>();
false);
} for (int i = 1; i <= key_count; i++) {
auto name = sqlite3_bind_parameter_name(handle_, i);
if (name == nullptr) {
NAPI_THROW(FormatError(env, "Unexpected anonymous param at %d", i),
false);
}
// Skip "$"
name = name + 1;
auto value = obj[name];
auto error = BindParam(env, i, value);
if (error != nullptr) {
NAPI_THROW(
FormatError(env, "Failed to bind param %s, error %s", name, error),
false);
} }
} }
} }
@ -1071,11 +689,6 @@ Napi::Value Statement::GetColumnValue(Napi::Env env, int column) {
return Napi::Value(); return Napi::Value();
} }
void AutoResetStatement::Reset() {
stmt_->Reset();
enabled_ = false;
}
AutoResetStatement::~AutoResetStatement() { AutoResetStatement::~AutoResetStatement() {
if (enabled_) { if (enabled_) {
stmt_->Reset(); stmt_->Reset();
@ -1087,7 +700,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
Database::Init(env, exports); Database::Init(env, exports);
Statement::Init(env, exports); Statement::Init(env, exports);
exports["setLogger"] = Napi::Function::New(env, &SetLogger);
exports["signalTokenize"] = Napi::Function::New(env, &SignalTokenize); exports["signalTokenize"] = Napi::Function::New(env, &SignalTokenize);
return exports; return exports;
} }

View File

@ -9,7 +9,6 @@
#include "sqlite3.h" #include "sqlite3.h"
class Statement; class Statement;
class WalHookWrap;
class Database { class Database {
public: public:
@ -28,11 +27,8 @@ class Database {
static Database* FromExternal(const Napi::Value value); static Database* FromExternal(const Napi::Value value);
static Napi::Value Open(const Napi::CallbackInfo& info); static Napi::Value Open(const Napi::CallbackInfo& info);
static Napi::Value InitTokenizer(const Napi::CallbackInfo& info);
static Napi::Value Close(const Napi::CallbackInfo& info); static Napi::Value Close(const Napi::CallbackInfo& info);
static Napi::Value Exec(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); fts5_api* GetFTS5API(Napi::Env env);
@ -47,8 +43,6 @@ class Database {
// All currently open statements for this database. Used to close all open // All currently open statements for this database. Used to close all open
// statements when closing the database. // statements when closing the database.
std::list<Statement*> statements_; std::list<Statement*> statements_;
WalHookWrap* wal_hook_wrap_ = nullptr;
}; };
class AutoResetStatement { class AutoResetStatement {
@ -58,9 +52,6 @@ class AutoResetStatement {
~AutoResetStatement(); ~AutoResetStatement();
// Force reset statement now and clear `enabled_`
void Reset();
private: private:
Statement* stmt_; Statement* stmt_;
bool enabled_; bool enabled_;
@ -98,7 +89,7 @@ class Statement {
if (std::isspace(ch) || ch == ';') { if (std::isspace(ch) || ch == ';') {
p = p.substr(1); p = p.substr(1);
} else if (p.rfind("--", 0) == 0) { } else if (p.rfind("--", 0) == 0) {
// Line comment: "--" // Line comment: "--"
p = p.substr(2); p = p.substr(2);
@ -136,7 +127,6 @@ class Statement {
static Napi::Value Close(const Napi::CallbackInfo& info); static Napi::Value Close(const Napi::CallbackInfo& info);
static Napi::Value Run(const Napi::CallbackInfo& info); static Napi::Value Run(const Napi::CallbackInfo& info);
static Napi::Value Step(const Napi::CallbackInfo& info); static Napi::Value Step(const Napi::CallbackInfo& info);
static Napi::Value ScanStats(const Napi::CallbackInfo& info);
bool BindParams(Napi::Env env, Napi::Value params); bool BindParams(Napi::Env env, Napi::Value params);

View File

@ -1,116 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef SRC_ERRORS_H_
#define SRC_ERRORS_H_
#define SQLITE_ERROR_ENUM(V) \
V(SQLITE_ERROR) \
V(SQLITE_INTERNAL) \
V(SQLITE_PERM) \
V(SQLITE_ABORT) \
V(SQLITE_BUSY) \
V(SQLITE_LOCKED) \
V(SQLITE_NOMEM) \
V(SQLITE_READONLY) \
V(SQLITE_INTERRUPT) \
V(SQLITE_IOERR) \
V(SQLITE_CORRUPT) \
V(SQLITE_NOTFOUND) \
V(SQLITE_FULL) \
V(SQLITE_CANTOPEN) \
V(SQLITE_PROTOCOL) \
V(SQLITE_EMPTY) \
V(SQLITE_SCHEMA) \
V(SQLITE_TOOBIG) \
V(SQLITE_CONSTRAINT) \
V(SQLITE_MISMATCH) \
V(SQLITE_MISUSE) \
V(SQLITE_NOLFS) \
V(SQLITE_AUTH) \
V(SQLITE_FORMAT) \
V(SQLITE_RANGE) \
V(SQLITE_NOTADB) \
V(SQLITE_NOTICE) \
V(SQLITE_WARNING) \
V(SQLITE_ROW) \
V(SQLITE_DONE) \
V(SQLITE_ERROR_MISSING_COLLSEQ) \
V(SQLITE_ERROR_RETRY) \
V(SQLITE_ERROR_SNAPSHOT) \
V(SQLITE_IOERR_READ) \
V(SQLITE_IOERR_SHORT_READ) \
V(SQLITE_IOERR_WRITE) \
V(SQLITE_IOERR_FSYNC) \
V(SQLITE_IOERR_DIR_FSYNC) \
V(SQLITE_IOERR_TRUNCATE) \
V(SQLITE_IOERR_FSTAT) \
V(SQLITE_IOERR_UNLOCK) \
V(SQLITE_IOERR_RDLOCK) \
V(SQLITE_IOERR_DELETE) \
V(SQLITE_IOERR_BLOCKED) \
V(SQLITE_IOERR_NOMEM) \
V(SQLITE_IOERR_ACCESS) \
V(SQLITE_IOERR_CHECKRESERVEDLOCK) \
V(SQLITE_IOERR_LOCK) \
V(SQLITE_IOERR_CLOSE) \
V(SQLITE_IOERR_DIR_CLOSE) \
V(SQLITE_IOERR_SHMOPEN) \
V(SQLITE_IOERR_SHMSIZE) \
V(SQLITE_IOERR_SHMLOCK) \
V(SQLITE_IOERR_SHMMAP) \
V(SQLITE_IOERR_SEEK) \
V(SQLITE_IOERR_DELETE_NOENT) \
V(SQLITE_IOERR_MMAP) \
V(SQLITE_IOERR_GETTEMPPATH) \
V(SQLITE_IOERR_CONVPATH) \
V(SQLITE_IOERR_VNODE) \
V(SQLITE_IOERR_AUTH) \
V(SQLITE_IOERR_BEGIN_ATOMIC) \
V(SQLITE_IOERR_COMMIT_ATOMIC) \
V(SQLITE_IOERR_ROLLBACK_ATOMIC) \
V(SQLITE_IOERR_DATA) \
V(SQLITE_IOERR_CORRUPTFS) \
V(SQLITE_IOERR_IN_PAGE) \
V(SQLITE_LOCKED_SHAREDCACHE) \
V(SQLITE_LOCKED_VTAB) \
V(SQLITE_BUSY_RECOVERY) \
V(SQLITE_BUSY_SNAPSHOT) \
V(SQLITE_BUSY_TIMEOUT) \
V(SQLITE_CANTOPEN_NOTEMPDIR) \
V(SQLITE_CANTOPEN_ISDIR) \
V(SQLITE_CANTOPEN_FULLPATH) \
V(SQLITE_CANTOPEN_CONVPATH) \
V(SQLITE_CANTOPEN_DIRTYWAL) \
V(SQLITE_CANTOPEN_SYMLINK) \
V(SQLITE_CORRUPT_VTAB) \
V(SQLITE_CORRUPT_SEQUENCE) \
V(SQLITE_CORRUPT_INDEX) \
V(SQLITE_READONLY_RECOVERY) \
V(SQLITE_READONLY_CANTLOCK) \
V(SQLITE_READONLY_ROLLBACK) \
V(SQLITE_READONLY_DBMOVED) \
V(SQLITE_READONLY_CANTINIT) \
V(SQLITE_READONLY_DIRECTORY) \
V(SQLITE_ABORT_ROLLBACK) \
V(SQLITE_CONSTRAINT_CHECK) \
V(SQLITE_CONSTRAINT_COMMITHOOK) \
V(SQLITE_CONSTRAINT_FOREIGNKEY) \
V(SQLITE_CONSTRAINT_FUNCTION) \
V(SQLITE_CONSTRAINT_NOTNULL) \
V(SQLITE_CONSTRAINT_PRIMARYKEY) \
V(SQLITE_CONSTRAINT_TRIGGER) \
V(SQLITE_CONSTRAINT_UNIQUE) \
V(SQLITE_CONSTRAINT_VTAB) \
V(SQLITE_CONSTRAINT_ROWID) \
V(SQLITE_CONSTRAINT_PINNED) \
V(SQLITE_CONSTRAINT_DATATYPE) \
V(SQLITE_NOTICE_RECOVER_WAL) \
V(SQLITE_NOTICE_RECOVER_ROLLBACK) \
V(SQLITE_NOTICE_RBU) \
V(SQLITE_WARNING_AUTOINDEX) \
V(SQLITE_AUTH_USER) \
V(SQLITE_OK_LOAD_PERMANENTLY) \
V(SQLITE_OK_SYMLINK)
#endif // SRC_ERRORS_H_

View File

@ -1,7 +1,7 @@
import { mkdtemp, rm } from 'node:fs/promises'; import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; 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'; import Database from '../lib/index.js';
@ -65,80 +65,3 @@ test.each([[false], [true]])('ciphertext=%j', (ciphertext) => {
expect(row).toEqual({ name: 'Adam', value: 'Sandler' }); 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,6 +1,6 @@
import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest'; import { describe, expect, test, beforeEach, afterEach } from 'vitest';
import Database, { setLogger } from '../lib/index.js'; import Database from '../lib/index.js';
const rows = [ const rows = [
{ {
@ -49,16 +49,6 @@ test('db.close', () => {
db = new Database(); db = new Database();
}); });
test('db.close with existing statement', () => {
const stmt = db.prepare('SELECT 1');
db.close();
expect(() => stmt.run()).toThrowError('Statement closed');
// Just to fix afterEach
db = new Database();
});
test('statement.close', () => { test('statement.close', () => {
const stmt = db.prepare('SELECT 1'); const stmt = db.prepare('SELECT 1');
stmt.close(); stmt.close();
@ -180,21 +170,6 @@ test('persistent statement recompilation', () => {
}); });
}); });
test('setLogger should log on statement recompilation', () => {
const messages = new Array<{ code: string; message: string }>();
setLogger((code, message) => messages.push({ code, message }));
const stmt = db.prepare('SELECT * FROM t', { persistent: true });
db.exec(`ALTER TABLE t ADD COLUMN d TEXT DEFAULT 'hello'`);
expect(stmt.get()).not.toBeUndefined();
expect(messages).toHaveLength(1);
expect(messages[0]?.code).toEqual('SQLITE_SCHEMA');
expect(messages[0]?.message).toMatch(/database schema has changed/);
});
describe('list parameters', () => { describe('list parameters', () => {
test('correct count', () => { test('correct count', () => {
expect(db.prepare('SELECT * FROM t WHERE a > ?').get([2])).toEqual(rows[2]); expect(db.prepare('SELECT * FROM t WHERE a > ?').get([2])).toEqual(rows[2]);
@ -212,16 +187,12 @@ describe('list parameters', () => {
test('object parameters', () => { test('object parameters', () => {
const stmt = db.prepare('SELECT * FROM t WHERE a > ?'); const stmt = db.prepare('SELECT * FROM t WHERE a > ?');
expect(() => stmt.get({})).toThrowError( expect(() => stmt.get({})).toThrowError('Unexpected anonymous param at 1');
'Query requires an array of anonymous params',
);
}); });
test('against named parameters', () => { test('against named parameters', () => {
const stmt = db.prepare('SELECT * FROM t WHERE a > $a'); const stmt = db.prepare('SELECT * FROM t WHERE a > $a');
expect(() => stmt.get([2])).toThrowError( expect(() => stmt.get([2])).toThrowError('Unexpected named param $a at 1');
'Query requires an object of named params',
);
}); });
}); });
@ -243,6 +214,13 @@ describe('object parameters', () => {
const stmt = db.prepare('SELECT * FROM t WHERE a > $a'); const stmt = db.prepare('SELECT * FROM t WHERE a > $a');
expect(() => stmt.get()).toThrowError('Expected 1 parameters, got 0'); expect(() => stmt.get()).toThrowError('Expected 1 parameters, got 0');
}); });
test('against anonymous parameters', () => {
const stmt = db.prepare('SELECT * FROM t WHERE a > ?');
expect(() => stmt.get({ a: 1 })).toThrowError(
'Unexpected anonymous param at 1',
);
});
}); });
describe('tail', () => { describe('tail', () => {
@ -402,37 +380,6 @@ test('bigint mode', () => {
).toEqual(n); ).toEqual(n);
}); });
test('extended error codes', () => {
db.exec(`
CREATE TABLE parent (id INTEGER PRIMARY KEY);
CREATE TABLE refs (id INTEGER, FOREIGN KEY (id) REFERENCES parent(id));
`);
const stmt = db.prepare('INSERT INTO refs (id) VALUES (?)');
expect(() => stmt.run([4])).toThrowError(
expect.objectContaining({
message:
'sqlite error(SQLITE_CONSTRAINT_FOREIGNKEY): ' +
'FOREIGN KEY constraint failed',
code: 'SQLITE_CONSTRAINT_FOREIGNKEY',
}),
);
});
test('tokenizer setup', () => {
db.initTokenizer();
});
test('tokenizer setup after close', () => {
db.close();
expect(() => db.initTokenizer()).toThrowError('Database closed');
// Just to fix afterEach
db = new Database();
});
test('signalTokenize', () => { test('signalTokenize', () => {
expect(db.signalTokenize('a b c')).toEqual(['a', 'b', 'c']); expect(db.signalTokenize('a b c')).toEqual(['a', 'b', 'c']);
}); });
@ -497,51 +444,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');
});
});