├── .gitattributes ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── bump-version.yml │ └── update-sqlite.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── benchmark ├── benchmark.js ├── drivers.js ├── index.js ├── seed.js ├── trials.js └── types │ ├── insert.js │ ├── select-all.js │ ├── select-iterate.js │ ├── select.js │ └── transaction.js ├── binding.gyp ├── deps ├── common.gypi ├── copy.js ├── defines.gypi ├── download.sh ├── patches │ └── 1208.patch ├── sqlite3.gyp ├── sqlite3 │ ├── sqlite3.c │ ├── sqlite3.h │ └── sqlite3ext.h └── test_extension.c ├── docs ├── api.md ├── benchmark.md ├── compilation.md ├── conduct.md ├── contribution.md ├── integer.md ├── performance.md ├── threads.md ├── tips.md ├── troubleshooting.md └── unsafe.md ├── lib ├── database.js ├── index.js ├── methods │ ├── aggregate.js │ ├── backup.js │ ├── function.js │ ├── inspect.js │ ├── pragma.js │ ├── serialize.js │ ├── table.js │ ├── transaction.js │ └── wrappers.js ├── sqlite-error.js └── util.js ├── package.json ├── src ├── better_sqlite3.cpp ├── better_sqlite3.hpp ├── better_sqlite3.lzz ├── objects │ ├── backup.lzz │ ├── database.lzz │ ├── statement-iterator.lzz │ └── statement.lzz └── util │ ├── bind-map.lzz │ ├── binder.lzz │ ├── constants.lzz │ ├── custom-aggregate.lzz │ ├── custom-function.lzz │ ├── custom-table.lzz │ ├── data-converter.lzz │ ├── data.lzz │ ├── macros.lzz │ └── query-macros.lzz └── test ├── 00.setup.js ├── 01.sqlite-error.js ├── 10.database.open.js ├── 11.database.close.js ├── 12.database.pragma.js ├── 13.database.prepare.js ├── 14.database.exec.js ├── 20.statement.run.js ├── 21.statement.get.js ├── 22.statement.all.js ├── 23.statement.iterate.js ├── 24.statement.bind.js ├── 25.statement.columns.js ├── 30.database.transaction.js ├── 31.database.checkpoint.js ├── 32.database.function.js ├── 33.database.aggregate.js ├── 34.database.table.js ├── 35.database.load-extension.js ├── 36.database.backup.js ├── 37.database.serialize.js ├── 40.bigints.js ├── 41.at-exit.js ├── 42.integrity.js ├── 43.verbose.js ├── 44.worker-threads.js ├── 45.unsafe-mode.js └── 50.misc.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lzz linguist-language=C++ 2 | *.cpp -diff 3 | *.hpp -diff 4 | *.c -diff 5 | *.h -diff 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @JoshuaWise 2 | /package.json @JoshuaWise @WiseLibs/better-sqlite3-team 3 | /docs/compilation.md @JoshuaWise @WiseLibs/better-sqlite3-team 4 | /docs/performance.md @JoshuaWise @WiseLibs/better-sqlite3-team 5 | /docs/troubleshooting.md @JoshuaWise @WiseLibs/better-sqlite3-team 6 | /deps/sqlite3/sqlite3.c @JoshuaWise @WiseLibs/better-sqlite3-team 7 | /deps/sqlite3/sqlite3.h @JoshuaWise @WiseLibs/better-sqlite3-team 8 | /deps/sqlite3/sqlite3ext.h @JoshuaWise @WiseLibs/better-sqlite3-team 9 | /.github/workflows/build.yml @JoshuaWise @WiseLibs/better-sqlite3-team 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JoshuaWise 2 | patreon: joshuawise 3 | custom: "https://www.paypal.me/joshuathomaswise" 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: 12 | - released 13 | workflow_dispatch: {} 14 | 15 | env: 16 | # See https://github.com/nodejs/release#release-schedule 17 | # Node.js v20 EOL = 2026-04-30. v22 EOL = 2027-04-30. v23 EOL = 2025-06-01. v24 EOL = 2028-04-30. 18 | NODE_BUILD_CMD: npx --no-install prebuild -r node -t 20.0.0 -t 22.0.0 -t 23.0.0 -t 24.0.0 --include-regex 'better_sqlite3.node$' 19 | 20 | # See https://www.electronjs.org/docs/latest/tutorial/electron-timelines#version-support-policy 21 | # Electron v29 EOL = 2024-08-20. v30 EOL = 2024-10-15. v31 EOL = 2025-01-14. v32 EOL = 2025-03-11. v33 EOL = 2025-05-13. v34 EOL = 2025-06-24. v35 EOL = 2025-09-02. v36 EOL = 2025-10-28. 22 | ELECTRON_BUILD_CMD: npx --no-install prebuild -r electron -t 29.0.0 -t 30.0.0 -t 31.0.0 -t 32.0.0 -t 33.0.0 -t 34.0.0 -t 35.0.0 -t 36.0.0 --include-regex 'better_sqlite3.node$' 23 | 24 | jobs: 25 | test: 26 | strategy: 27 | matrix: 28 | os: 29 | - ubuntu-22.04 30 | - macos-13 31 | - macos-14 32 | - windows-2019 33 | node: 34 | - 20 35 | - 22 36 | - 23 37 | - 24 38 | name: Testing Node ${{ matrix.node }} on ${{ matrix.os }} 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/setup-node@v4 43 | with: 44 | node-version: ${{ matrix.node }} 45 | - if: ${{ startsWith(matrix.os, 'windows') }} 46 | run: pip.exe install setuptools 47 | - if: ${{ startsWith(matrix.os, 'macos') }} 48 | run: brew install python-setuptools 49 | - if: ${{ !startsWith(matrix.os, 'windows') && !startsWith(matrix.os, 'macos') }} 50 | run: python3 -m pip install setuptools 51 | - if: ${{ startsWith(matrix.os, 'ubuntu') }} 52 | run: | 53 | sudo apt update 54 | sudo apt install gcc-10 g++-10 -y 55 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 56 | - run: npm install --ignore-scripts 57 | - run: npm run build-debug 58 | - run: npm test 59 | - name: Test SpatiaLite extension 60 | if: ${{ startsWith(matrix.os, 'ubuntu') }} 61 | run: | 62 | sudo apt update 63 | sudo apt install libsqlite3-mod-spatialite -y 64 | node -e "require('./lib/index.js')(':memory:').loadExtension('mod_spatialite').exec('SELECT InitSpatialMetaData();')" 65 | 66 | publish: 67 | if: ${{ github.event_name == 'release' }} 68 | name: Publishing to NPM 69 | runs-on: ubuntu-22.04 70 | needs: 71 | - prebuild 72 | - prebuild-alpine 73 | - prebuild-alpine-arm 74 | - prebuild-linux-x64 75 | - prebuild-linux-arm 76 | steps: 77 | - uses: actions/checkout@v4 78 | - uses: actions/setup-node@v4 79 | with: 80 | node-version: 20 81 | registry-url: https://registry.npmjs.org 82 | - run: npm publish 83 | env: 84 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 85 | 86 | prebuild: 87 | if: ${{ github.event_name == 'release' }} 88 | strategy: 89 | fail-fast: false 90 | matrix: 91 | os: 92 | - macos-13 93 | - macos-14 94 | - windows-2019 95 | name: Prebuild on ${{ matrix.os }} 96 | runs-on: ${{ matrix.os }} 97 | needs: test 98 | steps: 99 | - uses: actions/checkout@v4 100 | - uses: actions/setup-node@v4 101 | with: 102 | node-version: 20 103 | - if: ${{ startsWith(matrix.os, 'windows') }} 104 | run: pip.exe install setuptools 105 | - if: ${{ startsWith(matrix.os, 'macos') }} 106 | run: brew install python-setuptools 107 | - run: npm install --ignore-scripts 108 | - run: ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} 109 | - run: ${{ env.ELECTRON_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} 110 | - if: matrix.os == 'windows-2019' 111 | run: | 112 | ${{ env.NODE_BUILD_CMD }} --arch ia32 -u ${{ secrets.GITHUB_TOKEN }} 113 | ${{ env.NODE_BUILD_CMD }} --arch arm64 -u ${{ secrets.GITHUB_TOKEN }} 114 | ${{ env.ELECTRON_BUILD_CMD }} --arch ia32 -u ${{ secrets.GITHUB_TOKEN }} 115 | ${{ env.ELECTRON_BUILD_CMD }} --arch arm64 -u ${{ secrets.GITHUB_TOKEN }} 116 | 117 | prebuild-linux-x64: 118 | if: ${{ github.event_name == 'release' }} 119 | name: Prebuild on Linux x64 120 | runs-on: ubuntu-latest 121 | container: node:20-bullseye 122 | needs: test 123 | steps: 124 | - uses: actions/checkout@v4 125 | - run: npm install --ignore-scripts 126 | - run: ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} 127 | - run: ${{ env.ELECTRON_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} 128 | 129 | prebuild-alpine: 130 | if: ${{ github.event_name == 'release' }} 131 | name: Prebuild on alpine 132 | runs-on: ubuntu-latest 133 | container: node:20-alpine 134 | needs: test 135 | steps: 136 | - uses: actions/checkout@v4 137 | - run: apk add build-base git python3 py3-setuptools --update-cache 138 | - run: npm install --ignore-scripts 139 | - run: ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} 140 | 141 | prebuild-alpine-arm: 142 | if: ${{ github.event_name == 'release' }} 143 | strategy: 144 | fail-fast: false 145 | matrix: 146 | arch: 147 | - arm/v7 148 | - arm64 149 | name: Prebuild on alpine (${{ matrix.arch }}) 150 | runs-on: ubuntu-latest 151 | needs: test 152 | steps: 153 | - uses: actions/checkout@v4 154 | - uses: docker/setup-qemu-action@v3 155 | - run: | 156 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:20-alpine -c "\ 157 | apk add build-base git python3 py3-setuptools --update-cache && \ 158 | cd /tmp/project && \ 159 | npm install --ignore-scripts && \ 160 | ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }}" 161 | 162 | prebuild-linux-arm: 163 | if: ${{ github.event_name == 'release' }} 164 | strategy: 165 | fail-fast: false 166 | matrix: 167 | arch: 168 | - arm/v7 169 | - arm64 170 | name: Prebuild on Linux (${{ matrix.arch }}) 171 | runs-on: ubuntu-latest 172 | needs: test 173 | steps: 174 | - uses: actions/checkout@v4 175 | - uses: docker/setup-qemu-action@v3 176 | - run: | 177 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:20-bullseye -c "\ 178 | cd /tmp/project && \ 179 | npm install --ignore-scripts && \ 180 | ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} && \ 181 | if [ '${{ matrix.arch }}' = 'arm64' ]; then ${{ env.ELECTRON_BUILD_CMD }} --arch arm64 -u ${{ secrets.GITHUB_TOKEN }}; fi" 182 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | name: bump-version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | type: 7 | type: choice 8 | description: Type of version bump 9 | required: true 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | 15 | jobs: 16 | bump: 17 | name: Bump to a new version 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | token: ${{ secrets.PAT }} 23 | fetch-depth: 0 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | - name: Configure user 28 | run: | 29 | git config --local user.name "${{ github.actor }}" 30 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 31 | - name: Bump the version 32 | run: npm version ${{ github.event.inputs.type }} 33 | - name: Push commit 34 | run: git push origin master:master 35 | - name: Push tag 36 | run: git push origin --tags 37 | -------------------------------------------------------------------------------- /.github/workflows/update-sqlite.yml: -------------------------------------------------------------------------------- 1 | name: update-sqlite 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | year: 7 | description: SQLite release year 8 | required: true 9 | version: 10 | description: SQLite version (encoded) 11 | required: true 12 | 13 | jobs: 14 | download-and-update: 15 | name: Download and update SQLite 16 | runs-on: ubuntu-latest 17 | env: 18 | ENV_YEAR: ${{ github.event.inputs.year }} 19 | ENV_VERSION: ${{ github.event.inputs.version }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | token: ${{ secrets.PAT }} 24 | fetch-depth: 0 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | - name: Create new update branch 29 | run: git checkout -b sqlite-update-${{ env.ENV_VERSION }} 30 | - name: Update download script 31 | run: | 32 | sed -Ei "s/YEAR=\"[0-9]+\"/YEAR=\"${{ env.ENV_YEAR }}\"/g" ./deps/download.sh 33 | sed -Ei "s/VERSION=\"[0-9]+\"/VERSION=\"${{ env.ENV_VERSION }}\"/g" ./deps/download.sh 34 | echo "ENV_TRUE_VERSION=$((10#${ENV_VERSION:0:1})).$((10#${ENV_VERSION:1:2})).$((10#${ENV_VERSION:3:2}))" >> $GITHUB_ENV 35 | - name: Download, compile and package SQLite 36 | run: npm run download 37 | - name: Push update branch 38 | uses: stefanzweifel/git-auto-commit-action@v5 39 | with: 40 | commit_message: Update SQLite to version ${{ env.ENV_TRUE_VERSION }} 41 | branch: sqlite-update-${{ env.ENV_VERSION }} 42 | - name: Create new PR 43 | uses: repo-sync/pull-request@v2 44 | with: 45 | github_token: ${{ secrets.PAT }} 46 | source_branch: sqlite-update-${{ env.ENV_VERSION }} 47 | pr_title: Update SQLite to version ${{ env.ENV_TRUE_VERSION }} 48 | pr_body: This is an automated pull request, updating SQLite to version `${{ env.ENV_TRUE_VERSION }}`. 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | package-lock.json 6 | yarn.lock 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/ 27 | 28 | # Dependency directory 29 | node_modules 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # Project specific 38 | lib/binding 39 | .DS_Store 40 | temp/ 41 | TODO 42 | .local 43 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Joshua Wise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # better-sqlite3 [![Build Status](https://github.com/JoshuaWise/better-sqlite3/actions/workflows/build.yml/badge.svg)](https://github.com/JoshuaWise/better-sqlite3/actions/workflows/build.yml?query=branch%3Amaster) 2 | 3 | The fastest and simplest library for SQLite in Node.js. 4 | 5 | - Full transaction support 6 | - High performance, efficiency, and safety 7 | - Easy-to-use synchronous API *(better concurrency than an asynchronous API... yes, you read that correctly)* 8 | - Support for user-defined functions, aggregates, virtual tables, and extensions 9 | - 64-bit integers *(invisible until you need them)* 10 | - Worker thread support *(for large/slow queries)* 11 | 12 | ## Help this project stay strong! 💪 13 | 14 | `better-sqlite3` is used by thousands of developers and engineers on a daily basis. Long nights and weekends were spent keeping this project strong and dependable, with no ask for compensation or funding, until now. If your company uses `better-sqlite3`, ask your manager to consider supporting the project: 15 | 16 | - [Become a GitHub sponsor](https://github.com/sponsors/JoshuaWise) 17 | - [Become a backer on Patreon](https://www.patreon.com/joshuawise) 18 | - [Make a one-time donation on PayPal](https://www.paypal.me/joshuathomaswise) 19 | 20 | ## How other libraries compare 21 | 22 | | |select 1 row  `get()` |select 100 rows   `all()`  |select 100 rows `iterate()` 1-by-1|insert 1 row `run()`|insert 100 rows in a transaction| 23 | |---|---|---|---|---|---| 24 | |better-sqlite3|1x|1x|1x|1x|1x| 25 | |[sqlite](https://www.npmjs.com/package/sqlite) and [sqlite3](https://www.npmjs.com/package/sqlite3)|11.7x slower|2.9x slower|24.4x slower|2.8x slower|15.6x slower| 26 | 27 | > You can verify these results by [running the benchmark yourself](./docs/benchmark.md). 28 | 29 | ## Installation 30 | 31 | ```bash 32 | npm install better-sqlite3 33 | ``` 34 | 35 | > Requires Node.js v14.21.1 or later. 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). 36 | 37 | ## Usage 38 | 39 | ```js 40 | const db = require('better-sqlite3')('foobar.db', options); 41 | 42 | const row = db.prepare('SELECT * FROM users WHERE id = ?').get(userId); 43 | console.log(row.firstName, row.lastName, row.email); 44 | ``` 45 | 46 | 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). 47 | 48 | ```js 49 | db.pragma('journal_mode = WAL'); 50 | ``` 51 | 52 | ##### In ES6 module notation: 53 | 54 | ```js 55 | import Database from 'better-sqlite3'; 56 | const db = new Database('foobar.db', options); 57 | db.pragma('journal_mode = WAL'); 58 | ``` 59 | 60 | ## Why should I use this instead of [node-sqlite3](https://github.com/mapbox/node-sqlite3)? 61 | 62 | - `node-sqlite3` uses asynchronous APIs for tasks that are either CPU-bound or serialized. That's not only bad design, but it wastes tons of resources. It also causes [mutex thrashing](https://en.wikipedia.org/wiki/Resource_contention) which has devastating effects on performance. 63 | - `node-sqlite3` exposes low-level (C language) memory management functions. `better-sqlite3` does it the JavaScript way, allowing the garbage collector to worry about memory management. 64 | - `better-sqlite3` is simpler to use, and it provides nice utilities for some operations that are very difficult or impossible in `node-sqlite3`. 65 | - `better-sqlite3` is much faster than `node-sqlite3` in most cases, and just as fast in all other cases. 66 | 67 | #### When is this library not appropriate? 68 | 69 | In most cases, if you're attempting something that cannot be reasonably accomplished with `better-sqlite3`, it probably cannot be reasonably accomplished with SQLite in general. For example, if you're executing queries that take one second to complete, and you expect to have many concurrent users executing those queries, no amount of asynchronicity will save you from SQLite's serialized nature. Fortunately, SQLite is very *very* fast. With proper indexing, we've been able to achieve upward of 2000 queries per second with 5-way-joins in a 60 GB database, where each query was handling 5–50 kilobytes of real data. 70 | 71 | If you have a performance problem, the most likely causes are inefficient queries, improper indexing, or a lack of [WAL mode](./docs/performance.md)—not `better-sqlite3` itself. However, there are some cases where `better-sqlite3` could be inappropriate: 72 | 73 | - If you expect a high volume of concurrent reads each returning many megabytes of data (i.e., videos) 74 | - If you expect a high volume of concurrent writes (i.e., a social media site) 75 | - If your database's size is near the terabyte range 76 | 77 | For these situations, you should probably use a full-fledged RDBMS such as [PostgreSQL](https://www.postgresql.org/). 78 | 79 | ## Upgrading 80 | 81 | Upgrading your `better-sqlite3` dependency can potentially introduce breaking changes, either in the `better-sqlite3` API (if you upgrade to a new [major version](https://semver.org/)), or between your existing database(s) and the underlying version of SQLite. Before upgrading, review: 82 | 83 | * [`better-sqlite3` release notes](https://github.com/WiseLibs/better-sqlite3/releases) 84 | * [SQLite release history](https://www.sqlite.org/changes.html) 85 | 86 | # Documentation 87 | 88 | - [API documentation](./docs/api.md) 89 | - [Performance](./docs/performance.md) (also see [benchmark results](./docs/benchmark.md)) 90 | - [64-bit integer support](./docs/integer.md) 91 | - [Worker thread support](./docs/threads.md) 92 | - [Unsafe mode (advanced)](./docs/unsafe.md) 93 | - [SQLite compilation (advanced)](./docs/compilation.md) 94 | - [Contribution rules](./docs/contribution.md) 95 | - [Code of conduct](./docs/conduct.md) 96 | 97 | # License 98 | 99 | [MIT](./LICENSE) 100 | -------------------------------------------------------------------------------- /benchmark/benchmark.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | const benchmark = require('nodemark'); 4 | 5 | const sync = (fn) => { 6 | display(benchmark(fn)); 7 | }; 8 | 9 | const async = (fn) => { 10 | const wrapped = cb => fn().then(() => cb(), cb); 11 | benchmark(wrapped).then(display); 12 | }; 13 | 14 | const display = (result) => { 15 | process.stdout.write(String(result).replace(/ \(.*/, '')); 16 | process.exit(); 17 | }; 18 | 19 | (async () => { 20 | process.on('unhandledRejection', (err) => { throw err; }); 21 | const ctx = JSON.parse(process.argv[2]); 22 | const type = require(`./types/${ctx.type}`); 23 | const db = await require('./drivers').get(ctx.driver)('../temp/benchmark.db', ctx.pragma); 24 | if (!type.readonly) { 25 | for (const table of ctx.tables) await db.exec(`DELETE FROM ${table} WHERE rowid > 1;`); 26 | await db.exec('VACUUM;'); 27 | } 28 | const fn = type[ctx.driver](db, ctx); 29 | if (typeof fn === 'function') setImmediate(sync, fn); 30 | else setImmediate(async, await fn); 31 | })(); 32 | -------------------------------------------------------------------------------- /benchmark/drivers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | Every benchmark trial will be executed once for each SQLite driver listed 5 | below. Each driver has a function to open a new database connection on a 6 | given filename and a list of PRAGMA statements. 7 | */ 8 | 9 | module.exports = new Map([ 10 | ['better-sqlite3', async (filename, pragma) => { 11 | const db = require('../.')(filename); 12 | for (const str of pragma) db.pragma(str); 13 | return db; 14 | }], 15 | ['node-sqlite3', async (filename, pragma) => { 16 | const driver = require('sqlite3').Database; 17 | const db = await (require('sqlite').open)({ filename, driver }); 18 | for (const str of pragma) await db.run(`PRAGMA ${str}`); 19 | return db; 20 | }], 21 | ]); 22 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { execFileSync } = require('child_process'); 3 | const clc = require('cli-color'); 4 | 5 | const getTrials = (searchTerms) => { 6 | // Without any command-line arguments, we do a general-purpose benchmark. 7 | if (!searchTerms.length) return require('./trials').default; 8 | 9 | // With command-line arguments, the user can run specific groups of trials. 10 | return require('./trials').searchable.filter(filterBySearchTerms(searchTerms)); 11 | }; 12 | 13 | const filterBySearchTerms = (searchTerms) => (trial) => { 14 | const terms = [ 15 | trial.type, 16 | trial.table, 17 | `(${trial.columns.join(', ')})`, 18 | `(${trial.columns.join(',')})`, 19 | ...trial.columns, 20 | ...trial.customPragma, 21 | ]; 22 | return searchTerms.every(arg => terms.includes(arg)); 23 | }; 24 | 25 | const sortTrials = (a, b) => { 26 | const aRo = require(`./types/${a.type}`).readonly; 27 | const bRo = require(`./types/${b.type}`).readonly; 28 | if (typeof aRo !== 'boolean') throw new TypeError(`Missing readonly export in benchmark type ${a.type}`); 29 | if (typeof bRo !== 'boolean') throw new TypeError(`Missing readonly export in benchmark type ${b.type}`); 30 | return bRo - aRo; 31 | }; 32 | 33 | const displayTrialName = (trial) => { 34 | if (trial.description) return console.log(clc.magenta(`--- ${trial.description} ---`)); 35 | const name = `${trial.type} ${trial.table} (${trial.columns.join(', ')})`; 36 | const pragma = trial.customPragma.length ? ` | ${trial.customPragma.join('; ')}` : ''; 37 | console.log(clc.magenta(name) + clc.yellow(pragma)); 38 | }; 39 | 40 | const createContext = (trial, driver) => { 41 | const tableInfo = Object.assign({}, tables.get(trial.table), { data: undefined }); 42 | return JSON.stringify(Object.assign({}, trial, tableInfo, { driver, tables: [...tables.keys()] })); 43 | }; 44 | 45 | const erase = () => { 46 | return clc.move(0, -1) + clc.erase.line; 47 | }; 48 | 49 | // Determine which trials should be executed. 50 | process.chdir(__dirname); 51 | const trials = getTrials(process.argv.slice(2)).sort(sortTrials); 52 | if (!trials.length) { 53 | console.log(clc.yellow('No matching benchmarks found!')); 54 | process.exit(); 55 | } 56 | 57 | // Create the temporary database needed to run the benchmark trials. 58 | console.log('Generating tables...'); 59 | const tables = require('./seed')(); 60 | process.stdout.write(erase()); 61 | 62 | // Execute each trial for each available driver. 63 | const drivers = require('./drivers'); 64 | const nameLength = [...drivers.keys()].reduce((m, d) => Math.max(m, d.length), 0); 65 | for (const trial of trials) { 66 | displayTrialName(trial); 67 | for (const driver of drivers.keys()) { 68 | const driverName = driver.padEnd(nameLength); 69 | const ctx = createContext(trial, driver); 70 | process.stdout.write(`${driver} (running...)\n`); 71 | try { 72 | const result = execFileSync('node', ['./benchmark.js', ctx], { stdio: 'pipe', encoding: 'utf8' }); 73 | console.log(erase() + `${driverName} x ${result}`); 74 | } catch (err) { 75 | console.log(erase() + clc.red(`${driverName} ERROR (probably out of memory)`)); 76 | process.stderr.write(clc.xterm(247)(clc.strip(err.stderr))); 77 | } 78 | } 79 | console.log(''); 80 | } 81 | 82 | console.log(clc.green('All benchmarks complete!')); 83 | process.exit(); 84 | -------------------------------------------------------------------------------- /benchmark/seed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | 5 | const tables = new Map([ 6 | ['small', { 7 | schema: '(nul, integer INTEGER, real REAL, text TEXT, blob BLOB)', 8 | data: [null, 0x7fffffff, 1 / 3, 'this is the text', Buffer.from('this is the blob')], 9 | count: 10000, 10 | }], 11 | ['large_text', { 12 | schema: '(text TEXT)', 13 | data: ['this is the text'.repeat(2048)], 14 | count: 10000, 15 | }], 16 | ['large_blob', { 17 | schema: '(blob BLOB)', 18 | data: [Buffer.from('this is the blob'.repeat(2048))], 19 | count: 10000, 20 | }], 21 | ]); 22 | 23 | /* 24 | This function creates a pre-populated database that is deleted when the 25 | process exits. 26 | */ 27 | 28 | module.exports = () => { 29 | const tempDir = path.join(__dirname, '..', 'temp'); 30 | process.on('exit', () => fs.removeSync(tempDir)); 31 | fs.removeSync(tempDir); 32 | fs.ensureDirSync(tempDir); 33 | 34 | const db = require('../.')(path.join(tempDir, 'benchmark.db')); 35 | db.pragma('journal_mode = OFF'); 36 | db.pragma('synchronous = OFF'); 37 | 38 | for (const [name, ctx] of tables.entries()) { 39 | db.exec(`CREATE TABLE ${name} ${ctx.schema}`); 40 | const columns = db.pragma(`table_info(${name})`).map(() => '?'); 41 | const insert = db.prepare(`INSERT INTO ${name} VALUES (${columns.join(', ')})`).bind(ctx.data); 42 | for (let i = 0; i < ctx.count; ++i) insert.run(); 43 | } 44 | 45 | db.close(); 46 | return tables; 47 | }; 48 | -------------------------------------------------------------------------------- /benchmark/trials.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.default = [ 4 | { type: 'select', table: 'small', columns: ['nul', 'integer', 'real', 'text'], 5 | description: 'reading rows individually' }, 6 | { type: 'select-all', table: 'small', columns: ['nul', 'integer', 'real', 'text'], 7 | description: 'reading 100 rows into an array' }, 8 | { type: 'select-iterate', table: 'small', columns: ['nul', 'integer', 'real', 'text'], 9 | description: 'iterating over 100 rows' }, 10 | { type: 'insert', table: 'small', columns: ['nul', 'integer', 'real', 'text'], 11 | description: 'inserting rows individually' }, 12 | { type: 'transaction', table: 'small', columns: ['nul', 'integer', 'real', 'text'], 13 | description: 'inserting 100 rows in a single transaction' }, 14 | ]; 15 | 16 | exports.searchable = [ 17 | { type: 'select', table: 'small', columns: ['nul'] }, 18 | { type: 'select', table: 'small', columns: ['integer'] }, 19 | { type: 'select', table: 'small', columns: ['real'] }, 20 | { type: 'select', table: 'small', columns: ['text'] }, 21 | { type: 'select', table: 'small', columns: ['blob'] }, 22 | { type: 'select', table: 'large_text', columns: ['text'] }, 23 | { type: 'select', table: 'large_blob', columns: ['blob'] }, 24 | { type: 'select-all', table: 'small', columns: ['nul'] }, 25 | { type: 'select-all', table: 'small', columns: ['integer'] }, 26 | { type: 'select-all', table: 'small', columns: ['real'] }, 27 | { type: 'select-all', table: 'small', columns: ['text'] }, 28 | { type: 'select-all', table: 'small', columns: ['blob'] }, 29 | { type: 'select-all', table: 'large_text', columns: ['text'] }, 30 | { type: 'select-all', table: 'large_blob', columns: ['blob'] }, 31 | { type: 'select-iterate', table: 'small', columns: ['nul'] }, 32 | { type: 'select-iterate', table: 'small', columns: ['integer'] }, 33 | { type: 'select-iterate', table: 'small', columns: ['real'] }, 34 | { type: 'select-iterate', table: 'small', columns: ['text'] }, 35 | { type: 'select-iterate', table: 'small', columns: ['blob'] }, 36 | { type: 'select-iterate', table: 'large_text', columns: ['text'] }, 37 | { type: 'select-iterate', table: 'large_blob', columns: ['blob'] }, 38 | { type: 'insert', table: 'small', columns: ['nul'] }, 39 | { type: 'insert', table: 'small', columns: ['integer'] }, 40 | { type: 'insert', table: 'small', columns: ['real'] }, 41 | { type: 'insert', table: 'small', columns: ['text'] }, 42 | { type: 'insert', table: 'small', columns: ['blob'] }, 43 | { type: 'insert', table: 'large_text', columns: ['text'] }, 44 | { type: 'insert', table: 'large_blob', columns: ['blob'] }, 45 | { type: 'transaction', table: 'small', columns: ['nul'] }, 46 | { type: 'transaction', table: 'small', columns: ['integer'] }, 47 | { type: 'transaction', table: 'small', columns: ['real'] }, 48 | { type: 'transaction', table: 'small', columns: ['text'] }, 49 | { type: 'transaction', table: 'small', columns: ['blob'] }, 50 | { type: 'transaction', table: 'large_text', columns: ['text'] }, 51 | { type: 'transaction', table: 'large_blob', columns: ['blob'] }, 52 | ]; 53 | 54 | (() => { 55 | const defaultPragma = []; 56 | const yes = /^\s*(1|true|on|yes)\s*$/i; 57 | if (yes.test(process.env.NO_CACHE)) defaultPragma.push('cache_size = 0'); 58 | else defaultPragma.push('cache_size = -16000'); 59 | if (yes.test(process.env.NO_WAL)) defaultPragma.push('journal_mode = DELETE', 'synchronous = FULL'); 60 | else defaultPragma.push('journal_mode = WAL', 'synchronous = NORMAL'); 61 | for (const trial of [].concat(...Object.values(exports))) { 62 | trial.customPragma = trial.pragma || []; 63 | trial.pragma = defaultPragma.concat(trial.customPragma); 64 | } 65 | })(); 66 | -------------------------------------------------------------------------------- /benchmark/types/insert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.readonly = false; // Inserting rows individually (`.run()`) 3 | 4 | exports['better-sqlite3'] = (db, { table, columns }) => { 5 | const stmt = db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`); 6 | const row = db.prepare(`SELECT * FROM ${table} LIMIT 1`).get(); 7 | return () => stmt.run(row); 8 | }; 9 | 10 | exports['node-sqlite3'] = async (db, { table, columns }) => { 11 | const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`; 12 | const row = Object.assign({}, ...Object.entries(await db.get(`SELECT * FROM ${table} LIMIT 1`)) 13 | .filter(([k]) => columns.includes(k)) 14 | .map(([k, v]) => ({ ['@' + k]: v }))); 15 | return () => db.run(sql, row); 16 | }; 17 | -------------------------------------------------------------------------------- /benchmark/types/select-all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.readonly = true; // Reading 100 rows into an array (`.all()`) 3 | 4 | exports['better-sqlite3'] = (db, { table, columns, count }) => { 5 | const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid >= ? LIMIT 100`); 6 | let rowid = -100; 7 | return () => stmt.all((rowid += 100) % count + 1); 8 | }; 9 | 10 | exports['node-sqlite3'] = async (db, { table, columns, count }) => { 11 | const sql = `SELECT ${columns.join(', ')} FROM ${table} WHERE rowid >= ? LIMIT 100`; 12 | let rowid = -100; 13 | return () => db.all(sql, (rowid += 100) % count + 1); 14 | }; 15 | -------------------------------------------------------------------------------- /benchmark/types/select-iterate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.readonly = true; // Iterating over 100 rows (`.iterate()`) 3 | 4 | exports['better-sqlite3'] = (db, { table, columns, count }) => { 5 | const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid >= ? LIMIT 100`); 6 | let rowid = -100; 7 | return () => { 8 | for (const row of stmt.iterate((rowid += 100) % count + 1)) {} 9 | }; 10 | }; 11 | 12 | exports['node-sqlite3'] = async (db, { table, columns, count }) => { 13 | const sql = `SELECT ${columns.join(', ')} FROM ${table} WHERE rowid = ?`; 14 | let rowid = -100; 15 | return () => { 16 | rowid += 100; 17 | let index = 0; 18 | return (function next() { 19 | if (index === 100) return; 20 | return db.get(sql, (rowid + index++) % count + 1).then(next); 21 | })(); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /benchmark/types/select.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.readonly = true; // Reading rows individually (`.get()`) 3 | 4 | exports['better-sqlite3'] = (db, { table, columns, count }) => { 5 | const stmt = db.prepare(`SELECT ${columns.join(', ')} FROM ${table} WHERE rowid = ?`); 6 | let rowid = -1; 7 | return () => stmt.get(++rowid % count + 1); 8 | }; 9 | 10 | exports['node-sqlite3'] = async (db, { table, columns, count }) => { 11 | const sql = `SELECT ${columns.join(', ')} FROM ${table} WHERE rowid = ?`; 12 | let rowid = -1; 13 | return () => db.get(sql, ++rowid % count + 1); 14 | }; 15 | -------------------------------------------------------------------------------- /benchmark/types/transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.readonly = false; // Inserting 100 rows in a single transaction 3 | 4 | exports['better-sqlite3'] = (db, { table, columns }) => { 5 | const stmt = db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`); 6 | const row = db.prepare(`SELECT * FROM ${table} LIMIT 1`).get(); 7 | const trx = db.transaction((row) => { 8 | for (let i = 0; i < 100; ++i) stmt.run(row); 9 | }); 10 | return () => trx(row); 11 | }; 12 | 13 | exports['node-sqlite3'] = async (db, { table, columns, driver, pragma }) => { 14 | const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map(x => '@' + x).join(', ')})`; 15 | const row = Object.assign({}, ...Object.entries(await db.get(`SELECT * FROM ${table} LIMIT 1`)) 16 | .filter(([k]) => columns.includes(k)) 17 | .map(([k, v]) => ({ ['@' + k]: v }))); 18 | const open = require('../drivers').get(driver); 19 | /* 20 | The only way to create an isolated transaction with node-sqlite3 in a 21 | random-access environment (i.e., a web server) is to open a new database 22 | connection for each transaction. 23 | (http://github.com/mapbox/node-sqlite3/issues/304#issuecomment-45242331) 24 | */ 25 | return () => open('../temp/benchmark.db', pragma).then(async (db) => { 26 | try { 27 | await db.run('BEGIN'); 28 | try { 29 | for (let i = 0; i < 100; ++i) await db.run(sql, row); 30 | await db.run('COMMIT'); 31 | } catch (err) { 32 | try { await db.run('ROLLBACK'); } 33 | catch (_) { /* this is necessary because there's no db.inTransaction property */ } 34 | throw err; 35 | } 36 | } finally { 37 | await db.close(); 38 | } 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | # === 2 | # This is the main GYP file, which builds better-sqlite3 with SQLite itself. 3 | # === 4 | 5 | { 6 | 'includes': ['deps/common.gypi'], 7 | 'targets': [ 8 | { 9 | 'target_name': 'better_sqlite3', 10 | 'dependencies': ['deps/sqlite3.gyp:sqlite3'], 11 | 'sources': ['src/better_sqlite3.cpp'], 12 | 'cflags_cc': ['-std=c++20'], 13 | 'xcode_settings': { 14 | 'OTHER_CPLUSPLUSFLAGS': ['-std=c++20', '-stdlib=libc++'], 15 | }, 16 | 'msvs_settings': { 17 | 'VCCLCompilerTool': { 18 | 'AdditionalOptions': [ 19 | '/std:c++20', 20 | ], 21 | }, 22 | }, 23 | 'conditions': [ 24 | ['OS=="linux"', { 25 | 'ldflags': [ 26 | '-Wl,-Bsymbolic', 27 | '-Wl,--exclude-libs,ALL', 28 | ], 29 | }], 30 | ], 31 | }, 32 | { 33 | 'target_name': 'test_extension', 34 | 'dependencies': ['deps/sqlite3.gyp:sqlite3'], 35 | 'conditions': [['sqlite3 == ""', { 'sources': ['deps/test_extension.c'] }]], 36 | }, 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /deps/common.gypi: -------------------------------------------------------------------------------- 1 | # === 2 | # This configuration defines the differences between Release and Debug builds. 3 | # Some miscellaneous Windows settings are also defined here. 4 | # === 5 | 6 | { 7 | 'variables': { 'sqlite3%': '' }, 8 | 'target_defaults': { 9 | 'default_configuration': 'Release', 10 | 'msvs_settings': { 11 | 'VCCLCompilerTool': { 12 | 'ExceptionHandling': 1, 13 | }, 14 | }, 15 | 'conditions': [ 16 | ['OS == "win"', { 17 | 'defines': ['WIN32'], 18 | }], 19 | ], 20 | 'configurations': { 21 | 'Debug': { 22 | 'defines!': [ 23 | 'NDEBUG', 24 | ], 25 | 'defines': [ 26 | 'DEBUG', 27 | '_DEBUG', 28 | 'SQLITE_DEBUG', 29 | 'SQLITE_MEMDEBUG', 30 | 'SQLITE_ENABLE_API_ARMOR', 31 | 'SQLITE_WIN32_MALLOC_VALIDATE', 32 | ], 33 | 'cflags': [ 34 | '-O0', 35 | ], 36 | 'xcode_settings': { 37 | 'MACOSX_DEPLOYMENT_TARGET': '10.7', 38 | 'GCC_OPTIMIZATION_LEVEL': '0', 39 | 'GCC_GENERATE_DEBUGGING_SYMBOLS': 'YES', 40 | }, 41 | 'msvs_settings': { 42 | 'VCLinkerTool': { 43 | 'GenerateDebugInformation': 'true', 44 | }, 45 | }, 46 | }, 47 | 'Release': { 48 | 'defines!': [ 49 | 'DEBUG', 50 | '_DEBUG', 51 | ], 52 | 'defines': [ 53 | 'NDEBUG', 54 | ], 55 | 'cflags': [ 56 | '-O3', 57 | ], 58 | 'xcode_settings': { 59 | 'MACOSX_DEPLOYMENT_TARGET': '10.7', 60 | 'GCC_OPTIMIZATION_LEVEL': '3', 61 | 'GCC_GENERATE_DEBUGGING_SYMBOLS': 'NO', 62 | 'DEAD_CODE_STRIPPING': 'YES', 63 | 'GCC_INLINES_ARE_PRIVATE_EXTERN': 'YES', 64 | }, 65 | }, 66 | }, 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /deps/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | const dest = process.argv[2]; 6 | const source = path.resolve(path.sep, process.argv[3] || path.join(__dirname, 'sqlite3')); 7 | const files = [ 8 | { filename: 'sqlite3.c', optional: false }, 9 | { filename: 'sqlite3.h', optional: false }, 10 | ]; 11 | 12 | if (process.argv[3]) { 13 | // Support "_HAVE_SQLITE_CONFIG_H" in custom builds. 14 | files.push({ filename: 'config.h', optional: true }); 15 | } else { 16 | // Required for some tests. 17 | files.push({ filename: 'sqlite3ext.h', optional: false }); 18 | } 19 | 20 | for (const { filename, optional } of files) { 21 | const sourceFilepath = path.join(source, filename); 22 | const destFilepath = path.join(dest, filename); 23 | 24 | if (optional && !fs.existsSync(sourceFilepath)) { 25 | continue; 26 | } 27 | 28 | fs.accessSync(sourceFilepath); 29 | fs.mkdirSync(path.dirname(destFilepath), { recursive: true }); 30 | fs.copyFileSync(sourceFilepath, destFilepath); 31 | } 32 | -------------------------------------------------------------------------------- /deps/defines.gypi: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOMATICALLY GENERATED BY deps/download.sh (DO NOT EDIT) 2 | 3 | { 4 | 'defines': [ 5 | 'HAVE_INT16_T=1', 6 | 'HAVE_INT32_T=1', 7 | 'HAVE_INT8_T=1', 8 | 'HAVE_STDINT_H=1', 9 | 'HAVE_UINT16_T=1', 10 | 'HAVE_UINT32_T=1', 11 | 'HAVE_UINT8_T=1', 12 | 'HAVE_USLEEP=1', 13 | 'SQLITE_DEFAULT_CACHE_SIZE=-16000', 14 | 'SQLITE_DEFAULT_FOREIGN_KEYS=1', 15 | 'SQLITE_DEFAULT_MEMSTATUS=0', 16 | 'SQLITE_DEFAULT_WAL_SYNCHRONOUS=1', 17 | 'SQLITE_DQS=0', 18 | 'SQLITE_ENABLE_COLUMN_METADATA', 19 | 'SQLITE_ENABLE_DBSTAT_VTAB', 20 | 'SQLITE_ENABLE_DESERIALIZE', 21 | 'SQLITE_ENABLE_FTS3', 22 | 'SQLITE_ENABLE_FTS3_PARENTHESIS', 23 | 'SQLITE_ENABLE_FTS4', 24 | 'SQLITE_ENABLE_FTS5', 25 | 'SQLITE_ENABLE_GEOPOLY', 26 | 'SQLITE_ENABLE_JSON1', 27 | 'SQLITE_ENABLE_MATH_FUNCTIONS', 28 | 'SQLITE_ENABLE_RTREE', 29 | 'SQLITE_ENABLE_STAT4', 30 | 'SQLITE_ENABLE_UPDATE_DELETE_LIMIT', 31 | 'SQLITE_LIKE_DOESNT_MATCH_BLOBS', 32 | 'SQLITE_OMIT_DEPRECATED', 33 | 'SQLITE_OMIT_PROGRESS_CALLBACK', 34 | 'SQLITE_OMIT_SHARED_CACHE', 35 | 'SQLITE_OMIT_TCL_VARIABLE', 36 | 'SQLITE_SOUNDEX', 37 | 'SQLITE_THREADSAFE=2', 38 | 'SQLITE_TRACE_SIZE_LIMIT=32', 39 | 'SQLITE_USE_URI=0', 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /deps/download.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # === 4 | # This script defines and generates the bundled SQLite unit (sqlite3.c). 5 | # 6 | # The following steps are taken: 7 | # 1. populate the shell environment with the defined compile-time options. 8 | # 2. download and extract the SQLite source code into a temporary directory. 9 | # 3. run "sh configure" and "make sqlite3.c" within the source directory. 10 | # 4. copy the generated amalgamation into the output directory (./sqlite3). 11 | # 5. export the defined compile-time options to a gyp file (./defines.gypi). 12 | # 6. update the docs (../docs/compilation.md) with details of this distribution. 13 | # 14 | # When a user builds better-sqlite3, the following steps are taken: 15 | # 1. node-gyp loads the previously exported compile-time options (defines.gypi). 16 | # 2. the copy.js script copies the bundled amalgamation into the build folder. 17 | # 3. node-gyp compiles the copied sqlite3.c along with better_sqlite3.cpp. 18 | # 4. node-gyp links the two resulting binaries to generate better_sqlite3.node. 19 | # === 20 | 21 | YEAR="2025" 22 | VERSION="3490200" 23 | 24 | # Defines below are sorted alphabetically 25 | DEFINES=" 26 | HAVE_INT16_T=1 27 | HAVE_INT32_T=1 28 | HAVE_INT8_T=1 29 | HAVE_STDINT_H=1 30 | HAVE_UINT16_T=1 31 | HAVE_UINT32_T=1 32 | HAVE_UINT8_T=1 33 | HAVE_USLEEP=1 34 | SQLITE_DEFAULT_CACHE_SIZE=-16000 35 | SQLITE_DEFAULT_FOREIGN_KEYS=1 36 | SQLITE_DEFAULT_MEMSTATUS=0 37 | SQLITE_DEFAULT_WAL_SYNCHRONOUS=1 38 | SQLITE_DQS=0 39 | SQLITE_ENABLE_COLUMN_METADATA 40 | SQLITE_ENABLE_DBSTAT_VTAB 41 | SQLITE_ENABLE_DESERIALIZE 42 | SQLITE_ENABLE_FTS3 43 | SQLITE_ENABLE_FTS3_PARENTHESIS 44 | SQLITE_ENABLE_FTS4 45 | SQLITE_ENABLE_FTS5 46 | SQLITE_ENABLE_GEOPOLY 47 | SQLITE_ENABLE_JSON1 48 | SQLITE_ENABLE_MATH_FUNCTIONS 49 | SQLITE_ENABLE_RTREE 50 | SQLITE_ENABLE_STAT4 51 | SQLITE_ENABLE_UPDATE_DELETE_LIMIT 52 | SQLITE_LIKE_DOESNT_MATCH_BLOBS 53 | SQLITE_OMIT_DEPRECATED 54 | SQLITE_OMIT_PROGRESS_CALLBACK 55 | SQLITE_OMIT_SHARED_CACHE 56 | SQLITE_OMIT_TCL_VARIABLE 57 | SQLITE_SOUNDEX 58 | SQLITE_THREADSAFE=2 59 | SQLITE_TRACE_SIZE_LIMIT=32 60 | SQLITE_USE_URI=0 61 | " 62 | 63 | # ========== START SCRIPT ========== # 64 | 65 | echo "setting up environment..." 66 | DEPS="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" 67 | TEMP="$DEPS/temp" 68 | OUTPUT="$DEPS/sqlite3" 69 | rm -rf "$TEMP" 70 | rm -rf "$OUTPUT" 71 | mkdir -p "$TEMP" 72 | mkdir -p "$OUTPUT" 73 | export CFLAGS=`echo $(echo "$DEFINES" | sed -e "/^\s*$/d" -e "s/^/-D/")` 74 | 75 | echo "downloading source..." 76 | curl -#f "https://www.sqlite.org/$YEAR/sqlite-src-$VERSION.zip" > "$TEMP/source.zip" || exit 1 77 | 78 | echo "extracting source..." 79 | unzip "$TEMP/source.zip" -d "$TEMP" > /dev/null || exit 1 80 | cd "$TEMP/sqlite-src-$VERSION" || exit 1 81 | 82 | echo "configuring amalgamation..." 83 | sh configure > /dev/null || exit 1 84 | 85 | echo "building amalgamation..." 86 | make OPTIONS="$CFLAGS" sqlite3.c > /dev/null || exit 1 87 | 88 | echo "copying amalgamation..." 89 | cp sqlite3.c sqlite3.h sqlite3ext.h "$OUTPUT/" || exit 1 90 | 91 | echo "applying patches..." 92 | cd "$DEPS" || exit 1 93 | for patch in patches/*.patch; do 94 | # If a patch fails, just skip it an move on. 95 | # By default `patch` tries to be clever and reverse the patch, so we have to specify `--forward`. 96 | # If the patch fails, we # don't write .orig and .rej files, so we have to specify `--no-backup-if-mismatch` and `--reject-file=-`. 97 | patch --batch --forward --no-backup-if-mismatch --reject-file=- -p2 < "$patch" 98 | done 99 | 100 | echo "updating gyp configs..." 101 | GYP="$DEPS/defines.gypi" 102 | printf "# THIS FILE IS AUTOMATICALLY GENERATED BY deps/download.sh (DO NOT EDIT)\n\n{\n 'defines': [\n" > "$GYP" 103 | printf "$DEFINES" | sed -e "/^\s*$/d" -e "s/\(.*\)/ '\1',/" >> "$GYP" 104 | printf " ],\n}\n" >> "$GYP" 105 | 106 | echo "updating docs..." 107 | DOCS="$DEPS/../docs/compilation.md" 108 | MAJOR=`expr "${VERSION:0:1}" + 0` 109 | MINOR=`expr "${VERSION:1:2}" + 0` 110 | PATCH=`expr "${VERSION:3:2}" + 0` 111 | sed -Ei.bak -e "s/version [0-9]+\.[0-9]+\.[0-9]+/version $MAJOR.$MINOR.$PATCH/g" "$DOCS" 112 | sed -i.bak -e "/^SQLITE_/,\$d" "$DOCS" 113 | sed -i.bak -e "/^HAVE_/,\$d" "$DOCS" 114 | rm "$DOCS".bak 115 | printf "$DEFINES" | sed -e "/^\s*$/d" >> "$DOCS" 116 | printf "\`\`\`\n" >> "$DOCS" 117 | 118 | echo "cleaning up..." 119 | cd - > /dev/null || exit 1 120 | rm -rf "$TEMP" 121 | 122 | echo "done!" 123 | -------------------------------------------------------------------------------- /deps/patches/1208.patch: -------------------------------------------------------------------------------- 1 | diff --git a/deps/sqlite3/sqlite3.c b/deps/sqlite3/sqlite3.c 2 | index b1a807f..38bd1e6 100644 3 | --- a/deps/sqlite3/sqlite3.c 4 | +++ b/deps/sqlite3/sqlite3.c 5 | @@ -24887,8 +24887,8 @@ static const struct { 6 | /* 1 */ { 6, "minute", 7.7379e+12, 60.0 }, 7 | /* 2 */ { 4, "hour", 1.2897e+11, 3600.0 }, 8 | /* 3 */ { 3, "day", 5373485.0, 86400.0 }, 9 | - /* 4 */ { 5, "month", 176546.0, 30.0*86400.0 }, 10 | - /* 5 */ { 4, "year", 14713.0, 365.0*86400.0 }, 11 | + /* 4 */ { 5, "month", 176546.0, 2592000.0 }, 12 | + /* 5 */ { 4, "year", 14713.0, 31536000.0 }, 13 | }; 14 | 15 | /* 16 | -------------------------------------------------------------------------------- /deps/sqlite3.gyp: -------------------------------------------------------------------------------- 1 | # === 2 | # This configuration defines options specific to compiling SQLite itself. 3 | # Compile-time options are loaded by the auto-generated file "defines.gypi". 4 | # The --sqlite3 option can be provided to use a custom amalgamation instead. 5 | # === 6 | 7 | { 8 | 'includes': ['common.gypi'], 9 | 'targets': [ 10 | { 11 | 'target_name': 'locate_sqlite3', 12 | 'type': 'none', 13 | 'hard_dependency': 1, 14 | 'conditions': [ 15 | ['sqlite3 == ""', { 16 | 'actions': [{ 17 | 'action_name': 'copy_builtin_sqlite3', 18 | 'inputs': [ 19 | 'sqlite3/sqlite3.c', 20 | 'sqlite3/sqlite3.h', 21 | 'sqlite3/sqlite3ext.h', 22 | ], 23 | 'outputs': [ 24 | '<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.c', 25 | '<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.h', 26 | '<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3ext.h', 27 | ], 28 | 'action': ['node', 'copy.js', '<(SHARED_INTERMEDIATE_DIR)/sqlite3', ''], 29 | }], 30 | }, { 31 | 'actions': [{ 32 | 'action_name': 'copy_custom_sqlite3', 33 | 'inputs': [ 34 | '<(sqlite3)/sqlite3.c', 35 | '<(sqlite3)/sqlite3.h', 36 | ], 37 | 'outputs': [ 38 | '<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.c', 39 | '<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.h', 40 | ], 41 | 'action': ['node', 'copy.js', '<(SHARED_INTERMEDIATE_DIR)/sqlite3', '<(sqlite3)'], 42 | }], 43 | }], 44 | ], 45 | }, 46 | { 47 | 'target_name': 'sqlite3', 48 | 'type': 'static_library', 49 | 'dependencies': ['locate_sqlite3'], 50 | 'sources': ['<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.c'], 51 | 'include_dirs': ['<(SHARED_INTERMEDIATE_DIR)/sqlite3/'], 52 | 'direct_dependent_settings': { 53 | 'include_dirs': ['<(SHARED_INTERMEDIATE_DIR)/sqlite3/'], 54 | }, 55 | 'cflags': ['-std=c99', '-w'], 56 | 'xcode_settings': { 57 | 'OTHER_CFLAGS': ['-std=c99'], 58 | 'WARNING_CFLAGS': ['-w'], 59 | }, 60 | 'conditions': [ 61 | ['sqlite3 == ""', { 62 | 'includes': ['defines.gypi'], 63 | }, { 64 | 'defines': [ 65 | # This is currently required by better-sqlite3. 66 | 'SQLITE_ENABLE_COLUMN_METADATA', 67 | ], 68 | }] 69 | ], 70 | 'configurations': { 71 | 'Debug': { 72 | 'msvs_settings': { 'VCCLCompilerTool': { 'RuntimeLibrary': 1 } }, # static debug 73 | }, 74 | 'Release': { 75 | 'msvs_settings': { 'VCCLCompilerTool': { 'RuntimeLibrary': 0 } }, # static release 76 | }, 77 | }, 78 | }, 79 | ], 80 | } 81 | -------------------------------------------------------------------------------- /deps/test_extension.c: -------------------------------------------------------------------------------- 1 | #include 2 | SQLITE_EXTENSION_INIT1 3 | 4 | /* 5 | This SQLite extension is used only for testing purposes (npm test). 6 | */ 7 | 8 | static void TestExtensionFunction(sqlite3_context* pCtx, int nVal, sqlite3_value** _) { 9 | sqlite3_result_double(pCtx, (double)nVal); 10 | } 11 | 12 | #ifdef _WIN32 13 | __declspec(dllexport) 14 | #endif 15 | 16 | int sqlite3_extension_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) { 17 | SQLITE_EXTENSION_INIT2(pApi) 18 | if (pzErrMsg != 0) *pzErrMsg = 0; 19 | sqlite3_create_function(db, "testExtensionFunction", -1, SQLITE_UTF8, 0, TestExtensionFunction, 0, 0); 20 | return SQLITE_OK; 21 | } 22 | -------------------------------------------------------------------------------- /docs/benchmark.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | To run the benchmark yourself: 4 | 5 | ```bash 6 | git clone https://github.com/WiseLibs/better-sqlite3.git 7 | cd better-sqlite3 8 | npm install # if you're doing this as the root user, --unsafe-perm is required 9 | node benchmark 10 | ``` 11 | 12 | # Results 13 | 14 | These results are from 03/29/2020, on a MacBook Pro (Retina, 15-inch, Mid 2014, OSX 10.11.6), using nodejs v12.16.1. 15 | 16 | ``` 17 | --- reading rows individually --- 18 | better-sqlite3 x 313,899 ops/sec ±0.13% 19 | node-sqlite3 x 26,780 ops/sec ±2.9% 20 | 21 | --- reading 100 rows into an array --- 22 | better-sqlite3 x 8,508 ops/sec ±0.27% 23 | node-sqlite3 x 2,930 ops/sec ±0.37% 24 | 25 | --- iterating over 100 rows --- 26 | better-sqlite3 x 6,532 ops/sec ±0.32% 27 | node-sqlite3 x 268 ops/sec ±3.4% 28 | 29 | --- inserting rows individually --- 30 | better-sqlite3 x 62,554 ops/sec ±7.33% 31 | node-sqlite3 x 22,637 ops/sec ±4.37% 32 | 33 | --- inserting 100 rows in a single transaction --- 34 | better-sqlite3 x 4,141 ops/sec ±4.57% 35 | node-sqlite3 x 265 ops/sec ±4.87% 36 | ``` 37 | 38 | > All benchmarks are executed in [WAL mode](./performance.md). 39 | -------------------------------------------------------------------------------- /docs/compilation.md: -------------------------------------------------------------------------------- 1 | # Custom configuration 2 | 3 | If you want to use a customized version of [SQLite](https://www.sqlite.org) with `better-sqlite3`, you can do so by specifying the directory of your [custom amalgamation](https://www.sqlite.org/amalgamation.html) during installation. 4 | 5 | ```bash 6 | npm install better-sqlite3 --build-from-source --sqlite3=/path/to/sqlite-amalgamation 7 | ``` 8 | 9 | However, if you simply run `npm install` while `better-sqlite3` is listed as a dependency in your `package.json`, the required flags above will *not* be applied. Therefore, it's recommended that you remove `better-sqlite3` from your dependency list, and instead add a [`preinstall` script](https://docs.npmjs.com/misc/scripts) like the one shown below. 10 | 11 | ```json 12 | { 13 | "scripts": { 14 | "preinstall": "npm install better-sqlite3@'^7.0.0' --no-save --build-from-source --sqlite3=\"$(pwd)/sqlite-amalgamation\"" 15 | } 16 | } 17 | ``` 18 | 19 | Your amalgamation directory must contain `sqlite3.c` and `sqlite3.h`. Any desired [compile time options](https://www.sqlite.org/compile.html) must be defined directly within `sqlite3.c`, as shown below. 20 | 21 | ```c 22 | // These go at the top of the file 23 | #define SQLITE_ENABLE_FTS5 1 24 | #define SQLITE_DEFAULT_CACHE_SIZE 16000 25 | 26 | // ... the original content of the file remains below 27 | ``` 28 | 29 | ### Step by step example 30 | 31 | If you're creating a package that relies on a custom build of `better-sqlite3`, you can follow these steps to get started. 32 | 33 | 1. Download the SQLite source code from [their website](https://sqlite.com/download.html) (e.g., `sqlite-amalgamation-1234567.zip`) 34 | 2. Unzip the compressed archive 35 | 3. Move the `sqlite3.c` and `sqlite3.h` files to your project folder 36 | 4. Add a `preinstall` script to your `package.json`, like the one shown above 37 | 6. Make sure the `--sqlite3` flag points to the location of your `sqlite3.c` and `sqlite3.h` files 38 | 7. Define your preferred [compile time options](https://www.sqlite.org/compile.html) at the top of `sqlite3.c` 39 | 8. Make sure to remove `better-sqlite3` from your `dependencies` 40 | 9. Run `npm install` in your project folder 41 | 42 | If you're using a SQLite encryption extension that is a drop-in replacement for SQLite (such as [SEE](https://www.sqlite.org/see/doc/release/www/readme.wiki) or [sqleet](https://github.com/resilar/sqleet)), then simply replace `sqlite3.c` and `sqlite3.h` with the source files of your encryption extension. 43 | 44 | # Bundled configuration 45 | 46 | By default, this distribution currently uses SQLite **version 3.49.2** with the following [compilation options](https://www.sqlite.org/compile.html): 47 | 48 | ``` 49 | HAVE_INT16_T=1 50 | HAVE_INT32_T=1 51 | HAVE_INT8_T=1 52 | HAVE_STDINT_H=1 53 | HAVE_UINT16_T=1 54 | HAVE_UINT32_T=1 55 | HAVE_UINT8_T=1 56 | HAVE_USLEEP=1 57 | SQLITE_DEFAULT_CACHE_SIZE=-16000 58 | SQLITE_DEFAULT_FOREIGN_KEYS=1 59 | SQLITE_DEFAULT_MEMSTATUS=0 60 | SQLITE_DEFAULT_WAL_SYNCHRONOUS=1 61 | SQLITE_DQS=0 62 | SQLITE_ENABLE_COLUMN_METADATA 63 | SQLITE_ENABLE_DBSTAT_VTAB 64 | SQLITE_ENABLE_DESERIALIZE 65 | SQLITE_ENABLE_FTS3 66 | SQLITE_ENABLE_FTS3_PARENTHESIS 67 | SQLITE_ENABLE_FTS4 68 | SQLITE_ENABLE_FTS5 69 | SQLITE_ENABLE_GEOPOLY 70 | SQLITE_ENABLE_JSON1 71 | SQLITE_ENABLE_MATH_FUNCTIONS 72 | SQLITE_ENABLE_RTREE 73 | SQLITE_ENABLE_STAT4 74 | SQLITE_ENABLE_UPDATE_DELETE_LIMIT 75 | SQLITE_LIKE_DOESNT_MATCH_BLOBS 76 | SQLITE_OMIT_DEPRECATED 77 | SQLITE_OMIT_PROGRESS_CALLBACK 78 | SQLITE_OMIT_SHARED_CACHE 79 | SQLITE_OMIT_TCL_VARIABLE 80 | SQLITE_SOUNDEX 81 | SQLITE_THREADSAFE=2 82 | SQLITE_TRACE_SIZE_LIMIT=32 83 | SQLITE_USE_URI=0 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/conduct.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | Topics of discussion are expected to be constrained such that all discussion is relevant to the following goals: 4 | 5 | - Maintaining `better-sqlite3`'s code, documentation, and build artifacts 6 | - Helping people *get started* in using `better-sqlite3` within their software projects 7 | 8 | Other areas of discussion are considered to be off-topic, including but not limited to: 9 | 10 | - Politics 11 | - Name-calling, insults 12 | - Help with using SQLite (there's already [very good documentation](https://sqlite.org/docs.html) for that) 13 | - Help with application architecture, and other high-level decisions about software projects 14 | - Attention to personal traits such as race, gender, religion, national origin, sexual orientation, disability, etc. 15 | 16 | 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. 17 | 18 | Attempting to physically seize, sabotage, or distribute malware through `better-sqlite3` will result in being permanently banned from the community, without warning. 19 | -------------------------------------------------------------------------------- /docs/integer.md: -------------------------------------------------------------------------------- 1 | # The `BigInt` primitive type 2 | 3 | SQLite can store data in 64-bit signed integers, which are too big for JavaScript's [number format](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) to fully represent. To support this data type, `better-sqlite3` is fully compatible with [BigInts](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt). 4 | 5 | ```js 6 | const big = BigInt('1152735103331642317'); 7 | big === 1152735103331642317n; // returns true 8 | big.toString(); // returns "1152735103331642317" 9 | typeof big; // returns "bigint" 10 | ``` 11 | 12 | ## Binding BigInts 13 | 14 | `BigInts` can bind to [`Statements`](./api.md#class-statement) just like regular numbers. You can also return `BigInts` from [user-defined functions](./api.md#functionname-options-function---this). However, if you provide a `BigInt` that's too large to be a 64-bit signed integer, you'll get an error so that data integrity is protected. 15 | 16 | ```js 17 | db.prepare("SELECT * FROM users WHERE id=?").get(BigInt('1152735103331642317')); 18 | db.prepare("INSERT INTO users (id) VALUES (?)").run(BigInt('1152735103331642317')); 19 | 20 | db.prepare("SELECT ?").get(2n ** 63n - 1n); // returns successfully 21 | db.prepare("SELECT ?").get(2n ** 63n); // throws a RangeError 22 | ``` 23 | 24 | ## Getting BigInts from the database 25 | 26 | By default, integers returned from the database (including the [`info.lastInsertRowid`](./api.md#runbindparameters---object) property) are normal JavaScript numbers. You can change this default as you please: 27 | 28 | ```js 29 | db.defaultSafeIntegers(); // BigInts by default 30 | db.defaultSafeIntegers(true); // BigInts by default 31 | db.defaultSafeIntegers(false); // Numbers by default 32 | ``` 33 | 34 | Additionally, you can override the default for individual [`Statements`](./api.md#class-statement) like so: 35 | 36 | ```js 37 | const stmt = db.prepare(SQL); 38 | 39 | stmt.safeIntegers(); // Safe integers ON 40 | stmt.safeIntegers(true); // Safe integers ON 41 | stmt.safeIntegers(false); // Safe integers OFF 42 | ``` 43 | 44 | [User-defined functions](./api.md#functionname-options-function---this) can receive `BigInts` as arguments. You can override the database's default setting like so: 45 | 46 | ```js 47 | db.function('isInt', { safeIntegers: true }, (value) => { 48 | return String(typeof value === 'bigint'); 49 | }); 50 | 51 | db.prepare('SELECT isInt(?)').pluck().get(10); // => "false" 52 | db.prepare('SELECT isInt(?)').pluck().get(10n); // => "true" 53 | ``` 54 | 55 | Likewise, [user-defined aggregates](./api.md#aggregatename-options---this) and [virtual tables](./api.md#tablename-definition---this) can also receive `BigInts` as arguments: 56 | 57 | ```js 58 | db.aggregate('addInts', { 59 | safeIntegers: true, 60 | start: 0n, 61 | step: (total, nextValue) => total + nextValue, 62 | }); 63 | ``` 64 | 65 | ```js 66 | db.table('sequence', { 67 | safeIntegers: true, 68 | columns: ['value'], 69 | parameters: ['length', 'start'], 70 | rows: function* (length, start = 0n) { 71 | const end = start + length; 72 | for (let n = start; n < end; ++n) { 73 | yield { value: n }; 74 | } 75 | }, 76 | }); 77 | ``` 78 | 79 | It's worth noting that REAL (FLOAT) values returned from the database will always be represented as normal numbers. 80 | -------------------------------------------------------------------------------- /docs/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Concurrently reading and writing from an SQLite database can be very slow in some cases. Since concurrency is usually very important in web applications, it's recommended to turn on [WAL mode](https://www.sqlite.org/wal.html) to greatly increase overall performance. 4 | 5 | ```js 6 | db.pragma('journal_mode = WAL'); 7 | ``` 8 | 9 | WAL mode has a *few* disadvantages to consider: 10 | 11 | - Transactions that involve ATTACHed databases are atomic for each individual database, but are not atomic across all databases as a set. 12 | - Under rare circumstances, the [WAL file](https://www.sqlite.org/wal.html) may experience "checkpoint starvation" (see below). 13 | - There are some hardware/system limitations that may affect some users, [listed here](https://www.sqlite.org/wal.html). 14 | 15 | However, you trade those disadvantages for extremely fast performance in most web applications. 16 | 17 | ## Checkpoint starvation 18 | 19 | Checkpoint starvation is when SQLite is unable to recycle the [WAL file](https://www.sqlite.org/wal.html) due to everlasting concurrent reads to the database. If this happens, the WAL file will grow without bound, leading to unacceptable amounts of disk usage and deteriorating performance. 20 | 21 | If you don't access the database from multiple processes or threads simultaneously, you'll never encounter this issue. 22 | 23 | If you do access the database from multiple processes or threads simultaneously, just use the [`wal_checkpoint(RESTART)`](https://www.sqlite.org/pragma.html#pragma_wal_checkpoint) pragma when the WAL file gets too big. 24 | 25 | ```js 26 | setInterval(fs.stat.bind(null, 'foobar.db-wal', (err, stat) => { 27 | if (err) { 28 | if (err.code !== 'ENOENT') throw err; 29 | } else if (stat.size > someUnacceptableSize) { 30 | db.pragma('wal_checkpoint(RESTART)'); 31 | } 32 | }), 5000).unref(); 33 | ``` 34 | 35 | ## A note about durability 36 | 37 | This distribution of SQLite uses the `SQLITE_DEFAULT_WAL_SYNCHRONOUS=1` [compile-time option](https://sqlite.org/compile.html#default_wal_synchronous), which makes databases in WAL mode default to the ["NORMAL" synchronous setting](https://sqlite.org/pragma.html#pragma_synchronous). This allows applications to achieve extreme performance, but introduces a slight loss of [durability](https://en.wikipedia.org/wiki/Durability_(database_systems)) while in WAL mode. 38 | 39 | You can override this setting by running `db.pragma('synchronous = FULL')`. 40 | -------------------------------------------------------------------------------- /docs/threads.md: -------------------------------------------------------------------------------- 1 | # Worker threads 2 | 3 | For most applications, `better-sqlite3` is fast enough to use in the main thread without blocking for a noticeable amount of time. However, if you need to perform very slow queries, you have the option of using [worker threads](https://nodejs.org/api/worker_threads.html) to keep things running smoothly. Below is an example of using a thread pool to perform queries in the background. 4 | 5 | ### worker.js 6 | 7 | The worker logic is very simple in our case. It accepts messages from the master thread, executes each message's SQL (with any given parameters), and sends back the query results. 8 | 9 | ```js 10 | const { parentPort } = require('worker_threads'); 11 | const db = require('better-sqlite3')('foobar.db'); 12 | 13 | parentPort.on('message', ({ sql, parameters }) => { 14 | const result = db.prepare(sql).all(...parameters); 15 | parentPort.postMessage(result); 16 | }); 17 | ``` 18 | 19 | ### master.js 20 | 21 | The master thread is responsible for spawning workers, respawning threads that crash, and accepting query jobs. 22 | 23 | ```js 24 | const { Worker } = require('worker_threads'); 25 | const os = require('os'); 26 | 27 | /* 28 | Export a function that queues pending work. 29 | */ 30 | 31 | const queue = []; 32 | exports.asyncQuery = (sql, ...parameters) => { 33 | return new Promise((resolve, reject) => { 34 | queue.push({ 35 | resolve, 36 | reject, 37 | message: { sql, parameters }, 38 | }); 39 | drainQueue(); 40 | }); 41 | }; 42 | 43 | /* 44 | Instruct workers to drain the queue. 45 | */ 46 | 47 | let workers = []; 48 | function drainQueue() { 49 | for (const worker of workers) { 50 | worker.takeWork(); 51 | } 52 | } 53 | 54 | /* 55 | Spawn workers that try to drain the queue. 56 | */ 57 | 58 | new Array(os.availableParallelism()).fill(null).forEach(function spawn() { 59 | const worker = new Worker('./worker.js'); 60 | 61 | let job = null; // Current item from the queue 62 | let error = null; // Error that caused the worker to crash 63 | 64 | function takeWork() { 65 | if (!job && queue.length) { 66 | // If there's a job in the queue, send it to the worker 67 | job = queue.shift(); 68 | worker.postMessage(job.message); 69 | } 70 | } 71 | 72 | worker 73 | .on('online', () => { 74 | workers.push({ takeWork }); 75 | takeWork(); 76 | }) 77 | .on('message', (result) => { 78 | job.resolve(result); 79 | job = null; 80 | takeWork(); // Check if there's more work to do 81 | }) 82 | .on('error', (err) => { 83 | console.error(err); 84 | error = err; 85 | }) 86 | .on('exit', (code) => { 87 | workers = workers.filter(w => w.takeWork !== takeWork); 88 | if (job) { 89 | job.reject(error || new Error('worker died')); 90 | } 91 | if (code !== 0) { 92 | console.error(`worker exited with code ${code}`); 93 | spawn(); // Worker died, so spawn a new one 94 | } 95 | }); 96 | }); 97 | ``` 98 | -------------------------------------------------------------------------------- /docs/tips.md: -------------------------------------------------------------------------------- 1 | # Helpful tips for SQLite 2 | 3 | ## Creating good tables 4 | 5 | It's a good idea to use `INTEGER PRIMARY KEY AUTOINCREMENT` as one of the columns in a table. This ensures two things: 6 | 7 | - `INTEGER PRIMARY KEY`: improved performance by reusing SQLite's built-in `rowid` column. 8 | - `AUTOINCREMENT`: no future row will have the same ID as an old one that was deleted. This can prevent potential bugs and security breaches. 9 | 10 | If you don't use `INTEGER PRIMARY KEY`, then you *must* use `NOT NULL` in all of your your primary key columns. Otherwise you'll be victim to an SQLite bug that allows primary keys to be `NULL`. 11 | 12 | Any column with `INTEGER PRIMARY KEY` will automatically increment when setting its value to `NULL`. But without `AUTOINCREMENT`, the behavior only ensures uniqueness from currently existing rows. 13 | 14 | It should be noted that `NULL` values count as unique from each other. This has implications when using the `UNIQUE` contraint or any other equality test. 15 | 16 | ## Default values 17 | 18 | When a column has a `DEFAULT` value, it only gets applied when no value is specified for an `INSERT` statement. If the `INSERT` statement specifies a `NULL` value, the `DEFAULT` value is **NOT** used. 19 | 20 | ## Foreign keys 21 | 22 | Foreign key constraints are not enforced if the child's column value is `NULL`. To ensure that a relationship is always enforced, use `NOT NULL` on the child column. 23 | 24 | Example: 25 | ```sql 26 | CREATE TABLE comments (value TEXT, user_id INTEGER NOT NULL REFERENCES users); 27 | ``` 28 | 29 | Foreign key clauses can be followed by `ON DELETE` and/or `ON UPDATE`, with the following possible values: 30 | 31 | - `SET NULL`: if the parent column is deleted or updated, the child column becomes `NULL`. 32 | - *NOTE: This still causes a constraint violation if the child column has `NOT NULL`*. 33 | - `SET DEFAULT`: if the parent column is updated or deleted, the child column becomes its `DEFAULT` value. 34 | - *NOTE: This still causes a constraint violation if the child column's `DEFAULT` value does not correspond with an actual parent row*. 35 | - `CASCADE`: if the parent row is deleted, the child row is deleted; if the parent column is updated, the new value is propagated to the child column. 36 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting installation 2 | 3 | If `better-sqlite3` refuses to install, follow these guidelines: 4 | 5 | ## Use the latest version of better-sqlite3 6 | 7 | - Check the [releases page](https://github.com/WiseLibs/better-sqlite3/releases) to make sure you're using the latest and greatest. 8 | 9 | ## Install a recent Node.js 10 | 11 | - Make sure you're using a [supported version of Node.js](https://nodejs.org/en/about/previous-releases). `better-sqlite3` is only tested with currently-supported versions of Node.js. 12 | 13 | ## "Install the necessary tools" 14 | 15 | - If you're on Windows, during installation of Node.js, be sure to select "Automatically install the necessary tools" from the "Tools for Native Modules" page. 16 | 17 | - If you missed this when you installed Node.js, double-click `C:\Program Files\nodejs\install_tools.bat` from the File Explorer or run it in a terminal. 18 | 19 | This will open an administrative PowerShell terminal and installing Chocolatey, Visual Studio, and Python. 20 | 21 | This may take several minutes. 22 | 23 | ## No special characters in your project path 24 | 25 | - Make sure there are no spaces in your project path: `node-gyp` may not escape spaces or special characters (like `%` or `$`) properly. 26 | 27 | ## Electron 28 | 29 | 1. If you're using [Electron](https://github.com/electron/electron), use [`electron-rebuild`](https://www.npmjs.com/package/electron-rebuild). 30 | 31 | 2. If you're using an app.asar bundle, be sure all native libraries are "unpacked". If you're using [electron-forge]([url](https://www.electronforge.io)), you should use the [auto-unpack-natives plugin](https://www.electronforge.io/config/plugins/auto-unpack-natives) 32 | 33 | ## Windows 34 | 35 | If you still have issues, try these steps: 36 | 37 | 1. Delete your `node_modules` subdirectory 38 | 1. Delete your `$HOME/.node-gyp` directory 39 | 1. Run `npm install` 40 | 41 | ## Still stuck? 42 | 43 | Browse [previous installation issues](https://github.com/WiseLibs/better-sqlite3/issues?q=is%3Aissue). 44 | -------------------------------------------------------------------------------- /docs/unsafe.md: -------------------------------------------------------------------------------- 1 | # Unsafe mode 2 | 3 | By default, `better-sqlite3` prevents you from doing things that might corrupt your database or cause undefined behavior. Such unsafe operations include: 4 | 5 | - Anything blocked by [`SQLITE_DBCONFIG_DEFENSIVE`](https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigdefensive) 6 | - Mutating the database while [iterating](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/api.md#iteratebindparameters---iterator) through a query's result set 7 | 8 | However, some advanced users might want to use these functionalities at their own risk. For this reason, users have the option of enabling "unsafe mode". 9 | 10 | ```js 11 | db.unsafeMode(); // Unsafe mode ON 12 | db.unsafeMode(true); // Unsafe mode ON 13 | db.unsafeMode(false); // Unsafe mode OFF 14 | ``` 15 | 16 | Unsafe mode can be toggled at any time, and independently for each database connection. While toggled on, `better-sqlite3` will not prevent you from performing the dangerous operations listed above. 17 | -------------------------------------------------------------------------------- /lib/database.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const util = require('./util'); 5 | const SqliteError = require('./sqlite-error'); 6 | 7 | let DEFAULT_ADDON; 8 | 9 | function Database(filenameGiven, options) { 10 | if (new.target == null) { 11 | return new Database(filenameGiven, options); 12 | } 13 | 14 | // Apply defaults 15 | let buffer; 16 | if (Buffer.isBuffer(filenameGiven)) { 17 | buffer = filenameGiven; 18 | filenameGiven = ':memory:'; 19 | } 20 | if (filenameGiven == null) filenameGiven = ''; 21 | if (options == null) options = {}; 22 | 23 | // Validate arguments 24 | if (typeof filenameGiven !== 'string') throw new TypeError('Expected first argument to be a string'); 25 | if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object'); 26 | if ('readOnly' in options) throw new TypeError('Misspelled option "readOnly" should be "readonly"'); 27 | if ('memory' in options) throw new TypeError('Option "memory" was removed in v7.0.0 (use ":memory:" filename instead)'); 28 | 29 | // Interpret options 30 | const filename = filenameGiven.trim(); 31 | const anonymous = filename === '' || filename === ':memory:'; 32 | const readonly = util.getBooleanOption(options, 'readonly'); 33 | const fileMustExist = util.getBooleanOption(options, 'fileMustExist'); 34 | const timeout = 'timeout' in options ? options.timeout : 5000; 35 | const verbose = 'verbose' in options ? options.verbose : null; 36 | const nativeBinding = 'nativeBinding' in options ? options.nativeBinding : null; 37 | 38 | // Validate interpreted options 39 | if (readonly && anonymous && !buffer) throw new TypeError('In-memory/temporary databases cannot be readonly'); 40 | if (!Number.isInteger(timeout) || timeout < 0) throw new TypeError('Expected the "timeout" option to be a positive integer'); 41 | if (timeout > 0x7fffffff) throw new RangeError('Option "timeout" cannot be greater than 2147483647'); 42 | if (verbose != null && typeof verbose !== 'function') throw new TypeError('Expected the "verbose" option to be a function'); 43 | if (nativeBinding != null && typeof nativeBinding !== 'string' && typeof nativeBinding !== 'object') throw new TypeError('Expected the "nativeBinding" option to be a string or addon object'); 44 | 45 | // Load the native addon 46 | let addon; 47 | if (nativeBinding == null) { 48 | addon = DEFAULT_ADDON || (DEFAULT_ADDON = require('bindings')('better_sqlite3.node')); 49 | } else if (typeof nativeBinding === 'string') { 50 | // See 51 | const requireFunc = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : require; 52 | addon = requireFunc(path.resolve(nativeBinding).replace(/(\.node)?$/, '.node')); 53 | } else { 54 | // See 55 | addon = nativeBinding; 56 | } 57 | 58 | if (!addon.isInitialized) { 59 | addon.setErrorConstructor(SqliteError); 60 | addon.isInitialized = true; 61 | } 62 | 63 | // Make sure the specified directory exists 64 | if (!anonymous && !fs.existsSync(path.dirname(filename))) { 65 | throw new TypeError('Cannot open database because the directory does not exist'); 66 | } 67 | 68 | Object.defineProperties(this, { 69 | [util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null, buffer || null) }, 70 | ...wrappers.getters, 71 | }); 72 | } 73 | 74 | const wrappers = require('./methods/wrappers'); 75 | Database.prototype.prepare = wrappers.prepare; 76 | Database.prototype.transaction = require('./methods/transaction'); 77 | Database.prototype.pragma = require('./methods/pragma'); 78 | Database.prototype.backup = require('./methods/backup'); 79 | Database.prototype.serialize = require('./methods/serialize'); 80 | Database.prototype.function = require('./methods/function'); 81 | Database.prototype.aggregate = require('./methods/aggregate'); 82 | Database.prototype.table = require('./methods/table'); 83 | Database.prototype.loadExtension = wrappers.loadExtension; 84 | Database.prototype.exec = wrappers.exec; 85 | Database.prototype.close = wrappers.close; 86 | Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers; 87 | Database.prototype.unsafeMode = wrappers.unsafeMode; 88 | Database.prototype[util.inspect] = require('./methods/inspect'); 89 | 90 | module.exports = Database; 91 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = require('./database'); 3 | module.exports.SqliteError = require('./sqlite-error'); 4 | -------------------------------------------------------------------------------- /lib/methods/aggregate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { getBooleanOption, cppdb } = require('../util'); 3 | 4 | module.exports = function defineAggregate(name, options) { 5 | // Validate arguments 6 | if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string'); 7 | if (typeof options !== 'object' || options === null) throw new TypeError('Expected second argument to be an options object'); 8 | if (!name) throw new TypeError('User-defined function name cannot be an empty string'); 9 | 10 | // Interpret options 11 | const start = 'start' in options ? options.start : null; 12 | const step = getFunctionOption(options, 'step', true); 13 | const inverse = getFunctionOption(options, 'inverse', false); 14 | const result = getFunctionOption(options, 'result', false); 15 | const safeIntegers = 'safeIntegers' in options ? +getBooleanOption(options, 'safeIntegers') : 2; 16 | const deterministic = getBooleanOption(options, 'deterministic'); 17 | const directOnly = getBooleanOption(options, 'directOnly'); 18 | const varargs = getBooleanOption(options, 'varargs'); 19 | let argCount = -1; 20 | 21 | // Determine argument count 22 | if (!varargs) { 23 | argCount = Math.max(getLength(step), inverse ? getLength(inverse) : 0); 24 | if (argCount > 0) argCount -= 1; 25 | if (argCount > 100) throw new RangeError('User-defined functions cannot have more than 100 arguments'); 26 | } 27 | 28 | this[cppdb].aggregate(start, step, inverse, result, name, argCount, safeIntegers, deterministic, directOnly); 29 | return this; 30 | }; 31 | 32 | const getFunctionOption = (options, key, required) => { 33 | const value = key in options ? options[key] : null; 34 | if (typeof value === 'function') return value; 35 | if (value != null) throw new TypeError(`Expected the "${key}" option to be a function`); 36 | if (required) throw new TypeError(`Missing required option "${key}"`); 37 | return null; 38 | }; 39 | 40 | const getLength = ({ length }) => { 41 | if (Number.isInteger(length) && length >= 0) return length; 42 | throw new TypeError('Expected function.length to be a positive integer'); 43 | }; 44 | -------------------------------------------------------------------------------- /lib/methods/backup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { promisify } = require('util'); 5 | const { cppdb } = require('../util'); 6 | const fsAccess = promisify(fs.access); 7 | 8 | module.exports = async function backup(filename, options) { 9 | if (options == null) options = {}; 10 | 11 | // Validate arguments 12 | if (typeof filename !== 'string') throw new TypeError('Expected first argument to be a string'); 13 | if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object'); 14 | 15 | // Interpret options 16 | filename = filename.trim(); 17 | const attachedName = 'attached' in options ? options.attached : 'main'; 18 | const handler = 'progress' in options ? options.progress : null; 19 | 20 | // Validate interpreted options 21 | if (!filename) throw new TypeError('Backup filename cannot be an empty string'); 22 | if (filename === ':memory:') throw new TypeError('Invalid backup filename ":memory:"'); 23 | if (typeof attachedName !== 'string') throw new TypeError('Expected the "attached" option to be a string'); 24 | if (!attachedName) throw new TypeError('The "attached" option cannot be an empty string'); 25 | if (handler != null && typeof handler !== 'function') throw new TypeError('Expected the "progress" option to be a function'); 26 | 27 | // Make sure the specified directory exists 28 | await fsAccess(path.dirname(filename)).catch(() => { 29 | throw new TypeError('Cannot save backup because the directory does not exist'); 30 | }); 31 | 32 | const isNewFile = await fsAccess(filename).then(() => false, () => true); 33 | return runBackup(this[cppdb].backup(this, attachedName, filename, isNewFile), handler || null); 34 | }; 35 | 36 | const runBackup = (backup, handler) => { 37 | let rate = 0; 38 | let useDefault = true; 39 | 40 | return new Promise((resolve, reject) => { 41 | setImmediate(function step() { 42 | try { 43 | const progress = backup.transfer(rate); 44 | if (!progress.remainingPages) { 45 | backup.close(); 46 | resolve(progress); 47 | return; 48 | } 49 | if (useDefault) { 50 | useDefault = false; 51 | rate = 100; 52 | } 53 | if (handler) { 54 | const ret = handler(progress); 55 | if (ret !== undefined) { 56 | if (typeof ret === 'number' && ret === ret) rate = Math.max(0, Math.min(0x7fffffff, Math.round(ret))); 57 | else throw new TypeError('Expected progress callback to return a number or undefined'); 58 | } 59 | } 60 | setImmediate(step); 61 | } catch (err) { 62 | backup.close(); 63 | reject(err); 64 | } 65 | }); 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /lib/methods/function.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { getBooleanOption, cppdb } = require('../util'); 3 | 4 | module.exports = function defineFunction(name, options, fn) { 5 | // Apply defaults 6 | if (options == null) options = {}; 7 | if (typeof options === 'function') { fn = options; options = {}; } 8 | 9 | // Validate arguments 10 | if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string'); 11 | if (typeof fn !== 'function') throw new TypeError('Expected last argument to be a function'); 12 | if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object'); 13 | if (!name) throw new TypeError('User-defined function name cannot be an empty string'); 14 | 15 | // Interpret options 16 | const safeIntegers = 'safeIntegers' in options ? +getBooleanOption(options, 'safeIntegers') : 2; 17 | const deterministic = getBooleanOption(options, 'deterministic'); 18 | const directOnly = getBooleanOption(options, 'directOnly'); 19 | const varargs = getBooleanOption(options, 'varargs'); 20 | let argCount = -1; 21 | 22 | // Determine argument count 23 | if (!varargs) { 24 | argCount = fn.length; 25 | if (!Number.isInteger(argCount) || argCount < 0) throw new TypeError('Expected function.length to be a positive integer'); 26 | if (argCount > 100) throw new RangeError('User-defined functions cannot have more than 100 arguments'); 27 | } 28 | 29 | this[cppdb].function(fn, name, argCount, safeIntegers, deterministic, directOnly); 30 | return this; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/methods/inspect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const DatabaseInspection = function Database() {}; 3 | 4 | module.exports = function inspect(depth, opts) { 5 | return Object.assign(new DatabaseInspection(), this); 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /lib/methods/pragma.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { getBooleanOption, cppdb } = require('../util'); 3 | 4 | module.exports = function pragma(source, options) { 5 | if (options == null) options = {}; 6 | if (typeof source !== 'string') throw new TypeError('Expected first argument to be a string'); 7 | if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object'); 8 | const simple = getBooleanOption(options, 'simple'); 9 | 10 | const stmt = this[cppdb].prepare(`PRAGMA ${source}`, this, true); 11 | return simple ? stmt.pluck().get() : stmt.all(); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/methods/serialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { cppdb } = require('../util'); 3 | 4 | module.exports = function serialize(options) { 5 | if (options == null) options = {}; 6 | 7 | // Validate arguments 8 | if (typeof options !== 'object') throw new TypeError('Expected first argument to be an options object'); 9 | 10 | // Interpret and validate options 11 | const attachedName = 'attached' in options ? options.attached : 'main'; 12 | if (typeof attachedName !== 'string') throw new TypeError('Expected the "attached" option to be a string'); 13 | if (!attachedName) throw new TypeError('The "attached" option cannot be an empty string'); 14 | 15 | return this[cppdb].serialize(attachedName); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/methods/table.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { cppdb } = require('../util'); 3 | 4 | module.exports = function defineTable(name, factory) { 5 | // Validate arguments 6 | if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string'); 7 | if (!name) throw new TypeError('Virtual table module name cannot be an empty string'); 8 | 9 | // Determine whether the module is eponymous-only or not 10 | let eponymous = false; 11 | if (typeof factory === 'object' && factory !== null) { 12 | eponymous = true; 13 | factory = defer(parseTableDefinition(factory, 'used', name)); 14 | } else { 15 | if (typeof factory !== 'function') throw new TypeError('Expected second argument to be a function or a table definition object'); 16 | factory = wrapFactory(factory); 17 | } 18 | 19 | this[cppdb].table(factory, name, eponymous); 20 | return this; 21 | }; 22 | 23 | function wrapFactory(factory) { 24 | return function virtualTableFactory(moduleName, databaseName, tableName, ...args) { 25 | const thisObject = { 26 | module: moduleName, 27 | database: databaseName, 28 | table: tableName, 29 | }; 30 | 31 | // Generate a new table definition by invoking the factory 32 | const def = apply.call(factory, thisObject, args); 33 | if (typeof def !== 'object' || def === null) { 34 | throw new TypeError(`Virtual table module "${moduleName}" did not return a table definition object`); 35 | } 36 | 37 | return parseTableDefinition(def, 'returned', moduleName); 38 | }; 39 | } 40 | 41 | function parseTableDefinition(def, verb, moduleName) { 42 | // Validate required properties 43 | if (!hasOwnProperty.call(def, 'rows')) { 44 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "rows" property`); 45 | } 46 | if (!hasOwnProperty.call(def, 'columns')) { 47 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "columns" property`); 48 | } 49 | 50 | // Validate "rows" property 51 | const rows = def.rows; 52 | if (typeof rows !== 'function' || Object.getPrototypeOf(rows) !== GeneratorFunctionPrototype) { 53 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "rows" property (should be a generator function)`); 54 | } 55 | 56 | // Validate "columns" property 57 | let columns = def.columns; 58 | if (!Array.isArray(columns) || !(columns = [...columns]).every(x => typeof x === 'string')) { 59 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "columns" property (should be an array of strings)`); 60 | } 61 | if (columns.length !== new Set(columns).size) { 62 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate column names`); 63 | } 64 | if (!columns.length) { 65 | throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with zero columns`); 66 | } 67 | 68 | // Validate "parameters" property 69 | let parameters; 70 | if (hasOwnProperty.call(def, 'parameters')) { 71 | parameters = def.parameters; 72 | if (!Array.isArray(parameters) || !(parameters = [...parameters]).every(x => typeof x === 'string')) { 73 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "parameters" property (should be an array of strings)`); 74 | } 75 | } else { 76 | parameters = inferParameters(rows); 77 | } 78 | if (parameters.length !== new Set(parameters).size) { 79 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate parameter names`); 80 | } 81 | if (parameters.length > 32) { 82 | throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with more than the maximum number of 32 parameters`); 83 | } 84 | for (const parameter of parameters) { 85 | if (columns.includes(parameter)) { 86 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with column "${parameter}" which was ambiguously defined as both a column and parameter`); 87 | } 88 | } 89 | 90 | // Validate "safeIntegers" option 91 | let safeIntegers = 2; 92 | if (hasOwnProperty.call(def, 'safeIntegers')) { 93 | const bool = def.safeIntegers; 94 | if (typeof bool !== 'boolean') { 95 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "safeIntegers" property (should be a boolean)`); 96 | } 97 | safeIntegers = +bool; 98 | } 99 | 100 | // Validate "directOnly" option 101 | let directOnly = false; 102 | if (hasOwnProperty.call(def, 'directOnly')) { 103 | directOnly = def.directOnly; 104 | if (typeof directOnly !== 'boolean') { 105 | throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "directOnly" property (should be a boolean)`); 106 | } 107 | } 108 | 109 | // Generate SQL for the virtual table definition 110 | const columnDefinitions = [ 111 | ...parameters.map(identifier).map(str => `${str} HIDDEN`), 112 | ...columns.map(identifier), 113 | ]; 114 | return [ 115 | `CREATE TABLE x(${columnDefinitions.join(', ')});`, 116 | wrapGenerator(rows, new Map(columns.map((x, i) => [x, parameters.length + i])), moduleName), 117 | parameters, 118 | safeIntegers, 119 | directOnly, 120 | ]; 121 | } 122 | 123 | function wrapGenerator(generator, columnMap, moduleName) { 124 | return function* virtualTable(...args) { 125 | /* 126 | We must defensively clone any buffers in the arguments, because 127 | otherwise the generator could mutate one of them, which would cause 128 | us to return incorrect values for hidden columns, potentially 129 | corrupting the database. 130 | */ 131 | const output = args.map(x => Buffer.isBuffer(x) ? Buffer.from(x) : x); 132 | for (let i = 0; i < columnMap.size; ++i) { 133 | output.push(null); // Fill with nulls to prevent gaps in array (v8 optimization) 134 | } 135 | for (const row of generator(...args)) { 136 | if (Array.isArray(row)) { 137 | extractRowArray(row, output, columnMap.size, moduleName); 138 | yield output; 139 | } else if (typeof row === 'object' && row !== null) { 140 | extractRowObject(row, output, columnMap, moduleName); 141 | yield output; 142 | } else { 143 | throw new TypeError(`Virtual table module "${moduleName}" yielded something that isn't a valid row object`); 144 | } 145 | } 146 | }; 147 | } 148 | 149 | function extractRowArray(row, output, columnCount, moduleName) { 150 | if (row.length !== columnCount) { 151 | throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an incorrect number of columns`); 152 | } 153 | const offset = output.length - columnCount; 154 | for (let i = 0; i < columnCount; ++i) { 155 | output[i + offset] = row[i]; 156 | } 157 | } 158 | 159 | function extractRowObject(row, output, columnMap, moduleName) { 160 | let count = 0; 161 | for (const key of Object.keys(row)) { 162 | const index = columnMap.get(key); 163 | if (index === undefined) { 164 | throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an undeclared column "${key}"`); 165 | } 166 | output[index] = row[key]; 167 | count += 1; 168 | } 169 | if (count !== columnMap.size) { 170 | throw new TypeError(`Virtual table module "${moduleName}" yielded a row with missing columns`); 171 | } 172 | } 173 | 174 | function inferParameters({ length }) { 175 | if (!Number.isInteger(length) || length < 0) { 176 | throw new TypeError('Expected function.length to be a positive integer'); 177 | } 178 | const params = []; 179 | for (let i = 0; i < length; ++i) { 180 | params.push(`$${i + 1}`); 181 | } 182 | return params; 183 | } 184 | 185 | const { hasOwnProperty } = Object.prototype; 186 | const { apply } = Function.prototype; 187 | const GeneratorFunctionPrototype = Object.getPrototypeOf(function*(){}); 188 | const identifier = str => `"${str.replace(/"/g, '""')}"`; 189 | const defer = x => () => x; 190 | -------------------------------------------------------------------------------- /lib/methods/transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { cppdb } = require('../util'); 3 | const controllers = new WeakMap(); 4 | 5 | module.exports = function transaction(fn) { 6 | if (typeof fn !== 'function') throw new TypeError('Expected first argument to be a function'); 7 | 8 | const db = this[cppdb]; 9 | const controller = getController(db, this); 10 | const { apply } = Function.prototype; 11 | 12 | // Each version of the transaction function has these same properties 13 | const properties = { 14 | default: { value: wrapTransaction(apply, fn, db, controller.default) }, 15 | deferred: { value: wrapTransaction(apply, fn, db, controller.deferred) }, 16 | immediate: { value: wrapTransaction(apply, fn, db, controller.immediate) }, 17 | exclusive: { value: wrapTransaction(apply, fn, db, controller.exclusive) }, 18 | database: { value: this, enumerable: true }, 19 | }; 20 | 21 | Object.defineProperties(properties.default.value, properties); 22 | Object.defineProperties(properties.deferred.value, properties); 23 | Object.defineProperties(properties.immediate.value, properties); 24 | Object.defineProperties(properties.exclusive.value, properties); 25 | 26 | // Return the default version of the transaction function 27 | return properties.default.value; 28 | }; 29 | 30 | // Return the database's cached transaction controller, or create a new one 31 | const getController = (db, self) => { 32 | let controller = controllers.get(db); 33 | if (!controller) { 34 | const shared = { 35 | commit: db.prepare('COMMIT', self, false), 36 | rollback: db.prepare('ROLLBACK', self, false), 37 | savepoint: db.prepare('SAVEPOINT `\t_bs3.\t`', self, false), 38 | release: db.prepare('RELEASE `\t_bs3.\t`', self, false), 39 | rollbackTo: db.prepare('ROLLBACK TO `\t_bs3.\t`', self, false), 40 | }; 41 | controllers.set(db, controller = { 42 | default: Object.assign({ begin: db.prepare('BEGIN', self, false) }, shared), 43 | deferred: Object.assign({ begin: db.prepare('BEGIN DEFERRED', self, false) }, shared), 44 | immediate: Object.assign({ begin: db.prepare('BEGIN IMMEDIATE', self, false) }, shared), 45 | exclusive: Object.assign({ begin: db.prepare('BEGIN EXCLUSIVE', self, false) }, shared), 46 | }); 47 | } 48 | return controller; 49 | }; 50 | 51 | // Return a new transaction function by wrapping the given function 52 | const wrapTransaction = (apply, fn, db, { begin, commit, rollback, savepoint, release, rollbackTo }) => function sqliteTransaction() { 53 | let before, after, undo; 54 | if (db.inTransaction) { 55 | before = savepoint; 56 | after = release; 57 | undo = rollbackTo; 58 | } else { 59 | before = begin; 60 | after = commit; 61 | undo = rollback; 62 | } 63 | before.run(); 64 | try { 65 | const result = apply.call(fn, this, arguments); 66 | if (result && typeof result.then === 'function') { 67 | throw new TypeError('Transaction function cannot return a promise'); 68 | } 69 | after.run(); 70 | return result; 71 | } catch (ex) { 72 | if (db.inTransaction) { 73 | undo.run(); 74 | if (undo !== rollback) after.run(); 75 | } 76 | throw ex; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /lib/methods/wrappers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { cppdb } = require('../util'); 3 | 4 | exports.prepare = function prepare(sql) { 5 | return this[cppdb].prepare(sql, this, false); 6 | }; 7 | 8 | exports.exec = function exec(sql) { 9 | this[cppdb].exec(sql); 10 | return this; 11 | }; 12 | 13 | exports.close = function close() { 14 | this[cppdb].close(); 15 | return this; 16 | }; 17 | 18 | exports.loadExtension = function loadExtension(...args) { 19 | this[cppdb].loadExtension(...args); 20 | return this; 21 | }; 22 | 23 | exports.defaultSafeIntegers = function defaultSafeIntegers(...args) { 24 | this[cppdb].defaultSafeIntegers(...args); 25 | return this; 26 | }; 27 | 28 | exports.unsafeMode = function unsafeMode(...args) { 29 | this[cppdb].unsafeMode(...args); 30 | return this; 31 | }; 32 | 33 | exports.getters = { 34 | name: { 35 | get: function name() { return this[cppdb].name; }, 36 | enumerable: true, 37 | }, 38 | open: { 39 | get: function open() { return this[cppdb].open; }, 40 | enumerable: true, 41 | }, 42 | inTransaction: { 43 | get: function inTransaction() { return this[cppdb].inTransaction; }, 44 | enumerable: true, 45 | }, 46 | readonly: { 47 | get: function readonly() { return this[cppdb].readonly; }, 48 | enumerable: true, 49 | }, 50 | memory: { 51 | get: function memory() { return this[cppdb].memory; }, 52 | enumerable: true, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /lib/sqlite-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const descriptor = { value: 'SqliteError', writable: true, enumerable: false, configurable: true }; 3 | 4 | function SqliteError(message, code) { 5 | if (new.target !== SqliteError) { 6 | return new SqliteError(message, code); 7 | } 8 | if (typeof code !== 'string') { 9 | throw new TypeError('Expected second argument to be a string'); 10 | } 11 | Error.call(this, message); 12 | descriptor.value = '' + message; 13 | Object.defineProperty(this, 'message', descriptor); 14 | Error.captureStackTrace(this, SqliteError); 15 | this.code = code; 16 | } 17 | Object.setPrototypeOf(SqliteError, Error); 18 | Object.setPrototypeOf(SqliteError.prototype, Error.prototype); 19 | Object.defineProperty(SqliteError.prototype, 'name', descriptor); 20 | module.exports = SqliteError; 21 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.getBooleanOption = (options, key) => { 4 | let value = false; 5 | if (key in options && typeof (value = options[key]) !== 'boolean') { 6 | throw new TypeError(`Expected the "${key}" option to be a boolean`); 7 | } 8 | return value; 9 | }; 10 | 11 | exports.cppdb = Symbol(); 12 | exports.inspect = Symbol.for('nodejs.util.inspect.custom'); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-sqlite3", 3 | "version": "11.10.0", 4 | "description": "The fastest and simplest library for SQLite in Node.js.", 5 | "homepage": "http://github.com/WiseLibs/better-sqlite3", 6 | "author": "Joshua Wise ", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/WiseLibs/better-sqlite3.git" 10 | }, 11 | "main": "lib/index.js", 12 | "files": [ 13 | "binding.gyp", 14 | "src/*.[ch]pp", 15 | "lib/**", 16 | "deps/**" 17 | ], 18 | "engines": { 19 | "node": "20.x || 22.x || 23.x || 24.x" 20 | }, 21 | "dependencies": { 22 | "bindings": "^1.5.0", 23 | "prebuild-install": "^7.1.1" 24 | }, 25 | "devDependencies": { 26 | "chai": "^4.3.8", 27 | "cli-color": "^2.0.3", 28 | "fs-extra": "^11.1.1", 29 | "mocha": "^10.2.0", 30 | "nodemark": "^0.3.0", 31 | "prebuild": "^13.0.1", 32 | "sqlite": "^5.0.1", 33 | "sqlite3": "^5.1.6" 34 | }, 35 | "scripts": { 36 | "install": "prebuild-install || node-gyp rebuild --release", 37 | "build-release": "node-gyp rebuild --release", 38 | "build-debug": "node-gyp rebuild --debug", 39 | "rebuild-release": "npm run lzz && npm run build-release", 40 | "rebuild-debug": "npm run lzz && npm run build-debug", 41 | "test": "mocha --exit --slow=75 --timeout=5000", 42 | "benchmark": "node benchmark", 43 | "download": "bash ./deps/download.sh", 44 | "lzz": "lzz -hx hpp -sx cpp -k BETTER_SQLITE3 -d -hl -sl -e ./src/better_sqlite3.lzz" 45 | }, 46 | "license": "MIT", 47 | "keywords": [ 48 | "sql", 49 | "sqlite", 50 | "sqlite3", 51 | "transactions", 52 | "user-defined functions", 53 | "aggregate functions", 54 | "window functions", 55 | "database" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/better_sqlite3.lzz: -------------------------------------------------------------------------------- 1 | #hdr 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #end 15 | 16 | #insert "util/macros.lzz" 17 | #insert "util/query-macros.lzz" 18 | #insert "util/constants.lzz" 19 | #insert "util/bind-map.lzz" 20 | struct Addon; 21 | class Statement; 22 | class Backup; 23 | #insert "objects/database.lzz" 24 | #insert "objects/statement.lzz" 25 | #insert "objects/statement-iterator.lzz" 26 | #insert "objects/backup.lzz" 27 | #insert "util/data-converter.lzz" 28 | #insert "util/custom-function.lzz" 29 | #insert "util/custom-aggregate.lzz" 30 | #insert "util/custom-table.lzz" 31 | #insert "util/data.lzz" 32 | #insert "util/binder.lzz" 33 | 34 | struct Addon { 35 | NODE_METHOD(JS_setErrorConstructor) { 36 | REQUIRE_ARGUMENT_FUNCTION(first, v8::Local SqliteError); 37 | OnlyAddon->SqliteError.Reset(OnlyIsolate, SqliteError); 38 | } 39 | 40 | static void Cleanup(void* ptr) { 41 | Addon* addon = static_cast(ptr); 42 | for (Database* db : addon->dbs) db->CloseHandles(); 43 | addon->dbs.clear(); 44 | delete addon; 45 | } 46 | 47 | explicit Addon(v8::Isolate* isolate) : 48 | privileged_info(NULL), 49 | next_id(0), 50 | cs(isolate) {} 51 | 52 | inline sqlite3_uint64 NextId() { 53 | return next_id++; 54 | } 55 | 56 | v8::Global Statement; 57 | v8::Global StatementIterator; 58 | v8::Global Backup; 59 | v8::Global SqliteError; 60 | NODE_ARGUMENTS_POINTER privileged_info; 61 | sqlite3_uint64 next_id; 62 | CS cs; 63 | std::set dbs; 64 | }; 65 | 66 | #src 67 | NODE_MODULE_INIT(/* exports, context */) { 68 | v8::Isolate* isolate = context->GetIsolate(); 69 | v8::HandleScope scope(isolate); 70 | 71 | // Initialize addon instance. 72 | Addon* addon = new Addon(isolate); 73 | v8::Local data = v8::External::New(isolate, addon); 74 | node::AddEnvironmentCleanupHook(isolate, Addon::Cleanup, addon); 75 | 76 | // Create and export native-backed classes and functions. 77 | exports->Set(context, InternalizedFromLatin1(isolate, "Database"), Database::Init(isolate, data)).FromJust(); 78 | exports->Set(context, InternalizedFromLatin1(isolate, "Statement"), Statement::Init(isolate, data)).FromJust(); 79 | exports->Set(context, InternalizedFromLatin1(isolate, "StatementIterator"), StatementIterator::Init(isolate, data)).FromJust(); 80 | exports->Set(context, InternalizedFromLatin1(isolate, "Backup"), Backup::Init(isolate, data)).FromJust(); 81 | exports->Set(context, InternalizedFromLatin1(isolate, "setErrorConstructor"), v8::FunctionTemplate::New(isolate, Addon::JS_setErrorConstructor, data)->GetFunction(context).ToLocalChecked()).FromJust(); 82 | 83 | // Store addon instance data. 84 | addon->Statement.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "Statement")).ToLocalChecked().As()); 85 | addon->StatementIterator.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "StatementIterator")).ToLocalChecked().As()); 86 | addon->Backup.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "Backup")).ToLocalChecked().As()); 87 | } 88 | #end 89 | -------------------------------------------------------------------------------- /src/objects/backup.lzz: -------------------------------------------------------------------------------- 1 | class Backup : public node::ObjectWrap { 2 | public: 3 | 4 | INIT(Init) { 5 | v8::Local t = NewConstructorTemplate(isolate, data, JS_new, "Backup"); 6 | SetPrototypeMethod(isolate, data, t, "transfer", JS_transfer); 7 | SetPrototypeMethod(isolate, data, t, "close", JS_close); 8 | return t->GetFunction(OnlyContext).ToLocalChecked(); 9 | } 10 | 11 | // Used to support ordered containers. 12 | static inline bool Compare(Backup const * const a, Backup const * const b) { 13 | return a->id < b->id; 14 | } 15 | 16 | // Whenever this is used, db->RemoveBackup must be invoked beforehand. 17 | void CloseHandles() { 18 | if (alive) { 19 | alive = false; 20 | std::string filename(sqlite3_db_filename(dest_handle, "main")); 21 | sqlite3_backup_finish(backup_handle); 22 | int status = sqlite3_close(dest_handle); 23 | assert(status == SQLITE_OK); ((void)status); 24 | if (unlink) remove(filename.c_str()); 25 | } 26 | } 27 | 28 | ~Backup() { 29 | if (alive) db->RemoveBackup(this); 30 | CloseHandles(); 31 | } 32 | 33 | private: 34 | 35 | explicit Backup( 36 | Database* db, 37 | sqlite3* dest_handle, 38 | sqlite3_backup* backup_handle, 39 | sqlite3_uint64 id, 40 | bool unlink 41 | ) : 42 | node::ObjectWrap(), 43 | db(db), 44 | dest_handle(dest_handle), 45 | backup_handle(backup_handle), 46 | id(id), 47 | alive(true), 48 | unlink(unlink) { 49 | assert(db != NULL); 50 | assert(dest_handle != NULL); 51 | assert(backup_handle != NULL); 52 | db->AddBackup(this); 53 | } 54 | 55 | NODE_METHOD(JS_new) { 56 | UseAddon; 57 | if (!addon->privileged_info) return ThrowTypeError("Disabled constructor"); 58 | assert(info.IsConstructCall()); 59 | Database* db = Unwrap(addon->privileged_info->This()); 60 | REQUIRE_DATABASE_OPEN(db->GetState()); 61 | REQUIRE_DATABASE_NOT_BUSY(db->GetState()); 62 | 63 | v8::Local database = (*addon->privileged_info)[0].As(); 64 | v8::Local attachedName = (*addon->privileged_info)[1].As(); 65 | v8::Local destFile = (*addon->privileged_info)[2].As(); 66 | bool unlink = (*addon->privileged_info)[3].As()->Value(); 67 | 68 | UseIsolate; 69 | sqlite3* dest_handle; 70 | v8::String::Utf8Value dest_file(isolate, destFile); 71 | v8::String::Utf8Value attached_name(isolate, attachedName); 72 | int mask = (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); 73 | 74 | if (sqlite3_open_v2(*dest_file, &dest_handle, mask, NULL) != SQLITE_OK) { 75 | Database::ThrowSqliteError(addon, dest_handle); 76 | int status = sqlite3_close(dest_handle); 77 | assert(status == SQLITE_OK); ((void)status); 78 | return; 79 | } 80 | 81 | sqlite3_extended_result_codes(dest_handle, 1); 82 | sqlite3_limit(dest_handle, SQLITE_LIMIT_LENGTH, INT_MAX); 83 | sqlite3_backup* backup_handle = sqlite3_backup_init(dest_handle, "main", db->GetHandle(), *attached_name); 84 | if (backup_handle == NULL) { 85 | Database::ThrowSqliteError(addon, dest_handle); 86 | int status = sqlite3_close(dest_handle); 87 | assert(status == SQLITE_OK); ((void)status); 88 | return; 89 | } 90 | 91 | Backup* backup = new Backup(db, dest_handle, backup_handle, addon->NextId(), unlink); 92 | backup->Wrap(info.This()); 93 | SetFrozen(isolate, OnlyContext, info.This(), addon->cs.database, database); 94 | 95 | info.GetReturnValue().Set(info.This()); 96 | } 97 | 98 | NODE_METHOD(JS_transfer) { 99 | Backup* backup = Unwrap(info.This()); 100 | REQUIRE_ARGUMENT_INT32(first, int pages); 101 | REQUIRE_DATABASE_OPEN(backup->db->GetState()); 102 | assert(backup->db->GetState()->busy == false); 103 | assert(backup->alive == true); 104 | 105 | sqlite3_backup* backup_handle = backup->backup_handle; 106 | int status = sqlite3_backup_step(backup_handle, pages) & 0xff; 107 | 108 | Addon* addon = backup->db->GetAddon(); 109 | if (status == SQLITE_OK || status == SQLITE_DONE || status == SQLITE_BUSY) { 110 | int total_pages = sqlite3_backup_pagecount(backup_handle); 111 | int remaining_pages = sqlite3_backup_remaining(backup_handle); 112 | UseIsolate; 113 | UseContext; 114 | v8::Local result = v8::Object::New(isolate); 115 | result->Set(ctx, addon->cs.totalPages.Get(isolate), v8::Int32::New(isolate, total_pages)).FromJust(); 116 | result->Set(ctx, addon->cs.remainingPages.Get(isolate), v8::Int32::New(isolate, remaining_pages)).FromJust(); 117 | info.GetReturnValue().Set(result); 118 | if (status == SQLITE_DONE) backup->unlink = false; 119 | } else { 120 | Database::ThrowSqliteError(addon, sqlite3_errstr(status), status); 121 | } 122 | } 123 | 124 | NODE_METHOD(JS_close) { 125 | Backup* backup = Unwrap(info.This()); 126 | assert(backup->db->GetState()->busy == false); 127 | if (backup->alive) backup->db->RemoveBackup(backup); 128 | backup->CloseHandles(); 129 | info.GetReturnValue().Set(info.This()); 130 | } 131 | 132 | Database* const db; 133 | sqlite3* const dest_handle; 134 | sqlite3_backup* const backup_handle; 135 | const sqlite3_uint64 id; 136 | bool alive; 137 | bool unlink; 138 | }; 139 | -------------------------------------------------------------------------------- /src/objects/statement-iterator.lzz: -------------------------------------------------------------------------------- 1 | class StatementIterator : public node::ObjectWrap { 2 | public: 3 | 4 | INIT(Init) { 5 | v8::Local t = NewConstructorTemplate(isolate, data, JS_new, "StatementIterator"); 6 | SetPrototypeMethod(isolate, data, t, "next", JS_next); 7 | SetPrototypeMethod(isolate, data, t, "return", JS_return); 8 | SetPrototypeSymbolMethod(isolate, data, t, v8::Symbol::GetIterator(isolate), JS_symbolIterator); 9 | return t->GetFunction(OnlyContext).ToLocalChecked(); 10 | } 11 | 12 | // The ~Statement destructor currently covers any state this object creates. 13 | // Additionally, we actually DON'T want to revert stmt->locked or db_state 14 | // ->iterators in this destructor, to ensure deterministic database access. 15 | ~StatementIterator() {} 16 | 17 | private: 18 | 19 | explicit StatementIterator(Statement* stmt, bool bound) : node::ObjectWrap(), 20 | stmt(stmt), 21 | handle(stmt->handle), 22 | db_state(stmt->db->GetState()), 23 | bound(bound), 24 | safe_ints(stmt->safe_ints), 25 | mode(stmt->mode), 26 | alive(true), 27 | logged(!db_state->has_logger) { 28 | assert(stmt != NULL); 29 | assert(handle != NULL); 30 | assert(stmt->bound == bound); 31 | assert(stmt->alive == true); 32 | assert(stmt->locked == false); 33 | assert(db_state->iterators < USHRT_MAX); 34 | stmt->locked = true; 35 | db_state->iterators += 1; 36 | } 37 | 38 | NODE_METHOD(JS_new) { 39 | UseAddon; 40 | if (!addon->privileged_info) return ThrowTypeError("Disabled constructor"); 41 | assert(info.IsConstructCall()); 42 | 43 | StatementIterator* iter; 44 | { 45 | NODE_ARGUMENTS info = *addon->privileged_info; 46 | STATEMENT_START_LOGIC(REQUIRE_STATEMENT_RETURNS_DATA, DOES_ADD_ITERATOR); 47 | iter = new StatementIterator(stmt, bound); 48 | } 49 | UseIsolate; 50 | UseContext; 51 | iter->Wrap(info.This()); 52 | SetFrozen(isolate, ctx, info.This(), addon->cs.statement, addon->privileged_info->This()); 53 | 54 | info.GetReturnValue().Set(info.This()); 55 | } 56 | 57 | NODE_METHOD(JS_next) { 58 | StatementIterator* iter = Unwrap(info.This()); 59 | REQUIRE_DATABASE_NOT_BUSY(iter->db_state); 60 | if (iter->alive) iter->Next(info); 61 | else info.GetReturnValue().Set(DoneRecord(OnlyIsolate, iter->db_state->addon)); 62 | } 63 | 64 | NODE_METHOD(JS_return) { 65 | StatementIterator* iter = Unwrap(info.This()); 66 | REQUIRE_DATABASE_NOT_BUSY(iter->db_state); 67 | if (iter->alive) iter->Return(info); 68 | else info.GetReturnValue().Set(DoneRecord(OnlyIsolate, iter->db_state->addon)); 69 | } 70 | 71 | NODE_METHOD(JS_symbolIterator) { 72 | info.GetReturnValue().Set(info.This()); 73 | } 74 | 75 | void Next(NODE_ARGUMENTS info) { 76 | assert(alive == true); 77 | db_state->busy = true; 78 | if (!logged) { 79 | logged = true; 80 | if (stmt->db->Log(OnlyIsolate, handle)) { 81 | db_state->busy = false; 82 | Throw(); 83 | return; 84 | } 85 | } 86 | int status = sqlite3_step(handle); 87 | db_state->busy = false; 88 | if (status == SQLITE_ROW) { 89 | UseIsolate; 90 | UseContext; 91 | info.GetReturnValue().Set( 92 | NewRecord(isolate, ctx, Data::GetRowJS(isolate, ctx, handle, safe_ints, mode), db_state->addon, false) 93 | ); 94 | } else { 95 | if (status == SQLITE_DONE) Return(info); 96 | else Throw(); 97 | } 98 | } 99 | 100 | void Return(NODE_ARGUMENTS info) { 101 | Cleanup(); 102 | STATEMENT_RETURN_LOGIC(DoneRecord(OnlyIsolate, db_state->addon)); 103 | } 104 | 105 | void Throw() { 106 | Cleanup(); 107 | Database* db = stmt->db; 108 | STATEMENT_THROW_LOGIC(); 109 | } 110 | 111 | void Cleanup() { 112 | assert(alive == true); 113 | alive = false; 114 | stmt->locked = false; 115 | db_state->iterators -= 1; 116 | sqlite3_reset(handle); 117 | } 118 | 119 | static inline v8::Local NewRecord(v8::Isolate* isolate, v8::Local ctx, v8::Local value, Addon* addon, bool done) { 120 | v8::Local record = v8::Object::New(isolate); 121 | record->Set(ctx, addon->cs.value.Get(isolate), value).FromJust(); 122 | record->Set(ctx, addon->cs.done.Get(isolate), v8::Boolean::New(isolate, done)).FromJust(); 123 | return record; 124 | } 125 | 126 | static inline v8::Local DoneRecord(v8::Isolate* isolate, Addon* addon) { 127 | return NewRecord(isolate, OnlyContext, v8::Undefined(isolate), addon, true); 128 | } 129 | 130 | Statement* const stmt; 131 | sqlite3_stmt* const handle; 132 | Database::State* const db_state; 133 | const bool bound; 134 | const bool safe_ints; 135 | const char mode; 136 | bool alive; 137 | bool logged; 138 | }; 139 | -------------------------------------------------------------------------------- /src/util/bind-map.lzz: -------------------------------------------------------------------------------- 1 | class BindMap { 2 | public: 3 | 4 | // This nested class represents a single mapping between a parameter name 5 | // and its associated parameter index in a prepared statement. 6 | class Pair { friend class BindMap; 7 | public: 8 | 9 | inline int GetIndex() { 10 | return index; 11 | } 12 | 13 | inline v8::Local GetName(v8::Isolate* isolate) { 14 | return name.Get(isolate); 15 | } 16 | 17 | private: 18 | 19 | explicit Pair(v8::Isolate* isolate, const char* name, int index) 20 | : name(isolate, InternalizedFromUtf8(isolate, name, -1)), index(index) {} 21 | 22 | explicit Pair(v8::Isolate* isolate, Pair* pair) 23 | : name(isolate, pair->name), index(pair->index) {} 24 | 25 | const v8::Global name; 26 | const int index; 27 | }; 28 | 29 | explicit BindMap(char _) { 30 | assert(_ == 0); 31 | pairs = NULL; 32 | capacity = 0; 33 | length = 0; 34 | } 35 | 36 | ~BindMap() { 37 | while (length) pairs[--length].~Pair(); 38 | FREE_ARRAY(pairs); 39 | } 40 | 41 | inline Pair* GetPairs() { 42 | return pairs; 43 | } 44 | 45 | inline int GetSize() { 46 | return length; 47 | } 48 | 49 | // Adds a pair to the bind map, expanding the capacity if necessary. 50 | void Add(v8::Isolate* isolate, const char* name, int index) { 51 | assert(name != NULL); 52 | if (capacity == length) Grow(isolate); 53 | new (pairs + length++) Pair(isolate, name, index); 54 | } 55 | 56 | private: 57 | 58 | void Grow(v8::Isolate* isolate) { 59 | assert(capacity == length); 60 | capacity = (capacity << 1) | 2; 61 | Pair* new_pairs = ALLOC_ARRAY(capacity); 62 | for (int i = 0; i < length; ++i) { 63 | new (new_pairs + i) Pair(isolate, pairs + i); 64 | pairs[i].~Pair(); 65 | } 66 | FREE_ARRAY(pairs); 67 | pairs = new_pairs; 68 | } 69 | 70 | Pair* pairs; 71 | int capacity; 72 | int length; 73 | }; 74 | -------------------------------------------------------------------------------- /src/util/binder.lzz: -------------------------------------------------------------------------------- 1 | class Binder { 2 | public: 3 | 4 | explicit Binder(sqlite3_stmt* _handle) { 5 | handle = _handle; 6 | param_count = sqlite3_bind_parameter_count(_handle); 7 | anon_index = 0; 8 | success = true; 9 | } 10 | 11 | bool Bind(NODE_ARGUMENTS info, int argc, Statement* stmt) { 12 | assert(anon_index == 0); 13 | Result result = BindArgs(info, argc, stmt); 14 | if (success && result.count != param_count) { 15 | if (result.count < param_count) { 16 | if (!result.bound_object && stmt->GetBindMap(OnlyIsolate)->GetSize()) { 17 | Fail(ThrowTypeError, "Missing named parameters"); 18 | } else { 19 | Fail(ThrowRangeError, "Too few parameter values were provided"); 20 | } 21 | } else { 22 | Fail(ThrowRangeError, "Too many parameter values were provided"); 23 | } 24 | } 25 | return success; 26 | } 27 | 28 | private: 29 | 30 | struct Result { 31 | int count; 32 | bool bound_object; 33 | }; 34 | 35 | #hdr 36 | static bool IsPlainObject(v8::Isolate* isolate, v8::Local obj); 37 | #end 38 | #src 39 | static bool IsPlainObject(v8::Isolate* isolate, v8::Local obj) { 40 | v8::Local proto = obj->GetPrototype(); 41 | 42 | #if defined NODE_MODULE_VERSION && NODE_MODULE_VERSION < 93 43 | v8::Local ctx = obj->CreationContext(); 44 | #else 45 | v8::Local ctx = obj->GetCreationContext().ToLocalChecked(); 46 | #endif 47 | 48 | ctx->Enter(); 49 | v8::Local baseProto = v8::Object::New(isolate)->GetPrototype(); 50 | ctx->Exit(); 51 | return proto->StrictEquals(baseProto) || proto->StrictEquals(v8::Null(isolate)); 52 | } 53 | #end 54 | 55 | void Fail(void (*Throw)(const char* _), const char* message) { 56 | assert(success == true); 57 | assert((Throw == NULL) == (message == NULL)); 58 | assert(Throw == ThrowError || Throw == ThrowTypeError || Throw == ThrowRangeError || Throw == NULL); 59 | if (Throw) Throw(message); 60 | success = false; 61 | } 62 | 63 | int NextAnonIndex() { 64 | while (sqlite3_bind_parameter_name(handle, ++anon_index) != NULL) {} 65 | return anon_index; 66 | } 67 | 68 | // Binds the value at the given index or throws an appropriate error. 69 | void BindValue(v8::Isolate* isolate, v8::Local value, int index) { 70 | int status = Data::BindValueFromJS(isolate, handle, index, value); 71 | if (status != SQLITE_OK) { 72 | switch (status) { 73 | case -1: 74 | return Fail(ThrowTypeError, "SQLite3 can only bind numbers, strings, bigints, buffers, and null"); 75 | case SQLITE_TOOBIG: 76 | return Fail(ThrowRangeError, "The bound string, buffer, or bigint is too big"); 77 | case SQLITE_RANGE: 78 | return Fail(ThrowRangeError, "Too many parameter values were provided"); 79 | case SQLITE_NOMEM: 80 | return Fail(ThrowError, "Out of memory"); 81 | default: 82 | return Fail(ThrowError, "An unexpected error occured while trying to bind parameters"); 83 | } 84 | assert(false); 85 | } 86 | } 87 | 88 | // Binds each value in the array or throws an appropriate error. 89 | // The number of successfully bound parameters is returned. 90 | int BindArray(v8::Isolate* isolate, v8::Local arr) { 91 | UseContext; 92 | uint32_t length = arr->Length(); 93 | if (length > INT_MAX) { 94 | Fail(ThrowRangeError, "Too many parameter values were provided"); 95 | return 0; 96 | } 97 | int len = static_cast(length); 98 | for (int i = 0; i < len; ++i) { 99 | v8::MaybeLocal maybeValue = arr->Get(ctx, i); 100 | if (maybeValue.IsEmpty()) { 101 | Fail(NULL, NULL); 102 | return i; 103 | } 104 | BindValue(isolate, maybeValue.ToLocalChecked(), NextAnonIndex()); 105 | if (!success) { 106 | return i; 107 | } 108 | } 109 | return len; 110 | } 111 | 112 | // Binds all named parameters using the values found in the given object. 113 | // The number of successfully bound parameters is returned. 114 | // If a named parameter is missing from the object, an error is thrown. 115 | // This should only be invoked once per instance. 116 | int BindObject(v8::Isolate* isolate, v8::Local obj, Statement* stmt) { 117 | UseContext; 118 | BindMap* bind_map = stmt->GetBindMap(isolate); 119 | BindMap::Pair* pairs = bind_map->GetPairs(); 120 | int len = bind_map->GetSize(); 121 | 122 | for (int i = 0; i < len; ++i) { 123 | v8::Local key = pairs[i].GetName(isolate); 124 | 125 | // Check if the named parameter was provided. 126 | v8::Maybe has_property = obj->HasOwnProperty(ctx, key); 127 | if (has_property.IsNothing()) { 128 | Fail(NULL, NULL); 129 | return i; 130 | } 131 | if (!has_property.FromJust()) { 132 | v8::String::Utf8Value param_name(isolate, key); 133 | Fail(ThrowRangeError, (std::string("Missing named parameter \"") + *param_name + "\"").c_str()); 134 | return i; 135 | } 136 | 137 | // Get the current property value. 138 | v8::MaybeLocal maybeValue = obj->Get(ctx, key); 139 | if (maybeValue.IsEmpty()) { 140 | Fail(NULL, NULL); 141 | return i; 142 | } 143 | 144 | BindValue(isolate, maybeValue.ToLocalChecked(), pairs[i].GetIndex()); 145 | if (!success) { 146 | return i; 147 | } 148 | } 149 | 150 | return len; 151 | } 152 | 153 | // Binds all parameters using the values found in the arguments object. 154 | // Anonymous parameter values can be directly in the arguments object or in an Array. 155 | // Named parameter values can be provided in a plain Object argument. 156 | // Only one plain Object argument may be provided. 157 | // If an error occurs, an appropriate error is thrown. 158 | // The return value is a struct indicating how many parameters were successfully bound 159 | // and whether or not it tried to bind an object. 160 | Result BindArgs(NODE_ARGUMENTS info, int argc, Statement* stmt) { 161 | UseIsolate; 162 | int count = 0; 163 | bool bound_object = false; 164 | 165 | for (int i = 0; i < argc; ++i) { 166 | v8::Local arg = info[i]; 167 | 168 | if (arg->IsArray()) { 169 | count += BindArray(isolate, arg.As()); 170 | if (!success) break; 171 | continue; 172 | } 173 | 174 | if (arg->IsObject() && !node::Buffer::HasInstance(arg)) { 175 | v8::Local obj = arg.As(); 176 | if (IsPlainObject(isolate, obj)) { 177 | if (bound_object) { 178 | Fail(ThrowTypeError, "You cannot specify named parameters in two different objects"); 179 | break; 180 | } 181 | bound_object = true; 182 | 183 | count += BindObject(isolate, obj, stmt); 184 | if (!success) break; 185 | continue; 186 | } else if (stmt->GetBindMap(isolate)->GetSize()) { 187 | Fail(ThrowTypeError, "Named parameters can only be passed within plain objects"); 188 | break; 189 | } 190 | } 191 | 192 | BindValue(isolate, arg, NextAnonIndex()); 193 | if (!success) break; 194 | count += 1; 195 | } 196 | 197 | return { count, bound_object }; 198 | } 199 | 200 | sqlite3_stmt* handle; 201 | int param_count; 202 | int anon_index; // This value should only be used by NextAnonIndex() 203 | bool success; // This value should only be set by Fail() 204 | }; 205 | -------------------------------------------------------------------------------- /src/util/custom-aggregate.lzz: -------------------------------------------------------------------------------- 1 | class CustomAggregate : public CustomFunction { 2 | public: 3 | 4 | explicit CustomAggregate( 5 | v8::Isolate* isolate, 6 | Database* db, 7 | const char* name, 8 | v8::Local start, 9 | v8::Local step, 10 | v8::Local inverse, 11 | v8::Local result, 12 | bool safe_ints 13 | ) : 14 | CustomFunction(isolate, db, name, step, safe_ints), 15 | invoke_result(result->IsFunction()), 16 | invoke_start(start->IsFunction()), 17 | inverse(isolate, inverse->IsFunction() ? inverse.As() : v8::Local()), 18 | result(isolate, result->IsFunction() ? result.As() : v8::Local()), 19 | start(isolate, start) {} 20 | 21 | static void xStep(sqlite3_context* invocation, int argc, sqlite3_value** argv) { 22 | xStepBase(invocation, argc, argv, &CustomAggregate::fn); 23 | } 24 | 25 | static void xInverse(sqlite3_context* invocation, int argc, sqlite3_value** argv) { 26 | xStepBase(invocation, argc, argv, &CustomAggregate::inverse); 27 | } 28 | 29 | static void xValue(sqlite3_context* invocation) { 30 | xValueBase(invocation, false); 31 | } 32 | 33 | static void xFinal(sqlite3_context* invocation) { 34 | xValueBase(invocation, true); 35 | } 36 | 37 | private: 38 | 39 | static inline void xStepBase(sqlite3_context* invocation, int argc, sqlite3_value** argv, const v8::Global CustomAggregate::*ptrtm) { 40 | AGGREGATE_START(); 41 | 42 | v8::Local args_fast[5]; 43 | v8::Local* args = argc <= 4 ? args_fast : ALLOC_ARRAY>(argc + 1); 44 | args[0] = acc->value.Get(isolate); 45 | if (argc != 0) Data::GetArgumentsJS(isolate, args + 1, argv, argc, self->safe_ints); 46 | 47 | v8::MaybeLocal maybeReturnValue = (self->*ptrtm).Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), argc + 1, args); 48 | if (args != args_fast) delete[] args; 49 | 50 | if (maybeReturnValue.IsEmpty()) { 51 | self->PropagateJSError(invocation); 52 | } else { 53 | v8::Local returnValue = maybeReturnValue.ToLocalChecked(); 54 | if (!returnValue->IsUndefined()) acc->value.Reset(isolate, returnValue); 55 | } 56 | } 57 | 58 | static inline void xValueBase(sqlite3_context* invocation, bool is_final) { 59 | AGGREGATE_START(); 60 | 61 | if (!is_final) { 62 | acc->is_window = true; 63 | } else if (acc->is_window) { 64 | DestroyAccumulator(invocation); 65 | return; 66 | } 67 | 68 | v8::Local result = acc->value.Get(isolate); 69 | if (self->invoke_result) { 70 | v8::MaybeLocal maybeResult = self->result.Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), 1, &result); 71 | if (maybeResult.IsEmpty()) { 72 | self->PropagateJSError(invocation); 73 | return; 74 | } 75 | result = maybeResult.ToLocalChecked(); 76 | } 77 | 78 | Data::ResultValueFromJS(isolate, invocation, result, self); 79 | if (is_final) DestroyAccumulator(invocation); 80 | } 81 | 82 | struct Accumulator { public: 83 | v8::Global value; 84 | bool initialized; 85 | bool is_window; 86 | } 87 | 88 | Accumulator* GetAccumulator(sqlite3_context* invocation) { 89 | Accumulator* acc = static_cast(sqlite3_aggregate_context(invocation, sizeof(Accumulator))); 90 | if (!acc->initialized) { 91 | assert(acc->value.IsEmpty()); 92 | acc->initialized = true; 93 | if (invoke_start) { 94 | v8::MaybeLocal maybeSeed = start.Get(isolate).As()->Call(OnlyContext, v8::Undefined(isolate), 0, NULL); 95 | if (maybeSeed.IsEmpty()) PropagateJSError(invocation); 96 | else acc->value.Reset(isolate, maybeSeed.ToLocalChecked()); 97 | } else { 98 | assert(!start.IsEmpty()); 99 | acc->value.Reset(isolate, start); 100 | } 101 | } 102 | return acc; 103 | } 104 | 105 | static void DestroyAccumulator(sqlite3_context* invocation) { 106 | Accumulator* acc = static_cast(sqlite3_aggregate_context(invocation, sizeof(Accumulator))); 107 | assert(acc->initialized); 108 | acc->value.Reset(); 109 | } 110 | 111 | void PropagateJSError(sqlite3_context* invocation) { 112 | DestroyAccumulator(invocation); 113 | CustomFunction::PropagateJSError(invocation); 114 | } 115 | 116 | const bool invoke_result; 117 | const bool invoke_start; 118 | const v8::Global inverse; 119 | const v8::Global result; 120 | const v8::Global start; 121 | }; 122 | -------------------------------------------------------------------------------- /src/util/custom-function.lzz: -------------------------------------------------------------------------------- 1 | class CustomFunction : protected DataConverter { 2 | public: 3 | 4 | explicit CustomFunction( 5 | v8::Isolate* isolate, 6 | Database* db, 7 | const char* name, 8 | v8::Local fn, 9 | bool safe_ints 10 | ) : 11 | name(name), 12 | db(db), 13 | isolate(isolate), 14 | fn(isolate, fn), 15 | safe_ints(safe_ints) {} 16 | 17 | virtual ~CustomFunction() {} 18 | 19 | static void xDestroy(void* self) { 20 | delete static_cast(self); 21 | } 22 | 23 | static void xFunc(sqlite3_context* invocation, int argc, sqlite3_value** argv) { 24 | FUNCTION_START(); 25 | 26 | v8::Local args_fast[4]; 27 | v8::Local* args = NULL; 28 | if (argc != 0) { 29 | args = argc <= 4 ? args_fast : ALLOC_ARRAY>(argc); 30 | Data::GetArgumentsJS(isolate, args, argv, argc, self->safe_ints); 31 | } 32 | 33 | v8::MaybeLocal maybeReturnValue = self->fn.Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), argc, args); 34 | if (args != args_fast) delete[] args; 35 | 36 | if (maybeReturnValue.IsEmpty()) self->PropagateJSError(invocation); 37 | else Data::ResultValueFromJS(isolate, invocation, maybeReturnValue.ToLocalChecked(), self); 38 | } 39 | 40 | protected: 41 | 42 | void PropagateJSError(sqlite3_context* invocation) { 43 | assert(db->GetState()->was_js_error == false); 44 | db->GetState()->was_js_error = true; 45 | sqlite3_result_error(invocation, "", 0); 46 | } 47 | 48 | std::string GetDataErrorPrefix() { 49 | return std::string("User-defined function ") + name + "() returned"; 50 | } 51 | 52 | private: 53 | const std::string name; 54 | Database* const db; 55 | protected: 56 | v8::Isolate* const isolate; 57 | const v8::Global fn; 58 | const bool safe_ints; 59 | }; 60 | -------------------------------------------------------------------------------- /src/util/data-converter.lzz: -------------------------------------------------------------------------------- 1 | class DataConverter { 2 | public: 3 | 4 | void ThrowDataConversionError(sqlite3_context* invocation, bool isBigInt) { 5 | if (isBigInt) { 6 | ThrowRangeError((GetDataErrorPrefix() + " a bigint that was too big").c_str()); 7 | } else { 8 | ThrowTypeError((GetDataErrorPrefix() + " an invalid value").c_str()); 9 | } 10 | PropagateJSError(invocation); 11 | } 12 | 13 | protected: 14 | 15 | virtual void PropagateJSError(sqlite3_context* invocation) = 0; 16 | virtual std::string GetDataErrorPrefix() = 0; 17 | }; 18 | -------------------------------------------------------------------------------- /src/util/data.lzz: -------------------------------------------------------------------------------- 1 | #define JS_VALUE_TO_SQLITE(to, value, isolate, ...) \ 2 | if (value->IsNumber()) { \ 3 | return sqlite3_##to##_double( \ 4 | __VA_ARGS__, \ 5 | value.As()->Value() \ 6 | ); \ 7 | } else if (value->IsBigInt()) { \ 8 | bool lossless; \ 9 | int64_t v = value.As()->Int64Value(&lossless); \ 10 | if (lossless) { \ 11 | return sqlite3_##to##_int64(__VA_ARGS__, v); \ 12 | } \ 13 | } else if (value->IsString()) { \ 14 | v8::String::Utf8Value utf8(isolate, value.As()); \ 15 | return sqlite3_##to##_text( \ 16 | __VA_ARGS__, \ 17 | *utf8, \ 18 | utf8.length(), \ 19 | SQLITE_TRANSIENT \ 20 | ); \ 21 | } else if (node::Buffer::HasInstance(value)) { \ 22 | const char* data = node::Buffer::Data(value); \ 23 | return sqlite3_##to##_blob( \ 24 | __VA_ARGS__, \ 25 | data ? data : "", \ 26 | node::Buffer::Length(value), \ 27 | SQLITE_TRANSIENT \ 28 | ); \ 29 | } else if (value->IsNull() || value->IsUndefined()) { \ 30 | return sqlite3_##to##_null(__VA_ARGS__); \ 31 | } 32 | 33 | #define SQLITE_VALUE_TO_JS(from, isolate, safe_ints, ...) \ 34 | switch (sqlite3_##from##_type(__VA_ARGS__)) { \ 35 | case SQLITE_INTEGER: \ 36 | if (safe_ints) { \ 37 | return v8::BigInt::New( \ 38 | isolate, \ 39 | sqlite3_##from##_int64(__VA_ARGS__) \ 40 | ); \ 41 | } \ 42 | case SQLITE_FLOAT: \ 43 | return v8::Number::New( \ 44 | isolate, \ 45 | sqlite3_##from##_double(__VA_ARGS__) \ 46 | ); \ 47 | case SQLITE_TEXT: \ 48 | return StringFromUtf8( \ 49 | isolate, \ 50 | reinterpret_cast(sqlite3_##from##_text(__VA_ARGS__)), \ 51 | sqlite3_##from##_bytes(__VA_ARGS__) \ 52 | ); \ 53 | case SQLITE_BLOB: \ 54 | return node::Buffer::Copy( \ 55 | isolate, \ 56 | static_cast(sqlite3_##from##_blob(__VA_ARGS__)), \ 57 | sqlite3_##from##_bytes(__VA_ARGS__) \ 58 | ).ToLocalChecked(); \ 59 | default: \ 60 | assert(sqlite3_##from##_type(__VA_ARGS__) == SQLITE_NULL); \ 61 | return v8::Null(isolate); \ 62 | } \ 63 | assert(false); 64 | 65 | namespace Data { 66 | 67 | static const char FLAT = 0; 68 | static const char PLUCK = 1; 69 | static const char EXPAND = 2; 70 | static const char RAW = 3; 71 | 72 | v8::Local GetValueJS(v8::Isolate* isolate, sqlite3_stmt* handle, int column, bool safe_ints) { 73 | SQLITE_VALUE_TO_JS(column, isolate, safe_ints, handle, column); 74 | } 75 | 76 | v8::Local GetValueJS(v8::Isolate* isolate, sqlite3_value* value, bool safe_ints) { 77 | SQLITE_VALUE_TO_JS(value, isolate, safe_ints, value); 78 | } 79 | 80 | v8::Local GetFlatRowJS(v8::Isolate* isolate, v8::Local ctx, sqlite3_stmt* handle, bool safe_ints) { 81 | v8::Local row = v8::Object::New(isolate); 82 | int column_count = sqlite3_column_count(handle); 83 | for (int i = 0; i < column_count; ++i) { 84 | row->Set(ctx, 85 | InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1), 86 | Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust(); 87 | } 88 | return row; 89 | } 90 | 91 | v8::Local GetExpandedRowJS(v8::Isolate* isolate, v8::Local ctx, sqlite3_stmt* handle, bool safe_ints) { 92 | v8::Local row = v8::Object::New(isolate); 93 | int column_count = sqlite3_column_count(handle); 94 | for (int i = 0; i < column_count; ++i) { 95 | const char* table_raw = sqlite3_column_table_name(handle, i); 96 | v8::Local table = InternalizedFromUtf8(isolate, table_raw == NULL ? "$" : table_raw, -1); 97 | v8::Local column = InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1); 98 | v8::Local value = Data::GetValueJS(isolate, handle, i, safe_ints); 99 | if (row->HasOwnProperty(ctx, table).FromJust()) { 100 | row->Get(ctx, table).ToLocalChecked().As()->Set(ctx, column, value).FromJust(); 101 | } else { 102 | v8::Local nested = v8::Object::New(isolate); 103 | row->Set(ctx, table, nested).FromJust(); 104 | nested->Set(ctx, column, value).FromJust(); 105 | } 106 | } 107 | return row; 108 | } 109 | 110 | v8::Local GetRawRowJS(v8::Isolate* isolate, v8::Local ctx, sqlite3_stmt* handle, bool safe_ints) { 111 | v8::Local row = v8::Array::New(isolate); 112 | int column_count = sqlite3_column_count(handle); 113 | for (int i = 0; i < column_count; ++i) { 114 | row->Set(ctx, i, Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust(); 115 | } 116 | return row; 117 | } 118 | 119 | v8::Local GetRowJS(v8::Isolate* isolate, v8::Local ctx, sqlite3_stmt* handle, bool safe_ints, char mode) { 120 | if (mode == FLAT) return GetFlatRowJS(isolate, ctx, handle, safe_ints); 121 | if (mode == PLUCK) return GetValueJS(isolate, handle, 0, safe_ints); 122 | if (mode == EXPAND) return GetExpandedRowJS(isolate, ctx, handle, safe_ints); 123 | if (mode == RAW) return GetRawRowJS(isolate, ctx, handle, safe_ints); 124 | assert(false); 125 | return v8::Local(); 126 | } 127 | 128 | void GetArgumentsJS(v8::Isolate* isolate, v8::Local* out, sqlite3_value** values, int argument_count, bool safe_ints) { 129 | assert(argument_count > 0); 130 | for (int i = 0; i < argument_count; ++i) { 131 | out[i] = Data::GetValueJS(isolate, values[i], safe_ints); 132 | } 133 | } 134 | 135 | int BindValueFromJS(v8::Isolate* isolate, sqlite3_stmt* handle, int index, v8::Local value) { 136 | JS_VALUE_TO_SQLITE(bind, value, isolate, handle, index); 137 | return value->IsBigInt() ? SQLITE_TOOBIG : -1; 138 | } 139 | 140 | void ResultValueFromJS(v8::Isolate* isolate, sqlite3_context* invocation, v8::Local value, DataConverter* converter) { 141 | JS_VALUE_TO_SQLITE(result, value, isolate, invocation); 142 | converter->ThrowDataConversionError(invocation, value->IsBigInt()); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/util/query-macros.lzz: -------------------------------------------------------------------------------- 1 | #define STATEMENT_BIND(handle) \ 2 | Binder binder(handle); \ 3 | if (!binder.Bind(info, info.Length(), stmt)) { \ 4 | sqlite3_clear_bindings(handle); \ 5 | return; \ 6 | } ((void)0) 7 | 8 | #define STATEMENT_THROW_LOGIC() \ 9 | db->ThrowDatabaseError(); \ 10 | if (!bound) { sqlite3_clear_bindings(handle); } \ 11 | return 12 | 13 | #define STATEMENT_RETURN_LOGIC(return_value) \ 14 | info.GetReturnValue().Set(return_value); \ 15 | if (!bound) { sqlite3_clear_bindings(handle); } \ 16 | return 17 | 18 | #define STATEMENT_START_LOGIC(RETURNS_DATA_CHECK, MUTATE_CHECK) \ 19 | Statement* stmt = Unwrap(info.This()); \ 20 | RETURNS_DATA_CHECK(); \ 21 | sqlite3_stmt* handle = stmt->handle; \ 22 | Database* db = stmt->db; \ 23 | REQUIRE_DATABASE_OPEN(db->GetState()); \ 24 | REQUIRE_DATABASE_NOT_BUSY(db->GetState()); \ 25 | MUTATE_CHECK(); \ 26 | const bool bound = stmt->bound; \ 27 | if (!bound) { \ 28 | STATEMENT_BIND(handle); \ 29 | } else if (info.Length() > 0) { \ 30 | return ThrowTypeError("This statement already has bound parameters"); \ 31 | } ((void)0) 32 | 33 | 34 | #define STATEMENT_THROW() db->GetState()->busy = false; STATEMENT_THROW_LOGIC() 35 | #define STATEMENT_RETURN(x) db->GetState()->busy = false; STATEMENT_RETURN_LOGIC(x) 36 | #define STATEMENT_START(x, y) \ 37 | STATEMENT_START_LOGIC(x, y); \ 38 | db->GetState()->busy = true; \ 39 | UseIsolate; \ 40 | if (db->Log(isolate, handle)) { \ 41 | STATEMENT_THROW(); \ 42 | } ((void)0) 43 | 44 | 45 | #define DOES_NOT_MUTATE() REQUIRE_STATEMENT_NOT_LOCKED(stmt) 46 | #define DOES_MUTATE() \ 47 | REQUIRE_STATEMENT_NOT_LOCKED(stmt); \ 48 | REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db->GetState()) 49 | #define DOES_ADD_ITERATOR() \ 50 | DOES_NOT_MUTATE(); \ 51 | if (db->GetState()->iterators == USHRT_MAX) \ 52 | return ThrowRangeError("Too many active database iterators") 53 | #define REQUIRE_STATEMENT_RETURNS_DATA() \ 54 | if (!stmt->returns_data) \ 55 | return ThrowTypeError("This statement does not return data. Use run() instead") 56 | #define ALLOW_ANY_STATEMENT() \ 57 | ((void)0) 58 | 59 | 60 | #define _FUNCTION_START(type) \ 61 | type* self = static_cast(sqlite3_user_data(invocation)); \ 62 | v8::Isolate* isolate = self->isolate; \ 63 | v8::HandleScope scope(isolate) 64 | 65 | #define FUNCTION_START() \ 66 | _FUNCTION_START(CustomFunction) 67 | 68 | #define AGGREGATE_START() \ 69 | _FUNCTION_START(CustomAggregate); \ 70 | Accumulator* acc = self->GetAccumulator(invocation); \ 71 | if (acc->value.IsEmpty()) return 72 | -------------------------------------------------------------------------------- /test/00.setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | const os = require('os'); 5 | const chai = require('chai'); 6 | 7 | const isWindows = os.platform().startsWith('win'); 8 | const tempDir = path.join(__dirname, '..', 'temp'); 9 | let dbId = 0; 10 | 11 | global.expect = chai.expect; 12 | global.util = { 13 | current: () => path.join(tempDir, `${dbId}.db`), 14 | next: () => (++dbId, global.util.current()), 15 | itUnix: isWindows ? it.skip : it, 16 | }; 17 | 18 | before(function () { 19 | fs.removeSync(tempDir); 20 | fs.ensureDirSync(tempDir); 21 | }); 22 | 23 | after(function () { 24 | fs.removeSync(tempDir); 25 | }); 26 | -------------------------------------------------------------------------------- /test/01.sqlite-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const { SqliteError } = require('../.'); 4 | 5 | describe('SqliteError', function () { 6 | it('should be a subclass of Error', function () { 7 | expect(SqliteError).to.be.a('function'); 8 | expect(SqliteError).to.not.equal(Error); 9 | expect(SqliteError.prototype).to.be.an.instanceof(Error); 10 | expect(SqliteError('foo', 'bar')).to.be.an.instanceof(Error); 11 | expect(new SqliteError('foo', 'bar')).to.be.an.instanceof(Error); 12 | }); 13 | it('should have the correct name', function () { 14 | expect(SqliteError.prototype.name).to.equal('SqliteError'); 15 | }); 16 | it('should accept two arguments for setting the message and error code', function () { 17 | const err = SqliteError('foobar', 'baz'); 18 | expect(err.message).to.equal('foobar'); 19 | expect(err.code).to.equal('baz'); 20 | expect(SqliteError(123, 'baz').message).to.equal('123'); 21 | expect(() => SqliteError('foo')).to.throw(TypeError); 22 | expect(() => SqliteError('foo', 123)).to.throw(TypeError); 23 | }); 24 | it('should capture stack traces', function () { 25 | expect(SqliteError(null, 'baz').stack).to.be.a('string'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/11.database.close.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { existsSync } = require('fs'); 3 | const Database = require('../.'); 4 | 5 | describe('Database#close()', function () { 6 | beforeEach(function () { 7 | this.db = new Database(util.next()); 8 | }); 9 | afterEach(function () { 10 | this.db.close(); 11 | }); 12 | 13 | it('should cause db.open to return false', function () { 14 | expect(this.db.open).to.be.true; 15 | this.db.close(); 16 | expect(this.db.open).to.be.false; 17 | }); 18 | it('should return the database object', function () { 19 | expect(this.db.open).to.be.true; 20 | expect(this.db.close()).to.equal(this.db); 21 | expect(this.db.open).to.be.false; 22 | expect(this.db.close()).to.equal(this.db); 23 | expect(this.db.open).to.be.false; 24 | }); 25 | it('should prevent any further database operations', function () { 26 | this.db.close(); 27 | expect(() => this.db.exec('CREATE TABLE people (name TEXT)')).to.throw(TypeError); 28 | expect(() => this.db.prepare('CREATE TABLE cats (name TEXT)')).to.throw(TypeError); 29 | expect(() => this.db.transaction(() => {})).to.throw(TypeError); 30 | expect(() => this.db.pragma('cache_size')).to.throw(TypeError); 31 | expect(() => this.db.function('foo', () => {})).to.throw(TypeError); 32 | expect(() => this.db.aggregate('foo', { step: () => {} })).to.throw(TypeError); 33 | expect(() => this.db.table('foo', () => {})).to.throw(TypeError); 34 | }); 35 | it('should prevent any existing statements from running', function () { 36 | this.db.prepare('CREATE TABLE people (name TEXT)').run(); 37 | const stmt1 = this.db.prepare('SELECT * FROM people'); 38 | const stmt2 = this.db.prepare("INSERT INTO people VALUES ('foobar')"); 39 | 40 | this.db.prepare('SELECT * FROM people').bind(); 41 | this.db.prepare("INSERT INTO people VALUES ('foobar')").bind(); 42 | this.db.prepare('SELECT * FROM people').get(); 43 | this.db.prepare('SELECT * FROM people').all(); 44 | this.db.prepare('SELECT * FROM people').iterate().return(); 45 | this.db.prepare("INSERT INTO people VALUES ('foobar')").run(); 46 | 47 | this.db.close(); 48 | 49 | expect(() => stmt1.bind()).to.throw(TypeError); 50 | expect(() => stmt2.bind()).to.throw(TypeError); 51 | expect(() => stmt1.get()).to.throw(TypeError); 52 | expect(() => stmt1.all()).to.throw(TypeError); 53 | expect(() => stmt1.iterate()).to.throw(TypeError); 54 | expect(() => stmt2.run()).to.throw(TypeError); 55 | }); 56 | it('should delete the database\'s associated temporary files', function () { 57 | expect(existsSync(util.current())).to.be.true; 58 | this.db.pragma('journal_mode = WAL'); 59 | this.db.prepare('CREATE TABLE people (name TEXT)').run(); 60 | this.db.prepare('INSERT INTO people VALUES (?)').run('foobar'); 61 | expect(existsSync(`${util.current()}-wal`)).to.be.true; 62 | 63 | this.db.close(); 64 | 65 | expect(existsSync(util.current())).to.be.true; 66 | expect(existsSync(`${util.current()}-wal`)).to.be.false; 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/12.database.pragma.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#pragma()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | }); 8 | afterEach(function () { 9 | this.db.close(); 10 | }); 11 | 12 | it('should throw an exception if a string is not provided', function () { 13 | expect(() => this.db.pragma(123)).to.throw(TypeError); 14 | expect(() => this.db.pragma(0)).to.throw(TypeError); 15 | expect(() => this.db.pragma(null)).to.throw(TypeError); 16 | expect(() => this.db.pragma()).to.throw(TypeError); 17 | expect(() => this.db.pragma(new String('cache_size'))).to.throw(TypeError); 18 | }); 19 | it('should throw an exception if boolean options are provided as non-booleans', function () { 20 | expect(() => this.db.pragma('cache_size', { simple: undefined })).to.throw(TypeError); 21 | }); 22 | it('should throw an exception if invalid/redundant SQL is provided', function () { 23 | expect(() => this.db.pragma('PRAGMA cache_size')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR'); 24 | expect(() => this.db.pragma('cache_size; PRAGMA cache_size')).to.throw(RangeError); 25 | }); 26 | it('should execute the pragma, returning rows of results', function () { 27 | const rows = this.db.pragma('cache_size'); 28 | expect(rows).to.be.an('array'); 29 | expect(rows[0]).to.be.an('object'); 30 | expect(rows[0].cache_size).to.be.a('number'); 31 | expect(rows[0].cache_size).to.equal(-16000); 32 | }); 33 | it('should optionally return simpler results', function () { 34 | expect(this.db.pragma('cache_size', { simple: false })).to.be.an('array'); 35 | const cache_size = this.db.pragma('cache_size', { simple: true }); 36 | expect(cache_size).to.be.a('number'); 37 | expect(cache_size).to.equal(-16000); 38 | expect(() => this.db.pragma('cache_size', true)).to.throw(TypeError); 39 | expect(() => this.db.pragma('cache_size', 123)).to.throw(TypeError); 40 | expect(() => this.db.pragma('cache_size', function () {})).to.throw(TypeError); 41 | expect(() => this.db.pragma('cache_size', NaN)).to.throw(TypeError); 42 | expect(() => this.db.pragma('cache_size', 'true')).to.throw(TypeError); 43 | }); 44 | it('should obey PRAGMA changes', function () { 45 | expect(this.db.pragma('cache_size', { simple: true })).to.equal(-16000); 46 | this.db.pragma('cache_size = -8000'); 47 | expect(this.db.pragma('cache_size', { simple: true })).to.equal(-8000); 48 | expect(this.db.pragma('journal_mode', { simple: true })).to.equal('delete'); 49 | this.db.pragma('journal_mode = wal'); 50 | expect(this.db.pragma('journal_mode', { simple: true })).to.equal('wal'); 51 | }); 52 | it('should respect readonly connections', function () { 53 | this.db.close(); 54 | this.db = new Database(util.current(), { readonly: true, fileMustExist: true }); 55 | expect(this.db.pragma('cache_size', { simple: true })).to.equal(-16000); 56 | this.db.pragma('cache_size = -8000'); 57 | expect(this.db.pragma('cache_size', { simple: true })).to.equal(-8000); 58 | expect(this.db.pragma('journal_mode', { simple: true })).to.equal('delete'); 59 | expect(() => this.db.pragma('journal_mode = wal')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_READONLY'); 60 | expect(this.db.pragma('journal_mode', { simple: true })).to.equal('delete'); 61 | }); 62 | it('should return undefined if no rows exist and simpler results are desired', function () { 63 | expect(this.db.pragma('table_info', { simple: true })).to.be.undefined; 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/13.database.prepare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#prepare()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | }); 8 | afterEach(function () { 9 | this.db.close(); 10 | }); 11 | 12 | function assertStmt(stmt, source, db, reader, readonly) { 13 | expect(stmt.source).to.equal(source); 14 | expect(stmt.constructor.name).to.equal('Statement'); 15 | expect(stmt.database).to.equal(db); 16 | expect(stmt.reader).to.equal(reader); 17 | expect(stmt.readonly).to.equal(readonly); 18 | expect(() => new stmt.constructor(source)).to.throw(TypeError); 19 | } 20 | 21 | it('should throw an exception if a string is not provided', function () { 22 | expect(() => this.db.prepare(123)).to.throw(TypeError); 23 | expect(() => this.db.prepare(0)).to.throw(TypeError); 24 | expect(() => this.db.prepare(null)).to.throw(TypeError); 25 | expect(() => this.db.prepare()).to.throw(TypeError); 26 | expect(() => this.db.prepare(new String('CREATE TABLE people (name TEXT)'))).to.throw(TypeError); 27 | }); 28 | it('should throw an exception if invalid SQL is provided', function () { 29 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR'); 30 | expect(() => this.db.prepare('INSERT INTO people VALUES (?)')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR'); 31 | }); 32 | it('should throw an exception if no statements are provided', function () { 33 | expect(() => this.db.prepare('')).to.throw(RangeError); 34 | expect(() => this.db.prepare(';')).to.throw(RangeError); 35 | }); 36 | it('should throw an exception if more than one statement is provided', function () { 37 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);CREATE TABLE animals (name TEXT)')).to.throw(RangeError); 38 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);/')).to.throw(RangeError); 39 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);-')).to.throw(RangeError); 40 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);--\n/')).to.throw(RangeError); 41 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);--\nSELECT 123')).to.throw(RangeError); 42 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);-- comment\nSELECT 123')).to.throw(RangeError); 43 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);/**/-')).to.throw(RangeError); 44 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);/**/SELECT 123')).to.throw(RangeError); 45 | expect(() => this.db.prepare('CREATE TABLE people (name TEXT);/* comment */SELECT 123')).to.throw(RangeError); 46 | }); 47 | it('should create a prepared Statement object', function () { 48 | const stmt1 = this.db.prepare('CREATE TABLE people (name TEXT) '); 49 | const stmt2 = this.db.prepare('CREATE TABLE people (name TEXT); '); 50 | assertStmt(stmt1, 'CREATE TABLE people (name TEXT) ', this.db, false, false); 51 | assertStmt(stmt2, 'CREATE TABLE people (name TEXT); ', this.db, false, false); 52 | expect(stmt1).to.not.equal(stmt2); 53 | expect(stmt1).to.not.equal(this.db.prepare('CREATE TABLE people (name TEXT) ')); 54 | }); 55 | it('should create a prepared Statement object with just an expression', function () { 56 | const stmt = this.db.prepare('SELECT 555'); 57 | assertStmt(stmt, 'SELECT 555', this.db, true, true); 58 | }); 59 | it('should set the correct values for "reader" and "readonly"', function () { 60 | this.db.exec('CREATE TABLE data (value)'); 61 | assertStmt(this.db.prepare('SELECT 555'), 'SELECT 555', this.db, true, true); 62 | assertStmt(this.db.prepare('BEGIN'), 'BEGIN', this.db, false, true); 63 | assertStmt(this.db.prepare('BEGIN EXCLUSIVE'), 'BEGIN EXCLUSIVE', this.db, false, false); 64 | assertStmt(this.db.prepare('DELETE FROM data RETURNING *'), 'DELETE FROM data RETURNING *', this.db, true, false); 65 | }); 66 | it('should create a prepared Statement object ignoring trailing comments and whitespace', function () { 67 | assertStmt(this.db.prepare('SELECT 555; '), 'SELECT 555; ', this.db, true, true); 68 | assertStmt(this.db.prepare('SELECT 555;-- comment'), 'SELECT 555;-- comment', this.db, true, true); 69 | assertStmt(this.db.prepare('SELECT 555;--abc\n--de\n--f'), 'SELECT 555;--abc\n--de\n--f', this.db, true, true); 70 | assertStmt(this.db.prepare('SELECT 555;/* comment */'), 'SELECT 555;/* comment */', this.db, true, true); 71 | assertStmt(this.db.prepare('SELECT 555;/* comment */-- comment'), 'SELECT 555;/* comment */-- comment', this.db, true, true); 72 | assertStmt(this.db.prepare('SELECT 555;-- comment\n/* comment */'), 'SELECT 555;-- comment\n/* comment */', this.db, true, true); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/14.database.exec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#exec()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | }); 8 | afterEach(function () { 9 | this.db.close(); 10 | }); 11 | 12 | it('should throw an exception if a string is not provided', function () { 13 | expect(() => this.db.exec(123)).to.throw(TypeError); 14 | expect(() => this.db.exec(0)).to.throw(TypeError); 15 | expect(() => this.db.exec(null)).to.throw(TypeError); 16 | expect(() => this.db.exec()).to.throw(TypeError); 17 | expect(() => this.db.exec(new String('CREATE TABLE entries (a TEXT, b INTEGER)'))).to.throw(TypeError); 18 | }); 19 | it('should throw an exception if invalid SQL is provided', function () { 20 | expect(() => this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR'); 21 | }); 22 | it('should obey the restrictions of readonly mode', function () { 23 | this.db.close(); 24 | this.db = new Database(util.current(), { readonly: true }); 25 | expect(() => this.db.exec('CREATE TABLE people (name TEXT)')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_READONLY'); 26 | this.db.exec('SELECT 555'); 27 | }); 28 | it('should execute the SQL, returning the database object itself', function () { 29 | const returnValues = []; 30 | 31 | const r1 = this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER)'); 32 | const r2 = this.db.exec("INSERT INTO entries VALUES ('foobar', 44); INSERT INTO entries VALUES ('baz', NULL);"); 33 | const r3 = this.db.exec('SELECT * FROM entries'); 34 | 35 | expect(r1).to.equal(this.db); 36 | expect(r2).to.equal(this.db); 37 | expect(r3).to.equal(this.db); 38 | 39 | const rows = this.db.prepare('SELECT * FROM entries ORDER BY rowid').all(); 40 | expect(rows.length).to.equal(2); 41 | expect(rows[0].a).to.equal('foobar'); 42 | expect(rows[0].b).to.equal(44); 43 | expect(rows[1].a).to.equal('baz'); 44 | expect(rows[1].b).to.equal(null); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/21.statement.get.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Statement#get()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare('CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)').run(); 8 | 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 10) SELECT * FROM temp").run(); 9 | }); 10 | afterEach(function () { 11 | this.db.close(); 12 | }); 13 | 14 | it('should throw an exception when used on a statement that returns no data', function () { 15 | let stmt = this.db.prepare("INSERT INTO entries VALUES ('foo', 1, 3.14, x'dddddddd', NULL)"); 16 | expect(stmt.reader).to.be.false; 17 | expect(() => stmt.get()).to.throw(TypeError); 18 | 19 | stmt = this.db.prepare("CREATE TABLE IF NOT EXISTS entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)"); 20 | expect(stmt.reader).to.be.false; 21 | expect(() => stmt.get()).to.throw(TypeError); 22 | 23 | stmt = this.db.prepare("BEGIN TRANSACTION"); 24 | expect(stmt.reader).to.be.false; 25 | expect(() => stmt.get()).to.throw(TypeError); 26 | }); 27 | it('should return the first matching row', function () { 28 | let stmt = this.db.prepare("SELECT * FROM entries ORDER BY rowid"); 29 | expect(stmt.reader).to.be.true; 30 | expect(stmt.get()).to.deep.equal({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }); 31 | 32 | stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid"); 33 | expect(stmt.get()).to.deep.equal({ a: 'foo', b: 6, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }); 34 | }); 35 | it('should work with RETURNING clause', function () { 36 | let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *"); 37 | expect(stmt.reader).to.be.true; 38 | expect(stmt.get()).to.deep.equal({ a: 'bar', b: 888, c: null, d: null, e: null }); 39 | 40 | stmt = this.db.prepare("SELECT * FROM entries WHERE b > 900 ORDER BY rowid"); 41 | expect(stmt.get()).to.deep.equal({ a: 'baz', b: 999, c: null, d: null, e: null }); 42 | }); 43 | it('should obey the current pluck and expand settings', function () { 44 | const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid"); 45 | const expanded = { entries: { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }, $: { c: 5.5 } }; 46 | const row = Object.assign({}, expanded.entries, expanded.$); 47 | const plucked = expanded.entries.a; 48 | const raw = Object.values(expanded.entries).concat(expanded.$.c); 49 | expect(stmt.get()).to.deep.equal(row); 50 | expect(stmt.pluck(true).get()).to.deep.equal(plucked); 51 | expect(stmt.get()).to.deep.equal(plucked); 52 | expect(stmt.pluck(false).get()).to.deep.equal(row); 53 | expect(stmt.get()).to.deep.equal(row); 54 | expect(stmt.pluck().get()).to.deep.equal(plucked); 55 | expect(stmt.get()).to.deep.equal(plucked); 56 | expect(stmt.expand().get()).to.deep.equal(expanded); 57 | expect(stmt.get()).to.deep.equal(expanded); 58 | expect(stmt.expand(false).get()).to.deep.equal(row); 59 | expect(stmt.get()).to.deep.equal(row); 60 | expect(stmt.expand(true).get()).to.deep.equal(expanded); 61 | expect(stmt.get()).to.deep.equal(expanded); 62 | expect(stmt.pluck(true).get()).to.deep.equal(plucked); 63 | expect(stmt.get()).to.deep.equal(plucked); 64 | expect(stmt.raw().get()).to.deep.equal(raw); 65 | expect(stmt.get()).to.deep.equal(raw); 66 | expect(stmt.raw(false).get()).to.deep.equal(row); 67 | expect(stmt.get()).to.deep.equal(row); 68 | expect(stmt.raw(true).get()).to.deep.equal(raw); 69 | expect(stmt.get()).to.deep.equal(raw); 70 | expect(stmt.expand(true).get()).to.deep.equal(expanded); 71 | expect(stmt.get()).to.deep.equal(expanded); 72 | }); 73 | it('should return undefined when no rows were found', function () { 74 | const stmt = this.db.prepare("SELECT * FROM entries WHERE b == 999"); 75 | expect(stmt.get()).to.be.undefined; 76 | expect(stmt.pluck().get()).to.be.undefined; 77 | }); 78 | it('should accept bind parameters', function () { 79 | const row = { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }; 80 | const SQL1 = 'SELECT * FROM entries WHERE a=? AND b=? AND c=? AND d=? AND e IS ?'; 81 | const SQL2 = 'SELECT * FROM entries WHERE a=@a AND b=@b AND c=@c AND d=@d AND e IS @e'; 82 | let result = this.db.prepare(SQL1).get('foo', 1, 3.14, Buffer.alloc(4).fill(0xdd), null); 83 | expect(result).to.deep.equal(row); 84 | 85 | result = this.db.prepare(SQL1).get(['foo', 1, 3.14, Buffer.alloc(4).fill(0xdd), null]); 86 | expect(result).to.deep.equal(row); 87 | 88 | result = this.db.prepare(SQL1).get(['foo', 1], [3.14], Buffer.alloc(4).fill(0xdd), [,]); 89 | expect(result).to.deep.equal(row); 90 | 91 | result = this.db.prepare(SQL2).get({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: undefined }); 92 | expect(result).to.deep.equal(row); 93 | 94 | result = this.db.prepare(SQL2).get({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xaa), e: undefined }); 95 | expect(result).to.be.undefined; 96 | 97 | expect(() => 98 | this.db.prepare(SQL2).get({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd) }) 99 | ).to.throw(RangeError); 100 | 101 | expect(() => 102 | this.db.prepare(SQL1).get() 103 | ).to.throw(RangeError); 104 | 105 | expect(() => 106 | this.db.prepare(SQL2).get({}) 107 | ).to.throw(RangeError); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/22.statement.all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Statement#all()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare('CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)').run(); 8 | 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 10) SELECT * FROM temp").run(); 9 | }); 10 | afterEach(function () { 11 | this.db.close(); 12 | }); 13 | 14 | it('should throw an exception when used on a statement that returns no data', function () { 15 | let stmt = this.db.prepare("INSERT INTO entries VALUES ('foo', 1, 3.14, x'dddddddd', NULL)"); 16 | expect(stmt.reader).to.be.false; 17 | expect(() => stmt.all()).to.throw(TypeError); 18 | 19 | stmt = this.db.prepare("CREATE TABLE IF NOT EXISTS entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)"); 20 | expect(stmt.reader).to.be.false; 21 | expect(() => stmt.all()).to.throw(TypeError); 22 | 23 | stmt = this.db.prepare("BEGIN TRANSACTION"); 24 | expect(stmt.reader).to.be.false; 25 | expect(() => stmt.all()).to.throw(TypeError); 26 | }); 27 | it('should return an array of every matching row', function () { 28 | const row = { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }; 29 | 30 | let stmt = this.db.prepare("SELECT * FROM entries ORDER BY rowid"); 31 | expect(stmt.reader).to.be.true; 32 | matchesFrom(stmt.all(), 1); 33 | 34 | stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid"); 35 | matchesFrom(stmt.all(), 6); 36 | 37 | function matchesFrom(rows, i) { 38 | let index = 0; 39 | for (; i <= 10; ++i, ++index) { 40 | row.b = i; 41 | expect(rows[index]).to.deep.equal(row); 42 | } 43 | expect(index).to.equal(rows.length); 44 | } 45 | }); 46 | it('should work with RETURNING clause', function () { 47 | let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *"); 48 | expect(stmt.reader).to.be.true; 49 | expect(stmt.all()).to.deep.equal([ 50 | { a: 'bar', b: 888, c: null, d: null, e: null }, 51 | { a: 'baz', b: 999, c: null, d: null, e: null }, 52 | ]); 53 | 54 | stmt = this.db.prepare("SELECT * FROM entries WHERE b > 800 ORDER BY rowid"); 55 | expect(stmt.all()).to.deep.equal([ 56 | { a: 'bar', b: 888, c: null, d: null, e: null }, 57 | { a: 'baz', b: 999, c: null, d: null, e: null }, 58 | ]); 59 | }); 60 | it('should obey the current pluck and expand settings', function () { 61 | const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid"); 62 | const expanded = new Array(10).fill().map((_, i) => ({ 63 | entries: { a: 'foo', b: i + 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }, 64 | $: { c: 5.5 }, 65 | })); 66 | const rows = expanded.map(x => Object.assign({}, x.entries, x.$)); 67 | const plucked = expanded.map(x => x.entries.a); 68 | const raw = expanded.map(x => Object.values(x.entries).concat(x.$.c)) 69 | expect(stmt.all()).to.deep.equal(rows); 70 | expect(stmt.pluck(true).all()).to.deep.equal(plucked); 71 | expect(stmt.all()).to.deep.equal(plucked); 72 | expect(stmt.pluck(false).all()).to.deep.equal(rows); 73 | expect(stmt.all()).to.deep.equal(rows); 74 | expect(stmt.pluck().all()).to.deep.equal(plucked); 75 | expect(stmt.all()).to.deep.equal(plucked); 76 | expect(stmt.expand().all()).to.deep.equal(expanded); 77 | expect(stmt.all()).to.deep.equal(expanded); 78 | expect(stmt.expand(false).all()).to.deep.equal(rows); 79 | expect(stmt.all()).to.deep.equal(rows); 80 | expect(stmt.expand(true).all()).to.deep.equal(expanded); 81 | expect(stmt.all()).to.deep.equal(expanded); 82 | expect(stmt.pluck(true).all()).to.deep.equal(plucked); 83 | expect(stmt.all()).to.deep.equal(plucked); 84 | expect(stmt.raw().all()).to.deep.equal(raw); 85 | expect(stmt.all()).to.deep.equal(raw); 86 | expect(stmt.raw(false).all()).to.deep.equal(rows); 87 | expect(stmt.all()).to.deep.equal(rows); 88 | expect(stmt.raw(true).all()).to.deep.equal(raw); 89 | expect(stmt.all()).to.deep.equal(raw); 90 | expect(stmt.expand(true).all()).to.deep.equal(expanded); 91 | expect(stmt.all()).to.deep.equal(expanded); 92 | }); 93 | it('should return an empty array when no rows were found', function () { 94 | const stmt = this.db.prepare("SELECT * FROM entries WHERE b == 999"); 95 | expect(stmt.all()).to.deep.equal([]); 96 | expect(stmt.pluck().all()).to.deep.equal([]); 97 | }); 98 | it('should accept bind parameters', function () { 99 | const rows = [{ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }]; 100 | const SQL1 = 'SELECT * FROM entries WHERE a=? AND b=? AND c=? AND d=? AND e IS ?'; 101 | const SQL2 = 'SELECT * FROM entries WHERE a=@a AND b=@b AND c=@c AND d=@d AND e IS @e'; 102 | let result = this.db.prepare(SQL1).all('foo', 1, 3.14, Buffer.alloc(4).fill(0xdd), null); 103 | expect(result).to.deep.equal(rows); 104 | 105 | result = this.db.prepare(SQL1).all(['foo', 1, 3.14, Buffer.alloc(4).fill(0xdd), null]); 106 | expect(result).to.deep.equal(rows); 107 | 108 | result = this.db.prepare(SQL1).all(['foo', 1], [3.14], Buffer.alloc(4).fill(0xdd), [,]); 109 | expect(result).to.deep.equal(rows); 110 | 111 | result = this.db.prepare(SQL2).all({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: undefined }); 112 | expect(result).to.deep.equal(rows); 113 | 114 | result = this.db.prepare(SQL2).all({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xaa), e: undefined }); 115 | expect(result).to.deep.equal([]); 116 | 117 | expect(() => 118 | this.db.prepare(SQL2).all({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd) }) 119 | ).to.throw(RangeError); 120 | 121 | expect(() => 122 | this.db.prepare(SQL1).all() 123 | ).to.throw(RangeError); 124 | 125 | expect(() => 126 | this.db.prepare(SQL2).all({}) 127 | ).to.throw(RangeError); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/23.statement.iterate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Statement#iterate()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare("CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)").run(); 8 | 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 10) SELECT * FROM temp").run(); 9 | }); 10 | afterEach(function () { 11 | this.db.close(); 12 | }); 13 | 14 | it('should throw an exception when used on a statement that returns no data', function () { 15 | let stmt = this.db.prepare("INSERT INTO entries VALUES ('foo', 1, 3.14, x'dddddddd', NULL)"); 16 | expect(stmt.reader).to.be.false; 17 | expect(() => stmt.iterate()).to.throw(TypeError); 18 | 19 | stmt = this.db.prepare("CREATE TABLE IF NOT EXISTS entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)"); 20 | expect(stmt.reader).to.be.false; 21 | expect(() => stmt.iterate()).to.throw(TypeError); 22 | 23 | stmt = this.db.prepare("BEGIN TRANSACTION"); 24 | expect(stmt.reader).to.be.false; 25 | expect(() => stmt.iterate()).to.throw(TypeError); 26 | 27 | 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 10) SELECT * FROM temp").run(); 28 | }); 29 | it('should return an iterator over each matching row', function () { 30 | const row = { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }; 31 | 32 | let count = 0; 33 | let stmt = this.db.prepare("SELECT * FROM entries ORDER BY rowid"); 34 | expect(stmt.reader).to.be.true; 35 | expect(stmt.busy).to.be.false; 36 | 37 | const iterator = stmt.iterate(); 38 | expect(iterator).to.not.be.null; 39 | expect(typeof iterator).to.equal('object'); 40 | expect(iterator.next).to.be.a('function'); 41 | expect(iterator.return).to.be.a('function'); 42 | expect(iterator.throw).to.not.be.a('function'); 43 | expect(iterator[Symbol.iterator]).to.be.a('function'); 44 | expect(iterator[Symbol.iterator]()).to.equal(iterator); 45 | expect(stmt.busy).to.be.true; 46 | 47 | for (const data of iterator) { 48 | row.b = ++count; 49 | expect(data).to.deep.equal(row); 50 | expect(stmt.busy).to.be.true; 51 | } 52 | expect(count).to.equal(10); 53 | expect(stmt.busy).to.be.false; 54 | 55 | count = 0; 56 | stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid"); 57 | expect(stmt.busy).to.be.false; 58 | const iterator2 = stmt.iterate(); 59 | expect(iterator).to.not.equal(iterator2); 60 | expect(stmt.busy).to.be.true; 61 | for (const data of iterator2) { 62 | row.b = ++count + 5; 63 | expect(data).to.deep.equal(row); 64 | expect(stmt.busy).to.be.true; 65 | } 66 | expect(count).to.equal(5); 67 | expect(stmt.busy).to.be.false; 68 | }); 69 | it('should work with RETURNING clause', function () { 70 | let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *"); 71 | expect(stmt.reader).to.be.true; 72 | expect([...stmt.iterate()]).to.deep.equal([ 73 | { a: 'bar', b: 888, c: null, d: null, e: null }, 74 | { a: 'baz', b: 999, c: null, d: null, e: null }, 75 | ]); 76 | 77 | stmt = this.db.prepare("SELECT * FROM entries WHERE b > 800 ORDER BY rowid"); 78 | expect([...stmt.iterate()]).to.deep.equal([ 79 | { a: 'bar', b: 888, c: null, d: null, e: null }, 80 | { a: 'baz', b: 999, c: null, d: null, e: null }, 81 | ]); 82 | }); 83 | it('should obey the current pluck and expand settings', function () { 84 | const shouldHave = (desiredData) => { 85 | let i = 0; 86 | for (const data of stmt.iterate()) { 87 | i += 1; 88 | if (typeof desiredData === 'object' && desiredData !== null) { 89 | if (Array.isArray(desiredData)) { 90 | desiredData[1] = i; 91 | } else if (typeof desiredData.entries === 'object' && desiredData.entries !== null) { 92 | desiredData.entries.b = i; 93 | } else { 94 | desiredData.b = i; 95 | } 96 | } 97 | expect(data).to.deep.equal(desiredData); 98 | } 99 | expect(i).to.equal(10); 100 | }; 101 | const expanded = { 102 | entries: { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }, 103 | $: { c: 5.5 }, 104 | }; 105 | const row = Object.assign({}, expanded.entries, expanded.$); 106 | const plucked = expanded.entries.a; 107 | const raw = Object.values(expanded.entries).concat(expanded.$.c); 108 | const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid"); 109 | shouldHave(row); 110 | stmt.pluck(true); 111 | shouldHave(plucked); 112 | shouldHave(plucked); 113 | stmt.pluck(false); 114 | shouldHave(row); 115 | shouldHave(row); 116 | stmt.pluck(); 117 | shouldHave(plucked); 118 | shouldHave(plucked); 119 | stmt.expand(); 120 | shouldHave(expanded); 121 | shouldHave(expanded); 122 | stmt.expand(false); 123 | shouldHave(row); 124 | shouldHave(row); 125 | stmt.expand(true); 126 | shouldHave(expanded); 127 | shouldHave(expanded); 128 | stmt.pluck(true); 129 | shouldHave(plucked); 130 | shouldHave(plucked); 131 | stmt.raw(); 132 | shouldHave(raw); 133 | shouldHave(raw); 134 | stmt.raw(false); 135 | shouldHave(row); 136 | shouldHave(row); 137 | stmt.raw(true); 138 | shouldHave(raw); 139 | shouldHave(raw); 140 | stmt.expand(true); 141 | shouldHave(expanded); 142 | shouldHave(expanded); 143 | }); 144 | it('should close the iterator when throwing in a for-of loop', function () { 145 | const err = new Error('foobar'); 146 | const stmt = this.db.prepare("SELECT * FROM entries ORDER BY rowid"); 147 | const iterator = stmt.iterate(); 148 | let count = 0; 149 | expect(() => { 150 | for (const row of iterator) { ++count; throw err; } 151 | }).to.throw(err); 152 | expect(count).to.equal(1); 153 | expect(iterator.next()).to.deep.equal({ value: undefined, done: true }); 154 | for (const row of iterator) ++count; 155 | expect(count).to.equal(1); 156 | for (const row of stmt.iterate()) ++count; 157 | expect(count).to.equal(11); 158 | }); 159 | it('should close the iterator when using break in a for-of loop', function () { 160 | const stmt = this.db.prepare("SELECT * FROM entries ORDER BY rowid"); 161 | const iterator = stmt.iterate(); 162 | let count = 0; 163 | for (const row of iterator) { ++count; break; } 164 | expect(count).to.equal(1); 165 | expect(iterator.next()).to.deep.equal({ value: undefined, done: true }); 166 | for (const row of iterator) ++count; 167 | expect(count).to.equal(1); 168 | for (const row of stmt.iterate()) ++count; 169 | expect(count).to.equal(11); 170 | }); 171 | it('should return an empty iterator when no rows were found', function () { 172 | const stmt = this.db.prepare("SELECT * FROM entries WHERE b == 999"); 173 | expect(stmt.iterate().next()).to.deep.equal({ value: undefined, done: true }); 174 | for (const data of stmt.pluck().iterate()) { 175 | throw new Error('This callback should not have been invoked'); 176 | } 177 | }); 178 | it('should accept bind parameters', function () { 179 | const shouldHave = (SQL, desiredData, args) => { 180 | let i = 0; 181 | const stmt = this.db.prepare(SQL); 182 | for (const data of stmt.iterate(...args)) { 183 | desiredData.b = ++i; 184 | expect(data).to.deep.equal(desiredData); 185 | } 186 | expect(i).to.equal(1); 187 | }; 188 | 189 | const row = { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }; 190 | const SQL1 = 'SELECT * FROM entries WHERE a=? AND b=? AND c=? AND d=? AND e IS ?'; 191 | const SQL2 = 'SELECT * FROM entries WHERE a=@a AND b=@b AND c=@c AND d=@d AND e IS @e'; 192 | 193 | shouldHave(SQL1, row, ['foo', 1, 3.14, Buffer.alloc(4).fill(0xdd), null]); 194 | shouldHave(SQL1, row, [['foo', 1, 3.14, Buffer.alloc(4).fill(0xdd), null]]); 195 | shouldHave(SQL1, row, [['foo', 1], [3.14], Buffer.alloc(4).fill(0xdd), [,]]); 196 | shouldHave(SQL2, row, [{ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: undefined }]); 197 | 198 | for (const data of this.db.prepare(SQL2).iterate({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xaa), e: undefined })) { 199 | throw new Error('This callback should not have been invoked'); 200 | } 201 | 202 | expect(() => 203 | this.db.prepare(SQL2).iterate(row, () => {}) 204 | ).to.throw(TypeError); 205 | 206 | expect(() => 207 | this.db.prepare(SQL2).iterate({ a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd) }) 208 | ).to.throw(RangeError); 209 | 210 | expect(() => 211 | this.db.prepare(SQL1).iterate() 212 | ).to.throw(RangeError); 213 | 214 | expect(() => 215 | this.db.prepare(SQL2).iterate() 216 | ).to.throw(TypeError); 217 | 218 | expect(() => 219 | this.db.prepare(SQL2).iterate(row, {}) 220 | ).to.throw(TypeError); 221 | 222 | expect(() => 223 | this.db.prepare(SQL2).iterate({}) 224 | ).to.throw(RangeError); 225 | 226 | this.db.prepare(SQL1).iterate('foo', 1, 3.14, Buffer.alloc(4).fill(0xdd), null).return(); 227 | expect(() => 228 | this.db.prepare(SQL1).iterate('foo', 1, new (function(){})(), Buffer.alloc(4).fill(0xdd), null) 229 | ).to.throw(TypeError); 230 | }); 231 | }); 232 | -------------------------------------------------------------------------------- /test/24.statement.bind.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Statement#bind()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare('CREATE TABLE entries (a TEXT, b INTEGER, c BLOB)').run(); 8 | }); 9 | afterEach(function () { 10 | this.db.close(); 11 | }); 12 | 13 | it('should permanently bind the given parameters', function () { 14 | const stmt = this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)'); 15 | const buffer = Buffer.alloc(4).fill(0xdd); 16 | stmt.bind('foobar', 25, buffer) 17 | stmt.run(); 18 | buffer.fill(0xaa); 19 | stmt.run(); 20 | const row1 = this.db.prepare('SELECT * FROM entries WHERE rowid=1').get(); 21 | const row2 = this.db.prepare('SELECT * FROM entries WHERE rowid=2').get(); 22 | expect(row1.a).to.equal(row2.a); 23 | expect(row1.b).to.equal(row2.b); 24 | expect(row1.c).to.deep.equal(row2.c); 25 | }); 26 | it('should not allow you to bind temporary parameters afterwards', function () { 27 | const stmt = this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)'); 28 | const buffer = Buffer.alloc(4).fill(0xdd); 29 | stmt.bind('foobar', 25, buffer) 30 | expect(() => stmt.run(null)).to.throw(TypeError); 31 | expect(() => stmt.run(buffer)).to.throw(TypeError); 32 | expect(() => stmt.run('foobar', 25, buffer)).to.throw(TypeError); 33 | }); 34 | it('should throw an exception when invoked twice on the same statement', function () { 35 | let stmt = this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)'); 36 | stmt.bind('foobar', 25, null); 37 | expect(() => stmt.bind('foobar', 25, null)).to.throw(TypeError); 38 | expect(() => stmt.bind()).to.throw(TypeError); 39 | 40 | stmt = this.db.prepare('SELECT * FROM entries'); 41 | stmt.bind(); 42 | expect(() => stmt.bind()).to.throw(TypeError); 43 | }); 44 | it('should throw an exception when invalid parameters are given', function () { 45 | let stmt = this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)'); 46 | 47 | expect(() => 48 | stmt.bind('foo', 25) 49 | ).to.throw(RangeError); 50 | 51 | expect(() => 52 | stmt.bind('foo', 25, null, null) 53 | ).to.throw(RangeError); 54 | 55 | expect(() => 56 | stmt.bind('foo', new Number(25), null) 57 | ).to.throw(TypeError); 58 | 59 | expect(() => 60 | stmt.bind() 61 | ).to.throw(RangeError); 62 | 63 | stmt.bind('foo', 25, null); 64 | 65 | stmt = this.db.prepare('INSERT INTO entries VALUES (@a, @a, ?)'); 66 | 67 | expect(() => 68 | stmt.bind({ a: '123' }) 69 | ).to.throw(RangeError); 70 | 71 | expect(() => 72 | stmt.bind({ a: '123', 1: null }) 73 | ).to.throw(RangeError); 74 | 75 | expect(() => 76 | stmt.bind({ a: '123' }, null, null) 77 | ).to.throw(RangeError); 78 | 79 | stmt.bind({ a: '123' }, null); 80 | 81 | stmt = this.db.prepare('INSERT INTO entries VALUES (@a, @a, ?)'); 82 | stmt.bind({ a: '123', b: null }, null); 83 | }); 84 | it('should propagate exceptions thrown while accessing array/object members', function () { 85 | const arr = [22]; 86 | const obj = {}; 87 | const err = new TypeError('foobar'); 88 | Object.defineProperty(arr, '0', { get: () => { throw err; } }) 89 | Object.defineProperty(obj, 'baz', { get: () => { throw err; } }) 90 | const stmt1 = this.db.prepare('SELECT ?'); 91 | const stmt2 = this.db.prepare('SELECT @baz'); 92 | expect(() => stmt1.bind(arr)).to.throw(err); 93 | expect(() => stmt2.bind(obj)).to.throw(err); 94 | }); 95 | it('should properly bind empty buffers', function () { 96 | this.db.prepare('INSERT INTO entries (c) VALUES (?)').bind(Buffer.alloc(0)).run(); 97 | const result = this.db.prepare('SELECT c FROM entries').pluck().get(); 98 | expect(result).to.be.an.instanceof(Buffer); 99 | expect(result.length).to.equal(0); 100 | }); 101 | it('should properly bind empty strings', function () { 102 | this.db.prepare('INSERT INTO entries (a) VALUES (?)').bind('').run(); 103 | const result = this.db.prepare('SELECT a FROM entries').pluck().get(); 104 | expect(result).to.be.a('string'); 105 | expect(result.length).to.equal(0); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/25.statement.columns.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Statement#columns()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare('CREATE TABLE entries (a TEXT, b INTEGER, c WHATthe)').run(); 8 | }); 9 | afterEach(function () { 10 | this.db.close(); 11 | }); 12 | 13 | it('should throw an exception if invoked on a non-reader statement', function () { 14 | const stmt = this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)'); 15 | expect(() => stmt.columns()).to.throw(TypeError); 16 | }); 17 | it('should return an array of column descriptors', function () { 18 | expect(this.db.prepare('SELECT 5.0 as d, * FROM entries').columns()).to.deep.equal([ 19 | { name: 'd', column: null, table: null, database: null, type: null }, 20 | { name: 'a', column: 'a', table: 'entries', database: 'main', type: 'TEXT' }, 21 | { name: 'b', column: 'b', table: 'entries', database: 'main', type: 'INTEGER' }, 22 | { name: 'c', column: 'c', table: 'entries', database: 'main', type: 'WHATthe' }, 23 | ]); 24 | expect(this.db.prepare('SELECT a, c as b, b FROM entries').columns()).to.deep.equal([ 25 | { name: 'a', column: 'a', table: 'entries', database: 'main', type: 'TEXT' }, 26 | { name: 'b', column: 'c', table: 'entries', database: 'main', type: 'WHATthe' }, 27 | { name: 'b', column: 'b', table: 'entries', database: 'main', type: 'INTEGER' }, 28 | ]); 29 | }); 30 | it('should not return stale column descriptors after being recompiled', function () { 31 | const stmt = this.db.prepare('SELECT * FROM entries'); 32 | expect(stmt.columns()).to.deep.equal([ 33 | { name: 'a', column: 'a', table: 'entries', database: 'main', type: 'TEXT' }, 34 | { name: 'b', column: 'b', table: 'entries', database: 'main', type: 'INTEGER' }, 35 | { name: 'c', column: 'c', table: 'entries', database: 'main', type: 'WHATthe' }, 36 | ]); 37 | this.db.prepare('ALTER TABLE entries ADD COLUMN d FOOBAR').run(); 38 | stmt.get(); // Recompile 39 | expect(stmt.columns()).to.deep.equal([ 40 | { name: 'a', column: 'a', table: 'entries', database: 'main', type: 'TEXT' }, 41 | { name: 'b', column: 'b', table: 'entries', database: 'main', type: 'INTEGER' }, 42 | { name: 'c', column: 'c', table: 'entries', database: 'main', type: 'WHATthe' }, 43 | { name: 'd', column: 'd', table: 'entries', database: 'main', type: 'FOOBAR' }, 44 | ]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/30.database.transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#transaction()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare('CREATE TABLE data (x UNIQUE)').run(); 8 | this.db.prepare('INSERT INTO data VALUES (1), (2), (3)').run(); 9 | }); 10 | afterEach(function () { 11 | this.db.close(); 12 | }); 13 | 14 | it('should throw an exception if a function is not provided', function () { 15 | expect(() => this.db.transaction(123)).to.throw(TypeError); 16 | expect(() => this.db.transaction(0)).to.throw(TypeError); 17 | expect(() => this.db.transaction(null)).to.throw(TypeError); 18 | expect(() => this.db.transaction()).to.throw(TypeError); 19 | expect(() => this.db.transaction([])).to.throw(TypeError); 20 | expect(() => this.db.transaction('CREATE TABLE people (name TEXT)')).to.throw(TypeError); 21 | expect(() => this.db.transaction(['CREATE TABLE people (name TEXT)'])).to.throw(TypeError); 22 | }); 23 | it('should return a new transaction function', function () { 24 | const fn = () => {}; 25 | const trx = this.db.transaction(fn); 26 | expect(trx).to.not.equal(fn); 27 | expect(trx).to.be.a('function'); 28 | expect(trx).to.equal(trx.default); 29 | const keys = ['default', 'deferred', 'immediate', 'exclusive']; 30 | for (const key of keys) { 31 | const nested = trx[key]; 32 | expect(nested).to.not.equal(fn); 33 | expect(nested).to.be.a('function'); 34 | expect(nested.database).to.equal(this.db); 35 | expect(nested.run).to.be.undefined; 36 | expect(nested.get).to.be.undefined; 37 | expect(nested.all).to.be.undefined; 38 | expect(nested.iterate).to.be.undefined; 39 | expect(nested.reader).to.be.undefined; 40 | expect(nested.source).to.be.undefined; 41 | for (const key of keys) expect(nested[key]).to.equal(trx[key]); 42 | } 43 | }); 44 | describe('transaction function', function () { 45 | it('should execute the wrapped function', function () { 46 | const trx = this.db.transaction(function () { return [this, ...arguments]; }); 47 | const obj = {}; 48 | expect(trx.call(obj, 'foo', 'bar', 123, obj)).to.deep.equal([obj, 'foo', 'bar', 123, obj]); 49 | }); 50 | it('should execute within an isolated transaction', function () { 51 | const other = new Database(util.current()); 52 | try { 53 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 54 | expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 55 | expect(this.db.inTransaction).to.be.false; 56 | let ranOnce = false; 57 | const trx = this.db.transaction((arg) => { 58 | expect(this.db.inTransaction).to.be.true; 59 | expect(arg).to.equal('foo'); 60 | this.db.prepare('INSERT INTO data VALUES (100)').run(); 61 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]); 62 | expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 63 | ranOnce = true; 64 | expect(this.db.inTransaction).to.be.true; 65 | return 'bar'; 66 | }); 67 | expect(ranOnce).to.be.false; 68 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 69 | expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 70 | expect(this.db.inTransaction).to.be.false; 71 | expect(trx('foo')).to.equal('bar'); 72 | expect(this.db.inTransaction).to.be.false; 73 | expect(ranOnce).to.be.true; 74 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]); 75 | expect(other.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]); 76 | } finally { 77 | other.close(); 78 | } 79 | }); 80 | it('should rollback the transaction if an exception is thrown', function () { 81 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 82 | expect(this.db.inTransaction).to.be.false; 83 | const err = new Error('foobar'); 84 | let ranOnce = false; 85 | const trx = this.db.transaction((arg) => { 86 | expect(this.db.inTransaction).to.be.true; 87 | expect(arg).to.equal('baz'); 88 | this.db.prepare('INSERT INTO data VALUES (100)').run(); 89 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 100]); 90 | ranOnce = true; 91 | expect(this.db.inTransaction).to.be.true; 92 | throw err; 93 | }); 94 | expect(ranOnce).to.be.false; 95 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 96 | expect(this.db.inTransaction).to.be.false; 97 | expect(() => trx('baz')).to.throw(err); 98 | expect(this.db.inTransaction).to.be.false; 99 | expect(ranOnce).to.be.true; 100 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 101 | }); 102 | it('should work when nested within other transaction functions', function () { 103 | const stmt = this.db.prepare('INSERT INTO data VALUES (?)'); 104 | const insertOne = this.db.transaction(x => stmt.run(x)); 105 | const insertMany = this.db.transaction((...values) => values.map(insertOne)); 106 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 107 | insertMany(10, 20, 30); 108 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30]); 109 | expect(() => insertMany(40, 50, 3)).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CONSTRAINT_UNIQUE'); 110 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30]); 111 | }); 112 | it('should be able to perform partial rollbacks when nested', function () { 113 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 114 | const stmt = this.db.prepare('INSERT INTO data VALUES (?)'); 115 | const insertOne = this.db.transaction(x => stmt.run(x).changes); 116 | const insertMany = this.db.transaction((...values) => values.reduce((y, x) => y + insertOne(x), 0)); 117 | expect(this.db.inTransaction).to.be.false; 118 | const trx = this.db.transaction(() => { 119 | expect(this.db.inTransaction).to.be.true; 120 | let count = 0; 121 | count += insertMany(10, 20, 30); 122 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30]); 123 | try { 124 | insertMany(40, 50, 3, 60); 125 | } catch (_) { 126 | expect(this.db.inTransaction).to.be.true; 127 | count += insertOne(555); 128 | } 129 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30, 555]); 130 | this.db.prepare('SAVEPOINT foo').run(); 131 | insertOne(123); 132 | insertMany(456, 789); 133 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30, 555, 123, 456, 789]); 134 | this.db.prepare('ROLLBACK TO foo').run(); 135 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30, 555]); 136 | count += insertMany(1000); 137 | expect(this.db.inTransaction).to.be.true; 138 | return count; 139 | }); 140 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 141 | expect(this.db.inTransaction).to.be.false; 142 | expect(trx()).to.equal(5); 143 | expect(this.db.inTransaction).to.be.false; 144 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30, 555, 1000]); 145 | }); 146 | it('should work when the transaction is rolled back internally', function () { 147 | const stmt = this.db.prepare('INSERT OR ROLLBACK INTO data VALUES (?)'); 148 | const insertOne = this.db.transaction(x => stmt.run(x)); 149 | const insertMany = this.db.transaction((...values) => values.map(insertOne)); 150 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3]); 151 | insertMany(10, 20, 30); 152 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30]); 153 | expect(() => insertMany(40, 50, 10)).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CONSTRAINT_UNIQUE'); 154 | expect(this.db.prepare('SELECT x FROM data').pluck().all()).to.deep.equal([1, 2, 3, 10, 20, 30]); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/31.database.checkpoint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const Database = require('../.'); 4 | 5 | describe('Database#pragma(\'wal_checkpoint(RESTART)\')', function () { 6 | let db1, db2; 7 | before(function () { 8 | db1 = new Database(util.next()); 9 | db2 = new Database(util.next()); 10 | db1.pragma('journal_mode = WAL'); 11 | db1.prepare('CREATE TABLE entries (a TEXT, b INTEGER)').run(); 12 | db2.pragma('journal_mode = WAL'); 13 | db2.prepare('CREATE TABLE entries (a TEXT, b INTEGER)').run(); 14 | }); 15 | after(function () { 16 | db1.close(); 17 | db2.close(); 18 | }); 19 | 20 | function fillWall(count, expectation) { 21 | [db1, db2].forEach((db) => { 22 | let size1, size2; 23 | for (let i = 0; i < count; ++i) { 24 | size1 = fs.statSync(`${db.name}-wal`).size; 25 | db.prepare('INSERT INTO entries VALUES (?, ?)').run('bar', 999); 26 | size2 = fs.statSync(`${db.name}-wal`).size; 27 | expectation(size2, size1, db); 28 | } 29 | }); 30 | } 31 | 32 | describe('when used without a specified database', function () { 33 | specify('every insert should increase the size of the WAL file', function () { 34 | fillWall(10, (b, a) => expect(b).to.be.above(a)); 35 | }); 36 | specify('inserts after a checkpoint should NOT increase the size of the WAL file', function () { 37 | db1.prepare(`ATTACH '${db2.name}' AS foobar`).run(); 38 | db1.pragma('wal_checkpoint(RESTART)'); 39 | fillWall(10, (b, a) => expect(b).to.equal(a)); 40 | }); 41 | }); 42 | describe('when used on a specific database', function () { 43 | specify('every insert should increase the size of the WAL file', function () { 44 | db1.prepare('DETACH foobar').run(); 45 | db1.close(); 46 | db2.close(); 47 | db1 = new Database(db1.name); 48 | db2 = new Database(db2.name); 49 | db1.prepare('CREATE TABLE _unused (a TEXT, b INTEGER)').run(); 50 | db2.prepare('CREATE TABLE _unused (a TEXT, b INTEGER)').run(); 51 | fillWall(10, (b, a) => expect(b).to.be.above(a)); 52 | }); 53 | specify('inserts after a checkpoint should NOT increase the size of the WAL file', function () { 54 | db1.prepare(`ATTACH '${db2.name}' AS bazqux`).run(); 55 | db1.pragma('bazqux.wal_checkpoint(RESTART)'); 56 | fillWall(10, (b, a, db) => { 57 | if (db === db1) expect(b).to.be.above(a); 58 | else expect(b).to.be.equal(a); 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/35.database.load-extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Database = require('../.'); 5 | 6 | describe('Database#loadExtension()', function () { 7 | let filepath; 8 | before(function () { 9 | const releaseFilepath = path.join(__dirname, '..', 'build', 'Release', 'test_extension.node'); 10 | const debugFilepath = path.join(__dirname, '..', 'build', 'Debug', 'test_extension.node'); 11 | try { 12 | fs.accessSync(releaseFilepath); 13 | filepath = releaseFilepath; 14 | } catch (_) { 15 | fs.accessSync(debugFilepath); 16 | filepath = debugFilepath; 17 | } 18 | }); 19 | beforeEach(function () { 20 | this.db = new Database(util.next()); 21 | }); 22 | afterEach(function () { 23 | this.db.close(); 24 | }); 25 | 26 | it('should throw an exception if a string argument is not given', function () { 27 | expect(() => this.db.loadExtension()).to.throw(TypeError); 28 | expect(() => this.db.loadExtension(undefined)).to.throw(TypeError); 29 | expect(() => this.db.loadExtension(null)).to.throw(TypeError); 30 | expect(() => this.db.loadExtension(123)).to.throw(TypeError); 31 | expect(() => this.db.loadExtension(new String(filepath))).to.throw(TypeError); 32 | expect(() => this.db.loadExtension([filepath])).to.throw(TypeError); 33 | }); 34 | it('should throw an exception if the database is busy', function () { 35 | let invoked = false; 36 | for (const value of this.db.prepare('select 555').pluck().iterate()) { 37 | expect(value).to.equal(555); 38 | expect(() => this.db.loadExtension(filepath)).to.throw(TypeError); 39 | invoked = true; 40 | } 41 | expect(invoked).to.be.true; 42 | }); 43 | it('should throw an exception if the extension is not found', function () { 44 | try { 45 | this.db.loadExtension(filepath + 'x'); 46 | } catch (err) { 47 | expect(err).to.be.an.instanceof(Database.SqliteError); 48 | expect(err.message).to.be.a('string'); 49 | expect(err.message.length).to.be.above(0); 50 | expect(err.message).to.not.equal('not an error'); 51 | expect(err.code).to.equal('SQLITE_ERROR'); 52 | return; 53 | } 54 | throw new Error('This code should not have been reached'); 55 | }); 56 | it('should register the specified extension', function () { 57 | expect(this.db.loadExtension(filepath)).to.equal(this.db); 58 | expect(this.db.prepare('SELECT testExtensionFunction(NULL, 123, 99, 2)').pluck().get()).to.equal(4); 59 | expect(this.db.prepare('SELECT testExtensionFunction(NULL, 2)').pluck().get()).to.equal(2); 60 | }); 61 | it('should not allow registering extensions with SQL', function () { 62 | expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError); 63 | expect(this.db.loadExtension(filepath)).to.equal(this.db); 64 | expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError); 65 | this.db.close(); 66 | this.db = new Database(util.next()); 67 | try { 68 | this.db.loadExtension(filepath + 'x'); 69 | } catch (err) { 70 | expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError); 71 | return; 72 | } 73 | throw new Error('This code should not have been reached'); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/37.database.serialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#serialize()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare("CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)").run(); 8 | this.seed = () => { 9 | 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(); 10 | }; 11 | }); 12 | afterEach(function () { 13 | this.db.close(); 14 | }); 15 | 16 | it('should serialize the database and return a buffer', async function () { 17 | let buffer = this.db.serialize(); 18 | expect(buffer).to.be.an.instanceof(Buffer); 19 | expect(buffer.length).to.be.above(1000); 20 | const lengthBefore = buffer.length; 21 | this.seed(); 22 | buffer = this.db.serialize(); 23 | expect(buffer).to.be.an.instanceof(Buffer); 24 | expect(buffer.length).to.be.above(lengthBefore); 25 | }); 26 | it('should return a buffer that can be used by the Database constructor', async function () { 27 | this.seed(); 28 | const buffer = this.db.serialize(); 29 | expect(buffer).to.be.an.instanceof(Buffer); 30 | expect(buffer.length).to.be.above(1000); 31 | this.db.prepare('delete from entries').run(); 32 | this.db.close(); 33 | this.db = new Database(buffer); 34 | const bufferCopy = this.db.serialize(); 35 | expect(buffer.length).to.equal(bufferCopy.length); 36 | expect(buffer).to.deep.equal(bufferCopy); 37 | this.db.prepare('insert into entries (rowid, a, b) values (?, ?, ?)').run(0, 'bar', -999); 38 | expect(this.db.prepare('select a, b from entries order by rowid limit 2').all()) 39 | .to.deep.equal([{ a: 'bar', b: -999 }, { a: 'foo', b: 1 }]); 40 | }); 41 | it('should accept the "attached" option', async function () { 42 | const smallBuffer = this.db.serialize(); 43 | this.seed(); 44 | const bigBuffer = this.db.serialize(); 45 | this.db.close(); 46 | this.db = new Database(); 47 | this.db.prepare('attach ? as other').run(util.current()); 48 | const smallBuffer2 = this.db.serialize(); 49 | const bigBuffer2 = this.db.serialize({ attached: 'other' }); 50 | expect(bigBuffer.length === bigBuffer2.length); 51 | expect(bigBuffer).to.deep.equal(bigBuffer2); 52 | expect(smallBuffer.length < bigBuffer.length); 53 | expect(smallBuffer2.length < bigBuffer.length); 54 | expect(smallBuffer).to.not.deep.equal(smallBuffer2); 55 | }); 56 | it('should return a buffer that can be opened with the "readonly" option', async function () { 57 | this.seed(); 58 | const buffer = this.db.serialize(); 59 | expect(buffer).to.be.an.instanceof(Buffer); 60 | expect(buffer.length).to.be.above(1000); 61 | this.db.close(); 62 | this.db = new Database(buffer, { readonly: true }); 63 | expect(() => this.db.prepare('insert into entries (rowid, a, b) values (?, ?, ?)').run(0, 'bar', -999)) 64 | .to.throw(Database.SqliteError); 65 | expect(this.db.prepare('select a, b from entries order by rowid limit 2').all()) 66 | .to.deep.equal([{ a: 'foo', b: 1 }, { a: 'foo', b: 2 }]); 67 | const bufferCopy = this.db.serialize(); 68 | expect(buffer.length).to.equal(bufferCopy.length); 69 | expect(buffer).to.deep.equal(bufferCopy); 70 | }); 71 | it('should work with an empty database', async function () { 72 | this.db.close(); 73 | this.db = new Database(); 74 | const buffer = this.db.serialize(); 75 | expect(buffer).to.be.an.instanceof(Buffer); 76 | expect(buffer.length).to.be.lte(4096); 77 | this.db.close(); 78 | this.db = new Database(buffer); 79 | expect(this.db.prepare("select * from sqlite_master").all()).to.deep.equal([]); 80 | expect(this.db.serialize().length).to.be.lte(4096); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/40.bigints.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('BigInts', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare('CREATE TABLE entries (a INTEGER, b REAL, c TEXT)').run(); 8 | }); 9 | afterEach(function () { 10 | this.db.close(); 11 | }); 12 | 13 | it('should bind to prepared statements', function () { 14 | const int = BigInt('1006028374637854687'); 15 | this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int); 16 | this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').bind(int, int, int).run(); 17 | 18 | const db2 = new Database(util.next()); 19 | try { 20 | db2.prepare('CREATE TABLE entries (a INTEGER, b REAL, c TEXT)').run(); 21 | db2.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int); 22 | db2.prepare('INSERT INTO entries VALUES (?, ?, ?)').bind(int, int, int).run(); 23 | } finally { 24 | db2.close(); 25 | } 26 | }); 27 | it('should be allowed as a return value in user-defined functions', function () { 28 | this.db.function('returnsInteger', a => BigInt(a + a)); 29 | expect(this.db.prepare('SELECT returnsInteger(?)').pluck().get(42)).to.equal(84); 30 | }); 31 | it('should get returned by operations after setting .safeIntegers()', function () { 32 | const int = BigInt('1006028374637854687'); 33 | this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int); 34 | this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int); 35 | 36 | let stmt = this.db.prepare('SELECT a FROM entries').pluck(); 37 | expect(stmt.get()).to.equal(1006028374637854700); 38 | expect(stmt.safeIntegers().get()).to.deep.equal(int); 39 | expect(stmt.get()).to.deep.equal(int); 40 | expect(stmt.safeIntegers(false).get()).to.equal(1006028374637854700); 41 | expect(stmt.get()).to.equal(1006028374637854700); 42 | expect(stmt.safeIntegers(true).get()).to.deep.equal(int); 43 | expect(stmt.get()).to.deep.equal(int); 44 | 45 | stmt = this.db.prepare('SELECT b FROM entries').pluck(); 46 | expect(stmt.get()).to.equal(1006028374637854700); 47 | expect(stmt.safeIntegers().get()).to.equal(1006028374637854700); 48 | 49 | stmt = this.db.prepare('SELECT c FROM entries').pluck(); 50 | expect(stmt.get()).to.equal('1006028374637854687'); 51 | expect(stmt.safeIntegers().get()).to.equal('1006028374637854687'); 52 | 53 | let lastRowid = this.db.prepare('SELECT rowid FROM entries ORDER BY rowid DESC').pluck().get(); 54 | stmt = this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)'); 55 | expect(stmt.run(int, int, int).lastInsertRowid).to.equal(++lastRowid); 56 | expect(stmt.safeIntegers().run(int, int, int).lastInsertRowid).to.deep.equal(BigInt(++lastRowid)); 57 | expect(stmt.run(int, int, int).lastInsertRowid).to.deep.equal(BigInt(++lastRowid)); 58 | expect(stmt.safeIntegers(false).run(int, int, int).lastInsertRowid).to.equal(++lastRowid); 59 | }); 60 | it('should get passed to functions defined with the "safeIntegers" option', function () { 61 | this.db.function('customfunc', { safeIntegers: true }, (a) => { return (typeof a) + a; }); 62 | expect(this.db.prepare('SELECT customfunc(?)').pluck().get(2)).to.equal('number2'); 63 | expect(this.db.prepare('SELECT customfunc(?)').pluck().get(BigInt(2))).to.equal('bigint2'); 64 | }); 65 | it('should get passed to aggregates defined with the "safeIntegers" option', function () { 66 | this.db.aggregate('customagg', { safeIntegers: true, step: (_, a) => { return (typeof a) + a; } }); 67 | expect(this.db.prepare('SELECT customagg(?)').pluck().get(2)).to.equal('number2'); 68 | expect(this.db.prepare('SELECT customagg(?)').pluck().get(BigInt(2))).to.equal('bigint2'); 69 | }); 70 | it('should get passed to virtual tables defined with the "safeIntegers" option', function () { 71 | this.db.table('customvtab', { safeIntegers: true, columns: ['x'], *rows(a) { yield [(typeof a) + a]; } }); 72 | expect(this.db.prepare('SELECT * FROM customvtab(?)').pluck().get(2)).to.equal('number2'); 73 | expect(this.db.prepare('SELECT * FROM customvtab(?)').pluck().get(BigInt(2))).to.equal('bigint2'); 74 | }); 75 | it('should respect the default setting on the database', function () { 76 | let arg; 77 | const int = BigInt('1006028374637854687'); 78 | const customFunctionArg = (name, options, dontDefine) => { 79 | dontDefine || this.db.function(name, options, (a) => { arg = a; }); 80 | this.db.prepare(`SELECT ${name}(?)`).get(int); 81 | return arg; 82 | }; 83 | const customAggregateArg = (name, options, dontDefine) => { 84 | dontDefine || this.db.aggregate(name, { ...options, step: (_, a) => { arg = a; } }); 85 | this.db.prepare(`SELECT ${name}(?)`).get(int); 86 | return arg; 87 | }; 88 | const customTableArg = (name, options, dontDefine) => { 89 | dontDefine || this.db.table(name, { ...options, columns: ['x'], *rows(a) { arg = a; } }); 90 | this.db.prepare(`SELECT * FROM ${name}(?)`).get(int); 91 | return arg; 92 | }; 93 | this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int); 94 | this.db.defaultSafeIntegers(true); 95 | 96 | const stmt = this.db.prepare('SELECT a FROM entries').pluck(); 97 | expect(stmt.get()).to.deep.equal(int); 98 | expect(stmt.safeIntegers(false).get()).to.equal(1006028374637854700); 99 | expect(customFunctionArg('a1')).to.deep.equal(int); 100 | expect(customFunctionArg('a2', { safeIntegers: false })).to.equal(1006028374637854700); 101 | expect(customAggregateArg('a1')).to.deep.equal(int); 102 | expect(customAggregateArg('a2', { safeIntegers: false })).to.equal(1006028374637854700); 103 | expect(customTableArg('a1')).to.deep.equal(int); 104 | expect(customTableArg('a2', { safeIntegers: false })).to.equal(1006028374637854700); 105 | 106 | this.db.defaultSafeIntegers(false); 107 | 108 | const stmt2 = this.db.prepare('SELECT a FROM entries').pluck(); 109 | expect(stmt2.get()).to.equal(1006028374637854700); 110 | expect(stmt2.safeIntegers().get()).to.deep.equal(int); 111 | expect(customFunctionArg('a3')).to.equal(1006028374637854700); 112 | expect(customFunctionArg('a4', { safeIntegers: true })).to.deep.equal(int); 113 | expect(customAggregateArg('a3')).to.equal(1006028374637854700); 114 | expect(customAggregateArg('a4', { safeIntegers: true })).to.deep.equal(int); 115 | expect(customTableArg('a3')).to.equal(1006028374637854700); 116 | expect(customTableArg('a4', { safeIntegers: true })).to.deep.equal(int); 117 | 118 | this.db.defaultSafeIntegers(); 119 | 120 | expect(stmt.get()).to.equal(1006028374637854700); 121 | expect(stmt2.get()).to.deep.equal(int); 122 | expect(customFunctionArg('a1', {}, true)).to.deep.equal(int); 123 | expect(customFunctionArg('a2', {}, true)).to.equal(1006028374637854700); 124 | expect(customFunctionArg('a3', {}, true)).to.equal(1006028374637854700); 125 | expect(customFunctionArg('a4', {}, true)).to.deep.equal(int); 126 | expect(customAggregateArg('a1', {}, true)).to.deep.equal(int); 127 | expect(customAggregateArg('a2', {}, true)).to.equal(1006028374637854700); 128 | expect(customAggregateArg('a3', {}, true)).to.equal(1006028374637854700); 129 | expect(customAggregateArg('a4', {}, true)).to.deep.equal(int); 130 | expect(customTableArg('a1', {}, true)).to.deep.equal(int); 131 | expect(customTableArg('a2', {}, true)).to.equal(1006028374637854700); 132 | expect(customTableArg('a3', {}, true)).to.equal(1006028374637854700); 133 | expect(customTableArg('a4', {}, true)).to.deep.equal(int); 134 | 135 | const stmt3 = this.db.prepare('SELECT a FROM entries').pluck(); 136 | expect(stmt3.get()).to.deep.equal(int); 137 | expect(stmt3.safeIntegers(false).get()).to.equal(1006028374637854700); 138 | expect(customFunctionArg('a5')).to.deep.equal(int); 139 | expect(customFunctionArg('a6', { safeIntegers: false })).to.equal(1006028374637854700); 140 | expect(customAggregateArg('a5')).to.deep.equal(int); 141 | expect(customAggregateArg('a6', { safeIntegers: false })).to.equal(1006028374637854700); 142 | expect(customTableArg('a5')).to.deep.equal(int); 143 | expect(customTableArg('a6', { safeIntegers: false })).to.equal(1006028374637854700); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/41.at-exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { existsSync, writeFileSync } = require('fs'); 3 | const { fork } = require('child_process'); 4 | 5 | describe('node::AtExit()', function () { 6 | this.slow(500); 7 | 8 | const source = (filename1, filename2) => ` 9 | 'use strict'; 10 | const Database = require('../.'); 11 | const db1 = new Database('${filename1.replace(/(?=\W)/g, '\\')}'); 12 | const db2 = new Database('${filename2.replace(/(?=\W)/g, '\\')}'); 13 | for (const db of [db1, db2]) { 14 | db.pragma('journal_mode = WAL'); 15 | db.prepare('CREATE TABLE people (name TEXT)').run(); 16 | db.prepare('INSERT INTO people VALUES (\\'foobar\\')').run(); 17 | } 18 | const interval = setInterval(() => {}, 60000); 19 | const messageHandler = (message) => { 20 | if (message !== 'bar') return; 21 | clearInterval(interval); 22 | process.removeListener('message', messageHandler); 23 | }; 24 | process.on('message', messageHandler); 25 | process.send('foo'); 26 | `; 27 | 28 | it('should close all databases when the process exits gracefully', async function () { 29 | const filename1 = util.next(); 30 | const filename2 = util.next(); 31 | const jsFile = filename1 + '.js'; 32 | writeFileSync(jsFile, source(filename1, filename2)); 33 | await new Promise((resolve, reject) => { 34 | const child = fork(jsFile); 35 | child.on('error', reject); 36 | child.on('close', () => reject(new Error('Child process was closed prematurely'))); 37 | child.on('message', (message) => { 38 | if (message !== 'foo') return; 39 | expect(existsSync(filename1)).to.be.true; 40 | expect(existsSync(filename1 + '-wal')).to.be.true; 41 | expect(existsSync(filename2)).to.be.true; 42 | expect(existsSync(filename2 + '-wal')).to.be.true; 43 | child.on('exit', resolve); 44 | child.send('bar'); 45 | }); 46 | }); 47 | expect(existsSync(filename1)).to.be.true; 48 | expect(existsSync(filename1 + '-wal')).to.be.false; 49 | expect(existsSync(filename2)).to.be.true; 50 | expect(existsSync(filename2 + '-wal')).to.be.false; 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/43.verbose.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('verbose mode', function () { 5 | afterEach(function () { 6 | if (this.db) this.db.close(); 7 | }); 8 | 9 | it('should throw when not given a function or null/undefined', function () { 10 | expect(() => (this.db = new Database(util.next(), { verbose: false }))).to.throw(TypeError); 11 | expect(() => (this.db = new Database(util.next(), { verbose: true }))).to.throw(TypeError); 12 | expect(() => (this.db = new Database(util.next(), { verbose: 123 }))).to.throw(TypeError); 13 | expect(() => (this.db = new Database(util.next(), { verbose: 'null' }))).to.throw(TypeError); 14 | expect(() => (this.db = new Database(util.next(), { verbose: {} }))).to.throw(TypeError); 15 | expect(() => (this.db = new Database(util.next(), { verbose: [] }))).to.throw(TypeError); 16 | }); 17 | it('should allow explicit null or undefined as a no-op', function () { 18 | for (const verbose of [undefined, null]) { 19 | const db = this.db = new Database(util.next(), { verbose }); 20 | db.exec('select 5'); 21 | db.close(); 22 | } 23 | }); 24 | it('should invoke the given function with all executed SQL', function () { 25 | let calls = []; 26 | function verbose(...args) { 27 | calls.push([this, ...args]); 28 | } 29 | const db = this.db = new Database(util.next(), { verbose }); 30 | const stmt = db.prepare('select ?'); 31 | db.exec('select 5'); 32 | db.prepare('create table data (x)').run(); 33 | stmt.get(BigInt(10)); 34 | stmt.all(BigInt(15)); 35 | stmt.iterate(BigInt(20)).return(); 36 | for (const x of stmt.iterate(BigInt(25))) {} 37 | db.pragma('cache_size'); 38 | db.prepare("insert into data values ('hi')").run(); 39 | db.prepare("insert into data values ('bye')").run(); 40 | expect(Array.from(db.prepare('select x from data order by rowid').pluck().iterate())) 41 | .to.deep.equal(['hi', 'bye']); 42 | expect(calls).to.deep.equal([ 43 | [undefined, 'select 5'], 44 | [undefined, 'create table data (x)'], 45 | [undefined, 'select 10'], 46 | [undefined, 'select 15'], 47 | [undefined, 'select 25'], 48 | [undefined, 'PRAGMA cache_size'], 49 | [undefined, "insert into data values ('hi')"], 50 | [undefined, "insert into data values ('bye')"], 51 | [undefined, 'select x from data order by rowid'], 52 | ]); 53 | }); 54 | it('should not fully expand very long bound parameter', function () { 55 | let calls = []; 56 | function verbose(...args) { 57 | calls.push([this, ...args]); 58 | } 59 | const db = this.db = new Database(util.next(), { verbose }); 60 | const stmt = db.prepare('select ?'); 61 | stmt.get('this is a fairly short parameter'); 62 | stmt.get('this is a slightly longer parameter'); 63 | stmt.get('this is surely a very long bound parameter value that doesnt need to be logged in its entirety'); 64 | expect(calls).to.deep.equal([ 65 | [undefined, "select 'this is a fairly short parameter'"], 66 | [undefined, "select 'this is a slightly longer parame'/*+3 bytes*/"], 67 | [undefined, "select 'this is surely a very long bound'/*+62 bytes*/"], 68 | ]); 69 | }); 70 | it('should abort the execution if the logger function throws', function () { 71 | let fail = false; 72 | let failures = 0; 73 | const err = new Error('foo'); 74 | const db = this.db = new Database(util.next(), { verbose: () => { if (fail) throw err; } }); 75 | db.prepare('create table data (x)').run(); 76 | db.function('fn', (value) => { 77 | if (fail) failures += 1; 78 | return value; 79 | }); 80 | const shouldThrow = (fn) => { 81 | expect(fn).to.not.throw(); 82 | expect(fn).to.not.throw(); 83 | fail = true; 84 | try { 85 | expect(fn).to.throw(err); 86 | } finally { 87 | fail = false; 88 | } 89 | expect(fn).to.not.throw(); 90 | expect(failures).to.equal(0); 91 | }; 92 | const use = (stmt, fn) => () => fn(stmt); 93 | shouldThrow(() => db.exec('select fn(5)')); 94 | shouldThrow(use(db.prepare('insert into data values (fn(5))'), stmt => stmt.run())); 95 | shouldThrow(use(db.prepare('insert into data values (fn(?))'), stmt => stmt.run(5))); 96 | shouldThrow(use(db.prepare('select fn(?)'), stmt => stmt.get(5))); 97 | shouldThrow(use(db.prepare('select fn(?)'), stmt => stmt.all(5))); 98 | shouldThrow(use(db.prepare('select fn(?)'), stmt => Array.from(stmt.iterate(5)))); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/44.worker-threads.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | if (parseInt(process.versions.node) >= 12) { 3 | const threads = require('worker_threads'); 4 | const Database = require('../.'); 5 | 6 | if (threads.isMainThread) { 7 | describe('Worker Threads', function () { 8 | afterEach(function () { 9 | if (this.db) this.db.close(); 10 | return this.cleanup; 11 | }); 12 | it('are properly supported', function () { 13 | this.slow(1000); 14 | return new Promise((resolve, reject) => { 15 | const db = this.db = Database(util.next()).defaultSafeIntegers(); 16 | expect(db.prepare('select 555').constructor.foo).to.be.undefined; 17 | db.prepare('select 555').constructor.foo = 5; 18 | expect(db.prepare('select 555').constructor.foo).to.equal(5); 19 | const worker = new threads.Worker(__filename); 20 | worker.on('exit', code => reject(new Error(`worker exited with code ${code}`))); 21 | worker.on('error', reject); 22 | worker.on('message', ({ msg, info, data }) => { 23 | try { 24 | if (msg === 'hello') { 25 | db.exec('create table data (a, b)'); 26 | worker.postMessage({ msg: 'hello', filename: util.current() }); 27 | } else if (msg === 'success') { 28 | const checkedData = db.prepare("select * from data").all(); 29 | expect(info.changes).to.equal(checkedData.length); 30 | expect(data).to.not.equal(checkedData); 31 | expect(data).to.deep.equal(checkedData); 32 | expect(db.prepare('select 555').constructor.foo).to.equal(5); 33 | resolve(); 34 | this.cleanup = worker.terminate(); 35 | } else { 36 | throw new Error('unexpected message from worker'); 37 | } 38 | } catch (err) { 39 | reject(err); 40 | this.cleanup = worker.terminate(); 41 | } 42 | }); 43 | }); 44 | }); 45 | }); 46 | } else { 47 | const { expect } = require('chai'); 48 | threads.parentPort.on('message', ({ msg, filename }) => { 49 | if (msg === 'hello') { 50 | const db = Database(filename).defaultSafeIntegers(); 51 | expect(db.prepare('select 555').constructor.foo).to.be.undefined; 52 | db.prepare('select 555').constructor.foo = 27; 53 | expect(db.prepare('select 555').constructor.foo).to.equal(27); 54 | const info = db.prepare("insert into data values (1, 2), ('foo', 5.5)").run(); 55 | const data = db.prepare("select * from data").all(); 56 | expect(info.changes).to.be.a('number'); 57 | expect(info.lastInsertRowid).to.be.a('bigint'); 58 | expect(data.length).to.equal(2); 59 | threads.parentPort.postMessage({ msg: 'success', info, data }); 60 | } else { 61 | throw new Error('unexpected message from main thread'); 62 | } 63 | }); 64 | threads.parentPort.postMessage({ msg: 'hello' }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/45.unsafe-mode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#unsafeMode()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.exec('create table foo (x)'); 8 | this.read = this.db.prepare('select 5'); 9 | this.write = this.db.prepare('insert into foo values (0)'); 10 | }); 11 | afterEach(function () { 12 | this.db.close(); 13 | }); 14 | 15 | it('should not allow unsafe operations by default', function () { 16 | let hadRow = false; 17 | for (const row of this.read.iterate()) { 18 | expect(() => this.write.run()).to.throw(TypeError); 19 | expect(() => this.db.exec('select 5')).to.throw(TypeError); 20 | expect(() => this.db.pragma('cache_size')).to.throw(TypeError); 21 | hadRow = true; 22 | } 23 | expect(hadRow).to.be.true; 24 | 25 | this.db.pragma('journal_mode = OFF'); 26 | this.db.pragma('writable_schema = ON'); 27 | expect(this.db.pragma('journal_mode', { simple: true })).to.equal('delete'); 28 | expect(() => this.db.exec("update sqlite_master set name = 'bar' where name = 'foo'")).to.throw(Database.SqliteError); 29 | }); 30 | it('should allow unsafe operations when toggled on', function () { 31 | this.db.unsafeMode(); 32 | 33 | let hadRow = false; 34 | for (const row of this.read.iterate()) { 35 | this.write.run(); 36 | this.db.exec('select 5'); 37 | this.db.pragma('cache_size'); 38 | hadRow = true; 39 | } 40 | expect(hadRow).to.be.true; 41 | 42 | this.db.pragma('journal_mode = OFF'); 43 | this.db.pragma('writable_schema = ON'); 44 | expect(this.db.pragma('journal_mode', { simple: true })).to.equal('off'); 45 | this.db.exec("update sqlite_master set name = 'bar' where name = 'foo'"); 46 | 47 | this.db.unsafeMode(false); 48 | expect(() => this.db.exec("update sqlite_master set name = 'foo' where name = 'bar'")).to.throw(Database.SqliteError); 49 | this.db.unsafeMode(true); 50 | this.db.exec("update sqlite_master set name = 'foo' where name = 'bar'"); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/50.misc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('miscellaneous', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | }); 8 | afterEach(function () { 9 | this.db.close(); 10 | }); 11 | 12 | it('supports LIMIT in DELETE statements', function () { 13 | this.db.prepare("CREATE TABLE foo (x INTEGER PRIMARY KEY)").run(); 14 | expect(this.db.prepare('INSERT INTO foo (x) VALUES (1), (2), (3)').run()) 15 | .to.deep.equal({ changes: 3, lastInsertRowid: 3 }); 16 | 17 | expect(this.db.prepare('DELETE FROM foo ORDER BY x ASC LIMIT 1').run()) 18 | .to.have.property('changes', 1); 19 | 20 | expect(this.db.prepare('SELECT x FROM foo ORDER BY x ASC').all()) 21 | .to.deep.equal([{ x: 2 }, { x: 3 }]); 22 | }); 23 | 24 | it('supports LIMIT in UPDATE statements', function () { 25 | this.db.prepare("CREATE TABLE foo (x INTEGER PRIMARY KEY, y INTEGER)").run(); 26 | expect(this.db.prepare('INSERT INTO foo (x, y) VALUES (1, 1), (2, 2), (3, 3)').run()) 27 | .to.deep.equal({ changes: 3, lastInsertRowid: 3 }); 28 | 29 | expect(this.db.prepare('UPDATE foo SET y = 100 ORDER BY x DESC LIMIT 2').run()) 30 | .to.have.property('changes', 2); 31 | 32 | expect(this.db.prepare('SELECT x, y FROM foo ORDER BY x ASC').all()) 33 | .to.deep.equal([{ x: 1, y: 1 }, { x: 2, y: 100 }, { x: 3, y: 100 }]); 34 | }); 35 | 36 | it('persists non-trivial quantities of reads and writes', function () { 37 | const runDuration = 1000; 38 | const runUntil = Date.now() + runDuration; 39 | this.slow(runDuration * 10); 40 | this.timeout(runDuration * 3); 41 | this.db.pragma("journal_mode = WAL"); 42 | this.db.prepare("CREATE TABLE foo (a INTEGER, b TEXT, c REAL)").run(); 43 | 44 | let i = 1; 45 | const r = 0.141592654; 46 | const insert = this.db.prepare("INSERT INTO foo VALUES (?, ?, ?)"); 47 | const insertMany = this.db.transaction((count) => { 48 | for (const end = i + count; i < end; ++i) { 49 | expect(insert.run(i, String(i), i + r)) 50 | .to.deep.equal({ changes: 1, lastInsertRowid: i }); 51 | } 52 | }); 53 | 54 | // Batched transactions of 100 inserts. 55 | while (Date.now() < runUntil) insertMany(100); 56 | 57 | // Expect 10K~50K on reasonable machines. 58 | expect(i).to.be.above(1000); 59 | 60 | const select = this.db.prepare("SELECT * FROM foo ORDER BY a DESC"); 61 | for (const row of select.iterate()) { 62 | i -= 1; 63 | expect(row).to.deep.equal({ a: i, b: String(i), c: i + r }); 64 | } 65 | 66 | expect(i).to.equal(1); 67 | }); 68 | }); 69 | --------------------------------------------------------------------------------