├── .dockerignore ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── prebuild-test-all.yml │ ├── prebuild-test.yml │ ├── prebuild.yml │ ├── test.yml │ └── update-sqlite3mc.yml ├── .gitignore ├── .npmrc ├── Dockerfile ├── 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 ├── sqlite3.gyp ├── sqlite3 │ ├── sqlite3.c │ ├── sqlite3.h │ └── sqlite3ext.h ├── test_extension.c └── update-sqlite3mc.sh ├── docker-compose.yaml ├── docs ├── api.md ├── benchmark.md ├── compilation.md ├── integer.md ├── performance.md ├── threads.md ├── tips.md ├── troubleshooting.md └── unsafe.md ├── index.d.ts ├── 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 ├── 46.encryption.js ├── 47.database.key.js ├── 48.database.rekey.js └── 50.misc.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # IDEs 2 | .idea 3 | 4 | # Node 5 | node_modules 6 | 7 | # Project 8 | .github 9 | benchmark 10 | build 11 | docs 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lzz linguist-language=C++ 2 | *.cpp -diff 3 | *.hpp -diff 4 | *.c -diff 5 | *.h -diff 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: m4heshd 2 | ko_fi: m4heshd 3 | custom: [ 4 | "https://wise.com/pay/me/maheshw9", 5 | "https://www.paypal.me/m4heshdtt" 6 | ] -------------------------------------------------------------------------------- /.github/workflows/prebuild-test-all.yml: -------------------------------------------------------------------------------- 1 | name: prebuild-test-all 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | NODE_BUILD_CMD: npx --no-install prebuild -r node -t 18.0.0 -t 20.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' 8 | ELECTRON_BUILD_CMD: npx --no-install prebuild -r electron -t 26.0.0 -t 27.0.0 -t 28.0.0 -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$' 9 | 10 | jobs: 11 | prebuild: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: 16 | - macos-13 17 | - macos-14 18 | - windows-2019 19 | name: Prebuild on ${{ matrix.os }} 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 18 26 | - if: ${{ startsWith(matrix.os, 'windows') }} 27 | run: pip.exe install setuptools 28 | - if: ${{ startsWith(matrix.os, 'macos') }} 29 | run: brew install python-setuptools 30 | - run: npm install --ignore-scripts 31 | - run: ${{ env.NODE_BUILD_CMD }} 32 | - run: ${{ env.ELECTRON_BUILD_CMD }} 33 | - if: matrix.os == 'windows-2019' 34 | run: | 35 | ${{ env.NODE_BUILD_CMD }} --arch ia32 36 | npx --no-install prebuild -r node -t 20.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' --arch arm64 37 | ${{ env.ELECTRON_BUILD_CMD }} --arch ia32 38 | ${{ env.ELECTRON_BUILD_CMD }} --arch arm64 39 | 40 | prebuild-linux-x64: 41 | name: Prebuild on Linux x64 42 | runs-on: ubuntu-latest 43 | container: node:18-bullseye 44 | steps: 45 | - uses: actions/checkout@v4 46 | - run: npm install --ignore-scripts 47 | - run: ${{ env.NODE_BUILD_CMD }} 48 | - run: ${{ env.ELECTRON_BUILD_CMD }} 49 | 50 | prebuild-alpine-linux: 51 | name: Prebuild on Alpine-Linux (x64) 52 | runs-on: ubuntu-latest 53 | container: node:18-alpine 54 | steps: 55 | - uses: actions/checkout@v4 56 | - run: apk add build-base git python3 py3-setuptools --update-cache 57 | - run: npm install --ignore-scripts 58 | - run: ${{ env.NODE_BUILD_CMD }} 59 | 60 | prebuild-alpine-linux-arm64: 61 | name: Prebuild on Alpine-Linux (arm64) 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v4 65 | - uses: docker/setup-qemu-action@v3 66 | - run: | 67 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/arm64 node:18-alpine -c "\ 68 | apk add build-base git python3 py3-setuptools --update-cache && \ 69 | cd /tmp/project && \ 70 | npm install --ignore-scripts && \ 71 | ${{ env.NODE_BUILD_CMD }}" 72 | 73 | prebuild-linux-arm: 74 | strategy: 75 | fail-fast: false 76 | matrix: 77 | arch: 78 | - arm/v7 79 | - arm64 80 | name: Prebuild on Linux (${{ matrix.arch }}) 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v4 84 | - uses: docker/setup-qemu-action@v3 85 | - run: | 86 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:18-bullseye -c "\ 87 | cd /tmp/project && \ 88 | npm install --ignore-scripts && \ 89 | ${{ env.NODE_BUILD_CMD }} && \ 90 | if [ '${{ matrix.arch }}' = 'arm64' ]; then ${{ env.ELECTRON_BUILD_CMD }} --arch arm64; fi" 91 | -------------------------------------------------------------------------------- /.github/workflows/prebuild-test.yml: -------------------------------------------------------------------------------- 1 | name: prebuild-test 2 | 3 | run-name: Prebuild tests (${{ inputs.runtime }} [${{ inputs.targets }}]) 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | runtime: 9 | description: 'Prebuild runtime' 10 | type: choice 11 | required: true 12 | default: node 13 | options: 14 | - node 15 | - electron 16 | targets: 17 | description: 'Runtime versions (targets)' 18 | required: true 19 | default: -t 18.0.0 20 | ubuntu-22.04: 21 | description: 'Test on ubuntu-22.04' 22 | type: boolean 23 | default: true 24 | windows-2019: 25 | description: 'Test on windows-2019' 26 | type: boolean 27 | default: true 28 | macos-13: 29 | description: 'Test on macos-13' 30 | type: boolean 31 | default: true 32 | alpine: 33 | description: 'Test on Alpine-Linux' 34 | type: boolean 35 | arm: 36 | description: 'Test on ARM(v7/64) architectures' 37 | type: boolean 38 | 39 | env: 40 | TEST_COMMAND: npx --no-install prebuild -r ${{ inputs.runtime }} ${{ inputs.targets }} --include-regex 'better_sqlite3.node$' 41 | 42 | jobs: 43 | input-setup: 44 | if: inputs['ubuntu-22.04'] == true || inputs['windows-2019'] == true || inputs['macos-13'] == true 45 | name: Preparing tests 46 | runs-on: ubuntu-latest 47 | outputs: 48 | platforms: ${{ steps.set-platforms.outputs.platforms }} 49 | steps: 50 | - name: Setting up platform matrix 51 | id: set-platforms 52 | run: | 53 | INPUTS='${{ toJSON(inputs) }}' 54 | PLATFORMS='{"os":[]}' 55 | 56 | for os in 'ubuntu-22.04' 'windows-2019' 'macos-13' 57 | do 58 | if [ "$(jq ".[\"$os\"]" <<< "$INPUTS")" = "true" ]; then PLATFORMS=$(jq -c ".os += [\"$os\"]" <<< "$PLATFORMS"); fi 59 | done 60 | 61 | echo "platforms=$PLATFORMS" >> $GITHUB_OUTPUT 62 | 63 | prebuild-test: 64 | strategy: 65 | fail-fast: false 66 | matrix: ${{ fromJSON(needs.input-setup.outputs.platforms) }} 67 | name: Testing prebuild on ${{ matrix.os }} 68 | needs: input-setup 69 | runs-on: ${{ matrix.os }} 70 | steps: 71 | - uses: actions/checkout@v4 72 | - uses: actions/setup-node@v4 73 | with: 74 | node-version: 18 75 | - if: ${{ startsWith(matrix.os, 'windows') }} 76 | run: pip.exe install setuptools 77 | - if: ${{ startsWith(matrix.os, 'macos') }} 78 | run: brew install python-setuptools 79 | - if: ${{ !startsWith(matrix.os, 'windows') && !startsWith(matrix.os, 'macos') }} 80 | run: python3 -m pip install setuptools 81 | - if: ${{ startsWith(matrix.os, 'ubuntu') }} 82 | run: | 83 | sudo apt update 84 | sudo apt install gcc-10 g++-10 -y 85 | 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 86 | - run: npm install --ignore-scripts 87 | - run: ${{ env.TEST_COMMAND }} 88 | - if: matrix.os == 'windows-2019' && inputs.arm == true 89 | run: ${{ env.TEST_COMMAND }} --arch arm64 90 | 91 | prebuild-test-mac-arm64: 92 | if: inputs.macos-13 == true && inputs.arm == true 93 | name: Testing prebuild on M1 macOS (arm64) 94 | runs-on: macos-14 95 | steps: 96 | - uses: actions/checkout@v4 97 | - uses: actions/setup-node@v4 98 | with: 99 | node-version: 18 100 | - run: brew install python-setuptools 101 | - run: npm install --ignore-scripts 102 | - run: ${{ env.TEST_COMMAND }} 103 | 104 | prebuild-test-alpine-linux: 105 | if: inputs.alpine == true 106 | name: Testing prebuild on Alpine-Linux (x64) 107 | runs-on: ubuntu-latest 108 | container: node:18-alpine 109 | steps: 110 | - uses: actions/checkout@v4 111 | - run: apk add build-base git python3 py3-setuptools --update-cache 112 | - run: npm install --ignore-scripts 113 | - run: ${{ env.TEST_COMMAND }} 114 | 115 | prebuild-test-alpine-linux-arm64: 116 | if: inputs.alpine == true && inputs.arm == true 117 | name: Testing prebuild on Alpine-Linux (arm64) 118 | runs-on: ubuntu-latest 119 | steps: 120 | - uses: actions/checkout@v4 121 | - uses: docker/setup-qemu-action@v3 122 | - run: | 123 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/arm64 node:18-alpine -c "\ 124 | apk add build-base git python3 py3-setuptools --update-cache && \ 125 | cd /tmp/project && \ 126 | npm install --ignore-scripts && \ 127 | ${{ env.TEST_COMMAND }}" 128 | 129 | prebuild-test-linux-arm: 130 | if: inputs['ubuntu-22.04'] == true && inputs.arm == true 131 | strategy: 132 | fail-fast: false 133 | matrix: 134 | arch: 135 | - arm/v7 136 | - arm64 137 | name: Testing prebuild on Linux (${{ matrix.arch }}) 138 | runs-on: ubuntu-latest 139 | steps: 140 | - uses: actions/checkout@v4 141 | - uses: docker/setup-qemu-action@v3 142 | - run: | 143 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:18-bullseye -c "\ 144 | cd /tmp/project && \ 145 | npm install --ignore-scripts && \ 146 | ${{ env.TEST_COMMAND }}" 147 | -------------------------------------------------------------------------------- /.github/workflows/prebuild.yml: -------------------------------------------------------------------------------- 1 | name: prebuild 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | env: 9 | NODE_BUILD_CMD: npx --no-install prebuild -r node -t 18.0.0 -t 20.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }} 10 | ELECTRON_BUILD_CMD: npx --no-install prebuild -r electron -t 26.0.0 -t 27.0.0 -t 28.0.0 -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$' -u ${{ secrets.GITHUB_TOKEN }} 11 | 12 | jobs: 13 | prebuild: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: 18 | - macos-13 19 | - macos-14 20 | - windows-2019 21 | name: Prebuild on ${{ matrix.os }} 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 18 28 | - if: ${{ startsWith(matrix.os, 'windows') }} 29 | run: pip.exe install setuptools 30 | - if: ${{ startsWith(matrix.os, 'macos') }} 31 | run: brew install python-setuptools 32 | - run: npm install --ignore-scripts 33 | - run: ${{ env.NODE_BUILD_CMD }} 34 | - run: ${{ env.ELECTRON_BUILD_CMD }} 35 | - if: matrix.os == 'windows-2019' 36 | run: | 37 | ${{ env.NODE_BUILD_CMD }} --arch ia32 38 | npx --no-install prebuild -r node -t 20.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' --arch arm64 -u ${{ secrets.GITHUB_TOKEN }} 39 | ${{ env.ELECTRON_BUILD_CMD }} --arch ia32 40 | ${{ env.ELECTRON_BUILD_CMD }} --arch arm64 41 | 42 | prebuild-linux-x64: 43 | name: Prebuild on Linux x64 44 | runs-on: ubuntu-latest 45 | container: node:18-bullseye 46 | steps: 47 | - uses: actions/checkout@v4 48 | - run: npm install --ignore-scripts 49 | - run: ${{ env.NODE_BUILD_CMD }} 50 | - run: ${{ env.ELECTRON_BUILD_CMD }} 51 | 52 | prebuild-alpine-linux: 53 | name: Prebuild on Alpine-Linux (x64) 54 | runs-on: ubuntu-latest 55 | container: node:18-alpine 56 | steps: 57 | - uses: actions/checkout@v4 58 | - run: apk add build-base git python3 py3-setuptools --update-cache 59 | - run: npm install --ignore-scripts 60 | - run: ${{ env.NODE_BUILD_CMD }} 61 | 62 | prebuild-alpine-linux-arm64: 63 | name: Prebuild on Alpine-Linux (arm64) 64 | runs-on: ubuntu-latest 65 | steps: 66 | - uses: actions/checkout@v4 67 | - uses: docker/setup-qemu-action@v3 68 | - run: | 69 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/arm64 node:18-alpine -c "\ 70 | apk add build-base git python3 py3-setuptools --update-cache && \ 71 | cd /tmp/project && \ 72 | npm install --ignore-scripts && \ 73 | ${{ env.NODE_BUILD_CMD }}" 74 | 75 | prebuild-linux-arm: 76 | strategy: 77 | fail-fast: false 78 | matrix: 79 | arch: 80 | - arm/v7 81 | - arm64 82 | name: Prebuild on Linux (${{ matrix.arch }}) 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v4 86 | - uses: docker/setup-qemu-action@v3 87 | - run: | 88 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:18-bullseye -c "\ 89 | cd /tmp/project && \ 90 | npm install --ignore-scripts && \ 91 | ${{ env.NODE_BUILD_CMD }} && \ 92 | if [ '${{ matrix.arch }}' = 'arm64' ]; then ${{ env.ELECTRON_BUILD_CMD }} --arch arm64; fi" 93 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | pull_request: 9 | branches: 10 | - master 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - ubuntu-22.04 20 | - macos-13 21 | - macos-14 22 | - windows-2019 23 | node: 24 | - 18 25 | - 20 26 | - 22 27 | name: Testing Node ${{ matrix.node }} on ${{ matrix.os }} 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node }} 34 | - if: ${{ startsWith(matrix.os, 'windows') }} 35 | run: pip.exe install setuptools 36 | - if: ${{ startsWith(matrix.os, 'macos') }} 37 | run: brew install python-setuptools 38 | - if: ${{ !startsWith(matrix.os, 'windows') && !startsWith(matrix.os, 'macos') }} 39 | run: python3 -m pip install setuptools 40 | - if: ${{ startsWith(matrix.os, 'ubuntu') }} 41 | run: | 42 | sudo apt update 43 | sudo apt install gcc-10 g++-10 -y 44 | 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 45 | - run: npm install --ignore-scripts 46 | - run: npm run build-debug 47 | - run: npm test 48 | - name: Test SpatiaLite extension 49 | if: ${{ startsWith(matrix.os, 'ubuntu') }} 50 | run: | 51 | sudo apt update 52 | sudo apt install libsqlite3-mod-spatialite -y 53 | node -e "require('./lib/index.js')(':memory:').loadExtension('mod_spatialite').exec('SELECT InitSpatialMetaData();')" 54 | 55 | test-alpine-linux: 56 | name: Testing Node 20 on Alpine-Linux 57 | runs-on: ubuntu-latest 58 | container: node:20-alpine 59 | steps: 60 | - uses: actions/checkout@v4 61 | - run: apk add build-base git python3 py3-setuptools --update-cache 62 | - run: npm install --ignore-scripts 63 | - run: npm run build-debug 64 | - run: npm test 65 | 66 | test-alpine-linux-arm64: 67 | name: Testing Node 20 on Alpine-Linux (arm64) 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | - uses: docker/setup-qemu-action@v3 72 | - run: | 73 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/arm64 node:20-alpine -c "\ 74 | apk add build-base git python3 py3-setuptools --update-cache && \ 75 | cd /tmp/project && \ 76 | npm install --ignore-scripts && \ 77 | npm run build-debug && \ 78 | npm test" 79 | 80 | test-linux-arm: 81 | strategy: 82 | fail-fast: false 83 | matrix: 84 | arch: 85 | - arm/v7 86 | - arm64 87 | name: Testing Node 20 on Linux (${{ matrix.arch }}) 88 | runs-on: ubuntu-latest 89 | steps: 90 | - uses: actions/checkout@v4 91 | - uses: docker/setup-qemu-action@v3 92 | - run: | 93 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:20 -c "\ 94 | cd /tmp/project && \ 95 | npm install --ignore-scripts && \ 96 | npm run build-debug && \ 97 | npm test" 98 | -------------------------------------------------------------------------------- /.github/workflows/update-sqlite3mc.yml: -------------------------------------------------------------------------------- 1 | name: update-sqlite3mc 2 | 3 | run-name: SQLite3MC update PR for ${{ inputs.version }} (SQLite ${{ inputs.sqlite_version }}) 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'SQLite3MultipleCiphers version (prefixed with "v")' 10 | required: true 11 | sqlite_release_year: 12 | description: 'SQLite release year' 13 | default: '2025' 14 | required: true 15 | sqlite_version: 16 | description: 'SQLite version' 17 | required: true 18 | sqlite_version_enc: 19 | description: 'SQLite version (encoded)' 20 | required: true 21 | 22 | jobs: 23 | download-and-update: 24 | name: Download and update SQLite3MultipleCiphers 25 | runs-on: ubuntu-latest 26 | env: 27 | ENV_VERSION: ${{ github.event.inputs.version }} 28 | ENV_SQLITE_RELEASE_YEAR: ${{ github.event.inputs.sqlite_release_year }} 29 | ENV_SQLITE_VERSION: ${{ github.event.inputs.sqlite_version }} 30 | ENV_SQLITE_VERSION_ENC: ${{ github.event.inputs.sqlite_version_enc }} 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version: 18 38 | - name: Create new update branch 39 | run: git checkout -b sqlite3mc-update-${{ env.ENV_VERSION }} 40 | - name: Update update-sqlite3mc script 41 | run: | 42 | sed -Ei "s/YEAR=\"[0-9]+\"/YEAR=\"${{ env.ENV_SQLITE_RELEASE_YEAR }}\"/g" ./deps/update-sqlite3mc.sh 43 | sed -Ei "s/VERSION=\"[0-9]+\"/VERSION=\"${{ env.ENV_SQLITE_VERSION_ENC }}\"/g" ./deps/update-sqlite3mc.sh 44 | sed -Ei "s/SQLITE3MC_VERSION=\"v[0-9]+.[0-9]+.[0-9]+\"/SQLITE3MC_VERSION=\"${{ env.ENV_VERSION }}\"/g" ./deps/update-sqlite3mc.sh 45 | - name: Download, build and package SQLite and SQLite3MC 46 | run: npm run update-sqlite3mc 47 | - name: Push update branch 48 | uses: stefanzweifel/git-auto-commit-action@v4 49 | with: 50 | commit_message: 'Update `SQLite3MultipleCiphers` to `${{ env.ENV_VERSION }}` (SQLite `${{ env.ENV_SQLITE_VERSION }}`)' 51 | branch: sqlite3mc-update-${{ env.ENV_VERSION }} 52 | - name: Create new PR 53 | uses: repo-sync/pull-request@v2 54 | with: 55 | github_token: ${{ secrets.ACTIONS_REPO_ACCESS }} 56 | source_branch: sqlite3mc-update-${{ env.ENV_VERSION }} 57 | pr_title: 'Update `SQLite3MultipleCiphers` to `${{ env.ENV_VERSION }}` (SQLite `${{ env.ENV_SQLITE_VERSION }}`)' 58 | pr_body: 'This is an automated pull request, updating `SQLite3MultipleCiphers` version to `${{ env.ENV_VERSION }}` with SQLite `${{ env.ENV_SQLITE_VERSION }}`.' 59 | -------------------------------------------------------------------------------- /.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 | 44 | # IDEs 45 | .idea 46 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nikolaik/python-nodejs:python3.11-nodejs18-slim 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y build-essential 5 | 6 | WORKDIR /bs3mc 7 | 8 | COPY package.json . 9 | RUN npm install --ignore-scripts --no-audit 10 | 11 | COPY binding.gyp . 12 | COPY deps ./deps 13 | COPY src ./src 14 | RUN npm run build-debug 15 | 16 | COPY . . 17 | 18 | CMD ["npm", "test"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Mahesh Bandara Wijerathna 4 | Copyright (c) 2017 Joshua Wise 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /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 | 'OTHER_LDFLAGS': ['-framework Security'], 16 | }, 17 | 'msvs_settings': { 18 | 'VCCLCompilerTool': { 19 | 'AdditionalOptions': [ 20 | '/std:c++20', 21 | ], 22 | }, 23 | }, 24 | 'conditions': [ 25 | ['OS=="linux"', { 26 | 'ldflags': [ 27 | '-Wl,-Bsymbolic', 28 | '-Wl,--exclude-libs,ALL', 29 | ], 30 | }], 31 | ], 32 | }, 33 | { 34 | 'target_name': 'test_extension', 35 | 'dependencies': ['deps/sqlite3.gyp:sqlite3'], 36 | 'conditions': [['sqlite3 == ""', { 'sources': ['deps/test_extension.c'] }]], 37 | }, 38 | ], 39 | } 40 | -------------------------------------------------------------------------------- /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_USER_AUTHENTICATION=0', 40 | 'SQLITE_USE_URI=0', 41 | ], 42 | } 43 | -------------------------------------------------------------------------------- /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', '', 'sqlite3.c', 'sqlite3.h', 'sqlite3ext.h'], 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)', 'sqlite3.c', 'sqlite3.h'], 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 | -------------------------------------------------------------------------------- /deps/update-sqlite3mc.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. clone the SQLite3MultipleCiphers repo, replace SQLite amalgamation and patch it. 11 | # 5. build the SQLite3MultipleCiphers amalgamation 12 | # 6. copy the generated amalgamation into the output directory (./sqlite3). 13 | # 7. export the defined compile-time options to a gyp file (./defines.gypi). 14 | # 8. update the docs (../docs/compilation.md) with details of this distribution. 15 | # 16 | # When a user builds better-sqlite3-multiple-ciphers, the following steps are taken: 17 | # 1. node-gyp loads the previously exported compile-time options (defines.gypi). 18 | # 2. the copy.js script copies the bundled amalgamation into the build folder. 19 | # 3. node-gyp compiles the copied sqlite3.c along with better_sqlite3.cpp. 20 | # 4. node-gyp links the two resulting binaries to generate better_sqlite3.node. 21 | # === 22 | 23 | YEAR="2025" 24 | VERSION="3490200" 25 | SQLITE3MC_VERSION="v2.1.1" 26 | 27 | # Defines below are sorted alphabetically 28 | DEFINES=" 29 | HAVE_INT16_T=1 30 | HAVE_INT32_T=1 31 | HAVE_INT8_T=1 32 | HAVE_STDINT_H=1 33 | HAVE_UINT16_T=1 34 | HAVE_UINT32_T=1 35 | HAVE_UINT8_T=1 36 | HAVE_USLEEP=1 37 | SQLITE_DEFAULT_CACHE_SIZE=-16000 38 | SQLITE_DEFAULT_FOREIGN_KEYS=1 39 | SQLITE_DEFAULT_MEMSTATUS=0 40 | SQLITE_DEFAULT_WAL_SYNCHRONOUS=1 41 | SQLITE_DQS=0 42 | SQLITE_ENABLE_COLUMN_METADATA 43 | SQLITE_ENABLE_DBSTAT_VTAB 44 | SQLITE_ENABLE_DESERIALIZE 45 | SQLITE_ENABLE_FTS3 46 | SQLITE_ENABLE_FTS3_PARENTHESIS 47 | SQLITE_ENABLE_FTS4 48 | SQLITE_ENABLE_FTS5 49 | SQLITE_ENABLE_GEOPOLY 50 | SQLITE_ENABLE_JSON1 51 | SQLITE_ENABLE_MATH_FUNCTIONS 52 | SQLITE_ENABLE_RTREE 53 | SQLITE_ENABLE_STAT4 54 | SQLITE_ENABLE_UPDATE_DELETE_LIMIT 55 | SQLITE_LIKE_DOESNT_MATCH_BLOBS 56 | SQLITE_OMIT_DEPRECATED 57 | SQLITE_OMIT_PROGRESS_CALLBACK 58 | SQLITE_OMIT_SHARED_CACHE 59 | SQLITE_OMIT_TCL_VARIABLE 60 | SQLITE_SOUNDEX 61 | SQLITE_THREADSAFE=2 62 | SQLITE_TRACE_SIZE_LIMIT=32 63 | SQLITE_USER_AUTHENTICATION=0 64 | SQLITE_USE_URI=0 65 | " 66 | 67 | # ========== START SCRIPT ========== # 68 | 69 | echo -e "Setting up environment..." 70 | DEPS="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" 71 | TEMP="$DEPS/temp" 72 | OUTPUT="$DEPS/sqlite3" 73 | rm -rf "$TEMP" 74 | rm -rf "$OUTPUT" 75 | mkdir -p "$TEMP" 76 | mkdir -p "$OUTPUT" 77 | export CFLAGS=`echo $(echo "$DEFINES" | sed -e "/^\s*$/d" -e "s/^/-D/")` 78 | 79 | echo -e "\nDownloading SQLite source..." 80 | curl -#f "https://www.sqlite.org/$YEAR/sqlite-src-$VERSION.zip" > "$TEMP/source.zip" || exit 1 81 | 82 | echo -e "\nExtracting SQLite source..." 83 | unzip "$TEMP/source.zip" -d "$TEMP" > /dev/null || exit 1 84 | cd "$TEMP/sqlite-src-$VERSION" || exit 1 85 | 86 | echo -e "\nConfiguring SQLite amalgamation..." 87 | sh configure > /dev/null || exit 1 88 | 89 | echo -e "\nBuilding SQLite amalgamation..." 90 | make OPTIONS="$CFLAGS" sqlite3.c > /dev/null || exit 1 91 | 92 | echo -e "\nCloning SQLite3MultipleCiphers repo..." 93 | cd "$TEMP" || exit 1 94 | git clone --quiet https://github.com/utelle/SQLite3MultipleCiphers.git > /dev/null || exit 1 95 | cd "$TEMP/SQLite3MultipleCiphers" || exit 1 96 | git checkout --quiet "tags/$SQLITE3MC_VERSION" > /dev/null || exit 1 97 | 98 | echo -e "\nReplacing SQLite amalgamation in SQLite3MultipleCiphers..." 99 | cd "$TEMP/sqlite-src-$VERSION" || exit 1 100 | (yes | cp -rf sqlite3.c sqlite3.h sqlite3ext.h "$TEMP/SQLite3MultipleCiphers/src") || exit 1 101 | 102 | echo -e "\nPatching SQLite amalgamation in SQLite3MultipleCiphers..." 103 | cd "$TEMP/SQLite3MultipleCiphers" || exit 1 104 | chmod +x ./scripts/patchsqlite3.sh || exit 1 105 | chmod +x ./scripts/rekeyvacuum.sh || exit 1 106 | ./scripts/patchsqlite3.sh ./src/sqlite3.c >./src/sqlite3patched.c || exit 1 107 | ./scripts/rekeyvacuum.sh ./src/sqlite3.c >./src/rekeyvacuum.c || exit 1 108 | 109 | echo -e "\nBuilding SQLite3MultipleCiphers amalgamation..." 110 | python3 ./scripts/amalgamate.py --config ./scripts/sqlite3mc.c.json --source ./src/ || exit 1 111 | mv sqlite3mc_amalgamation.c sqlite3.c || exit 1 112 | python3 ./scripts/amalgamate.py --config ./scripts/sqlite3mc.h.json --source ./src/ || exit 1 113 | mv sqlite3mc_amalgamation.h sqlite3.h || exit 1 114 | 115 | echo -e "\nCopying SQLite3MultipleCiphers amalgamation..." 116 | cp sqlite3.c sqlite3.h ./src/sqlite3ext.h "$OUTPUT/" || exit 1 117 | 118 | echo -e "\nUpdating gyp configs..." 119 | GYP="$DEPS/defines.gypi" 120 | printf "# THIS FILE IS AUTOMATICALLY GENERATED BY deps/download.sh (DO NOT EDIT)\n\n{\n 'defines': [\n" > "$GYP" 121 | printf "$DEFINES" | sed -e "/^\s*$/d" -e "s/\(.*\)/ '\1',/" >> "$GYP" 122 | printf " ],\n}\n" >> "$GYP" 123 | 124 | echo -e "\nUpdating docs..." 125 | DOCS="$DEPS/../docs/compilation.md" 126 | MAJOR=`expr "${VERSION:0:1}" + 0` 127 | MINOR=`expr "${VERSION:1:2}" + 0` 128 | PATCH=`expr "${VERSION:3:2}" + 0` 129 | sed -Ei.bak -e "s/version [0-9]+\.[0-9]+\.[0-9]+/version $MAJOR.$MINOR.$PATCH/g" "$DOCS" 130 | sed -i.bak -e "/^SQLITE_/,\$d" "$DOCS" 131 | sed -i.bak -e "/^HAVE_/,\$d" "$DOCS" 132 | rm "$DOCS".bak 133 | printf "$DEFINES" | sed -e "/^\s*$/d" >> "$DOCS" 134 | printf "\`\`\`\n" >> "$DOCS" 135 | 136 | echo -e "\nCleaning up..." 137 | cd - > /dev/null || exit 1 138 | rm -rf "$TEMP" 139 | 140 | echo -e "\nSQLite3MultipleCiphers update process completed!" 141 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | bs3mc-test: 5 | build: . 6 | image: m4heshd/bs3mc-test 7 | container_name: bs3mc-test 8 | restart: "no" 9 | -------------------------------------------------------------------------------- /docs/benchmark.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | To run the benchmark yourself: 4 | 5 | ```bash 6 | git clone https://github.com/m4heshd/better-sqlite3-multiple-ciphers.git 7 | cd better-sqlite3-multiple-ciphers 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-multiple-ciphers 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-multiple-ciphers 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-multiple-ciphers x 6,532 ops/sec ±0.32% 27 | node-sqlite3 x 268 ops/sec ±3.4% 28 | 29 | --- inserting rows individually --- 30 | better-sqlite3-multiple-ciphers 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-multiple-ciphers 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-multiple-ciphers`, 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-multiple-ciphers --build-from-source --sqlite3=/path/to/sqlite-amalgamation 7 | ``` 8 | 9 | However, if you simply run `npm install` while `better-sqlite3-multiple-ciphers` 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-multiple-ciphers` 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-multiple-ciphers@'^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-multiple-ciphers`, 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-multiple-ciphers` 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_USER_AUTHENTICATION=0 84 | SQLITE_USE_URI=0 85 | ``` 86 | -------------------------------------------------------------------------------- /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-multiple-ciphers` 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-multiple-ciphers` 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-multiple-ciphers')('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-multiple-ciphers` refuses to install, follow these guidelines: 4 | 5 | ## Use the latest version of better-sqlite3-multiple-ciphers 6 | 7 | - Check the [releases page](https://github.com/m4heshd/better-sqlite3-multiple-ciphers/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-multiple-ciphers` 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/m4heshd/better-sqlite3-multiple-ciphers/issues?q=is%3Aissue). 44 | -------------------------------------------------------------------------------- /docs/unsafe.md: -------------------------------------------------------------------------------- 1 | # Unsafe mode 2 | 3 | By default, `better-sqlite3-multiple-ciphers` 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-multiple-ciphers` will not prevent you from performing the dangerous operations listed above. 17 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for better-sqlite3-multiple-ciphers 11.8.0 2 | // Project: https://github.com/m4heshd/better-sqlite3-multiple-ciphers 3 | // Definitions by: Ben Davies 4 | // Mathew Rumsey 5 | // Santiago Aguilar 6 | // Alessandro Vergani 7 | // Andrew Kaiser 8 | // Mark Stewart 9 | // Florian Stamer 10 | // Mahesh Bandara Wijerathna 11 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 12 | // TypeScript Version: 3.8 13 | 14 | /// 15 | 16 | // FIXME: Is this `any` really necessary? 17 | type VariableArgFunction = (...params: any[]) => unknown; 18 | type ArgumentTypes = F extends (...args: infer A) => unknown ? A : never; 19 | type ElementOf = T extends Array ? E : T; 20 | 21 | declare namespace BetterSqlite3MultipleCiphers { 22 | interface Statement { 23 | database: Database; 24 | source: string; 25 | reader: boolean; 26 | readonly: boolean; 27 | busy: boolean; 28 | 29 | run(...params: BindParameters): Database.RunResult; 30 | get(...params: BindParameters): Result | undefined; 31 | all(...params: BindParameters): Result[]; 32 | iterate(...params: BindParameters): IterableIterator; 33 | pluck(toggleState?: boolean): this; 34 | expand(toggleState?: boolean): this; 35 | raw(toggleState?: boolean): this; 36 | bind(...params: BindParameters): this; 37 | columns(): ColumnDefinition[]; 38 | safeIntegers(toggleState?: boolean): this; 39 | } 40 | 41 | interface ColumnDefinition { 42 | name: string; 43 | column: string | null; 44 | table: string | null; 45 | database: string | null; 46 | type: string | null; 47 | } 48 | 49 | interface Transaction { 50 | (...params: ArgumentTypes): ReturnType; 51 | default(...params: ArgumentTypes): ReturnType; 52 | deferred(...params: ArgumentTypes): ReturnType; 53 | immediate(...params: ArgumentTypes): ReturnType; 54 | exclusive(...params: ArgumentTypes): ReturnType; 55 | } 56 | 57 | interface VirtualTableOptions { 58 | rows: (...params: unknown[]) => Generator; 59 | columns: string[]; 60 | parameters?: string[] | undefined; 61 | safeIntegers?: boolean | undefined; 62 | directOnly?: boolean | undefined; 63 | } 64 | 65 | interface Database { 66 | memory: boolean; 67 | readonly: boolean; 68 | name: string; 69 | open: boolean; 70 | inTransaction: boolean; 71 | 72 | prepare( 73 | source: string, 74 | ): BindParameters extends unknown[] ? Statement : Statement<[BindParameters], Result>; 75 | transaction(fn: F): Transaction; 76 | exec(source: string): this; 77 | key(key: Buffer): number; 78 | rekey(key: Buffer): number; 79 | pragma(source: string, options?: Database.PragmaOptions): unknown; 80 | function(name: string, cb: (...params: unknown[]) => unknown): this; 81 | function(name: string, options: Database.RegistrationOptions, cb: (...params: unknown[]) => unknown): this; 82 | aggregate( 83 | name: string, 84 | options: Database.RegistrationOptions & { 85 | start?: T | (() => T); 86 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 87 | step: (total: T, next: ElementOf) => T | void; 88 | inverse?: ((total: T, dropped: T) => T) | undefined; 89 | result?: ((total: T) => unknown) | undefined; 90 | }, 91 | ): this; 92 | loadExtension(path: string): this; 93 | close(): this; 94 | defaultSafeIntegers(toggleState?: boolean): this; 95 | backup(destinationFile: string, options?: Database.BackupOptions): Promise; 96 | table(name: string, options: VirtualTableOptions): this; 97 | unsafeMode(unsafe?: boolean): this; 98 | serialize(options?: Database.SerializeOptions): Buffer; 99 | } 100 | 101 | interface DatabaseConstructor { 102 | new(filename?: string | Buffer, options?: Database.Options): Database; 103 | (filename?: string, options?: Database.Options): Database; 104 | prototype: Database; 105 | SqliteError: SqliteErrorType; 106 | } 107 | } 108 | 109 | declare class SqliteErrorClass extends Error { 110 | name: string; 111 | message: string; 112 | code: string; 113 | constructor(message: string, code: string); 114 | } 115 | 116 | type SqliteErrorType = typeof SqliteErrorClass; 117 | 118 | declare namespace Database { 119 | interface RunResult { 120 | changes: number; 121 | lastInsertRowid: number | bigint; 122 | } 123 | 124 | interface Options { 125 | readonly?: boolean | undefined; 126 | fileMustExist?: boolean | undefined; 127 | timeout?: number | undefined; 128 | verbose?: ((message?: unknown, ...additionalArgs: unknown[]) => void) | undefined; 129 | nativeBinding?: string | undefined; 130 | } 131 | 132 | interface SerializeOptions { 133 | attached?: string; 134 | } 135 | 136 | interface PragmaOptions { 137 | simple?: boolean | undefined; 138 | } 139 | 140 | interface RegistrationOptions { 141 | varargs?: boolean | undefined; 142 | deterministic?: boolean | undefined; 143 | safeIntegers?: boolean | undefined; 144 | directOnly?: boolean | undefined; 145 | } 146 | 147 | type AggregateOptions = Parameters[1]; 148 | 149 | interface BackupMetadata { 150 | totalPages: number; 151 | remainingPages: number; 152 | } 153 | interface BackupOptions { 154 | progress: (info: BackupMetadata) => number; 155 | } 156 | 157 | type SqliteError = SqliteErrorType; 158 | type Statement = BindParameters extends unknown[] ? 159 | BetterSqlite3MultipleCiphers.Statement : 160 | BetterSqlite3MultipleCiphers.Statement<[BindParameters], Result>; 161 | type Transaction = BetterSqlite3MultipleCiphers.Transaction; 162 | type Database = BetterSqlite3MultipleCiphers.Database; 163 | } 164 | 165 | declare const Database: BetterSqlite3MultipleCiphers.DatabaseConstructor; 166 | export = Database; 167 | -------------------------------------------------------------------------------- /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.key = wrappers.key; 86 | Database.prototype.rekey = wrappers.rekey; 87 | Database.prototype.close = wrappers.close; 88 | Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers; 89 | Database.prototype.unsafeMode = wrappers.unsafeMode; 90 | Database.prototype[util.inspect] = require('./methods/inspect'); 91 | 92 | module.exports = Database; 93 | -------------------------------------------------------------------------------- /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.key = function key(key) { 9 | if (!Buffer.isBuffer(key)) throw new TypeError('Expected first argument to be a Buffer'); 10 | return this[cppdb].key(key, key.length); 11 | }; 12 | 13 | exports.rekey = function rekey(key) { 14 | if (!Buffer.isBuffer(key)) throw new TypeError('Expected first argument to be a Buffer'); 15 | return this[cppdb].rekey(key, key.length); 16 | }; 17 | 18 | exports.exec = function exec(sql) { 19 | this[cppdb].exec(sql); 20 | return this; 21 | }; 22 | 23 | exports.close = function close() { 24 | this[cppdb].close(); 25 | return this; 26 | }; 27 | 28 | exports.loadExtension = function loadExtension(...args) { 29 | this[cppdb].loadExtension(...args); 30 | return this; 31 | }; 32 | 33 | exports.defaultSafeIntegers = function defaultSafeIntegers(...args) { 34 | this[cppdb].defaultSafeIntegers(...args); 35 | return this; 36 | }; 37 | 38 | exports.unsafeMode = function unsafeMode(...args) { 39 | this[cppdb].unsafeMode(...args); 40 | return this; 41 | }; 42 | 43 | exports.getters = { 44 | name: { 45 | get: function name() { return this[cppdb].name; }, 46 | enumerable: true, 47 | }, 48 | open: { 49 | get: function open() { return this[cppdb].open; }, 50 | enumerable: true, 51 | }, 52 | inTransaction: { 53 | get: function inTransaction() { return this[cppdb].inTransaction; }, 54 | enumerable: true, 55 | }, 56 | readonly: { 57 | get: function readonly() { return this[cppdb].readonly; }, 58 | enumerable: true, 59 | }, 60 | memory: { 61 | get: function memory() { return this[cppdb].memory; }, 62 | enumerable: true, 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /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-multiple-ciphers", 3 | "version": "11.10.0", 4 | "description": "better-sqlite3 with multiple-cipher encryption support", 5 | "homepage": "https://github.com/m4heshd/better-sqlite3-multiple-ciphers", 6 | "author": "Mahesh Bandara Wijerathna (m4heshd) ", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/m4heshd/better-sqlite3-multiple-ciphers.git" 10 | }, 11 | "main": "lib/index.js", 12 | "types": "index.d.ts", 13 | "files": [ 14 | "index.d.ts", 15 | "binding.gyp", 16 | "src/*.[ch]pp", 17 | "lib/**", 18 | "deps/**" 19 | ], 20 | "dependencies": { 21 | "bindings": "^1.5.0", 22 | "prebuild-install": "^7.1.1" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "20.4.9", 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=30000", 42 | "test:container": "docker-compose up --detach --build", 43 | "benchmark": "node benchmark", 44 | "update-sqlite3mc": "bash ./deps/update-sqlite3mc.sh", 45 | "lzz": "lzz -hx hpp -sx cpp -k BETTER_SQLITE3 -d -hl -sl -e ./src/better_sqlite3.lzz", 46 | "bump:patch": "npm --no-git-tag-version version patch", 47 | "bump:minor": "npm --no-git-tag-version version minor", 48 | "bump:major": "npm --no-git-tag-version version major", 49 | "bump:patch:beta": "npm --no-git-tag-version --preid=beta version prepatch", 50 | "bump:minor:beta": "npm --no-git-tag-version --preid=beta version preminor", 51 | "bump:major:beta": "npm --no-git-tag-version --preid=beta version premajor", 52 | "bump:prerelease:beta": "npm --no-git-tag-version --preid=beta version prerelease", 53 | "release": "npm publish", 54 | "release:beta": "npm publish --tag beta" 55 | }, 56 | "license": "MIT", 57 | "keywords": [ 58 | "sql", 59 | "sqlite", 60 | "sqlite3", 61 | "sqleet", 62 | "sqlcipher", 63 | "sqlite3multipleciphers", 64 | "encryption", 65 | "transactions", 66 | "user-defined functions", 67 | "aggregate functions", 68 | "window functions", 69 | "database" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /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/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/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.equal(4096); 77 | this.db.close(); 78 | this.db = new Database(buffer); 79 | expect(this.db.serialize().length).to.equal(4096); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /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/46.encryption.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Encryption using default cipher (Sqleet)', () => { 5 | afterEach(() => { 6 | this.db.close(); 7 | }); 8 | 9 | it('should create an encrypted database', () => { 10 | this.db = new Database(util.next()); 11 | this.db.pragma(`rekey='passphrase'`); 12 | this.db.prepare('CREATE TABLE user ("name" TEXT)').run(); 13 | this.db.prepare("INSERT INTO user (name) VALUES ('octocat')").run(); 14 | this.db.prepare('VACUUM').run(); 15 | }); 16 | it('should not allow access without decryption', () => { 17 | this.db = new Database(util.current()); 18 | expect(() => this.db.prepare('SELECT * FROM user')).to.throw(Database.SqliteError); 19 | }); 20 | it('should not allow access with an incorrect passphrase', () => { 21 | this.db = new Database(util.current()); 22 | this.db.pragma(`key='false_passphrase'`); 23 | expect(() => this.db.prepare('SELECT * FROM user')).to.throw(Database.SqliteError); 24 | }); 25 | it('should allow access with the correct passphrase', () => { 26 | this.db = new Database(util.current()); 27 | this.db.pragma(`key='passphrase'`); 28 | const stmt = this.db.prepare('SELECT * FROM user'); 29 | expect(stmt.get()).to.deep.equal({name: 'octocat'}); 30 | }); 31 | it('should not allow to encrypt an in-memory database', () => { 32 | this.db = new Database(':memory:'); 33 | expect(() => this.db.pragma(`rekey='passphrase'`)).to.throw(Database.SqliteError); 34 | }); 35 | }); 36 | 37 | describe('Encryption using SQLCiper', () => { 38 | afterEach(() => { 39 | this.db.close(); 40 | }); 41 | 42 | it('should create an encrypted database', () => { 43 | this.db = new Database(util.next()); 44 | this.db.pragma(`cipher='sqlcipher'`); 45 | this.db.pragma(`rekey='passphrase'`); 46 | this.db.prepare('CREATE TABLE user ("name" TEXT)').run(); 47 | this.db.prepare("INSERT INTO user (name) VALUES ('octocat')").run(); 48 | this.db.prepare('VACUUM').run(); 49 | }); 50 | it('should not allow access without decryption', () => { 51 | this.db = new Database(util.current()); 52 | this.db.pragma(`cipher='sqlcipher'`); 53 | expect(() => this.db.prepare('SELECT * FROM user')).to.throw(Database.SqliteError); 54 | }); 55 | it('should not allow access with an incorrect passphrase', () => { 56 | this.db = new Database(util.current()); 57 | this.db.pragma(`cipher='sqlcipher'`); 58 | this.db.pragma(`key='false_passphrase'`); 59 | expect(() => this.db.prepare('SELECT * FROM user')).to.throw(Database.SqliteError); 60 | }); 61 | it('should not allow access with a different cipher', () => { 62 | this.db = new Database(util.current()); 63 | this.db.pragma(`key='passphrase'`); 64 | expect(() => this.db.prepare('SELECT * FROM user')).to.throw(Database.SqliteError); 65 | }); 66 | it('should allow access with the correct passphrase and cipher', () => { 67 | this.db = new Database(util.current()); 68 | this.db.pragma(`cipher='sqlcipher'`); 69 | this.db.pragma(`key='passphrase'`); 70 | const stmt = this.db.prepare('SELECT * FROM user'); 71 | expect(stmt.get()).to.deep.equal({name: 'octocat'}); 72 | }); 73 | it('should not allow to encrypt an in-memory database', () => { 74 | this.db = new Database(':memory:'); 75 | this.db.pragma(`cipher='sqlcipher'`); 76 | expect(() => this.db.pragma(`rekey='passphrase'`)).to.throw(Database.SqliteError); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/47.database.key.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#key()', function () { 5 | afterEach(function () { 6 | this.db.close(); 7 | }); 8 | 9 | it('should throw error if a Buffer is not provided', function () { 10 | this.db = new Database(util.next()); 11 | expect(() => this.db.key(123)).to.throw(TypeError); 12 | expect(() => this.db.key(0)).to.throw(TypeError); 13 | expect(() => this.db.key(null)).to.throw(TypeError); 14 | expect(() => this.db.key()).to.throw(TypeError); 15 | expect(() => this.db.key(new String('cache_size'))).to.throw(TypeError); 16 | }); 17 | it('should execute key() without errors', function () { 18 | this.db = new Database(util.current()); 19 | this.db.pragma(`cipher='aes256cbc'`); 20 | const status = this.db.key(Buffer.from('OkPassword')); 21 | this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER)'); 22 | expect(status).to.be.an('number'); 23 | expect(status).to.equal(0); 24 | }); 25 | it('should throw error when an incorrect key is provided', function () { 26 | this.db = new Database(util.current()); 27 | this.db.pragma(`cipher='aes256cbc'`); 28 | this.db.key(Buffer.from('WrongPassword')); 29 | expect(() => this.db.exec('select * from sqlite_schema')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_NOTADB'); 30 | }); 31 | it('should not throw error when the correct key is provided', function () { 32 | this.db = new Database(util.current()); 33 | this.db.pragma(`cipher='aes256cbc'`); 34 | this.db.key(Buffer.from('OkPassword')); 35 | expect(() => this.db.exec('select * from sqlite_schema')).to.not.throw(Database.SqliteError); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/48.database.rekey.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#rekey()', function () { 5 | afterEach(function () { 6 | this.db.close(); 7 | }); 8 | 9 | it('should throw error if a Buffer is not provided', function () { 10 | this.db = new Database(util.next()); 11 | expect(() => this.db.rekey(123)).to.throw(TypeError); 12 | expect(() => this.db.rekey(0)).to.throw(TypeError); 13 | expect(() => this.db.rekey(null)).to.throw(TypeError); 14 | expect(() => this.db.rekey()).to.throw(TypeError); 15 | expect(() => this.db.rekey(new String('cache_size'))).to.throw(TypeError); 16 | }); 17 | it('should execute rekey() without errors', function () { 18 | this.db = new Database(util.current()); 19 | this.db.pragma(`cipher='aes256cbc'`); 20 | const status = this.db.rekey(Buffer.from('OkPassword')); 21 | this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER)'); 22 | expect(status).to.be.an('number'); 23 | expect(status).to.equal(0); 24 | }); 25 | it('should throw error if an encrypted database is not decrypted before rekey()', function () { 26 | this.db = new Database(util.current()); 27 | this.db.pragma(`cipher='aes256cbc'`); 28 | expect(() => this.db.rekey(Buffer.from('NewPassword'))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_NOTADB'); 29 | }); 30 | it('should allow to rekey() if an already encrypted database is properly decrypted in advance', function () { 31 | this.db = new Database(util.current()); 32 | this.db.pragma(`cipher='aes256cbc'`); 33 | this.db.key(Buffer.from('OkPassword')); 34 | this.db.rekey(Buffer.from('NewPassword')); 35 | this.db.exec('select * from sqlite_schema'); 36 | this.db.close(); 37 | this.db = new Database(util.current()); 38 | this.db.pragma(`cipher='aes256cbc'`); 39 | this.db.key(Buffer.from('NewPassword')); 40 | expect(() => this.db.exec('select * from sqlite_schema')).to.not.throw(Database.SqliteError); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/50.misc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | describe('miscellaneous', function () { 4 | beforeEach(function () { 5 | this.db = new Database(util.next()); 6 | }); 7 | afterEach(function () { 8 | this.db.close(); 9 | }); 10 | 11 | it('supports LIMIT in DELETE statements', function () { 12 | this.db.prepare("CREATE TABLE foo (x INTEGER PRIMARY KEY)").run(); 13 | expect(this.db.prepare('INSERT INTO foo (x) VALUES (1), (2), (3)').run()) 14 | .to.deep.equal({ changes: 3, lastInsertRowid: 3 }); 15 | 16 | expect(this.db.prepare('DELETE FROM foo ORDER BY x ASC LIMIT 1').run()) 17 | .to.have.property('changes', 1); 18 | 19 | expect(this.db.prepare('SELECT x FROM foo ORDER BY x ASC').all()) 20 | .to.deep.equal([{ x: 2 }, { x: 3 }]); 21 | }); 22 | 23 | it('supports LIMIT in UPDATE statements', function () { 24 | this.db.prepare("CREATE TABLE foo (x INTEGER PRIMARY KEY, y INTEGER)").run(); 25 | expect(this.db.prepare('INSERT INTO foo (x, y) VALUES (1, 1), (2, 2), (3, 3)').run()) 26 | .to.deep.equal({ changes: 3, lastInsertRowid: 3 }); 27 | 28 | expect(this.db.prepare('UPDATE foo SET y = 100 ORDER BY x DESC LIMIT 2').run()) 29 | .to.have.property('changes', 2); 30 | 31 | expect(this.db.prepare('SELECT x, y FROM foo ORDER BY x ASC').all()) 32 | .to.deep.equal([{ x: 1, y: 1 }, { x: 2, y: 100 }, { x: 3, y: 100 }]); 33 | }); 34 | it('persists non-trivial quantities of reads and writes', function () { 35 | const runDuration = 10000; 36 | const runUntil = Date.now() + runDuration; 37 | this.slow(runDuration * 10); 38 | this.timeout(runDuration * 3); 39 | this.db.pragma("journal_mode = WAL"); 40 | this.db.prepare("CREATE TABLE foo (a INTEGER, b TEXT, c REAL)").run(); 41 | 42 | let i = 1; 43 | const r = 0.141592654; 44 | const insert = this.db.prepare("INSERT INTO foo VALUES (?, ?, ?)"); 45 | const insertMany = this.db.transaction((count) => { 46 | for (const end = i + count; i < end; ++i) { 47 | expect(insert.run(i, String(i), i + r)) 48 | .to.deep.equal({ changes: 1, lastInsertRowid: i }); 49 | } 50 | }); 51 | 52 | // Batched transactions of 100 inserts. 53 | while (Date.now() < runUntil) insertMany(100); 54 | 55 | // Expect 10K~50K on reasonable machines. 56 | expect(i).to.be.above(1000); 57 | 58 | const select = this.db.prepare("SELECT * FROM foo ORDER BY a DESC"); 59 | for (const row of select.iterate()) { 60 | i -= 1; 61 | expect(row).to.deep.equal({ a: i, b: String(i), c: i + r }); 62 | } 63 | 64 | expect(i).to.equal(1); 65 | }); 66 | }); 67 | --------------------------------------------------------------------------------