Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f90dd065e9 | ||
|
|
9e947e02ae | ||
|
|
ef6443f848 | ||
|
|
84a54d03d5 | ||
|
|
d27e1d9a27 | ||
|
|
19eb15285d | ||
|
|
c5c58ee0c4 | ||
|
|
d755f4997e | ||
|
|
3df7bb3fda | ||
|
|
14ea17da26 | ||
|
|
c2ba9fcc16 | ||
|
|
788376d0cf | ||
|
|
f6c6098522 | ||
|
|
341ba5387b | ||
|
|
0180ac2982 | ||
|
|
059c56e5e1 | ||
|
|
8ca829224f | ||
|
|
e303f7f333 | ||
|
|
8f625a6628 | ||
|
|
e7dad7dc6e | ||
|
|
4ef618cee4 | ||
|
|
e7aa532457 | ||
|
|
a7bf1b5a41 | ||
|
|
dcc79168bf | ||
|
|
808689b6ef | ||
|
|
c50acaa2ed | ||
|
|
db0e2cdde6 | ||
|
|
d5a55f168b | ||
|
|
e9110912fb | ||
|
|
102d205512 | ||
|
|
c41fadeb7a | ||
|
|
ac72ab5354 | ||
|
|
e528fa8aaa | ||
|
|
ee6c652c6b |
15
.github/workflows/publish.yaml
vendored
15
.github/workflows/publish.yaml
vendored
@ -40,7 +40,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.3.0
|
version: 10.18.1
|
||||||
|
|
||||||
- name: Get Node version from .nvmrc
|
- name: Get Node version from .nvmrc
|
||||||
id: get-nvm-version
|
id: get-nvm-version
|
||||||
@ -90,7 +90,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.3.0
|
version: 10.18.1
|
||||||
|
|
||||||
- name: Get Node version from .nvmrc
|
- name: Get Node version from .nvmrc
|
||||||
id: get-nvm-version
|
id: get-nvm-version
|
||||||
@ -119,6 +119,8 @@ jobs:
|
|||||||
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'
|
||||||
|
|
||||||
@ -135,13 +137,16 @@ 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.3.0
|
version: 10.18.1
|
||||||
- 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
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
- name: Download built production libraries
|
- name: Download built production libraries
|
||||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.19.1
|
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.19.1
|
||||||
with:
|
with:
|
||||||
@ -165,8 +170,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish production
|
- name: Publish production
|
||||||
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: |
|
||||||
@ -205,5 +208,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish profiling
|
- name: Publish profiling
|
||||||
run: pnpm publish --tag 'profiling' --access public --no-git-checks ${{ inputs.dry_run && '--dry-run' || ''}}
|
run: pnpm publish --tag 'profiling' --access public --no-git-checks ${{ inputs.dry_run && '--dry-run' || ''}}
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|||||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@ -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.3.0
|
version: 10.18.1
|
||||||
- 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
1
.gitignore
vendored
@ -7,3 +7,4 @@ docs/
|
|||||||
.tmp/
|
.tmp/
|
||||||
.eslintcache
|
.eslintcache
|
||||||
todo.md
|
todo.md
|
||||||
|
.vscode
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@ -2,7 +2,7 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
#
|
#
|
||||||
|
|
||||||
FROM ubuntu:focal-20240530@sha256:fa17826afb526a9fc7250e0fbcbfd18d03fe7a54849472f86879d8bf562c629e
|
FROM ubuntu:jammy-20250714@sha256:1ec65b2719518e27d4d25f104d93f9fac60dc437f81452302406825c46fcc9cb
|
||||||
|
|
||||||
# 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,13 +15,15 @@ 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-get update -oAcquire::https::Verify-Peer=false \
|
RUN apt update -oAcquire::https::Verify-Peer=false
|
||||||
&& apt-get install -oAcquire::https::Verify-Peer=false -y ca-certificates
|
RUN apt 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-get update && apt-get install -y apt-transport-https xz-utils unzip
|
RUN apt update
|
||||||
|
RUN apt install -y apt-transport-https xz-utils unzip
|
||||||
|
|
||||||
# User-specific setup!
|
# User-specific setup!
|
||||||
|
|
||||||
@ -65,6 +67,9 @@ 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
|
||||||
|
|||||||
@ -2,7 +2,8 @@ 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 Database from '../lib/index.js';
|
import { DatabaseSync as NDatabase } from 'node:sqlite';
|
||||||
|
import Database from '../dist/index.mjs';
|
||||||
|
|
||||||
const PREPARE = `
|
const PREPARE = `
|
||||||
CREATE TABLE t (
|
CREATE TABLE t (
|
||||||
@ -21,12 +22,15 @@ 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',
|
||||||
@ -51,4 +55,16 @@ describe('INSERT INTO t', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
bench(
|
||||||
|
'node:sqlite',
|
||||||
|
() => {
|
||||||
|
ninsert.run({ b: BLOB });
|
||||||
|
},
|
||||||
|
{
|
||||||
|
teardown: () => {
|
||||||
|
ndb.exec(DELETE);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { bench, describe } from 'vitest';
|
import { bench, describe } from 'vitest';
|
||||||
|
|
||||||
import BDatabase from '@signalapp/better-sqlite3';
|
import BDatabase from '@signalapp/better-sqlite3';
|
||||||
import Database from '../lib/index.js';
|
import { DatabaseSync as NDatabase } from 'node:sqlite';
|
||||||
|
import Database from '../dist/index.mjs';
|
||||||
|
|
||||||
const PREPARE = `
|
const PREPARE = `
|
||||||
CREATE TABLE t (
|
CREATE TABLE t (
|
||||||
@ -24,12 +25,15 @@ 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',
|
||||||
@ -54,4 +58,16 @@ 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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { bench, describe } from 'vitest';
|
import { bench, describe } from 'vitest';
|
||||||
|
|
||||||
import BDatabase from '@signalapp/better-sqlite3';
|
import BDatabase from '@signalapp/better-sqlite3';
|
||||||
import Database from '../lib/index.js';
|
import { DatabaseSync as NDatabase } from 'node:sqlite';
|
||||||
|
import Database from '../dist/index.mjs';
|
||||||
|
|
||||||
const PREPARE = `
|
const PREPARE = `
|
||||||
CREATE TABLE t (
|
CREATE TABLE t (
|
||||||
@ -36,12 +37,15 @@ 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) {
|
||||||
@ -55,6 +59,12 @@ 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);
|
||||||
|
|
||||||
@ -65,4 +75,10 @@ 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
14
deps/extension/Cargo.lock
generated
vendored
@ -425,6 +425,17 @@ 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"
|
||||||
@ -454,7 +465,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-sqlcipher-extension"
|
name = "signal-sqlcipher-extension"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"cbc",
|
"cbc",
|
||||||
@ -462,6 +473,7 @@ dependencies = [
|
|||||||
"hmac",
|
"hmac",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
|
"sha1",
|
||||||
"sha2",
|
"sha2",
|
||||||
"signal-tokenizer",
|
"signal-tokenizer",
|
||||||
]
|
]
|
||||||
|
|||||||
3
deps/extension/Cargo.toml
vendored
3
deps/extension/Cargo.toml
vendored
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "signal-sqlcipher-extension"
|
name = "signal-sqlcipher-extension"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ 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" }
|
||||||
|
|
||||||
|
|||||||
56
deps/extension/src/lib.rs
vendored
56
deps/extension/src/lib.rs
vendored
@ -10,6 +10,7 @@ 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;
|
||||||
@ -70,6 +71,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,9 +87,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -99,16 +98,34 @@ 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) })
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(mut mac) = Hmac::<Sha512>::new_from_slice(key) else {
|
match algorithm {
|
||||||
return SQLITE_ERROR;
|
SQLCIPHER_HMAC_SHA512 => {
|
||||||
};
|
let Ok(mut mac) = Hmac::<Sha512>::new_from_slice(key) else {
|
||||||
mac.update(in1);
|
return SQLITE_ERROR;
|
||||||
if let Some(in2) = in2 {
|
};
|
||||||
mac.update(in2);
|
mac.update(in1);
|
||||||
}
|
if let Some(in2) = in2 {
|
||||||
let digest = mac.finalize().into_bytes();
|
mac.update(in2);
|
||||||
unsafe {
|
}
|
||||||
out.copy_from(digest.as_ptr(), digest.len());
|
let digest = mac.finalize().into_bytes();
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@ -124,16 +141,21 @@ 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) };
|
||||||
pbkdf2_hmac::<Sha512>(password, salt, workfactor as u32, buf);
|
match algorithm {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
deps/extension/src/sqlcipher.rs
vendored
4
deps/extension/src/sqlcipher.rs
vendored
@ -10,6 +10,10 @@ 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)]
|
||||||
|
|||||||
22
deps/sqlcipher/sqlcipher.gyp
vendored
22
deps/sqlcipher/sqlcipher.gyp
vendored
@ -21,13 +21,16 @@
|
|||||||
'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_DEFAULT_MEMSTATUS=0',
|
'SQLITE_USE_ALLOCA',
|
||||||
'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',
|
||||||
@ -36,22 +39,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_TRACE_SIZE_LIMIT=32',
|
'SQLITE_OMIT_INTROSPECTION_PRAGMAS',
|
||||||
|
'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',
|
||||||
|
|
||||||
@ -98,6 +101,13 @@
|
|||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
|
|
||||||
|
# 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
|
||||||
["\"-profiling.\" in \"<!(node -p \"require('../../package.json').version\")\"", {
|
["\"-profiling.\" in \"<!(node -p \"require('../../package.json').version\")\"", {
|
||||||
'defines': [
|
'defines': [
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
deb [snapshot=20240829T060900Z] http://archive.ubuntu.com/ubuntu/ focal main universe
|
deb [snapshot=20250811T060900Z] http://archive.ubuntu.com/ubuntu/ jammy main universe
|
||||||
deb [snapshot=20240829T060900Z] http://archive.ubuntu.com/ubuntu/ focal-updates main universe
|
deb [snapshot=20250811T060900Z] http://archive.ubuntu.com/ubuntu/ jammy-updates main universe
|
||||||
deb [snapshot=20240829T060900Z] http://security.ubuntu.com/ubuntu focal-security main universe
|
deb [snapshot=20250811T060900Z] http://security.ubuntu.com/ubuntu jammy-security main universe
|
||||||
|
|||||||
144
lib/index.ts
144
lib/index.ts
@ -27,15 +27,16 @@ 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: StatementParameters<Options> | undefined,
|
params: NativeParameters<Options> | undefined,
|
||||||
result: [number, number],
|
result: [number, number],
|
||||||
): void;
|
): void;
|
||||||
statementStep<Options extends StatementOptions>(
|
statementStep<Options extends StatementOptions>(
|
||||||
stmt: NativeStatement,
|
stmt: NativeStatement,
|
||||||
params: StatementParameters<Options> | null | undefined,
|
params: NativeParameters<Options> | null | undefined,
|
||||||
cache: Array<SqliteValue<Options>> | undefined,
|
cache: Array<SqliteValue<Options>> | undefined,
|
||||||
isGet: boolean,
|
isGet: boolean,
|
||||||
): Array<SqliteValue<Options>>;
|
): Array<SqliteValue<Options>>;
|
||||||
@ -46,6 +47,16 @@ const addon = bindings<{
|
|||||||
databaseInitTokenizer(db: NativeDatabase): void;
|
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>;
|
||||||
|
|
||||||
@ -85,11 +96,15 @@ 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> =
|
||||||
| ReadonlyArray<SqliteValue<Options>>
|
| NativeParameters<Options>
|
||||||
| Readonly<Record<string, SqliteValue<Options>>>;
|
| Readonly<Record<string, SqliteValue<Options>>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,7 +112,7 @@ export type StatementParameters<Options extends StatementOptions> =
|
|||||||
*/
|
*/
|
||||||
export type SqliteValue<Options extends StatementOptions> =
|
export type SqliteValue<Options extends StatementOptions> =
|
||||||
| string
|
| string
|
||||||
| Uint8Array
|
| Uint8Array<ArrayBuffer>
|
||||||
| number
|
| number
|
||||||
| null
|
| null
|
||||||
| (Options extends { bigint: true } ? bigint : never);
|
| (Options extends { bigint: true } ? bigint : never);
|
||||||
@ -111,6 +126,14 @@ 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.
|
||||||
*/
|
*/
|
||||||
@ -119,6 +142,9 @@ 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;
|
||||||
|
|
||||||
@ -131,14 +157,47 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,8 +213,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];
|
||||||
this.#checkParams(params);
|
const nativeParams = this.#checkParams(params);
|
||||||
addon.statementRun(this.#native, params, result);
|
addon.statementRun(this.#native, nativeParams, result);
|
||||||
return { changes: result[0], lastInsertRowid: result[1] };
|
return { changes: result[0], lastInsertRowid: result[1] };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,8 +233,13 @@ class Statement<Options extends StatementOptions = object> {
|
|||||||
if (this.#native === undefined) {
|
if (this.#native === undefined) {
|
||||||
throw new Error('Statement closed');
|
throw new Error('Statement closed');
|
||||||
}
|
}
|
||||||
this.#checkParams(params);
|
const nativeParams = this.#checkParams(params);
|
||||||
const result = addon.statementStep(this.#native, params, this.#cache, true);
|
const result = addon.statementStep(
|
||||||
|
this.#native,
|
||||||
|
nativeParams,
|
||||||
|
this.#cache,
|
||||||
|
true,
|
||||||
|
);
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -202,9 +266,8 @@ class Statement<Options extends StatementOptions = object> {
|
|||||||
throw new Error('Statement closed');
|
throw new Error('Statement closed');
|
||||||
}
|
}
|
||||||
const result = [];
|
const result = [];
|
||||||
this.#checkParams(params);
|
const nativeParams = this.#checkParams(params);
|
||||||
let singleUseParams: StatementParameters<Options> | undefined | null =
|
let singleUseParams: typeof nativeParams | undefined | null = nativeParams;
|
||||||
params;
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const single = addon.statementStep(
|
const single = addon.statementStep(
|
||||||
this.#native,
|
this.#native,
|
||||||
@ -282,9 +345,11 @@ class Statement<Options extends StatementOptions = object> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
#checkParams(params: StatementParameters<Options> | undefined): void {
|
#checkParams(
|
||||||
|
params: StatementParameters<Options> | undefined,
|
||||||
|
): NativeParameters<Options> | undefined {
|
||||||
if (params === undefined) {
|
if (params === undefined) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
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');
|
||||||
@ -292,6 +357,7 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,6 +413,13 @@ 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.
|
||||||
*/
|
*/
|
||||||
@ -404,6 +477,51 @@ 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.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"packageManager": "pnpm@10.18.1",
|
||||||
"name": "@signalapp/sqlcipher",
|
"name": "@signalapp/sqlcipher",
|
||||||
"version": "2.4.3",
|
"version": "3.3.5",
|
||||||
"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",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
nightly-2025-02-25
|
nightly-2025-09-24
|
||||||
|
|||||||
245
src/addon.cc
245
src/addon.cc
@ -74,6 +74,122 @@ 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
|
// Global Settings
|
||||||
|
|
||||||
thread_local Napi::Reference<Napi::Function> logger_fn_;
|
thread_local Napi::Reference<Napi::Function> logger_fn_;
|
||||||
@ -101,10 +217,15 @@ static void LoggerWrapper(void* _ctx, int code, const char* msg) {
|
|||||||
|
|
||||||
#undef CODE_STR
|
#undef CODE_STR
|
||||||
|
|
||||||
logger_fn_.Value().Call({
|
auto result = logger_fn_.Value().Call({
|
||||||
Napi::String::New(env, code_name),
|
Napi::String::New(env, code_name),
|
||||||
Napi::String::New(env, msg),
|
Napi::String::New(env, msg),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ignore exceptions
|
||||||
|
if (result.IsEmpty()) {
|
||||||
|
env.GetAndClearPendingException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SetLogger(const Napi::CallbackInfo& info) {
|
static void SetLogger(const Napi::CallbackInfo& info) {
|
||||||
@ -145,6 +266,10 @@ Napi::Object Database::Init(Napi::Env env, Napi::Object exports) {
|
|||||||
Napi::Function::New(env, &Database::InitTokenizer);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +285,9 @@ 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");
|
||||||
@ -251,6 +379,9 @@ 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);
|
||||||
@ -284,6 +415,68 @@ 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_);
|
||||||
@ -406,12 +599,14 @@ 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();
|
||||||
|
|
||||||
@ -440,6 +635,18 @@ 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; });
|
||||||
}
|
}
|
||||||
@ -733,36 +940,18 @@ 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) {
|
||||||
NAPI_THROW(
|
if (name == nullptr) {
|
||||||
FormatError(env, "Failed to bind param %d, error %s", i, error),
|
NAPI_THROW(
|
||||||
false);
|
FormatError(env, "Failed to bind param %d, error %s", i, error),
|
||||||
}
|
false);
|
||||||
}
|
} else {
|
||||||
} else {
|
NAPI_THROW(FormatError(env, "Failed to bind param %s, error %s",
|
||||||
auto obj = params.As<Napi::Object>();
|
name + 1, error),
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include "sqlite3.h"
|
#include "sqlite3.h"
|
||||||
|
|
||||||
class Statement;
|
class Statement;
|
||||||
|
class WalHookWrap;
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
public:
|
public:
|
||||||
@ -30,6 +31,8 @@ class Database {
|
|||||||
static Napi::Value InitTokenizer(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);
|
||||||
|
|
||||||
@ -44,6 +47,8 @@ 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 {
|
||||||
|
|||||||
@ -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, beforeEach, afterEach } from 'vitest';
|
import { expect, test, describe, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
|
||||||
import Database from '../lib/index.js';
|
import Database from '../lib/index.js';
|
||||||
|
|
||||||
@ -65,3 +65,80 @@ 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, test, beforeEach, afterEach } from 'vitest';
|
import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
|
||||||
import Database, { setLogger } from '../lib/index.js';
|
import Database, { setLogger } from '../lib/index.js';
|
||||||
|
|
||||||
@ -212,12 +212,16 @@ 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('Unexpected anonymous param at 1');
|
expect(() => stmt.get({})).toThrowError(
|
||||||
|
'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('Unexpected named param $a at 1');
|
expect(() => stmt.get([2])).toThrowError(
|
||||||
|
'Query requires an object of named params',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -239,13 +243,6 @@ 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', () => {
|
||||||
@ -500,3 +497,51 @@ 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user