├── .npmrc ├── .gitattributes ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── bump-version.yml │ ├── update-sqlite.yml │ └── build.yml ├── deps ├── bearssl.tar.gz ├── sqlcipher.tar.gz ├── extract.js ├── patches │ └── 1208.patch ├── test_extension.c ├── copy.js ├── defines.gypi ├── common.gypi └── download.sh ├── lib ├── index.js ├── methods │ ├── inspect.js │ ├── pragma.js │ ├── serialize.js │ ├── wrappers.js │ ├── function.js │ ├── aggregate.js │ ├── backup.js │ ├── transaction.js │ └── table.js ├── util.js ├── sqlite-error.js ├── database.js └── index.d.ts ├── src ├── util │ ├── data-converter.lzz │ ├── custom-function.lzz │ ├── bind-map.lzz │ ├── query-macros.lzz │ ├── custom-aggregate.lzz │ ├── binder.lzz │ ├── constants.lzz │ └── data.lzz ├── better_sqlite3.lzz └── objects │ ├── statement-iterator.lzz │ └── backup.lzz ├── benchmark ├── types │ ├── select.js │ ├── select-all.js │ ├── insert.js │ ├── select-iterate.js │ └── transaction.js ├── drivers.js ├── benchmark.js ├── seed.js ├── index.js └── trials.js ├── README.md ├── test ├── 00.setup.js ├── 01.sqlite-error.js ├── 50.misc.js ├── 45.unsafe-mode.js ├── 14.database.exec.js ├── 41.at-exit.js ├── 31.database.checkpoint.js ├── 25.statement.columns.js ├── 44.worker-threads.js ├── 11.database.close.js ├── 35.database.load-extension.js ├── 12.database.pragma.js ├── 37.database.serialize.js ├── 24.statement.bind.js ├── 43.verbose.js ├── 13.database.prepare.js ├── 60.sqlcipher.js ├── 21.statement.get.js ├── 22.statement.all.js ├── 40.bigints.js └── 30.database.transaction.js ├── .gitlab-ci.yml ├── .gitignore ├── docs ├── unsafe.md ├── benchmark.md ├── conduct.md ├── troubleshooting.md ├── tips.md ├── performance.md ├── threads.md ├── integer.md └── compilation.md ├── LICENSE ├── binding.gyp └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lzz linguist-language=C++ 2 | *.cpp -diff 3 | *.hpp -diff 4 | *.c -diff 5 | *.h -diff 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JoshuaWise 2 | patreon: joshuawise 3 | custom: "https://www.paypal.me/joshuathomaswise" 4 | -------------------------------------------------------------------------------- /deps/bearssl.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threema-ch/better-sqlcipher/11.3.0-sqlcipher4.6.1-bearssl0.6/deps/bearssl.tar.gz -------------------------------------------------------------------------------- /deps/sqlcipher.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threema-ch/better-sqlcipher/11.3.0-sqlcipher4.6.1-bearssl0.6/deps/sqlcipher.tar.gz -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = require('./database'); 3 | module.exports.SqliteError = require('./sqlite-error'); 4 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /deps/extract.js: -------------------------------------------------------------------------------- 1 | const tar = require("tar"); 2 | 3 | const source = process.argv[2]; 4 | const destination = process.argv[3]; 5 | 6 | process.on("unhandledRejection", (err) => { 7 | throw err; 8 | }); 9 | 10 | /* 11 | * This extracts the <$2> tar file and places the resulting files into the 12 | * directory specified by <$3>. 13 | */ 14 | tar 15 | .extract({ file: source, cwd: destination, onwarn: process.emitWarning }) 16 | .then(() => process.exit(0)); 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @JoshuaWise 2 | /package.json @JoshuaWise @WiseLibs/better-sqlite3-team 3 | /docs/compilation.md @JoshuaWise @WiseLibs/better-sqlite3-team 4 | /docs/performance.md @JoshuaWise @WiseLibs/better-sqlite3-team 5 | /docs/troubleshooting.md @JoshuaWise @WiseLibs/better-sqlite3-team 6 | /deps/sqlite3/sqlite3.c @JoshuaWise @WiseLibs/better-sqlite3-team 7 | /deps/sqlite3/sqlite3.h @JoshuaWise @WiseLibs/better-sqlite3-team 8 | /deps/sqlite3/sqlite3ext.h @JoshuaWise @WiseLibs/better-sqlite3-team 9 | /.github/workflows/build.yml @JoshuaWise @WiseLibs/better-sqlite3-team 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # better-sqlcipher 2 | 3 | [better-sqlite3](https://github.com/JoshuaWise/better-sqlite3) built with 4 | [SQLCipher](https://www.zetetic.net/sqlcipher/). 5 | 6 | This is the better-sqlite3 library patched to use SQLCipher instead. 7 | 8 | ## Changes 9 | 10 | - Use SQLCipher instead of SQLite 11 | - Disable some SQLite features that we don't use to reduce the attack surface 12 | - Remove `prebuild` dependency and always build locally 13 | - Remove `bindings` dependency 14 | - Remove `sqlite`, `sqlite3` and `nodemark` dependencies (this breaks benchmarks) 15 | - Add type declarations 16 | - Add tests for the encryption functionality 17 | -------------------------------------------------------------------------------- /deps/patches/1208.patch: -------------------------------------------------------------------------------- 1 | diff --git a/deps/sqlite3/sqlite3.c b/deps/sqlite3/sqlite3.c 2 | index b1a807f..38bd1e6 100644 3 | --- a/deps/sqlite3/sqlite3.c 4 | +++ b/deps/sqlite3/sqlite3.c 5 | @@ -24887,8 +24887,8 @@ static const struct { 6 | /* 1 */ { 6, "minute", 7.7379e+12, 60.0 }, 7 | /* 2 */ { 4, "hour", 1.2897e+11, 3600.0 }, 8 | /* 3 */ { 3, "day", 5373485.0, 86400.0 }, 9 | - /* 4 */ { 5, "month", 176546.0, 30.0*86400.0 }, 10 | - /* 5 */ { 4, "year", 14713.0, 365.0*86400.0 }, 11 | + /* 4 */ { 5, "month", 176546.0, 2592000.0 }, 12 | + /* 5 */ { 4, "year", 14713.0, 31536000.0 }, 13 | }; 14 | 15 | /* 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /deps/test_extension.c: -------------------------------------------------------------------------------- 1 | #include 2 | SQLITE_EXTENSION_INIT1 3 | 4 | /* 5 | This SQLite3 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /benchmark/drivers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | Every benchmark trial will be executed once for each SQLite3 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 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | workflow: 2 | rules: 3 | - if: $CI_COMMIT_TAG 4 | - if: $CI_COMMIT_BRANCH 5 | 6 | image: docker.io/node:16-buster 7 | 8 | .cache: &cache 9 | cache: 10 | key: 11 | files: 12 | - package-lock.json 13 | paths: 14 | - .npm/ 15 | - node_modules/ 16 | 17 | audit: 18 | stage: test 19 | <<: *cache 20 | script: 21 | - rm .npmrc 22 | - npm install --ignore-scripts --cache .npm 23 | - npm audit 24 | allow_failure: true 25 | 26 | build_and_test: 27 | stage: test 28 | <<: *cache 29 | script: 30 | - npm install --ignore-scripts --cache .npm 31 | - npm run build-debug 32 | - sed -i 's/Release/Debug/' lib/database.js 33 | - sed -i 's/Release/Debug/' test/10.database.open.js 34 | - npm run test 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /.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 | # Generated binaries 45 | bin/ 46 | -------------------------------------------------------------------------------- /docs/unsafe.md: -------------------------------------------------------------------------------- 1 | # Unsafe mode 2 | 3 | By default, `better-sqlite3` prevents you from doing things that might corrupt your database or cause undefined behavior. Such unsafe operations include: 4 | 5 | - Anything blocked by [`SQLITE_DBCONFIG_DEFENSIVE`](https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigdefensive) 6 | - Mutating the database while [iterating](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/api.md#iteratebindparameters---iterator) through a query's result set 7 | 8 | However, some advanced users might want to use these functionalities at their own risk. For this reason, users have the option of enabling "unsafe mode". 9 | 10 | ```js 11 | db.unsafeMode(); // Unsafe mode ON 12 | db.unsafeMode(true); // Unsafe mode ON 13 | db.unsafeMode(false); // Unsafe mode OFF 14 | ``` 15 | 16 | Unsafe mode can be toggled at any time, and independently for each database connection. While toggled on, `better-sqlite3` will not prevent you from performing the dangerous operations listed above. 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | name: bump-version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | type: 7 | type: choice 8 | description: Type of version bump 9 | required: true 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | 15 | jobs: 16 | bump: 17 | name: Bump to a new version 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | token: ${{ secrets.PAT }} 23 | fetch-depth: 0 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 18 27 | - name: Configure user 28 | run: | 29 | git config --local user.name "${{ github.actor }}" 30 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 31 | - name: Bump the version 32 | run: npm version ${{ github.event.inputs.type }} 33 | - name: Push commit 34 | run: git push origin master:master 35 | - name: Push tag 36 | run: git push origin --tags 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Joshua Wise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/benchmark.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | To run the benchmark yourself: 4 | 5 | ```bash 6 | git clone https://github.com/JoshuaWise/better-sqlite3.git 7 | cd better-sqlite3 8 | npm install # if you're doing this as the root user, --unsafe-perm is required 9 | node benchmark 10 | ``` 11 | 12 | # Results 13 | 14 | These results are from 03/29/2020, on a MacBook Pro (Retina, 15-inch, Mid 2014, OSX 10.11.6), using nodejs v12.16.1. 15 | 16 | ``` 17 | --- reading rows individually --- 18 | better-sqlite3 x 313,899 ops/sec ±0.13% 19 | node-sqlite3 x 26,780 ops/sec ±2.9% 20 | 21 | --- reading 100 rows into an array --- 22 | better-sqlite3 x 8,508 ops/sec ±0.27% 23 | node-sqlite3 x 2,930 ops/sec ±0.37% 24 | 25 | --- iterating over 100 rows --- 26 | better-sqlite3 x 6,532 ops/sec ±0.32% 27 | node-sqlite3 x 268 ops/sec ±3.4% 28 | 29 | --- inserting rows individually --- 30 | better-sqlite3 x 62,554 ops/sec ±7.33% 31 | node-sqlite3 x 22,637 ops/sec ±4.37% 32 | 33 | --- inserting 100 rows in a single transaction --- 34 | better-sqlite3 x 4,141 ops/sec ±4.57% 35 | node-sqlite3 x 265 ops/sec ±4.87% 36 | ``` 37 | 38 | > All benchmarks are executed in [WAL mode](./performance.md). 39 | -------------------------------------------------------------------------------- /docs/conduct.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | Topics of discussion are expected to be constrained such that all discussion is relevant to the following goals: 4 | 5 | - Maintaining `better-sqlite3`'s code, documentation, and build artifacts 6 | - Helping people *get started* in using `better-sqlite3` within their software projects 7 | 8 | Other areas of discussion are considered to be off-topic, including but not limited to: 9 | 10 | - Politics 11 | - Name-calling, insults 12 | - Help with using SQLite (there's already [very good documentation](https://sqlite.org/docs.html) for that) 13 | - Help with application architecture, and other high-level decisions about software projects 14 | - Attention to personal traits such as race, gender, religion, national origin, sexual orientation, disability, etc. 15 | 16 | Repeated offenses against this code of conduct may result in being temporarily banned from the community. Unofficially, the community is expected to maintain a manner of professionalism and to treat others with respect. 17 | 18 | Attempting to physically seize, sabotage, or distribute malware through `better-sqlite3` will result in being permanently banned from the community, without warning. 19 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | # === 2 | # This is the main GYP file, which builds better-sqlcipher. 3 | # === 4 | 5 | { 6 | 'includes': ['deps/common.gypi'], 7 | 'targets': [ 8 | { 9 | 'target_name': 'better_sqlcipher', 10 | 'dependencies': ['deps/sqlcipher.gyp:sqlcipher'], 11 | 'sources': ['src/better_sqlite3.cpp'], 12 | 'cflags_cc': ['-std=c++20'], 13 | 'xcode_settings': { 14 | 'OTHER_CPLUSPLUSFLAGS': ['-std=c++20', '-stdlib=libc++'], 15 | }, 16 | 'msvs_settings': { 17 | 'VCCLCompilerTool': { 18 | 'AdditionalOptions': [ 19 | '/std:c++20', 20 | ], 21 | }, 22 | 'VCLinkerTool': { 23 | 'AdditionalOptions': ['/Brepro', '/PDBALTPATH:%_PDB%'] # Reproducible builds 24 | } 25 | }, 26 | 'conditions': [ 27 | ['OS=="linux"', { 28 | 'ldflags': [ 29 | '-Wl,-Bsymbolic', 30 | '-Wl,--exclude-libs,ALL', 31 | ], 32 | }], 33 | ], 34 | }, 35 | { 36 | 'target_name': 'test_extension', 37 | 'dependencies': ['deps/sqlcipher.gyp:sqlcipher'], 38 | 'sources': ['deps/test_extension.c'], 39 | }, 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 'SQLCIPHER_CRYPTO_BEARSSL', 14 | 'SQLITE_DEFAULT_CACHE_SIZE=-16000', 15 | 'SQLITE_DEFAULT_FOREIGN_KEYS=1', 16 | 'SQLITE_DEFAULT_MEMSTATUS=0', 17 | 'SQLITE_DEFAULT_WAL_SYNCHRONOUS=1', 18 | 'SQLITE_DQS=0', 19 | 'SQLITE_ENABLE_COLUMN_METADATA', 20 | 'SQLITE_ENABLE_DESERIALIZE', 21 | 'SQLITE_ENABLE_FTS5', 22 | 'SQLITE_ENABLE_JSON1', 23 | 'SQLITE_ENABLE_MATH_FUNCTIONS', 24 | 'SQLITE_ENABLE_STAT4', 25 | 'SQLITE_ENABLE_UPDATE_DELETE_LIMIT', 26 | 'SQLITE_HAS_CODEC', 27 | 'SQLITE_LIKE_DOESNT_MATCH_BLOBS', 28 | 'SQLITE_OMIT_DEPRECATED', 29 | 'SQLITE_OMIT_GET_TABLE', 30 | 'SQLITE_OMIT_PROGRESS_CALLBACK', 31 | 'SQLITE_OMIT_SHARED_CACHE', 32 | 'SQLITE_OMIT_TCL_VARIABLE', 33 | 'SQLITE_SECURE_DELETE', 34 | 'SQLITE_TEMP_STORE=3', 35 | 'SQLITE_THREADSAFE=2', 36 | 'SQLITE_TRACE_SIZE_LIMIT=32', 37 | 'SQLITE_USE_URI=0', 38 | ], 39 | } 40 | -------------------------------------------------------------------------------- /lib/methods/wrappers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { cppdb } = require('../util'); 3 | 4 | exports.prepare = function prepare(sql) { 5 | return this[cppdb].prepare(sql, this, false); 6 | }; 7 | 8 | exports.exec = function exec(sql) { 9 | this[cppdb].exec(sql); 10 | return this; 11 | }; 12 | 13 | exports.close = function close() { 14 | this[cppdb].close(); 15 | return this; 16 | }; 17 | 18 | exports.loadExtension = function loadExtension(...args) { 19 | this[cppdb].loadExtension(...args); 20 | return this; 21 | }; 22 | 23 | exports.defaultSafeIntegers = function defaultSafeIntegers(...args) { 24 | this[cppdb].defaultSafeIntegers(...args); 25 | return this; 26 | }; 27 | 28 | exports.unsafeMode = function unsafeMode(...args) { 29 | this[cppdb].unsafeMode(...args); 30 | return this; 31 | }; 32 | 33 | exports.getters = { 34 | name: { 35 | get: function name() { return this[cppdb].name; }, 36 | enumerable: true, 37 | }, 38 | open: { 39 | get: function open() { return this[cppdb].open; }, 40 | enumerable: true, 41 | }, 42 | inTransaction: { 43 | get: function inTransaction() { return this[cppdb].inTransaction; }, 44 | enumerable: true, 45 | }, 46 | readonly: { 47 | get: function readonly() { return this[cppdb].readonly; }, 48 | enumerable: true, 49 | }, 50 | memory: { 51 | get: function memory() { return this[cppdb].memory; }, 52 | enumerable: true, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-sqlcipher", 3 | "version": "11.3.0-sqlcipher4.6.1-bearssl0.6", 4 | "description": "better-sqlite3 built with SQLCipher", 5 | "author": "Threema GmbH ", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "files": [ 9 | "binding.gyp", 10 | "src/*.[ch]pp", 11 | "lib/**", 12 | "deps/**" 13 | ], 14 | "devDependencies": { 15 | "chai": "^4.3.8", 16 | "cli-color": "^2.0.3", 17 | "fs-extra": "^11.1.1", 18 | "mocha": "^10.2.0", 19 | "tar": "^6.1.13" 20 | }, 21 | "scripts": { 22 | "install": "node-gyp rebuild --release -j max", 23 | "build-release": "node-gyp rebuild --release -j max", 24 | "build-debug": "node-gyp rebuild --debug -j max", 25 | "rebuild-release": "npm run lzz && npm run build-release", 26 | "rebuild-debug": "npm run lzz && npm run build-debug", 27 | "test": "mocha --exit --slow=75 --timeout=5000", 28 | "benchmark": "node benchmark", 29 | "download": "(cd deps && bash ./download.sh)", 30 | "lzz": "lzz -hx hpp -sx cpp -k BETTER_SQLITE3 -d -hl -sl -e ./src/better_sqlite3.lzz" 31 | }, 32 | "license": "MIT", 33 | "keywords": [ 34 | "sql", 35 | "sqlite", 36 | "sqlite3", 37 | "sqlcipher", 38 | "transactions", 39 | "user-defined functions", 40 | "aggregate functions", 41 | "window functions", 42 | "database" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /test/50.misc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('miscellaneous', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | }); 8 | afterEach(function () { 9 | this.db.close(); 10 | }); 11 | 12 | it('persists non-trivial quantities of reads and writes', function () { 13 | const runDuration = 1000; 14 | const runUntil = Date.now() + runDuration; 15 | this.slow(runDuration * 10); 16 | this.timeout(runDuration * 3); 17 | this.db.pragma("journal_mode = WAL"); 18 | this.db.prepare("CREATE TABLE foo (a INTEGER, b TEXT, c REAL)").run(); 19 | 20 | let i = 1; 21 | const r = 0.141592654; 22 | const insert = this.db.prepare("INSERT INTO foo VALUES (?, ?, ?)"); 23 | const insertMany = this.db.transaction((count) => { 24 | for (const end = i + count; i < end; ++i) { 25 | expect(insert.run(i, String(i), i + r)) 26 | .to.deep.equal({ changes: 1, lastInsertRowid: i }); 27 | } 28 | }); 29 | 30 | // Batched transactions of 100 inserts. 31 | while (Date.now() < runUntil) insertMany(100); 32 | 33 | // Expect 10K~50K on reasonable machines. 34 | expect(i).to.be.above(1000); 35 | 36 | const select = this.db.prepare("SELECT * FROM foo ORDER BY a DESC"); 37 | for (const row of select.iterate()) { 38 | i -= 1; 39 | expect(row).to.deep.equal({ a: i, b: String(i), c: i + r }); 40 | } 41 | 42 | expect(i).to.equal(1); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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': { 8 | 'sqlcipher_version%': '4.6.1', # Maps to SQLite 3.46.1 9 | 'bearssl_version%': '0.6', 10 | }, 11 | 'target_defaults': { 12 | 'default_configuration': 'Release', 13 | 'msvs_settings': { 14 | 'VCCLCompilerTool': { 15 | 'ExceptionHandling': 1, 16 | }, 17 | }, 18 | 'conditions': [ 19 | ['OS == "win"', { 20 | 'defines': ['WIN32'], 21 | }], 22 | ], 23 | 'configurations': { 24 | 'Debug': { 25 | 'defines!': [ 26 | 'NDEBUG', 27 | ], 28 | 'defines': [ 29 | 'DEBUG', 30 | '_DEBUG', 31 | 'SQLITE_DEBUG', 32 | 'SQLITE_MEMDEBUG', 33 | 'SQLITE_ENABLE_API_ARMOR', 34 | 'SQLITE_WIN32_MALLOC_VALIDATE', 35 | ], 36 | 'cflags': [ 37 | '-O0', 38 | ], 39 | 'xcode_settings': { 40 | 'MACOSX_DEPLOYMENT_TARGET': '10.7', 41 | 'GCC_OPTIMIZATION_LEVEL': '0', 42 | 'GCC_GENERATE_DEBUGGING_SYMBOLS': 'YES', 43 | }, 44 | 'msvs_settings': { 45 | 'VCLinkerTool': { 46 | 'GenerateDebugInformation': 'true', 47 | }, 48 | }, 49 | }, 50 | 'Release': { 51 | 'defines!': [ 52 | 'DEBUG', 53 | '_DEBUG', 54 | ], 55 | 'defines': [ 56 | 'NDEBUG', 57 | ], 58 | 'cflags': [ 59 | '-O3', 60 | ], 61 | 'xcode_settings': { 62 | 'MACOSX_DEPLOYMENT_TARGET': '10.7', 63 | 'GCC_OPTIMIZATION_LEVEL': '3', 64 | 'GCC_GENERATE_DEBUGGING_SYMBOLS': 'NO', 65 | 'DEAD_CODE_STRIPPING': 'YES', 66 | 'GCC_INLINES_ARE_PRIVATE_EXTERN': 'YES', 67 | }, 68 | }, 69 | }, 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/update-sqlite.yml: -------------------------------------------------------------------------------- 1 | name: update-sqlite 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | year: 7 | description: SQLite release year 8 | required: true 9 | version: 10 | description: SQLite version (encoded) 11 | required: true 12 | 13 | jobs: 14 | download-and-update: 15 | name: Download and update SQLite 16 | runs-on: ubuntu-latest 17 | env: 18 | ENV_YEAR: ${{ github.event.inputs.year }} 19 | ENV_VERSION: ${{ github.event.inputs.version }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | token: ${{ secrets.PAT }} 24 | fetch-depth: 0 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 18 28 | - name: Create new update branch 29 | run: git checkout -b sqlite-update-${{ env.ENV_VERSION }} 30 | - name: Update download script 31 | run: | 32 | sed -Ei "s/YEAR=\"[0-9]+\"/YEAR=\"${{ env.ENV_YEAR }}\"/g" ./deps/download.sh 33 | sed -Ei "s/VERSION=\"[0-9]+\"/VERSION=\"${{ env.ENV_VERSION }}\"/g" ./deps/download.sh 34 | echo "ENV_TRUE_VERSION=$((10#${ENV_VERSION:0:1})).$((10#${ENV_VERSION:1:2})).$((10#${ENV_VERSION:3:2}))" >> $GITHUB_ENV 35 | - name: Download, compile and package SQLite 36 | run: npm run download 37 | - name: Push update branch 38 | uses: stefanzweifel/git-auto-commit-action@v5 39 | with: 40 | commit_message: Update SQLite to version ${{ env.ENV_TRUE_VERSION }} 41 | branch: sqlite-update-${{ env.ENV_VERSION }} 42 | - name: Create new PR 43 | uses: repo-sync/pull-request@v2 44 | with: 45 | github_token: ${{ secrets.PAT }} 46 | source_branch: sqlite-update-${{ env.ENV_VERSION }} 47 | pr_title: Update SQLite to version ${{ env.ENV_TRUE_VERSION }} 48 | pr_body: This is an automated pull request, updating SQLite to version `${{ env.ENV_TRUE_VERSION }}`. 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting installation 2 | 3 | If `better-sqlite3` refuses to install, follow these guidelines: 4 | 5 | ## Use the latest version of better-sqlite3 6 | 7 | - Check the [releases page](https://github.com/WiseLibs/better-sqlite3/releases) to make sure you're using the latest and greatest. 8 | 9 | ## Install a recent Node.js 10 | 11 | - Make sure you're using a [supported version of Node.js](https://nodejs.org/en/about/previous-releases). `better-sqlite3` is only tested with currently-supported versions of Node.js. 12 | 13 | ## "Install the necessary tools" 14 | 15 | - If you're on Windows, during installation of Node.js, be sure to select "Automatically install the necessary tools" from the "Tools for Native Modules" page. 16 | 17 | - If you missed this when you installed Node.js, double-click `C:\Program Files\nodejs\install_tools.bat` from the File Explorer or run it in a terminal. 18 | 19 | This will open an administrative PowerShell terminal and installing Chocolatey, Visual Studio, and Python. 20 | 21 | This may take several minutes. 22 | 23 | ## No special characters in your project path 24 | 25 | - Make sure there are no spaces in your project path: `node-gyp` may not escape spaces or special characters (like `%` or `$`) properly. 26 | 27 | ## Electron 28 | 29 | 1. If you're using [Electron](https://github.com/electron/electron), use [`electron-rebuild`](https://www.npmjs.com/package/electron-rebuild). 30 | 31 | 2. If you're using an app.asar bundle, be sure all native libraries are "unpacked". If you're using [electron-forge]([url](https://www.electronforge.io)), you should use the [auto-unpack-natives plugin](https://www.electronforge.io/config/plugins/auto-unpack-natives) 32 | 33 | ## Windows 34 | 35 | If you still have issues, try these steps: 36 | 37 | 1. Delete your `node_modules` subdirectory 38 | 1. Delete your `$HOME/.node-gyp` directory 39 | 1. Run `npm install` 40 | 41 | ## Still stuck? 42 | 43 | Browse [previous installation issues](https://github.com/WiseLibs/better-sqlite3/issues?q=is%3Aissue). 44 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/tips.md: -------------------------------------------------------------------------------- 1 | # Helpful tips for SQLite3 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 SQLite3'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 SQLite3 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /docs/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Concurrently reading and writing from an SQLite3 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 SQLite3 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 SQLite3 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /deps/download.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | BEARSSL_VERSION=$(grep bearssl_version common.gypi | sed -E "s/.*: '([^']+)'.*/\1/") 5 | SQLCIPHER_VERSION=$(grep sqlcipher_version common.gypi | sed -E "s/.*: '([^']+)'.*/\1/") 6 | 7 | BEARSSL_URL="https://www.bearssl.org/bearssl-${BEARSSL_VERSION}.tar.gz" 8 | SQLCIPHER_REPO="git@git.threema.ch:clients/web/sqlcipher.git" 9 | 10 | DEFINES=" 11 | HAVE_INT16_T=1 12 | HAVE_INT32_T=1 13 | HAVE_INT8_T=1 14 | HAVE_STDINT_H=1 15 | HAVE_UINT16_T=1 16 | HAVE_UINT32_T=1 17 | HAVE_UINT8_T=1 18 | HAVE_USLEEP=1 19 | SQLCIPHER_CRYPTO_BEARSSL 20 | SQLITE_DEFAULT_CACHE_SIZE=-16000 21 | SQLITE_DEFAULT_FOREIGN_KEYS=1 22 | SQLITE_DEFAULT_MEMSTATUS=0 23 | SQLITE_DEFAULT_WAL_SYNCHRONOUS=1 24 | SQLITE_DQS=0 25 | SQLITE_ENABLE_COLUMN_METADATA 26 | SQLITE_ENABLE_DESERIALIZE 27 | SQLITE_ENABLE_FTS5 28 | SQLITE_ENABLE_JSON1 29 | SQLITE_ENABLE_MATH_FUNCTIONS 30 | SQLITE_ENABLE_STAT4 31 | SQLITE_ENABLE_UPDATE_DELETE_LIMIT 32 | SQLITE_HAS_CODEC 33 | SQLITE_LIKE_DOESNT_MATCH_BLOBS 34 | SQLITE_OMIT_DEPRECATED 35 | SQLITE_OMIT_GET_TABLE 36 | SQLITE_OMIT_PROGRESS_CALLBACK 37 | SQLITE_OMIT_SHARED_CACHE 38 | SQLITE_OMIT_TCL_VARIABLE 39 | SQLITE_SECURE_DELETE 40 | SQLITE_TEMP_STORE=3 41 | SQLITE_THREADSAFE=2 42 | SQLITE_TRACE_SIZE_LIMIT=32 43 | SQLITE_USE_URI=0 44 | " 45 | 46 | GREEN="\033[0;32m" 47 | RESET="\033[0m" 48 | function log() { 49 | echo -en "$GREEN" 50 | echo -n "$1" 51 | echo -e "$RESET" 52 | } 53 | 54 | log "Setting up environment" 55 | DEPS="$PWD" 56 | TEMP="$DEPS/temp" 57 | rm -rf "$TEMP" 58 | mkdir -p "$TEMP" 59 | CFLAGS=$(echo "$DEFINES" | sed -e "/^\s*$/d" -e "s/^/-D/") 60 | export CFLAGS 61 | 62 | log "Downloading BearSSL" 63 | curl "$BEARSSL_URL" > bearssl.tar.gz 64 | 65 | log "Cloning SQLCipher..." 66 | pushd "$TEMP" 67 | git clone $SQLCIPHER_REPO -b "bearssl-crypto-provider-${SQLCIPHER_VERSION}" 68 | 69 | log "Configuring amalgamation..." 70 | mkdir -p sqlcipher/build "sqlcipher/build/sqlcipher-amalgamation-${SQLCIPHER_VERSION}" 71 | pushd ./sqlcipher/build 72 | ../configure --with-crypto-lib=bearssl --enable-tempstore="yes" 73 | 74 | log "Building amalgamation..." 75 | make sqlite3.c 76 | cp sqlite3.c sqlite3.h sqlite3ext.h "sqlcipher-amalgamation-${SQLCIPHER_VERSION}/" 77 | cp ../VERSION "sqlcipher-amalgamation-${SQLCIPHER_VERSION}/VERSION.txt" 78 | tar -czvf sqlcipher.tar.gz "sqlcipher-amalgamation-${SQLCIPHER_VERSION}" 79 | popd 80 | popd 81 | mv "$TEMP/sqlcipher/build/sqlcipher.tar.gz" . 82 | 83 | log "Writing GYP defines..." 84 | GYP="$DEPS/defines.gypi" 85 | printf "# THIS FILE IS AUTOMATICALLY GENERATED BY deps/download.sh (DO NOT EDIT)\n\n{\n 'defines': [\n" > "$GYP" 86 | printf "$DEFINES" | sed -e "/^\s*$/d" -e "s/\(.*\)/ '\1',/" >> "$GYP" 87 | printf " ],\n}\n" >> "$GYP" 88 | 89 | log "Cleaning up..." 90 | rm -rf "$TEMP" 91 | 92 | log "Done!" 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | after.run(); 67 | return result; 68 | } catch (ex) { 69 | if (db.inTransaction) { 70 | undo.run(); 71 | if (undo !== rollback) after.run(); 72 | } 73 | throw ex; 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /docs/threads.md: -------------------------------------------------------------------------------- 1 | # Worker threads 2 | 3 | For most applications, `better-sqlite3` is fast enough to use in the main thread without blocking for a noticeable amount of time. However, if you need to perform very slow queries, you have the option of using [worker threads](https://nodejs.org/api/worker_threads.html) to keep things running smoothly. Below is an example of using a thread pool to perform queries in the background. 4 | 5 | ### worker.js 6 | 7 | The worker logic is very simple in our case. It accepts messages from the master thread, executes each message's SQL (with any given parameters), and sends back the query results. 8 | 9 | ```js 10 | const { parentPort } = require('worker_threads'); 11 | const db = require('better-sqlite3')('foobar.db'); 12 | 13 | parentPort.on('message', ({ sql, parameters }) => { 14 | const result = db.prepare(sql).all(...parameters); 15 | parentPort.postMessage(result); 16 | }); 17 | ``` 18 | 19 | ### master.js 20 | 21 | The master thread is responsible for spawning workers, respawning threads that crash, and accepting query jobs. 22 | 23 | ```js 24 | const { Worker } = require('worker_threads'); 25 | const os = require('os'); 26 | 27 | /* 28 | Export a function that queues pending work. 29 | */ 30 | 31 | const queue = []; 32 | exports.asyncQuery = (sql, ...parameters) => { 33 | return new Promise((resolve, reject) => { 34 | queue.push({ 35 | resolve, 36 | reject, 37 | message: { sql, parameters }, 38 | }); 39 | drainQueue(); 40 | }); 41 | }; 42 | 43 | /* 44 | Instruct workers to drain the queue. 45 | */ 46 | 47 | let workers = []; 48 | function drainQueue() { 49 | for (const worker of workers) { 50 | worker.takeWork(); 51 | } 52 | } 53 | 54 | /* 55 | Spawn workers that try to drain the queue. 56 | */ 57 | 58 | os.cpus().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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/integer.md: -------------------------------------------------------------------------------- 1 | # The `BigInt` primitive type 2 | 3 | SQLite3 can store data in 64-bit signed integers, which are too big for JavaScript's [number format](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) to fully represent. To support this data type, `better-sqlite3` is fully compatible with [BigInts](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt). 4 | 5 | ```js 6 | const big = BigInt('1152735103331642317'); 7 | big === 1152735103331642317n; // returns true 8 | big.toString(); // returns "1152735103331642317" 9 | typeof big; // returns "bigint" 10 | ``` 11 | 12 | ## Binding BigInts 13 | 14 | `BigInts` can bind to [`Statements`](./api.md#class-statement) just like regular numbers. You can also return `BigInts` from [user-defined functions](./api.md#functionname-options-function---this). However, if you provide a `BigInt` that's too large to be a 64-bit signed integer, you'll get an error so that data integrity is protected. 15 | 16 | ```js 17 | db.prepare("SELECT * FROM users WHERE id=?").get(BigInt('1152735103331642317')); 18 | db.prepare("INSERT INTO users (id) VALUES (?)").run(BigInt('1152735103331642317')); 19 | 20 | db.prepare("SELECT ?").get(2n ** 63n - 1n); // returns successfully 21 | db.prepare("SELECT ?").get(2n ** 63n); // throws a RangeError 22 | ``` 23 | 24 | ## Getting BigInts from the database 25 | 26 | By default, integers returned from the database (including the [`info.lastInsertRowid`](./api.md#runbindparameters---object) property) are normal JavaScript numbers. You can change this default as you please: 27 | 28 | ```js 29 | db.defaultSafeIntegers(); // BigInts by default 30 | db.defaultSafeIntegers(true); // BigInts by default 31 | db.defaultSafeIntegers(false); // Numbers by default 32 | ``` 33 | 34 | Additionally, you can override the default for individual [`Statements`](./api.md#class-statement) like so: 35 | 36 | ```js 37 | const stmt = db.prepare(SQL); 38 | 39 | stmt.safeIntegers(); // Safe integers ON 40 | stmt.safeIntegers(true); // Safe integers ON 41 | stmt.safeIntegers(false); // Safe integers OFF 42 | ``` 43 | 44 | [User-defined functions](./api.md#functionname-options-function---this) can receive `BigInts` as arguments. You can override the database's default setting like so: 45 | 46 | ```js 47 | db.function('isInt', { safeIntegers: true }, (value) => { 48 | return String(typeof value === 'bigint'); 49 | }); 50 | 51 | db.prepare('SELECT isInt(?)').pluck().get(10); // => "false" 52 | db.prepare('SELECT isInt(?)').pluck().get(10n); // => "true" 53 | ``` 54 | 55 | Likewise, [user-defined aggregates](./api.md#aggregatename-options---this) and [virtual tables](./api.md#tablename-definition---this) can also receive `BigInts` as arguments: 56 | 57 | ```js 58 | db.aggregate('addInts', { 59 | safeIntegers: true, 60 | start: 0n, 61 | step: (total, nextValue) => total + nextValue, 62 | }); 63 | ``` 64 | 65 | ```js 66 | db.table('sequence', { 67 | safeIntegers: true, 68 | columns: ['value'], 69 | parameters: ['length', 'start'], 70 | rows: function* (length, start = 0n) { 71 | const end = start + length; 72 | for (let n = start; n < end; ++n) { 73 | yield { value: n }; 74 | } 75 | }, 76 | }); 77 | ``` 78 | 79 | It's worth noting that REAL (FLOAT) values returned from the database will always be represented as normal numbers. 80 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/compilation.md: -------------------------------------------------------------------------------- 1 | # Custom configuration 2 | 3 | If you want to use a customized version of [SQLite3](https://www.sqlite.org) with `better-sqlite3`, you can do so by specifying the directory of your [custom amalgamation](https://www.sqlite.org/amalgamation.html) during installation. 4 | 5 | ```bash 6 | npm install better-sqlite3 --build-from-source --sqlite3=/path/to/sqlite-amalgamation 7 | ``` 8 | 9 | However, if you simply run `npm install` while `better-sqlite3` is listed as a dependency in your `package.json`, the required flags above will *not* be applied. Therefore, it's recommended that you remove `better-sqlite3` from your dependency list, and instead add a [`preinstall` script](https://docs.npmjs.com/misc/scripts) like the one shown below. 10 | 11 | ```json 12 | { 13 | "scripts": { 14 | "preinstall": "npm install better-sqlite3@'^7.0.0' --no-save --build-from-source --sqlite3=\"$(pwd)/sqlite-amalgamation\"" 15 | } 16 | } 17 | ``` 18 | 19 | Your amalgamation directory must contain `sqlite3.c` and `sqlite3.h`. Any desired [compile time options](https://www.sqlite.org/compile.html) must be defined directly within `sqlite3.c`, as shown below. 20 | 21 | ```c 22 | // These go at the top of the file 23 | #define SQLITE_ENABLE_FTS5 1 24 | #define SQLITE_DEFAULT_CACHE_SIZE 16000 25 | 26 | // ... the original content of the file remains below 27 | ``` 28 | 29 | ### Step by step example 30 | 31 | If you're creating a package that relies on a custom build of `better-sqlite3`, you can follow these steps to get started. 32 | 33 | 1. Download the SQLite3 source code from [their website](https://sqlite.com/download.html) (e.g., `sqlite-amalgamation-1234567.zip`) 34 | 2. Unzip the compressed archive 35 | 3. Move the `sqlite3.c` and `sqlite3.h` files to your project folder 36 | 4. Add a `preinstall` script to your `package.json`, like the one shown above 37 | 6. Make sure the `--sqlite3` flag points to the location of your `sqlite3.c` and `sqlite3.h` files 38 | 7. Define your preferred [compile time options](https://www.sqlite.org/compile.html) at the top of `sqlite3.c` 39 | 8. Make sure to remove `better-sqlite3` from your `dependencies` 40 | 9. Run `npm install` in your project folder 41 | 42 | If you're using a SQLite3 encryption extension that is a drop-in replacement for SQLite3 (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 SQLite3 **version 3.46.1** with the following [compilation options](https://www.sqlite.org/compile.html): 47 | 48 | ``` 49 | HAVE_INT16_T=1 50 | HAVE_INT32_T=1 51 | HAVE_INT8_T=1 52 | HAVE_STDINT_H=1 53 | HAVE_UINT16_T=1 54 | HAVE_UINT32_T=1 55 | HAVE_UINT8_T=1 56 | HAVE_USLEEP=1 57 | SQLITE_DEFAULT_CACHE_SIZE=-16000 58 | SQLITE_DEFAULT_FOREIGN_KEYS=1 59 | SQLITE_DEFAULT_MEMSTATUS=0 60 | SQLITE_DEFAULT_WAL_SYNCHRONOUS=1 61 | SQLITE_DQS=0 62 | SQLITE_ENABLE_COLUMN_METADATA 63 | SQLITE_ENABLE_DBSTAT_VTAB 64 | SQLITE_ENABLE_DESERIALIZE 65 | SQLITE_ENABLE_FTS3 66 | SQLITE_ENABLE_FTS3_PARENTHESIS 67 | SQLITE_ENABLE_FTS4 68 | SQLITE_ENABLE_FTS5 69 | SQLITE_ENABLE_GEOPOLY 70 | SQLITE_ENABLE_JSON1 71 | SQLITE_ENABLE_MATH_FUNCTIONS 72 | SQLITE_ENABLE_RTREE 73 | SQLITE_ENABLE_STAT4 74 | SQLITE_ENABLE_UPDATE_DELETE_LIMIT 75 | SQLITE_LIKE_DOESNT_MATCH_BLOBS 76 | SQLITE_OMIT_DEPRECATED 77 | SQLITE_OMIT_PROGRESS_CALLBACK 78 | SQLITE_OMIT_SHARED_CACHE 79 | SQLITE_OMIT_TCL_VARIABLE 80 | SQLITE_SOUNDEX 81 | SQLITE_THREADSAFE=2 82 | SQLITE_TRACE_SIZE_LIMIT=32 83 | SQLITE_USE_URI=0 84 | ``` 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/37.database.serialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Database = require('../.'); 3 | 4 | describe('Database#serialize()', function () { 5 | beforeEach(function () { 6 | this.db = new Database(util.next()); 7 | this.db.prepare("CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)").run(); 8 | this.seed = () => { 9 | this.db.prepare("INSERT INTO entries WITH RECURSIVE temp(a, b, c, d, e) AS (SELECT 'foo', 1, 3.14, x'dddddddd', NULL UNION ALL SELECT a, b + 1, c, d, e FROM temp LIMIT 1000) SELECT * FROM temp").run(); 10 | }; 11 | }); 12 | afterEach(function () { 13 | this.db.close(); 14 | }); 15 | 16 | it('should serialize the database and return a buffer', async function () { 17 | let buffer = this.db.serialize(); 18 | expect(buffer).to.be.an.instanceof(Buffer); 19 | expect(buffer.length).to.be.above(1000); 20 | const lengthBefore = buffer.length; 21 | this.seed(); 22 | buffer = this.db.serialize(); 23 | expect(buffer).to.be.an.instanceof(Buffer); 24 | expect(buffer.length).to.be.above(lengthBefore); 25 | }); 26 | it('should return a buffer that can be used by the Database constructor', async function () { 27 | this.seed(); 28 | const buffer = this.db.serialize(); 29 | expect(buffer).to.be.an.instanceof(Buffer); 30 | expect(buffer.length).to.be.above(1000); 31 | this.db.prepare('delete from entries').run(); 32 | this.db.close(); 33 | this.db = new Database(buffer); 34 | const bufferCopy = this.db.serialize(); 35 | expect(buffer.length).to.equal(bufferCopy.length); 36 | expect(buffer).to.deep.equal(bufferCopy); 37 | this.db.prepare('insert into entries (rowid, a, b) values (?, ?, ?)').run(0, 'bar', -999); 38 | expect(this.db.prepare('select a, b from entries order by rowid limit 2').all()) 39 | .to.deep.equal([{ a: 'bar', b: -999 }, { a: 'foo', b: 1 }]); 40 | }); 41 | it('should accept the "attached" option', async function () { 42 | const smallBuffer = this.db.serialize(); 43 | this.seed(); 44 | const bigBuffer = this.db.serialize(); 45 | this.db.close(); 46 | this.db = new Database(); 47 | this.db.prepare('attach ? as other').run(util.current()); 48 | const smallBuffer2 = this.db.serialize(); 49 | const bigBuffer2 = this.db.serialize({ attached: 'other' }); 50 | expect(bigBuffer.length === bigBuffer2.length); 51 | expect(bigBuffer).to.deep.equal(bigBuffer2); 52 | expect(smallBuffer.length < bigBuffer.length); 53 | expect(smallBuffer2.length < bigBuffer.length); 54 | expect(smallBuffer).to.not.deep.equal(smallBuffer2); 55 | }); 56 | it('should return a buffer that can be opened with the "readonly" option', async function () { 57 | this.seed(); 58 | const buffer = this.db.serialize(); 59 | expect(buffer).to.be.an.instanceof(Buffer); 60 | expect(buffer.length).to.be.above(1000); 61 | this.db.close(); 62 | this.db = new Database(buffer, { readonly: true }); 63 | expect(() => this.db.prepare('insert into entries (rowid, a, b) values (?, ?, ?)').run(0, 'bar', -999)) 64 | .to.throw(Database.SqliteError); 65 | expect(this.db.prepare('select a, b from entries order by rowid limit 2').all()) 66 | .to.deep.equal([{ a: 'foo', b: 1 }, { a: 'foo', b: 2 }]); 67 | const bufferCopy = this.db.serialize(); 68 | expect(buffer.length).to.equal(bufferCopy.length); 69 | expect(buffer).to.deep.equal(bufferCopy); 70 | }); 71 | it('should work with an empty database', async function () { 72 | this.db.close(); 73 | this.db = new Database(); 74 | const buffer = this.db.serialize(); 75 | expect(buffer).to.be.an.instanceof(Buffer); 76 | expect(buffer.length).to.be.lte(4096); 77 | this.db.close(); 78 | this.db = new Database(buffer); 79 | expect(this.db.prepare("select * from sqlite_master").all()).to.deep.equal([]); 80 | expect(this.db.serialize().length).to.be.lte(4096); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | function Database(filenameGiven, options) { 8 | if (new.target == null) { 9 | return new Database(filenameGiven, options); 10 | } 11 | 12 | // Apply defaults 13 | let buffer; 14 | if (Buffer.isBuffer(filenameGiven)) { 15 | buffer = filenameGiven; 16 | filenameGiven = ':memory:'; 17 | } 18 | if (filenameGiven == null) filenameGiven = ''; 19 | if (options == null) options = {}; 20 | 21 | // Validate arguments 22 | if (typeof filenameGiven !== 'string') throw new TypeError('Expected first argument to be a string'); 23 | if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object'); 24 | if ('readOnly' in options) throw new TypeError('Misspelled option "readOnly" should be "readonly"'); 25 | if ('memory' in options) throw new TypeError('Option "memory" was removed in v7.0.0 (use ":memory:" filename instead)'); 26 | 27 | // Interpret options 28 | const filename = filenameGiven.trim(); 29 | const anonymous = filename === '' || filename === ':memory:'; 30 | const readonly = util.getBooleanOption(options, 'readonly'); 31 | const fileMustExist = util.getBooleanOption(options, 'fileMustExist'); 32 | const timeout = 'timeout' in options ? options.timeout : 5000; 33 | const verbose = 'verbose' in options ? options.verbose : null; 34 | const nativeBinding = 'nativeBinding' in options ? options.nativeBinding : null; 35 | 36 | // Validate interpreted options 37 | if (readonly && anonymous && !buffer) throw new TypeError('In-memory/temporary databases cannot be readonly'); 38 | if (!Number.isInteger(timeout) || timeout < 0) throw new TypeError('Expected the "timeout" option to be a positive integer'); 39 | if (timeout > 0x7fffffff) throw new RangeError('Option "timeout" cannot be greater than 2147483647'); 40 | if (verbose != null && typeof verbose !== 'function') throw new TypeError('Expected the "verbose" option to be a function'); 41 | if (nativeBinding != null && typeof nativeBinding !== 'string' && typeof nativeBinding !== 'object') throw new TypeError('Expected the "nativeBinding" option to be a string or addon object'); 42 | 43 | // Load the native addon 44 | let addon; 45 | if (nativeBinding == null) { 46 | addon = require(path.join(__dirname, '..', 'build', 'Release', 'better_sqlcipher.node')); 47 | } else if (typeof nativeBinding === 'string') { 48 | // See 49 | const requireFunc = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : require; 50 | addon = requireFunc(path.resolve(nativeBinding).replace(/(\.node)?$/, '.node')); 51 | } else { 52 | // See 53 | addon = nativeBinding; 54 | } 55 | 56 | if (!addon.isInitialized) { 57 | addon.setErrorConstructor(SqliteError); 58 | addon.isInitialized = true; 59 | } 60 | 61 | // Make sure the specified directory exists 62 | if (!anonymous && !fs.existsSync(path.dirname(filename))) { 63 | throw new TypeError('Cannot open database because the directory does not exist'); 64 | } 65 | 66 | Object.defineProperties(this, { 67 | [util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null, buffer || null) }, 68 | ...wrappers.getters, 69 | }); 70 | } 71 | 72 | const wrappers = require('./methods/wrappers'); 73 | Database.prototype.prepare = wrappers.prepare; 74 | Database.prototype.transaction = require('./methods/transaction'); 75 | Database.prototype.pragma = require('./methods/pragma'); 76 | Database.prototype.backup = require('./methods/backup'); 77 | Database.prototype.serialize = require('./methods/serialize'); 78 | Database.prototype.function = require('./methods/function'); 79 | Database.prototype.aggregate = require('./methods/aggregate'); 80 | Database.prototype.table = require('./methods/table'); 81 | Database.prototype.loadExtension = wrappers.loadExtension; 82 | Database.prototype.exec = wrappers.exec; 83 | Database.prototype.close = wrappers.close; 84 | Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers; 85 | Database.prototype.unsafeMode = wrappers.unsafeMode; 86 | Database.prototype[util.inspect] = require('./methods/inspect'); 87 | 88 | module.exports = Database; 89 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/60.sqlcipher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { readFileSync } = require('fs'); 3 | const Database = require('../.'); 4 | const { expect } = require('chai'); 5 | 6 | describe('sqlcipher', function() { 7 | const MAGIC = 'SQLite format 3'; 8 | 9 | class Builder { 10 | constructor(db) { 11 | this.db = db; 12 | } 13 | 14 | withDerivedKey(key = 'meow') { 15 | this.db.pragma(`key = '${key}'`); 16 | return this; 17 | } 18 | 19 | withRawKey(key = '06fde20554abf0e09997a5df9bf6b44d343ed5bb6fea54b2788ef577e0f27434') { 20 | this.db.pragma(`key = "x'${key}'"`); 21 | return this; 22 | } 23 | 24 | withDummyData() { 25 | this.db.prepare('CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)').run(); 26 | 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(); 27 | return this; 28 | } 29 | } 30 | 31 | beforeEach(function () { 32 | this.builder = new Builder(new Database(util.next())); 33 | }); 34 | afterEach(function () { 35 | this.builder.db.close(); 36 | }); 37 | 38 | it('should use bearssl (derived key)', function() { 39 | const db = this.builder 40 | .withDerivedKey() 41 | .db; 42 | expect(db.pragma('cipher_provider', { simple: true })).to.equal('bearssl'); 43 | }); 44 | 45 | it('should use bearssl (raw key)', function() { 46 | const db = this.builder 47 | .withRawKey() 48 | .db; 49 | expect(db.pragma('cipher_provider', { simple: true })).to.equal('bearssl'); 50 | }); 51 | 52 | it('should be plaintext without a key', function() { 53 | this.builder 54 | .withDummyData() 55 | .db 56 | .close(); 57 | expect(readFileSync(util.current(), 'utf8')).to.satisfy(data => data.startsWith(MAGIC)); 58 | }); 59 | 60 | it('should not be plaintext with a raw key', function() { 61 | this.builder 62 | .withRawKey() 63 | .withDummyData() 64 | .db 65 | .close(); 66 | expect(readFileSync(util.current(), 'utf8')).to.not.satisfy(data => data.startsWith(MAGIC)); 67 | }); 68 | 69 | it('should not be plaintext with a derived key', function() { 70 | this.builder 71 | .withDerivedKey() 72 | .withDummyData() 73 | .db 74 | .close(); 75 | expect(readFileSync(util.current(), 'utf8')).to.not.satisfy(data => data.startsWith(MAGIC)); 76 | }); 77 | 78 | it('should fail to reopen without a key', function() { 79 | let db = this.builder 80 | .withRawKey() 81 | .withDummyData() 82 | .db; 83 | db.close(); 84 | this.builder = new Builder(new Database(util.current())); 85 | expect(() => { 86 | db = this.builder 87 | .withDummyData() 88 | .db; 89 | }).to.throw(Database.SqliteError).with.property('code', 'SQLITE_NOTADB');; 90 | }); 91 | 92 | it('should fail to reopen with wrong key', function() { 93 | let db = this.builder 94 | .withRawKey() 95 | .withDummyData() 96 | .db; 97 | db.close(); 98 | this.builder = new Builder(new Database(util.current())); 99 | expect(() => { 100 | db = this.builder 101 | .withRawKey('0000000011111111222222223333333300000000111111112222222233333333') 102 | .withDummyData() 103 | .db; 104 | }).to.throw(Database.SqliteError).with.property('code', 'SQLITE_NOTADB');; 105 | }); 106 | 107 | it('should succeed to reopen with same raw key', function() { 108 | let db = this.builder 109 | .withRawKey() 110 | .withDummyData() 111 | .db; 112 | const rows = db.prepare('SELECT * FROM entries').all(); 113 | db.close(); 114 | this.builder = new Builder(new Database(util.current())); 115 | db = this.builder 116 | .withRawKey() 117 | .db; 118 | expect(db.prepare('SELECT * FROM entries').all()).to.deep.equal(rows); 119 | }); 120 | 121 | it('should succeed to reopen with same derived key', function() { 122 | let db = this.builder 123 | .withDerivedKey() 124 | .withDummyData() 125 | .db; 126 | const rows = db.prepare('SELECT * FROM entries').all(); 127 | db.close(); 128 | this.builder = new Builder(new Database(util.current())); 129 | db = this.builder 130 | .withDerivedKey() 131 | .db; 132 | expect(db.prepare('SELECT * FROM entries').all()).to.deep.equal(rows); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // FIXME: Is this `any` really necessary? 4 | type VariableArgFunction = (...params: any[]) => unknown; 5 | type ArgumentTypes = F extends (...args: infer A) => unknown ? A : never; 6 | type ElementOf = T extends Array ? E : T; 7 | 8 | declare namespace BetterSqlite3 { 9 | interface Statement { 10 | database: Database; 11 | source: string; 12 | reader: boolean; 13 | readonly: boolean; 14 | busy: boolean; 15 | 16 | run(...params: BindParameters): Database.RunResult; 17 | get(...params: BindParameters): Result | undefined; 18 | all(...params: BindParameters): Result[]; 19 | iterate(...params: BindParameters): IterableIterator; 20 | pluck(toggleState?: boolean): this; 21 | expand(toggleState?: boolean): this; 22 | raw(toggleState?: boolean): this; 23 | bind(...params: BindParameters): this; 24 | columns(): ColumnDefinition[]; 25 | safeIntegers(toggleState?: boolean): this; 26 | } 27 | 28 | interface ColumnDefinition { 29 | name: string; 30 | column: string | null; 31 | table: string | null; 32 | database: string | null; 33 | type: string | null; 34 | } 35 | 36 | interface Transaction { 37 | (...params: ArgumentTypes): ReturnType; 38 | default(...params: ArgumentTypes): ReturnType; 39 | deferred(...params: ArgumentTypes): ReturnType; 40 | immediate(...params: ArgumentTypes): ReturnType; 41 | exclusive(...params: ArgumentTypes): ReturnType; 42 | } 43 | 44 | interface VirtualTableOptions { 45 | rows: (...params: unknown[]) => Generator; 46 | columns: string[]; 47 | parameters?: string[] | undefined; 48 | safeIntegers?: boolean | undefined; 49 | directOnly?: boolean | undefined; 50 | } 51 | 52 | interface Database { 53 | memory: boolean; 54 | readonly: boolean; 55 | name: string; 56 | open: boolean; 57 | inTransaction: boolean; 58 | 59 | prepare( 60 | source: string, 61 | ): BindParameters extends unknown[] ? Statement : Statement<[BindParameters], Result>; 62 | transaction(fn: F): Transaction; 63 | exec(source: string): this; 64 | pragma(source: string, options?: Database.PragmaOptions): unknown; 65 | function(name: string, cb: (...params: unknown[]) => unknown): this; 66 | function(name: string, options: Database.RegistrationOptions, cb: (...params: unknown[]) => unknown): this; 67 | aggregate( 68 | name: string, 69 | options: Database.RegistrationOptions & { 70 | start?: T | (() => T); 71 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 72 | step: (total: T, next: ElementOf) => T | void; 73 | inverse?: ((total: T, dropped: T) => T) | undefined; 74 | result?: ((total: T) => unknown) | undefined; 75 | }, 76 | ): this; 77 | loadExtension(path: string): this; 78 | close(): this; 79 | defaultSafeIntegers(toggleState?: boolean): this; 80 | backup(destinationFile: string, options?: Database.BackupOptions): Promise; 81 | table(name: string, options: VirtualTableOptions): this; 82 | unsafeMode(unsafe?: boolean): this; 83 | serialize(options?: Database.SerializeOptions): Buffer; 84 | } 85 | 86 | interface DatabaseConstructor { 87 | new(filename?: string | Buffer, options?: Database.Options): Database; 88 | (filename?: string, options?: Database.Options): Database; 89 | prototype: Database; 90 | 91 | SqliteError: typeof SqliteError; 92 | } 93 | } 94 | 95 | declare class SqliteError extends Error { 96 | name: string; 97 | message: string; 98 | code: string; 99 | constructor(message: string, code: string); 100 | } 101 | 102 | declare namespace Database { 103 | interface RunResult { 104 | changes: number; 105 | lastInsertRowid: number | bigint; 106 | } 107 | 108 | interface Options { 109 | readonly?: boolean | undefined; 110 | fileMustExist?: boolean | undefined; 111 | timeout?: number | undefined; 112 | verbose?: ((message?: unknown, ...additionalArgs: unknown[]) => void) | undefined; 113 | nativeBinding?: string | undefined; 114 | } 115 | 116 | interface SerializeOptions { 117 | attached?: string; 118 | } 119 | 120 | interface PragmaOptions { 121 | simple?: boolean | undefined; 122 | } 123 | 124 | interface RegistrationOptions { 125 | varargs?: boolean | undefined; 126 | deterministic?: boolean | undefined; 127 | safeIntegers?: boolean | undefined; 128 | directOnly?: boolean | undefined; 129 | } 130 | 131 | type AggregateOptions = Parameters[1]; 132 | 133 | interface BackupMetadata { 134 | totalPages: number; 135 | remainingPages: number; 136 | } 137 | interface BackupOptions { 138 | progress: (info: BackupMetadata) => number; 139 | } 140 | 141 | type SqliteError = typeof SqliteError; 142 | type Statement = BindParameters extends 143 | unknown[] ? BetterSqlite3.Statement 144 | : BetterSqlite3.Statement<[BindParameters], Result>; 145 | type ColumnDefinition = BetterSqlite3.ColumnDefinition; 146 | type Transaction = BetterSqlite3.Transaction; 147 | type Database = BetterSqlite3.Database; 148 | } 149 | 150 | declare const Database: BetterSqlite3.DatabaseConstructor; 151 | export = Database; 152 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: 12 | - released 13 | workflow_dispatch: {} 14 | 15 | env: 16 | # See https://github.com/nodejs/release#release-schedule 17 | # Node.js v16 EOL = 2023-09-11. v21 EOL = 2024-06-01. 18 | 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$' 19 | # Merge with NODE_BUILD_CMD when Node.js v18 is EOL 20 | NO_V18_NODE_BUILD_CMD: npx --no-install prebuild -r node -t 20.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' 21 | # See https://www.electronjs.org/docs/latest/tutorial/electron-timelines#version-support-policy 22 | # Electron v25 EOL = 2023-12-05. v26 EOL = 2024-02-20. v27 EOL = 2024-04-16. v28 EOL = 2024-06-11. v29 EOL = 2024-08-20. 23 | 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 --include-regex 'better_sqlite3.node$' 24 | 25 | jobs: 26 | test: 27 | strategy: 28 | matrix: 29 | os: 30 | - ubuntu-20.04 31 | - macos-13 32 | - macos-14 33 | - windows-2019 34 | node: 35 | - 18 36 | - 20 37 | - 22 38 | name: Testing Node ${{ matrix.node }} on ${{ matrix.os }} 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/setup-node@v4 43 | with: 44 | node-version: ${{ matrix.node }} 45 | - if: ${{ startsWith(matrix.os, 'windows') }} 46 | run: pip.exe install setuptools 47 | - if: ${{ startsWith(matrix.os, 'macos') }} 48 | run: brew install python-setuptools 49 | - if: ${{ !startsWith(matrix.os, 'windows') && !startsWith(matrix.os, 'macos') }} 50 | run: python3 -m pip install setuptools 51 | - if: ${{ startsWith(matrix.os, 'ubuntu') }} 52 | run: | 53 | sudo apt update 54 | sudo apt install gcc-10 g++-10 -y 55 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 56 | - run: npm install --ignore-scripts 57 | - run: npm run build-debug 58 | - run: npm test 59 | - name: Test SpatiaLite extension 60 | if: ${{ startsWith(matrix.os, 'ubuntu') }} 61 | run: | 62 | sudo apt update 63 | sudo apt install libsqlite3-mod-spatialite -y 64 | node -e "require('./lib/index.js')(':memory:').loadExtension('mod_spatialite').exec('SELECT InitSpatialMetaData();')" 65 | 66 | publish: 67 | if: ${{ github.event_name == 'release' }} 68 | name: Publishing to NPM 69 | runs-on: ubuntu-20.04 70 | needs: 71 | - prebuild 72 | - prebuild-alpine 73 | - prebuild-alpine-arm 74 | - prebuild-linux-arm 75 | steps: 76 | - uses: actions/checkout@v4 77 | - uses: actions/setup-node@v4 78 | with: 79 | node-version: 18 80 | registry-url: https://registry.npmjs.org 81 | - run: npm publish 82 | env: 83 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 84 | 85 | prebuild: 86 | if: ${{ github.event_name == 'release' }} 87 | strategy: 88 | fail-fast: false 89 | matrix: 90 | os: 91 | - ubuntu-20.04 92 | - macos-13 93 | - macos-14 94 | - windows-2019 95 | name: Prebuild on ${{ matrix.os }} 96 | runs-on: ${{ matrix.os }} 97 | needs: test 98 | steps: 99 | - uses: actions/checkout@v4 100 | - uses: actions/setup-node@v4 101 | with: 102 | node-version: 18 103 | - if: ${{ startsWith(matrix.os, 'windows') }} 104 | run: pip.exe install setuptools 105 | - if: ${{ startsWith(matrix.os, 'macos') }} 106 | run: brew install python-setuptools 107 | - if: ${{ !startsWith(matrix.os, 'windows') && !startsWith(matrix.os, 'macos') }} 108 | run: python3 -m pip install setuptools 109 | - if: ${{ startsWith(matrix.os, 'ubuntu') }} 110 | run: | 111 | sudo apt update 112 | sudo apt install gcc-10 g++-10 -y 113 | 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 114 | - run: npm install --ignore-scripts 115 | - run: ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} 116 | - run: ${{ env.ELECTRON_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} 117 | - if: matrix.os == 'windows-2019' 118 | run: | 119 | ${{ env.NODE_BUILD_CMD }} --arch ia32 -u ${{ secrets.GITHUB_TOKEN }} 120 | ${{ env.NO_V18_NODE_BUILD_CMD }} --arch arm64 -u ${{ secrets.GITHUB_TOKEN }} 121 | ${{ env.ELECTRON_BUILD_CMD }} --arch ia32 -u ${{ secrets.GITHUB_TOKEN }} 122 | ${{ env.ELECTRON_BUILD_CMD }} --arch arm64 -u ${{ secrets.GITHUB_TOKEN }} 123 | 124 | prebuild-alpine: 125 | if: ${{ github.event_name == 'release' }} 126 | strategy: 127 | fail-fast: false 128 | name: Prebuild on alpine 129 | runs-on: ubuntu-latest 130 | container: node:18-alpine 131 | needs: test 132 | steps: 133 | - uses: actions/checkout@v4 134 | - run: apk add build-base git python3 py3-setuptools --update-cache 135 | - run: npm install --ignore-scripts 136 | - run: ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }} 137 | 138 | prebuild-alpine-arm: 139 | if: ${{ github.event_name == 'release' }} 140 | strategy: 141 | fail-fast: false 142 | matrix: 143 | arch: 144 | - arm/v7 145 | - arm64 146 | name: Prebuild on alpine (${{ matrix.arch }}) 147 | runs-on: ubuntu-latest 148 | needs: test 149 | steps: 150 | - uses: actions/checkout@v4 151 | - uses: docker/setup-qemu-action@v3 152 | - run: | 153 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:18-alpine -c "\ 154 | apk add build-base git python3 py3-setuptools --update-cache && \ 155 | cd /tmp/project && \ 156 | npm install --ignore-scripts && \ 157 | ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }}" 158 | 159 | prebuild-linux-arm: 160 | if: ${{ github.event_name == 'release' }} 161 | strategy: 162 | fail-fast: false 163 | matrix: 164 | arch: 165 | - arm/v7 166 | - arm64 167 | name: Prebuild on Linux (${{ matrix.arch }}) 168 | runs-on: ubuntu-latest 169 | needs: test 170 | steps: 171 | - uses: actions/checkout@v4 172 | - uses: docker/setup-qemu-action@v3 173 | - run: | 174 | docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:18-bullseye -c "\ 175 | cd /tmp/project && \ 176 | npm install --ignore-scripts && \ 177 | ${{ env.NODE_BUILD_CMD }} -u ${{ secrets.GITHUB_TOKEN }}" 178 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/util/constants.lzz: -------------------------------------------------------------------------------- 1 | class CS { 2 | public: 3 | 4 | v8::Local Code(v8::Isolate* isolate, int code) { 5 | auto element = codes.find(code); 6 | if (element != codes.end()) return element->second.Get(isolate); 7 | return StringFromUtf8(isolate, (std::string("UNKNOWN_SQLITE_ERROR_") + std::to_string(code)).c_str(), -1); 8 | } 9 | 10 | explicit CS(v8::Isolate* isolate) { 11 | SetString(isolate, database, "database"); 12 | SetString(isolate, reader, "reader"); 13 | SetString(isolate, source, "source"); 14 | SetString(isolate, memory, "memory"); 15 | SetString(isolate, readonly, "readonly"); 16 | SetString(isolate, name, "name"); 17 | SetString(isolate, next, "next"); 18 | SetString(isolate, length, "length"); 19 | SetString(isolate, done, "done"); 20 | SetString(isolate, value, "value"); 21 | SetString(isolate, changes, "changes"); 22 | SetString(isolate, lastInsertRowid, "lastInsertRowid"); 23 | SetString(isolate, statement, "statement"); 24 | SetString(isolate, column, "column"); 25 | SetString(isolate, table, "table"); 26 | SetString(isolate, type, "type"); 27 | SetString(isolate, totalPages, "totalPages"); 28 | SetString(isolate, remainingPages, "remainingPages"); 29 | 30 | SetCode(isolate, SQLITE_OK, "SQLITE_OK"); 31 | SetCode(isolate, SQLITE_ERROR, "SQLITE_ERROR"); 32 | SetCode(isolate, SQLITE_INTERNAL, "SQLITE_INTERNAL"); 33 | SetCode(isolate, SQLITE_PERM, "SQLITE_PERM"); 34 | SetCode(isolate, SQLITE_ABORT, "SQLITE_ABORT"); 35 | SetCode(isolate, SQLITE_BUSY, "SQLITE_BUSY"); 36 | SetCode(isolate, SQLITE_LOCKED, "SQLITE_LOCKED"); 37 | SetCode(isolate, SQLITE_NOMEM, "SQLITE_NOMEM"); 38 | SetCode(isolate, SQLITE_READONLY, "SQLITE_READONLY"); 39 | SetCode(isolate, SQLITE_INTERRUPT, "SQLITE_INTERRUPT"); 40 | SetCode(isolate, SQLITE_IOERR, "SQLITE_IOERR"); 41 | SetCode(isolate, SQLITE_CORRUPT, "SQLITE_CORRUPT"); 42 | SetCode(isolate, SQLITE_NOTFOUND, "SQLITE_NOTFOUND"); 43 | SetCode(isolate, SQLITE_FULL, "SQLITE_FULL"); 44 | SetCode(isolate, SQLITE_CANTOPEN, "SQLITE_CANTOPEN"); 45 | SetCode(isolate, SQLITE_PROTOCOL, "SQLITE_PROTOCOL"); 46 | SetCode(isolate, SQLITE_EMPTY, "SQLITE_EMPTY"); 47 | SetCode(isolate, SQLITE_SCHEMA, "SQLITE_SCHEMA"); 48 | SetCode(isolate, SQLITE_TOOBIG, "SQLITE_TOOBIG"); 49 | SetCode(isolate, SQLITE_CONSTRAINT, "SQLITE_CONSTRAINT"); 50 | SetCode(isolate, SQLITE_MISMATCH, "SQLITE_MISMATCH"); 51 | SetCode(isolate, SQLITE_MISUSE, "SQLITE_MISUSE"); 52 | SetCode(isolate, SQLITE_NOLFS, "SQLITE_NOLFS"); 53 | SetCode(isolate, SQLITE_AUTH, "SQLITE_AUTH"); 54 | SetCode(isolate, SQLITE_FORMAT, "SQLITE_FORMAT"); 55 | SetCode(isolate, SQLITE_RANGE, "SQLITE_RANGE"); 56 | SetCode(isolate, SQLITE_NOTADB, "SQLITE_NOTADB"); 57 | SetCode(isolate, SQLITE_NOTICE, "SQLITE_NOTICE"); 58 | SetCode(isolate, SQLITE_WARNING, "SQLITE_WARNING"); 59 | SetCode(isolate, SQLITE_ROW, "SQLITE_ROW"); 60 | SetCode(isolate, SQLITE_DONE, "SQLITE_DONE"); 61 | SetCode(isolate, SQLITE_IOERR_READ, "SQLITE_IOERR_READ"); 62 | SetCode(isolate, SQLITE_IOERR_SHORT_READ, "SQLITE_IOERR_SHORT_READ"); 63 | SetCode(isolate, SQLITE_IOERR_WRITE, "SQLITE_IOERR_WRITE"); 64 | SetCode(isolate, SQLITE_IOERR_FSYNC, "SQLITE_IOERR_FSYNC"); 65 | SetCode(isolate, SQLITE_IOERR_DIR_FSYNC, "SQLITE_IOERR_DIR_FSYNC"); 66 | SetCode(isolate, SQLITE_IOERR_TRUNCATE, "SQLITE_IOERR_TRUNCATE"); 67 | SetCode(isolate, SQLITE_IOERR_FSTAT, "SQLITE_IOERR_FSTAT"); 68 | SetCode(isolate, SQLITE_IOERR_UNLOCK, "SQLITE_IOERR_UNLOCK"); 69 | SetCode(isolate, SQLITE_IOERR_RDLOCK, "SQLITE_IOERR_RDLOCK"); 70 | SetCode(isolate, SQLITE_IOERR_DELETE, "SQLITE_IOERR_DELETE"); 71 | SetCode(isolate, SQLITE_IOERR_BLOCKED, "SQLITE_IOERR_BLOCKED"); 72 | SetCode(isolate, SQLITE_IOERR_NOMEM, "SQLITE_IOERR_NOMEM"); 73 | SetCode(isolate, SQLITE_IOERR_ACCESS, "SQLITE_IOERR_ACCESS"); 74 | SetCode(isolate, SQLITE_IOERR_CHECKRESERVEDLOCK, "SQLITE_IOERR_CHECKRESERVEDLOCK"); 75 | SetCode(isolate, SQLITE_IOERR_LOCK, "SQLITE_IOERR_LOCK"); 76 | SetCode(isolate, SQLITE_IOERR_CLOSE, "SQLITE_IOERR_CLOSE"); 77 | SetCode(isolate, SQLITE_IOERR_DIR_CLOSE, "SQLITE_IOERR_DIR_CLOSE"); 78 | SetCode(isolate, SQLITE_IOERR_SHMOPEN, "SQLITE_IOERR_SHMOPEN"); 79 | SetCode(isolate, SQLITE_IOERR_SHMSIZE, "SQLITE_IOERR_SHMSIZE"); 80 | SetCode(isolate, SQLITE_IOERR_SHMLOCK, "SQLITE_IOERR_SHMLOCK"); 81 | SetCode(isolate, SQLITE_IOERR_SHMMAP, "SQLITE_IOERR_SHMMAP"); 82 | SetCode(isolate, SQLITE_IOERR_SEEK, "SQLITE_IOERR_SEEK"); 83 | SetCode(isolate, SQLITE_IOERR_DELETE_NOENT, "SQLITE_IOERR_DELETE_NOENT"); 84 | SetCode(isolate, SQLITE_IOERR_MMAP, "SQLITE_IOERR_MMAP"); 85 | SetCode(isolate, SQLITE_IOERR_GETTEMPPATH, "SQLITE_IOERR_GETTEMPPATH"); 86 | SetCode(isolate, SQLITE_IOERR_CONVPATH, "SQLITE_IOERR_CONVPATH"); 87 | SetCode(isolate, SQLITE_IOERR_VNODE, "SQLITE_IOERR_VNODE"); 88 | SetCode(isolate, SQLITE_IOERR_AUTH, "SQLITE_IOERR_AUTH"); 89 | SetCode(isolate, SQLITE_LOCKED_SHAREDCACHE, "SQLITE_LOCKED_SHAREDCACHE"); 90 | SetCode(isolate, SQLITE_BUSY_RECOVERY, "SQLITE_BUSY_RECOVERY"); 91 | SetCode(isolate, SQLITE_BUSY_SNAPSHOT, "SQLITE_BUSY_SNAPSHOT"); 92 | SetCode(isolate, SQLITE_CANTOPEN_NOTEMPDIR, "SQLITE_CANTOPEN_NOTEMPDIR"); 93 | SetCode(isolate, SQLITE_CANTOPEN_ISDIR, "SQLITE_CANTOPEN_ISDIR"); 94 | SetCode(isolate, SQLITE_CANTOPEN_FULLPATH, "SQLITE_CANTOPEN_FULLPATH"); 95 | SetCode(isolate, SQLITE_CANTOPEN_CONVPATH, "SQLITE_CANTOPEN_CONVPATH"); 96 | SetCode(isolate, SQLITE_CORRUPT_VTAB, "SQLITE_CORRUPT_VTAB"); 97 | SetCode(isolate, SQLITE_READONLY_RECOVERY, "SQLITE_READONLY_RECOVERY"); 98 | SetCode(isolate, SQLITE_READONLY_CANTLOCK, "SQLITE_READONLY_CANTLOCK"); 99 | SetCode(isolate, SQLITE_READONLY_ROLLBACK, "SQLITE_READONLY_ROLLBACK"); 100 | SetCode(isolate, SQLITE_READONLY_DBMOVED, "SQLITE_READONLY_DBMOVED"); 101 | SetCode(isolate, SQLITE_ABORT_ROLLBACK, "SQLITE_ABORT_ROLLBACK"); 102 | SetCode(isolate, SQLITE_CONSTRAINT_CHECK, "SQLITE_CONSTRAINT_CHECK"); 103 | SetCode(isolate, SQLITE_CONSTRAINT_COMMITHOOK, "SQLITE_CONSTRAINT_COMMITHOOK"); 104 | SetCode(isolate, SQLITE_CONSTRAINT_FOREIGNKEY, "SQLITE_CONSTRAINT_FOREIGNKEY"); 105 | SetCode(isolate, SQLITE_CONSTRAINT_FUNCTION, "SQLITE_CONSTRAINT_FUNCTION"); 106 | SetCode(isolate, SQLITE_CONSTRAINT_NOTNULL, "SQLITE_CONSTRAINT_NOTNULL"); 107 | SetCode(isolate, SQLITE_CONSTRAINT_PRIMARYKEY, "SQLITE_CONSTRAINT_PRIMARYKEY"); 108 | SetCode(isolate, SQLITE_CONSTRAINT_TRIGGER, "SQLITE_CONSTRAINT_TRIGGER"); 109 | SetCode(isolate, SQLITE_CONSTRAINT_UNIQUE, "SQLITE_CONSTRAINT_UNIQUE"); 110 | SetCode(isolate, SQLITE_CONSTRAINT_VTAB, "SQLITE_CONSTRAINT_VTAB"); 111 | SetCode(isolate, SQLITE_CONSTRAINT_ROWID, "SQLITE_CONSTRAINT_ROWID"); 112 | SetCode(isolate, SQLITE_NOTICE_RECOVER_WAL, "SQLITE_NOTICE_RECOVER_WAL"); 113 | SetCode(isolate, SQLITE_NOTICE_RECOVER_ROLLBACK, "SQLITE_NOTICE_RECOVER_ROLLBACK"); 114 | SetCode(isolate, SQLITE_WARNING_AUTOINDEX, "SQLITE_WARNING_AUTOINDEX"); 115 | SetCode(isolate, SQLITE_AUTH_USER, "SQLITE_AUTH_USER"); 116 | SetCode(isolate, SQLITE_OK_LOAD_PERMANENTLY, "SQLITE_OK_LOAD_PERMANENTLY"); 117 | } 118 | 119 | v8::Global database; 120 | v8::Global reader; 121 | v8::Global source; 122 | v8::Global memory; 123 | v8::Global readonly; 124 | v8::Global name; 125 | v8::Global next; 126 | v8::Global length; 127 | v8::Global done; 128 | v8::Global value; 129 | v8::Global changes; 130 | v8::Global lastInsertRowid; 131 | v8::Global statement; 132 | v8::Global column; 133 | v8::Global table; 134 | v8::Global type; 135 | v8::Global totalPages; 136 | v8::Global remainingPages; 137 | 138 | private: 139 | 140 | static void SetString(v8::Isolate* isolate, v8::Global& constant, const char* str) { 141 | constant.Reset(isolate, InternalizedFromLatin1(isolate, str)); 142 | } 143 | 144 | void SetCode(v8::Isolate* isolate, int code, const char* str) { 145 | codes.emplace(std::piecewise_construct, 146 | std::forward_as_tuple(code), 147 | std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str))); 148 | } 149 | 150 | std::unordered_map > codes; 151 | }; 152 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/util/data.lzz: -------------------------------------------------------------------------------- 1 | #define JS_VALUE_TO_SQLITE(to, value, isolate, ...) \ 2 | if (value->IsNumber()) { \ 3 | return sqlite3_##to##_double( \ 4 | __VA_ARGS__, \ 5 | value.As()->Value() \ 6 | ); \ 7 | } else if (value->IsBigInt()) { \ 8 | bool lossless; \ 9 | int64_t v = value.As()->Int64Value(&lossless); \ 10 | if (lossless) { \ 11 | return sqlite3_##to##_int64(__VA_ARGS__, v); \ 12 | } \ 13 | } else if (value->IsString()) { \ 14 | v8::String::Utf8Value utf8(isolate, value.As()); \ 15 | return sqlite3_##to##_text( \ 16 | __VA_ARGS__, \ 17 | *utf8, \ 18 | utf8.length(), \ 19 | SQLITE_TRANSIENT \ 20 | ); \ 21 | } else if (node::Buffer::HasInstance(value)) { \ 22 | const char* data = node::Buffer::Data(value); \ 23 | return sqlite3_##to##_blob( \ 24 | __VA_ARGS__, \ 25 | data ? data : "", \ 26 | node::Buffer::Length(value), \ 27 | SQLITE_TRANSIENT \ 28 | ); \ 29 | } else if (value->IsNull() || value->IsUndefined()) { \ 30 | return sqlite3_##to##_null(__VA_ARGS__); \ 31 | } 32 | 33 | #define SQLITE_VALUE_TO_JS(from, isolate, safe_ints, ...) \ 34 | switch (sqlite3_##from##_type(__VA_ARGS__)) { \ 35 | case SQLITE_INTEGER: \ 36 | if (safe_ints) { \ 37 | return v8::BigInt::New( \ 38 | isolate, \ 39 | sqlite3_##from##_int64(__VA_ARGS__) \ 40 | ); \ 41 | } \ 42 | case SQLITE_FLOAT: \ 43 | return v8::Number::New( \ 44 | isolate, \ 45 | sqlite3_##from##_double(__VA_ARGS__) \ 46 | ); \ 47 | case SQLITE_TEXT: \ 48 | return StringFromUtf8( \ 49 | isolate, \ 50 | reinterpret_cast(sqlite3_##from##_text(__VA_ARGS__)), \ 51 | sqlite3_##from##_bytes(__VA_ARGS__) \ 52 | ); \ 53 | case SQLITE_BLOB: \ 54 | return node::Buffer::Copy( \ 55 | isolate, \ 56 | static_cast(sqlite3_##from##_blob(__VA_ARGS__)), \ 57 | sqlite3_##from##_bytes(__VA_ARGS__) \ 58 | ).ToLocalChecked(); \ 59 | default: \ 60 | assert(sqlite3_##from##_type(__VA_ARGS__) == SQLITE_NULL); \ 61 | return v8::Null(isolate); \ 62 | } \ 63 | assert(false); 64 | 65 | namespace Data { 66 | 67 | static const char FLAT = 0; 68 | static const char PLUCK = 1; 69 | static const char EXPAND = 2; 70 | static const char RAW = 3; 71 | 72 | v8::Local GetValueJS(v8::Isolate* isolate, sqlite3_stmt* handle, int column, bool safe_ints) { 73 | SQLITE_VALUE_TO_JS(column, isolate, safe_ints, handle, column); 74 | } 75 | 76 | v8::Local GetValueJS(v8::Isolate* isolate, sqlite3_value* value, bool safe_ints) { 77 | SQLITE_VALUE_TO_JS(value, isolate, safe_ints, value); 78 | } 79 | 80 | v8::Local GetFlatRowJS(v8::Isolate* isolate, v8::Local ctx, sqlite3_stmt* handle, bool safe_ints) { 81 | v8::Local row = v8::Object::New(isolate); 82 | int column_count = sqlite3_column_count(handle); 83 | for (int i = 0; i < column_count; ++i) { 84 | row->Set(ctx, 85 | InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1), 86 | Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust(); 87 | } 88 | return row; 89 | } 90 | 91 | v8::Local GetExpandedRowJS(v8::Isolate* isolate, v8::Local ctx, sqlite3_stmt* handle, bool safe_ints) { 92 | v8::Local row = v8::Object::New(isolate); 93 | int column_count = sqlite3_column_count(handle); 94 | for (int i = 0; i < column_count; ++i) { 95 | const char* table_raw = sqlite3_column_table_name(handle, i); 96 | v8::Local table = InternalizedFromUtf8(isolate, table_raw == NULL ? "$" : table_raw, -1); 97 | v8::Local column = InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1); 98 | v8::Local value = Data::GetValueJS(isolate, handle, i, safe_ints); 99 | if (row->HasOwnProperty(ctx, table).FromJust()) { 100 | row->Get(ctx, table).ToLocalChecked().As()->Set(ctx, column, value).FromJust(); 101 | } else { 102 | v8::Local nested = v8::Object::New(isolate); 103 | row->Set(ctx, table, nested).FromJust(); 104 | nested->Set(ctx, column, value).FromJust(); 105 | } 106 | } 107 | return row; 108 | } 109 | 110 | v8::Local GetRawRowJS(v8::Isolate* isolate, v8::Local ctx, sqlite3_stmt* handle, bool safe_ints) { 111 | v8::Local row = v8::Array::New(isolate); 112 | int column_count = sqlite3_column_count(handle); 113 | for (int i = 0; i < column_count; ++i) { 114 | row->Set(ctx, i, Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust(); 115 | } 116 | return row; 117 | } 118 | 119 | v8::Local GetRowJS(v8::Isolate* isolate, v8::Local ctx, sqlite3_stmt* handle, bool safe_ints, char mode) { 120 | if (mode == FLAT) return GetFlatRowJS(isolate, ctx, handle, safe_ints); 121 | if (mode == PLUCK) return GetValueJS(isolate, handle, 0, safe_ints); 122 | if (mode == EXPAND) return GetExpandedRowJS(isolate, ctx, handle, safe_ints); 123 | if (mode == RAW) return GetRawRowJS(isolate, ctx, handle, safe_ints); 124 | assert(false); 125 | return v8::Local(); 126 | } 127 | 128 | void GetArgumentsJS(v8::Isolate* isolate, v8::Local* out, sqlite3_value** values, int argument_count, bool safe_ints) { 129 | assert(argument_count > 0); 130 | for (int i = 0; i < argument_count; ++i) { 131 | out[i] = Data::GetValueJS(isolate, values[i], safe_ints); 132 | } 133 | } 134 | 135 | int BindValueFromJS(v8::Isolate* isolate, sqlite3_stmt* handle, int index, v8::Local value) { 136 | JS_VALUE_TO_SQLITE(bind, value, isolate, handle, index); 137 | return value->IsBigInt() ? SQLITE_TOOBIG : -1; 138 | } 139 | 140 | void ResultValueFromJS(v8::Isolate* isolate, sqlite3_context* invocation, v8::Local value, DataConverter* converter) { 141 | JS_VALUE_TO_SQLITE(result, value, isolate, invocation); 142 | converter->ThrowDataConversionError(invocation, value->IsBigInt()); 143 | } 144 | 145 | } 146 | --------------------------------------------------------------------------------