Compare commits
10 Commits
better-sql
...
feature/lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f66ee7b85 | ||
|
|
92ed9e3635 | ||
|
|
9de3488f68 | ||
|
|
917a6f5cf8 | ||
|
|
32828e03be | ||
|
|
2fa02d2484 | ||
|
|
a78376d86b | ||
|
|
86b685e046 | ||
|
|
a78e94135c | ||
|
|
5acdaf9d03 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,2 +1,2 @@
|
||||
*.c -diff
|
||||
*.h -diff
|
||||
*.lzz linguist-language=C++
|
||||
deps/sqlite3.tar.gz filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@ -1,9 +0,0 @@
|
||||
* @JoshuaWise
|
||||
/package.json @JoshuaWise @WiseLibs/better-sqlite3-team
|
||||
/docs/compilation.md @JoshuaWise @WiseLibs/better-sqlite3-team
|
||||
/docs/performance.md @JoshuaWise @WiseLibs/better-sqlite3-team
|
||||
/docs/troubleshooting.md @JoshuaWise @WiseLibs/better-sqlite3-team
|
||||
/deps/sqlite3/sqlite3.c @JoshuaWise @WiseLibs/better-sqlite3-team
|
||||
/deps/sqlite3/sqlite3.h @JoshuaWise @WiseLibs/better-sqlite3-team
|
||||
/deps/sqlite3/sqlite3ext.h @JoshuaWise @WiseLibs/better-sqlite3-team
|
||||
/.github/workflows/build.yml @JoshuaWise @WiseLibs/better-sqlite3-team
|
||||
75
.github/workflows/build.yml
vendored
75
.github/workflows/build.yml
vendored
@ -10,7 +10,6 @@ on:
|
||||
release:
|
||||
types:
|
||||
- released
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
|
||||
@ -18,13 +17,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-20.04
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-2019
|
||||
- windows-latest
|
||||
node:
|
||||
- 10
|
||||
- 12
|
||||
- 14
|
||||
- 16
|
||||
- 18
|
||||
name: Testing Node ${{ matrix.node }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@ -39,13 +38,13 @@ jobs:
|
||||
publish:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
name: Publishing to NPM
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: npm publish
|
||||
env:
|
||||
@ -55,9 +54,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-18.04
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-2019
|
||||
- windows-latest
|
||||
name: Prebuild on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: publish
|
||||
@ -65,59 +64,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 14
|
||||
- run: npm install --ignore-scripts
|
||||
- run: npx --no-install prebuild -r node -t 14.0.0 -t 16.0.0 -t 18.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: npx --no-install prebuild -r electron -t 16.0.0 -t 17.0.0 -t 18.0.0 -t 19.0.0 -t 20.0.0 -t 21.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
|
||||
- if: matrix.os == 'windows-2019'
|
||||
run: npx --no-install prebuild -r electron -t 16.0.0 -t 17.0.0 -t 18.0.0 -t 19.0.0 -t 20.0.0 -t 21.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' --arch ia32 -u ${{ secrets.GITHUB_TOKEN }}
|
||||
- if: matrix.os == 'macos-latest'
|
||||
run: npx --no-install prebuild -r electron -t 16.0.0 -t 17.0.0 -t 18.0.0 -t 19.0.0 -t 20.0.0 -t 21.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' --arch arm64 -u ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
prebuild-alpine:
|
||||
name: Prebuild on alpine
|
||||
runs-on: ubuntu-latest
|
||||
container: node:16-alpine
|
||||
needs: publish
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: apk add build-base git python3 --update-cache
|
||||
- run: npm install --ignore-scripts
|
||||
- run: npx --no-install prebuild -r node -t 14.0.0 -t 16.0.0 -t 18.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
prebuild-alpine-arm:
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- arm/v7
|
||||
- arm64
|
||||
name: Prebuild on alpine (${{ matrix.arch }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- run: |
|
||||
docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:16-alpine -c "\
|
||||
apk add build-base git python3 --update-cache && \
|
||||
cd /tmp/project && \
|
||||
npm install --ignore-scripts && \
|
||||
npx --no-install prebuild -r node -t 14.0.0 -t 16.0.0 -t 18.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
prebuild-linux-arm:
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- arm/v7
|
||||
- arm64
|
||||
name: Prebuild on Linux (${{ matrix.arch }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- run: |
|
||||
docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:16 -c "\
|
||||
cd /tmp/project && \
|
||||
npm install --ignore-scripts && \
|
||||
npx --no-install prebuild -r node -t 14.0.0 -t 16.0.0 -t 18.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}"
|
||||
- run: npx --no-install prebuild -r node -t 10.20.0 -t 12.0.0 -t 14.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: npx --no-install prebuild -r electron -t 10.0.0 -t 11.0.0 -t 12.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
36
.github/workflows/bump-version.yml
vendored
36
.github/workflows/bump-version.yml
vendored
@ -1,36 +0,0 @@
|
||||
name: bump-version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
type:
|
||||
type: choice
|
||||
description: Type of version bump
|
||||
required: true
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Bump to a new version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Configure user
|
||||
run: |
|
||||
git config --local user.name "${{ github.actor }}"
|
||||
git config --local user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
- name: Bump the version
|
||||
run: npm version ${{ github.event.inputs.type }}
|
||||
- name: Push commit
|
||||
run: git push origin master:master
|
||||
- name: Push tag
|
||||
run: git push origin --tags
|
||||
48
.github/workflows/update-sqlite.yml
vendored
48
.github/workflows/update-sqlite.yml
vendored
@ -1,48 +0,0 @@
|
||||
name: update-sqlite
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
year:
|
||||
description: SQLite release year
|
||||
required: true
|
||||
version:
|
||||
description: SQLite version (encoded)
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
download-and-update:
|
||||
name: Download and update SQLite
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ENV_YEAR: ${{ github.event.inputs.year }}
|
||||
ENV_VERSION: ${{ github.event.inputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Create new update branch
|
||||
run: git checkout -b sqlite-update-${{ env.ENV_VERSION }}
|
||||
- name: Update download script
|
||||
run: |
|
||||
sed -Ei "s/YEAR=\"[0-9]+\"/YEAR=\"${{ env.ENV_YEAR }}\"/g" ./deps/download.sh
|
||||
sed -Ei "s/VERSION=\"[0-9]+\"/VERSION=\"${{ env.ENV_VERSION }}\"/g" ./deps/download.sh
|
||||
echo "ENV_TRUE_VERSION=$((10#${ENV_VERSION:0:1})).$((10#${ENV_VERSION:1:2})).$((10#${ENV_VERSION:3:2}))" >> $GITHUB_ENV
|
||||
- name: Download, compile and package SQLite
|
||||
run: npm run download
|
||||
- name: Push update branch
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Update SQLite to version ${{ env.ENV_TRUE_VERSION }}
|
||||
branch: sqlite-update-${{ env.ENV_VERSION }}
|
||||
- name: Create new PR
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
github_token: ${{ secrets.PAT }}
|
||||
source_branch: sqlite-update-${{ env.ENV_VERSION }}
|
||||
pr_title: Update SQLite to version ${{ env.ENV_TRUE_VERSION }}
|
||||
pr_body: This is an automated pull request, updating SQLite to version \`${{ env.ENV_TRUE_VERSION }}\`.
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -40,7 +40,3 @@ lib/binding
|
||||
temp/
|
||||
TODO
|
||||
.local
|
||||
|
||||
# Downloaded artifact
|
||||
deps/unverified.tmp
|
||||
deps/sqlcipher.tar.gz
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
# Acknowledgments
|
||||
|
||||
## @types/better-sqlite3
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
24
README.md
24
README.md
@ -5,7 +5,7 @@ The fastest and simplest library for SQLite3 in Node.js.
|
||||
- Full transaction support
|
||||
- High performance, efficiency, and safety
|
||||
- Easy-to-use synchronous API *(better concurrency than an asynchronous API... yes, you read that correctly)*
|
||||
- Support for user-defined functions, aggregates, virtual tables, and extensions
|
||||
- Support for user-defined functions, aggregates, and extensions
|
||||
- 64-bit integers *(invisible until you need them)*
|
||||
- Worker thread support *(for large/slow queries)*
|
||||
|
||||
@ -32,7 +32,9 @@ The fastest and simplest library for SQLite3 in Node.js.
|
||||
npm install better-sqlite3
|
||||
```
|
||||
|
||||
> You must be using Node.js v14.21.1 or above. Prebuilt binaries are available for [LTS versions](https://nodejs.org/en/about/releases/). If you have trouble installing, check the [troubleshooting guide](./docs/troubleshooting.md).
|
||||
> You must be using Node.js v10.20.1 or above. Prebuilt binaries are available for [LTS versions](https://nodejs.org/en/about/releases/).
|
||||
|
||||
> If you have trouble installing, check the [troubleshooting guide](./docs/troubleshooting.md).
|
||||
|
||||
## Usage
|
||||
|
||||
@ -43,20 +45,6 @@ const row = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
|
||||
console.log(row.firstName, row.lastName, row.email);
|
||||
```
|
||||
|
||||
Though not required, [it is generally important to set the WAL pragma for performance reasons](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/performance.md).
|
||||
|
||||
```js
|
||||
db.pragma('journal_mode = WAL');
|
||||
```
|
||||
|
||||
##### In ES6 module notation:
|
||||
|
||||
```js
|
||||
import Database from 'better-sqlite3';
|
||||
const db = new Database('foobar.db', options);
|
||||
db.pragma('journal_mode = WAL');
|
||||
```
|
||||
|
||||
## Why should I use this instead of [node-sqlite3](https://github.com/mapbox/node-sqlite3)?
|
||||
|
||||
- `node-sqlite3` uses asynchronous APIs for tasks that are either CPU-bound or serialized. That's not only bad design, but it wastes tons of resources. It also causes [mutex thrashing](https://en.wikipedia.org/wiki/Resource_contention) which has devastating effects on performance.
|
||||
@ -83,9 +71,7 @@ For these situations, you should probably use a full-fledged RDBMS such as [Post
|
||||
- [64-bit integer support](./docs/integer.md)
|
||||
- [Worker thread support](./docs/threads.md)
|
||||
- [Unsafe mode (advanced)](./docs/unsafe.md)
|
||||
- [SQLite3 compilation (advanced)](./docs/compilation.md)
|
||||
- [Contribution rules](./docs/contribution.md)
|
||||
- [Code of conduct](./docs/conduct.md)
|
||||
- [SQLite3 compilation](./docs/compilation.md)
|
||||
|
||||
# License
|
||||
|
||||
|
||||
15
binding.gyp
15
binding.gyp
@ -9,13 +9,9 @@
|
||||
'target_name': 'better_sqlite3',
|
||||
'dependencies': ['deps/sqlite3.gyp:sqlite3'],
|
||||
'sources': ['src/better_sqlite3.cpp'],
|
||||
'cflags_cc': ['-std=c++20'],
|
||||
'msvs_settings': {
|
||||
'VCCLCompilerTool': {
|
||||
'AdditionalOptions': [
|
||||
'/std:c++20',
|
||||
],
|
||||
},
|
||||
'cflags': ['-std=c++14'],
|
||||
'xcode_settings': {
|
||||
'OTHER_CPLUSPLUSFLAGS': ['-std=c++14', '-stdlib=libc++'],
|
||||
},
|
||||
'conditions': [
|
||||
['OS=="linux"', {
|
||||
@ -26,5 +22,10 @@
|
||||
}],
|
||||
],
|
||||
},
|
||||
{
|
||||
'target_name': 'test_extension',
|
||||
'dependencies': ['deps/sqlite3.gyp:sqlite3'],
|
||||
'conditions': [['sqlite3 == ""', { 'sources': ['deps/test_extension.c'] }]],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
31
deps/common.gypi
vendored
31
deps/common.gypi
vendored
@ -13,20 +13,19 @@
|
||||
},
|
||||
},
|
||||
'conditions': [
|
||||
['target_arch == "x64"', {
|
||||
'variables': {
|
||||
'rust_arch%': 'x86_64',
|
||||
}
|
||||
}, {
|
||||
'variables': {
|
||||
'rust_arch%': 'aarch64',
|
||||
}
|
||||
}],
|
||||
['OS == "win"', {
|
||||
'defines': ['WIN32'],
|
||||
'variables': {
|
||||
'openssl_root%': 'OpenSSL-win-<(target_arch)',
|
||||
}
|
||||
'conditions': [
|
||||
['target_arch == "ia32"', {
|
||||
'variables': {
|
||||
'openssl_root%': 'OpenSSL-Win32',
|
||||
}
|
||||
}, {
|
||||
'variables': {
|
||||
'openssl_root%': 'OpenSSL-Win64',
|
||||
}
|
||||
}]
|
||||
],
|
||||
}],
|
||||
],
|
||||
'configurations': {
|
||||
@ -46,7 +45,7 @@
|
||||
'-O0',
|
||||
],
|
||||
'xcode_settings': {
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.15',
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.7',
|
||||
'GCC_OPTIMIZATION_LEVEL': '0',
|
||||
'GCC_GENERATE_DEBUGGING_SYMBOLS': 'YES',
|
||||
},
|
||||
@ -68,15 +67,11 @@
|
||||
'-O3',
|
||||
],
|
||||
'xcode_settings': {
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.15',
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.7',
|
||||
'GCC_OPTIMIZATION_LEVEL': '3',
|
||||
'GCC_GENERATE_DEBUGGING_SYMBOLS': 'NO',
|
||||
'DEAD_CODE_STRIPPING': 'YES',
|
||||
'GCC_INLINES_ARE_PRIVATE_EXTERN': 'YES',
|
||||
'OTHER_CPLUSPLUSFLAGS': ['-std=c++20', '-stdlib=libc++'],
|
||||
'GCC_ENABLE_CPP_EXCEPTIONS': 'NO',
|
||||
'GCC_SYMBOLS_PRIVATE_EXTERN': 'YES', # -fvisibility=hidden
|
||||
'LLVM_LTO': 'YES',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
26
deps/copy.js
vendored
26
deps/copy.js
vendored
@ -1,26 +0,0 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const dest = process.argv[2];
|
||||
const source = path.resolve(path.sep, process.argv[3] || path.join(__dirname, 'sqlite3'));
|
||||
const files = [
|
||||
{ filename: 'sqlite3.c', optional: false },
|
||||
{ filename: 'sqlite3.h', optional: false },
|
||||
];
|
||||
|
||||
if (process.argv[3]) {
|
||||
// Support "_HAVE_SQLITE_CONFIG_H" in custom builds.
|
||||
files.push({ filename: 'config.h', optional: true });
|
||||
} else {
|
||||
// Required for some tests.
|
||||
files.push({ filename: 'sqlite3ext.h', optional: false });
|
||||
}
|
||||
|
||||
for (const { filename, optional } of files) {
|
||||
if (optional && !fs.existsSync(path.join(source, filename))) {
|
||||
continue;
|
||||
}
|
||||
fs.accessSync(path.join(source, filename));
|
||||
fs.copyFileSync(path.join(source, filename), path.join(dest, filename));
|
||||
}
|
||||
25
deps/defines.gypi
vendored
25
deps/defines.gypi
vendored
@ -1,47 +1,28 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED (DO NOT EDIT)
|
||||
|
||||
{
|
||||
'defines': [
|
||||
'SQLITE_LIKE_DOESNT_MATCH_BLOBS',
|
||||
'SQLITE_THREADSAFE=2',
|
||||
'SQLITE_USE_URI=0',
|
||||
'SQLITE_DEFAULT_MEMSTATUS=0',
|
||||
'SQLITE_OMIT_AUTOINIT',
|
||||
'SQLITE_OMIT_DEPRECATED',
|
||||
'SQLITE_OMIT_DESERIALIZE',
|
||||
'SQLITE_OMIT_GET_TABLE',
|
||||
'SQLITE_OMIT_TCL_VARIABLE',
|
||||
'SQLITE_OMIT_PROGRESS_CALLBACK',
|
||||
'SQLITE_OMIT_SHARED_CACHE',
|
||||
'SQLITE_OMIT_UTF16',
|
||||
'SQLITE_OMIT_COMPLETE',
|
||||
'SQLITE_OMIT_GET_TABLE',
|
||||
'SQLITE_OMIT_AUTHORIZATION',
|
||||
'SQLITE_OMIT_LOAD_EXTENSION',
|
||||
'SQLITE_TRACE_SIZE_LIMIT=32',
|
||||
'SQLITE_DEFAULT_CACHE_SIZE=-16000',
|
||||
'SQLITE_DEFAULT_FOREIGN_KEYS=1',
|
||||
'SQLITE_DEFAULT_WAL_SYNCHRONOUS=1',
|
||||
'SQLITE_DQS=0',
|
||||
'SQLITE_ENABLE_MATH_FUNCTIONS',
|
||||
'SQLITE_ENABLE_DESERIALIZE',
|
||||
'SQLITE_ENABLE_COLUMN_METADATA',
|
||||
'SQLITE_ENABLE_UPDATE_DELETE_LIMIT',
|
||||
'SQLITE_ENABLE_STAT4',
|
||||
'SQLITE_ENABLE_FTS5',
|
||||
'SQLITE_ENABLE_JSON1',
|
||||
'SQLITE_ENABLE_RTREE',
|
||||
'SQLITE_INTROSPECTION_PRAGMAS',
|
||||
|
||||
'SQLCIPHER_CRYPTO_CUSTOM=signal_crypto_provider_setup',
|
||||
|
||||
'HAVE_STDINT_H=1',
|
||||
'HAVE_INT8_T=1',
|
||||
'HAVE_INT16_T=1',
|
||||
'HAVE_INT32_T=1',
|
||||
'HAVE_UINT8_T=1',
|
||||
'HAVE_INT8_T=1',
|
||||
'HAVE_STDINT_H=1',
|
||||
'HAVE_UINT16_T=1',
|
||||
'HAVE_UINT32_T=1',
|
||||
|
||||
# SQLCipher-specific options
|
||||
'SQLITE_HAS_CODEC',
|
||||
'SQLITE_TEMP_STORE=2',
|
||||
|
||||
66
deps/download.js
vendored
66
deps/download.js
vendored
@ -1,66 +0,0 @@
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { Transform } = require('stream');
|
||||
const { pipeline } = require('stream/promises');
|
||||
|
||||
const BASE_URI = `https://build-artifacts.signal.org/desktop`;
|
||||
const HASH = '6253f886c40e49bf892d5cdc92b2eb200b12cd8d80c48ce5b05967cfd01ee8c7';
|
||||
const SQLCIPHER_VERSION = '4.6.1-signal-patch2';
|
||||
const EXTENSION_VERSION = '0.2.1-asm2';
|
||||
const TAG = [SQLCIPHER_VERSION, EXTENSION_VERSION].join('--');
|
||||
const URL = `${BASE_URI}/sqlcipher-v2-${TAG}-${HASH}.tar.gz`;
|
||||
|
||||
const buildFile = process.argv[2];
|
||||
const targetFile = path.join(__dirname, 'sqlcipher.tar.gz');
|
||||
const tmpFile = `${targetFile}.tmp`;
|
||||
|
||||
async function main() {
|
||||
if (fs.statSync(targetFile, { throwIfNoEntry: false })) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const existingHash = await pipeline(
|
||||
fs.createReadStream(targetFile),
|
||||
hash,
|
||||
);
|
||||
if (hash.digest('hex') === HASH) {
|
||||
console.log('local build artifact is up-to-date');
|
||||
fs.copyFileSync(targetFile, buildFile);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('local build artifact is outdated');
|
||||
} else {
|
||||
console.log('local build artifact is absent');
|
||||
}
|
||||
download();
|
||||
}
|
||||
|
||||
function download() {
|
||||
console.log(`downloading ${URL}`);
|
||||
https.get(URL, async (res) => {
|
||||
const out = fs.createWriteStream(tmpFile);
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
|
||||
const t = new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
hash.write(chunk, encoding);
|
||||
callback(null, chunk);
|
||||
}
|
||||
});
|
||||
|
||||
await pipeline(res, t, out);
|
||||
|
||||
const actualDigest = hash.digest('hex');
|
||||
if (actualDigest !== HASH) {
|
||||
fs.unlinkSync(tmpFile);
|
||||
throw new Error(`Digest mismatch. Expected ${HASH} got ${actualDigest}`);
|
||||
}
|
||||
|
||||
fs.renameSync(tmpFile, targetFile);
|
||||
fs.copyFileSync(targetFile, buildFile);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
98
deps/download.sh
vendored
Executable file
98
deps/download.sh
vendored
Executable file
@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ===
|
||||
# This script defines and generates the bundled SQLite3 unit (sqlite3.c).
|
||||
#
|
||||
# The following steps are taken:
|
||||
# 1. populate the shell environment with the defined compile-time options.
|
||||
# 2. download and extract the SQLite3 source code into a temporary directory.
|
||||
# 3. run "sh configure" and "make sqlite3.c" within the source directory.
|
||||
# 4. bundle the generated amalgamation into a tar.gz file (sqlite3.tar.gz).
|
||||
# 5. export the defined compile-time options to a gyp file (defines.gypi).
|
||||
# 6. update the docs (../docs/compilation.md) with details of this distribution.
|
||||
#
|
||||
# When a user builds better-sqlite3, the following steps are taken:
|
||||
# 1. node-gyp loads the previously exported compile-time options (defines.gypi).
|
||||
# 2. the extract.js script unpacks the bundled amalgamation (sqlite3.tar.gz).
|
||||
# 3. node-gyp compiles the extracted sqlite3.c along with better_sqlite3.cpp.
|
||||
# 3. node-gyp links the two resulting binaries to generate better_sqlite3.node.
|
||||
# ===
|
||||
|
||||
YEAR="2021"
|
||||
VERSION="3350200"
|
||||
|
||||
DEFINES="
|
||||
SQLITE_DQS=0
|
||||
SQLITE_LIKE_DOESNT_MATCH_BLOBS
|
||||
SQLITE_THREADSAFE=2
|
||||
SQLITE_USE_URI=0
|
||||
SQLITE_DEFAULT_MEMSTATUS=0
|
||||
SQLITE_OMIT_DEPRECATED
|
||||
SQLITE_OMIT_GET_TABLE
|
||||
SQLITE_OMIT_TCL_VARIABLE
|
||||
SQLITE_OMIT_PROGRESS_CALLBACK
|
||||
SQLITE_OMIT_SHARED_CACHE
|
||||
SQLITE_TRACE_SIZE_LIMIT=32
|
||||
SQLITE_DEFAULT_CACHE_SIZE=-16000
|
||||
SQLITE_DEFAULT_FOREIGN_KEYS=1
|
||||
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
|
||||
SQLITE_ENABLE_COLUMN_METADATA
|
||||
SQLITE_ENABLE_UPDATE_DELETE_LIMIT
|
||||
SQLITE_ENABLE_STAT4
|
||||
SQLITE_ENABLE_FTS3_PARENTHESIS
|
||||
SQLITE_ENABLE_FTS3
|
||||
SQLITE_ENABLE_FTS4
|
||||
SQLITE_ENABLE_FTS5
|
||||
SQLITE_ENABLE_JSON1
|
||||
SQLITE_ENABLE_RTREE
|
||||
SQLITE_ENABLE_GEOPOLY
|
||||
SQLITE_INTROSPECTION_PRAGMAS
|
||||
SQLITE_SOUNDEX
|
||||
"
|
||||
|
||||
# ========== START SCRIPT ========== #
|
||||
|
||||
echo "setting up environment..."
|
||||
DEPS="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
TEMP="$DEPS/temp"
|
||||
rm -rf "$TEMP"
|
||||
mkdir -p "$TEMP"
|
||||
export CFLAGS=`echo $(echo "$DEFINES" | sed -e "/^\s*$/d" -e "s/^/-D/")`
|
||||
|
||||
echo "downloading source..."
|
||||
curl -#f "https://www.sqlite.org/$YEAR/sqlite-src-$VERSION.zip" > "$TEMP/source.zip" || exit 1
|
||||
|
||||
echo "extracting source..."
|
||||
unzip "$TEMP/source.zip" -d "$TEMP" > /dev/null || exit 1
|
||||
cd "$TEMP/sqlite-src-$VERSION" || exit 1
|
||||
|
||||
echo "configuring amalgamation..."
|
||||
sh configure > /dev/null || exit 1
|
||||
|
||||
echo "building amalgamation..."
|
||||
make sqlite3.c > /dev/null || exit 1
|
||||
|
||||
echo "generating tarball..."
|
||||
tar czf "$DEPS/sqlite3.tar.gz" sqlite3.c sqlite3.h sqlite3ext.h || exit 1
|
||||
|
||||
echo "updating gyp configs..."
|
||||
GYP="$DEPS/defines.gypi"
|
||||
printf "# THIS FILE IS AUTOMATICALLY GENERATED (DO NOT EDIT)\n\n{\n 'defines': [\n" > "$GYP"
|
||||
printf "$DEFINES" | sed -e "/^\s*$/d" -e "s/\(.*\)/ '\1',/" >> "$GYP"
|
||||
printf " ],\n}\n" >> "$GYP"
|
||||
|
||||
echo "updating docs..."
|
||||
DOCS="$DEPS/../docs/compilation.md"
|
||||
MAJOR=`expr "${VERSION:0:1}" + 0`
|
||||
MINOR=`expr "${VERSION:1:2}" + 0`
|
||||
PATCH=`expr "${VERSION:3:2}" + 0`
|
||||
sed -Ei "" -e "s/version [0-9]+\.[0-9]+\.[0-9]+/version $MAJOR.$MINOR.$PATCH/g" "$DOCS"
|
||||
sed -i "" -e "/^SQLITE_/,\$d" "$DOCS"
|
||||
printf "$DEFINES" | sed -e "/^\s*$/d" >> "$DOCS"
|
||||
printf "\`\`\`\n" >> "$DOCS"
|
||||
|
||||
echo "cleaning up..."
|
||||
cd - > /dev/null || exit 1
|
||||
rm -rf "$TEMP"
|
||||
|
||||
echo "done!"
|
||||
6
deps/extract.js
vendored
6
deps/extract.js
vendored
@ -2,13 +2,13 @@
|
||||
const path = require('path');
|
||||
const tar = require('tar');
|
||||
|
||||
const source = process.argv[2];
|
||||
const dest = process.argv[3];
|
||||
const dest = process.argv[2];
|
||||
const source = path.join(__dirname, 'sqlite3.tar.gz');
|
||||
|
||||
process.on('unhandledRejection', (err) => { throw err; });
|
||||
|
||||
/*
|
||||
This extracts the bundled sqlcipher.tar.gz file and places the resulting files
|
||||
This extracts the bundled sqlite3.tar.gz file and places the resulting files
|
||||
into the directory specified by <$2>.
|
||||
*/
|
||||
|
||||
|
||||
49
deps/sqlite3.gyp
vendored
49
deps/sqlite3.gyp
vendored
@ -1,7 +1,7 @@
|
||||
# ===
|
||||
# This configuration defines options specific to compiling SQLite3 itself.
|
||||
# Compile-time options are loaded by the auto-generated file "defines.gypi".
|
||||
# Before SQLite3 is compiled, it gets extracted from "sqlcipher.tar.gz".
|
||||
# Before SQLite3 is compiled, it gets extracted from "sqlite3.tar.gz".
|
||||
# The --sqlite3 option can be provided to use a custom amalgamation instead.
|
||||
# ===
|
||||
|
||||
@ -9,25 +9,12 @@
|
||||
'includes': ['common.gypi'],
|
||||
'targets': [
|
||||
{
|
||||
'target_name': 'download_sqlite3',
|
||||
'target_name': 'locate_sqlite3',
|
||||
'type': 'none',
|
||||
'hard_dependency': 1,
|
||||
'actions': [{
|
||||
'action_name': 'download_sqlite3',
|
||||
'inputs': ['download.js'],
|
||||
'outputs': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz'
|
||||
],
|
||||
'action': ['node', 'download.js', '<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz'],
|
||||
}],
|
||||
},
|
||||
{
|
||||
'target_name': 'locate_sqlite3',
|
||||
'type': 'none',
|
||||
'dependencies': ['download_sqlite3'],
|
||||
'actions': [{
|
||||
'action_name': 'extract_sqlite3',
|
||||
'inputs': ['<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz'],
|
||||
'inputs': ['sqlite3.tar.gz'],
|
||||
'outputs': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.c',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.h',
|
||||
@ -36,11 +23,13 @@
|
||||
'conditions': [
|
||||
['OS == "win"', {
|
||||
'outputs': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-pc-windows-msvc/signal_sqlcipher_extension.lib',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/libssl.lib',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/libcrypto.lib',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/ossl_static.pdb',
|
||||
],
|
||||
}],
|
||||
],
|
||||
'action': ['node', 'extract.js', '<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz', '<(SHARED_INTERMEDIATE_DIR)/sqlite3'],
|
||||
'action': ['node', 'extract.js', '<(SHARED_INTERMEDIATE_DIR)/sqlite3'],
|
||||
}],
|
||||
},
|
||||
{
|
||||
@ -51,7 +40,9 @@
|
||||
['OS == "win"', {
|
||||
'copies': [{
|
||||
'files': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-pc-windows-msvc/signal_sqlcipher_extension.lib',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/libssl.lib',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/libcrypto.lib',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/ossl_static.pdb',
|
||||
],
|
||||
'destination': '<(PRODUCT_DIR)',
|
||||
}],
|
||||
@ -65,11 +56,12 @@
|
||||
'sources': ['<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.c'],
|
||||
'include_dirs': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/openssl-include',
|
||||
],
|
||||
'direct_dependent_settings': {
|
||||
'include_dirs': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/include',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/openssl-include',
|
||||
],
|
||||
},
|
||||
'cflags': ['-std=c99', '-w'],
|
||||
@ -85,28 +77,29 @@
|
||||
],
|
||||
'link_settings': {
|
||||
'libraries': [
|
||||
'-luserenv.lib',
|
||||
'-lntdll.lib',
|
||||
'-lbcrypt.lib',
|
||||
'-lcrypt32.lib',
|
||||
'-lsignal_sqlcipher_extension.lib'
|
||||
'-llibcrypto.lib',
|
||||
'-llibssl.lib',
|
||||
'-lws2_32.lib',
|
||||
'-lcrypt32.lib'
|
||||
],
|
||||
'library_dirs': [
|
||||
'<(PRODUCT_DIR)',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)'
|
||||
]
|
||||
}
|
||||
},
|
||||
'OS == "mac"', {
|
||||
'link_settings': {
|
||||
'libraries': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-apple-darwin/libsignal_sqlcipher_extension.a',
|
||||
# This statically links libcrypto, whereas -lcrypto would dynamically link it
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/OpenSSL-macOS/libcrypto.a'
|
||||
]
|
||||
}
|
||||
},
|
||||
{ # Linux
|
||||
'link_settings': {
|
||||
'libraries': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-unknown-linux-gnu/libsignal_sqlcipher_extension.a',
|
||||
# This statically links libcrypto, whereas -lcrypto would dynamically link it
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/OpenSSL-Linux/libcrypto.a'
|
||||
]
|
||||
}
|
||||
}],
|
||||
|
||||
BIN
deps/sqlite3.tar.gz
(Stored with Git LFS)
vendored
Normal file
BIN
deps/sqlite3.tar.gz
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
16
deps/symlink.js
vendored
Normal file
16
deps/symlink.js
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const dest = process.argv[2];
|
||||
const source = path.resolve(path.sep, process.argv[3]);
|
||||
|
||||
/*
|
||||
This creates symlinks inside the <$2> directory, linking to the SQLite3
|
||||
amalgamation files inside the directory specified by the absolute path <$3>.
|
||||
*/
|
||||
|
||||
for (const filename of ['sqlite3.c', 'sqlite3.h']) {
|
||||
fs.accessSync(path.join(source, filename));
|
||||
fs.symlinkSync(path.join(source, filename), path.join(dest, filename), 'file');
|
||||
}
|
||||
169
docs/api.md
169
docs/api.md
@ -2,8 +2,6 @@
|
||||
|
||||
- [class `Database`](#class-database)
|
||||
- [class `Statement`](#class-statement)
|
||||
- [class `SqliteError`](#class-sqliteerror)
|
||||
- [Binding Parameters](#binding-parameters)
|
||||
|
||||
# class *Database*
|
||||
|
||||
@ -12,10 +10,8 @@
|
||||
- [Database#transaction()](#transactionfunction---function)
|
||||
- [Database#pragma()](#pragmastring-options---results)
|
||||
- [Database#backup()](#backupdestination-options---promise)
|
||||
- [Database#serialize()](#serializeoptions---buffer)
|
||||
- [Database#function()](#functionname-options-function---this)
|
||||
- [Database#aggregate()](#aggregatename-options---this)
|
||||
- [Database#table()](#tablename-definition---this)
|
||||
- [Database#loadExtension()](#loadextensionpath-entrypoint---this)
|
||||
- [Database#exec()](#execstring---this)
|
||||
- [Database#close()](#close---this)
|
||||
@ -23,22 +19,18 @@
|
||||
|
||||
### new Database(*path*, [*options*])
|
||||
|
||||
Creates a new database connection. If the database file does not exist, it is created. This happens synchronously, which means you can start executing queries right away. You can create an [in-memory database](https://www.sqlite.org/inmemorydb.html) by passing `":memory:"` as the first argument. You can create a temporary database by passing an empty string (or by omitting all arguments).
|
||||
|
||||
> In-memory databases can also be created by passing a buffer returned by [`.serialize()`](#serializeoptions---buffer), instead of passing a string as the first argument.
|
||||
Creates a new database connection. If the database file does not exist, it is created. This happens synchronously, which means you can start executing queries right away. You can create an [in-memory database](https://www.sqlite.org/inmemorydb.html) by passing `":memory:"` as the first argument.
|
||||
|
||||
Various options are accepted:
|
||||
|
||||
- `options.readonly`: open the database connection in readonly mode (default: `false`).
|
||||
|
||||
- `options.fileMustExist`: if the database does not exist, an `Error` will be thrown instead of creating a new file. This option is ignored for in-memory, temporary, or readonly database connections (default: `false`).
|
||||
- `options.fileMustExist`: if the database does not exist, an `Error` will be thrown instead of creating a new file. This option does not affect in-memory or readonly database connections (default: `false`).
|
||||
|
||||
- `options.timeout`: the number of milliseconds to wait when executing queries on a locked database, before throwing a `SQLITE_BUSY` error (default: `5000`).
|
||||
|
||||
- `options.verbose`: provide a function that gets called with every SQL string executed by the database connection (default: `null`).
|
||||
|
||||
- `options.nativeBinding`: if you're using a complicated build system that moves, transforms, or concatenates your JS files, `better-sqlite3` might have trouble locating its native C++ addon (`better_sqlite3.node`). If you get an error that looks like [this](https://github.com/JoshuaWise/better-sqlite3/issues/534#issuecomment-757907190), you can solve it by using this option to provide the file path of `better_sqlite3.node` (relative to the current working directory).
|
||||
|
||||
```js
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('foobar.db', { verbose: console.log });
|
||||
@ -98,7 +90,7 @@ If you'd like to manage transactions manually, you're free to do so with regular
|
||||
|
||||
Transaction functions do not work with async functions. Technically speaking, async functions always return after the first `await`, which means the transaction will already be committed before any async code executes. Also, because SQLite3 serializes all transactions, it's generally a very bad idea to keep a transaction open across event loop ticks anyways.
|
||||
|
||||
It's important to know that SQLite3 may sometimes rollback a transaction without us asking it to. This can happen either because of an [`ON CONFLICT`](https://sqlite.org/lang_conflict.html) clause, the [`RAISE()`](https://www.sqlite.org/lang_createtrigger.html) trigger function, or certain errors such as `SQLITE_FULL` or `SQLITE_BUSY`. In other words, if you catch an SQLite3 error *within* a transaction, you must be aware that any further SQL that you execute might not be within the same transaction. Usually, the best course of action for such cases is to simply re-throw the error, exiting the transaction function.
|
||||
It's important to know that SQLite3 may sometimes rollback a transaction without us asking it to. This can happen either because of an [`ON CONFLICT`](https://sqlite.org/lang_conflict.html) clause, the [`RAISE()`](https://www.sqlite.org/lang_createtrigger.html) trigger function, or certain errors such as `SQLITE_FULL` or `SQLITE_BUSY`. In other words, if you catch an SQLite3 error *within* a transaction, you must be aware that any futher SQL that you execute might not be within the same transaction. Usually, the best course of action for such cases is to simply re-throw the error, exiting the transaction function.
|
||||
|
||||
```js
|
||||
try {
|
||||
@ -126,7 +118,7 @@ It's better to use this method instead of normal [prepared statements](#prepares
|
||||
|
||||
### .backup(*destination*, [*options*]) -> *promise*
|
||||
|
||||
Initiates a [backup](https://www.sqlite.org/backup.html) of the database, returning a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for when the backup is complete. If the backup fails, the promise will be rejected with an `Error`. You can optionally backup an attached database instead by setting the `attached` option to the name of the desired attached database. A backup file is just a regular SQLite3 database file. It can be opened by [`new Database()`](#new-databasepath-options) just like any SQLite3 database.
|
||||
Initiates a [backup](https://www.sqlite.org/backup.html) of the database, returning a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for when the backup is complete. If the backup fails, the promise will be rejected with an `Error`. You can optionally backup an attached database by setting the `attached` option to the name of the desired attached database.
|
||||
|
||||
```js
|
||||
db.backup(`backup-${Date.now()}.db`)
|
||||
@ -159,18 +151,6 @@ db.backup(`backup-${Date.now()}.db`, {
|
||||
});
|
||||
```
|
||||
|
||||
### .serialize([*options*]) -> *Buffer*
|
||||
|
||||
Returns a [buffer](https://nodejs.org/api/buffer.html#buffer_class_buffer) containing the serialized contents of the database. You can optionally serialize an attached database instead by setting the `attached` option to the name of the desired attached database.
|
||||
|
||||
The returned buffer can be written to disk to create a regular SQLite3 database file, or it can be opened directly as an in-memory database by passing it to [`new Database()`](#new-databasepath-options).
|
||||
|
||||
```js
|
||||
const buffer = db.serialize();
|
||||
db.close();
|
||||
db = new Database(buffer);
|
||||
```
|
||||
|
||||
### .function(*name*, [*options*], *function*) -> *this*
|
||||
|
||||
Registers a user-defined `function` so that it can be used by SQL statements.
|
||||
@ -187,8 +167,6 @@ By default, user-defined functions have a strict number of arguments (determined
|
||||
|
||||
If `options.varargs` is `true`, the registered function can accept any number of arguments.
|
||||
|
||||
If `options.directOnly` is `true`, the registered function can only be invoked from top-level SQL, and cannot be used in [VIEWs](https://sqlite.org/lang_createview.html), [TRIGGERs](https://sqlite.org/lang_createtrigger.html), or schema structures such as [CHECK constraints](https://www.sqlite.org/lang_createtable.html#ckconst), [DEFAULT clauses](https://www.sqlite.org/lang_createtable.html#dfltval), etc.
|
||||
|
||||
If your function is [deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm), you can set `options.deterministic` to `true`, which may improve performance under some circumstances.
|
||||
|
||||
```js
|
||||
@ -231,7 +209,7 @@ db.prepare('SELECT getAverage(dollars) FROM expenses').pluck().get(); // => 20.2
|
||||
|
||||
As shown above, you can use arbitrary JavaScript objects as your aggregation context, as long as a valid SQLite3 value is returned by `result()` in the end. If `step()` doesn't return anything (`undefined`), the aggregate value will not be replaced (be careful of this when using functions that return `undefined` when `null` is desired).
|
||||
|
||||
Just like regular [user-defined functions](#functionname-options-function---this), user-defined aggregates can accept multiple arguments. Furthermore, `options.varargs`, `options.directOnly`, and `options.deterministic` [are also](#functionname-options-function---this) accepted.
|
||||
Just like regular [user-defined functions](#functionname-options-function---this), user-defined aggregates can accept multiple arguments. Furthermore, `options.varargs` and `options.deterministic` [are also](#functionname-options-function---this) accepted.
|
||||
|
||||
If you provide an `inverse()` function, the aggregate can be used as a [window function](https://www.sqlite.org/windowfunctions.html). Where `step()` is used to add a row to the current window, `inverse()` is used to remove a row from the current window. When using window functions, `result()` may be invoked multiple times.
|
||||
|
||||
@ -251,117 +229,6 @@ db.prepare(`
|
||||
`).all();
|
||||
```
|
||||
|
||||
### .table(*name*, *definition*) -> *this*
|
||||
|
||||
Registers a [virtual table](https://www.sqlite.org/vtab.html). Virtual tables can be queried just like real tables, except their results do not exist in the database file; instead, they are calculated on-the-fly by a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) in JavaScript.
|
||||
|
||||
```js
|
||||
const fs = require('fs');
|
||||
|
||||
db.table('filesystem_directory', {
|
||||
columns: ['filename', 'data'],
|
||||
rows: function* () {
|
||||
for (const filename of fs.readdirSync(process.cwd())) {
|
||||
const data = fs.readFileSync(filename);
|
||||
yield { filename, data };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const files = db.prepare('SELECT * FROM filesystem_directory').all();
|
||||
// => [{ filename, data }, { filename, data }]
|
||||
```
|
||||
|
||||
To generate a row in a virtual table, you can either yield an object whose keys correspond to column names, or yield an array whose elements represent columns in the order that they were declared. Every virtual table **must** declare its columns via the `columns` option.
|
||||
|
||||
Virtual tables can be used like [table-valued functions](https://www.sqlite.org/vtab.html#tabfunc2); you can pass parameters to them, unlike regular tables.
|
||||
|
||||
```js
|
||||
db.table('regex_matches', {
|
||||
columns: ['match', 'capture'],
|
||||
rows: function* (pattern, text) {
|
||||
const regex = new RegExp(pattern, 'g');
|
||||
let match;
|
||||
|
||||
while (match = regex.exec(text)) {
|
||||
yield [match[0], match[1]];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const stmt = db.prepare("SELECT * FROM regex('\\$(\\d+)', ?)");
|
||||
|
||||
stmt.all('Desks cost $500 and chairs cost $27');
|
||||
// => [{ match: '$500', capture: '500' }, { match: '$27', capture: '27' }]
|
||||
```
|
||||
|
||||
By default, the number of parameters accepted by a virtual table is inferred by `function.length`, and the parameters are automatically named `$1`, `$2`, etc. However, you can optionally provide an explicit list of parameters via the `parameters` option.
|
||||
|
||||
```js
|
||||
db.table('regex_matches', {
|
||||
columns: ['match', 'capture'],
|
||||
parameters: ['pattern', 'text'],
|
||||
rows: function* (pattern, text) {
|
||||
...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> In virtual tables, parameters are actually [*hidden columns*](https://www.sqlite.org/vtab.html#hidden_columns_in_virtual_tables), and they can be selected in the result set of a query, just like any other column. That's why it may sometimes be desirable to give them explicit names.
|
||||
|
||||
When querying a virtual table, any omitted parameters will be `undefined`. You can use this behavior to implement required parameters and default parameter values.
|
||||
|
||||
```js
|
||||
db.table('sequence', {
|
||||
columns: ['value'],
|
||||
parameters: ['length', 'start'],
|
||||
rows: function* (length, start = 0) {
|
||||
if (length === undefined) {
|
||||
throw new TypeError('missing required parameter "length"');
|
||||
}
|
||||
|
||||
const end = start + length;
|
||||
for (let n = start; n < end; ++n) {
|
||||
yield { value: n };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
db.prepare('SELECT * FROM sequence(10)').pluck().all();
|
||||
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
```
|
||||
|
||||
> Note that when using syntax like `start = 0` for default parameter values (shown above), the function's `.length` property does not include the optional parameter, so you need to explicitly declare `parameters` in this case.
|
||||
|
||||
Normally, when you register a virtual table, the virtual table *automatically exists* without needing to run a `CREATE VIRTUAL TABLE` statement. However, if you provide a factory function as the second argument (a function that *returns* virtual table definitions), then no virtual table will be created automatically. Instead, you can create multiple similar virtual tables by running [`CREATE VIRTUAL TABLE`](https://sqlite.org/lang_createvtab.html) statements, each with their own module arguments. Think of it like defining a virtual table "class" that can be instantiated by running `CREATE VIRTUAL TABLE` statements.
|
||||
|
||||
```js
|
||||
const fs = require('fs');
|
||||
|
||||
db.table('csv', (filename) => {
|
||||
const firstLine = getFirstLineOfFile(filename);
|
||||
return {
|
||||
columns: firstLine.split(','),
|
||||
rows: function* () {
|
||||
// This is just an example. Real CSV files are more complicated to parse.
|
||||
const contents = fs.readFileSync(filename, 'utf8');
|
||||
for (const line of contents.split('\n')) {
|
||||
yield line.split(',');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
db.exec('CREATE VIRTUAL TABLE my_data USING csv(my_data.csv)');
|
||||
const allData = db.prepare('SELECT * FROM my_data').all();
|
||||
```
|
||||
|
||||
The factory function will be invoked each time a corresponding `CREATE VIRTUAL TABLE` statement runs. The arguments to the factory function correspond to the module arguments passed in the `CREATE VIRTUAL TABLE` statement; always a list of arbitrary strings separated by commas. It's your responsibility to parse and interpret those module arguments. Note that SQLite3 does not allow [bound parameters](#binding-parameters) inside module arguments.
|
||||
|
||||
Just like [user-defined functions](#functionname-options-function---this) and [user-defined aggregates](#aggregatename-options---this), virtual tables support `options.directOnly`, which prevents the table from being used inside [VIEWs](https://sqlite.org/lang_createview.html), [TRIGGERs](https://sqlite.org/lang_createtrigger.html), or schema structures such as [CHECK constraints](https://www.sqlite.org/lang_createtable.html#ckconst), [DEFAULT clauses](https://www.sqlite.org/lang_createtable.html#dfltval), etc.
|
||||
|
||||
> Some [extensions](#loadextensionpath-entrypoint---this) can provide virtual tables that have write capabilities, but `db.table()` is only capable of creating read-only virtual tables, primarily for the purpose of supporting table-valued functions.
|
||||
|
||||
### .loadExtension(*path*, [*entryPoint*]) -> *this*
|
||||
|
||||
Loads a compiled [SQLite3 extension](https://sqlite.org/loadext.html) and applies it to the current database connection.
|
||||
@ -400,7 +267,7 @@ process.on('SIGTERM', () => process.exit(128 + 15));
|
||||
|
||||
**.name -> _string_** - The string that was used to open the database connection.
|
||||
|
||||
**.memory -> _boolean_** - Whether the database is an in-memory or temporary database.
|
||||
**.memory -> _boolean_** - Whether the database is an in-memory database.
|
||||
|
||||
**.readonly -> _boolean_** - Whether the database connection was created in readonly mode.
|
||||
|
||||
@ -421,6 +288,8 @@ An object representing a single SQL statement.
|
||||
|
||||
### .run([*...bindParameters*]) -> *object*
|
||||
|
||||
**(only on statements that do not return data)*
|
||||
|
||||
Executes the prepared statement. When execution completes it returns an `info` object describing any changes made. The `info` object has two properties:
|
||||
|
||||
- `info.changes`: the total number of rows that were inserted, updated, or deleted by this operation. Changes made by [foreign key actions](https://www.sqlite.org/foreignkeys.html#fk_actions) or [trigger programs](https://www.sqlite.org/lang_createtrigger.html) do not count.
|
||||
@ -598,18 +467,6 @@ console.log(cat.name); // => "Joey"
|
||||
|
||||
**.reader -> _boolean_** - Whether the prepared statement returns data.
|
||||
|
||||
**.readonly -> _boolean_** - Whether the prepared statement is readonly, meaning it does not mutate the database (note that [SQL functions might still change the database indirectly](https://www.sqlite.org/c3ref/stmt_readonly.html) as a side effect, even if the `.readonly` property is `true`).
|
||||
|
||||
**.busy -> _boolean_** - Whether the prepared statement is busy executing a query via the [`.iterate()`](#iteratebindparameters---iterator) method.
|
||||
|
||||
# class *SqliteError*
|
||||
|
||||
Whenever an error occurs within SQLite3, a `SqliteError` object will be thrown. `SqliteError` is a subclass of `Error`. Every `SqliteError` object has a `code` property, which is a string matching one of error codes defined [here](https://sqlite.org/rescode.html) (for example, `"SQLITE_CONSTRAINT"`).
|
||||
|
||||
If you receive a `SqliteError`, it probably means you're using SQLite3 incorrectly. The error didn't originate in `better-sqlite3`, so it's probably not an issue with `better-sqlite3`. It's recommended that you learn about the meaning of the error [here](https://sqlite.org/rescode.html), and perhaps learn more about how to use SQLite3 by reading [their docs](https://sqlite.org/docs.html).
|
||||
|
||||
> In the unlikely scenario that SQLite3 throws an error that is not recognized by `better-sqlite3` (this would be considered a bug in `better-sqlite3`), the `code` property will be `"UNKNOWN_SQLITE_ERROR_NNNN"`, where `NNNN` is the numeric error code. If this happens to you, please report it as an [issue](https://github.com/JoshuaWise/better-sqlite3/issues).
|
||||
|
||||
# Binding Parameters
|
||||
|
||||
This section refers to anywhere in the documentation that specifies the optional argument [*`...bindParameters`*].
|
||||
@ -647,13 +504,3 @@ Below is an example of mixing anonymous parameters with named parameters.
|
||||
const stmt = db.prepare('INSERT INTO people VALUES (@name, @name, ?)');
|
||||
stmt.run(45, { name: 'Henry' });
|
||||
```
|
||||
|
||||
Here is how `better-sqlite3` converts values between SQLite3 and JavaScript:
|
||||
|
||||
|SQLite3|JavaScript|
|
||||
|---|---|
|
||||
|`NULL`|`null`|
|
||||
|`REAL`|`number`|
|
||||
|`INTEGER`|`number` [or `BigInt`](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/integer.md#the-bigint-primitive-type)|
|
||||
|`TEXT`|`string`|
|
||||
|`BLOB`|[`Buffer`](https://nodejs.org/api/buffer.html#buffer_class_buffer)|
|
||||
|
||||
@ -16,15 +16,7 @@ However, if you simply run `npm install` while `better-sqlite3` is listed as a d
|
||||
}
|
||||
```
|
||||
|
||||
Your amalgamation directory must contain `sqlite3.c` and `sqlite3.h`. Any desired [compile time options](https://www.sqlite.org/compile.html) must be defined directly within `sqlite3.c`, as shown below.
|
||||
|
||||
```c
|
||||
// These go at the top of the file
|
||||
#define SQLITE_ENABLE_FTS5 1
|
||||
#define SQLITE_DEFAULT_CACHE_SIZE 16000
|
||||
|
||||
// ... the original content of the file remains below
|
||||
```
|
||||
Your amalgamation directory must contain `sqlite3.c` and `sqlite3.h`. Any desired [compile time options](https://www.sqlite.org/compile.html) must be defined directly within `sqlite3.c`.
|
||||
|
||||
### Step by step example
|
||||
|
||||
@ -42,42 +34,33 @@ If you're using a SQLite3 encryption extension that is a drop-in replacement for
|
||||
|
||||
# Bundled configuration
|
||||
|
||||
By default, this distribution currently uses SQLite3 **version 3.40.0** with the following [compilation options](https://www.sqlite.org/compile.html):
|
||||
By default, this distribution currently uses SQLite3 **version 3.35.2** with the following [compilation options](https://www.sqlite.org/compile.html):
|
||||
|
||||
```
|
||||
HAVE_INT16_T=1
|
||||
HAVE_INT32_T=1
|
||||
HAVE_INT8_T=1
|
||||
HAVE_STDINT_H=1
|
||||
HAVE_UINT16_T=1
|
||||
HAVE_UINT32_T=1
|
||||
HAVE_UINT8_T=1
|
||||
SQLITE_DEFAULT_CACHE_SIZE=-16000
|
||||
SQLITE_DEFAULT_FOREIGN_KEYS=1
|
||||
SQLITE_DEFAULT_MEMSTATUS=0
|
||||
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
|
||||
SQLITE_DQS=0
|
||||
SQLITE_ENABLE_COLUMN_METADATA
|
||||
SQLITE_ENABLE_DESERIALIZE
|
||||
SQLITE_ENABLE_FTS3
|
||||
SQLITE_ENABLE_FTS3_PARENTHESIS
|
||||
SQLITE_ENABLE_FTS4
|
||||
SQLITE_ENABLE_FTS5
|
||||
SQLITE_ENABLE_GEOPOLY
|
||||
SQLITE_ENABLE_JSON1
|
||||
SQLITE_ENABLE_MATH_FUNCTIONS
|
||||
SQLITE_ENABLE_RTREE
|
||||
SQLITE_ENABLE_STAT4
|
||||
SQLITE_ENABLE_UPDATE_DELETE_LIMIT
|
||||
SQLITE_INTROSPECTION_PRAGMAS
|
||||
SQLITE_LIKE_DOESNT_MATCH_BLOBS
|
||||
SQLITE_THREADSAFE=2
|
||||
SQLITE_USE_URI=0
|
||||
SQLITE_DEFAULT_MEMSTATUS=0
|
||||
SQLITE_OMIT_DEPRECATED
|
||||
SQLITE_OMIT_GET_TABLE
|
||||
SQLITE_OMIT_TCL_VARIABLE
|
||||
SQLITE_OMIT_PROGRESS_CALLBACK
|
||||
SQLITE_OMIT_SHARED_CACHE
|
||||
SQLITE_OMIT_TCL_VARIABLE
|
||||
SQLITE_SOUNDEX
|
||||
SQLITE_THREADSAFE=2
|
||||
SQLITE_TRACE_SIZE_LIMIT=32
|
||||
SQLITE_USE_URI=0
|
||||
SQLITE_DEFAULT_CACHE_SIZE=-16000
|
||||
SQLITE_DEFAULT_FOREIGN_KEYS=1
|
||||
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
|
||||
SQLITE_ENABLE_COLUMN_METADATA
|
||||
SQLITE_ENABLE_UPDATE_DELETE_LIMIT
|
||||
SQLITE_ENABLE_STAT4
|
||||
SQLITE_ENABLE_FTS3_PARENTHESIS
|
||||
SQLITE_ENABLE_FTS3
|
||||
SQLITE_ENABLE_FTS4
|
||||
SQLITE_ENABLE_FTS5
|
||||
SQLITE_ENABLE_JSON1
|
||||
SQLITE_ENABLE_RTREE
|
||||
SQLITE_ENABLE_GEOPOLY
|
||||
SQLITE_INTROSPECTION_PRAGMAS
|
||||
SQLITE_SOUNDEX
|
||||
```
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
# Code of conduct
|
||||
|
||||
Topics of discussion are expected to be constrained such that all discussion is relevant to the following goals:
|
||||
|
||||
- Maintaining `better-sqlite3`'s code, documentation, and build artifacts
|
||||
- Helping people *get started* in using `better-sqlite3` within their software projects
|
||||
|
||||
Other areas of discussion are considered to be off-topic, including but not limited to:
|
||||
|
||||
- Politics
|
||||
- Name-calling, insults
|
||||
- Help with using SQLite (there's already [very good documentation](https://sqlite.org/docs.html) for that)
|
||||
- Help with application architecture, and other high-level decisions about software projects
|
||||
- Attention to personal traits such as race, gender, religion, national origin, sexual orientation, disability, etc.
|
||||
|
||||
Repeated offenses against this code of conduct may result in being temporarily banned from the community. Unofficially, the community is expected to maintain a manner of professionalism and to treat others with respect.
|
||||
|
||||
Attempting to physically seize, sabotage, or distribute malware through `better-sqlite3` will result in being permanently banned from the community, without warning.
|
||||
@ -1,147 +0,0 @@
|
||||
# Contribution
|
||||
|
||||
## Introduction and scope
|
||||
|
||||
`better-sqlite3` is a low-level Node.js package that provides bindings to [SQLite](https://sqlite.org/index.html). `better-sqlite3` is not an ORM, and does not lend itself to specific types of applications or frameworks.
|
||||
|
||||
Anything that SQLite does not directly provide is considered out-of-scope for `better-sqlite3`. Anything that SQLite *does* directly provide *may* be considered in-scope for `better-sqlite3`, with the additional requirements that it:
|
||||
|
||||
- can be implemented sensibly and safely (i.e., it cannot lead to [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior))
|
||||
- is used commonly enough to warrent the extra code complexity that it brings
|
||||
- cannot be reasonably implemented by a user in JavaScript (e.g., by monkey-patching)
|
||||
|
||||
#### Native addons
|
||||
|
||||
`better-sqlite3` is a combination of JavaScript and C++. The C++ part is necessary in order to communicate with the [underlying SQLite library](https://sqlite.org/index.html), which is written in C. Node.js supports [C++ addons](https://nodejs.org/api/addons.html) through a build system called [`node-gyp`](https://github.com/nodejs/node-gyp), which is automatically bundled with every installation of [npm](https://docs.npmjs.com/about-npm). On most systems, C++ addons will simply be compiled as part of the installation process when running `npm install`. However, [history has shown](https://github.com/nodejs/node-gyp/issues/629) that Windows users have struggled significantly when trying to build C++ addons for Node.js. This is an issue with Node.js as a whole, and not specific to `better-sqlite3`.
|
||||
|
||||
#### Electron
|
||||
|
||||
`better-sqlite3` is a Node.js package, *not* an [Electron](https://www.electronjs.org/) package. Electron is considered a third-party platform that is not officially supported. However, many users do find great success in using `better-sqlite3` with Electron, and helpful contributors such as [@mceachen](https://github.com/mceachen) have provided support to the Electron community.
|
||||
|
||||
#### TypeScript
|
||||
|
||||
Lastly, `better-sqlite3` is a JavaScript package, not a TypeScript package. Type definitions have been generously provided by the community at [`@types/better-sqlite3`](https://www.npmjs.com/package/@types/better-sqlite3), but no official support for TypeScript is currently provided (this may change in the future).
|
||||
|
||||
## Principles
|
||||
|
||||
Code that gets contributed to `better-sqlite3` must adhere to the following principles, prioritized from first to last:
|
||||
|
||||
#### 1) Correctness
|
||||
|
||||
The code must behave as expected in all siutations. Often when writing new features, only the nominal case is considered. However, many edge cases exist when you consider race conditions, uncommon states, and improper usage. All possibilities of improper usage must be detected, and an appropriate error must be thrown (never ignored). All possibilities of proper usage must be supported, and must behave as expected.
|
||||
|
||||
#### 2) Simplicity
|
||||
|
||||
`better-sqlite3`'s public API must be as simple as possible. Rather than calling 3 functions in a specific order, it's simpler for users to call a single function. Rather than providing many similar functions for doing similar things (e.g., "convenience functions"), there should just be one function that is already convenient by design. Sane defaults should be applied when possible. A function's minimal call signature should be as small as possible, with progressively complex customization available when needed. Function names should only be as long as necessary to convey their purpose. For any new feature, it should be easy to showcase code examples that is are so simple that they are self-explanatory.
|
||||
|
||||
> This principle only applies to the public API, not necessarily to internal functions.
|
||||
|
||||
#### 3) Readability
|
||||
|
||||
Code must be written in a way that is intuitive and understandable by other programmers, now and in the future. Some code is naturally complex, and thus should be explained with comments (only when necesary). Code should be written in a style that is similar to existing code.
|
||||
|
||||
#### 4) Performance
|
||||
|
||||
Code should be written such that it does not use unnecessary computing resources. If a task can be accomplished without copying a potentially large buffer, it should be. If a complex algorithm can generally be avoided with a simple check, it should be. Calls to the operating system or filesystem should be limited to only occur when absolutely necessary. The public API should naturally encourage good performance habits, such as re-using prepared statements.
|
||||
|
||||
> It's okay to sacrifice readability for performance if doing so has a clear, measureable benefit to users.
|
||||
|
||||
## How to contribute
|
||||
|
||||
If you've never written a native addon for Node.js before, you should start by reading the [official documentation](https://nodejs.org/api/addons.html) on the subject.
|
||||
|
||||
#### C++
|
||||
|
||||
The C++ code in `better-sqlite3` is written using a tool called [`lzz`](https://github.com/WiseLibs/lzz), which alleviates the programmer from needing to write header files. If you plan on changing any C++ code, you'll need to edit `*.lzz` files and then re-compile them into `*.cpp` and `*.hpp` by running `npm run lzz` (while the `lzz` executable is in your PATH). You can learn how to download and install `lzz` [here](https://github.com/WiseLibs/lzz).
|
||||
|
||||
#### Style guide
|
||||
|
||||
There is currently no linter or style guide associated with `better-sqlite3` (this may change in the future). For now, just try to match the style of existing code as much as possible. Code owners will reject your PR or rewrite your changes if they feel that you've used a coding style that doesn't match the existing code. Although the rules aren't layed out formally, you are expected to adhere to them by using your eyeballs.
|
||||
|
||||
#### Testing
|
||||
|
||||
All tests are written in JavaScript, and they test `better-sqlite3`'s public API. All new features must be accompinied by a robust set of tests that scrutinize the new feature under all manner of circumstances and edge cases. It's not enough to simply test the "common case". If you write code that detects errors and throws exceptions, those error cases should be tested too, to ensure that all errors are being properly detected. If a new feature interacts with existing features, those interactions must be tested as well.
|
||||
|
||||
#### Documentation
|
||||
|
||||
All new features must be accompinied by [clear documentation](./api.md). All new methods and classes must be included in the [Table of Contents](./api.md#api), and must include code examples. Documentation must follow the existing formatting:
|
||||
|
||||
- Literal values use monospace code formatting
|
||||
- Examples: `"my string"`, `true`, `false`, `null`, `undefined`, `123`
|
||||
- Package names and code identifiers use monospace code formatting
|
||||
- Examples: `better-sqlite3`, `db.myMethod()`, `options.readOnly`, `this`
|
||||
- Primitive data types are lower-cased, while other data types are capatalized
|
||||
- Examples: `string`, `number`, `Buffer`, `Database`
|
||||
- References to other classes or methods must be linked and use monospace code formatting
|
||||
- Examples: [`.get()`](./api.md#getbindparameters---row), [`new Database()`](./api.md#new-databasepath-options)
|
||||
- Function signatures are written as: .funcName(*requiredArg*, [*optionalArg*]) -> *returnValue*
|
||||
- Note that the arguments and return values are *italicized*
|
||||
- Note that optional arguments are surrounded by square brackets []
|
||||
- All code blocks should be highlighted using `js` syntax, except for bash commands which don't need highlighting
|
||||
|
||||
## Categories of contribution
|
||||
|
||||
Depending on the nature of your contribution, it will be held to a different level of scrutiny, from lowest to highest:
|
||||
|
||||
#### 1) General maintenance
|
||||
|
||||
These changes are self-explanatory. They include:
|
||||
|
||||
- Updating the bundled version of SQLite (using [this workflow](https://github.com/WiseLibs/better-sqlite3/actions/workflows/update-sqlite.yml))
|
||||
- Updating dependencies in `package.json`
|
||||
- Adding prebuild binaries for a new version of Node.js or Electron
|
||||
- Adding prebuild binaries for a new architecture or operating system
|
||||
|
||||
These kinds of updates happen on a regular basis, and require zero knowledge of `better-sqlite3`'s code. Trusted contributors can merge these changes without approval from the original author.
|
||||
|
||||
#### 2) Documentation
|
||||
|
||||
Changes to documentation are usually helpful and harmless. However, they should be treated with a higher level of scrutiny because they affect how users learn about and use `better-sqlite3`. Importance is placed on the correctness and truthfuness of documentation. For example, documentation should not "go out of date" based on events outside of our control.
|
||||
|
||||
Depending on the type of documentation, trusted contributors might be able to merge these changes without approval from the original author.
|
||||
|
||||
#### 3) Minor quality-of-life improvements
|
||||
|
||||
These are code changes with a very small blast radius, such as adding a new read-only property to an object, or augmenting a function with a new option that gets passed directly to SQLite. These changes are *probably* harmless, but require additional scrutiny because they must be thoroughly tested and documented. These changes must be completely backwards-compatible, unless they're part of a major version update.
|
||||
|
||||
> It's considered a backwards-**incompatible** change for a prebuilt binary to be removed.
|
||||
|
||||
#### 4) New features
|
||||
|
||||
These are code changes with a substantial blast radius, such as implementing a new class or method. These changes must be completely backwards-compatible, unless they're part of a major version update.
|
||||
|
||||
New features are rarely accepted from external contributors because they are rarely held to the extremely high standard that `better-sqlite3` sets for itself. New features must behave correctly in all possible circumstances, including race conditions and edge cases. Likewise, even the most obscure circumstances must have test cases covering them.
|
||||
|
||||
When implementing a new feature, ask yourself:
|
||||
|
||||
- What could go wrong if I use this feature while executing a [user-defined function](./api.md#functionname-options-function---this)?
|
||||
- What could go wrong if I use this feature while [iterating](./api.md#iteratebindparameters---iterator) through a prepared statement?
|
||||
- What could go wrong if I use this feature while the database is [closed](./api.md#close---this)?
|
||||
- What could go wrong if I use this feature from within the [verbose callback](./api.md#new-databasepath-options)?
|
||||
- What could go wrong if I use this feature from within a [transaction](./api.md#transactionfunction---function)?
|
||||
- What could go wrong if I use this feature on a prepared statement that has [bound parameters](./api.md#bindbindparameters---this)?
|
||||
- What could go wrong if I use this feature within a [worker thread](./threads.md#worker-threads)?
|
||||
- What could go wrong if I pass the wrong data type?
|
||||
- What could go wrong if I pass an unexpected value, such as `null`, `undefined`, `""`, `NaN`, a negative/non-integer number, etc.?
|
||||
- Should the user's [64-bit integer setting](integer.md#the-bigint-primitive-type) affect this feature?
|
||||
- If this feature accepts a callback function:
|
||||
- What could go wrong if that callback function throws an exception?
|
||||
- What could go wrong if that callback function is triggered during one of the above scenarios?
|
||||
- Could this feature cause memory leaks?
|
||||
- What if a C++ object gets garbage-collected from JavaScript while it has open handles?
|
||||
- What if a JavaScript error is thrown within a callback, after I allocated a C++ object?
|
||||
|
||||
People love `better-sqlite3` because of its robustness and reliability. Each and every feature of `better-sqlite3` accounts for every single scenario listed above. Additionally, all possible error scenarios are explicitly handled and tested. Any new feature of `better-sqlite3` must be held to the same standard. Currently, no new features are merged without approval from the original author.
|
||||
|
||||
## Creating a release
|
||||
|
||||
Trusted contributors have the privileges necessary to create a release. Here are the steps to create a release:
|
||||
|
||||
1. Run [this workflow](https://github.com/WiseLibs/better-sqlite3/actions/workflows/bump-version.yml) from the `master` branch to create a new version tag
|
||||
- Select `patch` for bug fixes and general maintenance
|
||||
- Select `minor` for larger releases with new features
|
||||
- Select `major` for releases with backwards-incompatible changes
|
||||
2. [Draft a new release](https://github.com/WiseLibs/better-sqlite3/releases/new), and select the version tag that you just created
|
||||
3. Leave the "Release title" blank, and click "Auto-generate release notes"
|
||||
4. Click "Publish release"
|
||||
5. Wait for the `build` job to complete ([here](https://github.com/WiseLibs/better-sqlite3/actions))
|
||||
@ -52,28 +52,4 @@ db.prepare('SELECT isInt(?)').pluck().get(10); // => "false"
|
||||
db.prepare('SELECT isInt(?)').pluck().get(10n); // => "true"
|
||||
```
|
||||
|
||||
Likewise, [user-defined aggregates](./api.md#aggregatename-options---this) and [virtual tables](./api.md#tablename-definition---this) can also receive `BigInts` as arguments:
|
||||
|
||||
```js
|
||||
db.aggregate('addInts', {
|
||||
safeIntegers: true,
|
||||
start: 0n,
|
||||
step: (total, nextValue) => total + nextValue,
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
db.table('sequence', {
|
||||
safeIntegers: true,
|
||||
columns: ['value'],
|
||||
parameters: ['length', 'start'],
|
||||
rows: function* (length, start = 0n) {
|
||||
const end = start + length;
|
||||
for (let n = start; n < end; ++n) {
|
||||
yield { value: n };
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
It's worth noting that REAL (FLOAT) values returned from the database will always be represented as normal numbers.
|
||||
|
||||
@ -2,40 +2,22 @@
|
||||
|
||||
If you have trouble installing `better-sqlite3`, follow this checklist:
|
||||
|
||||
## Install a recent Node.js
|
||||
1. Make sure you're using nodejs v10.20.1 or later
|
||||
|
||||
1. Make sure you're using Node.js v14.21.1 or later.
|
||||
2. If you're on Windows, while installing, be sure to select "Automatically install the necessary tools" on the "Tools for Native Modules" page, and follow the remaining steps, including opening an admin PowerShell and installing visual studio and python. Everything _should_ just work.
|
||||
2. Make sure you have [`node-gyp`](https://github.com/nodejs/node-gyp#installation) globally installed, including all of [its dependencies](https://github.com/nodejs/node-gyp#on-unix). On Windows you may need to [configure some things manually](https://github.com/nodejs/node-gyp#on-windows).
|
||||
|
||||
## Install the `node-gyp` toolchain
|
||||
3. If you're using [Electron](https://github.com/electron/electron), try running [`electron-rebuild`](https://www.npmjs.com/package/electron-rebuild)
|
||||
|
||||
1. Make sure you have [`node-gyp`](https://github.com/nodejs/node-gyp#installation) globally installed
|
||||
1. Make sure all [`node-gyp` dependencies are installed](https://github.com/nodejs/node-gyp#on-unix). On Windows you may need to [configure some things manually](https://github.com/nodejs/node-gyp#on-windows). Use `npm ls node-gyp` to make sure none of your local packages installed an outdated version of `node-gyp` that is used over the global one.
|
||||
4. If you're using Windows, follow these steps. Do them **in this order**, and **don't skip steps**.
|
||||
|
||||
## No special characters in your project path
|
||||
|
||||
1. Make sure there are no spaces in your project path: `node-gyp` may not escape spaces or special characters (like `%` or `$`) properly.
|
||||
|
||||
## Electron
|
||||
|
||||
1. If you're using [Electron](https://github.com/electron/electron), try running [`electron-rebuild`](https://www.npmjs.com/package/electron-rebuild)
|
||||
|
||||
## Windows
|
||||
|
||||
If you still have issues on Windows and are on an older version of Node, try these steps:
|
||||
|
||||
1. Install the **latest** of node 14, 16, or 18.
|
||||
1. Install **latest** Visual Studio Community and Desktop Development with C++ extension.
|
||||
1. Install **latest** Python.
|
||||
1. Run following commands:
|
||||
```
|
||||
npm config set msvs_version 2019
|
||||
npm config set msbuild_path "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
```
|
||||
1. Delete your `node_modules` subdirectory
|
||||
1. Delete your `$HOME/.node-gyp` directory
|
||||
1. Run `npm install`
|
||||
|
||||
## If all else fails
|
||||
1. Install the **latest** of node 10, 12, or 14.
|
||||
2. Install **latest** Visual Studio Community and Desktop Development with C++ extension.
|
||||
3. Install **latest** Python.
|
||||
4. Run following commands:
|
||||
```
|
||||
npm config set msvs_version 2019
|
||||
npm config set msbuild_path "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
```
|
||||
5. Run `npm install`
|
||||
|
||||
If none of these solved your problem, try browsing [previous issues](https://github.com/JoshuaWise/better-sqlite3/issues?q=is%3Aissue) or open a [new issue](https://github.com/JoshuaWise/better-sqlite3/issues/new).
|
||||
|
||||
173
index.d.ts
vendored
173
index.d.ts
vendored
@ -1,173 +0,0 @@
|
||||
// Type definitions for better-sqlite3 7.6
|
||||
// Project: https://github.com/JoshuaWise/better-sqlite3
|
||||
// Definitions by: Ben Davies <https://github.com/Morfent>
|
||||
// Mathew Rumsey <https://github.com/matrumz>
|
||||
// Santiago Aguilar <https://github.com/sant123>
|
||||
// Alessandro Vergani <https://github.com/loghorn>
|
||||
// Andrew Kaiser <https://github.com/andykais>
|
||||
// Mark Stewart <https://github.com/mrkstwrt>
|
||||
// Florian Stamer <https://github.com/stamerf>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// TypeScript Version: 3.8
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
type VariableArgFunction = (...params: any[]) => any;
|
||||
type ArgumentTypes<F extends VariableArgFunction> = F extends (...args: infer A) => any ? A : never;
|
||||
|
||||
declare namespace BetterSqlite3 {
|
||||
interface Statement<BindParameters extends any[]> {
|
||||
database: Database;
|
||||
source: string;
|
||||
reader: boolean;
|
||||
readonly: boolean;
|
||||
busy: boolean;
|
||||
|
||||
run(...params: BindParameters): Database.RunResult;
|
||||
get(...params: BindParameters): any;
|
||||
all(...params: BindParameters): any[];
|
||||
iterate(...params: BindParameters): IterableIterator<any>;
|
||||
pluck(toggleState?: boolean): this;
|
||||
expand(toggleState?: boolean): this;
|
||||
raw(toggleState?: boolean): this;
|
||||
bind(...params: BindParameters): this;
|
||||
columns(): ColumnDefinition[];
|
||||
safeIntegers(toggleState?: boolean): this;
|
||||
}
|
||||
|
||||
interface ColumnDefinition {
|
||||
name: string;
|
||||
column: string | null;
|
||||
table: string | null;
|
||||
database: string | null;
|
||||
type: string | null;
|
||||
}
|
||||
|
||||
interface FTS5Tokenizer {
|
||||
// The resulting array consists of the following triples:
|
||||
// [..., segment_start_idx, segment_end_idx, segment | null, ...]
|
||||
//
|
||||
// `segment` could be `null` or `undefined` if no normalization was
|
||||
// performed or if the string is unchanged after the normalization.
|
||||
run(value: string): ReadonlyArray<number | string | undefined | null>;
|
||||
}
|
||||
|
||||
interface FTS5TokenizerConstructor {
|
||||
new (params: ReadonlyArray<string>): FTS5Tokenizer;
|
||||
}
|
||||
|
||||
interface Transaction<F extends VariableArgFunction> {
|
||||
(...params: ArgumentTypes<F>): ReturnType<F>;
|
||||
default(...params: ArgumentTypes<F>): ReturnType<F>;
|
||||
deferred(...params: ArgumentTypes<F>): ReturnType<F>;
|
||||
immediate(...params: ArgumentTypes<F>): ReturnType<F>;
|
||||
exclusive(...params: ArgumentTypes<F>): ReturnType<F>;
|
||||
}
|
||||
|
||||
interface VirtualTableOptions {
|
||||
rows: () => Generator;
|
||||
columns: string[];
|
||||
parameters?: string[] | undefined;
|
||||
safeIntegers?: boolean | undefined;
|
||||
directOnly?: boolean | undefined;
|
||||
}
|
||||
|
||||
interface Database {
|
||||
memory: boolean;
|
||||
readonly: boolean;
|
||||
name: string;
|
||||
open: boolean;
|
||||
inTransaction: boolean;
|
||||
|
||||
prepare<BindParameters extends any[] | {} = any[]>(
|
||||
source: string,
|
||||
): BindParameters extends any[] ? Statement<BindParameters> : Statement<[BindParameters]>;
|
||||
transaction<F extends VariableArgFunction>(fn: F): Transaction<F>;
|
||||
exec(source: string): this;
|
||||
pragma(source: string, options?: Database.PragmaOptions): any;
|
||||
function(name: string, cb: (...params: any[]) => any): this;
|
||||
function(name: string, options: Database.RegistrationOptions, cb: (...params: any[]) => any): this;
|
||||
aggregate(name: string, options: Database.AggregateOptions): this;
|
||||
close(): this;
|
||||
defaultSafeIntegers(toggleState?: boolean): this;
|
||||
backup(destinationFile: string, options?: Database.BackupOptions): Promise<Database.BackupMetadata>;
|
||||
table(name: string, options: VirtualTableOptions): this;
|
||||
unsafeMode(unsafe?: boolean): this;
|
||||
createFTS5Tokenizer(name: string, tokenizer: FTS5TokenizerConstructor): void;
|
||||
signalTokenize(value: string): Array<string>;
|
||||
}
|
||||
|
||||
interface DatabaseConstructor {
|
||||
new (filename: string, options?: Database.Options): Database;
|
||||
(filename: string, options?: Database.Options): Database;
|
||||
prototype: Database;
|
||||
|
||||
SqliteError: typeof SqliteError;
|
||||
|
||||
setLogHandler(fn: (code: number, value: string) => void): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare class SqliteError extends Error {
|
||||
name: string;
|
||||
message: string;
|
||||
code: string;
|
||||
constructor(message: string, code: string);
|
||||
}
|
||||
|
||||
declare namespace Database {
|
||||
interface RunResult {
|
||||
changes: number;
|
||||
lastInsertRowid: number | bigint;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
readonly?: boolean | undefined;
|
||||
fileMustExist?: boolean | undefined;
|
||||
timeout?: number | undefined;
|
||||
verbose?: ((message?: any, ...additionalArgs: any[]) => void) | undefined;
|
||||
nativeBinding?: string | undefined;
|
||||
}
|
||||
|
||||
interface SerializeOptions {
|
||||
attached?: string;
|
||||
}
|
||||
|
||||
interface PragmaOptions {
|
||||
simple?: boolean | undefined;
|
||||
}
|
||||
|
||||
interface RegistrationOptions {
|
||||
varargs?: boolean | undefined;
|
||||
deterministic?: boolean | undefined;
|
||||
safeIntegers?: boolean | undefined;
|
||||
directOnly?: boolean | undefined;
|
||||
}
|
||||
|
||||
interface AggregateOptions extends RegistrationOptions {
|
||||
start?: any;
|
||||
step: (total: any, next: any) => any;
|
||||
inverse?: ((total: any, dropped: any) => any) | undefined;
|
||||
result?: ((total: any) => any) | undefined;
|
||||
}
|
||||
|
||||
interface BackupMetadata {
|
||||
totalPages: number;
|
||||
remainingPages: number;
|
||||
}
|
||||
interface BackupOptions {
|
||||
progress: (info: BackupMetadata) => number;
|
||||
}
|
||||
|
||||
type SqliteError = typeof SqliteError;
|
||||
type Statement<BindParameters extends any[] | {} = any[]> = BindParameters extends any[]
|
||||
? BetterSqlite3.Statement<BindParameters>
|
||||
: BetterSqlite3.Statement<[BindParameters]>;
|
||||
type ColumnDefinition = BetterSqlite3.ColumnDefinition;
|
||||
type Transaction<T extends VariableArgFunction = VariableArgFunction> = BetterSqlite3.Transaction<T>;
|
||||
type FTS5Tokenizer = BetterSqlite3.FTS5Tokenizer;
|
||||
type Database = BetterSqlite3.Database;
|
||||
}
|
||||
|
||||
declare const Database: BetterSqlite3.DatabaseConstructor;
|
||||
export = Database;
|
||||
@ -2,12 +2,15 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('./util');
|
||||
const SqliteError = require('./sqlite-error');
|
||||
|
||||
let DEFAULT_ADDON;
|
||||
const {
|
||||
Database: CPPDatabase,
|
||||
setErrorConstructor,
|
||||
setCorruptionLogger,
|
||||
} = require('bindings')('better_sqlite3.node');
|
||||
|
||||
function Database(filenameGiven, options) {
|
||||
if (new.target == null) {
|
||||
if (new.target !== Database) {
|
||||
return new Database(filenameGiven, options);
|
||||
}
|
||||
|
||||
@ -28,27 +31,12 @@ function Database(filenameGiven, options) {
|
||||
const fileMustExist = util.getBooleanOption(options, 'fileMustExist');
|
||||
const timeout = 'timeout' in options ? options.timeout : 5000;
|
||||
const verbose = 'verbose' in options ? options.verbose : null;
|
||||
const nativeBindingPath = 'nativeBinding' in options ? options.nativeBinding : null;
|
||||
|
||||
// Validate interpreted options
|
||||
if (readonly && anonymous) throw new TypeError('In-memory/temporary databases cannot be readonly');
|
||||
if (!Number.isInteger(timeout) || timeout < 0) throw new TypeError('Expected the "timeout" option to be a positive integer');
|
||||
if (timeout > 0x7fffffff) throw new RangeError('Option "timeout" cannot be greater than 2147483647');
|
||||
if (verbose != null && typeof verbose !== 'function') throw new TypeError('Expected the "verbose" option to be a function');
|
||||
if (nativeBindingPath != null && typeof nativeBindingPath !== 'string') throw new TypeError('Expected the "nativeBinding" option to be a string');
|
||||
|
||||
// Load the native addon
|
||||
let addon;
|
||||
if (nativeBindingPath == null) {
|
||||
addon = DEFAULT_ADDON || (DEFAULT_ADDON = require('bindings')('better_sqlite3.node'));
|
||||
} else {
|
||||
addon = require(path.resolve(nativeBindingPath).replace(/(\.node)?$/, '.node'));
|
||||
}
|
||||
if (!addon.isInitialized) {
|
||||
addon.setErrorConstructor(SqliteError);
|
||||
addon.setLogHandler(logHandlerWrap);
|
||||
addon.isInitialized = true;
|
||||
}
|
||||
|
||||
// Make sure the specified directory exists
|
||||
if (!anonymous && !fs.existsSync(path.dirname(filename))) {
|
||||
@ -56,20 +44,11 @@ function Database(filenameGiven, options) {
|
||||
}
|
||||
|
||||
Object.defineProperties(this, {
|
||||
[util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null) },
|
||||
[util.cppdb]: { value: new CPPDatabase(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null) },
|
||||
...wrappers.getters,
|
||||
});
|
||||
}
|
||||
|
||||
let logHandler;
|
||||
function logHandlerWrap(code, warning) {
|
||||
if (logHandler) {
|
||||
logHandler(code, warning);
|
||||
}
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
const wrappers = require('./methods/wrappers');
|
||||
Database.prototype.prepare = wrappers.prepare;
|
||||
Database.prototype.transaction = require('./methods/transaction');
|
||||
@ -77,18 +56,13 @@ Database.prototype.pragma = require('./methods/pragma');
|
||||
Database.prototype.backup = require('./methods/backup');
|
||||
Database.prototype.function = require('./methods/function');
|
||||
Database.prototype.aggregate = require('./methods/aggregate');
|
||||
Database.prototype.table = require('./methods/table');
|
||||
Database.prototype.createFTS5Tokenizer = require('./methods/createFTS5Tokenizer');
|
||||
Database.prototype.loadExtension = wrappers.loadExtension;
|
||||
Database.prototype.exec = wrappers.exec;
|
||||
Database.prototype.close = wrappers.close;
|
||||
Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers;
|
||||
Database.prototype.unsafeMode = wrappers.unsafeMode;
|
||||
Database.prototype.signalTokenize = wrappers.signalTokenize;
|
||||
Database.prototype[util.inspect] = require('./methods/inspect');
|
||||
|
||||
// Static
|
||||
Database.setLogHandler = function setLogHandler(fn) {
|
||||
logHandler = fn;
|
||||
}
|
||||
Database.setCorruptionLogger = setCorruptionLogger;
|
||||
|
||||
module.exports = Database;
|
||||
setErrorConstructor(require('./sqlite-error'));
|
||||
|
||||
@ -14,7 +14,6 @@ module.exports = function defineAggregate(name, options) {
|
||||
const result = getFunctionOption(options, 'result', false);
|
||||
const safeIntegers = 'safeIntegers' in options ? +getBooleanOption(options, 'safeIntegers') : 2;
|
||||
const deterministic = getBooleanOption(options, 'deterministic');
|
||||
const directOnly = getBooleanOption(options, 'directOnly');
|
||||
const varargs = getBooleanOption(options, 'varargs');
|
||||
let argCount = -1;
|
||||
|
||||
@ -25,7 +24,7 @@ module.exports = function defineAggregate(name, options) {
|
||||
if (argCount > 100) throw new RangeError('User-defined functions cannot have more than 100 arguments');
|
||||
}
|
||||
|
||||
this[cppdb].aggregate(start, step, inverse, result, name, argCount, safeIntegers, deterministic, directOnly);
|
||||
this[cppdb].aggregate(start, step, inverse, result, name, argCount, safeIntegers, deterministic);
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
const { cppdb } = require('../util');
|
||||
|
||||
module.exports = function createFTS5Tokenizer(name, factory) {
|
||||
// Validate arguments
|
||||
if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string');
|
||||
if (!name) throw new TypeError('Virtual table module name cannot be an empty string');
|
||||
if (typeof factory !== 'function') throw new TypeError('Expected second argument to be a constructor');
|
||||
|
||||
this[cppdb].createFTS5Tokenizer(name, function create(params) {
|
||||
const instance = new factory(params);
|
||||
|
||||
function run(str) {
|
||||
if (!instance.run) {
|
||||
// This will throw in C++
|
||||
return;
|
||||
}
|
||||
return instance.run(str);
|
||||
}
|
||||
|
||||
return run;
|
||||
});
|
||||
return this;
|
||||
};
|
||||
@ -15,7 +15,6 @@ module.exports = function defineFunction(name, options, fn) {
|
||||
// Interpret options
|
||||
const safeIntegers = 'safeIntegers' in options ? +getBooleanOption(options, 'safeIntegers') : 2;
|
||||
const deterministic = getBooleanOption(options, 'deterministic');
|
||||
const directOnly = getBooleanOption(options, 'directOnly');
|
||||
const varargs = getBooleanOption(options, 'varargs');
|
||||
let argCount = -1;
|
||||
|
||||
@ -26,6 +25,6 @@ module.exports = function defineFunction(name, options, fn) {
|
||||
if (argCount > 100) throw new RangeError('User-defined functions cannot have more than 100 arguments');
|
||||
}
|
||||
|
||||
this[cppdb].function(fn, name, argCount, safeIntegers, deterministic, directOnly);
|
||||
this[cppdb].function(fn, name, argCount, safeIntegers, deterministic);
|
||||
return this;
|
||||
};
|
||||
|
||||
@ -1,189 +0,0 @@
|
||||
'use strict';
|
||||
const { cppdb } = require('../util');
|
||||
|
||||
module.exports = function defineTable(name, factory) {
|
||||
// Validate arguments
|
||||
if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string');
|
||||
if (!name) throw new TypeError('Virtual table module name cannot be an empty string');
|
||||
|
||||
// Determine whether the module is eponymous-only or not
|
||||
let eponymous = false;
|
||||
if (typeof factory === 'object' && factory !== null) {
|
||||
eponymous = true;
|
||||
factory = defer(parseTableDefinition(factory, 'used', name));
|
||||
} else {
|
||||
if (typeof factory !== 'function') throw new TypeError('Expected second argument to be a function or a table definition object');
|
||||
factory = wrapFactory(factory);
|
||||
}
|
||||
|
||||
this[cppdb].table(factory, name, eponymous);
|
||||
return this;
|
||||
};
|
||||
|
||||
function wrapFactory(factory) {
|
||||
return function virtualTableFactory(moduleName, databaseName, tableName, ...args) {
|
||||
const thisObject = {
|
||||
module: moduleName,
|
||||
database: databaseName,
|
||||
table: tableName,
|
||||
};
|
||||
|
||||
// Generate a new table definition by invoking the factory
|
||||
const def = apply.call(factory, thisObject, args);
|
||||
if (typeof def !== 'object' || def === null) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" did not return a table definition object`);
|
||||
}
|
||||
|
||||
return parseTableDefinition(def, 'returned', moduleName);
|
||||
};
|
||||
}
|
||||
|
||||
function parseTableDefinition(def, verb, moduleName) {
|
||||
// Validate required properties
|
||||
if (!hasOwnProperty.call(def, 'rows')) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "rows" property`);
|
||||
}
|
||||
if (!hasOwnProperty.call(def, 'columns')) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "columns" property`);
|
||||
}
|
||||
|
||||
// Validate "rows" property
|
||||
const rows = def.rows;
|
||||
if (typeof rows !== 'function' || Object.getPrototypeOf(rows) !== GeneratorFunctionPrototype) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "rows" property (should be a generator function)`);
|
||||
}
|
||||
|
||||
// Validate "columns" property
|
||||
let columns = def.columns;
|
||||
if (!Array.isArray(columns) || !(columns = [...columns]).every(x => typeof x === 'string')) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "columns" property (should be an array of strings)`);
|
||||
}
|
||||
if (columns.length !== new Set(columns).size) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate column names`);
|
||||
}
|
||||
if (!columns.length) {
|
||||
throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with zero columns`);
|
||||
}
|
||||
|
||||
// Validate "parameters" property
|
||||
let parameters;
|
||||
if (hasOwnProperty.call(def, 'parameters')) {
|
||||
parameters = def.parameters;
|
||||
if (!Array.isArray(parameters) || !(parameters = [...parameters]).every(x => typeof x === 'string')) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "parameters" property (should be an array of strings)`);
|
||||
}
|
||||
} else {
|
||||
parameters = inferParameters(rows);
|
||||
}
|
||||
if (parameters.length !== new Set(parameters).size) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate parameter names`);
|
||||
}
|
||||
if (parameters.length > 32) {
|
||||
throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with more than the maximum number of 32 parameters`);
|
||||
}
|
||||
for (const parameter of parameters) {
|
||||
if (columns.includes(parameter)) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with column "${parameter}" which was ambiguously defined as both a column and parameter`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate "safeIntegers" option
|
||||
let safeIntegers = 2;
|
||||
if (hasOwnProperty.call(def, 'safeIntegers')) {
|
||||
const bool = def.safeIntegers;
|
||||
if (typeof bool !== 'boolean') {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "safeIntegers" property (should be a boolean)`);
|
||||
}
|
||||
safeIntegers = +bool;
|
||||
}
|
||||
|
||||
// Validate "directOnly" option
|
||||
let directOnly = false;
|
||||
if (hasOwnProperty.call(def, 'directOnly')) {
|
||||
directOnly = def.directOnly;
|
||||
if (typeof directOnly !== 'boolean') {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "directOnly" property (should be a boolean)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate SQL for the virtual table definition
|
||||
const columnDefinitions = [
|
||||
...parameters.map(identifier).map(str => `${str} HIDDEN`),
|
||||
...columns.map(identifier),
|
||||
];
|
||||
return [
|
||||
`CREATE TABLE x(${columnDefinitions.join(', ')});`,
|
||||
wrapGenerator(rows, new Map(columns.map((x, i) => [x, parameters.length + i])), moduleName),
|
||||
parameters,
|
||||
safeIntegers,
|
||||
directOnly,
|
||||
];
|
||||
}
|
||||
|
||||
function wrapGenerator(generator, columnMap, moduleName) {
|
||||
return function* virtualTable(...args) {
|
||||
/*
|
||||
We must defensively clone any buffers in the arguments, because
|
||||
otherwise the generator could mutate one of them, which would cause
|
||||
us to return incorrect values for hidden columns, potentially
|
||||
corrupting the database.
|
||||
*/
|
||||
const output = args.map(x => Buffer.isBuffer(x) ? Buffer.from(x) : x);
|
||||
for (let i = 0; i < columnMap.size; ++i) {
|
||||
output.push(null); // Fill with nulls to prevent gaps in array (v8 optimization)
|
||||
}
|
||||
for (const row of generator(...args)) {
|
||||
if (Array.isArray(row)) {
|
||||
extractRowArray(row, output, columnMap.size, moduleName);
|
||||
yield output;
|
||||
} else if (typeof row === 'object' && row !== null) {
|
||||
extractRowObject(row, output, columnMap, moduleName);
|
||||
yield output;
|
||||
} else {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" yielded something that isn't a valid row object`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function extractRowArray(row, output, columnCount, moduleName) {
|
||||
if (row.length !== columnCount) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an incorrect number of columns`);
|
||||
}
|
||||
const offset = output.length - columnCount;
|
||||
for (let i = 0; i < columnCount; ++i) {
|
||||
output[i + offset] = row[i];
|
||||
}
|
||||
}
|
||||
|
||||
function extractRowObject(row, output, columnMap, moduleName) {
|
||||
let count = 0;
|
||||
for (const key of Object.keys(row)) {
|
||||
const index = columnMap.get(key);
|
||||
if (index === undefined) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an undeclared column "${key}"`);
|
||||
}
|
||||
output[index] = row[key];
|
||||
count += 1;
|
||||
}
|
||||
if (count !== columnMap.size) {
|
||||
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with missing columns`);
|
||||
}
|
||||
}
|
||||
|
||||
function inferParameters({ length }) {
|
||||
if (!Number.isInteger(length) || length < 0) {
|
||||
throw new TypeError('Expected function.length to be a positive integer');
|
||||
}
|
||||
const params = [];
|
||||
for (let i = 0; i < length; ++i) {
|
||||
params.push(`$${i + 1}`);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
const { hasOwnProperty } = Object.prototype;
|
||||
const { apply } = Function.prototype;
|
||||
const GeneratorFunctionPrototype = Object.getPrototypeOf(function*(){});
|
||||
const identifier = str => `"${str.replace(/"/g, '""')}"`;
|
||||
const defer = x => () => x;
|
||||
@ -15,6 +15,11 @@ exports.close = function close() {
|
||||
return this;
|
||||
};
|
||||
|
||||
exports.loadExtension = function loadExtension(...args) {
|
||||
this[cppdb].loadExtension(...args);
|
||||
return this;
|
||||
};
|
||||
|
||||
exports.defaultSafeIntegers = function defaultSafeIntegers(...args) {
|
||||
this[cppdb].defaultSafeIntegers(...args);
|
||||
return this;
|
||||
@ -25,10 +30,6 @@ exports.unsafeMode = function unsafeMode(...args) {
|
||||
return this;
|
||||
};
|
||||
|
||||
exports.signalTokenize = function signalTokenize(...args) {
|
||||
return this[cppdb].signalTokenize(...args);
|
||||
};
|
||||
|
||||
exports.getters = {
|
||||
name: {
|
||||
get: function name() { return this[cppdb].name; },
|
||||
|
||||
@ -12,7 +12,8 @@ function SqliteError(message, code) {
|
||||
descriptor.value = '' + message;
|
||||
Object.defineProperty(this, 'message', descriptor);
|
||||
Error.captureStackTrace(this, SqliteError);
|
||||
this.code = code;
|
||||
descriptor.value = code;
|
||||
Object.defineProperty(this, 'code', descriptor);
|
||||
}
|
||||
Object.setPrototypeOf(SqliteError, Error);
|
||||
Object.setPrototypeOf(SqliteError.prototype, Error.prototype);
|
||||
|
||||
38
package.json
38
package.json
@ -1,41 +1,37 @@
|
||||
{
|
||||
"name": "@signalapp/better-sqlite3",
|
||||
"version": "9.0.13",
|
||||
"name": "better-sqlite3",
|
||||
"version": "7.1.4",
|
||||
"description": "The fastest and simplest library for SQLite3 in Node.js.",
|
||||
"homepage": "http://github.com/WiseLibs/better-sqlite3",
|
||||
"homepage": "http://github.com/JoshuaWise/better-sqlite3",
|
||||
"author": "Joshua Wise <joshuathomaswise@gmail.com>",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/WiseLibs/better-sqlite3.git"
|
||||
"url": "git://github.com/JoshuaWise/better-sqlite3.git"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"types": "index.d.ts",
|
||||
"files": [
|
||||
"index.d.ts",
|
||||
"binding.gyp",
|
||||
"src/*.[ch]pp",
|
||||
"lib/**",
|
||||
"deps/**",
|
||||
"!deps/sqlcipher.tar.gz",
|
||||
"!deps/unverified.tmp"
|
||||
],
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"tar": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.6",
|
||||
"cli-color": "^2.0.2",
|
||||
"fs-extra": "^10.1.0",
|
||||
"chai": "^4.3.4",
|
||||
"cli-color": "^2.0.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"mocha": "^8.3.2",
|
||||
"nodemark": "^0.3.0",
|
||||
"sqlite": "^4.0.23",
|
||||
"sqlite": "^4.0.19",
|
||||
"sqlite3": "^5.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "xcrun clang-format --style=chromium -Werror --verbose -i src/*.cpp src/*.hpp",
|
||||
"install": "npm run build-release",
|
||||
"build-release": "node-gyp rebuild --release",
|
||||
"build-debug": "node-gyp rebuild --debug",
|
||||
"rebuild-release": "npm run lzz && npm run build-release",
|
||||
"rebuild-debug": "npm run lzz && npm run build-debug",
|
||||
"test": "mocha --exit --slow=75 --timeout=5000",
|
||||
"benchmark": "node benchmark"
|
||||
"benchmark": "node benchmark",
|
||||
"download": "bash ./deps/download.sh",
|
||||
"lzz": "lzz -hx hpp -sx cpp -k BETTER_SQLITE3 -d -hl -sl -e ./src/better_sqlite3.lzz"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
82
src/better_sqlite3.lzz
Normal file
82
src/better_sqlite3.lzz
Normal file
@ -0,0 +1,82 @@
|
||||
#hdr
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <sqlite3.h>
|
||||
#include <node.h>
|
||||
#include <node_object_wrap.h>
|
||||
#include <node_buffer.h>
|
||||
#end
|
||||
|
||||
#insert "util/macros.lzz"
|
||||
#insert "util/query-macros.lzz"
|
||||
#insert "util/constants.lzz"
|
||||
#insert "util/bind-map.lzz"
|
||||
struct Addon;
|
||||
class Statement;
|
||||
class Backup;
|
||||
#insert "objects/database.lzz"
|
||||
#insert "objects/statement.lzz"
|
||||
#insert "objects/statement-iterator.lzz"
|
||||
#insert "objects/backup.lzz"
|
||||
#insert "util/custom-function.lzz"
|
||||
#insert "util/custom-aggregate.lzz"
|
||||
#insert "util/data.lzz"
|
||||
#insert "util/binder.lzz"
|
||||
|
||||
struct Addon {
|
||||
Addon(v8::Isolate* isolate) : privileged_info(NULL), next_id(0), cs(isolate) {}
|
||||
|
||||
CopyablePersistent<v8::Function> Statement;
|
||||
CopyablePersistent<v8::Function> StatementIterator;
|
||||
CopyablePersistent<v8::Function> Backup;
|
||||
CopyablePersistent<v8::Function> SqliteError;
|
||||
NODE_ARGUMENTS_POINTER privileged_info;
|
||||
sqlite3_uint64 next_id;
|
||||
CS cs;
|
||||
std::set<Database*, Database::CompareDatabase> dbs;
|
||||
|
||||
NODE_METHOD(JS_setErrorConstructor) {
|
||||
REQUIRE_ARGUMENT_FUNCTION(first, v8::Local<v8::Function> SqliteError);
|
||||
OnlyAddon->SqliteError.Reset(OnlyIsolate, SqliteError);
|
||||
}
|
||||
|
||||
static void Cleanup(void* ptr) {
|
||||
Addon* addon = static_cast<Addon*>(ptr);
|
||||
for (Database* db : addon->dbs) db->CloseHandles();
|
||||
addon->dbs.clear();
|
||||
delete addon;
|
||||
}
|
||||
|
||||
inline sqlite3_uint64 NextId() {
|
||||
return next_id++;
|
||||
}
|
||||
};
|
||||
|
||||
#src
|
||||
NODE_MODULE_INIT(/* exports, context */) {
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
|
||||
// Initialize addon instance.
|
||||
Addon* addon = new Addon(isolate);
|
||||
v8::Local<v8::External> data = v8::External::New(isolate, addon);
|
||||
node::AddEnvironmentCleanupHook(isolate, Addon::Cleanup, addon);
|
||||
|
||||
// Create and export native-backed classes and functions.
|
||||
exports->Set(context, InternalizedFromLatin1(isolate, "Database"), Database::Init(isolate, data)).FromJust();
|
||||
exports->Set(context, InternalizedFromLatin1(isolate, "Statement"), Statement::Init(isolate, data)).FromJust();
|
||||
exports->Set(context, InternalizedFromLatin1(isolate, "StatementIterator"), StatementIterator::Init(isolate, data)).FromJust();
|
||||
exports->Set(context, InternalizedFromLatin1(isolate, "Backup"), Backup::Init(isolate, data)).FromJust();
|
||||
exports->Set(context, InternalizedFromLatin1(isolate, "setErrorConstructor"), v8::FunctionTemplate::New(isolate, Addon::JS_setErrorConstructor, data)->GetFunction(context).ToLocalChecked()).FromJust();
|
||||
|
||||
// Store addon instance data.
|
||||
addon->Statement.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "Statement")).ToLocalChecked()));
|
||||
addon->StatementIterator.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "StatementIterator")).ToLocalChecked()));
|
||||
addon->Backup.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "Backup")).ToLocalChecked()));
|
||||
}
|
||||
#end
|
||||
@ -1,39 +0,0 @@
|
||||
#ifndef SRC_LOCAL_VECTOR_H_
|
||||
#define SRC_LOCAL_VECTOR_H_
|
||||
|
||||
#include <node.h>
|
||||
|
||||
// See: https://github.com/v8/v8/commit/e1649301dfbfd34a448c3a0232c8a6206b716c73
|
||||
// Required V8 verison: 12.0.54 or higher
|
||||
|
||||
#if V8_MAJOR_VERSION > 12 || \
|
||||
V8_MINOR_VERSION == 12 && \
|
||||
(V8_MINOR_VERSION > 0 || \
|
||||
V8_MINOR_VERSION == 0 && V8_PATCH_VERSION >= 54)
|
||||
|
||||
template <class T>
|
||||
class LocalVector : public v8::LocalVector<T> {
|
||||
public:
|
||||
LocalVector(v8::Isolate* isolate) : v8::LocalVector<T>(isolate) {}
|
||||
|
||||
inline bool is_supported() { return true; }
|
||||
};
|
||||
|
||||
#else
|
||||
|
||||
template <class T>
|
||||
class LocalVector {
|
||||
public:
|
||||
LocalVector(v8::Isolate* isolate) {}
|
||||
|
||||
inline void reserve(size_t size) {}
|
||||
inline size_t size() { return 0; }
|
||||
inline void emplace_back(v8::Local<T> value) { abort(); }
|
||||
inline v8::Local<T>* data() { abort(); }
|
||||
|
||||
inline bool is_supported() { return false; }
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif // SRC_LOCAL_VECTOR_H_
|
||||
132
src/objects/backup.lzz
Normal file
132
src/objects/backup.lzz
Normal file
@ -0,0 +1,132 @@
|
||||
class Backup : public node::ObjectWrap {
|
||||
public:
|
||||
|
||||
INIT(Init) {
|
||||
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "Backup");
|
||||
SetPrototypeMethod(isolate, data, t, "transfer", JS_transfer);
|
||||
SetPrototypeMethod(isolate, data, t, "close", JS_close);
|
||||
return t->GetFunction(OnlyContext).ToLocalChecked();
|
||||
}
|
||||
|
||||
// Used to support ordered containers.
|
||||
static inline bool Compare(Backup const * const a, Backup const * const b) {
|
||||
return a->id < b->id;
|
||||
}
|
||||
|
||||
// Whenever this is used, db->RemoveBackup must be invoked beforehand.
|
||||
void CloseHandles() {
|
||||
if (alive) {
|
||||
alive = false;
|
||||
std::string filename(sqlite3_db_filename(dest_handle, "main"));
|
||||
sqlite3_backup_finish(backup_handle);
|
||||
int status = sqlite3_close(dest_handle);
|
||||
assert(status == SQLITE_OK); ((void)status);
|
||||
if (unlink) remove(filename.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
~Backup() {
|
||||
if (alive) db->RemoveBackup(this);
|
||||
CloseHandles();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
explicit Backup(Database* _db, sqlite3* _dest_handle, sqlite3_backup* _backup_handle, sqlite3_uint64 _id, bool _unlink) : node::ObjectWrap(),
|
||||
db(_db),
|
||||
dest_handle(_dest_handle),
|
||||
backup_handle(_backup_handle),
|
||||
id(_id),
|
||||
alive(true),
|
||||
unlink(_unlink) {
|
||||
assert(db != NULL);
|
||||
assert(dest_handle != NULL);
|
||||
assert(backup_handle != NULL);
|
||||
db->AddBackup(this);
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_new) {
|
||||
UseAddon;
|
||||
if (!addon->privileged_info) return ThrowTypeError("Disabled constructor");
|
||||
assert(info.IsConstructCall());
|
||||
Database* db = Unwrap<Database>(addon->privileged_info->This());
|
||||
REQUIRE_DATABASE_OPEN(db->GetState());
|
||||
REQUIRE_DATABASE_NOT_BUSY(db->GetState());
|
||||
|
||||
v8::Local<v8::Object> database = v8::Local<v8::Object>::Cast((*addon->privileged_info)[0]);
|
||||
v8::Local<v8::String> attachedName = v8::Local<v8::String>::Cast((*addon->privileged_info)[1]);
|
||||
v8::Local<v8::String> destFile = v8::Local<v8::String>::Cast((*addon->privileged_info)[2]);
|
||||
bool unlink = v8::Local<v8::Boolean>::Cast((*addon->privileged_info)[3])->Value();
|
||||
|
||||
UseIsolate;
|
||||
sqlite3* dest_handle;
|
||||
v8::String::Utf8Value dest_file(isolate, destFile);
|
||||
v8::String::Utf8Value attached_name(isolate, attachedName);
|
||||
int mask = (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
|
||||
|
||||
if (sqlite3_open_v2(*dest_file, &dest_handle, mask, NULL) != SQLITE_OK) {
|
||||
Database::ThrowSqliteError(addon, dest_handle);
|
||||
int status = sqlite3_close(dest_handle);
|
||||
assert(status == SQLITE_OK); ((void)status);
|
||||
return;
|
||||
}
|
||||
|
||||
sqlite3_extended_result_codes(dest_handle, 1);
|
||||
sqlite3_limit(dest_handle, SQLITE_LIMIT_LENGTH, INT_MAX);
|
||||
sqlite3_backup* backup_handle = sqlite3_backup_init(dest_handle, "main", db->GetHandle(), *attached_name);
|
||||
if (backup_handle == NULL) {
|
||||
Database::ThrowSqliteError(addon, dest_handle);
|
||||
int status = sqlite3_close(dest_handle);
|
||||
assert(status == SQLITE_OK); ((void)status);
|
||||
return;
|
||||
}
|
||||
|
||||
Backup* backup = new Backup(db, dest_handle, backup_handle, addon->NextId(), unlink);
|
||||
backup->Wrap(info.This());
|
||||
SetFrozen(isolate, OnlyContext, info.This(), addon->cs.database, database);
|
||||
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_transfer) {
|
||||
Backup* backup = Unwrap<Backup>(info.This());
|
||||
REQUIRE_ARGUMENT_INT32(first, int pages);
|
||||
REQUIRE_DATABASE_OPEN(backup->db->GetState());
|
||||
assert(backup->db->GetState()->busy == false);
|
||||
assert(backup->alive == true);
|
||||
|
||||
sqlite3_backup* backup_handle = backup->backup_handle;
|
||||
int status = sqlite3_backup_step(backup_handle, pages) & 0xff;
|
||||
|
||||
Addon* addon = backup->db->GetAddon();
|
||||
if (status == SQLITE_OK || status == SQLITE_DONE || status == SQLITE_BUSY) {
|
||||
int total_pages = sqlite3_backup_pagecount(backup_handle);
|
||||
int remaining_pages = sqlite3_backup_remaining(backup_handle);
|
||||
UseIsolate;
|
||||
UseContext;
|
||||
v8::Local<v8::Object> result = v8::Object::New(isolate);
|
||||
result->Set(ctx, CS::Get(isolate, addon->cs.totalPages), v8::Int32::New(isolate, total_pages)).FromJust();
|
||||
result->Set(ctx, CS::Get(isolate, addon->cs.remainingPages), v8::Int32::New(isolate, remaining_pages)).FromJust();
|
||||
info.GetReturnValue().Set(result);
|
||||
if (status == SQLITE_DONE) backup->unlink = false;
|
||||
} else {
|
||||
Database::ThrowSqliteError(addon, sqlite3_errstr(status), status);
|
||||
}
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_close) {
|
||||
Backup* backup = Unwrap<Backup>(info.This());
|
||||
assert(backup->db->GetState()->busy == false);
|
||||
if (backup->alive) backup->db->RemoveBackup(backup);
|
||||
backup->CloseHandles();
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
Database* const db;
|
||||
sqlite3* const dest_handle;
|
||||
sqlite3_backup* const backup_handle;
|
||||
const sqlite3_uint64 id;
|
||||
bool alive;
|
||||
bool unlink;
|
||||
};
|
||||
376
src/objects/database.lzz
Normal file
376
src/objects/database.lzz
Normal file
@ -0,0 +1,376 @@
|
||||
class Database : public node::ObjectWrap {
|
||||
public:
|
||||
|
||||
INIT(Init) {
|
||||
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "Database");
|
||||
SetPrototypeMethod(isolate, data, t, "prepare", JS_prepare);
|
||||
SetPrototypeMethod(isolate, data, t, "exec", JS_exec);
|
||||
SetPrototypeMethod(isolate, data, t, "backup", JS_backup);
|
||||
SetPrototypeMethod(isolate, data, t, "function", JS_function);
|
||||
SetPrototypeMethod(isolate, data, t, "aggregate", JS_aggregate);
|
||||
SetPrototypeMethod(isolate, data, t, "loadExtension", JS_loadExtension);
|
||||
SetPrototypeMethod(isolate, data, t, "close", JS_close);
|
||||
SetPrototypeMethod(isolate, data, t, "defaultSafeIntegers", JS_defaultSafeIntegers);
|
||||
SetPrototypeMethod(isolate, data, t, "unsafeMode", JS_unsafeMode);
|
||||
SetPrototypeGetter(isolate, data, t, "open", JS_open);
|
||||
SetPrototypeGetter(isolate, data, t, "inTransaction", JS_inTransaction);
|
||||
return t->GetFunction(OnlyContext).ToLocalChecked();
|
||||
}
|
||||
|
||||
// Used to support ordered containers.
|
||||
class CompareDatabase { public:
|
||||
bool operator() (Database const * const a, Database const * const b) const {
|
||||
return a < b;
|
||||
}
|
||||
};
|
||||
class CompareStatement { public:
|
||||
bool operator() (Statement const * const a, Statement const * const b) const {
|
||||
return Statement::Compare(a, b);
|
||||
}
|
||||
};
|
||||
class CompareBackup { public:
|
||||
bool operator() (Backup const * const a, Backup const * const b) const {
|
||||
return Backup::Compare(a, b);
|
||||
}
|
||||
};
|
||||
|
||||
// Proper error handling logic for when an sqlite3 operation fails.
|
||||
void ThrowDatabaseError() {
|
||||
if (was_js_error) was_js_error = false;
|
||||
else ThrowSqliteError(addon, db_handle);
|
||||
}
|
||||
static void ThrowSqliteError(Addon* addon, sqlite3* db_handle) {
|
||||
assert(db_handle != NULL);
|
||||
ThrowSqliteError(addon, sqlite3_errmsg(db_handle), sqlite3_extended_errcode(db_handle));
|
||||
}
|
||||
static void ThrowSqliteError(Addon* addon, const char* message, int code) {
|
||||
assert(message != NULL);
|
||||
assert((code & 0xff) != SQLITE_OK);
|
||||
assert((code & 0xff) != SQLITE_ROW);
|
||||
assert((code & 0xff) != SQLITE_DONE);
|
||||
EasyIsolate;
|
||||
v8::Local<v8::Value> args[2] = {
|
||||
StringFromUtf8(isolate, message, -1),
|
||||
addon->cs.Code(isolate, code)
|
||||
};
|
||||
isolate->ThrowException(v8::Local<v8::Function>::New(isolate, addon->SqliteError)
|
||||
->NewInstance(OnlyContext, 2, args)
|
||||
.ToLocalChecked());
|
||||
}
|
||||
|
||||
// Allows Statements to log their executed SQL.
|
||||
bool Log(v8::Isolate* isolate, sqlite3_stmt* handle) {
|
||||
assert(was_js_error == false);
|
||||
if (!has_logger) return false;
|
||||
char* expanded = sqlite3_expanded_sql(handle);
|
||||
v8::Local<v8::Value> arg = StringFromUtf8(isolate, expanded ? expanded : sqlite3_sql(handle), -1);
|
||||
was_js_error = v8::Local<v8::Function>::Cast(v8::Local<v8::Value>::New(isolate, logger))
|
||||
->Call(OnlyContext, v8::Undefined(isolate), 1, &arg)
|
||||
.IsEmpty();
|
||||
if (expanded) sqlite3_free(expanded);
|
||||
return was_js_error;
|
||||
}
|
||||
|
||||
// Allow Statements to manage themselves when created and garbage collected.
|
||||
inline void AddStatement(Statement* stmt) { stmts.insert(stmts.end(), stmt); }
|
||||
inline void RemoveStatement(Statement* stmt) { stmts.erase(stmt); }
|
||||
|
||||
// Allow Backups to manage themselves when created and garbage collected.
|
||||
inline void AddBackup(Backup* backup) { backups.insert(backups.end(), backup); }
|
||||
inline void RemoveBackup(Backup* backup) { backups.erase(backup); }
|
||||
|
||||
// A view for Statements to see and modify Database state.
|
||||
// The order of these fields must exactly match their actual order.
|
||||
struct State {
|
||||
const bool open;
|
||||
bool busy;
|
||||
const bool safe_ints;
|
||||
const bool unsafe_mode;
|
||||
bool was_js_error;
|
||||
const bool has_logger;
|
||||
unsigned short iterators;
|
||||
Addon* const addon;
|
||||
};
|
||||
inline State* GetState() {
|
||||
return reinterpret_cast<State*>(&open);
|
||||
}
|
||||
inline sqlite3* GetHandle() {
|
||||
return db_handle;
|
||||
}
|
||||
inline Addon* GetAddon() {
|
||||
return addon;
|
||||
}
|
||||
|
||||
// Whenever this is used, addon->dbs.erase() must be invoked beforehand.
|
||||
void CloseHandles() {
|
||||
if (open) {
|
||||
open = false;
|
||||
for (Statement* stmt : stmts) stmt->CloseHandles();
|
||||
for (Backup* backup : backups) backup->CloseHandles();
|
||||
stmts.clear();
|
||||
backups.clear();
|
||||
int status = sqlite3_close(db_handle);
|
||||
assert(status == SQLITE_OK); ((void)status);
|
||||
}
|
||||
}
|
||||
|
||||
~Database() {
|
||||
if (open) addon->dbs.erase(this);
|
||||
CloseHandles();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
explicit Database(sqlite3* _db_handle, v8::Isolate* isolate, Addon* _addon, v8::Local<v8::Value> _logger) : node::ObjectWrap(),
|
||||
db_handle(_db_handle),
|
||||
open(true),
|
||||
busy(false),
|
||||
safe_ints(false),
|
||||
unsafe_mode(false),
|
||||
was_js_error(false),
|
||||
has_logger(_logger->IsFunction()),
|
||||
iterators(0),
|
||||
addon(_addon),
|
||||
logger(isolate, _logger),
|
||||
stmts(),
|
||||
backups() {
|
||||
assert(_db_handle != NULL);
|
||||
addon->dbs.insert(this);
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_new) {
|
||||
assert(info.IsConstructCall());
|
||||
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> filename);
|
||||
REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> filenameGiven);
|
||||
REQUIRE_ARGUMENT_BOOLEAN(third, bool in_memory);
|
||||
REQUIRE_ARGUMENT_BOOLEAN(fourth, bool readonly);
|
||||
REQUIRE_ARGUMENT_BOOLEAN(fifth, bool must_exist);
|
||||
REQUIRE_ARGUMENT_INT32(sixth, int timeout);
|
||||
REQUIRE_ARGUMENT_ANY(seventh, v8::Local<v8::Value> logger);
|
||||
|
||||
UseAddon;
|
||||
UseIsolate;
|
||||
sqlite3* db_handle;
|
||||
v8::String::Utf8Value utf8(isolate, filename);
|
||||
int mask = readonly ? SQLITE_OPEN_READONLY
|
||||
: must_exist ? SQLITE_OPEN_READWRITE
|
||||
: (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
|
||||
|
||||
if (sqlite3_open_v2(*utf8, &db_handle, mask, NULL) != SQLITE_OK) {
|
||||
ThrowSqliteError(addon, db_handle);
|
||||
int status = sqlite3_close(db_handle);
|
||||
assert(status == SQLITE_OK); ((void)status);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(sqlite3_db_mutex(db_handle) == NULL);
|
||||
sqlite3_extended_result_codes(db_handle, 1);
|
||||
sqlite3_busy_timeout(db_handle, timeout);
|
||||
sqlite3_limit(db_handle, SQLITE_LIMIT_LENGTH, MAX_BUFFER_SIZE < MAX_STRING_SIZE ? MAX_BUFFER_SIZE : MAX_STRING_SIZE);
|
||||
sqlite3_limit(db_handle, SQLITE_LIMIT_SQL_LENGTH, MAX_STRING_SIZE);
|
||||
int status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);
|
||||
assert(status == SQLITE_OK);
|
||||
status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL);
|
||||
assert(status == SQLITE_OK);
|
||||
|
||||
UseContext;
|
||||
Database* db = new Database(db_handle, isolate, addon, logger);
|
||||
db->Wrap(info.This());
|
||||
SetFrozen(isolate, ctx, info.This(), addon->cs.memory, v8::Boolean::New(isolate, in_memory));
|
||||
SetFrozen(isolate, ctx, info.This(), addon->cs.readonly, v8::Boolean::New(isolate, readonly));
|
||||
SetFrozen(isolate, ctx, info.This(), addon->cs.name, filenameGiven);
|
||||
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_prepare) {
|
||||
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> source);
|
||||
REQUIRE_ARGUMENT_OBJECT(second, v8::Local<v8::Object> database);
|
||||
REQUIRE_ARGUMENT_BOOLEAN(third, bool pragmaMode);
|
||||
(void)source;
|
||||
(void)database;
|
||||
(void)pragmaMode;
|
||||
UseAddon;
|
||||
UseIsolate;
|
||||
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->Statement);
|
||||
addon->privileged_info = &info;
|
||||
v8::MaybeLocal<v8::Object> maybe_statement = c->NewInstance(OnlyContext, 0, NULL);
|
||||
addon->privileged_info = NULL;
|
||||
if (!maybe_statement.IsEmpty()) info.GetReturnValue().Set(maybe_statement.ToLocalChecked());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_exec) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> source);
|
||||
REQUIRE_DATABASE_OPEN(db);
|
||||
REQUIRE_DATABASE_NOT_BUSY(db);
|
||||
REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db);
|
||||
db->busy = true;
|
||||
|
||||
UseIsolate;
|
||||
v8::String::Utf8Value utf8(isolate, source);
|
||||
const char* sql = *utf8;
|
||||
const char* tail;
|
||||
|
||||
int status;
|
||||
const bool has_logger = db->has_logger;
|
||||
sqlite3* const db_handle = db->db_handle;
|
||||
sqlite3_stmt* handle;
|
||||
|
||||
for (;;) {
|
||||
while (IS_SKIPPED(*sql)) ++sql;
|
||||
status = sqlite3_prepare_v2(db_handle, sql, -1, &handle, &tail);
|
||||
sql = tail;
|
||||
if (!handle) break;
|
||||
if (has_logger && db->Log(isolate, handle)) {
|
||||
sqlite3_finalize(handle);
|
||||
status = -1;
|
||||
break;
|
||||
}
|
||||
do status = sqlite3_step(handle);
|
||||
while (status == SQLITE_ROW);
|
||||
status = sqlite3_finalize(handle);
|
||||
if (status != SQLITE_OK) break;
|
||||
}
|
||||
|
||||
db->busy = false;
|
||||
if (status != SQLITE_OK) {
|
||||
db->ThrowDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_backup) {
|
||||
REQUIRE_ARGUMENT_OBJECT(first, v8::Local<v8::Object> database);
|
||||
REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> attachedName);
|
||||
REQUIRE_ARGUMENT_STRING(third, v8::Local<v8::String> destFile);
|
||||
REQUIRE_ARGUMENT_BOOLEAN(fourth, bool unlink);
|
||||
(void)database;
|
||||
(void)attachedName;
|
||||
(void)destFile;
|
||||
(void)unlink;
|
||||
UseAddon;
|
||||
UseIsolate;
|
||||
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->Backup);
|
||||
addon->privileged_info = &info;
|
||||
v8::MaybeLocal<v8::Object> maybe_backup = c->NewInstance(OnlyContext, 0, NULL);
|
||||
addon->privileged_info = NULL;
|
||||
if (!maybe_backup.IsEmpty()) info.GetReturnValue().Set(maybe_backup.ToLocalChecked());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_function) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
REQUIRE_ARGUMENT_FUNCTION(first, v8::Local<v8::Function> fn);
|
||||
REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> nameString);
|
||||
REQUIRE_ARGUMENT_INT32(third, int argc);
|
||||
REQUIRE_ARGUMENT_INT32(fourth, int safe_ints);
|
||||
REQUIRE_ARGUMENT_BOOLEAN(fifth, bool deterministic);
|
||||
REQUIRE_DATABASE_OPEN(db);
|
||||
REQUIRE_DATABASE_NOT_BUSY(db);
|
||||
REQUIRE_DATABASE_NO_ITERATORS(db);
|
||||
|
||||
UseIsolate;
|
||||
v8::String::Utf8Value name(isolate, nameString);
|
||||
int mask = deterministic ? SQLITE_UTF8 | SQLITE_DETERMINISTIC : SQLITE_UTF8;
|
||||
safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(db->safe_ints);
|
||||
|
||||
if (sqlite3_create_function_v2(db->db_handle, *name, argc, mask, new CustomFunction(isolate, db, fn, *name, safe_ints), CustomFunction::xFunc, NULL, NULL, CustomFunction::xDestroy) != SQLITE_OK) {
|
||||
db->ThrowDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_aggregate) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
REQUIRE_ARGUMENT_ANY(first, v8::Local<v8::Value> start);
|
||||
REQUIRE_ARGUMENT_FUNCTION(second, v8::Local<v8::Function> step);
|
||||
REQUIRE_ARGUMENT_ANY(third, v8::Local<v8::Value> inverse);
|
||||
REQUIRE_ARGUMENT_ANY(fourth, v8::Local<v8::Value> result);
|
||||
REQUIRE_ARGUMENT_STRING(fifth, v8::Local<v8::String> nameString);
|
||||
REQUIRE_ARGUMENT_INT32(sixth, int argc);
|
||||
REQUIRE_ARGUMENT_INT32(seventh, int safe_ints);
|
||||
REQUIRE_ARGUMENT_BOOLEAN(eighth, bool deterministic);
|
||||
REQUIRE_DATABASE_OPEN(db);
|
||||
REQUIRE_DATABASE_NOT_BUSY(db);
|
||||
REQUIRE_DATABASE_NO_ITERATORS(db);
|
||||
|
||||
UseIsolate;
|
||||
v8::String::Utf8Value name(isolate, nameString);
|
||||
auto xInverse = inverse->IsFunction() ? CustomAggregate::xInverse : NULL;
|
||||
auto xValue = xInverse ? CustomAggregate::xValue : NULL;
|
||||
int mask = deterministic ? SQLITE_UTF8 | SQLITE_DETERMINISTIC : SQLITE_UTF8;
|
||||
safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(db->safe_ints);
|
||||
|
||||
if (sqlite3_create_window_function(db->db_handle, *name, argc, mask, new CustomAggregate(isolate, db, start, step, inverse, result, *name, safe_ints), CustomAggregate::xStep, CustomAggregate::xFinal, xValue, xInverse, CustomAggregate::xDestroy) != SQLITE_OK) {
|
||||
db->ThrowDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_loadExtension) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
v8::Local<v8::String> entryPoint;
|
||||
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> filename);
|
||||
if (info.Length() > 1) { REQUIRE_ARGUMENT_STRING(second, entryPoint); }
|
||||
REQUIRE_DATABASE_OPEN(db);
|
||||
REQUIRE_DATABASE_NOT_BUSY(db);
|
||||
REQUIRE_DATABASE_NO_ITERATORS(db);
|
||||
UseIsolate;
|
||||
char* error;
|
||||
int status = sqlite3_load_extension(
|
||||
db->db_handle,
|
||||
*v8::String::Utf8Value(isolate, filename),
|
||||
entryPoint.IsEmpty() ? NULL : *v8::String::Utf8Value(isolate, entryPoint),
|
||||
&error
|
||||
);
|
||||
if (status != SQLITE_OK) {
|
||||
ThrowSqliteError(db->addon, error, status);
|
||||
}
|
||||
sqlite3_free(error);
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_close) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
if (db->open) {
|
||||
REQUIRE_DATABASE_NOT_BUSY(db);
|
||||
REQUIRE_DATABASE_NO_ITERATORS(db);
|
||||
db->addon->dbs.erase(db);
|
||||
db->CloseHandles();
|
||||
}
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_defaultSafeIntegers) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
if (info.Length() == 0) db->safe_ints = true;
|
||||
else { REQUIRE_ARGUMENT_BOOLEAN(first, db->safe_ints); }
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_unsafeMode) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
if (info.Length() == 0) db->unsafe_mode = true;
|
||||
else { REQUIRE_ARGUMENT_BOOLEAN(first, db->unsafe_mode); }
|
||||
sqlite3_db_config(db->db_handle, SQLITE_DBCONFIG_DEFENSIVE, static_cast<int>(!db->unsafe_mode), NULL);
|
||||
}
|
||||
|
||||
NODE_GETTER(JS_open) {
|
||||
info.GetReturnValue().Set(Unwrap<Database>(info.This())->open);
|
||||
}
|
||||
|
||||
NODE_GETTER(JS_inTransaction) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
info.GetReturnValue().Set(db->open && !static_cast<bool>(sqlite3_get_autocommit(db->db_handle)));
|
||||
}
|
||||
|
||||
static const int MAX_BUFFER_SIZE = node::Buffer::kMaxLength > INT_MAX ? INT_MAX : static_cast<int>(node::Buffer::kMaxLength);
|
||||
static const int MAX_STRING_SIZE = v8::String::kMaxLength > INT_MAX ? INT_MAX : static_cast<int>(v8::String::kMaxLength);
|
||||
|
||||
sqlite3* const db_handle;
|
||||
bool open;
|
||||
bool busy;
|
||||
bool safe_ints;
|
||||
bool unsafe_mode;
|
||||
bool was_js_error;
|
||||
const bool has_logger;
|
||||
unsigned short iterators;
|
||||
Addon* const addon;
|
||||
const CopyablePersistent<v8::Value> logger;
|
||||
std::set<Statement*, CompareStatement> stmts;
|
||||
std::set<Backup*, CompareBackup> backups;
|
||||
};
|
||||
138
src/objects/statement-iterator.lzz
Normal file
138
src/objects/statement-iterator.lzz
Normal file
@ -0,0 +1,138 @@
|
||||
class StatementIterator : public node::ObjectWrap {
|
||||
public:
|
||||
|
||||
INIT(Init) {
|
||||
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "StatementIterator");
|
||||
SetPrototypeMethod(isolate, data, t, "next", JS_next);
|
||||
SetPrototypeMethod(isolate, data, t, "return", JS_return);
|
||||
SetPrototypeSymbolMethod(isolate, data, t, v8::Symbol::GetIterator(isolate), JS_symbolIterator);
|
||||
return t->GetFunction(OnlyContext).ToLocalChecked();
|
||||
}
|
||||
|
||||
// The ~Statement destructor currently covers any state this object creates.
|
||||
// Additionally, we actually DON'T want to set stmt->locked or db_state
|
||||
// ->iterators in this destructor, to ensure deterministic database access.
|
||||
~StatementIterator() {}
|
||||
|
||||
private:
|
||||
|
||||
explicit StatementIterator(Statement* _stmt, bool _bound) : node::ObjectWrap(),
|
||||
stmt(_stmt),
|
||||
handle(_stmt->handle),
|
||||
db_state(_stmt->db->GetState()),
|
||||
bound(_bound),
|
||||
safe_ints(_stmt->safe_ints),
|
||||
mode(_stmt->mode),
|
||||
alive(true),
|
||||
logged(!db_state->has_logger) {
|
||||
assert(stmt != NULL);
|
||||
assert(handle != NULL);
|
||||
assert(stmt->bound == bound);
|
||||
assert(stmt->alive == true);
|
||||
assert(stmt->locked == false);
|
||||
assert(db_state->iterators < USHRT_MAX);
|
||||
stmt->locked = true;
|
||||
db_state->iterators += 1;
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_new) {
|
||||
UseAddon;
|
||||
if (!addon->privileged_info) return ThrowTypeError("Disabled constructor");
|
||||
assert(info.IsConstructCall());
|
||||
|
||||
StatementIterator* iter;
|
||||
{
|
||||
NODE_ARGUMENTS info = *addon->privileged_info;
|
||||
STATEMENT_START_LOGIC(REQUIRE_STATEMENT_RETURNS_DATA, DOES_ADD_ITERATOR);
|
||||
iter = new StatementIterator(stmt, bound);
|
||||
}
|
||||
UseIsolate;
|
||||
UseContext;
|
||||
iter->Wrap(info.This());
|
||||
SetFrozen(isolate, ctx, info.This(), addon->cs.statement, addon->privileged_info->This());
|
||||
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_next) {
|
||||
StatementIterator* iter = Unwrap<StatementIterator>(info.This());
|
||||
REQUIRE_DATABASE_NOT_BUSY(iter->db_state);
|
||||
if (iter->alive) iter->Next(info);
|
||||
else info.GetReturnValue().Set(DoneRecord(OnlyIsolate, iter->db_state->addon));
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_return) {
|
||||
StatementIterator* iter = Unwrap<StatementIterator>(info.This());
|
||||
REQUIRE_DATABASE_NOT_BUSY(iter->db_state);
|
||||
if (iter->alive) iter->Return(info);
|
||||
else info.GetReturnValue().Set(DoneRecord(OnlyIsolate, iter->db_state->addon));
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_symbolIterator) {
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
void Next(NODE_ARGUMENTS info) {
|
||||
assert(alive == true);
|
||||
db_state->busy = true;
|
||||
if (!logged) {
|
||||
logged = true;
|
||||
if (stmt->db->Log(OnlyIsolate, handle)) {
|
||||
db_state->busy = false;
|
||||
Throw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
int status = sqlite3_step(handle);
|
||||
db_state->busy = false;
|
||||
if (status == SQLITE_ROW) {
|
||||
UseIsolate;
|
||||
UseContext;
|
||||
info.GetReturnValue().Set(
|
||||
NewRecord(isolate, ctx, Data::GetRowJS(isolate, ctx, handle, safe_ints, mode), db_state->addon, false)
|
||||
);
|
||||
} else {
|
||||
if (status == SQLITE_DONE) Return(info);
|
||||
else Throw();
|
||||
}
|
||||
}
|
||||
|
||||
void Return(NODE_ARGUMENTS info) {
|
||||
Cleanup();
|
||||
STATEMENT_RETURN_LOGIC(DoneRecord(OnlyIsolate, db_state->addon));
|
||||
}
|
||||
|
||||
void Throw() {
|
||||
Cleanup();
|
||||
Database* db = stmt->db;
|
||||
STATEMENT_THROW_LOGIC();
|
||||
}
|
||||
|
||||
void Cleanup() {
|
||||
assert(alive == true);
|
||||
alive = false;
|
||||
stmt->locked = false;
|
||||
db_state->iterators -= 1;
|
||||
sqlite3_reset(handle);
|
||||
}
|
||||
|
||||
static inline v8::Local<v8::Object> NewRecord(v8::Isolate* isolate, v8::Local<v8::Context> ctx, v8::Local<v8::Value> value, Addon* addon, bool done) {
|
||||
v8::Local<v8::Object> record = v8::Object::New(isolate);
|
||||
record->Set(ctx, CS::Get(isolate, addon->cs.value), value).FromJust();
|
||||
record->Set(ctx, CS::Get(isolate, addon->cs.done), v8::Boolean::New(isolate, done)).FromJust();
|
||||
return record;
|
||||
}
|
||||
|
||||
static inline v8::Local<v8::Object> DoneRecord(v8::Isolate* isolate, Addon* addon) {
|
||||
return NewRecord(isolate, OnlyContext, v8::Undefined(isolate), addon, true);
|
||||
}
|
||||
|
||||
Statement* const stmt;
|
||||
sqlite3_stmt* const handle;
|
||||
Database::State* const db_state;
|
||||
const bool bound;
|
||||
const bool safe_ints;
|
||||
const char mode;
|
||||
bool alive;
|
||||
bool logged;
|
||||
};
|
||||
317
src/objects/statement.lzz
Normal file
317
src/objects/statement.lzz
Normal file
@ -0,0 +1,317 @@
|
||||
class Statement : public node::ObjectWrap {
|
||||
friend class StatementIterator;
|
||||
public:
|
||||
|
||||
INIT(Init) {
|
||||
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "Statement");
|
||||
SetPrototypeMethod(isolate, data, t, "run", JS_run);
|
||||
SetPrototypeMethod(isolate, data, t, "get", JS_get);
|
||||
SetPrototypeMethod(isolate, data, t, "all", JS_all);
|
||||
SetPrototypeMethod(isolate, data, t, "iterate", JS_iterate);
|
||||
SetPrototypeMethod(isolate, data, t, "bind", JS_bind);
|
||||
SetPrototypeMethod(isolate, data, t, "pluck", JS_pluck);
|
||||
SetPrototypeMethod(isolate, data, t, "expand", JS_expand);
|
||||
SetPrototypeMethod(isolate, data, t, "raw", JS_raw);
|
||||
SetPrototypeMethod(isolate, data, t, "safeIntegers", JS_safeIntegers);
|
||||
SetPrototypeMethod(isolate, data, t, "columns", JS_columns);
|
||||
return t->GetFunction(OnlyContext).ToLocalChecked();
|
||||
}
|
||||
|
||||
// Used to support ordered containers.
|
||||
static inline bool Compare(Statement const * const a, Statement const * const b) {
|
||||
return a->extras->id < b->extras->id;
|
||||
}
|
||||
|
||||
// Returns the Statement's bind map (creates it upon first execution).
|
||||
BindMap* GetBindMap(v8::Isolate* isolate) {
|
||||
if (has_bind_map) return &extras->bind_map;
|
||||
BindMap* bind_map = &extras->bind_map;
|
||||
int param_count = sqlite3_bind_parameter_count(handle);
|
||||
for (int i=1; i<=param_count; ++i) {
|
||||
const char* name = sqlite3_bind_parameter_name(handle, i);
|
||||
if (name != NULL) bind_map->Add(isolate, name + 1, i);
|
||||
}
|
||||
has_bind_map = true;
|
||||
return bind_map;
|
||||
}
|
||||
|
||||
// Whenever this is used, db->RemoveStatement must be invoked beforehand.
|
||||
void CloseHandles() {
|
||||
if (alive) {
|
||||
alive = false;
|
||||
sqlite3_finalize(handle);
|
||||
}
|
||||
}
|
||||
|
||||
~Statement() {
|
||||
if (alive) db->RemoveStatement(this);
|
||||
CloseHandles();
|
||||
delete extras;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
// A class for holding values that are less often used.
|
||||
class Extras { friend class Statement;
|
||||
explicit Extras(sqlite3_uint64 _id) : bind_map(0), id(_id) {}
|
||||
BindMap bind_map;
|
||||
const sqlite3_uint64 id;
|
||||
};
|
||||
|
||||
explicit Statement(Database* _db, sqlite3_stmt* _handle, sqlite3_uint64 _id, bool _returns_data) : node::ObjectWrap(),
|
||||
db(_db),
|
||||
handle(_handle),
|
||||
extras(new Extras(_id)),
|
||||
alive(true),
|
||||
locked(false),
|
||||
bound(false),
|
||||
has_bind_map(false),
|
||||
safe_ints(_db->GetState()->safe_ints),
|
||||
mode(Data::FLAT),
|
||||
returns_data(_returns_data) {
|
||||
assert(db != NULL);
|
||||
assert(handle != NULL);
|
||||
assert(db->GetState()->open);
|
||||
assert(!db->GetState()->busy);
|
||||
db->AddStatement(this);
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_new) {
|
||||
UseAddon;
|
||||
if (!addon->privileged_info) {
|
||||
return ThrowTypeError("Statements can only be constructed by the db.prepare() method");
|
||||
}
|
||||
assert(info.IsConstructCall());
|
||||
Database* db = Unwrap<Database>(addon->privileged_info->This());
|
||||
REQUIRE_DATABASE_OPEN(db->GetState());
|
||||
REQUIRE_DATABASE_NOT_BUSY(db->GetState());
|
||||
|
||||
v8::Local<v8::String> source = v8::Local<v8::String>::Cast((*addon->privileged_info)[0]);
|
||||
v8::Local<v8::Object> database = v8::Local<v8::Object>::Cast((*addon->privileged_info)[1]);
|
||||
bool pragmaMode = v8::Local<v8::Boolean>::Cast((*addon->privileged_info)[2])->Value();
|
||||
int flags = SQLITE_PREPARE_PERSISTENT;
|
||||
|
||||
if (pragmaMode) {
|
||||
REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db->GetState());
|
||||
flags = 0;
|
||||
}
|
||||
|
||||
UseIsolate;
|
||||
v8::String::Utf8Value utf8(isolate, source);
|
||||
sqlite3_stmt* handle;
|
||||
const char* tail;
|
||||
|
||||
if (sqlite3_prepare_v3(db->GetHandle(), *utf8, utf8.length() + 1, flags, &handle, &tail) != SQLITE_OK) {
|
||||
return db->ThrowDatabaseError();
|
||||
}
|
||||
if (handle == NULL) {
|
||||
return ThrowRangeError("The supplied SQL string contains no statements");
|
||||
}
|
||||
for (char c; (c = *tail); ++tail) {
|
||||
if (IS_SKIPPED(c)) continue;
|
||||
if (c == '/' && tail[1] == '*') {
|
||||
tail += 2;
|
||||
for (char c; (c = *tail); ++tail) {
|
||||
if (c == '*' && tail[1] == '/') {
|
||||
tail += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (c == '-' && tail[1] == '-') {
|
||||
tail += 2;
|
||||
for (char c; (c = *tail); ++tail) {
|
||||
if (c == '\n') break;
|
||||
}
|
||||
} else {
|
||||
sqlite3_finalize(handle);
|
||||
return ThrowRangeError("The supplied SQL string contains more than one statement");
|
||||
}
|
||||
}
|
||||
|
||||
UseContext;
|
||||
bool returns_data = (sqlite3_stmt_readonly(handle) && sqlite3_column_count(handle) >= 1) || pragmaMode;
|
||||
Statement* stmt = new Statement(db, handle, addon->NextId(), returns_data);
|
||||
stmt->Wrap(info.This());
|
||||
SetFrozen(isolate, ctx, info.This(), addon->cs.reader, v8::Boolean::New(isolate, returns_data));
|
||||
SetFrozen(isolate, ctx, info.This(), addon->cs.source, source);
|
||||
SetFrozen(isolate, ctx, info.This(), addon->cs.database, database);
|
||||
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_run) {
|
||||
STATEMENT_START(REQUIRE_STATEMENT_DOESNT_RETURN_DATA, DOES_MUTATE);
|
||||
sqlite3* db_handle = db->GetHandle();
|
||||
int total_changes_before = sqlite3_total_changes(db_handle);
|
||||
|
||||
sqlite3_step(handle);
|
||||
if (sqlite3_reset(handle) == SQLITE_OK) {
|
||||
int changes = sqlite3_total_changes(db_handle) == total_changes_before ? 0 : sqlite3_changes(db_handle);
|
||||
sqlite3_int64 id = sqlite3_last_insert_rowid(db_handle);
|
||||
Addon* addon = db->GetAddon();
|
||||
UseContext;
|
||||
v8::Local<v8::Object> result = v8::Object::New(isolate);
|
||||
result->Set(ctx, CS::Get(isolate, addon->cs.changes), v8::Int32::New(isolate, changes)).FromJust();
|
||||
result->Set(ctx, CS::Get(isolate, addon->cs.lastInsertRowid),
|
||||
stmt->safe_ints
|
||||
? v8::Local<v8::Value>::Cast(v8::BigInt::New(isolate, id))
|
||||
: v8::Local<v8::Value>::Cast(v8::Number::New(isolate, (double)id))
|
||||
).FromJust();
|
||||
STATEMENT_RETURN(result);
|
||||
}
|
||||
STATEMENT_THROW();
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_get) {
|
||||
STATEMENT_START(REQUIRE_STATEMENT_RETURNS_DATA, DOES_NOT_MUTATE);
|
||||
int status = sqlite3_step(handle);
|
||||
if (status == SQLITE_ROW) {
|
||||
v8::Local<v8::Value> result = Data::GetRowJS(isolate, OnlyContext, handle, stmt->safe_ints, stmt->mode);
|
||||
sqlite3_reset(handle);
|
||||
STATEMENT_RETURN(result);
|
||||
} else if (status == SQLITE_DONE) {
|
||||
sqlite3_reset(handle);
|
||||
STATEMENT_RETURN(v8::Undefined(isolate));
|
||||
}
|
||||
sqlite3_reset(handle);
|
||||
STATEMENT_THROW();
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_all) {
|
||||
STATEMENT_START(REQUIRE_STATEMENT_RETURNS_DATA, DOES_NOT_MUTATE);
|
||||
UseContext;
|
||||
v8::Local<v8::Array> result = v8::Array::New(isolate, 0);
|
||||
uint32_t row_count = 0;
|
||||
const bool safe_ints = stmt->safe_ints;
|
||||
const char mode = stmt->mode;
|
||||
bool js_error = false;
|
||||
|
||||
while (sqlite3_step(handle) == SQLITE_ROW) {
|
||||
if (row_count == 0xffffffff) { ThrowRangeError("Array overflow (too many rows returned)"); js_error = true; break; }
|
||||
result->Set(ctx, row_count++, Data::GetRowJS(isolate, ctx, handle, safe_ints, mode)).FromJust();
|
||||
}
|
||||
|
||||
if (sqlite3_reset(handle) == SQLITE_OK && !js_error) {
|
||||
STATEMENT_RETURN(result);
|
||||
}
|
||||
if (js_error) db->GetState()->was_js_error = true;
|
||||
STATEMENT_THROW();
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_iterate) {
|
||||
UseAddon;
|
||||
UseIsolate;
|
||||
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->StatementIterator);
|
||||
addon->privileged_info = &info;
|
||||
v8::MaybeLocal<v8::Object> maybe_iter = c->NewInstance(OnlyContext, 0, NULL);
|
||||
addon->privileged_info = NULL;
|
||||
if (!maybe_iter.IsEmpty()) info.GetReturnValue().Set(maybe_iter.ToLocalChecked());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_bind) {
|
||||
Statement* stmt = Unwrap<Statement>(info.This());
|
||||
if (stmt->bound) return ThrowTypeError("The bind() method can only be invoked once per statement object");
|
||||
REQUIRE_DATABASE_OPEN(stmt->db->GetState());
|
||||
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
|
||||
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
|
||||
STATEMENT_BIND(stmt->handle);
|
||||
stmt->bound = true;
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_pluck) {
|
||||
Statement* stmt = Unwrap<Statement>(info.This());
|
||||
if (!stmt->returns_data) return ThrowTypeError("The pluck() method is only for statements that return data");
|
||||
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
|
||||
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
|
||||
bool use = true;
|
||||
if (info.Length() != 0) { REQUIRE_ARGUMENT_BOOLEAN(first, use); }
|
||||
stmt->mode = use ? Data::PLUCK : stmt->mode == Data::PLUCK ? Data::FLAT : stmt->mode;
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_expand) {
|
||||
Statement* stmt = Unwrap<Statement>(info.This());
|
||||
if (!stmt->returns_data) return ThrowTypeError("The expand() method is only for statements that return data");
|
||||
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
|
||||
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
|
||||
bool use = true;
|
||||
if (info.Length() != 0) { REQUIRE_ARGUMENT_BOOLEAN(first, use); }
|
||||
stmt->mode = use ? Data::EXPAND : stmt->mode == Data::EXPAND ? Data::FLAT : stmt->mode;
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_raw) {
|
||||
Statement* stmt = Unwrap<Statement>(info.This());
|
||||
if (!stmt->returns_data) return ThrowTypeError("The raw() method is only for statements that return data");
|
||||
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
|
||||
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
|
||||
bool use = true;
|
||||
if (info.Length() != 0) { REQUIRE_ARGUMENT_BOOLEAN(first, use); }
|
||||
stmt->mode = use ? Data::RAW : stmt->mode == Data::RAW ? Data::FLAT : stmt->mode;
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_safeIntegers) {
|
||||
Statement* stmt = Unwrap<Statement>(info.This());
|
||||
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
|
||||
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
|
||||
if (info.Length() == 0) stmt->safe_ints = true;
|
||||
else { REQUIRE_ARGUMENT_BOOLEAN(first, stmt->safe_ints); }
|
||||
info.GetReturnValue().Set(info.This());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_columns) {
|
||||
Statement* stmt = Unwrap<Statement>(info.This());
|
||||
if (!stmt->returns_data) return ThrowTypeError("The columns() method is only for statements that return data");
|
||||
REQUIRE_DATABASE_OPEN(stmt->db->GetState());
|
||||
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
|
||||
Addon* addon = stmt->db->GetAddon();
|
||||
UseIsolate;
|
||||
UseContext;
|
||||
|
||||
int column_count = sqlite3_column_count(stmt->handle);
|
||||
v8::Local<v8::Array> columns = v8::Array::New(isolate);
|
||||
|
||||
v8::Local<v8::String> name = CS::Get(isolate, addon->cs.name);
|
||||
v8::Local<v8::String> columnName = CS::Get(isolate, addon->cs.column);
|
||||
v8::Local<v8::String> tableName = CS::Get(isolate, addon->cs.table);
|
||||
v8::Local<v8::String> databaseName = CS::Get(isolate, addon->cs.database);
|
||||
v8::Local<v8::String> typeName = CS::Get(isolate, addon->cs.type);
|
||||
|
||||
for (int i=0; i<column_count; ++i) {
|
||||
v8::Local<v8::Object> column = v8::Object::New(isolate);
|
||||
|
||||
column->Set(ctx, name,
|
||||
InternalizedFromUtf8OrNull(isolate, sqlite3_column_name(stmt->handle, i), -1)
|
||||
).FromJust();
|
||||
column->Set(ctx, columnName,
|
||||
InternalizedFromUtf8OrNull(isolate, sqlite3_column_origin_name(stmt->handle, i), -1)
|
||||
).FromJust();
|
||||
column->Set(ctx, tableName,
|
||||
InternalizedFromUtf8OrNull(isolate, sqlite3_column_table_name(stmt->handle, i), -1)
|
||||
).FromJust();
|
||||
column->Set(ctx, databaseName,
|
||||
InternalizedFromUtf8OrNull(isolate, sqlite3_column_database_name(stmt->handle, i), -1)
|
||||
).FromJust();
|
||||
column->Set(ctx, typeName,
|
||||
InternalizedFromUtf8OrNull(isolate, sqlite3_column_decltype(stmt->handle, i), -1)
|
||||
).FromJust();
|
||||
|
||||
columns->Set(ctx, i, column).FromJust();
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(columns);
|
||||
}
|
||||
|
||||
Database* const db;
|
||||
sqlite3_stmt* const handle;
|
||||
Extras* const extras;
|
||||
bool alive;
|
||||
bool locked;
|
||||
bool bound;
|
||||
bool has_bind_map;
|
||||
bool safe_ints;
|
||||
char mode;
|
||||
const bool returns_data;
|
||||
};
|
||||
67
src/util/bind-map.lzz
Normal file
67
src/util/bind-map.lzz
Normal file
@ -0,0 +1,67 @@
|
||||
class BindMap {
|
||||
public:
|
||||
|
||||
// This class represents a mapping between a parameter name and its
|
||||
// associated parameter index in a prepared statement.
|
||||
class Pair {
|
||||
friend class BindMap;
|
||||
public:
|
||||
|
||||
inline int GetIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
inline v8::Local<v8::String> GetName(v8::Isolate* isolate) {
|
||||
return v8::Local<v8::String>::New(isolate, name);
|
||||
}
|
||||
|
||||
private:
|
||||
explicit Pair(v8::Isolate* isolate, const char* _name, int _index)
|
||||
: name(isolate, InternalizedFromUtf8(isolate, _name, -1)), index(_index) {}
|
||||
|
||||
explicit Pair(v8::Isolate* isolate, Pair* pair)
|
||||
: name(isolate, pair->name), index(pair->index) {}
|
||||
|
||||
const CopyablePersistent<v8::String> name;
|
||||
const int index;
|
||||
};
|
||||
|
||||
explicit BindMap(char _) {
|
||||
assert(_ == 0);
|
||||
pairs = NULL;
|
||||
capacity = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
~BindMap() {
|
||||
while (length) pairs[--length].~Pair();
|
||||
FREE_ARRAY<Pair>(pairs);
|
||||
}
|
||||
|
||||
inline Pair* GetPairs() { return pairs; }
|
||||
inline int GetSize() { return length; }
|
||||
|
||||
// Adds a pair to the bind map, expanding the capacity if necessary.
|
||||
void Add(v8::Isolate* isolate, const char* name, int index) {
|
||||
assert(name != NULL);
|
||||
if (capacity == length) Grow(isolate);
|
||||
new (pairs + length++) Pair(isolate, name, index);
|
||||
}
|
||||
|
||||
private:
|
||||
void Grow(v8::Isolate* isolate) {
|
||||
assert(capacity == length);
|
||||
capacity = (capacity << 1) | 2;
|
||||
Pair* new_pairs = ALLOC_ARRAY<Pair>(capacity);
|
||||
for (int i=0; i<length; ++i) {
|
||||
new (new_pairs + i) Pair(isolate, pairs + i);
|
||||
pairs[i].~Pair();
|
||||
}
|
||||
FREE_ARRAY<Pair>(pairs);
|
||||
pairs = new_pairs;
|
||||
}
|
||||
|
||||
Pair* pairs;
|
||||
int capacity;
|
||||
int length;
|
||||
};
|
||||
189
src/util/binder.lzz
Normal file
189
src/util/binder.lzz
Normal file
@ -0,0 +1,189 @@
|
||||
class Binder {
|
||||
public:
|
||||
|
||||
explicit Binder(sqlite3_stmt* _handle) {
|
||||
handle = _handle;
|
||||
param_count = sqlite3_bind_parameter_count(_handle);
|
||||
anon_index = 0;
|
||||
success = true;
|
||||
}
|
||||
|
||||
bool Bind(NODE_ARGUMENTS info, int argc, Statement* stmt) {
|
||||
assert(anon_index == 0);
|
||||
Result result = BindArgs(info, argc, stmt);
|
||||
if (success && result.count != param_count) {
|
||||
if (result.count < param_count) {
|
||||
if (!result.bound_object && stmt->GetBindMap(OnlyIsolate)->GetSize()) {
|
||||
Fail(ThrowTypeError, "Missing named parameters");
|
||||
} else {
|
||||
Fail(ThrowRangeError, "Too few parameter values were provided");
|
||||
}
|
||||
} else {
|
||||
Fail(ThrowRangeError, "Too many parameter values were provided");
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Result {
|
||||
int count;
|
||||
bool bound_object;
|
||||
};
|
||||
|
||||
static bool IsPlainObject(v8::Isolate* isolate, v8::Local<v8::Object> obj) {
|
||||
v8::Local<v8::Value> proto = obj->GetPrototype();
|
||||
v8::Local<v8::Context> ctx = obj->CreationContext();
|
||||
ctx->Enter();
|
||||
v8::Local<v8::Value> baseProto = v8::Object::New(isolate)->GetPrototype();
|
||||
ctx->Exit();
|
||||
return proto->StrictEquals(baseProto) || proto->StrictEquals(v8::Null(isolate));
|
||||
}
|
||||
|
||||
void Fail(void (*Throw)(const char* _), const char* message) {
|
||||
assert(success == true);
|
||||
assert((Throw == NULL) == (message == NULL));
|
||||
assert(Throw == ThrowError || Throw == ThrowTypeError || Throw == ThrowRangeError || Throw == NULL);
|
||||
if (Throw) Throw(message);
|
||||
success = false;
|
||||
}
|
||||
|
||||
int NextAnonIndex() {
|
||||
while (sqlite3_bind_parameter_name(handle, ++anon_index) != NULL) {}
|
||||
return anon_index;
|
||||
}
|
||||
|
||||
// Binds the value at the given index or throws an appropriate error.
|
||||
void BindValue(v8::Isolate* isolate, v8::Local<v8::Value> value, int index) {
|
||||
int status = Data::BindValueFromJS(isolate, handle, index, value);
|
||||
if (status != SQLITE_OK) {
|
||||
switch (status) {
|
||||
case -1:
|
||||
return Fail(ThrowTypeError, "SQLite3 can only bind numbers, strings, bigints, buffers, and null");
|
||||
case SQLITE_TOOBIG:
|
||||
return Fail(ThrowRangeError, "The bound string, buffer, or bigint is too big");
|
||||
case SQLITE_RANGE:
|
||||
return Fail(ThrowRangeError, "Too many parameter values were provided");
|
||||
case SQLITE_NOMEM:
|
||||
return Fail(ThrowError, "Out of memory");
|
||||
default:
|
||||
return Fail(ThrowError, "An unexpected error occured while trying to bind parameters");
|
||||
}
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Binds each value in the array or throws an appropriate error.
|
||||
// The number of successfully bound parameters is returned.
|
||||
int BindArray(v8::Isolate* isolate, v8::Local<v8::Array> arr) {
|
||||
UseContext;
|
||||
uint32_t length = arr->Length();
|
||||
if (length > INT_MAX) {
|
||||
Fail(ThrowRangeError, "Too many parameter values were provided");
|
||||
return 0;
|
||||
}
|
||||
int len = static_cast<int>(length);
|
||||
for (int i=0; i<len; ++i) {
|
||||
v8::MaybeLocal<v8::Value> maybeValue = arr->Get(ctx, i);
|
||||
if (maybeValue.IsEmpty()) {
|
||||
Fail(NULL, NULL);
|
||||
return i;
|
||||
}
|
||||
BindValue(isolate, maybeValue.ToLocalChecked(), NextAnonIndex());
|
||||
if (!success) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
// Binds all named parameters using the values found in the given object.
|
||||
// The number of successfully bound parameters is returned.
|
||||
// If a named parameter is missing from the object, an error is thrown.
|
||||
// This should only be invoked once per instance.
|
||||
int BindObject(v8::Isolate* isolate, v8::Local<v8::Object> obj, Statement* stmt) {
|
||||
UseContext;
|
||||
BindMap* bind_map = stmt->GetBindMap(isolate);
|
||||
BindMap::Pair* pairs = bind_map->GetPairs();
|
||||
int len = bind_map->GetSize();
|
||||
|
||||
for (int i=0; i<len; ++i) {
|
||||
v8::Local<v8::String> key = pairs[i].GetName(isolate);
|
||||
|
||||
// Check if the named parameter was provided.
|
||||
v8::Maybe<bool> has_property = obj->HasOwnProperty(ctx, key);
|
||||
if (has_property.IsNothing()) {
|
||||
Fail(NULL, NULL);
|
||||
return i;
|
||||
}
|
||||
if (!has_property.FromJust()) {
|
||||
v8::String::Utf8Value param_name(isolate, key);
|
||||
Fail(ThrowRangeError, CONCAT("Missing named parameter \"", *param_name, "\"").c_str());
|
||||
return i;
|
||||
}
|
||||
|
||||
// Get the current property value.
|
||||
v8::MaybeLocal<v8::Value> maybeValue = obj->Get(ctx, key);
|
||||
if (maybeValue.IsEmpty()) {
|
||||
Fail(NULL, NULL);
|
||||
return i;
|
||||
}
|
||||
|
||||
BindValue(isolate, maybeValue.ToLocalChecked(), pairs[i].GetIndex());
|
||||
if (!success) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
// Binds all parameters using the values found in the arguments object.
|
||||
// Anonymous parameter values can be directly in the arguments object or in an Array.
|
||||
// Named parameter values can be provided in a plain Object argument.
|
||||
// Only one plain Object argument may be provided.
|
||||
// If an error occurs, an appropriate error is thrown.
|
||||
// The return value is a struct indicating how many parameters were successfully bound
|
||||
// and whether or not it tried to bind an object.
|
||||
Result BindArgs(NODE_ARGUMENTS info, int argc, Statement* stmt) {
|
||||
UseIsolate;
|
||||
int count = 0;
|
||||
bool bound_object = false;
|
||||
|
||||
for (int i=0; i<argc; ++i) {
|
||||
v8::Local<v8::Value> arg = info[i];
|
||||
|
||||
if (arg->IsArray()) {
|
||||
count += BindArray(isolate, v8::Local<v8::Array>::Cast(arg));
|
||||
if (!success) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg->IsObject() && !node::Buffer::HasInstance(arg)) {
|
||||
v8::Local<v8::Object> obj = v8::Local<v8::Object>::Cast(arg);
|
||||
if (IsPlainObject(isolate, obj)) {
|
||||
if (bound_object) {
|
||||
Fail(ThrowTypeError, "You cannot specify named parameters in two different objects");
|
||||
break;
|
||||
}
|
||||
bound_object = true;
|
||||
|
||||
count += BindObject(isolate, obj, stmt);
|
||||
if (!success) break;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
BindValue(isolate, arg, NextAnonIndex());
|
||||
if (!success) break;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return { count, bound_object };
|
||||
}
|
||||
|
||||
sqlite3_stmt* handle;
|
||||
int param_count;
|
||||
int anon_index; // This value should only be used by NextAnonIndex()
|
||||
bool success; // This value should only be set by Fail()
|
||||
};
|
||||
150
src/util/constants.lzz
Normal file
150
src/util/constants.lzz
Normal file
@ -0,0 +1,150 @@
|
||||
class CS {
|
||||
public:
|
||||
|
||||
static inline v8::Local<v8::String> Get(v8::Isolate* isolate, CopyablePersistent<v8::String>& constant) {
|
||||
return v8::Local<v8::String>::New(isolate, constant);
|
||||
}
|
||||
v8::Local<v8::String> Code(v8::Isolate* isolate, int code) {
|
||||
auto element = codes.find(code);
|
||||
if (element != codes.end()) return v8::Local<v8::String>::New(isolate, element->second);
|
||||
return StringFromUtf8(isolate, CONCAT("UNKNOWN_SQLITE_ERROR_", std::to_string(code).c_str(), "").c_str(), -1);
|
||||
}
|
||||
|
||||
explicit CS(v8::Isolate* isolate) {
|
||||
SetString(isolate, database, "database");
|
||||
SetString(isolate, reader, "reader");
|
||||
SetString(isolate, source, "source");
|
||||
SetString(isolate, memory, "memory");
|
||||
SetString(isolate, readonly, "readonly");
|
||||
SetString(isolate, name, "name");
|
||||
SetString(isolate, next, "next");
|
||||
SetString(isolate, length, "length");
|
||||
SetString(isolate, done, "done");
|
||||
SetString(isolate, value, "value");
|
||||
SetString(isolate, changes, "changes");
|
||||
SetString(isolate, lastInsertRowid, "lastInsertRowid");
|
||||
SetString(isolate, statement, "statement");
|
||||
SetString(isolate, column, "column");
|
||||
SetString(isolate, table, "table");
|
||||
SetString(isolate, type, "type");
|
||||
SetString(isolate, totalPages, "totalPages");
|
||||
SetString(isolate, remainingPages, "remainingPages");
|
||||
|
||||
SetCode(isolate, SQLITE_OK, "SQLITE_OK");
|
||||
SetCode(isolate, SQLITE_ERROR, "SQLITE_ERROR");
|
||||
SetCode(isolate, SQLITE_INTERNAL, "SQLITE_INTERNAL");
|
||||
SetCode(isolate, SQLITE_PERM, "SQLITE_PERM");
|
||||
SetCode(isolate, SQLITE_ABORT, "SQLITE_ABORT");
|
||||
SetCode(isolate, SQLITE_BUSY, "SQLITE_BUSY");
|
||||
SetCode(isolate, SQLITE_LOCKED, "SQLITE_LOCKED");
|
||||
SetCode(isolate, SQLITE_NOMEM, "SQLITE_NOMEM");
|
||||
SetCode(isolate, SQLITE_READONLY, "SQLITE_READONLY");
|
||||
SetCode(isolate, SQLITE_INTERRUPT, "SQLITE_INTERRUPT");
|
||||
SetCode(isolate, SQLITE_IOERR, "SQLITE_IOERR");
|
||||
SetCode(isolate, SQLITE_CORRUPT, "SQLITE_CORRUPT");
|
||||
SetCode(isolate, SQLITE_NOTFOUND, "SQLITE_NOTFOUND");
|
||||
SetCode(isolate, SQLITE_FULL, "SQLITE_FULL");
|
||||
SetCode(isolate, SQLITE_CANTOPEN, "SQLITE_CANTOPEN");
|
||||
SetCode(isolate, SQLITE_PROTOCOL, "SQLITE_PROTOCOL");
|
||||
SetCode(isolate, SQLITE_EMPTY, "SQLITE_EMPTY");
|
||||
SetCode(isolate, SQLITE_SCHEMA, "SQLITE_SCHEMA");
|
||||
SetCode(isolate, SQLITE_TOOBIG, "SQLITE_TOOBIG");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT, "SQLITE_CONSTRAINT");
|
||||
SetCode(isolate, SQLITE_MISMATCH, "SQLITE_MISMATCH");
|
||||
SetCode(isolate, SQLITE_MISUSE, "SQLITE_MISUSE");
|
||||
SetCode(isolate, SQLITE_NOLFS, "SQLITE_NOLFS");
|
||||
SetCode(isolate, SQLITE_AUTH, "SQLITE_AUTH");
|
||||
SetCode(isolate, SQLITE_FORMAT, "SQLITE_FORMAT");
|
||||
SetCode(isolate, SQLITE_RANGE, "SQLITE_RANGE");
|
||||
SetCode(isolate, SQLITE_NOTADB, "SQLITE_NOTADB");
|
||||
SetCode(isolate, SQLITE_NOTICE, "SQLITE_NOTICE");
|
||||
SetCode(isolate, SQLITE_WARNING, "SQLITE_WARNING");
|
||||
SetCode(isolate, SQLITE_ROW, "SQLITE_ROW");
|
||||
SetCode(isolate, SQLITE_DONE, "SQLITE_DONE");
|
||||
SetCode(isolate, SQLITE_IOERR_READ, "SQLITE_IOERR_READ");
|
||||
SetCode(isolate, SQLITE_IOERR_SHORT_READ, "SQLITE_IOERR_SHORT_READ");
|
||||
SetCode(isolate, SQLITE_IOERR_WRITE, "SQLITE_IOERR_WRITE");
|
||||
SetCode(isolate, SQLITE_IOERR_FSYNC, "SQLITE_IOERR_FSYNC");
|
||||
SetCode(isolate, SQLITE_IOERR_DIR_FSYNC, "SQLITE_IOERR_DIR_FSYNC");
|
||||
SetCode(isolate, SQLITE_IOERR_TRUNCATE, "SQLITE_IOERR_TRUNCATE");
|
||||
SetCode(isolate, SQLITE_IOERR_FSTAT, "SQLITE_IOERR_FSTAT");
|
||||
SetCode(isolate, SQLITE_IOERR_UNLOCK, "SQLITE_IOERR_UNLOCK");
|
||||
SetCode(isolate, SQLITE_IOERR_RDLOCK, "SQLITE_IOERR_RDLOCK");
|
||||
SetCode(isolate, SQLITE_IOERR_DELETE, "SQLITE_IOERR_DELETE");
|
||||
SetCode(isolate, SQLITE_IOERR_BLOCKED, "SQLITE_IOERR_BLOCKED");
|
||||
SetCode(isolate, SQLITE_IOERR_NOMEM, "SQLITE_IOERR_NOMEM");
|
||||
SetCode(isolate, SQLITE_IOERR_ACCESS, "SQLITE_IOERR_ACCESS");
|
||||
SetCode(isolate, SQLITE_IOERR_CHECKRESERVEDLOCK, "SQLITE_IOERR_CHECKRESERVEDLOCK");
|
||||
SetCode(isolate, SQLITE_IOERR_LOCK, "SQLITE_IOERR_LOCK");
|
||||
SetCode(isolate, SQLITE_IOERR_CLOSE, "SQLITE_IOERR_CLOSE");
|
||||
SetCode(isolate, SQLITE_IOERR_DIR_CLOSE, "SQLITE_IOERR_DIR_CLOSE");
|
||||
SetCode(isolate, SQLITE_IOERR_SHMOPEN, "SQLITE_IOERR_SHMOPEN");
|
||||
SetCode(isolate, SQLITE_IOERR_SHMSIZE, "SQLITE_IOERR_SHMSIZE");
|
||||
SetCode(isolate, SQLITE_IOERR_SHMLOCK, "SQLITE_IOERR_SHMLOCK");
|
||||
SetCode(isolate, SQLITE_IOERR_SHMMAP, "SQLITE_IOERR_SHMMAP");
|
||||
SetCode(isolate, SQLITE_IOERR_SEEK, "SQLITE_IOERR_SEEK");
|
||||
SetCode(isolate, SQLITE_IOERR_DELETE_NOENT, "SQLITE_IOERR_DELETE_NOENT");
|
||||
SetCode(isolate, SQLITE_IOERR_MMAP, "SQLITE_IOERR_MMAP");
|
||||
SetCode(isolate, SQLITE_IOERR_GETTEMPPATH, "SQLITE_IOERR_GETTEMPPATH");
|
||||
SetCode(isolate, SQLITE_IOERR_CONVPATH, "SQLITE_IOERR_CONVPATH");
|
||||
SetCode(isolate, SQLITE_IOERR_VNODE, "SQLITE_IOERR_VNODE");
|
||||
SetCode(isolate, SQLITE_IOERR_AUTH, "SQLITE_IOERR_AUTH");
|
||||
SetCode(isolate, SQLITE_LOCKED_SHAREDCACHE, "SQLITE_LOCKED_SHAREDCACHE");
|
||||
SetCode(isolate, SQLITE_BUSY_RECOVERY, "SQLITE_BUSY_RECOVERY");
|
||||
SetCode(isolate, SQLITE_BUSY_SNAPSHOT, "SQLITE_BUSY_SNAPSHOT");
|
||||
SetCode(isolate, SQLITE_CANTOPEN_NOTEMPDIR, "SQLITE_CANTOPEN_NOTEMPDIR");
|
||||
SetCode(isolate, SQLITE_CANTOPEN_ISDIR, "SQLITE_CANTOPEN_ISDIR");
|
||||
SetCode(isolate, SQLITE_CANTOPEN_FULLPATH, "SQLITE_CANTOPEN_FULLPATH");
|
||||
SetCode(isolate, SQLITE_CANTOPEN_CONVPATH, "SQLITE_CANTOPEN_CONVPATH");
|
||||
SetCode(isolate, SQLITE_CORRUPT_VTAB, "SQLITE_CORRUPT_VTAB");
|
||||
SetCode(isolate, SQLITE_READONLY_RECOVERY, "SQLITE_READONLY_RECOVERY");
|
||||
SetCode(isolate, SQLITE_READONLY_CANTLOCK, "SQLITE_READONLY_CANTLOCK");
|
||||
SetCode(isolate, SQLITE_READONLY_ROLLBACK, "SQLITE_READONLY_ROLLBACK");
|
||||
SetCode(isolate, SQLITE_READONLY_DBMOVED, "SQLITE_READONLY_DBMOVED");
|
||||
SetCode(isolate, SQLITE_ABORT_ROLLBACK, "SQLITE_ABORT_ROLLBACK");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_CHECK, "SQLITE_CONSTRAINT_CHECK");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_COMMITHOOK, "SQLITE_CONSTRAINT_COMMITHOOK");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_FOREIGNKEY, "SQLITE_CONSTRAINT_FOREIGNKEY");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_FUNCTION, "SQLITE_CONSTRAINT_FUNCTION");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_NOTNULL, "SQLITE_CONSTRAINT_NOTNULL");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_PRIMARYKEY, "SQLITE_CONSTRAINT_PRIMARYKEY");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_TRIGGER, "SQLITE_CONSTRAINT_TRIGGER");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_UNIQUE, "SQLITE_CONSTRAINT_UNIQUE");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_VTAB, "SQLITE_CONSTRAINT_VTAB");
|
||||
SetCode(isolate, SQLITE_CONSTRAINT_ROWID, "SQLITE_CONSTRAINT_ROWID");
|
||||
SetCode(isolate, SQLITE_NOTICE_RECOVER_WAL, "SQLITE_NOTICE_RECOVER_WAL");
|
||||
SetCode(isolate, SQLITE_NOTICE_RECOVER_ROLLBACK, "SQLITE_NOTICE_RECOVER_ROLLBACK");
|
||||
SetCode(isolate, SQLITE_WARNING_AUTOINDEX, "SQLITE_WARNING_AUTOINDEX");
|
||||
SetCode(isolate, SQLITE_AUTH_USER, "SQLITE_AUTH_USER");
|
||||
SetCode(isolate, SQLITE_OK_LOAD_PERMANENTLY, "SQLITE_OK_LOAD_PERMANENTLY");
|
||||
}
|
||||
|
||||
CopyablePersistent<v8::String> database;
|
||||
CopyablePersistent<v8::String> reader;
|
||||
CopyablePersistent<v8::String> source;
|
||||
CopyablePersistent<v8::String> memory;
|
||||
CopyablePersistent<v8::String> readonly;
|
||||
CopyablePersistent<v8::String> name;
|
||||
CopyablePersistent<v8::String> next;
|
||||
CopyablePersistent<v8::String> length;
|
||||
CopyablePersistent<v8::String> done;
|
||||
CopyablePersistent<v8::String> value;
|
||||
CopyablePersistent<v8::String> changes;
|
||||
CopyablePersistent<v8::String> lastInsertRowid;
|
||||
CopyablePersistent<v8::String> statement;
|
||||
CopyablePersistent<v8::String> column;
|
||||
CopyablePersistent<v8::String> table;
|
||||
CopyablePersistent<v8::String> type;
|
||||
CopyablePersistent<v8::String> totalPages;
|
||||
CopyablePersistent<v8::String> remainingPages;
|
||||
|
||||
private:
|
||||
static void SetString(v8::Isolate* isolate, CopyablePersistent<v8::String>& constant, const char* str) {
|
||||
constant.Reset(isolate, InternalizedFromLatin1(isolate, str));
|
||||
}
|
||||
void SetCode(v8::Isolate* isolate, int code, const char* str) {
|
||||
codes.emplace(std::piecewise_construct, std::forward_as_tuple(code), std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str)));
|
||||
}
|
||||
|
||||
std::unordered_map<int, CopyablePersistent<v8::String> > codes;
|
||||
};
|
||||
106
src/util/custom-aggregate.lzz
Normal file
106
src/util/custom-aggregate.lzz
Normal file
@ -0,0 +1,106 @@
|
||||
class CustomAggregate : public CustomFunction {
|
||||
public:
|
||||
|
||||
explicit CustomAggregate(v8::Isolate* _isolate, Database* _db, v8::Local<v8::Value> _start, v8::Local<v8::Function> _step, v8::Local<v8::Value> _inverse, v8::Local<v8::Value> _result, const char* _name, bool _safe_ints)
|
||||
: CustomFunction(_isolate, _db, _step, _name, _safe_ints), invoke_result(_result->IsFunction()), invoke_start(_start->IsFunction()), inverse(_isolate, _inverse->IsFunction() ? v8::Local<v8::Function>::Cast(_inverse) : v8::Local<v8::Function>()), result(_isolate, _result->IsFunction() ? v8::Local<v8::Function>::Cast(_result) : v8::Local<v8::Function>()), start(_isolate, _start) {}
|
||||
|
||||
static void xStep(sqlite3_context* invocation, int argc, sqlite3_value** argv) {
|
||||
xStepBase(invocation, argc, argv, &CustomAggregate::fn);
|
||||
}
|
||||
|
||||
static void xInverse(sqlite3_context* invocation, int argc, sqlite3_value** argv) {
|
||||
xStepBase(invocation, argc, argv, &CustomAggregate::inverse);
|
||||
}
|
||||
|
||||
static void xValue(sqlite3_context* invocation) {
|
||||
xValueBase(invocation, false);
|
||||
}
|
||||
|
||||
static void xFinal(sqlite3_context* invocation) {
|
||||
xValueBase(invocation, true);
|
||||
}
|
||||
|
||||
private:
|
||||
static inline void xStepBase(sqlite3_context* invocation, int argc, sqlite3_value** argv, const CopyablePersistent<v8::Function> CustomAggregate::*ptrtm) {
|
||||
AGGREGATE_START();
|
||||
|
||||
v8::Local<v8::Value> args_fast[5];
|
||||
v8::Local<v8::Value>* args = argc <= 4 ? args_fast : ALLOC_ARRAY<v8::Local<v8::Value>>(argc + 1);
|
||||
args[0] = v8::Local<v8::Value>::New(isolate, acc->value);
|
||||
if (argc != 0) Data::GetArgumentsJS(isolate, args + 1, argv, argc, self->safe_ints);
|
||||
|
||||
v8::MaybeLocal<v8::Value> maybe_return_value = v8::Local<v8::Function>::New(isolate, self->*ptrtm)->Call(OnlyContext, v8::Undefined(isolate), argc + 1, args);
|
||||
if (args != args_fast) delete[] args;
|
||||
|
||||
if (maybe_return_value.IsEmpty()) {
|
||||
self->PropagateJSError(invocation);
|
||||
} else {
|
||||
v8::Local<v8::Value> return_value = maybe_return_value.ToLocalChecked();
|
||||
if (!return_value->IsUndefined()) acc->value.Reset(isolate, return_value);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void xValueBase(sqlite3_context* invocation, bool is_final) {
|
||||
AGGREGATE_START();
|
||||
|
||||
if (!is_final) {
|
||||
acc->is_window = true;
|
||||
} else if (acc->is_window) {
|
||||
DestroyAccumulator(invocation);
|
||||
return;
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> result = v8::Local<v8::Value>::New(isolate, acc->value);
|
||||
if (self->invoke_result) {
|
||||
v8::MaybeLocal<v8::Value> maybe_result = v8::Local<v8::Function>::New(isolate, self->result)->Call(OnlyContext, v8::Undefined(isolate), 1, &result);
|
||||
if (maybe_result.IsEmpty()) {
|
||||
self->PropagateJSError(invocation);
|
||||
return;
|
||||
}
|
||||
result = maybe_result.ToLocalChecked();
|
||||
}
|
||||
|
||||
Data::ResultValueFromJS(isolate, invocation, result, self);
|
||||
if (is_final) DestroyAccumulator(invocation);
|
||||
}
|
||||
|
||||
struct Accumulator { public:
|
||||
CopyablePersistent<v8::Value> value;
|
||||
bool initialized;
|
||||
bool is_window;
|
||||
}
|
||||
|
||||
Accumulator* GetAccumulator(sqlite3_context* invocation) {
|
||||
Accumulator* acc = static_cast<Accumulator*>(sqlite3_aggregate_context(invocation, sizeof(Accumulator)));
|
||||
if (!acc->initialized) {
|
||||
assert(acc->value.IsEmpty());
|
||||
acc->initialized = true;
|
||||
if (invoke_start) {
|
||||
v8::MaybeLocal<v8::Value> maybe_seed = v8::Local<v8::Function>::Cast(v8::Local<v8::Value>::New(isolate, start))->Call(OnlyContext, v8::Undefined(isolate), 0, NULL);
|
||||
if (maybe_seed.IsEmpty()) PropagateJSError(invocation);
|
||||
else acc->value.Reset(isolate, maybe_seed.ToLocalChecked());
|
||||
} else {
|
||||
assert(!start.IsEmpty());
|
||||
acc->value.Reset(isolate, start);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static void DestroyAccumulator(sqlite3_context* invocation) {
|
||||
Accumulator* acc = static_cast<Accumulator*>(sqlite3_aggregate_context(invocation, sizeof(Accumulator)));
|
||||
assert(acc->initialized);
|
||||
acc->value.Reset();
|
||||
}
|
||||
|
||||
void PropagateJSError(sqlite3_context* invocation) {
|
||||
DestroyAccumulator(invocation);
|
||||
CustomFunction::PropagateJSError(invocation);
|
||||
}
|
||||
|
||||
const bool invoke_result;
|
||||
const bool invoke_start;
|
||||
const CopyablePersistent<v8::Function> inverse;
|
||||
const CopyablePersistent<v8::Function> result;
|
||||
const CopyablePersistent<v8::Value> start;
|
||||
};
|
||||
52
src/util/custom-function.lzz
Normal file
52
src/util/custom-function.lzz
Normal file
@ -0,0 +1,52 @@
|
||||
class CustomFunction {
|
||||
public:
|
||||
|
||||
explicit CustomFunction(v8::Isolate* _isolate, Database* _db, v8::Local<v8::Function> _fn, const char* _name, bool _safe_ints)
|
||||
: name(COPY(_name)), db(_db), isolate(_isolate), fn(_isolate, _fn), safe_ints(_safe_ints) {}
|
||||
virtual ~CustomFunction() { delete[] name; }
|
||||
|
||||
static void xDestroy(void* self) {
|
||||
delete static_cast<CustomFunction*>(self);
|
||||
}
|
||||
|
||||
static void xFunc(sqlite3_context* invocation, int argc, sqlite3_value** argv) {
|
||||
FUNCTION_START();
|
||||
|
||||
v8::Local<v8::Value> args_fast[4];
|
||||
v8::Local<v8::Value>* args = NULL;
|
||||
if (argc != 0) {
|
||||
args = argc <= 4 ? args_fast : ALLOC_ARRAY<v8::Local<v8::Value>>(argc);
|
||||
Data::GetArgumentsJS(isolate, args, argv, argc, self->safe_ints);
|
||||
}
|
||||
|
||||
v8::MaybeLocal<v8::Value> maybe_return_value = v8::Local<v8::Function>::New(isolate, self->fn)->Call(OnlyContext, v8::Undefined(isolate), argc, args);
|
||||
if (args != args_fast) delete[] args;
|
||||
|
||||
if (maybe_return_value.IsEmpty()) self->PropagateJSError(invocation);
|
||||
else Data::ResultValueFromJS(isolate, invocation, maybe_return_value.ToLocalChecked(), self);
|
||||
}
|
||||
|
||||
void ThrowResultValueError(sqlite3_context* invocation, bool isBigInt) {
|
||||
if (isBigInt) {
|
||||
ThrowRangeError(CONCAT("User-defined function ", name, "() returned a bigint that was too big").c_str());
|
||||
} else {
|
||||
ThrowTypeError(CONCAT("User-defined function ", name, "() returned an invalid value").c_str());
|
||||
}
|
||||
PropagateJSError(invocation);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void PropagateJSError(sqlite3_context* invocation) {
|
||||
assert(db->GetState()->was_js_error == false);
|
||||
db->GetState()->was_js_error = true;
|
||||
sqlite3_result_error(invocation, "", 0);
|
||||
}
|
||||
|
||||
private:
|
||||
const char* const name;
|
||||
Database* const db;
|
||||
protected:
|
||||
v8::Isolate* const isolate;
|
||||
const CopyablePersistent<v8::Function> fn;
|
||||
const bool safe_ints;
|
||||
};
|
||||
147
src/util/data.lzz
Normal file
147
src/util/data.lzz
Normal file
@ -0,0 +1,147 @@
|
||||
#define JS_VALUE_TO_SQLITE(to, value, isolate, ...) \
|
||||
if (value->IsNumber()) { \
|
||||
return sqlite3_##to##_double( \
|
||||
__VA_ARGS__, \
|
||||
v8::Local<v8::Number>::Cast(value)->Value() \
|
||||
); \
|
||||
} else if (value->IsBigInt()) { \
|
||||
bool lossless; \
|
||||
int64_t v = v8::Local<v8::BigInt>::Cast(value)->Int64Value(&lossless); \
|
||||
if (lossless) { \
|
||||
return sqlite3_##to##_int64(__VA_ARGS__, v); \
|
||||
} \
|
||||
} else if (value->IsString()) { \
|
||||
v8::String::Utf8Value utf8( \
|
||||
isolate, \
|
||||
v8::Local<v8::String>::Cast(value) \
|
||||
); \
|
||||
return sqlite3_##to##_text( \
|
||||
__VA_ARGS__, \
|
||||
*utf8, \
|
||||
utf8.length(), \
|
||||
SQLITE_TRANSIENT \
|
||||
); \
|
||||
} else if (node::Buffer::HasInstance(value)) { \
|
||||
return sqlite3_##to##_blob( \
|
||||
__VA_ARGS__, \
|
||||
node::Buffer::Data(value), \
|
||||
node::Buffer::Length(value), \
|
||||
SQLITE_TRANSIENT \
|
||||
); \
|
||||
} else if (value->IsNull() || value->IsUndefined()) { \
|
||||
return sqlite3_##to##_null(__VA_ARGS__); \
|
||||
}
|
||||
|
||||
#define SQLITE_VALUE_TO_JS(from, isolate, safe_ints, ...) \
|
||||
switch (sqlite3_##from##_type(__VA_ARGS__)) { \
|
||||
case SQLITE_INTEGER: \
|
||||
if (safe_ints) { \
|
||||
return v8::BigInt::New( \
|
||||
isolate, \
|
||||
sqlite3_##from##_int64(__VA_ARGS__) \
|
||||
); \
|
||||
} \
|
||||
case SQLITE_FLOAT: \
|
||||
return v8::Number::New( \
|
||||
isolate, \
|
||||
sqlite3_##from##_double(__VA_ARGS__) \
|
||||
); \
|
||||
case SQLITE_TEXT: \
|
||||
return StringFromUtf8( \
|
||||
isolate, \
|
||||
reinterpret_cast<const char*>(sqlite3_##from##_text(__VA_ARGS__)), \
|
||||
sqlite3_##from##_bytes(__VA_ARGS__) \
|
||||
); \
|
||||
case SQLITE_BLOB: \
|
||||
return node::Buffer::Copy( \
|
||||
isolate, \
|
||||
static_cast<const char*>(sqlite3_##from##_blob(__VA_ARGS__)), \
|
||||
sqlite3_##from##_bytes(__VA_ARGS__) \
|
||||
).ToLocalChecked(); \
|
||||
default: \
|
||||
assert(sqlite3_##from##_type(__VA_ARGS__) == SQLITE_NULL); \
|
||||
return v8::Null(isolate); \
|
||||
} \
|
||||
assert(false);
|
||||
|
||||
namespace Data {
|
||||
|
||||
static const char FLAT = 0;
|
||||
static const char PLUCK = 1;
|
||||
static const char EXPAND = 2;
|
||||
static const char RAW = 3;
|
||||
|
||||
v8::Local<v8::Value> GetValueJS(v8::Isolate* isolate, sqlite3_stmt* handle, int column, bool safe_ints) {
|
||||
SQLITE_VALUE_TO_JS(column, isolate, safe_ints, handle, column);
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> GetValueJS(v8::Isolate* isolate, sqlite3_value* value, bool safe_ints) {
|
||||
SQLITE_VALUE_TO_JS(value, isolate, safe_ints, value);
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> GetFlatRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
|
||||
v8::Local<v8::Object> row = v8::Object::New(isolate);
|
||||
int column_count = sqlite3_column_count(handle);
|
||||
for (int i=0; i<column_count; ++i) {
|
||||
row->Set(ctx,
|
||||
InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1),
|
||||
Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust();
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> GetExpandedRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
|
||||
v8::Local<v8::Object> row = v8::Object::New(isolate);
|
||||
int column_count = sqlite3_column_count(handle);
|
||||
for (int i=0; i<column_count; ++i) {
|
||||
const char* table_raw = sqlite3_column_table_name(handle, i);
|
||||
v8::Local<v8::String> table = InternalizedFromUtf8(isolate, table_raw == NULL ? "$" : table_raw, -1);
|
||||
v8::Local<v8::String> column = InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1);
|
||||
v8::Local<v8::Value> value = Data::GetValueJS(isolate, handle, i, safe_ints);
|
||||
if (row->HasOwnProperty(ctx, table).FromJust()) {
|
||||
v8::Local<v8::Object>::Cast(row->Get(ctx, table).ToLocalChecked())->Set(ctx, column, value).FromJust();
|
||||
} else {
|
||||
v8::Local<v8::Object> nested = v8::Object::New(isolate);
|
||||
row->Set(ctx, table, nested).FromJust();
|
||||
nested->Set(ctx, column, value).FromJust();
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> GetRawRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
|
||||
v8::Local<v8::Array> row = v8::Array::New(isolate);
|
||||
int column_count = sqlite3_column_count(handle);
|
||||
for (int i=0; i<column_count; ++i) {
|
||||
row->Set(ctx, i, Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust();
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> GetRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints, char mode) {
|
||||
if (mode == FLAT) return GetFlatRowJS(isolate, ctx, handle, safe_ints);
|
||||
if (mode == PLUCK) return GetValueJS(isolate, handle, 0, safe_ints);
|
||||
if (mode == EXPAND) return GetExpandedRowJS(isolate, ctx, handle, safe_ints);
|
||||
if (mode == RAW) return GetRawRowJS(isolate, ctx, handle, safe_ints);
|
||||
assert(false);
|
||||
return v8::Local<v8::Value>();
|
||||
}
|
||||
|
||||
void GetArgumentsJS(v8::Isolate* isolate, v8::Local<v8::Value>* out, sqlite3_value** values, int argument_count, bool safe_ints) {
|
||||
assert(argument_count > 0);
|
||||
for (int i=0; i<argument_count; ++i) {
|
||||
out[i] = Data::GetValueJS(isolate, values[i], safe_ints);
|
||||
}
|
||||
}
|
||||
|
||||
int BindValueFromJS(v8::Isolate* isolate, sqlite3_stmt* handle, int index, v8::Local<v8::Value> value) {
|
||||
JS_VALUE_TO_SQLITE(bind, value, isolate, handle, index);
|
||||
return value->IsBigInt() ? SQLITE_TOOBIG : -1;
|
||||
}
|
||||
|
||||
void ResultValueFromJS(v8::Isolate* isolate, sqlite3_context* invocation, v8::Local<v8::Value> value, CustomFunction* function) {
|
||||
JS_VALUE_TO_SQLITE(result, value, isolate, invocation);
|
||||
function->ThrowResultValueError(invocation, value->IsBigInt());
|
||||
}
|
||||
|
||||
}
|
||||
175
src/util/macros.lzz
Normal file
175
src/util/macros.lzz
Normal file
@ -0,0 +1,175 @@
|
||||
#define NODE_ARGUMENTS const v8::FunctionCallbackInfo<v8::Value>&
|
||||
#define NODE_ARGUMENTS_POINTER const v8::FunctionCallbackInfo<v8::Value>*
|
||||
#define NODE_METHOD(name) static void name(NODE_ARGUMENTS info)
|
||||
#define NODE_GETTER(name) static void name(v8::Local<v8::String> _, const v8::PropertyCallbackInfo<v8::Value>& info)
|
||||
#define INIT(name) static v8::Local<v8::Function> name(v8::Isolate* isolate, v8::Local<v8::External> data)
|
||||
|
||||
#define EasyIsolate v8::Isolate* isolate = v8::Isolate::GetCurrent()
|
||||
#define OnlyIsolate info.GetIsolate()
|
||||
#define OnlyContext isolate->GetCurrentContext()
|
||||
#define OnlyAddon static_cast<Addon*>(v8::Local<v8::External>::Cast(info.Data())->Value())
|
||||
#define UseIsolate v8::Isolate* isolate = OnlyIsolate
|
||||
#define UseContext v8::Local<v8::Context> ctx = OnlyContext
|
||||
#define UseAddon Addon* addon = OnlyAddon
|
||||
#define Unwrap node::ObjectWrap::Unwrap
|
||||
|
||||
inline v8::Local<v8::String> StringFromUtf8(v8::Isolate* isolate, const char* data, int length) {
|
||||
return v8::String::NewFromUtf8(isolate, data, v8::NewStringType::kNormal, length).ToLocalChecked();
|
||||
}
|
||||
inline v8::Local<v8::String> InternalizedFromUtf8(v8::Isolate* isolate, const char* data, int length) {
|
||||
return v8::String::NewFromUtf8(isolate, data, v8::NewStringType::kInternalized, length).ToLocalChecked();
|
||||
}
|
||||
inline v8::Local<v8::Value> InternalizedFromUtf8OrNull(v8::Isolate* isolate, const char* data, int length) {
|
||||
if (data == NULL) return v8::Null(isolate);
|
||||
return InternalizedFromUtf8(isolate, data, length);
|
||||
}
|
||||
inline v8::Local<v8::String> InternalizedFromLatin1(v8::Isolate* isolate, const char* str) {
|
||||
return v8::String::NewFromOneByte(isolate, reinterpret_cast<const uint8_t*>(str), v8::NewStringType::kInternalized).ToLocalChecked();
|
||||
}
|
||||
|
||||
#hdr
|
||||
template <class T> using CopyablePersistent = v8::Persistent<T, v8::CopyablePersistentTraits<T>>;
|
||||
#end
|
||||
inline void SetFrozen(v8::Isolate* isolate, v8::Local<v8::Context> ctx, v8::Local<v8::Object> obj, CopyablePersistent<v8::String>& key, v8::Local<v8::Value> value) {
|
||||
obj->DefineOwnProperty(ctx, CS::Get(isolate, key), value, static_cast<v8::PropertyAttribute>(v8::DontDelete | v8::ReadOnly)).FromJust();
|
||||
}
|
||||
|
||||
void ThrowError(const char* message) { EasyIsolate; isolate->ThrowException(v8::Exception::Error(StringFromUtf8(isolate, message, -1))); }
|
||||
void ThrowTypeError(const char* message) { EasyIsolate; isolate->ThrowException(v8::Exception::TypeError(StringFromUtf8(isolate, message, -1))); }
|
||||
void ThrowRangeError(const char* message) { EasyIsolate; isolate->ThrowException(v8::Exception::RangeError(StringFromUtf8(isolate, message, -1))); }
|
||||
|
||||
#define REQUIRE_ARGUMENT_ANY(at, var) \
|
||||
if (info.Length() <= (at())) \
|
||||
return ThrowTypeError("Expected a "#at" argument"); \
|
||||
var = info[at()]
|
||||
|
||||
#define _REQUIRE_ARGUMENT(at, var, Type, message, ...) \
|
||||
if (info.Length() <= (at()) || !info[at()]->Is##Type()) \
|
||||
return ThrowTypeError("Expected "#at" argument to be "#message); \
|
||||
var = v8::Local<v8::Type>::Cast(info[at()])__VA_ARGS__
|
||||
|
||||
#define REQUIRE_ARGUMENT_INT32(at, var) \
|
||||
_REQUIRE_ARGUMENT(at, var, Int32, a 32-bit signed integer, ->Value())
|
||||
#define REQUIRE_ARGUMENT_BOOLEAN(at, var) \
|
||||
_REQUIRE_ARGUMENT(at, var, Boolean, a boolean, ->Value())
|
||||
#define REQUIRE_ARGUMENT_STRING(at, var) \
|
||||
_REQUIRE_ARGUMENT(at, var, String, a string)
|
||||
#define REQUIRE_ARGUMENT_OBJECT(at, var) \
|
||||
_REQUIRE_ARGUMENT(at, var, Object, an object)
|
||||
#define REQUIRE_ARGUMENT_FUNCTION(at, var) \
|
||||
_REQUIRE_ARGUMENT(at, var, Function, a function)
|
||||
|
||||
#define REQUIRE_DATABASE_OPEN(db) \
|
||||
if (!db->open) \
|
||||
return ThrowTypeError("The database connection is not open")
|
||||
#define REQUIRE_DATABASE_NOT_BUSY(db) \
|
||||
if (db->busy) \
|
||||
return ThrowTypeError("This database connection is busy executing a query")
|
||||
#define REQUIRE_DATABASE_NO_ITERATORS(db) \
|
||||
if (db->iterators) \
|
||||
return ThrowTypeError("This database connection is busy executing a query")
|
||||
#define REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db) \
|
||||
if (!db->unsafe_mode) { \
|
||||
REQUIRE_DATABASE_NO_ITERATORS(db); \
|
||||
} ((void)0)
|
||||
#define REQUIRE_STATEMENT_NOT_LOCKED(stmt) \
|
||||
if (stmt->locked) \
|
||||
return ThrowTypeError("This statement is busy executing a query")
|
||||
|
||||
#define first() 0
|
||||
#define second() 1
|
||||
#define third() 2
|
||||
#define fourth() 3
|
||||
#define fifth() 4
|
||||
#define sixth() 5
|
||||
#define seventh() 6
|
||||
#define eighth() 7
|
||||
#define ninth() 8
|
||||
#define tenth() 9
|
||||
|
||||
// Returns a std:string of the concatenation of 3 well-formed C-strings.
|
||||
std::string CONCAT(const char* a, const char* b, const char* c) {
|
||||
std::string result(a);
|
||||
result += b;
|
||||
result += c;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns a copy of a well-formed C-string.
|
||||
const char* COPY(const char* source) {
|
||||
size_t bytes = strlen(source) + 1;
|
||||
char* dest = new char[bytes];
|
||||
memcpy(dest, source, bytes);
|
||||
return dest;
|
||||
}
|
||||
|
||||
// Determines whether to skip the given character at the start of an SQL string.
|
||||
inline bool IS_SKIPPED(char c) {
|
||||
return c == ' ' || c == ';' || (c >= '\t' && c <= '\r');
|
||||
}
|
||||
|
||||
// Allocates an empty array, without calling constructors/initializers.
|
||||
template<class T> inline T* ALLOC_ARRAY(size_t count) {
|
||||
return static_cast<T*>(::operator new[](count * sizeof(T)));
|
||||
}
|
||||
|
||||
// Deallocates an array, without calling destructors.
|
||||
template<class T> inline void FREE_ARRAY(T* array_pointer) {
|
||||
::operator delete[](array_pointer);
|
||||
}
|
||||
|
||||
v8::Local<v8::FunctionTemplate> NewConstructorTemplate(
|
||||
v8::Isolate* isolate,
|
||||
v8::Local<v8::External> data,
|
||||
v8::FunctionCallback func,
|
||||
const char* name
|
||||
) {
|
||||
v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New(isolate, func, data);
|
||||
t->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
t->SetClassName(InternalizedFromLatin1(isolate, name));
|
||||
return t;
|
||||
}
|
||||
void SetPrototypeMethod(
|
||||
v8::Isolate* isolate,
|
||||
v8::Local<v8::External> data,
|
||||
v8::Local<v8::FunctionTemplate> recv,
|
||||
const char* name,
|
||||
v8::FunctionCallback func
|
||||
) {
|
||||
v8::HandleScope scope(isolate);
|
||||
recv->PrototypeTemplate()->Set(
|
||||
InternalizedFromLatin1(isolate, name),
|
||||
v8::FunctionTemplate::New(isolate, func, data, v8::Signature::New(isolate, recv))
|
||||
);
|
||||
}
|
||||
void SetPrototypeSymbolMethod(
|
||||
v8::Isolate* isolate,
|
||||
v8::Local<v8::External> data,
|
||||
v8::Local<v8::FunctionTemplate> recv,
|
||||
v8::Local<v8::Symbol> symbol,
|
||||
v8::FunctionCallback func
|
||||
) {
|
||||
v8::HandleScope scope(isolate);
|
||||
recv->PrototypeTemplate()->Set(
|
||||
symbol,
|
||||
v8::FunctionTemplate::New(isolate, func, data, v8::Signature::New(isolate, recv))
|
||||
);
|
||||
}
|
||||
void SetPrototypeGetter(
|
||||
v8::Isolate* isolate,
|
||||
v8::Local<v8::External> data,
|
||||
v8::Local<v8::FunctionTemplate> recv,
|
||||
const char* name,
|
||||
v8::AccessorGetterCallback func
|
||||
) {
|
||||
v8::HandleScope scope(isolate);
|
||||
recv->InstanceTemplate()->SetAccessor(
|
||||
InternalizedFromLatin1(isolate, name),
|
||||
func,
|
||||
0,
|
||||
data,
|
||||
v8::AccessControl::DEFAULT,
|
||||
v8::PropertyAttribute::None,
|
||||
v8::AccessorSignature::New(isolate, recv)
|
||||
);
|
||||
}
|
||||
72
src/util/query-macros.lzz
Normal file
72
src/util/query-macros.lzz
Normal file
@ -0,0 +1,72 @@
|
||||
#define STATEMENT_BIND(handle) \
|
||||
Binder binder(handle); \
|
||||
if (!binder.Bind(info, info.Length(), stmt)) { \
|
||||
sqlite3_clear_bindings(handle); \
|
||||
return; \
|
||||
} ((void)0)
|
||||
|
||||
#define STATEMENT_THROW_LOGIC() \
|
||||
db->ThrowDatabaseError(); \
|
||||
if (!bound) { sqlite3_clear_bindings(handle); } \
|
||||
return
|
||||
|
||||
#define STATEMENT_RETURN_LOGIC(return_value) \
|
||||
info.GetReturnValue().Set(return_value); \
|
||||
if (!bound) { sqlite3_clear_bindings(handle); } \
|
||||
return
|
||||
|
||||
#define STATEMENT_START_LOGIC(RETURNS_DATA_CHECK, MUTATE_CHECK) \
|
||||
Statement* stmt = Unwrap<Statement>(info.This()); \
|
||||
RETURNS_DATA_CHECK(); \
|
||||
sqlite3_stmt* handle = stmt->handle; \
|
||||
Database* db = stmt->db; \
|
||||
REQUIRE_DATABASE_OPEN(db->GetState()); \
|
||||
REQUIRE_DATABASE_NOT_BUSY(db->GetState()); \
|
||||
MUTATE_CHECK(); \
|
||||
const bool bound = stmt->bound; \
|
||||
if (!bound) { \
|
||||
STATEMENT_BIND(handle); \
|
||||
} else if (info.Length() > 0) { \
|
||||
return ThrowTypeError("This statement already has bound parameters"); \
|
||||
} ((void)0)
|
||||
|
||||
|
||||
#define STATEMENT_THROW() db->GetState()->busy = false; STATEMENT_THROW_LOGIC()
|
||||
#define STATEMENT_RETURN(x) db->GetState()->busy = false; STATEMENT_RETURN_LOGIC(x)
|
||||
#define STATEMENT_START(x, y) \
|
||||
STATEMENT_START_LOGIC(x, y); \
|
||||
db->GetState()->busy = true; \
|
||||
UseIsolate; \
|
||||
if (db->Log(isolate, handle)) { \
|
||||
STATEMENT_THROW(); \
|
||||
} ((void)0)
|
||||
|
||||
|
||||
#define DOES_NOT_MUTATE() REQUIRE_STATEMENT_NOT_LOCKED(stmt)
|
||||
#define DOES_MUTATE() \
|
||||
assert(!stmt->locked); \
|
||||
REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db->GetState())
|
||||
#define DOES_ADD_ITERATOR() \
|
||||
DOES_NOT_MUTATE(); \
|
||||
if (db->GetState()->iterators == USHRT_MAX) \
|
||||
return ThrowRangeError("Too many active database iterators")
|
||||
#define REQUIRE_STATEMENT_RETURNS_DATA() \
|
||||
if (!stmt->returns_data) \
|
||||
return ThrowTypeError("This statement does not return data. Use run() instead")
|
||||
#define REQUIRE_STATEMENT_DOESNT_RETURN_DATA() \
|
||||
if (stmt->returns_data) \
|
||||
return ThrowTypeError("This statement returns data. Use get(), all(), or iterate() instead")
|
||||
|
||||
|
||||
#define _FUNCTION_START(type) \
|
||||
type* self = static_cast<type*>(sqlite3_user_data(invocation)); \
|
||||
v8::Isolate* isolate = self->isolate; \
|
||||
v8::HandleScope scope(isolate)
|
||||
|
||||
#define FUNCTION_START() \
|
||||
_FUNCTION_START(CustomFunction)
|
||||
|
||||
#define AGGREGATE_START() \
|
||||
_FUNCTION_START(CustomAggregate); \
|
||||
Accumulator* acc = self->GetAccumulator(invocation); \
|
||||
if (acc->value.IsEmpty()) return
|
||||
@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { existsSync } = require('fs');
|
||||
const Database = require('../.');
|
||||
|
||||
describe('new Database()', function () {
|
||||
@ -32,10 +31,10 @@ describe('new Database()', function () {
|
||||
expect(db.readonly).to.be.false;
|
||||
expect(db.open).to.be.true;
|
||||
expect(db.inTransaction).to.be.false;
|
||||
expect(fs.existsSync('')).to.be.false;
|
||||
expect(fs.existsSync('null')).to.be.false;
|
||||
expect(fs.existsSync('undefined')).to.be.false;
|
||||
expect(fs.existsSync('[object Object]')).to.be.false;
|
||||
expect(existsSync('')).to.be.false;
|
||||
expect(existsSync('null')).to.be.false;
|
||||
expect(existsSync('undefined')).to.be.false;
|
||||
expect(existsSync('[object Object]')).to.be.false;
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
@ -46,49 +45,49 @@ describe('new Database()', function () {
|
||||
expect(db.readonly).to.be.false;
|
||||
expect(db.open).to.be.true;
|
||||
expect(db.inTransaction).to.be.false;
|
||||
expect(fs.existsSync(':memory:')).to.be.false;
|
||||
expect(existsSync(':memory:')).to.be.false;
|
||||
});
|
||||
it('should allow disk-bound databases to be created', function () {
|
||||
expect(fs.existsSync(util.next())).to.be.false;
|
||||
const db = this.db = new Database(util.current());
|
||||
expect(existsSync(util.next())).to.be.false;
|
||||
const db = this.db = Database(util.current());
|
||||
expect(db.name).to.equal(util.current());
|
||||
expect(db.memory).to.be.false;
|
||||
expect(db.readonly).to.be.false;
|
||||
expect(db.open).to.be.true;
|
||||
expect(db.inTransaction).to.be.false;
|
||||
expect(fs.existsSync(util.current())).to.be.true;
|
||||
expect(existsSync(util.current())).to.be.true;
|
||||
});
|
||||
it('should allow readonly database connections to be created', function () {
|
||||
expect(fs.existsSync(util.next())).to.be.false;
|
||||
expect(existsSync(util.next())).to.be.false;
|
||||
expect(() => (this.db = new Database(util.current(), { readonly: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
|
||||
(new Database(util.current())).close();
|
||||
expect(fs.existsSync(util.current())).to.be.true;
|
||||
expect(existsSync(util.current())).to.be.true;
|
||||
const db = this.db = new Database(util.current(), { readonly: true });
|
||||
expect(db.name).to.equal(util.current());
|
||||
expect(db.memory).to.be.false;
|
||||
expect(db.readonly).to.be.true;
|
||||
expect(db.open).to.be.true;
|
||||
expect(db.inTransaction).to.be.false;
|
||||
expect(fs.existsSync(util.current())).to.be.true;
|
||||
expect(existsSync(util.current())).to.be.true;
|
||||
});
|
||||
it('should not allow the "readonly" option for in-memory databases', function () {
|
||||
expect(fs.existsSync(util.next())).to.be.false;
|
||||
expect(existsSync(util.next())).to.be.false;
|
||||
expect(() => (this.db = new Database(':memory:', { readonly: true }))).to.throw(TypeError);
|
||||
expect(() => (this.db = new Database('', { readonly: true }))).to.throw(TypeError);
|
||||
expect(fs.existsSync(util.current())).to.be.false;
|
||||
expect(existsSync(util.current())).to.be.false;
|
||||
});
|
||||
it('should accept the "fileMustExist" option', function () {
|
||||
expect(fs.existsSync(util.next())).to.be.false;
|
||||
expect(existsSync(util.next())).to.be.false;
|
||||
expect(() => (this.db = new Database(util.current(), { fileMustExist: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
|
||||
(new Database(util.current())).close();
|
||||
expect(fs.existsSync(util.current())).to.be.true;
|
||||
expect(existsSync(util.current())).to.be.true;
|
||||
const db = this.db = new Database(util.current(), { fileMustExist: true });
|
||||
expect(db.name).to.equal(util.current());
|
||||
expect(db.memory).to.be.false;
|
||||
expect(db.readonly).to.be.false;
|
||||
expect(db.open).to.be.true;
|
||||
expect(db.inTransaction).to.be.false;
|
||||
expect(fs.existsSync(util.current())).to.be.true;
|
||||
expect(existsSync(util.current())).to.be.true;
|
||||
});
|
||||
util.itUnix('should accept the "timeout" option', function () {
|
||||
this.slow(4000);
|
||||
@ -116,38 +115,12 @@ describe('new Database()', function () {
|
||||
expect(() => (this.db = new Database(util.current(), { timeout: 75.01 }))).to.throw(TypeError);
|
||||
expect(() => (this.db = new Database(util.current(), { timeout: 0x80000000 }))).to.throw(RangeError);
|
||||
});
|
||||
it('should accept the "nativeBinding" option', function () {
|
||||
this.slow(500);
|
||||
const oldBinding = require('bindings')({ bindings: 'better_sqlite3.node', path: true });
|
||||
const newBinding = path.join(path.dirname(oldBinding), 'test.node');
|
||||
expect(oldBinding).to.be.a('string');
|
||||
fs.copyFileSync(oldBinding, newBinding);
|
||||
const getBinding = db => db[Object.getOwnPropertySymbols(db)[0]].constructor;
|
||||
let db1;
|
||||
let db2;
|
||||
let db3;
|
||||
try {
|
||||
db1 = new Database('');
|
||||
db2 = new Database('', { nativeBinding: oldBinding });
|
||||
db3 = new Database('', { nativeBinding: newBinding });
|
||||
expect(db1.open).to.be.true;
|
||||
expect(db2.open).to.be.true;
|
||||
expect(db3.open).to.be.true;
|
||||
expect(getBinding(db1)).to.equal(getBinding(db2));
|
||||
expect(getBinding(db1)).to.not.equal(getBinding(db3));
|
||||
expect(getBinding(db2)).to.not.equal(getBinding(db3));
|
||||
} finally {
|
||||
if (db1) db1.close();
|
||||
if (db2) db2.close();
|
||||
if (db3) db3.close();
|
||||
}
|
||||
});
|
||||
it('should throw an Error if the directory does not exist', function () {
|
||||
expect(fs.existsSync(util.next())).to.be.false;
|
||||
expect(existsSync(util.next())).to.be.false;
|
||||
const filepath = `temp/nonexistent/abcfoobar123/${util.current()}`;
|
||||
expect(() => (this.db = new Database(filepath))).to.throw(TypeError);
|
||||
expect(fs.existsSync(filepath)).to.be.false;
|
||||
expect(fs.existsSync(util.current())).to.be.false;
|
||||
expect(existsSync(filepath)).to.be.false;
|
||||
expect(existsSync(util.current())).to.be.false;
|
||||
});
|
||||
it('should have a proper prototype chain', function () {
|
||||
const db = this.db = new Database(util.next());
|
||||
@ -158,29 +131,4 @@ describe('new Database()', function () {
|
||||
expect(Database.prototype.close).to.equal(db.close);
|
||||
expect(Database.prototype).to.equal(Object.getPrototypeOf(db));
|
||||
});
|
||||
it('should work properly when called as a function', function () {
|
||||
const db = this.db = Database(util.next());
|
||||
expect(db).to.be.an.instanceof(Database);
|
||||
expect(db.constructor).to.equal(Database);
|
||||
expect(Database.prototype.close).to.equal(db.close);
|
||||
expect(Database.prototype).to.equal(Object.getPrototypeOf(db));
|
||||
});
|
||||
it('should work properly when subclassed', function () {
|
||||
class MyDatabase extends Database {
|
||||
foo() {
|
||||
return 999;
|
||||
}
|
||||
}
|
||||
const db = this.db = new MyDatabase(util.next());
|
||||
expect(db).to.be.an.instanceof(Database);
|
||||
expect(db).to.be.an.instanceof(MyDatabase);
|
||||
expect(db.constructor).to.equal(MyDatabase);
|
||||
expect(Database.prototype.close).to.equal(db.close);
|
||||
expect(MyDatabase.prototype.close).to.equal(db.close);
|
||||
expect(Database.prototype.foo).to.be.undefined;
|
||||
expect(MyDatabase.prototype.foo).to.equal(db.foo);
|
||||
expect(Database.prototype).to.equal(Object.getPrototypeOf(MyDatabase.prototype));
|
||||
expect(MyDatabase.prototype).to.equal(Object.getPrototypeOf(db));
|
||||
expect(db.foo()).to.equal(999);
|
||||
});
|
||||
});
|
||||
|
||||
@ -30,7 +30,6 @@ describe('Database#close()', function () {
|
||||
expect(() => this.db.pragma('cache_size')).to.throw(TypeError);
|
||||
expect(() => this.db.function('foo', () => {})).to.throw(TypeError);
|
||||
expect(() => this.db.aggregate('foo', { step: () => {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('foo', () => {})).to.throw(TypeError);
|
||||
});
|
||||
it('should prevent any existing statements from running', function () {
|
||||
this.db.prepare('CREATE TABLE people (name TEXT)').run();
|
||||
|
||||
@ -9,12 +9,11 @@ describe('Database#prepare()', function () {
|
||||
this.db.close();
|
||||
});
|
||||
|
||||
function assertStmt(stmt, source, db, reader, readonly) {
|
||||
function assertStmt(stmt, source, db, reader) {
|
||||
expect(stmt.source).to.equal(source);
|
||||
expect(stmt.constructor.name).to.equal('Statement');
|
||||
expect(stmt.database).to.equal(db);
|
||||
expect(stmt.reader).to.equal(reader);
|
||||
expect(stmt.readonly).to.equal(readonly);
|
||||
expect(() => new stmt.constructor(source)).to.throw(TypeError);
|
||||
}
|
||||
|
||||
@ -41,20 +40,13 @@ describe('Database#prepare()', function () {
|
||||
it('should create a prepared Statement object', function () {
|
||||
const stmt1 = this.db.prepare('CREATE TABLE people (name TEXT) ');
|
||||
const stmt2 = this.db.prepare('CREATE TABLE people (name TEXT); ');
|
||||
assertStmt(stmt1, 'CREATE TABLE people (name TEXT) ', this.db, false, false);
|
||||
assertStmt(stmt2, 'CREATE TABLE people (name TEXT); ', this.db, false, false);
|
||||
assertStmt(stmt1, 'CREATE TABLE people (name TEXT) ', this.db, false);
|
||||
assertStmt(stmt2, 'CREATE TABLE people (name TEXT); ', this.db, false);
|
||||
expect(stmt1).to.not.equal(stmt2);
|
||||
expect(stmt1).to.not.equal(this.db.prepare('CREATE TABLE people (name TEXT) '));
|
||||
});
|
||||
it('should create a prepared Statement object with just an expression', function () {
|
||||
const stmt = this.db.prepare('SELECT 555');
|
||||
assertStmt(stmt, 'SELECT 555', this.db, true, true);
|
||||
});
|
||||
it('should set the correct values for "reader" and "readonly"', function () {
|
||||
this.db.exec('CREATE TABLE data (value)');
|
||||
assertStmt(this.db.prepare('SELECT 555'), 'SELECT 555', this.db, true, true);
|
||||
assertStmt(this.db.prepare('BEGIN'), 'BEGIN', this.db, false, true);
|
||||
assertStmt(this.db.prepare('BEGIN EXCLUSIVE'), 'BEGIN EXCLUSIVE', this.db, false, false);
|
||||
assertStmt(this.db.prepare('DELETE FROM data RETURNING *'), 'DELETE FROM data RETURNING *', this.db, true, false);
|
||||
assertStmt(stmt, 'SELECT 555', this.db, true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,6 +19,10 @@ describe('Statement#run()', function () {
|
||||
this.db.close();
|
||||
});
|
||||
|
||||
it('should throw an exception when used on a statement that returns data', function () {
|
||||
const stmt = this.db.prepare('SELECT 555');
|
||||
expect(() => stmt.run()).to.throw(TypeError);
|
||||
});
|
||||
it('should work with CREATE TABLE', function () {
|
||||
const { info } = this.db.init();
|
||||
expect(info.changes).to.equal(0);
|
||||
@ -30,12 +34,6 @@ describe('Statement#run()', function () {
|
||||
expect(info.changes).to.equal(0);
|
||||
expect(info.lastInsertRowid).to.equal(0);
|
||||
});
|
||||
it('should work with SELECT', function () {
|
||||
const stmt = this.db.prepare('SELECT 555');
|
||||
const info = stmt.run();
|
||||
expect(info.changes).to.equal(0);
|
||||
expect(info.lastInsertRowid).to.equal(0);
|
||||
});
|
||||
it('should work with INSERT INTO', function () {
|
||||
let stmt = this.db.init().prepare("INSERT INTO entries VALUES ('foo', 25, 3.14, x'1133ddff')");
|
||||
let info = stmt.run();
|
||||
|
||||
@ -32,14 +32,6 @@ describe('Statement#get()', function () {
|
||||
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid");
|
||||
expect(stmt.get()).to.deep.equal({ a: 'foo', b: 6, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null });
|
||||
});
|
||||
it('should work with RETURNING clause', function () {
|
||||
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
|
||||
expect(stmt.reader).to.be.true;
|
||||
expect(stmt.get()).to.deep.equal({ a: 'bar', b: 888, c: null, d: null, e: null });
|
||||
|
||||
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 900 ORDER BY rowid");
|
||||
expect(stmt.get()).to.deep.equal({ a: 'baz', b: 999, c: null, d: null, e: null });
|
||||
});
|
||||
it('should obey the current pluck and expand settings', function () {
|
||||
const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid");
|
||||
const expanded = { entries: { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }, $: { c: 5.5 } };
|
||||
|
||||
@ -43,20 +43,6 @@ describe('Statement#all()', function () {
|
||||
expect(index).to.equal(rows.length);
|
||||
}
|
||||
});
|
||||
it('should work with RETURNING clause', function () {
|
||||
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
|
||||
expect(stmt.reader).to.be.true;
|
||||
expect(stmt.all()).to.deep.equal([
|
||||
{ a: 'bar', b: 888, c: null, d: null, e: null },
|
||||
{ a: 'baz', b: 999, c: null, d: null, e: null },
|
||||
]);
|
||||
|
||||
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 800 ORDER BY rowid");
|
||||
expect(stmt.all()).to.deep.equal([
|
||||
{ a: 'bar', b: 888, c: null, d: null, e: null },
|
||||
{ a: 'baz', b: 999, c: null, d: null, e: null },
|
||||
]);
|
||||
});
|
||||
it('should obey the current pluck and expand settings', function () {
|
||||
const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid");
|
||||
const expanded = new Array(10).fill().map((_, i) => ({
|
||||
|
||||
@ -32,7 +32,6 @@ describe('Statement#iterate()', function () {
|
||||
let count = 0;
|
||||
let stmt = this.db.prepare("SELECT * FROM entries ORDER BY rowid");
|
||||
expect(stmt.reader).to.be.true;
|
||||
expect(stmt.busy).to.be.false;
|
||||
|
||||
const iterator = stmt.iterate();
|
||||
expect(iterator).to.not.be.null;
|
||||
@ -42,43 +41,22 @@ describe('Statement#iterate()', function () {
|
||||
expect(iterator.throw).to.not.be.a('function');
|
||||
expect(iterator[Symbol.iterator]).to.be.a('function');
|
||||
expect(iterator[Symbol.iterator]()).to.equal(iterator);
|
||||
expect(stmt.busy).to.be.true;
|
||||
|
||||
for (const data of iterator) {
|
||||
row.b = ++count;
|
||||
expect(data).to.deep.equal(row);
|
||||
expect(stmt.busy).to.be.true;
|
||||
}
|
||||
expect(count).to.equal(10);
|
||||
expect(stmt.busy).to.be.false;
|
||||
|
||||
count = 0;
|
||||
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid");
|
||||
expect(stmt.busy).to.be.false;
|
||||
const iterator2 = stmt.iterate();
|
||||
expect(iterator).to.not.equal(iterator2);
|
||||
expect(stmt.busy).to.be.true;
|
||||
for (const data of iterator2) {
|
||||
row.b = ++count + 5;
|
||||
expect(data).to.deep.equal(row);
|
||||
expect(stmt.busy).to.be.true;
|
||||
}
|
||||
expect(count).to.equal(5);
|
||||
expect(stmt.busy).to.be.false;
|
||||
});
|
||||
it('should work with RETURNING clause', function () {
|
||||
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
|
||||
expect(stmt.reader).to.be.true;
|
||||
expect([...stmt.iterate()]).to.deep.equal([
|
||||
{ a: 'bar', b: 888, c: null, d: null, e: null },
|
||||
{ a: 'baz', b: 999, c: null, d: null, e: null },
|
||||
]);
|
||||
|
||||
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 800 ORDER BY rowid");
|
||||
expect([...stmt.iterate()]).to.deep.equal([
|
||||
{ a: 'bar', b: 888, c: null, d: null, e: null },
|
||||
{ a: 'baz', b: 999, c: null, d: null, e: null },
|
||||
]);
|
||||
});
|
||||
it('should obey the current pluck and expand settings', function () {
|
||||
const shouldHave = (desiredData) => {
|
||||
|
||||
@ -92,16 +92,4 @@ describe('Statement#bind()', function () {
|
||||
expect(() => stmt1.bind(arr)).to.throw(err);
|
||||
expect(() => stmt2.bind(obj)).to.throw(err);
|
||||
});
|
||||
it('should properly bind empty buffers', function () {
|
||||
this.db.prepare('INSERT INTO entries (c) VALUES (?)').bind(Buffer.alloc(0)).run();
|
||||
const result = this.db.prepare('SELECT c FROM entries').pluck().get();
|
||||
expect(result).to.be.an.instanceof(Buffer);
|
||||
expect(result.length).to.equal(0);
|
||||
});
|
||||
it('should properly bind empty strings', function () {
|
||||
this.db.prepare('INSERT INTO entries (a) VALUES (?)').bind('').run();
|
||||
const result = this.db.prepare('SELECT a FROM entries').pluck().get();
|
||||
expect(result).to.be.a('string');
|
||||
expect(result.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -27,7 +27,6 @@ describe('Database#function()', function () {
|
||||
it('should throw an exception if boolean options are provided as non-booleans', function () {
|
||||
expect(() => this.db.function('a', { varargs: undefined }, () => {})).to.throw(TypeError);
|
||||
expect(() => this.db.function('b', { deterministic: undefined }, () => {})).to.throw(TypeError);
|
||||
expect(() => this.db.function('b', { directOnly: undefined }, () => {})).to.throw(TypeError);
|
||||
expect(() => this.db.function('c', { safeIntegers: undefined }, () => {})).to.throw(TypeError);
|
||||
});
|
||||
it('should throw an exception if the provided name is empty', function () {
|
||||
|
||||
@ -40,7 +40,6 @@ describe('Database#aggregate()', function () {
|
||||
it('should throw an exception if boolean options are provided as non-booleans', function () {
|
||||
expect(() => this.db.aggregate('a', { step: () => {}, varargs: undefined })).to.throw(TypeError);
|
||||
expect(() => this.db.aggregate('b', { step: () => {}, deterministic: undefined })).to.throw(TypeError);
|
||||
expect(() => this.db.aggregate('b', { step: () => {}, directOnly: undefined })).to.throw(TypeError);
|
||||
expect(() => this.db.aggregate('c', { step: () => {}, safeIntegers: undefined })).to.throw(TypeError);
|
||||
});
|
||||
it('should throw an exception if function options are provided as non-fns', function () {
|
||||
|
||||
75
test/34.database.load-extension.js
Normal file
75
test/34.database.load-extension.js
Normal file
@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Database = require('../.');
|
||||
|
||||
describe('Database#loadExtension()', function () {
|
||||
let filepath;
|
||||
before(function () {
|
||||
const releaseFilepath = path.join(__dirname, '..', 'build', 'Release', 'test_extension.node');
|
||||
const debugFilepath = path.join(__dirname, '..', 'build', 'Debug', 'test_extension.node');
|
||||
try {
|
||||
fs.accessSync(releaseFilepath);
|
||||
filepath = releaseFilepath;
|
||||
} catch (_) {
|
||||
fs.accessSync(debugFilepath);
|
||||
filepath = debugFilepath;
|
||||
}
|
||||
});
|
||||
beforeEach(function () {
|
||||
this.db = new Database(util.next());
|
||||
});
|
||||
afterEach(function () {
|
||||
this.db.close();
|
||||
});
|
||||
|
||||
it('should throw an exception if a string argument is not given', function () {
|
||||
expect(() => this.db.loadExtension()).to.throw(TypeError);
|
||||
expect(() => this.db.loadExtension(undefined)).to.throw(TypeError);
|
||||
expect(() => this.db.loadExtension(null)).to.throw(TypeError);
|
||||
expect(() => this.db.loadExtension(123)).to.throw(TypeError);
|
||||
expect(() => this.db.loadExtension(new String(filepath))).to.throw(TypeError);
|
||||
expect(() => this.db.loadExtension([filepath])).to.throw(TypeError);
|
||||
});
|
||||
it('should throw an exception if the database is busy', function () {
|
||||
let invoked = false;
|
||||
for (const value of this.db.prepare('select 555').pluck().iterate()) {
|
||||
expect(value).to.equal(555);
|
||||
expect(() => this.db.loadExtension(filepath)).to.throw(TypeError);
|
||||
invoked = true;
|
||||
}
|
||||
expect(invoked).to.be.true;
|
||||
});
|
||||
it('should throw an exception if the extension is not found', function () {
|
||||
try {
|
||||
this.db.loadExtension(filepath + 'x');
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(Database.SqliteError);
|
||||
expect(err.message).to.be.a('string');
|
||||
expect(err.message.length).to.be.above(0);
|
||||
expect(err.message).to.not.equal('not an error');
|
||||
expect(err.code).to.equal('SQLITE_ERROR');
|
||||
return;
|
||||
}
|
||||
throw new Error('This code should not have been reached');
|
||||
});
|
||||
it('should register the specified extension', function () {
|
||||
expect(this.db.loadExtension(filepath)).to.equal(this.db);
|
||||
expect(this.db.prepare('SELECT testExtensionFunction(NULL, 123, 99, 2)').pluck().get()).to.equal(4);
|
||||
expect(this.db.prepare('SELECT testExtensionFunction(NULL, 2)').pluck().get()).to.equal(2);
|
||||
});
|
||||
it('should not allow registering extensions with SQL', function () {
|
||||
expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError);
|
||||
expect(this.db.loadExtension(filepath)).to.equal(this.db);
|
||||
expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError);
|
||||
this.db.close();
|
||||
this.db = new Database(util.next());
|
||||
try {
|
||||
this.db.loadExtension(filepath + 'x');
|
||||
} catch (err) {
|
||||
expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError);
|
||||
return;
|
||||
}
|
||||
throw new Error('This code should not have been reached');
|
||||
});
|
||||
});
|
||||
@ -1,695 +0,0 @@
|
||||
'use strict';
|
||||
const Database = require('../.');
|
||||
|
||||
describe('Database#table()', function () {
|
||||
beforeEach(function () {
|
||||
this.db = new Database(util.next());
|
||||
});
|
||||
afterEach(function () {
|
||||
this.db.close();
|
||||
});
|
||||
|
||||
it('should throw an exception if the correct arguments are not provided', function () {
|
||||
expect(() => this.db.table()).to.throw(TypeError);
|
||||
expect(() => this.db.table(null)).to.throw(TypeError);
|
||||
expect(() => this.db.table('a')).to.throw(TypeError);
|
||||
expect(() => this.db.table({})).to.throw(TypeError);
|
||||
expect(() => this.db.table({ rows: function*(){}, columns: ['x'] })).to.throw(TypeError);
|
||||
expect(() => this.db.table({ name: 'b', rows: function*(){}, columns: ['x'] })).to.throw(TypeError);
|
||||
expect(() => this.db.table(() => {})).to.throw(TypeError);
|
||||
expect(() => this.db.table(function* c() {})).to.throw(TypeError);
|
||||
expect(() => this.db.table({}, function d() {})).to.throw(TypeError);
|
||||
expect(() => this.db.table({ name: 'e', rows: function* e() {}, columns: ['x'] }, function e() {})).to.throw(TypeError);
|
||||
expect(() => this.db.table('f')).to.throw(TypeError);
|
||||
expect(() => this.db.table('g', null)).to.throw(TypeError);
|
||||
expect(() => this.db.table('h', {})).to.throw(TypeError);
|
||||
expect(() => this.db.table('i', Object.create(Function.prototype))).to.throw(TypeError);
|
||||
expect(() => this.db.table('j', { columns: ['x'] }, function j() {})).to.throw(TypeError);
|
||||
expect(() => this.db.table('k', { name: 'k', columns: ['x'] }, function* k() {})).to.throw(TypeError);
|
||||
expect(() => this.db.table('l', { name: 'l', rows: function* l() {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table(new String('m'), { columns: ['x'], rows: function* m() {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table(new String('n'), () => {})).to.throw(TypeError);
|
||||
});
|
||||
it('should throw an exception if boolean options are provided as non-booleans', function () {
|
||||
expect(() => this.db.table('a', { columns: ['x'], rows: function*(){}, directOnly: undefined })).to.throw(TypeError);
|
||||
expect(() => this.db.table('b', { columns: ['x'], rows: function*(){}, safeIntegers: undefined })).to.throw(TypeError);
|
||||
});
|
||||
it('should throw an exception if the "columns" option is invalid', function () {
|
||||
expect(() => this.db.table('a', { rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('b', { columns: undefined, rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('c', { columns: 'x', rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('d', { columns: { length: 1, 0: 'x', [Symbol.iterator]: () => ['x'].values() }, rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('e', { columns: ['x',, 'y'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('f', { columns: ['x', new String('y')], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('g', { columns: ['x', 'x'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('h', { columns: [], rows: function*(){} })).to.throw(RangeError);
|
||||
});
|
||||
it('should throw an exception if the "parameters" option is invalid', function () {
|
||||
expect(() => this.db.table('a', { parameters: undefined, columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('b', { parameters: 'x', columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('c', { parameters: { length: 1, 0: 'x', [Symbol.iterator]: () => ['x'].values() }, columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('d', { parameters: ['x',, 'y'], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('e', { parameters: ['x', new String('y')], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('f', { parameters: ['x', 'x'], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('g', { parameters: ['x'], columns: ['x'], rows: function*(){} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('h', { parameters: [...Array(33)].map((_, i) => `p${i}`), columns: ['foo'], rows: function*(){} })).to.throw(RangeError);
|
||||
});
|
||||
it('should throw an exception if the "rows" option is invalid', function () {
|
||||
expect(() => this.db.table('a', { columns: ['x'] })).to.throw(TypeError);
|
||||
expect(() => this.db.table('b', { columns: ['x'], rows: undefined })).to.throw(TypeError);
|
||||
expect(() => this.db.table('c', { columns: ['x'], rows: {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('d', { columns: ['x'], rows: () => {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('e', { columns: ['x'], rows: function () {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('f', { columns: ['x'], rows: Object.create(Function.prototype) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('g', { columns: ['x'], rows: Object.create(Object.getPrototypeOf(function*(){})) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('h', { columns: ['x'], rows: Object.setPrototypeOf(() => {}, Object.create(Object.getPrototypeOf(function*(){}))) })).to.throw(TypeError);
|
||||
});
|
||||
it('should throw an exception if the provided name is empty', function () {
|
||||
expect(() => this.db.table('', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('', { name: 'a', columns: ['x'], rows: function* () {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('', { name: 'b', columns: ['x'], rows: function* b() {} })).to.throw(TypeError);
|
||||
expect(() => this.db.table('', function c() {})).to.throw(TypeError);
|
||||
});
|
||||
it('should throw an exception if generator.length is invalid', function () {
|
||||
const length = x => Object.defineProperty(function*(){}, 'length', { value: x });
|
||||
expect(() => this.db.table('a', { columns: ['x'], rows: length(undefined) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('b', { columns: ['x'], rows: length(null) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('c', { columns: ['x'], rows: length('1') })).to.throw(TypeError);
|
||||
expect(() => this.db.table('d', { columns: ['x'], rows: length(NaN) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('e', { columns: ['x'], rows: length(Infinity) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('f', { columns: ['x'], rows: length(1.000000001) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('g', { columns: ['x'], rows: length(-0.000000001) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('h', { columns: ['x'], rows: length(-1) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('i', { columns: ['x'], rows: length(32.000000001) })).to.throw(TypeError);
|
||||
expect(() => this.db.table('j', { columns: ['x'], rows: length(33) })).to.throw(RangeError);
|
||||
});
|
||||
it('should register a virtual table and return the database object', function () {
|
||||
const length = x => Object.defineProperty(function*(){}, 'length', { value: x });
|
||||
expect(this.db.table('a', { columns: ['x'], rows: function* () {} })).to.equal(this.db);
|
||||
expect(this.db.table('b', { columns: ['x'], rows: length(1) })).to.equal(this.db);
|
||||
expect(this.db.table('c', { columns: ['x'], rows: length(32) })).to.equal(this.db);
|
||||
});
|
||||
it('should enable the registered virtual table to be queried from SQL', function () {
|
||||
const rows = [
|
||||
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
|
||||
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
|
||||
];
|
||||
this.db.table('vtab', {
|
||||
columns: ['a', 'b', 'c', 'd', 'e'],
|
||||
*rows() {
|
||||
for (const obj of rows) {
|
||||
yield Object.values(obj);
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE b < 500').all()).to.deep.equal(rows.slice(0, 1));
|
||||
expect(this.db.prepare('SELECT * FROM vtab ORDER BY d DESC').all()).to.deep.equal(rows.slice().reverse());
|
||||
});
|
||||
it('should infer parameters for the virtual table', function () {
|
||||
this.db.table('vtab', {
|
||||
columns: ['a', 'b'],
|
||||
*rows(x, y) {
|
||||
yield [x, y];
|
||||
yield [x * 2, y * 3];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?, ?)').all(2, 3))
|
||||
.to.deep.equal([{ a: 2, b: 3 }, { a: 4, b: 9 }]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ?').all(2, 3))
|
||||
.to.deep.equal([{ a: 2, b: 3 }, { a: 4, b: 9 }]);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?)'))
|
||||
.to.throw(Database.SqliteError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ? AND "$3" = ?'))
|
||||
.to.throw(Database.SqliteError);
|
||||
});
|
||||
it('should accept explicit parameters for the virtual table', function () {
|
||||
this.db.table('vtab', {
|
||||
columns: ['a', 'b'],
|
||||
parameters: ['x', 'y', 'z'],
|
||||
*rows(p1, p2, p3, p4) {
|
||||
yield [arguments[0], arguments[1] + arguments[2]];
|
||||
yield [arguments[0] * 2, (arguments[1] + arguments[2]) * 3];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').all(2, 3, 4))
|
||||
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').all(2, 3, 4))
|
||||
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?)'))
|
||||
.to.throw(Database.SqliteError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ? AND "$3" = ?'))
|
||||
.to.throw(Database.SqliteError);
|
||||
});
|
||||
it('should accept a large number of parameters for the virtual table', function () {
|
||||
const args = ['foo', 'bar', 1, -2, Buffer.from('hello'), 5, -10, 'baz', 99.9, -0.5];
|
||||
this.db.table('vtab', {
|
||||
columns: ['x'],
|
||||
*rows(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) {
|
||||
yield [p10];
|
||||
yield [p9];
|
||||
yield [p8];
|
||||
yield [p7];
|
||||
yield [p6];
|
||||
yield [p5];
|
||||
yield [p4];
|
||||
yield [p3];
|
||||
yield [p2];
|
||||
yield [p1];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').pluck().all(args))
|
||||
.to.deep.equal(args.slice().reverse());
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?)').pluck().all(args.slice(0, -1)))
|
||||
.to.deep.equal([null].concat(args.slice(0, -1).reverse()));
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'))
|
||||
.to.throw(Database.SqliteError);
|
||||
});
|
||||
it('should correctly handle arguments even when used out of order', function () {
|
||||
const calls = [];
|
||||
this.db.table('vtab', {
|
||||
columns: ['x', 'y'],
|
||||
*rows(x, y) {
|
||||
calls.push([...arguments]);
|
||||
yield { x, y };
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ?').get(10, 5))
|
||||
.to.deep.equal({ x: 10, y: 5 });
|
||||
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$1" = ?').get(5, 10))
|
||||
.to.deep.equal({ x: 10, y: 5 });
|
||||
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(5, 5, 10))
|
||||
.to.deep.equal({ x: 10, y: 5 });
|
||||
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(5, 9, 10))
|
||||
.to.be.undefined;
|
||||
expect(calls.splice(0)).to.deep.equal([]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(9, 5, 10))
|
||||
.to.be.undefined;
|
||||
expect(calls.splice(0)).to.deep.equal([]);
|
||||
});
|
||||
it('should correctly handle arguments that are constrained to other arguments', function () {
|
||||
const calls = [];
|
||||
this.db.table('vtab', {
|
||||
columns: ['x', 'y'],
|
||||
*rows(x, y) {
|
||||
calls.push([...arguments]);
|
||||
yield { x, y };
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = "$1"').get(10))
|
||||
.to.deep.equal({ x: 10, y: 10 });
|
||||
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = "$1" AND "$1" = ?').get(10))
|
||||
.to.deep.equal({ x: 10, y: 10 });
|
||||
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = "$1" AND "$1" = ?').get(10, 10))
|
||||
.to.deep.equal({ x: 10, y: 10 });
|
||||
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = "$1" AND "$1" = ?').get(5, 10))
|
||||
.to.be.undefined;
|
||||
expect(calls.splice(0)).to.deep.equal([]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = "$1" AND "$2" = ? AND "$1" = ?').get(5, 10))
|
||||
.to.be.undefined;
|
||||
expect(calls.splice(0)).to.deep.equal([]);
|
||||
});
|
||||
it('should throw an exception if the database is busy', function () {
|
||||
let ranOnce = false;
|
||||
for (const x of this.db.prepare('SELECT 2').pluck().iterate()) {
|
||||
expect(x).to.equal(2);
|
||||
ranOnce = true;
|
||||
expect(() => this.db.table('a', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
|
||||
}
|
||||
expect(ranOnce).to.be.true;
|
||||
this.db.table('b', { columns: ['x'], rows: function* () {} });
|
||||
});
|
||||
it('should cause the database to become busy when querying the virtual table', function () {
|
||||
let checkCount = 0;
|
||||
const expectBusy = function* () {
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
expect(() => this.db.exec('SELECT * FROM a')).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT 555')).to.throw(TypeError);
|
||||
expect(() => this.db.pragma('cache_size')).to.throw(TypeError);
|
||||
expect(() => this.db.function('x', () => {})).to.throw(TypeError);
|
||||
expect(() => this.db.table('y', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
|
||||
checkCount += 1;
|
||||
yield [i];
|
||||
}
|
||||
};
|
||||
this.db.table('a', { columns: ['x'], rows: function* () {} });
|
||||
this.db.table('b', { columns: ['x'], rows: expectBusy });
|
||||
|
||||
expect(this.db.prepare('SELECT * FROM b').pluck().all()).to.deep.equal([0, 1, 2]);
|
||||
expect(checkCount).to.equal(3);
|
||||
|
||||
this.db.exec('SELECT * FROM a');
|
||||
this.db.prepare('SELECT 555');
|
||||
this.db.pragma('cache_size');
|
||||
this.db.function('xx', () => {});
|
||||
this.db.table('yy', { columns: ['x'], rows: function* () {} })
|
||||
});
|
||||
it('should cause the virtual table to throw when yielding an invalid value', function () {
|
||||
this.db.table('a', {
|
||||
columns: ['x'],
|
||||
*rows() { yield [42]; }
|
||||
});
|
||||
this.db.table('b', {
|
||||
columns: ['x'],
|
||||
*rows() { yield 42; }
|
||||
});
|
||||
this.db.table('c', {
|
||||
columns: ['x'],
|
||||
*rows() { yield; }
|
||||
});
|
||||
this.db.table('d', {
|
||||
columns: ['x'],
|
||||
*rows() { yield null; }
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM a').get()).to.deep.equal({ x: 42 });
|
||||
expect(() => this.db.prepare('SELECT * FROM b').get()).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM c').get()).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM d').get()).to.throw(TypeError);
|
||||
});
|
||||
it('should allow arrays to be yielded as rows', function () {
|
||||
const rows = [
|
||||
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
|
||||
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
|
||||
];
|
||||
this.db.table('vtab', {
|
||||
columns: ['a', 'b', 'c', 'd', 'e'],
|
||||
*rows() {
|
||||
for (const obj of rows) {
|
||||
yield Object.values(obj);
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
|
||||
});
|
||||
it('should allow objects to be yielded as rows', function () {
|
||||
const rows = [
|
||||
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
|
||||
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
|
||||
{ e: Buffer.from('hello'), d: 'world', c: 0.1, b: 10, a: null },
|
||||
{ d: 'old friend', c: -0.1, e: Buffer.from('goodbye'), a: null, b: -10 },
|
||||
];
|
||||
this.db.table('vtab', {
|
||||
columns: ['a', 'b', 'c', 'd', 'e'],
|
||||
*rows() {
|
||||
for (const obj of rows) {
|
||||
yield obj;
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
|
||||
});
|
||||
it('should throw an exception if an invalid array is yielded', function () {
|
||||
const tests = [
|
||||
[1, 2, 3, 4, 5],
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[1, 2, 3, 4],
|
||||
[],
|
||||
[1, 2, 3, 4, new Number(5)],
|
||||
[1, 2, 3, 4, [5]],
|
||||
[1, 2, 3, 4, new Date()],
|
||||
];
|
||||
this.db.table('vtab', {
|
||||
columns: ['a', 'b', 'c', 'd', 'e'],
|
||||
*rows(n) {
|
||||
yield tests[n];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?)').raw().all(0)).to.deep.equal([tests[0]]);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(1)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(2)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(3)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(4)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(5)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(6)).to.throw(TypeError);
|
||||
});
|
||||
it('should throw an exception if an invalid object is yielded', function () {
|
||||
const tests = [
|
||||
{ a: 1, b: 2, c: 3, d: 4, e: 5 },
|
||||
{ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 },
|
||||
{ a: 1, b: 2, c: 3, d: 4 },
|
||||
{},
|
||||
{ a: 1, b: 2, c: 3, d: 4, e: new Number(5) },
|
||||
{ a: 1, b: 2, c: 3, d: 4, e: [5] },
|
||||
{ a: 1, b: 2, c: 3, d: 4, e: new Date() },
|
||||
{ a: 1, b: 2, c: 3, d: 4, f: 5 },
|
||||
];
|
||||
this.db.table('vtab', {
|
||||
columns: ['a', 'b', 'c', 'd', 'e'],
|
||||
*rows(n) {
|
||||
yield tests[n];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?)').all(0)).to.deep.equal([tests[0]]);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(1)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(2)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(3)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(4)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(5)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(6)).to.throw(TypeError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(7)).to.throw(TypeError);
|
||||
});
|
||||
it('should automatically assign rowids without affecting yielded objects', function () {
|
||||
let rows = [{ x: 5 }, { x: 10 }];
|
||||
this.db.table('a', {
|
||||
columns: ['x'],
|
||||
*rows() { yield* rows; },
|
||||
});
|
||||
expect(this.db.prepare('SELECT rowid, * FROM a').all())
|
||||
.to.deep.equal([{ rowid: 1, x: 5 }, { rowid: 2, x: 10 }]);
|
||||
expect(rows).to.deep.equal([{ x: 5 }, { x: 10 }]);
|
||||
|
||||
rows = [{ rowid: 5 }, { rowid: 10 }];
|
||||
this.db.table('b', {
|
||||
columns: ['rowid'],
|
||||
*rows() { yield* rows; },
|
||||
});
|
||||
expect(this.db.prepare('SELECT oid AS oid, * FROM b').all())
|
||||
.to.deep.equal([{ oid: 1, rowid: 5 }, { oid: 2, rowid: 10 }]);
|
||||
expect(rows).to.deep.equal([{ rowid: 5 }, { rowid: 10 }]);
|
||||
});
|
||||
it('should be driven by stmt.iterate() one row at a time', function () {
|
||||
let state = 0;
|
||||
this.db.table('vtab', {
|
||||
columns: ['x'],
|
||||
*rows() {
|
||||
state += 1;
|
||||
yield ['foo'];
|
||||
state += 1;
|
||||
yield ['bar'];
|
||||
state += 1;
|
||||
yield ['baz'];
|
||||
state += 1;
|
||||
yield ['qux'];
|
||||
state += 1;
|
||||
},
|
||||
});
|
||||
const values = [];
|
||||
for (const value of this.db.prepare('SELECT * FROM vtab').pluck().iterate()) {
|
||||
values.push(value);
|
||||
if (value === 'baz') break;
|
||||
}
|
||||
expect(values).to.deep.equal(['foo', 'bar', 'baz']);
|
||||
expect(state).to.equal(3);
|
||||
});
|
||||
it('should throw an exception if preparing a statement that uses an unsupported operator on a parameter', function () {
|
||||
this.db.table('vtab', {
|
||||
columns: ['a', 'b'],
|
||||
parameters: ['x', 'y', 'z'],
|
||||
*rows(x, y, z) {
|
||||
yield [x, y + z];
|
||||
yield [x * 2, (y + z) * 3];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').all(2, 3, 4))
|
||||
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').all(2, 3, 4))
|
||||
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z > ?'))
|
||||
.to.throw(Database.SqliteError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y < ? AND z = ?'))
|
||||
.to.throw(Database.SqliteError);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x IS ? AND y = ? AND z = ?'))
|
||||
.to.throw(Database.SqliteError);
|
||||
});
|
||||
it('should properly escape column and parameter names', function () {
|
||||
this.db.table('vtab', {
|
||||
columns: ['foo);'],
|
||||
parameters: ['x"); SELECT "y', 'y'],
|
||||
*rows(x, y) {
|
||||
yield [x];
|
||||
yield [y];
|
||||
yield [x + y];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT "foo);" FROM vtab WHERE "x""); SELECT ""y" = ? AND y = ?').all(5, 10))
|
||||
.to.deep.equal([{ 'foo);': 5 }, { 'foo);': 10 }, { 'foo);': 15 }]);
|
||||
});
|
||||
it('should not allow CREATE VIRTUAL TABLE statements by default', function () {
|
||||
this.db.table('mod', {
|
||||
columns: ['x'],
|
||||
*rows() {},
|
||||
});
|
||||
expect(() => this.db.exec('CREATE VIRTUAL TABLE a USING mod')).to.throw(Database.SqliteError);
|
||||
expect(() => this.db.exec('CREATE VIRTUAL TABLE b USING mod()')).to.throw(Database.SqliteError);
|
||||
expect(() => this.db.exec('CREATE VIRTUAL TABLE c USING mod(foo)')).to.throw(Database.SqliteError);
|
||||
});
|
||||
it('should support CREATE VIRTUAL TABLE statements by accepting a factory function', function () {
|
||||
let table = '';
|
||||
this.db.table('mod', function (...args) {
|
||||
expect(this).to.deep.equal({ module: 'mod', database: 'main', table });
|
||||
return {
|
||||
columns: ['x'],
|
||||
*rows() { yield* args.map(x => [x]); },
|
||||
};
|
||||
});
|
||||
expect(() => this.db.prepare('SELECT * FROM mod')).to.throw(Database.SqliteError);
|
||||
table = 'foo';
|
||||
this.db.exec(`CREATE VIRTUAL TABLE ${table} USING mod(hello world, how are you?)`);
|
||||
table = 'bar';
|
||||
this.db.exec(`CREATE VIRTUAL TABLE ${table} USING mod(1, 2, 3)`);
|
||||
expect(this.db.prepare('SELECT x FROM foo').pluck().all()).to.deep.equal(['hello world', 'how are you?']);
|
||||
expect(this.db.prepare('SELECT x FROM bar').pluck().all()).to.deep.equal(['1', '2', '3']);
|
||||
expect(() => this.db.prepare('SELECT * FROM mod')).to.throw(Database.SqliteError);
|
||||
});
|
||||
it('should correctly handle omitted arguments in any order', function () {
|
||||
this.db.table('vtab', {
|
||||
columns: ['value'],
|
||||
parameters: ['x', 'y', 'z'],
|
||||
*rows(x = 100, y = 10, z = 1) {
|
||||
expect(arguments.length).to.equal(3);
|
||||
yield [x + y + z];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').pluck().get(2.2, 3.3, 4.4)).to.equal(9.9);
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?, ?)').pluck().get(2.2, 3.3)).to.equal(6.5);
|
||||
expect(this.db.prepare('SELECT * FROM vtab(?)').pluck().get(2.2)).to.equal(13.2);
|
||||
expect(this.db.prepare('SELECT * FROM vtab').pluck().get()).to.equal(111);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').pluck().get(2.2, 3.3, 4.4)).to.equal(9.9);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ?').pluck().get(2.2, 3.3)).to.equal(6.5);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND z = ?').pluck().get(2.2, 3.3)).to.equal(15.5);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE y = ? AND z = ?').pluck().get(2.2, 3.3)).to.equal(105.5);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ?').pluck().get(2.2)).to.equal(13.2);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE y = ?').pluck().get(2.2)).to.equal(103.2);
|
||||
expect(this.db.prepare('SELECT * FROM vtab WHERE z = ?').pluck().get(2.2)).to.equal(112.2);
|
||||
});
|
||||
it('should not call the generator function if any arguments are NULL', function () {
|
||||
let calls = 0;
|
||||
this.db.table('vtab', {
|
||||
columns: ['val'],
|
||||
parameters: ['x', 'y', 'z'],
|
||||
*rows(x = 0, y = 0, z = 0) {
|
||||
calls += 1;
|
||||
yield [x + y + z];
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, 10, 100)).to.deep.equal([111]);
|
||||
expect(this.db.prepare('SELECT val FROM vtab(?, ?)').pluck().all(1, 10)).to.deep.equal([11]);
|
||||
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, 10, null)).to.deep.equal([]);
|
||||
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, null, 100)).to.deep.equal([]);
|
||||
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(null, 10, 100)).to.deep.equal([]);
|
||||
expect(this.db.prepare('SELECT val FROM vtab(?, ?)').pluck().all(1, null)).to.deep.equal([]);
|
||||
expect(calls).to.equal(2);
|
||||
});
|
||||
it('should close a statement iterator that caused a virtual table to throw', function () {
|
||||
this.db.prepare('CREATE TABLE iterable (x INTEGER)').run();
|
||||
this.db.prepare('INSERT INTO iterable WITH RECURSIVE temp(x) AS (SELECT 1 UNION ALL SELECT x * 2 FROM temp LIMIT 10) SELECT * FROM temp').run();
|
||||
|
||||
let i = 0;
|
||||
const err = new Error('foo');
|
||||
this.db.table('vtab', {
|
||||
columns: ['value'],
|
||||
parameters: ['x'],
|
||||
*rows(x) {
|
||||
if (++i >= 5) throw err;
|
||||
yield [x];
|
||||
},
|
||||
});
|
||||
const iterator = this.db.prepare('SELECT value FROM vtab JOIN iterable USING (x)').pluck().iterate();
|
||||
|
||||
let total = 0;
|
||||
expect(() => {
|
||||
for (const value of iterator) {
|
||||
total += value;
|
||||
expect(() => this.db.exec('SELECT value FROM vtab JOIN iterable USING (x) LIMIT 4')).to.throw(TypeError);
|
||||
}
|
||||
}).to.throw(err);
|
||||
|
||||
expect(total).to.equal(1 + 2 + 4 + 8);
|
||||
expect(iterator.next()).to.deep.equal({ value: undefined, done: true });
|
||||
expect(total).to.equal(1 + 2 + 4 + 8);
|
||||
|
||||
i = 0;
|
||||
this.db.exec('SELECT value FROM vtab JOIN iterable USING (x) LIMIT 4');
|
||||
expect(i).to.equal(4);
|
||||
});
|
||||
it('should not be able to affect bound buffers mid-query', function () {
|
||||
const input = Buffer.alloc(1024 * 8).fill(0xbb);
|
||||
let called = false;
|
||||
this.db.table('vtab', {
|
||||
columns: ['x'],
|
||||
*rows(arg) {
|
||||
called = true;
|
||||
input[0] = 2;
|
||||
arg[0] = 2;
|
||||
yield [123];
|
||||
},
|
||||
});
|
||||
const [output, arg, num] = this.db.prepare('SELECT :input, "$1", x FROM vtab(:input)').raw().get({ input });
|
||||
expect(called).to.be.true;
|
||||
expect(output.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true;
|
||||
expect(arg.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true;
|
||||
expect(num).to.equal(123);
|
||||
});
|
||||
describe('should propagate exceptions', function () {
|
||||
const exceptions = [new TypeError('foobar'), new Error('baz'), { yup: 'ok' }, 'foobarbazqux', '', null, 123.4];
|
||||
const expectError = (exception, fn) => {
|
||||
try { fn(); } catch (ex) {
|
||||
expect(ex).to.equal(exception);
|
||||
return;
|
||||
}
|
||||
throw new TypeError('Expected table to throw an exception');
|
||||
};
|
||||
|
||||
specify('thrown in the factory function', function () {
|
||||
exceptions.forEach((exception, index) => {
|
||||
const calls = [];
|
||||
this.db.table(`mod${index}`, () => {
|
||||
calls.push('a');
|
||||
throw exception;
|
||||
calls.push('b');
|
||||
return {
|
||||
columns: ['x'],
|
||||
*rows() {
|
||||
calls.push('c');
|
||||
yield [42];
|
||||
calls.push('d');
|
||||
},
|
||||
};
|
||||
});
|
||||
expect(calls.splice(0)).to.deep.equal([]);
|
||||
expectError(exception, () => this.db.exec(`CREATE VIRTUAL TABLE vtab${index} USING mod${index}()`));
|
||||
expect(calls.splice(0)).to.deep.equal(['a']);
|
||||
expect(() => this.db.prepare(`SELECT * FROM vtab${index}`)).to.throw(Database.SqliteError);
|
||||
expect(calls.splice(0)).to.deep.equal([]);
|
||||
});
|
||||
});
|
||||
specify('thrown in the rows() function', function () {
|
||||
exceptions.forEach((exception, index) => {
|
||||
const calls = [];
|
||||
this.db.table(`mod${index}`, () => {
|
||||
calls.push('a');
|
||||
return {
|
||||
columns: ['x'],
|
||||
*rows() {
|
||||
calls.push('b');
|
||||
yield [42];
|
||||
calls.push('c');
|
||||
throw exception;
|
||||
calls.push('d');
|
||||
},
|
||||
};
|
||||
});
|
||||
expect(calls.splice(0)).to.deep.equal([]);
|
||||
this.db.exec(`CREATE VIRTUAL TABLE vtab${index} USING mod${index}()`);
|
||||
expect(calls.splice(0)).to.deep.equal(['a']);
|
||||
expect(this.db.prepare(`SELECT * FROM vtab${index}`).pluck().get()).to.equal(42);
|
||||
expect(calls.splice(0)).to.deep.equal(['b']);
|
||||
expectError(exception, () => this.db.prepare(`SELECT * FROM vtab${index}`).pluck().all());
|
||||
expect(calls.splice(0)).to.deep.equal(['b', 'c']);
|
||||
});
|
||||
});
|
||||
specify('thrown due to yielding an invalid value', function () {
|
||||
const calls = [];
|
||||
this.db.table('mod', () => {
|
||||
calls.push('a');
|
||||
return {
|
||||
columns: ['x'],
|
||||
*rows() {
|
||||
calls.push('b');
|
||||
yield [42];
|
||||
calls.push('c');
|
||||
yield [new Number(42)];
|
||||
calls.push('d');
|
||||
},
|
||||
};
|
||||
});
|
||||
expect(calls.splice(0)).to.deep.equal([]);
|
||||
this.db.exec('CREATE VIRTUAL TABLE vtab USING mod()');
|
||||
expect(calls.splice(0)).to.deep.equal(['a']);
|
||||
expect(this.db.prepare('SELECT * FROM vtab').pluck().get()).to.equal(42);
|
||||
expect(calls.splice(0)).to.deep.equal(['b']);
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab').pluck().all()).to.throw(TypeError);
|
||||
expect(calls.splice(0)).to.deep.equal(['b', 'c']);
|
||||
});
|
||||
});
|
||||
describe('should not affect external environment', function () {
|
||||
specify('busy state', function () {
|
||||
this.db.table('vtab', {
|
||||
columns: ['x'],
|
||||
*rows(arg) {
|
||||
expect(() => this.db.exec('SELECT 555')).to.throw(TypeError);
|
||||
yield [arg * 2];
|
||||
},
|
||||
});
|
||||
let ranOnce = false;
|
||||
for (const x of this.db.prepare('SELECT * FROM vtab(555)').pluck().iterate()) {
|
||||
ranOnce = true;
|
||||
expect(x).to.equal(1110);
|
||||
expect(() => this.db.exec('SELECT 555')).to.throw(TypeError);
|
||||
}
|
||||
expect(ranOnce).to.be.true;
|
||||
this.db.exec('SELECT 555');
|
||||
});
|
||||
specify('was_js_error state', function () {
|
||||
this.db.prepare('CREATE TABLE data (value INTEGER)').run();
|
||||
const stmt = this.db.prepare('SELECT value FROM data');
|
||||
this.db.prepare('DROP TABLE data').run();
|
||||
|
||||
const err = new Error('foo');
|
||||
this.db.table('vtab', {
|
||||
columns: ['x'],
|
||||
*rows() { throw err; },
|
||||
});
|
||||
|
||||
expect(() => this.db.prepare('SELECT * FROM vtab').get()).to.throw(err);
|
||||
try { stmt.get(); } catch (ex) {
|
||||
expect(ex).to.be.an.instanceof(Error);
|
||||
expect(ex).to.not.equal(err);
|
||||
expect(ex.message).to.not.equal(err.message);
|
||||
expect(ex).to.be.an.instanceof(Database.SqliteError);
|
||||
return;
|
||||
}
|
||||
throw new TypeError('Expected the statement to throw an exception');
|
||||
});
|
||||
});
|
||||
it('should correctly handle limit and offset clause', function () {
|
||||
let lastValue;
|
||||
this.db.table('vtab', {
|
||||
columns: ['x'],
|
||||
*rows() {
|
||||
lastValue = 1;
|
||||
yield { x: lastValue };
|
||||
lastValue = 2;
|
||||
yield { x: lastValue };
|
||||
lastValue = 3;
|
||||
yield { x: lastValue };
|
||||
lastValue = null;
|
||||
},
|
||||
});
|
||||
expect(this.db.prepare('SELECT * FROM vtab LIMIT 1').all())
|
||||
.to.deep.equal([{ x: 1 }]);
|
||||
expect(lastValue).to.equal(1);
|
||||
expect(this.db.prepare('SELECT * FROM vtab LIMIT 1 OFFSET 2').all())
|
||||
.to.deep.equal([{ x: 3 }]);
|
||||
expect(lastValue).to.equal(3);
|
||||
expect(this.db.prepare('SELECT * FROM vtab LIMIT 100 OFFSET 1').all())
|
||||
.to.deep.equal([{ x: 2 }, { x: 3 }]);
|
||||
expect(lastValue).to.be.null;
|
||||
});
|
||||
});
|
||||
@ -1,78 +0,0 @@
|
||||
'use strict';
|
||||
const Database = require('../.');
|
||||
|
||||
const segmenter = new Intl.Segmenter([], {
|
||||
granularity: 'word',
|
||||
});
|
||||
|
||||
const DIACRITICS = /[\u0300-\u036f]/g;
|
||||
|
||||
function removeDiacritics(str) {
|
||||
return str.normalize('NFD').replace(DIACRITICS, '');
|
||||
}
|
||||
|
||||
describe('Database#serialize()', function () {
|
||||
beforeEach(function () {
|
||||
this.db = new Database(':memory:');
|
||||
|
||||
this.db.createFTS5Tokenizer('js', class Tokenizer {
|
||||
constructor(params) {
|
||||
expect(params).to.eql(['arg1', 'arg2']);
|
||||
}
|
||||
|
||||
run(str) {
|
||||
const result = [];
|
||||
let off = 0;
|
||||
for (const seg of segmenter.segment(str)) {
|
||||
const len = Buffer.byteLength(seg.segment);
|
||||
if (seg.isWordLike) {
|
||||
const normalized = removeDiacritics(seg.segment);
|
||||
result.push(off, off + len, normalized === seg.segment ? undefined : normalized);
|
||||
}
|
||||
off += len;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
this.db.prepare("CREATE VIRTUAL TABLE fts USING fts5(content, tokenize='js arg1 arg2')").run();
|
||||
this.insertStmt = this.db.prepare("INSERT INTO fts (content) VALUES (?)");
|
||||
this.lookupStmt = this.db.prepare(
|
||||
"SELECT snippet(fts, -1, '[', ']', '...', 20) " +
|
||||
"FROM fts " +
|
||||
"WHERE content MATCH $query").pluck();
|
||||
});
|
||||
afterEach(function () {
|
||||
this.db.close();
|
||||
});
|
||||
|
||||
it("should support CJK symbols at the start", function() {
|
||||
this.insertStmt.run("知识需要时间");
|
||||
const rows = this.lookupStmt.all({ query: "知*" });
|
||||
expect(rows).to.eql(["[知识]需要时间"]);
|
||||
});
|
||||
|
||||
it("should support CJK symbols in the middle", function() {
|
||||
this.insertStmt.run("知识需要时间");
|
||||
const rows = this.lookupStmt.all({ query: "需*" });
|
||||
expect(rows).to.eql(["知识[需要]时间"]);
|
||||
});
|
||||
|
||||
it("should support Korean symbols", function() {
|
||||
this.insertStmt.run("안녕 세상");
|
||||
const rows = this.lookupStmt.all({ query: "세*" });
|
||||
expect(rows).to.eql(["안녕 [세상]"]);
|
||||
});
|
||||
|
||||
it("should support normalization", function() {
|
||||
this.insertStmt.run("dïācrîtįcs");
|
||||
const rows = this.lookupStmt.all({ query: "diacritics*" });
|
||||
expect(rows).to.eql(["[dïācrîtįcs]"]);
|
||||
});
|
||||
|
||||
it("should support punctuation", function() {
|
||||
this.insertStmt.run("hello!world! how are you?");
|
||||
const rows = this.lookupStmt.all({ query: "h*" });
|
||||
expect(rows).to.eql(["[hello]!world! [how] are you?"]);
|
||||
});
|
||||
});
|
||||
@ -1,71 +0,0 @@
|
||||
'use strict';
|
||||
const Database = require('../.');
|
||||
|
||||
const segmenter = new Intl.Segmenter([], {
|
||||
granularity: 'word',
|
||||
});
|
||||
|
||||
const DIACRITICS = /[\u0300-\u036f]/g;
|
||||
|
||||
function removeDiacritics(str) {
|
||||
return str.normalize('NFD').replace(DIACRITICS, '');
|
||||
}
|
||||
|
||||
describe('Database#serialize()', function () {
|
||||
beforeEach(function () {
|
||||
this.db = new Database(':memory:');
|
||||
|
||||
this.db.prepare("CREATE VIRTUAL TABLE fts USING fts5(content, tokenize='signal_tokenizer')").run();
|
||||
this.insertStmt = this.db.prepare("INSERT INTO fts (content) VALUES (?)");
|
||||
this.lookupStmt = this.db.prepare(
|
||||
"SELECT snippet(fts, -1, '[', ']', '...', 20) " +
|
||||
"FROM fts " +
|
||||
"WHERE content MATCH $query").pluck();
|
||||
});
|
||||
afterEach(function () {
|
||||
this.db.close();
|
||||
});
|
||||
|
||||
it("should support CJK symbols at the start", function() {
|
||||
this.insertStmt.run("知识需要时间");
|
||||
const rows = this.lookupStmt.all({ query: "知*" });
|
||||
expect(rows).to.eql(["[知]识需要时间"]);
|
||||
});
|
||||
|
||||
it("should support CJK symbols in the middle", function() {
|
||||
this.insertStmt.run("知识需要时间");
|
||||
const rows = this.lookupStmt.all({ query: "需*" });
|
||||
expect(rows).to.eql(["知识[需]要时间"]);
|
||||
});
|
||||
|
||||
it("should support Korean symbols", function() {
|
||||
this.insertStmt.run("안녕 세상");
|
||||
const rows = this.lookupStmt.all({ query: "세*" });
|
||||
expect(rows).to.eql(["안녕 [세상]"]);
|
||||
});
|
||||
|
||||
it("should support normalization", function() {
|
||||
this.insertStmt.run("dïācrîtįcs");
|
||||
const rows = this.lookupStmt.all({ query: "diacritics*" });
|
||||
expect(rows).to.eql(["[dïācrîtįcs]"]);
|
||||
});
|
||||
|
||||
it("should support punctuation", function() {
|
||||
this.insertStmt.run("Hello!world! how are you?");
|
||||
const rows = this.lookupStmt.all({ query: "h*" });
|
||||
expect(rows).to.eql(["[Hello]!world! [how] are you?"]);
|
||||
});
|
||||
|
||||
it("should ignore invalid utf8", function() {
|
||||
this.insertStmt.run(Buffer.from([ 0x74 /* 't' */, 0xc3, 0x28 ]));
|
||||
const rows = this.lookupStmt.all({ query: "t*" });
|
||||
expect(rows).to.eql([]);
|
||||
});
|
||||
|
||||
it("should tokenize using signalTokenize", function() {
|
||||
expect(this.db.signalTokenize("Hello signal.org!")).to.eql([
|
||||
"hello",
|
||||
"signal.org",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -62,16 +62,6 @@ describe('BigInts', function () {
|
||||
expect(this.db.prepare('SELECT customfunc(?)').pluck().get(2)).to.equal('number2');
|
||||
expect(this.db.prepare('SELECT customfunc(?)').pluck().get(BigInt(2))).to.equal('bigint2');
|
||||
});
|
||||
it('should get passed to aggregates defined with the "safeIntegers" option', function () {
|
||||
this.db.aggregate('customagg', { safeIntegers: true, step: (_, a) => { return (typeof a) + a; } });
|
||||
expect(this.db.prepare('SELECT customagg(?)').pluck().get(2)).to.equal('number2');
|
||||
expect(this.db.prepare('SELECT customagg(?)').pluck().get(BigInt(2))).to.equal('bigint2');
|
||||
});
|
||||
it('should get passed to virtual tables defined with the "safeIntegers" option', function () {
|
||||
this.db.table('customvtab', { safeIntegers: true, columns: ['x'], *rows(a) { yield [(typeof a) + a]; } });
|
||||
expect(this.db.prepare('SELECT * FROM customvtab(?)').pluck().get(2)).to.equal('number2');
|
||||
expect(this.db.prepare('SELECT * FROM customvtab(?)').pluck().get(BigInt(2))).to.equal('bigint2');
|
||||
});
|
||||
it('should respect the default setting on the database', function () {
|
||||
let arg;
|
||||
const int = BigInt('1006028374637854687');
|
||||
@ -80,16 +70,6 @@ describe('BigInts', function () {
|
||||
this.db.prepare(`SELECT ${name}(?)`).get(int);
|
||||
return arg;
|
||||
};
|
||||
const customAggregateArg = (name, options, dontDefine) => {
|
||||
dontDefine || this.db.aggregate(name, { ...options, step: (_, a) => { arg = a; } });
|
||||
this.db.prepare(`SELECT ${name}(?)`).get(int);
|
||||
return arg;
|
||||
};
|
||||
const customTableArg = (name, options, dontDefine) => {
|
||||
dontDefine || this.db.table(name, { ...options, columns: ['x'], *rows(a) { arg = a; } });
|
||||
this.db.prepare(`SELECT * FROM ${name}(?)`).get(int);
|
||||
return arg;
|
||||
};
|
||||
this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int);
|
||||
this.db.defaultSafeIntegers(true);
|
||||
|
||||
@ -98,10 +78,6 @@ describe('BigInts', function () {
|
||||
expect(stmt.safeIntegers(false).get()).to.equal(1006028374637854700);
|
||||
expect(customFunctionArg('a1')).to.deep.equal(int);
|
||||
expect(customFunctionArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
|
||||
expect(customAggregateArg('a1')).to.deep.equal(int);
|
||||
expect(customAggregateArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
|
||||
expect(customTableArg('a1')).to.deep.equal(int);
|
||||
expect(customTableArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
|
||||
|
||||
this.db.defaultSafeIntegers(false);
|
||||
|
||||
@ -110,10 +86,6 @@ describe('BigInts', function () {
|
||||
expect(stmt2.safeIntegers().get()).to.deep.equal(int);
|
||||
expect(customFunctionArg('a3')).to.equal(1006028374637854700);
|
||||
expect(customFunctionArg('a4', { safeIntegers: true })).to.deep.equal(int);
|
||||
expect(customAggregateArg('a3')).to.equal(1006028374637854700);
|
||||
expect(customAggregateArg('a4', { safeIntegers: true })).to.deep.equal(int);
|
||||
expect(customTableArg('a3')).to.equal(1006028374637854700);
|
||||
expect(customTableArg('a4', { safeIntegers: true })).to.deep.equal(int);
|
||||
|
||||
this.db.defaultSafeIntegers();
|
||||
|
||||
@ -123,23 +95,11 @@ describe('BigInts', function () {
|
||||
expect(customFunctionArg('a2', {}, true)).to.equal(1006028374637854700);
|
||||
expect(customFunctionArg('a3', {}, true)).to.equal(1006028374637854700);
|
||||
expect(customFunctionArg('a4', {}, true)).to.deep.equal(int);
|
||||
expect(customAggregateArg('a1', {}, true)).to.deep.equal(int);
|
||||
expect(customAggregateArg('a2', {}, true)).to.equal(1006028374637854700);
|
||||
expect(customAggregateArg('a3', {}, true)).to.equal(1006028374637854700);
|
||||
expect(customAggregateArg('a4', {}, true)).to.deep.equal(int);
|
||||
expect(customTableArg('a1', {}, true)).to.deep.equal(int);
|
||||
expect(customTableArg('a2', {}, true)).to.equal(1006028374637854700);
|
||||
expect(customTableArg('a3', {}, true)).to.equal(1006028374637854700);
|
||||
expect(customTableArg('a4', {}, true)).to.deep.equal(int);
|
||||
|
||||
const stmt3 = this.db.prepare('SELECT a FROM entries').pluck();
|
||||
expect(stmt3.get()).to.deep.equal(int);
|
||||
expect(stmt3.safeIntegers(false).get()).to.equal(1006028374637854700);
|
||||
expect(customFunctionArg('a5')).to.deep.equal(int);
|
||||
expect(customFunctionArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
|
||||
expect(customAggregateArg('a5')).to.deep.equal(int);
|
||||
expect(customAggregateArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
|
||||
expect(customTableArg('a5')).to.deep.equal(int);
|
||||
expect(customTableArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
|
||||
});
|
||||
});
|
||||
|
||||
@ -167,23 +167,30 @@ describe('integrity checks', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Database#table()', function () {
|
||||
describe('Database#loadExtension()', function () {
|
||||
let filepath;
|
||||
before(function () {
|
||||
const releaseFilepath = path.join(__dirname, '..', 'build', 'Release', 'test_extension.node');
|
||||
const debugFilepath = path.join(__dirname, '..', 'build', 'Debug', 'test_extension.node');
|
||||
try {
|
||||
fs.accessSync(releaseFilepath);
|
||||
filepath = releaseFilepath;
|
||||
} catch (_) {
|
||||
fs.accessSync(debugFilepath);
|
||||
filepath = debugFilepath;
|
||||
}
|
||||
});
|
||||
|
||||
specify('while iterating (blocked)', function () {
|
||||
let i = 0;
|
||||
whileIterating(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
|
||||
expect(i).to.equal(5);
|
||||
normally(allowed(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
|
||||
whileIterating(this, blocked(() => this.db.loadExtension(filepath)));
|
||||
normally(allowed(() => this.db.loadExtension(filepath)));
|
||||
});
|
||||
specify('while busy (blocked)', function () {
|
||||
let i = 0;
|
||||
whileBusy(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
|
||||
expect(i).to.equal(5);
|
||||
normally(allowed(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
|
||||
whileBusy(this, blocked(() => this.db.loadExtension(filepath)));
|
||||
normally(allowed(() => this.db.loadExtension(filepath)));
|
||||
});
|
||||
specify('while closed (blocked)', function () {
|
||||
let i = 0;
|
||||
whileClosed(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
|
||||
expect(i).to.equal(1);
|
||||
whileClosed(this, blocked(() => this.db.loadExtension(filepath)));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user