Skip to content

Commit

Permalink
implemented "nativeBinding" database option
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaWise committed Jan 18, 2022
1 parent 539ff32 commit 9f6050a
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 25 deletions.
2 changes: 2 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Various options are accepted:

- `options.verbose`: provide a function that gets called with every SQL string executed by the database connection (default: `null`).

- `options.nativeBinding`: if you're using a complicated build system that moves, transforms, or concatenates your JS files, `better-sqlite3` might have trouble locating its native C++ addon, `better_sqlite3.node`. If you get an error that looks like [this](https://github.com/JoshuaWise/better-sqlite3/issues/146#issue-337752663), you can solve it by using this option to provide the file path of `better_sqlite3.node` (relative to the current working directory).

```js
const Database = require('better-sqlite3');
const db = new Database('foobar.db', { verbose: console.log });
Expand Down
23 changes: 17 additions & 6 deletions lib/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
const fs = require('fs');
const path = require('path');
const util = require('./util');
const SqliteError = require('./sqlite-error');

const {
Database: CPPDatabase,
setErrorConstructor,
} = require('bindings')('better_sqlite3.node');
let DEFAULT_ADDON;

function Database(filenameGiven, options) {
if (new.target == null) {
Expand Down Expand Up @@ -35,20 +33,34 @@ function Database(filenameGiven, options) {
const fileMustExist = util.getBooleanOption(options, 'fileMustExist');
const timeout = 'timeout' in options ? options.timeout : 5000;
const verbose = 'verbose' in options ? options.verbose : null;
const nativeBindingPath = 'nativeBinding' in options ? options.nativeBinding : null;

// Validate interpreted options
if (readonly && anonymous && !buffer) throw new TypeError('In-memory/temporary databases cannot be readonly');
if (!Number.isInteger(timeout) || timeout < 0) throw new TypeError('Expected the "timeout" option to be a positive integer');
if (timeout > 0x7fffffff) throw new RangeError('Option "timeout" cannot be greater than 2147483647');
if (verbose != null && typeof verbose !== 'function') throw new TypeError('Expected the "verbose" option to be a function');
if (nativeBindingPath != null && typeof nativeBindingPath !== 'string') throw new TypeError('Expected the "nativeBinding" option to be a string');

// Load the native addon
let addon;
if (nativeBindingPath == null) {
addon = DEFAULT_ADDON || (DEFAULT_ADDON = require('bindings')('better_sqlite3.node'));
} else {
addon = require(path.resolve(nativeBindingPath).replace(/(\.node)?$/, '.node'));
}
if (!addon.isInitialized) {
addon.setErrorConstructor(SqliteError);
addon.isInitialized = true;
}

// Make sure the specified directory exists
if (!anonymous && !fs.existsSync(path.dirname(filename))) {
throw new TypeError('Cannot open database because the directory does not exist');
}

Object.defineProperties(this, {
[util.cppdb]: { value: new CPPDatabase(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null, buffer || null) },
[util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null, buffer || null) },
...wrappers.getters,
});
}
Expand All @@ -70,4 +82,3 @@ Database.prototype.unsafeMode = wrappers.unsafeMode;
Database.prototype[util.inspect] = require('./methods/inspect');

module.exports = Database;
setErrorConstructor(require('./sqlite-error'));
65 changes: 46 additions & 19 deletions test/10.database.open.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';
const { existsSync } = require('fs');
const fs = require('fs');
const path = require('path');
const Database = require('../.');

describe('new Database()', function () {
Expand Down Expand Up @@ -31,10 +32,10 @@ describe('new Database()', function () {
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync('')).to.be.false;
expect(existsSync('null')).to.be.false;
expect(existsSync('undefined')).to.be.false;
expect(existsSync('[object Object]')).to.be.false;
expect(fs.existsSync('')).to.be.false;
expect(fs.existsSync('null')).to.be.false;
expect(fs.existsSync('undefined')).to.be.false;
expect(fs.existsSync('[object Object]')).to.be.false;
db.close();
}
});
Expand All @@ -45,49 +46,49 @@ describe('new Database()', function () {
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(':memory:')).to.be.false;
expect(fs.existsSync(':memory:')).to.be.false;
});
it('should allow disk-bound databases to be created', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
const db = this.db = new Database(util.current());
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
it('should allow readonly database connections to be created', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(util.current(), { readonly: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
(new Database(util.current())).close();
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
const db = this.db = new Database(util.current(), { readonly: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.true;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
it('should not allow the "readonly" option for in-memory databases', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(':memory:', { readonly: true }))).to.throw(TypeError);
expect(() => (this.db = new Database('', { readonly: true }))).to.throw(TypeError);
expect(existsSync(util.current())).to.be.false;
expect(fs.existsSync(util.current())).to.be.false;
});
it('should accept the "fileMustExist" option', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(util.current(), { fileMustExist: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
(new Database(util.current())).close();
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
const db = this.db = new Database(util.current(), { fileMustExist: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
util.itUnix('should accept the "timeout" option', function () {
this.slow(4000);
Expand Down Expand Up @@ -115,12 +116,38 @@ describe('new Database()', function () {
expect(() => (this.db = new Database(util.current(), { timeout: 75.01 }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: 0x80000000 }))).to.throw(RangeError);
});
it('should accept the "nativeBinding" option', function () {
this.slow(500);
const oldBinding = require('bindings')({ bindings: 'better_sqlite3.node', path: true });
const newBinding = path.join(__dirname, '..', 'temp', 'foo.node');
expect(oldBinding).to.be.a('string');
fs.copyFileSync(oldBinding, newBinding);
const getBinding = db => db[Object.getOwnPropertySymbols(db)[0]].constructor;
let db1;
let db2;
let db3;
try {
db1 = new Database('');
db2 = new Database('', { nativeBinding: oldBinding });
db3 = new Database('', { nativeBinding: newBinding });
expect(db1.open).to.be.true;
expect(db2.open).to.be.true;
expect(db3.open).to.be.true;
expect(getBinding(db1)).to.equal(getBinding(db2));
expect(getBinding(db1)).to.not.equal(getBinding(db3));
expect(getBinding(db2)).to.not.equal(getBinding(db3));
} finally {
if (db1) db1.close();
if (db2) db2.close();
if (db3) db3.close();
}
});
it('should throw an Error if the directory does not exist', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
const filepath = `temp/nonexistent/abcfoobar123/${util.current()}`;
expect(() => (this.db = new Database(filepath))).to.throw(TypeError);
expect(existsSync(filepath)).to.be.false;
expect(existsSync(util.current())).to.be.false;
expect(fs.existsSync(filepath)).to.be.false;
expect(fs.existsSync(util.current())).to.be.false;
});
it('should have a proper prototype chain', function () {
const db = this.db = new Database(util.next());
Expand Down

0 comments on commit 9f6050a

Please sign in to comment.