Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/darwin-arm64.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'use strict';
module.exports = require('./database')(() => require('../prebuilds/darwin-arm64.node'), false);
module.exports = require('./with-binding')(() => require('../prebuilds/darwin-arm64.node'), false);
module.exports.SqliteError = require('./sqlite-error');
2 changes: 1 addition & 1 deletion lib/darwin-x64.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'use strict';
module.exports = require('./database')(() => require('../prebuilds/darwin-x64.node'), false);
module.exports = require('./with-binding')(() => require('../prebuilds/darwin-x64.node'), false);
module.exports.SqliteError = require('./sqlite-error');
167 changes: 102 additions & 65 deletions lib/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,82 +3,119 @@ const fs = require('fs');
const path = require('path');
const util = require('./util');
const SqliteError = require('./sqlite-error');
const wrappers = require('./methods/wrappers');

module.exports = function createDatabase(getAddon, allowNativeBinding) {
function Database(filenameGiven, options) {
if (new.target == null) {
return new Database(filenameGiven, options);
}
/**
* A database connection.
*
* Declared at module scope rather than inside a factory so that the type can be
* named. Each package entrypoint exports its own constructor, produced by
* lib/with-binding.js, which supplies the native addon loader for that
* entrypoint. Constructing this base directly is not supported; it has no
* addon loader of its own.
*
* @param {string|Buffer} [filenameGiven] Path to the database file; ':memory:'
* for an in-memory database, an empty string or omitted for a temporary one,
* or a Buffer previously returned by `.serialize()`.
* @param {object} [options] Connection options: `readonly`, `fileMustExist`,
* `timeout` in milliseconds, `verbose`, and `nativeBinding`.
* @returns {Database} The new connection. Returned explicitly so that invoking
* an entrypoint constructor without `new` behaves identically to using `new`.
*
* @throws {TypeError} If an argument or option is invalid, if the containing
* directory does not exist, or if this base is constructed without a bound
* addon loader.
* @throws {RangeError} If `timeout` exceeds 2147483647.
*
* @example
* const Database = require('better-sqlite3');
* const db = new Database('foobar.db', { verbose: console.log });
* db.prepare('SELECT 1 AS value').get(); // => { value: 1 }
*
* @see lib/with-binding.js
*/
function Database(filenameGiven, options) {
if (new.target == null) {
return new Database(filenameGiven, options);
}

// Apply defaults
let buffer;
if (Buffer.isBuffer(filenameGiven)) {
buffer = filenameGiven;
filenameGiven = ':memory:';
}
if (filenameGiven == null) filenameGiven = '';
if (options == null) options = {};
// Each entrypoint's constructor carries its own addon loader. Reading it
// from new.target rather than a closure is what allows this function to be
// declared at module scope, and it resolves through the static prototype
// chain so that subclasses of an entrypoint constructor work unchanged.
const bound = new.target[util.binding];
if (bound == null) {
throw new TypeError('Database cannot be constructed directly; require a better-sqlite3 entrypoint instead');
}
const { getAddon, allowNativeBinding } = bound;

// Validate arguments
if (typeof filenameGiven !== 'string') throw new TypeError('Expected first argument to be a string');
if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object');
if ('readOnly' in options) throw new TypeError('Misspelled option "readOnly" should be "readonly"');
if ('memory' in options) throw new TypeError('Option "memory" was removed in v7.0.0 (use ":memory:" filename instead)');
// Apply defaults
let buffer;
if (Buffer.isBuffer(filenameGiven)) {
buffer = filenameGiven;
filenameGiven = ':memory:';
}
if (filenameGiven == null) filenameGiven = '';
if (options == null) options = {};

// Interpret options
const filename = filenameGiven.trim();
const anonymous = filename === '' || filename === ':memory:';
const readonly = util.getBooleanOption(options, 'readonly');
const fileMustExist = util.getBooleanOption(options, 'fileMustExist');
const timeout = 'timeout' in options ? options.timeout : 5000;
const verbose = 'verbose' in options ? options.verbose : null;
const nativeBinding = 'nativeBinding' in options ? options.nativeBinding : null;
// Validate arguments
if (typeof filenameGiven !== 'string') throw new TypeError('Expected first argument to be a string');
if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object');
if ('readOnly' in options) throw new TypeError('Misspelled option "readOnly" should be "readonly"');
if ('memory' in options) throw new TypeError('Option "memory" was removed in v7.0.0 (use ":memory:" filename instead)');

// 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 (!allowNativeBinding && 'nativeBinding' in options) throw new TypeError('The "nativeBinding" option is only supported by the default better-sqlite3 entrypoint');
if (allowNativeBinding && nativeBinding != null && typeof nativeBinding !== 'string' && typeof nativeBinding !== 'object') throw new TypeError('Expected the "nativeBinding" option to be a string or addon object');
// Interpret options
const filename = filenameGiven.trim();
const anonymous = filename === '' || filename === ':memory:';
const readonly = util.getBooleanOption(options, 'readonly');
const fileMustExist = util.getBooleanOption(options, 'fileMustExist');
const timeout = 'timeout' in options ? options.timeout : 5000;
const verbose = 'verbose' in options ? options.verbose : null;
const nativeBinding = 'nativeBinding' in options ? options.nativeBinding : null;

// Load the native addon
const addon = getAddon(nativeBinding);
if (!addon.isInitialized) {
addon.initialize(SqliteError, arrayFactory, arrayAppender, rowFactory, recordFactory);
addon.isInitialized = true;
}
// 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 (!allowNativeBinding && 'nativeBinding' in options) throw new TypeError('The "nativeBinding" option is only supported by the default better-sqlite3 entrypoint');
if (allowNativeBinding && nativeBinding != null && typeof nativeBinding !== 'string' && typeof nativeBinding !== 'object') throw new TypeError('Expected the "nativeBinding" option to be a string or addon object');

// Make sure the specified directory exists
if (!anonymous && !filename.startsWith('file:') && !fs.existsSync(path.dirname(filename))) {
throw new TypeError('Cannot open database because the directory does not exist');
}
// Load the native addon
const addon = getAddon(nativeBinding);
if (!addon.isInitialized) {
addon.initialize(SqliteError, arrayFactory, arrayAppender, rowFactory, recordFactory);
addon.isInitialized = true;
}

Object.defineProperties(this, {
[util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null, buffer || null) },
...wrappers.getters,
});
// Make sure the specified directory exists
if (!anonymous && !filename.startsWith('file:') && !fs.existsSync(path.dirname(filename))) {
throw new TypeError('Cannot open database because the directory does not exist');
}

const wrappers = require('./methods/wrappers');
Database.prototype.prepare = wrappers.prepare;
Database.prototype.transaction = require('./methods/transaction');
Database.prototype.pragma = require('./methods/pragma');
Database.prototype.explain = require('./methods/explain');
Database.prototype.backup = require('./methods/backup');
Database.prototype.serialize = require('./methods/serialize');
Database.prototype.function = require('./methods/function');
Database.prototype.aggregate = require('./methods/aggregate');
Database.prototype.table = require('./methods/table');
Database.prototype.loadExtension = wrappers.loadExtension;
Database.prototype.exec = wrappers.exec;
Database.prototype.close = wrappers.close;
Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers;
Database.prototype.unsafeMode = wrappers.unsafeMode;
Database.prototype[util.inspect] = require('./methods/inspect');
Object.defineProperties(this, {
[util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null, buffer || null) },
...wrappers.getters,
});
}

Database.prototype.prepare = wrappers.prepare;
Database.prototype.transaction = require('./methods/transaction');
Database.prototype.pragma = require('./methods/pragma');
Database.prototype.explain = require('./methods/explain');
Database.prototype.backup = require('./methods/backup');
Database.prototype.serialize = require('./methods/serialize');
Database.prototype.function = require('./methods/function');
Database.prototype.aggregate = require('./methods/aggregate');
Database.prototype.table = require('./methods/table');
Database.prototype.loadExtension = wrappers.loadExtension;
Database.prototype.exec = wrappers.exec;
Database.prototype.close = wrappers.close;
Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers;
Database.prototype.unsafeMode = wrappers.unsafeMode;
Database.prototype[util.inspect] = require('./methods/inspect');

return Database;
};
module.exports = Database;

function arrayFactory(...values) {
return values;
Expand Down
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'use strict';
module.exports = require('./database')(require('./binding').getBinding, true);
module.exports = require('./with-binding')(require('./binding').getBinding, true);
module.exports.SqliteError = require('./sqlite-error');
2 changes: 1 addition & 1 deletion lib/linux-arm64.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'use strict';
module.exports = require('./database')(() => require('../prebuilds/linux-arm64.node'), false);
module.exports = require('./with-binding')(() => require('../prebuilds/linux-arm64.node'), false);
module.exports.SqliteError = require('./sqlite-error');
2 changes: 1 addition & 1 deletion lib/linux-x64.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'use strict';
module.exports = require('./database')(() => require('../prebuilds/linux-x64.node'), false);
module.exports = require('./with-binding')(() => require('../prebuilds/linux-x64.node'), false);
module.exports.SqliteError = require('./sqlite-error');
2 changes: 1 addition & 1 deletion lib/linuxmusl-arm64.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'use strict';
const Database = require('./database')(() => require('../prebuilds/linuxmusl-arm64.node'), false);
const Database = require('./with-binding')(() => require('../prebuilds/linuxmusl-arm64.node'), false);
Database.SqliteError = require('./sqlite-error');
module.exports = Database;
2 changes: 1 addition & 1 deletion lib/linuxmusl-x64.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'use strict';
const Database = require('./database')(() => require('../prebuilds/linuxmusl-x64.node'), false);
const Database = require('./with-binding')(() => require('../prebuilds/linuxmusl-x64.node'), false);
Database.SqliteError = require('./sqlite-error');
module.exports = Database;
1 change: 1 addition & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ exports.getBooleanOption = (options, key) => {
};

exports.cppdb = Symbol();
exports.binding = Symbol();
exports.inspect = Symbol.for('nodejs.util.inspect.custom');
2 changes: 1 addition & 1 deletion lib/win32-arm64.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'use strict';
module.exports = require('./database')(() => require('../prebuilds/win32-arm64.node'), false);
module.exports = require('./with-binding')(() => require('../prebuilds/win32-arm64.node'), false);
module.exports.SqliteError = require('./sqlite-error');
2 changes: 1 addition & 1 deletion lib/win32-x64.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'use strict';
module.exports = require('./database')(() => require('../prebuilds/win32-x64.node'), false);
module.exports = require('./with-binding')(() => require('../prebuilds/win32-x64.node'), false);
module.exports.SqliteError = require('./sqlite-error');
63 changes: 63 additions & 0 deletions lib/with-binding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';
const util = require('./util');
const Database = require('./database');

/**
* Creates a Database constructor bound to a specific native addon loader.
*
* Every package entrypoint calls this once, so each entrypoint exports its own
* constructor with its own prototype. That isolation is deliberate: patching
* one entrypoint's prototype must not affect another's, and an instance from
* one entrypoint must not be `instanceof` another's constructor.
*
* The returned constructor is observationally identical to the one the previous
* factory produced: same name, same arity, same prototype contents, same
* prototype depth, and the same behavior with and without `new`.
*
* @param {Function} getAddon Called with the `nativeBinding` option's value and
* returning the native addon object.
* @param {boolean} allowNativeBinding Whether this entrypoint permits the
* `nativeBinding` option; only the default entrypoint does.
* @returns {Function} A Database constructor bound to `getAddon`.
*
* @example
* // lib/index.js
* module.exports = require('./with-binding')(require('./binding').getBinding, true);
*
* @see lib/database.js
*/
module.exports = function withBinding(getAddon, allowNativeBinding) {
function BoundDatabase(...args) {
// new.target is forwarded so that subclasses construct as themselves.
// Hardcoding BoundDatabase here would silently produce base instances
// from `class Sub extends Database {}`, with no error raised.
return Reflect.construct(Database, args, new.target || BoundDatabase);
}

// Copy the prototype's descriptors rather than chaining to it, so instances
// sit exactly one link from Object.prototype as they always have. Chaining
// would add a hop to every method lookup and change the observable shape.
// The copy is safe because lib/database.js populates Database.prototype at
// module load, before any entrypoint can call this function.
BoundDatabase.prototype = Object.create(
Object.prototype,
Object.getOwnPropertyDescriptors(Database.prototype),
);

// Redefined rather than assigned, to keep `constructor` non-enumerable and
// therefore absent from for..in over an instance.
Object.defineProperty(BoundDatabase.prototype, 'constructor', {
value: BoundDatabase,
writable: true,
enumerable: false,
configurable: true,
});

// Preserve the identity callers and stack traces see.
Object.defineProperty(BoundDatabase, 'name', { value: Database.name, configurable: true });
Object.defineProperty(BoundDatabase, 'length', { value: Database.length, configurable: true });

BoundDatabase[util.binding] = { getAddon, allowNativeBinding };

return BoundDatabase;
};
Loading