Compare commits
182 Commits
tmp
...
better-sql
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2dd94be8f | ||
|
|
34dced2d27 | ||
|
|
6dcd4fa333 | ||
|
|
2a6d9a51fe | ||
|
|
3a3cad1d03 | ||
|
|
0000015ba0 | ||
|
|
551fc92c17 | ||
|
|
8c74ab6e62 | ||
|
|
cd50a61cb0 | ||
|
|
841b625bd4 | ||
|
|
aedb91ab92 | ||
|
|
9abaedb382 | ||
|
|
46134ef439 | ||
|
|
a610cb4899 | ||
|
|
c10c625237 | ||
|
|
6cf974078d | ||
|
|
13fecd9041 | ||
|
|
2c5d0d3e8a | ||
|
|
fc60881b64 | ||
|
|
896c585802 | ||
|
|
3a959e1c31 | ||
|
|
00731353b9 | ||
|
|
19577c3691 | ||
|
|
a3b182985c | ||
|
|
fc026bf4b6 | ||
|
|
5c0f7371f7 | ||
|
|
844b3893ca | ||
|
|
e3ef568532 | ||
|
|
eed787dec6 | ||
|
|
078808e7c2 | ||
|
|
75babc345a | ||
|
|
f66b8c6dd1 | ||
|
|
01d99874a6 | ||
|
|
0da082ad0d | ||
|
|
28d1929af0 | ||
|
|
89ff55bf0b | ||
|
|
312269c8c7 | ||
|
|
99400c7bdc | ||
|
|
6439afa9d5 | ||
|
|
850afac9a5 | ||
|
|
27576d4b2e | ||
|
|
00b49de136 | ||
|
|
6c22e97453 | ||
|
|
c3a02fb0eb | ||
|
|
e64c401b72 | ||
|
|
ca1e8a28dd | ||
|
|
f6eaf92037 | ||
|
|
2f59f4f15b | ||
|
|
b24625232b | ||
|
|
1931cc95f2 | ||
|
|
3bf6cd2b9f | ||
|
|
2a3355445f | ||
|
|
20e778de1f | ||
|
|
f668e9b8d9 | ||
|
|
0e26efeeb6 | ||
|
|
b82abd0fab | ||
|
|
843689519c | ||
|
|
e1693c48d0 | ||
|
|
4917cd51d7 | ||
|
|
99cede2168 | ||
|
|
4bd283e08a | ||
|
|
c418b73871 | ||
|
|
d161201f25 | ||
|
|
903bcd399a | ||
|
|
53f4c04f91 | ||
|
|
dc1bb76b97 | ||
|
|
3626b4c8d7 | ||
|
|
0fd1a2745b | ||
|
|
98bd9063a2 | ||
|
|
1a19f1fc6a | ||
|
|
e8ab1dc902 | ||
|
|
81f37a2f04 | ||
|
|
f1e1811589 | ||
|
|
d467d0fc5e | ||
|
|
07b59659ce | ||
|
|
23dfe23e16 | ||
|
|
5ea24501fc | ||
|
|
fe42144e18 | ||
|
|
b1df4a4e5f | ||
|
|
0ff0819c0d | ||
|
|
9a92ca84ca | ||
|
|
f328d9b496 | ||
|
|
79ba68c957 | ||
|
|
d48edee39a | ||
|
|
26ae9e0fc7 | ||
|
|
2e432b0aad | ||
|
|
583db4c9ee | ||
|
|
bfcdb16e52 | ||
|
|
ce0f0aa094 | ||
|
|
211b4bceec | ||
|
|
50230955c0 | ||
|
|
d4af9a32c1 | ||
|
|
56d9872ae5 | ||
|
|
dbe34b75a4 | ||
|
|
0a1cf9cae6 | ||
|
|
ac7338ad39 | ||
|
|
0807c512f3 | ||
|
|
dc255273fe | ||
|
|
6d8b420079 | ||
|
|
05a8cc2be6 | ||
|
|
4830a01ea6 | ||
|
|
3b92255205 | ||
|
|
a2b01b922a | ||
|
|
5054b50288 | ||
|
|
6beeec2f07 | ||
|
|
ca250488c4 | ||
|
|
7331a93430 | ||
|
|
835217678a | ||
|
|
1bf85ef39e | ||
|
|
22ef5d8cc6 | ||
|
|
ce2676e8d8 | ||
|
|
c821d92663 | ||
|
|
cb49b9118d | ||
|
|
87e3a97254 | ||
|
|
00d21ca645 | ||
|
|
3a340883dd | ||
|
|
bd45ac7818 | ||
|
|
2194095aa1 | ||
|
|
f0e622dd8c | ||
|
|
5eab05e68d | ||
|
|
b992d041cb | ||
|
|
ae12001cdf | ||
|
|
dc76c77326 | ||
|
|
36cf1c47bd | ||
|
|
1b32770139 | ||
|
|
ba9fcecb87 | ||
|
|
54e6cbadfb | ||
|
|
afdbd49cf1 | ||
|
|
c4ab3697af | ||
|
|
5caf211b72 | ||
|
|
f51f6fe710 | ||
|
|
fc70f3d416 | ||
|
|
dab57610e8 | ||
|
|
1cfaa8c9c6 | ||
|
|
d66747ef56 | ||
|
|
6a4a80090c | ||
|
|
15652ad800 | ||
|
|
a92f637708 | ||
|
|
52a3df2a31 | ||
|
|
65d5e336e2 | ||
|
|
0c42307437 | ||
|
|
4c6a583c94 | ||
|
|
157ea8bd83 | ||
|
|
e7d1b668f2 | ||
|
|
de2b2e95d0 | ||
|
|
793c6b2f84 | ||
|
|
3c9ac3b6c6 | ||
|
|
f52b3b00cf | ||
|
|
f639d61200 | ||
|
|
28667b11bd | ||
|
|
e4a1442f51 | ||
|
|
8fd426f3e5 | ||
|
|
2083409858 | ||
|
|
e56fae6954 | ||
|
|
dfbedccf83 | ||
|
|
a39e0acd84 | ||
|
|
499f748ae0 | ||
|
|
25e1c11650 | ||
|
|
55fec9ec6a | ||
|
|
3cb78917ec | ||
|
|
76ab48f0b0 | ||
|
|
c39c7deaf8 | ||
|
|
223598bf0e | ||
|
|
a7b1d94c87 | ||
|
|
9130cc8aee | ||
|
|
b1144a8232 | ||
|
|
1f58728bde | ||
|
|
48fdf9a3a4 | ||
|
|
9cd009ba04 | ||
|
|
9a98290bb4 | ||
|
|
81fee0a8d7 | ||
|
|
6da9e569db | ||
|
|
7aec2ac297 | ||
|
|
e886811f77 | ||
|
|
587721adcf | ||
|
|
feba6d6cc3 | ||
|
|
05da7712eb | ||
|
|
7d37224630 | ||
|
|
fbde33b1fc | ||
|
|
b544892631 | ||
|
|
d09615c936 | ||
|
|
5e07181b11 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,6 +1,2 @@
|
||||
*.lzz linguist-language=C++
|
||||
*.cpp -diff
|
||||
*.hpp -diff
|
||||
*.c -diff
|
||||
*.h -diff
|
||||
deps/sqlcipher.tar.gz filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@ -1,2 +1,9 @@
|
||||
* @JoshuaWise
|
||||
/.github/workflows/build.yml @JoshuaWise @mceachen
|
||||
/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
|
||||
|
||||
57
.github/workflows/build.yml
vendored
57
.github/workflows/build.yml
vendored
@ -18,14 +18,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-18.04
|
||||
- ubuntu-20.04
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
- windows-2019
|
||||
node:
|
||||
- 10
|
||||
- 12
|
||||
- 14
|
||||
- 16
|
||||
- 18
|
||||
name: Testing Node ${{ matrix.node }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@ -58,7 +57,7 @@ jobs:
|
||||
os:
|
||||
- ubuntu-18.04
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
- windows-2019
|
||||
name: Prebuild on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: publish
|
||||
@ -68,10 +67,12 @@ jobs:
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install --ignore-scripts
|
||||
- run: npx --no-install prebuild -r node -t 10.20.0 -t 12.0.0 -t 14.0.0 -t 16.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 -t 13.0.0 -t 14.0.0 -t 15.0.0 -t 16.0.0 -t 17.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: npx --no-install prebuild -r electron -t 10.0.0 -t 11.0.0 -t 12.0.0 -t 13.0.0 -t 14.0.0 -t 15.0.0 -t 16.0.0 -t 17.0.0 --include-regex 'better_sqlite3.node$' --arch ia32 -u ${{ secrets.GITHUB_TOKEN }}
|
||||
- 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
|
||||
@ -82,17 +83,41 @@ jobs:
|
||||
- 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 10.20.0 -t 12.0.0 -t 14.0.0 -t 16.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
|
||||
- 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-arm64-alpine:
|
||||
name: Prebuild on arm64 alpine
|
||||
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 --entrypoint /bin/sh --platform linux/arm64 node:16-alpine -c "apk add build-base git python3 --update-cache && \
|
||||
git clone ${{ github.event.repository.clone_url }} && \
|
||||
cd ${{ github.event.repository.name }} && \
|
||||
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 10.20.0 -t 12.0.0 -t 14.0.0 -t 16.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}"
|
||||
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 }}"
|
||||
|
||||
36
.github/workflows/bump-version.yml
vendored
Normal file
36
.github/workflows/bump-version.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
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
Normal file
48
.github/workflows/update-sqlite.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
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,3 +40,7 @@ lib/binding
|
||||
temp/
|
||||
TODO
|
||||
.local
|
||||
|
||||
# Downloaded artifact
|
||||
deps/unverified.tmp
|
||||
deps/sqlcipher.tar.gz
|
||||
|
||||
25
ACKNOWLEDGMENTS.md
Normal file
25
ACKNOWLEDGMENTS.md
Normal file
@ -0,0 +1,25 @@
|
||||
# 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
|
||||
13
README.md
13
README.md
@ -32,7 +32,7 @@ The fastest and simplest library for SQLite3 in Node.js.
|
||||
npm install better-sqlite3
|
||||
```
|
||||
|
||||
> 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).
|
||||
> 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).
|
||||
|
||||
## Usage
|
||||
|
||||
@ -43,11 +43,18 @@ 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)?
|
||||
@ -76,7 +83,9 @@ 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](./docs/compilation.md)
|
||||
- [SQLite3 compilation (advanced)](./docs/compilation.md)
|
||||
- [Contribution rules](./docs/contribution.md)
|
||||
- [Code of conduct](./docs/conduct.md)
|
||||
|
||||
# License
|
||||
|
||||
|
||||
15
binding.gyp
15
binding.gyp
@ -9,9 +9,13 @@
|
||||
'target_name': 'better_sqlite3',
|
||||
'dependencies': ['deps/sqlite3.gyp:sqlite3'],
|
||||
'sources': ['src/better_sqlite3.cpp'],
|
||||
'cflags': ['-std=c++14'],
|
||||
'xcode_settings': {
|
||||
'OTHER_CPLUSPLUSFLAGS': ['-std=c++14', '-stdlib=libc++'],
|
||||
'cflags_cc': ['-std=c++20'],
|
||||
'msvs_settings': {
|
||||
'VCCLCompilerTool': {
|
||||
'AdditionalOptions': [
|
||||
'/std:c++20',
|
||||
],
|
||||
},
|
||||
},
|
||||
'conditions': [
|
||||
['OS=="linux"', {
|
||||
@ -22,10 +26,5 @@
|
||||
}],
|
||||
],
|
||||
},
|
||||
{
|
||||
'target_name': 'test_extension',
|
||||
'dependencies': ['deps/sqlite3.gyp:sqlite3'],
|
||||
'conditions': [['sqlite3 == ""', { 'sources': ['deps/test_extension.c'] }]],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
17
deps/common.gypi
vendored
17
deps/common.gypi
vendored
@ -13,6 +13,15 @@
|
||||
},
|
||||
},
|
||||
'conditions': [
|
||||
['target_arch == "x64"', {
|
||||
'variables': {
|
||||
'rust_arch%': 'x86_64',
|
||||
}
|
||||
}, {
|
||||
'variables': {
|
||||
'rust_arch%': 'aarch64',
|
||||
}
|
||||
}],
|
||||
['OS == "win"', {
|
||||
'defines': ['WIN32'],
|
||||
'variables': {
|
||||
@ -37,7 +46,7 @@
|
||||
'-O0',
|
||||
],
|
||||
'xcode_settings': {
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.7',
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.15',
|
||||
'GCC_OPTIMIZATION_LEVEL': '0',
|
||||
'GCC_GENERATE_DEBUGGING_SYMBOLS': 'YES',
|
||||
},
|
||||
@ -59,11 +68,15 @@
|
||||
'-O3',
|
||||
],
|
||||
'xcode_settings': {
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.7',
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.15',
|
||||
'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
Normal file
26
deps/copy.js
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
'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));
|
||||
}
|
||||
15
deps/defines.gypi
vendored
15
deps/defines.gypi
vendored
@ -1,20 +1,26 @@
|
||||
# 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',
|
||||
@ -22,14 +28,17 @@
|
||||
'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',
|
||||
|
||||
|
||||
66
deps/download.js
vendored
Normal file
66
deps/download.js
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
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();
|
||||
110
deps/download.sh
vendored
110
deps/download.sh
vendored
@ -1,110 +0,0 @@
|
||||
#!/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. copy the generated amalgamation into the output directory (./sqlite3).
|
||||
# 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 symlink.js script creates symlinks to the bundled amalgamation.
|
||||
# 3. node-gyp compiles the symlinked sqlite3.c along with better_sqlite3.cpp.
|
||||
# 4. node-gyp links the two resulting binaries to generate better_sqlite3.node.
|
||||
# ===
|
||||
|
||||
YEAR="2022"
|
||||
VERSION="3370200"
|
||||
|
||||
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_MATH_FUNCTIONS
|
||||
SQLITE_ENABLE_DESERIALIZE
|
||||
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
|
||||
HAVE_STDINT_H=1
|
||||
HAVE_INT8_T=1
|
||||
HAVE_INT16_T=1
|
||||
HAVE_INT32_T=1
|
||||
HAVE_UINT8_T=1
|
||||
HAVE_UINT16_T=1
|
||||
HAVE_UINT32_T=1
|
||||
"
|
||||
|
||||
# ========== START SCRIPT ========== #
|
||||
|
||||
echo "setting up environment..."
|
||||
DEPS="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
TEMP="$DEPS/temp"
|
||||
OUTPUT="$DEPS/sqlite3"
|
||||
rm -rf "$TEMP"
|
||||
rm -rf "$OUTPUT"
|
||||
mkdir -p "$TEMP"
|
||||
mkdir -p "$OUTPUT"
|
||||
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 "copying amalgamation..."
|
||||
cp sqlite3.c sqlite3.h sqlite3ext.h "$OUTPUT/" || 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!"
|
||||
4
deps/extract.js
vendored
4
deps/extract.js
vendored
@ -2,8 +2,8 @@
|
||||
const path = require('path');
|
||||
const tar = require('tar');
|
||||
|
||||
const dest = process.argv[2];
|
||||
const source = path.join(__dirname, 'sqlcipher.tar.gz');
|
||||
const source = process.argv[2];
|
||||
const dest = process.argv[3];
|
||||
|
||||
process.on('unhandledRejection', (err) => { throw err; });
|
||||
|
||||
|
||||
BIN
deps/sqlcipher.tar.gz
(Stored with Git LFS)
vendored
BIN
deps/sqlcipher.tar.gz
(Stored with Git LFS)
vendored
Binary file not shown.
47
deps/sqlite3.gyp
vendored
47
deps/sqlite3.gyp
vendored
@ -9,12 +9,25 @@
|
||||
'includes': ['common.gypi'],
|
||||
'targets': [
|
||||
{
|
||||
'target_name': 'locate_sqlite3',
|
||||
'target_name': 'download_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': ['sqlcipher.tar.gz'],
|
||||
'inputs': ['<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz'],
|
||||
'outputs': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.c',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.h',
|
||||
@ -23,13 +36,11 @@
|
||||
'conditions': [
|
||||
['OS == "win"', {
|
||||
'outputs': [
|
||||
'<(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',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-pc-windows-msvc/signal_sqlcipher_extension.lib',
|
||||
],
|
||||
}],
|
||||
],
|
||||
'action': ['node', 'extract.js', '<(SHARED_INTERMEDIATE_DIR)/sqlite3'],
|
||||
'action': ['node', 'extract.js', '<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz', '<(SHARED_INTERMEDIATE_DIR)/sqlite3'],
|
||||
}],
|
||||
},
|
||||
{
|
||||
@ -40,9 +51,7 @@
|
||||
['OS == "win"', {
|
||||
'copies': [{
|
||||
'files': [
|
||||
'<(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',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-pc-windows-msvc/signal_sqlcipher_extension.lib',
|
||||
],
|
||||
'destination': '<(PRODUCT_DIR)',
|
||||
}],
|
||||
@ -56,12 +65,11 @@
|
||||
'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/openssl-include',
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/include',
|
||||
],
|
||||
},
|
||||
'cflags': ['-std=c99', '-w'],
|
||||
@ -77,29 +85,28 @@
|
||||
],
|
||||
'link_settings': {
|
||||
'libraries': [
|
||||
'-llibcrypto.lib',
|
||||
'-llibssl.lib',
|
||||
'-lws2_32.lib',
|
||||
'-lcrypt32.lib'
|
||||
'-luserenv.lib',
|
||||
'-lntdll.lib',
|
||||
'-lbcrypt.lib',
|
||||
'-lcrypt32.lib',
|
||||
'-lsignal_sqlcipher_extension.lib'
|
||||
],
|
||||
'library_dirs': [
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)'
|
||||
'<(PRODUCT_DIR)',
|
||||
]
|
||||
}
|
||||
},
|
||||
'OS == "mac"', {
|
||||
'link_settings': {
|
||||
'libraries': [
|
||||
# This statically links libcrypto, whereas -lcrypto would dynamically link it
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/OpenSSL-mac-<(target_arch)/libcrypto.a'
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-apple-darwin/libsignal_sqlcipher_extension.a',
|
||||
]
|
||||
}
|
||||
},
|
||||
{ # Linux
|
||||
'link_settings': {
|
||||
'libraries': [
|
||||
# This statically links libcrypto, whereas -lcrypto would dynamically link it
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/OpenSSL-linux-<(target_arch)/libcrypto.a'
|
||||
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-unknown-linux-gnu/libsignal_sqlcipher_extension.a',
|
||||
]
|
||||
}
|
||||
}],
|
||||
|
||||
BIN
deps/sqlite3/sqlite3.c
vendored
BIN
deps/sqlite3/sqlite3.c
vendored
Binary file not shown.
BIN
deps/sqlite3/sqlite3.h
vendored
BIN
deps/sqlite3/sqlite3.h
vendored
Binary file not shown.
BIN
deps/sqlite3/sqlite3ext.h
vendored
BIN
deps/sqlite3/sqlite3ext.h
vendored
Binary file not shown.
19
deps/symlink.js
vendored
19
deps/symlink.js
vendored
@ -1,19 +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 filenames = process.argv.slice(4).map(str => path.basename(str));
|
||||
|
||||
/*
|
||||
This creates symlinks inside the <$2> directory, linking to files inside the
|
||||
directory specified by the absolute path <$3>. If no path <$3> is provided,
|
||||
the default path of "./deps/sqlite3" is used. The basenames of the files to
|
||||
link are specified by <$4...>.
|
||||
*/
|
||||
|
||||
for (const filename of filenames) {
|
||||
fs.accessSync(path.join(source, filename));
|
||||
fs.symlinkSync(path.join(source, filename), path.join(dest, filename), 'file');
|
||||
}
|
||||
@ -42,42 +42,42 @@ 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.37.2** with the following [compilation options](https://www.sqlite.org/compile.html):
|
||||
By default, this distribution currently uses SQLite3 **version 3.40.0** with the following [compilation options](https://www.sqlite.org/compile.html):
|
||||
|
||||
```
|
||||
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_MATH_FUNCTIONS
|
||||
SQLITE_ENABLE_DESERIALIZE
|
||||
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
|
||||
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
|
||||
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_OMIT_DEPRECATED
|
||||
SQLITE_OMIT_GET_TABLE
|
||||
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
|
||||
```
|
||||
|
||||
18
docs/conduct.md
Normal file
18
docs/conduct.md
Normal file
@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
147
docs/contribution.md
Normal file
147
docs/contribution.md
Normal file
@ -0,0 +1,147 @@
|
||||
# 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))
|
||||
@ -2,22 +2,40 @@
|
||||
|
||||
If you have trouble installing `better-sqlite3`, follow this checklist:
|
||||
|
||||
1. Make sure you're using nodejs v10.20.1 or later
|
||||
## Install a recent Node.js
|
||||
|
||||
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). 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.
|
||||
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.
|
||||
|
||||
3. If you're using [Electron](https://github.com/electron/electron), try running [`electron-rebuild`](https://www.npmjs.com/package/electron-rebuild)
|
||||
## Install the `node-gyp` toolchain
|
||||
|
||||
4. If you're using Windows, follow these steps. Do them **in this order**, and **don't skip steps**.
|
||||
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.
|
||||
|
||||
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`
|
||||
## 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
|
||||
|
||||
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
Normal file
173
index.d.ts
vendored
Normal file
@ -0,0 +1,173 @@
|
||||
// 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;
|
||||
@ -12,11 +12,6 @@ function Database(filenameGiven, options) {
|
||||
}
|
||||
|
||||
// Apply defaults
|
||||
let buffer;
|
||||
if (Buffer.isBuffer(filenameGiven)) {
|
||||
buffer = filenameGiven;
|
||||
filenameGiven = ':memory:';
|
||||
}
|
||||
if (filenameGiven == null) filenameGiven = '';
|
||||
if (options == null) options = {};
|
||||
|
||||
@ -36,7 +31,7 @@ function Database(filenameGiven, options) {
|
||||
const nativeBindingPath = 'nativeBinding' in options ? options.nativeBinding : null;
|
||||
|
||||
// Validate interpreted options
|
||||
if (readonly && anonymous && !buffer) throw new TypeError('In-memory/temporary databases cannot be readonly');
|
||||
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');
|
||||
@ -51,6 +46,7 @@ function Database(filenameGiven, options) {
|
||||
}
|
||||
if (!addon.isInitialized) {
|
||||
addon.setErrorConstructor(SqliteError);
|
||||
addon.setLogHandler(logHandlerWrap);
|
||||
addon.isInitialized = true;
|
||||
}
|
||||
|
||||
@ -60,25 +56,39 @@ function Database(filenameGiven, options) {
|
||||
}
|
||||
|
||||
Object.defineProperties(this, {
|
||||
[util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null, buffer || null) },
|
||||
[util.cppdb]: { value: new addon.Database(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');
|
||||
Database.prototype.pragma = require('./methods/pragma');
|
||||
Database.prototype.backup = require('./methods/backup');
|
||||
Database.prototype.serialize = require('./methods/serialize');
|
||||
Database.prototype.function = require('./methods/function');
|
||||
Database.prototype.aggregate = require('./methods/aggregate');
|
||||
Database.prototype.table = require('./methods/table');
|
||||
Database.prototype.loadExtension = wrappers.loadExtension;
|
||||
Database.prototype.createFTS5Tokenizer = require('./methods/createFTS5Tokenizer');
|
||||
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;
|
||||
}
|
||||
|
||||
module.exports = Database;
|
||||
|
||||
24
lib/methods/createFTS5Tokenizer.js
Normal file
24
lib/methods/createFTS5Tokenizer.js
Normal file
@ -0,0 +1,24 @@
|
||||
'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;
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
const { cppdb } = require('../util');
|
||||
|
||||
module.exports = function serialize(options) {
|
||||
if (options == null) options = {};
|
||||
|
||||
// Validate arguments
|
||||
if (typeof options !== 'object') throw new TypeError('Expected first argument to be an options object');
|
||||
|
||||
// Interpret and validate options
|
||||
const attachedName = 'attached' in options ? options.attached : 'main';
|
||||
if (typeof attachedName !== 'string') throw new TypeError('Expected the "attached" option to be a string');
|
||||
if (!attachedName) throw new TypeError('The "attached" option cannot be an empty string');
|
||||
|
||||
return this[cppdb].serialize(attachedName);
|
||||
};
|
||||
@ -15,11 +15,6 @@ 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;
|
||||
@ -30,6 +25,10 @@ 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; },
|
||||
|
||||
30
package.json
30
package.json
@ -1,43 +1,41 @@
|
||||
{
|
||||
"name": "better-sqlite3",
|
||||
"version": "7.5.0",
|
||||
"name": "@signalapp/better-sqlite3",
|
||||
"version": "9.0.13",
|
||||
"description": "The fastest and simplest library for SQLite3 in Node.js.",
|
||||
"homepage": "http://github.com/JoshuaWise/better-sqlite3",
|
||||
"homepage": "http://github.com/WiseLibs/better-sqlite3",
|
||||
"author": "Joshua Wise <joshuathomaswise@gmail.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/JoshuaWise/better-sqlite3.git"
|
||||
"url": "git://github.com/WiseLibs/better-sqlite3.git"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"types": "index.d.ts",
|
||||
"files": [
|
||||
"index.d.ts",
|
||||
"binding.gyp",
|
||||
"src/*.[ch]pp",
|
||||
"lib/**",
|
||||
"deps/**"
|
||||
"deps/**",
|
||||
"!deps/sqlcipher.tar.gz",
|
||||
"!deps/unverified.tmp"
|
||||
],
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"tar": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.4",
|
||||
"cli-color": "^2.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"chai": "^4.3.6",
|
||||
"cli-color": "^2.0.2",
|
||||
"fs-extra": "^10.1.0",
|
||||
"mocha": "^8.3.2",
|
||||
"nodemark": "^0.3.0",
|
||||
"sqlite": "^4.0.23",
|
||||
"sqlite3": "^5.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"format": "xcrun clang-format --style=chromium -Werror --verbose -i src/*.cpp src/*.hpp",
|
||||
"test": "mocha --exit --slow=75 --timeout=5000",
|
||||
"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"
|
||||
"benchmark": "node benchmark"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,88 +0,0 @@
|
||||
#hdr
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#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/data-converter.lzz"
|
||||
#insert "util/custom-function.lzz"
|
||||
#insert "util/custom-aggregate.lzz"
|
||||
#insert "util/custom-table.lzz"
|
||||
#insert "util/data.lzz"
|
||||
#insert "util/binder.lzz"
|
||||
|
||||
struct Addon {
|
||||
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;
|
||||
}
|
||||
|
||||
explicit Addon(v8::Isolate* isolate) :
|
||||
privileged_info(NULL),
|
||||
next_id(0),
|
||||
cs(isolate) {}
|
||||
|
||||
inline sqlite3_uint64 NextId() {
|
||||
return next_id++;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
#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, exports->Get(context, InternalizedFromLatin1(isolate, "Statement")).ToLocalChecked().As<v8::Function>());
|
||||
addon->StatementIterator.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "StatementIterator")).ToLocalChecked().As<v8::Function>());
|
||||
addon->Backup.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "Backup")).ToLocalChecked().As<v8::Function>());
|
||||
}
|
||||
#end
|
||||
39
src/local_vector.hpp
Normal file
39
src/local_vector.hpp
Normal file
@ -0,0 +1,39 @@
|
||||
#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_
|
||||
@ -1,138 +0,0 @@
|
||||
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 = (*addon->privileged_info)[0].As<v8::Object>();
|
||||
v8::Local<v8::String> attachedName = (*addon->privileged_info)[1].As<v8::String>();
|
||||
v8::Local<v8::String> destFile = (*addon->privileged_info)[2].As<v8::String>();
|
||||
bool unlink = (*addon->privileged_info)[3].As<v8::Boolean>()->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, addon->cs.totalPages.Get(isolate), v8::Int32::New(isolate, total_pages)).FromJust();
|
||||
result->Set(ctx, addon->cs.remainingPages.Get(isolate), 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;
|
||||
};
|
||||
@ -1,468 +0,0 @@
|
||||
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, "serialize", JS_serialize);
|
||||
SetPrototypeMethod(isolate, data, t, "function", JS_function);
|
||||
SetPrototypeMethod(isolate, data, t, "aggregate", JS_aggregate);
|
||||
SetPrototypeMethod(isolate, data, t, "table", JS_table);
|
||||
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(addon->SqliteError.Get(isolate)
|
||||
->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 = logger.Get(isolate).As<v8::Function>()
|
||||
->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(
|
||||
v8::Isolate* isolate,
|
||||
Addon* addon,
|
||||
sqlite3* db_handle,
|
||||
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);
|
||||
REQUIRE_ARGUMENT_ANY(eighth, v8::Local<v8::Value> buffer);
|
||||
|
||||
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);
|
||||
|
||||
if (node::Buffer::HasInstance(buffer) && !Deserialize(buffer.As<v8::Object>(), addon, db_handle, readonly)) {
|
||||
int status = sqlite3_close(db_handle);
|
||||
assert(status == SQLITE_OK); ((void)status);
|
||||
return;
|
||||
}
|
||||
|
||||
UseContext;
|
||||
Database* db = new Database(isolate, addon, db_handle, 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 = addon->Statement.Get(isolate);
|
||||
addon->privileged_info = &info;
|
||||
v8::MaybeLocal<v8::Object> maybeStatement = c->NewInstance(OnlyContext, 0, NULL);
|
||||
addon->privileged_info = NULL;
|
||||
if (!maybeStatement.IsEmpty()) info.GetReturnValue().Set(maybeStatement.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 = addon->Backup.Get(isolate);
|
||||
addon->privileged_info = &info;
|
||||
v8::MaybeLocal<v8::Object> maybeBackup = c->NewInstance(OnlyContext, 0, NULL);
|
||||
addon->privileged_info = NULL;
|
||||
if (!maybeBackup.IsEmpty()) info.GetReturnValue().Set(maybeBackup.ToLocalChecked());
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_serialize) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> attachedName);
|
||||
REQUIRE_DATABASE_OPEN(db);
|
||||
REQUIRE_DATABASE_NOT_BUSY(db);
|
||||
REQUIRE_DATABASE_NO_ITERATORS(db);
|
||||
|
||||
UseIsolate;
|
||||
v8::String::Utf8Value attached_name(isolate, attachedName);
|
||||
sqlite3_int64 length = -1;
|
||||
unsigned char* data = sqlite3_serialize(db->db_handle, *attached_name, &length, 0);
|
||||
|
||||
if (!data && length) {
|
||||
ThrowError("Out of memory");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(
|
||||
node::Buffer::New(isolate, reinterpret_cast<char*>(data), length, FreeSerialization, NULL).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_ARGUMENT_BOOLEAN(sixth, bool direct_only);
|
||||
REQUIRE_DATABASE_OPEN(db);
|
||||
REQUIRE_DATABASE_NOT_BUSY(db);
|
||||
REQUIRE_DATABASE_NO_ITERATORS(db);
|
||||
|
||||
UseIsolate;
|
||||
v8::String::Utf8Value name(isolate, nameString);
|
||||
int mask = SQLITE_UTF8;
|
||||
if (deterministic) mask |= SQLITE_DETERMINISTIC;
|
||||
if (direct_only) mask |= SQLITE_DIRECTONLY;
|
||||
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, *name, fn, 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_ARGUMENT_BOOLEAN(ninth, bool direct_only);
|
||||
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 = SQLITE_UTF8;
|
||||
if (deterministic) mask |= SQLITE_DETERMINISTIC;
|
||||
if (direct_only) mask |= SQLITE_DIRECTONLY;
|
||||
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, *name, start, step, inverse, result, safe_ints), CustomAggregate::xStep, CustomAggregate::xFinal, xValue, xInverse, CustomAggregate::xDestroy) != SQLITE_OK) {
|
||||
db->ThrowDatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
NODE_METHOD(JS_table) {
|
||||
Database* db = Unwrap<Database>(info.This());
|
||||
REQUIRE_ARGUMENT_FUNCTION(first, v8::Local<v8::Function> factory);
|
||||
REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> nameString);
|
||||
REQUIRE_ARGUMENT_BOOLEAN(third, bool eponymous);
|
||||
REQUIRE_DATABASE_OPEN(db);
|
||||
REQUIRE_DATABASE_NOT_BUSY(db);
|
||||
REQUIRE_DATABASE_NO_ITERATORS(db);
|
||||
|
||||
UseIsolate;
|
||||
v8::String::Utf8Value name(isolate, nameString);
|
||||
sqlite3_module* module = eponymous ? &CustomTable::EPONYMOUS_MODULE : &CustomTable::MODULE;
|
||||
|
||||
db->busy = true;
|
||||
if (sqlite3_create_module_v2(db->db_handle, *name, module, new CustomTable(isolate, db, *name, factory), CustomTable::Destructor) != SQLITE_OK) {
|
||||
db->ThrowDatabaseError();
|
||||
}
|
||||
db->busy = false;
|
||||
}
|
||||
|
||||
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 bool Deserialize(v8::Local<v8::Object> buffer, Addon* addon, sqlite3* db_handle, bool readonly) {
|
||||
size_t length = node::Buffer::Length(buffer);
|
||||
unsigned char* data = (unsigned char*)sqlite3_malloc64(length);
|
||||
unsigned int flags = SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE;
|
||||
|
||||
if (readonly) {
|
||||
flags |= SQLITE_DESERIALIZE_READONLY;
|
||||
}
|
||||
if (length) {
|
||||
if (!data) {
|
||||
ThrowError("Out of memory");
|
||||
return false;
|
||||
}
|
||||
memcpy(data, node::Buffer::Data(buffer), length);
|
||||
}
|
||||
|
||||
int status = sqlite3_deserialize(db_handle, "main", data, length, length, flags);
|
||||
if (status != SQLITE_OK) {
|
||||
ThrowSqliteError(addon, status == SQLITE_ERROR ? "unable to deserialize database" : sqlite3_errstr(status), status);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void FreeSerialization(char* data, void* _) {
|
||||
sqlite3_free(data);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@ -1,138 +0,0 @@
|
||||
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 revert 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, addon->cs.value.Get(isolate), value).FromJust();
|
||||
record->Set(ctx, addon->cs.done.Get(isolate), 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;
|
||||
};
|
||||
@ -1,329 +0,0 @@
|
||||
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);
|
||||
SetPrototypeGetter(isolate, data, t, "busy", JS_busy);
|
||||
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 = (*addon->privileged_info)[0].As<v8::String>();
|
||||
v8::Local<v8::Object> database = (*addon->privileged_info)[1].As<v8::Object>();
|
||||
bool pragmaMode = (*addon->privileged_info)[2].As<v8::Boolean>()->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_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.readonly, v8::Boolean::New(isolate, sqlite3_stmt_readonly(handle) != 0));
|
||||
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(ALLOW_ANY_STATEMENT, 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, addon->cs.changes.Get(isolate), v8::Int32::New(isolate, changes)).FromJust();
|
||||
result->Set(ctx, addon->cs.lastInsertRowid.Get(isolate),
|
||||
stmt->safe_ints
|
||||
? v8::BigInt::New(isolate, id).As<v8::Value>()
|
||||
: v8::Number::New(isolate, (double)id).As<v8::Value>()
|
||||
).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 = addon->StatementIterator.Get(isolate);
|
||||
addon->privileged_info = &info;
|
||||
v8::MaybeLocal<v8::Object> maybeIterator = c->NewInstance(OnlyContext, 0, NULL);
|
||||
addon->privileged_info = NULL;
|
||||
if (!maybeIterator.IsEmpty()) info.GetReturnValue().Set(maybeIterator.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 = addon->cs.name.Get(isolate);
|
||||
v8::Local<v8::String> columnName = addon->cs.column.Get(isolate);
|
||||
v8::Local<v8::String> tableName = addon->cs.table.Get(isolate);
|
||||
v8::Local<v8::String> databaseName = addon->cs.database.Get(isolate);
|
||||
v8::Local<v8::String> typeName = addon->cs.type.Get(isolate);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
NODE_GETTER(JS_busy) {
|
||||
Statement* stmt = Unwrap<Statement>(info.This());
|
||||
info.GetReturnValue().Set(stmt->alive && stmt->locked);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@ -1,73 +0,0 @@
|
||||
class BindMap {
|
||||
public:
|
||||
|
||||
// This nested class represents a single 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 name.Get(isolate);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@ -1,193 +0,0 @@
|
||||
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, (std::string("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, arg.As<v8::Array>());
|
||||
if (!success) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg->IsObject() && !node::Buffer::HasInstance(arg)) {
|
||||
v8::Local<v8::Object> obj = arg.As<v8::Object>();
|
||||
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;
|
||||
} else if (stmt->GetBindMap(isolate)->GetSize()) {
|
||||
Fail(ThrowTypeError, "Named parameters can only be passed within plain objects");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
};
|
||||
@ -1,151 +0,0 @@
|
||||
class CS {
|
||||
public:
|
||||
|
||||
v8::Local<v8::String> Code(v8::Isolate* isolate, int code) {
|
||||
auto element = codes.find(code);
|
||||
if (element != codes.end()) return element->second.Get(isolate);
|
||||
return StringFromUtf8(isolate, (std::string("UNKNOWN_SQLITE_ERROR_") + std::to_string(code)).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;
|
||||
};
|
||||
@ -1,121 +0,0 @@
|
||||
class CustomAggregate : public CustomFunction {
|
||||
public:
|
||||
|
||||
explicit CustomAggregate(
|
||||
v8::Isolate* isolate,
|
||||
Database* db,
|
||||
const char* name,
|
||||
v8::Local<v8::Value> start,
|
||||
v8::Local<v8::Function> step,
|
||||
v8::Local<v8::Value> inverse,
|
||||
v8::Local<v8::Value> result,
|
||||
bool safe_ints
|
||||
) :
|
||||
CustomFunction(isolate, db, name, step, safe_ints),
|
||||
invoke_result(result->IsFunction()),
|
||||
invoke_start(start->IsFunction()),
|
||||
inverse(isolate, inverse->IsFunction() ? inverse.As<v8::Function>() : v8::Local<v8::Function>()),
|
||||
result(isolate, result->IsFunction() ? result.As<v8::Function>() : 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] = acc->value.Get(isolate);
|
||||
if (argc != 0) Data::GetArgumentsJS(isolate, args + 1, argv, argc, self->safe_ints);
|
||||
|
||||
v8::MaybeLocal<v8::Value> maybeReturnValue = (self->*ptrtm).Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), argc + 1, args);
|
||||
if (args != args_fast) delete[] args;
|
||||
|
||||
if (maybeReturnValue.IsEmpty()) {
|
||||
self->PropagateJSError(invocation);
|
||||
} else {
|
||||
v8::Local<v8::Value> returnValue = maybeReturnValue.ToLocalChecked();
|
||||
if (!returnValue->IsUndefined()) acc->value.Reset(isolate, returnValue);
|
||||
}
|
||||
}
|
||||
|
||||
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 = acc->value.Get(isolate);
|
||||
if (self->invoke_result) {
|
||||
v8::MaybeLocal<v8::Value> maybeResult = self->result.Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), 1, &result);
|
||||
if (maybeResult.IsEmpty()) {
|
||||
self->PropagateJSError(invocation);
|
||||
return;
|
||||
}
|
||||
result = maybeResult.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> maybeSeed = start.Get(isolate).As<v8::Function>()->Call(OnlyContext, v8::Undefined(isolate), 0, NULL);
|
||||
if (maybeSeed.IsEmpty()) PropagateJSError(invocation);
|
||||
else acc->value.Reset(isolate, maybeSeed.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;
|
||||
};
|
||||
@ -1,59 +0,0 @@
|
||||
class CustomFunction : protected DataConverter {
|
||||
public:
|
||||
|
||||
explicit CustomFunction(
|
||||
v8::Isolate* isolate,
|
||||
Database* db,
|
||||
const char* name,
|
||||
v8::Local<v8::Function> fn,
|
||||
bool safe_ints
|
||||
) :
|
||||
name(name),
|
||||
db(db),
|
||||
isolate(isolate),
|
||||
fn(isolate, fn),
|
||||
safe_ints(safe_ints) {}
|
||||
|
||||
virtual ~CustomFunction() {}
|
||||
|
||||
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> maybeReturnValue = self->fn.Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), argc, args);
|
||||
if (args != args_fast) delete[] args;
|
||||
|
||||
if (maybeReturnValue.IsEmpty()) self->PropagateJSError(invocation);
|
||||
else Data::ResultValueFromJS(isolate, invocation, maybeReturnValue.ToLocalChecked(), self);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
void PropagateJSError(sqlite3_context* invocation) {
|
||||
assert(db->GetState()->was_js_error == false);
|
||||
db->GetState()->was_js_error = true;
|
||||
sqlite3_result_error(invocation, "", 0);
|
||||
}
|
||||
|
||||
std::string GetDataErrorPrefix() {
|
||||
return std::string("User-defined function ") + name + "() returned";
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string name;
|
||||
Database* const db;
|
||||
protected:
|
||||
v8::Isolate* const isolate;
|
||||
const CopyablePersistent<v8::Function> fn;
|
||||
const bool safe_ints;
|
||||
};
|
||||
@ -1,397 +0,0 @@
|
||||
class CustomTable {
|
||||
public:
|
||||
|
||||
explicit CustomTable(
|
||||
v8::Isolate* isolate,
|
||||
Database* db,
|
||||
const char* name,
|
||||
v8::Local<v8::Function> factory
|
||||
) :
|
||||
addon(db->GetAddon()),
|
||||
isolate(isolate),
|
||||
db(db),
|
||||
name(name),
|
||||
factory(isolate, factory) {}
|
||||
|
||||
static void Destructor(void* self) {
|
||||
delete static_cast<CustomTable*>(self);
|
||||
}
|
||||
|
||||
static sqlite3_module MODULE = {
|
||||
0, /* iVersion */
|
||||
xCreate, /* xCreate */
|
||||
xConnect, /* xConnect */
|
||||
xBestIndex, /* xBestIndex */
|
||||
xDisconnect, /* xDisconnect */
|
||||
xDisconnect, /* xDestroy */
|
||||
xOpen, /* xOpen */
|
||||
xClose, /* xClose */
|
||||
xFilter, /* xFilter */
|
||||
xNext, /* xNext */
|
||||
xEof, /* xEof */
|
||||
xColumn, /* xColumn */
|
||||
xRowid, /* xRowid */
|
||||
NULL, /* xUpdate */
|
||||
NULL, /* xBegin */
|
||||
NULL, /* xSync */
|
||||
NULL, /* xCommit */
|
||||
NULL, /* xRollback */
|
||||
NULL, /* xFindMethod */
|
||||
NULL, /* xRename */
|
||||
NULL, /* xSavepoint */
|
||||
NULL, /* xRelease */
|
||||
NULL, /* xRollbackTo */
|
||||
NULL /* xShadowName */
|
||||
};
|
||||
|
||||
static sqlite3_module EPONYMOUS_MODULE = {
|
||||
0, /* iVersion */
|
||||
NULL, /* xCreate */
|
||||
xConnect, /* xConnect */
|
||||
xBestIndex, /* xBestIndex */
|
||||
xDisconnect, /* xDisconnect */
|
||||
xDisconnect, /* xDestroy */
|
||||
xOpen, /* xOpen */
|
||||
xClose, /* xClose */
|
||||
xFilter, /* xFilter */
|
||||
xNext, /* xNext */
|
||||
xEof, /* xEof */
|
||||
xColumn, /* xColumn */
|
||||
xRowid, /* xRowid */
|
||||
NULL, /* xUpdate */
|
||||
NULL, /* xBegin */
|
||||
NULL, /* xSync */
|
||||
NULL, /* xCommit */
|
||||
NULL, /* xRollback */
|
||||
NULL, /* xFindMethod */
|
||||
NULL, /* xRename */
|
||||
NULL, /* xSavepoint */
|
||||
NULL, /* xRelease */
|
||||
NULL, /* xRollbackTo */
|
||||
NULL /* xShadowName */
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
// This nested class is instantiated on each CREATE VIRTUAL TABLE statement.
|
||||
class VTab { friend class CustomTable;
|
||||
explicit VTab(
|
||||
CustomTable* parent,
|
||||
v8::Local<v8::Function> generator,
|
||||
std::vector<std::string> parameter_names,
|
||||
bool safe_ints
|
||||
) :
|
||||
parent(parent),
|
||||
parameter_count(parameter_names.size()),
|
||||
safe_ints(safe_ints),
|
||||
generator(parent->isolate, generator),
|
||||
parameter_names(parameter_names) {
|
||||
((void)base);
|
||||
}
|
||||
|
||||
static inline CustomTable::VTab* Upcast(sqlite3_vtab* vtab) {
|
||||
return reinterpret_cast<VTab*>(vtab);
|
||||
}
|
||||
|
||||
inline sqlite3_vtab* Downcast() {
|
||||
return reinterpret_cast<sqlite3_vtab*>(this);
|
||||
}
|
||||
|
||||
sqlite3_vtab base;
|
||||
CustomTable * const parent;
|
||||
const int parameter_count;
|
||||
const bool safe_ints;
|
||||
const CopyablePersistent<v8::Function> generator;
|
||||
const std::vector<std::string> parameter_names;
|
||||
};
|
||||
|
||||
// This nested class is instantiated each time a virtual table is scanned.
|
||||
class Cursor { friend class CustomTable;
|
||||
static inline CustomTable::Cursor* Upcast(sqlite3_vtab_cursor* cursor) {
|
||||
return reinterpret_cast<Cursor*>(cursor);
|
||||
}
|
||||
|
||||
inline sqlite3_vtab_cursor* Downcast() {
|
||||
return reinterpret_cast<sqlite3_vtab_cursor*>(this);
|
||||
}
|
||||
|
||||
inline CustomTable::VTab* GetVTab() {
|
||||
return VTab::Upcast(base.pVtab);
|
||||
}
|
||||
|
||||
sqlite3_vtab_cursor base;
|
||||
CopyablePersistent<v8::Object> iterator;
|
||||
CopyablePersistent<v8::Function> next;
|
||||
CopyablePersistent<v8::Array> row;
|
||||
bool done;
|
||||
sqlite_int64 rowid;
|
||||
};
|
||||
|
||||
// This nested class is used by Data::ResultValueFromJS to report errors.
|
||||
class TempDataConverter : DataConverter { friend class CustomTable;
|
||||
explicit TempDataConverter(CustomTable* parent) :
|
||||
parent(parent),
|
||||
status(SQLITE_OK) {}
|
||||
|
||||
void PropagateJSError(sqlite3_context* invocation) {
|
||||
status = SQLITE_ERROR;
|
||||
parent->PropagateJSError();
|
||||
}
|
||||
|
||||
std::string GetDataErrorPrefix() {
|
||||
return std::string("Virtual table module \"") + parent->name + "\" yielded";
|
||||
}
|
||||
|
||||
CustomTable * const parent;
|
||||
int status;
|
||||
};
|
||||
|
||||
// Although this function does nothing, we cannot use xConnect directly,
|
||||
// because that would cause SQLite to register an eponymous virtual table.
|
||||
static int xCreate(sqlite3* db_handle, void* _self, int argc, const char* const * argv, sqlite3_vtab** output, char** errOutput) {
|
||||
return xConnect(db_handle, _self, argc, argv, output, errOutput);
|
||||
}
|
||||
|
||||
// This method uses the factory function to instantiate a new virtual table.
|
||||
static int xConnect(sqlite3* db_handle, void* _self, int argc, const char* const * argv, sqlite3_vtab** output, char** errOutput) {
|
||||
CustomTable* self = static_cast<CustomTable*>(_self);
|
||||
v8::Isolate* isolate = self->isolate;
|
||||
v8::HandleScope scope(isolate);
|
||||
UseContext;
|
||||
|
||||
v8::Local<v8::Value>* args = ALLOC_ARRAY<v8::Local<v8::Value>>(argc);
|
||||
for (int i = 0; i < argc; ++i) {
|
||||
args[i] = StringFromUtf8(isolate, argv[i], -1);
|
||||
}
|
||||
|
||||
// Run the factory function to receive a new virtual table definition.
|
||||
v8::MaybeLocal<v8::Value> maybeReturnValue = self->factory.Get(isolate)->Call(ctx, v8::Undefined(isolate), argc, args);
|
||||
delete[] args;
|
||||
|
||||
if (maybeReturnValue.IsEmpty()) {
|
||||
self->PropagateJSError();
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
|
||||
// Extract each part of the virtual table definition.
|
||||
v8::Local<v8::Array> returnValue = maybeReturnValue.ToLocalChecked().As<v8::Array>();
|
||||
v8::Local<v8::String> sqlString = returnValue->Get(ctx, 0).ToLocalChecked().As<v8::String>();
|
||||
v8::Local<v8::Function> generator = returnValue->Get(ctx, 1).ToLocalChecked().As<v8::Function>();
|
||||
v8::Local<v8::Array> parameterNames = returnValue->Get(ctx, 2).ToLocalChecked().As<v8::Array>();
|
||||
int safe_ints = returnValue->Get(ctx, 3).ToLocalChecked().As<v8::Int32>()->Value();
|
||||
bool direct_only = returnValue->Get(ctx, 4).ToLocalChecked().As<v8::Boolean>()->Value();
|
||||
|
||||
v8::String::Utf8Value sql(isolate, sqlString);
|
||||
safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(self->db->GetState()->safe_ints);
|
||||
|
||||
// Copy the parameter names into a std::vector.
|
||||
std::vector<std::string> parameter_names;
|
||||
for (int i = 0, len = parameterNames->Length(); i < len; ++i) {
|
||||
v8::Local<v8::String> parameterName = parameterNames->Get(ctx, i).ToLocalChecked().As<v8::String>();
|
||||
v8::String::Utf8Value parameter_name(isolate, parameterName);
|
||||
parameter_names.emplace_back(*parameter_name);
|
||||
}
|
||||
|
||||
// Pass our SQL table definition to SQLite (this should never fail).
|
||||
if (sqlite3_declare_vtab(db_handle, *sql) != SQLITE_OK) {
|
||||
*errOutput = sqlite3_mprintf("failed to declare virtual table \"%s\"", argv[2]);
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
if (direct_only && sqlite3_vtab_config(db_handle, SQLITE_VTAB_DIRECTONLY) != SQLITE_OK) {
|
||||
*errOutput = sqlite3_mprintf("failed to configure virtual table \"%s\"", argv[2]);
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
|
||||
// Return the successfully created virtual table.
|
||||
*output = (new VTab(self, generator, parameter_names, safe_ints))->Downcast();
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
static int xDisconnect(sqlite3_vtab* vtab) {
|
||||
delete VTab::Upcast(vtab);
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
static int xOpen(sqlite3_vtab* vtab, sqlite3_vtab_cursor** output) {
|
||||
*output = (new Cursor())->Downcast();
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
static int xClose(sqlite3_vtab_cursor* cursor) {
|
||||
delete Cursor::Upcast(cursor);
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
// This method uses a fresh cursor to start a new scan of a virtual table.
|
||||
// The args and idxNum are provided by xBestIndex (idxStr is unused).
|
||||
// idxNum is a bitmap that provides the proper indices of the received args.
|
||||
static int xFilter(sqlite3_vtab_cursor* _cursor, int idxNum, const char* idxStr, int argc, sqlite3_value** argv) {
|
||||
Cursor* cursor = Cursor::Upcast(_cursor);
|
||||
VTab* vtab = cursor->GetVTab();
|
||||
CustomTable* self = vtab->parent;
|
||||
Addon* addon = self->addon;
|
||||
v8::Isolate* isolate = self->isolate;
|
||||
v8::HandleScope scope(isolate);
|
||||
UseContext;
|
||||
|
||||
// Convert the SQLite arguments into JavaScript arguments. Note that
|
||||
// the values in argv may be in the wrong order, so we fix that here.
|
||||
v8::Local<v8::Value> args_fast[4];
|
||||
v8::Local<v8::Value>* args = NULL;
|
||||
int parameter_count = vtab->parameter_count;
|
||||
if (parameter_count != 0) {
|
||||
args = parameter_count <= 4 ? args_fast : ALLOC_ARRAY<v8::Local<v8::Value>>(parameter_count);
|
||||
int argn = 0;
|
||||
bool safe_ints = vtab->safe_ints;
|
||||
for (int i = 0; i < parameter_count; ++i) {
|
||||
if (idxNum & 1 << i) {
|
||||
args[i] = Data::GetValueJS(isolate, argv[argn++], safe_ints);
|
||||
// If any arguments are NULL, the result set is necessarily
|
||||
// empty, so don't bother to run the generator function.
|
||||
if (args[i]->IsNull()) {
|
||||
if (args != args_fast) delete[] args;
|
||||
cursor->done = true;
|
||||
return SQLITE_OK;
|
||||
}
|
||||
} else {
|
||||
args[i] = v8::Undefined(isolate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the generator function to create a new iterator.
|
||||
v8::MaybeLocal<v8::Value> maybeIterator = vtab->generator.Get(isolate)->Call(ctx, v8::Undefined(isolate), parameter_count, args);
|
||||
if (args != args_fast) delete[] args;
|
||||
|
||||
if (maybeIterator.IsEmpty()) {
|
||||
self->PropagateJSError();
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
|
||||
// Store the iterator and its next() method; we'll be using it a lot.
|
||||
v8::Local<v8::Object> iterator = maybeIterator.ToLocalChecked().As<v8::Object>();
|
||||
v8::Local<v8::Function> next = iterator->Get(ctx, addon->cs.next.Get(isolate)).ToLocalChecked().As<v8::Function>();
|
||||
cursor->iterator.Reset(isolate, iterator);
|
||||
cursor->next.Reset(isolate, next);
|
||||
cursor->rowid = 0;
|
||||
|
||||
// Advance the iterator/cursor to the first row.
|
||||
return xNext(cursor->Downcast());
|
||||
}
|
||||
|
||||
// This method advances a virtual table's cursor to the next row.
|
||||
// SQLite will call this method repeatedly, driving the generator function.
|
||||
static int xNext(sqlite3_vtab_cursor* _cursor) {
|
||||
Cursor* cursor = Cursor::Upcast(_cursor);
|
||||
CustomTable* self = cursor->GetVTab()->parent;
|
||||
Addon* addon = self->addon;
|
||||
v8::Isolate* isolate = self->isolate;
|
||||
v8::HandleScope scope(isolate);
|
||||
UseContext;
|
||||
|
||||
v8::Local<v8::Object> iterator = cursor->iterator.Get(isolate);
|
||||
v8::Local<v8::Function> next = cursor->next.Get(isolate);
|
||||
|
||||
v8::MaybeLocal<v8::Value> maybeRecord = next->Call(ctx, iterator, 0, NULL);
|
||||
if (maybeRecord.IsEmpty()) {
|
||||
self->PropagateJSError();
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
|
||||
v8::Local<v8::Object> record = maybeRecord.ToLocalChecked().As<v8::Object>();
|
||||
bool done = record->Get(ctx, addon->cs.done.Get(isolate)).ToLocalChecked().As<v8::Boolean>()->Value();
|
||||
if (!done) {
|
||||
cursor->row.Reset(isolate, record->Get(ctx, addon->cs.value.Get(isolate)).ToLocalChecked().As<v8::Array>());
|
||||
}
|
||||
cursor->done = done;
|
||||
cursor->rowid += 1;
|
||||
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
// If this method returns 1, SQLite will stop scanning the virtual table.
|
||||
static int xEof(sqlite3_vtab_cursor* cursor) {
|
||||
return Cursor::Upcast(cursor)->done;
|
||||
}
|
||||
|
||||
// This method extracts some column from the cursor's current row.
|
||||
static int xColumn(sqlite3_vtab_cursor* _cursor, sqlite3_context* invocation, int column) {
|
||||
Cursor* cursor = Cursor::Upcast(_cursor);
|
||||
CustomTable* self = cursor->GetVTab()->parent;
|
||||
TempDataConverter temp_data_converter(self);
|
||||
v8::Isolate* isolate = self->isolate;
|
||||
v8::HandleScope scope(isolate);
|
||||
|
||||
v8::Local<v8::Array> row = cursor->row.Get(isolate);
|
||||
v8::MaybeLocal<v8::Value> maybeColumnValue = row->Get(OnlyContext, column);
|
||||
if (maybeColumnValue.IsEmpty()) {
|
||||
temp_data_converter.PropagateJSError(NULL);
|
||||
} else {
|
||||
Data::ResultValueFromJS(isolate, invocation, maybeColumnValue.ToLocalChecked(), &temp_data_converter);
|
||||
}
|
||||
return temp_data_converter.status;
|
||||
}
|
||||
|
||||
// This method outputs the rowid of the cursor's current row.
|
||||
static int xRowid(sqlite3_vtab_cursor* cursor, sqlite_int64* output) {
|
||||
*output = Cursor::Upcast(cursor)->rowid;
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
// This method tells SQLite how to *plan* queries on our virtual table.
|
||||
// It gets invoked (typically multiple times) during db.prepare().
|
||||
static int xBestIndex(sqlite3_vtab* vtab, sqlite3_index_info* output) {
|
||||
int parameter_count = VTab::Upcast(vtab)->parameter_count;
|
||||
int argument_count = 0;
|
||||
std::vector<std::pair<int, int>> forwarded;
|
||||
|
||||
for (int i = 0, len = output->nConstraint; i < len; ++i) {
|
||||
auto item = output->aConstraint[i];
|
||||
|
||||
// We only care about constraints on parameters, not regular columns.
|
||||
if (item.iColumn >= 0 && item.iColumn < parameter_count) {
|
||||
if (item.op != SQLITE_INDEX_CONSTRAINT_EQ) {
|
||||
sqlite3_free(vtab->zErrMsg);
|
||||
vtab->zErrMsg = sqlite3_mprintf(
|
||||
"virtual table parameter \"%s\" can only be constrained by the '=' operator",
|
||||
VTab::Upcast(vtab)->parameter_names.at(item.iColumn).c_str());
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
if (!item.usable) {
|
||||
// Don't allow SQLite to make plans that ignore arguments.
|
||||
// Otherwise, a user could pass arguments, but then they
|
||||
// could appear undefined in the generator function.
|
||||
return SQLITE_CONSTRAINT;
|
||||
}
|
||||
forwarded.emplace_back(item.iColumn, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Tell SQLite to forward arguments to xFilter.
|
||||
std::sort(forwarded.begin(), forwarded.end());
|
||||
for (std::pair<int, int> pair : forwarded) {
|
||||
int bit = 1 << pair.first;
|
||||
if (!(output->idxNum & bit)) {
|
||||
output->idxNum |= bit;
|
||||
output->aConstraintUsage[pair.second].argvIndex = ++argument_count;
|
||||
output->aConstraintUsage[pair.second].omit = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Use a very high estimated cost so SQLite is not tempted to invoke the
|
||||
// generator function within a loop, if it can be avoided.
|
||||
output->estimatedCost = output->estimatedRows = 1000000000 / (argument_count + 1);
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
void PropagateJSError() {
|
||||
assert(db->GetState()->was_js_error == false);
|
||||
db->GetState()->was_js_error = true;
|
||||
}
|
||||
|
||||
Addon* const addon;
|
||||
v8::Isolate* const isolate;
|
||||
Database* const db;
|
||||
const std::string name;
|
||||
const CopyablePersistent<v8::Function> factory;
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
class DataConverter {
|
||||
public:
|
||||
|
||||
void ThrowDataConversionError(sqlite3_context* invocation, bool isBigInt) {
|
||||
if (isBigInt) {
|
||||
ThrowRangeError((GetDataErrorPrefix() + " a bigint that was too big").c_str());
|
||||
} else {
|
||||
ThrowTypeError((GetDataErrorPrefix() + " an invalid value").c_str());
|
||||
}
|
||||
PropagateJSError(invocation);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
virtual void PropagateJSError(sqlite3_context* invocation) = 0;
|
||||
virtual std::string GetDataErrorPrefix() = 0;
|
||||
};
|
||||
@ -1,145 +0,0 @@
|
||||
#define JS_VALUE_TO_SQLITE(to, value, isolate, ...) \
|
||||
if (value->IsNumber()) { \
|
||||
return sqlite3_##to##_double( \
|
||||
__VA_ARGS__, \
|
||||
value.As<v8::Number>()->Value() \
|
||||
); \
|
||||
} else if (value->IsBigInt()) { \
|
||||
bool lossless; \
|
||||
int64_t v = value.As<v8::BigInt>()->Int64Value(&lossless); \
|
||||
if (lossless) { \
|
||||
return sqlite3_##to##_int64(__VA_ARGS__, v); \
|
||||
} \
|
||||
} else if (value->IsString()) { \
|
||||
v8::String::Utf8Value utf8(isolate, value.As<v8::String>()); \
|
||||
return sqlite3_##to##_text( \
|
||||
__VA_ARGS__, \
|
||||
*utf8, \
|
||||
utf8.length(), \
|
||||
SQLITE_TRANSIENT \
|
||||
); \
|
||||
} else if (node::Buffer::HasInstance(value)) { \
|
||||
const char* data = node::Buffer::Data(value); \
|
||||
return sqlite3_##to##_blob( \
|
||||
__VA_ARGS__, \
|
||||
data ? data : "", \
|
||||
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()) {
|
||||
row->Get(ctx, table).ToLocalChecked().As<v8::Object>()->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, DataConverter* converter) {
|
||||
JS_VALUE_TO_SQLITE(result, value, isolate, invocation);
|
||||
converter->ThrowDataConversionError(invocation, value->IsBigInt());
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,159 +0,0 @@
|
||||
#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*>(info.Data().As<v8::External>()->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, key.Get(isolate), 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 = (info[at()].As<v8::Type>())__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
|
||||
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
#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() \
|
||||
REQUIRE_STATEMENT_NOT_LOCKED(stmt); \
|
||||
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 ALLOW_ANY_STATEMENT() \
|
||||
((void)0)
|
||||
|
||||
|
||||
#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
|
||||
@ -668,4 +668,28 @@ describe('Database#table()', function () {
|
||||
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,75 +0,0 @@
|
||||
'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,81 +0,0 @@
|
||||
'use strict';
|
||||
const Database = require('../.');
|
||||
|
||||
describe('Database#serialize()', function () {
|
||||
beforeEach(function () {
|
||||
this.db = new Database(util.next());
|
||||
this.db.prepare("CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)").run();
|
||||
this.seed = () => {
|
||||
this.db.prepare("INSERT INTO entries WITH RECURSIVE temp(a, b, c, d, e) AS (SELECT 'foo', 1, 3.14, x'dddddddd', NULL UNION ALL SELECT a, b + 1, c, d, e FROM temp LIMIT 1000) SELECT * FROM temp").run();
|
||||
};
|
||||
});
|
||||
afterEach(function () {
|
||||
this.db.close();
|
||||
});
|
||||
|
||||
it('should serialize the database and return a buffer', async function () {
|
||||
let buffer = this.db.serialize();
|
||||
expect(buffer).to.be.an.instanceof(Buffer);
|
||||
expect(buffer.length).to.be.above(1000);
|
||||
const lengthBefore = buffer.length;
|
||||
this.seed();
|
||||
buffer = this.db.serialize();
|
||||
expect(buffer).to.be.an.instanceof(Buffer);
|
||||
expect(buffer.length).to.be.above(lengthBefore);
|
||||
});
|
||||
it('should return a buffer that can be used by the Database constructor', async function () {
|
||||
this.seed();
|
||||
const buffer = this.db.serialize();
|
||||
expect(buffer).to.be.an.instanceof(Buffer);
|
||||
expect(buffer.length).to.be.above(1000);
|
||||
this.db.prepare('delete from entries').run();
|
||||
this.db.close();
|
||||
this.db = new Database(buffer);
|
||||
const bufferCopy = this.db.serialize();
|
||||
expect(buffer.length).to.equal(bufferCopy.length);
|
||||
expect(buffer).to.deep.equal(bufferCopy);
|
||||
this.db.prepare('insert into entries (rowid, a, b) values (?, ?, ?)').run(0, 'bar', -999);
|
||||
expect(this.db.prepare('select a, b from entries order by rowid limit 2').all())
|
||||
.to.deep.equal([{ a: 'bar', b: -999 }, { a: 'foo', b: 1 }]);
|
||||
});
|
||||
it('should accept the "attached" option', async function () {
|
||||
const smallBuffer = this.db.serialize();
|
||||
this.seed();
|
||||
const bigBuffer = this.db.serialize();
|
||||
this.db.close();
|
||||
this.db = new Database();
|
||||
this.db.prepare('attach ? as other').run(util.current());
|
||||
const smallBuffer2 = this.db.serialize();
|
||||
const bigBuffer2 = this.db.serialize({ attached: 'other' });
|
||||
expect(bigBuffer.length === bigBuffer2.length);
|
||||
expect(bigBuffer).to.deep.equal(bigBuffer2);
|
||||
expect(smallBuffer.length < bigBuffer.length);
|
||||
expect(smallBuffer2.length < bigBuffer.length);
|
||||
expect(smallBuffer).to.not.deep.equal(smallBuffer2);
|
||||
});
|
||||
it('should return a buffer that can be opened with the "readonly" option', async function () {
|
||||
this.seed();
|
||||
const buffer = this.db.serialize();
|
||||
expect(buffer).to.be.an.instanceof(Buffer);
|
||||
expect(buffer.length).to.be.above(1000);
|
||||
this.db.close();
|
||||
this.db = new Database(buffer, { readonly: true });
|
||||
expect(() => this.db.prepare('insert into entries (rowid, a, b) values (?, ?, ?)').run(0, 'bar', -999))
|
||||
.to.throw(Database.SqliteError);
|
||||
expect(this.db.prepare('select a, b from entries order by rowid limit 2').all())
|
||||
.to.deep.equal([{ a: 'foo', b: 1 }, { a: 'foo', b: 2 }]);
|
||||
const bufferCopy = this.db.serialize();
|
||||
expect(buffer.length).to.equal(bufferCopy.length);
|
||||
expect(buffer).to.deep.equal(bufferCopy);
|
||||
});
|
||||
it('should work with an empty database', async function () {
|
||||
this.db.close();
|
||||
this.db = new Database();
|
||||
const buffer = this.db.serialize();
|
||||
expect(buffer).to.be.an.instanceof(Buffer);
|
||||
expect(buffer.length).to.equal(0);
|
||||
this.db.close();
|
||||
this.db = new Database(buffer);
|
||||
expect(this.db.serialize().length).to.equal(0);
|
||||
});
|
||||
});
|
||||
78
test/38.database.js-tokenizer.js
Normal file
78
test/38.database.js-tokenizer.js
Normal file
@ -0,0 +1,78 @@
|
||||
'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?"]);
|
||||
});
|
||||
});
|
||||
71
test/39.database.signal-tokenizer.js
Normal file
71
test/39.database.signal-tokenizer.js
Normal file
@ -0,0 +1,71 @@
|
||||
'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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -187,33 +187,6 @@ describe('integrity checks', 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 () {
|
||||
whileIterating(this, blocked(() => this.db.loadExtension(filepath)));
|
||||
normally(allowed(() => this.db.loadExtension(filepath)));
|
||||
});
|
||||
specify('while busy (blocked)', function () {
|
||||
whileBusy(this, blocked(() => this.db.loadExtension(filepath)));
|
||||
normally(allowed(() => this.db.loadExtension(filepath)));
|
||||
});
|
||||
specify('while closed (blocked)', function () {
|
||||
whileClosed(this, blocked(() => this.db.loadExtension(filepath)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Database#close()', function () {
|
||||
specify('while iterating (blocked)', function () {
|
||||
whileIterating(this, blocked(() => this.db.close()));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user