- A simple automated schema migration system is provided.
See files in `test-integration` for example implementation.
+
+Your database interface is defined by extending the Abstract class.
+
+Your database engines are implemented by extending a class created by passing your abstract interface class to an engine class creator function.
+
const Errors = require('./lib/errors');
const Factory = require('./lib/factory');
const SchemaVersionHelper = require('./lib/schema-version-helper');
-const Postgres = require('./lib/postgres');
-const SQLite = require('./lib/sqlite');
+const PostgresCreator = require('./lib/postgres-creator');
+const SQLiteCreator = require('./lib/sqlite-creator');
module.exports = {
Abstract,
Errors,
Factory,
SchemaVersionHelper,
- Postgres,
- SQLite,
-};
\ No newline at end of file
+ SQLiteCreator,
+ PostgresCreator,
+};
* @param {...any} engineArguments
* @returns {Database}
*/
- constructor(logger, options, ...engineArguments) {
+ constructor(Abstract, logger, options, ...engineArguments) {
const connectionString = options.db.connectionString || '';
const protocol = connectionString.slice(0, connectionString.indexOf('://')).toLowerCase();
- const Engine = DatabaseFactory.requireEngine(protocol);
+ const EngineCreator = DatabaseFactory.requireEngineCreator(protocol);
+ const Engine = EngineCreator(Abstract);
return new Engine(logger, options, ...engineArguments);
}
* @param {String} protocol
* @returns {Object}
*/
- static requireEngine(protocol) {
+ static requireEngineCreator(protocol) {
const Engines = DatabaseFactory.Engines;
switch (protocol) {
case Engines.PostgreSQL:
- return require('./postgres');
+ return require('./postgres-creator');
case Engines.SQLite:
- return require('./sqlite');
+ return require('./sqlite-creator');
default:
throw new UnsupportedEngine(protocol);
--- /dev/null
+'use strict';
+
+const PGPromise = require('pg-promise');
+const path = require('node:path');
+const DBErrors = require('./errors');
+const { unappliedSchemaVersions } = require('./schema-version-helper');
+const { fileScope } = require('@squeep/log-helper');
+
+const _fileScope = fileScope(__filename);
+
+const PGTypeId = {
+ INT8: 20, // Type Id 20 == INT8 (BIGINT)
+ INT8Array: 1016, // Type Id 1016 == INT8[] (BIGINT[])
+};
+
+const PostgresCreator = (Abstract) => {
+
+ class DatabasePostgres extends Abstract {
+ _pgpInitOptions = {
+ capSQL: true,
+ };
+
+
+ // eslint-disable-next-line class-methods-use-this
+ get schemaVersionsSupported() {
+ return {
+ min: {
+ major: 0,
+ minor: 0,
+ patch: 0,
+ },
+ max: {
+ major: 0,
+ minor: 0,
+ patch: 0,
+ },
+ };
+ }
+
+
+ /**
+ *
+ * @param {ConsoleLike} logger
+ * @param {Object} options
+ * @param {Object} options.db
+ * @param {String} options.db.connectionString
+ * @param {String} options.db.queryLogLevel
+ * @param {Boolean} options.db.noWarnings set when running tests
+ * @param {} options.db.
+ * @param {*} pgpStub for tests
+ */
+ constructor(logger, options, pgpStub) {
+ super(logger, options);
+
+ // Set up
+ this._pgpInitOptionsConfigure(options.db);
+
+ // istanbul ignore next
+ this._pgp = pgpStub || PGPromise(this._pgpInitOptions);
+
+ // Return native BigInts
+ this._pgp.pg.types.setTypeParser(PGTypeId.INT8, BigInt); // Type Id 20 = INT8 (BIGINT)
+ const parseBigIntArray = this._pgp.pg.types.getTypeParser(PGTypeId.INT8Array); // Type Id 1016 = INT8[] (BIGINT[])
+ this._pgp.pg.types.setTypeParser(PGTypeId.INT8Array, (a) => parseBigIntArray(a).map(BigInt));
+
+ // Establish db connection
+ this.db = this._pgp(options.db.connectionString);
+
+ // Suppress QueryFile warnings when running tests
+ this.noWarnings = options.db.noWarnings;
+ }
+
+
+ async initialize(sqlPath = __dirname, applyMigrations = true) {
+ const _scope = _fileScope('initialize');
+ this.logger.debug(_scope, 'called', { applyMigrations });
+ if (applyMigrations) {
+ await this._initTables(sqlPath);
+ }
+ this._initStatements(sqlPath);
+ await super.initialize();
+ }
+
+
+ async _tableExists(tableName, schema) {
+ if (schema) {
+ return this.db.oneOrNone('SELECT table_name FROM information_schema.tables WHERE table_name=$(tableName) AND table_schema=$(schema)', { tableName, schema });
+ } else {
+ return this.db.oneOrNone('SELECT table_name FROM information_schema.tables WHERE table_name=$(tableName)', { tableName });
+ }
+ }
+
+ /**
+ * Create schema version tracking table and other common stuff.
+ */
+ async _createMetaVersionTable() {
+ const schema = this.pgSchema ? `CREATE SCHEMA ${this.pgSchema};
+ SET SCHEMA '${this.pgSchema}';` : '';
+ return await this.db.multiResult(`BEGIN;
+CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+${schema}
+CREATE TABLE IF NOT EXISTS _meta_schema_version (
+ major BIGINT NOT NULL,
+ minor BIGINT NOT NULL,
+ patch BIGINT NOT NULL,
+ applied TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
+ PRIMARY KEY (major, minor, patch)
+);
+INSERT INTO _meta_schema_version (major, minor, patch) VALUES (0, 0, 0);
+COMMIT;`);
+ }
+
+
+ async _initTables(sqlPath = __dirname) {
+ const _scope = _fileScope('_initTables');
+ this.logger.debug(_scope, 'called', {});
+
+ const _queryFile = this._queryFileHelper();
+
+ // Migrations rely upon this table, ensure it exists.
+ const metaVersionTable = '_meta_schema_version';
+
+ let metaExists = await this._tableExists(metaVersionTable, this.pgSchema);
+ if (!metaExists) {
+ const results = await this._createMetaVersionTable();
+ this.logger.debug(_scope, 'created meta version table', { results });
+ metaExists = await this._tableExists(metaVersionTable, this.pgSchema);
+ /* istanbul ignore if */
+ if (!metaExists) {
+ throw new DBErrors.UnexpectedResult(`did not create ${metaVersionTable} table`);
+ }
+ this.logger.info(_scope, 'created schema version table', { metaVersionTable });
+ }
+
+ // Apply migrations
+ const currentSchema = await this._currentSchema();
+ const migrationsWanted = unappliedSchemaVersions(sqlPath, currentSchema, this.schemaVersionsSupported);
+ this.logger.debug(_scope, 'schema migrations wanted', { migrationsWanted });
+ for (const v of migrationsWanted) {
+ const fPath = path.join(sqlPath, 'sql', 'schema', v, 'apply.sql');
+ const migrationSql = _queryFile(fPath);
+ const results = await this.db.multiResult(migrationSql);
+ this.logger.debug(_scope, 'executed migration sql', { version: v, results });
+ this.logger.info(_scope, 'applied migration', { version: v });
+ }
+ }
+
+
+ /**
+ * Configure the pgp init options object with assorted logging hooks,
+ * and converting returned column names from snake_case to camelCase.
+ * @param {Object} options
+ * @param {String} options.queryLogLevel
+ */
+ _pgpInitOptionsConfigure({ queryLogLevel }) {
+ // Log errors
+ const errorScope = _fileScope('pgp:error');
+ const errorLogger = this.logger.error;
+
+ this._pgpInitOptions.error = (err, event) => {
+ errorLogger(errorScope, '', { err, event });
+ };
+
+ // Log queries
+ let queryLogger;
+ if (queryLogLevel) {
+ const queryScope = _fileScope('pgp:query');
+ queryLogger = this.logger[queryLogLevel]; // eslint-disable-line security/detect-object-injection
+ this._pgpInitOptions.query = (event) => {
+ queryLogger(queryScope, '', {
+ query: event?.query,
+ params: event?.params,
+ });
+ };
+ }
+
+ // Deophidiate column names in-place, log results
+ const resultScope = _fileScope('pgp:result');
+ this._pgpInitOptions.receive = ({ data, result, ctx: event }) => {
+ const exemplaryRow = data[0];
+ for (const prop in exemplaryRow) {
+ const camel = Abstract._camelfy(prop);
+ if (!(camel in exemplaryRow)) {
+ for (const d of data) {
+ d[camel] = d[prop]; // eslint-disable-line security/detect-object-injection
+ delete d[prop]; // eslint-disable-line security/detect-object-injection
+ }
+ }
+ }
+ if (queryLogLevel) {
+ // Omitting .rows
+ queryLogger(resultScope, '', {
+ query: event?.query,
+ command: result?.command,
+ rowCount: result?.rowCount,
+ duration: result?.duration,
+ });
+ }
+ };
+ }
+
+
+ /**
+ * Wrap pgp QueryFile to impose settings and log errors.
+ * @returns {QueryFile}
+ */
+ _queryFileHelper() {
+ return (file) => {
+ const _scope = _fileScope('_queryFile');
+ /* istanbul ignore next */
+ const qfParams = {
+ minify: true,
+ ...(this.noWarnings && { noWarnings: this.noWarnings }),
+ };
+ const qf = new this._pgp.QueryFile(file, qfParams);
+ if (qf.error) {
+ this.logger.error(_scope, 'failed to create SQL statement', { error: qf.error, file });
+ throw qf.error;
+ }
+ return qf;
+ };
+ }
+
+
+ /**
+ * Load and prepare SQL from files.
+ */
+ _initStatements(sqlPath = __dirname) {
+ const _scope = _fileScope('_initStatements');
+ const _queryFile = this._queryFileHelper();
+ this.statement = this._pgp.utils.enumSql(path.join(sqlPath, 'sql'), {}, _queryFile);
+ this.logger.debug(_scope, 'statements initialized', { statements: Object.keys(this.statement).length });
+ }
+
+
+ async _currentSchema() {
+ return this.db.one(`SELECT major, minor, patch FROM ${this.pgSchema ? (this.pgSchema + '.') : ''}_meta_schema_version ORDER BY major DESC, minor DESC, patch DESC LIMIT 1`);
+ }
+
+
+ async context(fn) {
+ return this.db.task(async (t) => fn(t));
+ }
+
+
+ // eslint-disable-next-line class-methods-use-this
+ async transaction(dbCtx, fn) {
+ return dbCtx.txIf(async (t) => fn(t));
+ }
+
+
+ async healthCheck() {
+ const _scope = _fileScope('healthCheck');
+ this.logger.debug(_scope, 'called', {});
+ const c = await this.db.connect();
+ c.done();
+ return { serverVersion: c.client.serverVersion };
+ }
+
+
+ async _closeConnection() {
+ const _scope = _fileScope('_closeConnection');
+ try {
+ await this._pgp.end();
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e });
+ throw e;
+ }
+ }
+
+
+ /* istanbul ignore next */
+ async _purgeTables(really = false) {
+ const _scope = _fileScope('_purgeTables');
+ try {
+ if (really) {
+ await this.db.tx(async (t) => {
+ await t.batch(this._tableNames.map(async (table) => t.query('TRUNCATE TABLE $(table:name) CASCADE', { table })));
+ });
+ }
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e });
+ throw e;
+ }
+ }
+
+ }
+ return DatabasePostgres;
+};
+
+module.exports = PostgresCreator;
\ No newline at end of file
+++ /dev/null
-'use strict';
-
-const PGPromise = require('pg-promise');
-const path = require('node:path');
-const Database = require('./abstract');
-const DBErrors = require('./errors');
-const { unappliedSchemaVersions } = require('./schema-version-helper');
-const { fileScope } = require('@squeep/log-helper');
-
-const _fileScope = fileScope(__filename);
-
-const PGTypeId = {
- INT8: 20, // Type Id 20 == INT8 (BIGINT)
- INT8Array: 1016, // Type Id 1016 == INT8[] (BIGINT[])
-};
-
-class DatabasePostgres extends Database {
- _pgpInitOptions = {
- capSQL: true,
- };
-
-
- // eslint-disable-next-line class-methods-use-this
- get schemaVersionsSupported() {
- return {
- min: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- max: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- };
- }
-
-
- /**
- *
- * @param {ConsoleLike} logger
- * @param {Object} options
- * @param {Object} options.db
- * @param {String} options.db.connectionString
- * @param {String} options.db.queryLogLevel
- * @param {Boolean} options.db.noWarnings set when running tests
- * @param {} options.db.
- * @param {*} pgpStub for tests
- */
- constructor(logger, options, pgpStub) {
- super(logger, options);
-
- // Set up
- this._pgpInitOptionsConfigure(options.db);
-
- // istanbul ignore next
- this._pgp = pgpStub || PGPromise(this._pgpInitOptions);
-
- // Return native BigInts
- this._pgp.pg.types.setTypeParser(PGTypeId.INT8, BigInt); // Type Id 20 = INT8 (BIGINT)
- const parseBigIntArray = this._pgp.pg.types.getTypeParser(PGTypeId.INT8Array); // Type Id 1016 = INT8[] (BIGINT[])
- this._pgp.pg.types.setTypeParser(PGTypeId.INT8Array, (a) => parseBigIntArray(a).map(BigInt));
-
- // Establish db connection
- this.db = this._pgp(options.db.connectionString);
-
- // Suppress QueryFile warnings when running tests
- this.noWarnings = options.db.noWarnings;
- }
-
-
- async initialize(sqlPath = __dirname, applyMigrations = true) {
- const _scope = _fileScope('initialize');
- this.logger.debug(_scope, 'called', { applyMigrations });
- if (applyMigrations) {
- await this._initTables(sqlPath);
- }
- this._initStatements(sqlPath);
- await super.initialize();
- }
-
-
- async _tableExists(tableName) {
- return this.db.oneOrNone('SELECT table_name FROM information_schema.tables WHERE table_name=$(tableName)', { tableName });
- }
-
- /**
- * Create schema version tracking table and other common stuff.
- */
- async _createMetaVersionTable() {
- return await this.db.multiResult(`BEGIN;
-CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-CREATE TABLE IF NOT EXISTS _meta_schema_version (
- major BIGINT NOT NULL,
- minor BIGINT NOT NULL,
- patch BIGINT NOT NULL,
- applied TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
- PRIMARY KEY (major, minor, patch)
-);
-INSERT INTO _meta_schema_version (major, minor, patch) VALUES (0, 0, 0);
-COMMIT;`);
- }
-
-
- async _initTables(sqlPath = __dirname) {
- const _scope = _fileScope('_initTables');
- this.logger.debug(_scope, 'called', {});
-
- const _queryFile = this._queryFileHelper();
-
- // Migrations rely upon this table, ensure it exists.
- const metaVersionTable = '_meta_schema_version';
-
- let metaExists = await this._tableExists(metaVersionTable);
- if (!metaExists) {
- const results = await this._createMetaVersionTable();
- this.logger.debug(_scope, 'created meta version table', { results });
- metaExists = await this._tableExists(metaVersionTable);
- /* istanbul ignore if */
- if (!metaExists) {
- throw new DBErrors.UnexpectedResult(`did not create ${metaVersionTable} table`);
- }
- this.logger.info(_scope, 'created schema version table', { metaVersionTable });
- }
-
- // Apply migrations
- const currentSchema = await this._currentSchema();
- const migrationsWanted = unappliedSchemaVersions(sqlPath, currentSchema, this.schemaVersionsSupported);
- this.logger.debug(_scope, 'schema migrations wanted', { migrationsWanted });
- for (const v of migrationsWanted) {
- const fPath = path.join(sqlPath, 'sql', 'schema', v, 'apply.sql');
- const migrationSql = _queryFile(fPath);
- const results = await this.db.multiResult(migrationSql);
- this.logger.debug(_scope, 'executed migration sql', { version: v, results });
- this.logger.info(_scope, 'applied migration', { version: v });
- }
- }
-
-
- /**
- * Configure the pgp init options object with assorted logging hooks,
- * and converting returned column names from snake_case to camelCase.
- * @param {Object} options
- * @param {String} options.queryLogLevel
- */
- _pgpInitOptionsConfigure({ queryLogLevel }) {
- // Log errors
- const errorScope = _fileScope('pgp:error');
- const errorLogger = this.logger.error;
-
- this._pgpInitOptions.error = (err, event) => {
- errorLogger(errorScope, '', { err, event });
- };
-
- // Log queries
- let queryLogger;
- if (queryLogLevel) {
- const queryScope = _fileScope('pgp:query');
- queryLogger = this.logger[queryLogLevel]; // eslint-disable-line security/detect-object-injection
- this._pgpInitOptions.query = (event) => {
- queryLogger(queryScope, '', {
- query: event?.query,
- params: event?.params,
- });
- };
- }
-
- // Deophidiate column names in-place, log results
- const resultScope = _fileScope('pgp:result');
- this._pgpInitOptions.receive = ({ data, result, ctx: event }) => {
- const exemplaryRow = data[0];
- for (const prop in exemplaryRow) {
- const camel = Database._camelfy(prop);
- if (!(camel in exemplaryRow)) {
- for (const d of data) {
- d[camel] = d[prop]; // eslint-disable-line security/detect-object-injection
- delete d[prop]; // eslint-disable-line security/detect-object-injection
- }
- }
- }
- if (queryLogLevel) {
- // Omitting .rows
- queryLogger(resultScope, '', {
- query: event?.query,
- command: result?.command,
- rowCount: result?.rowCount,
- duration: result?.duration,
- });
- }
- };
- }
-
-
- /**
- * Wrap pgp QueryFile to impose settings and log errors.
- * @returns {QueryFile}
- */
- _queryFileHelper() {
- return (file) => {
- const _scope = _fileScope('_queryFile');
- /* istanbul ignore next */
- const qfParams = {
- minify: true,
- ...(this.noWarnings && { noWarnings: this.noWarnings }),
- };
- const qf = new this._pgp.QueryFile(file, qfParams);
- if (qf.error) {
- this.logger.error(_scope, 'failed to create SQL statement', { error: qf.error, file });
- throw qf.error;
- }
- return qf;
- };
- }
-
-
- /**
- * Load and prepare SQL from files.
- */
- _initStatements(sqlPath = __dirname) {
- const _scope = _fileScope('_initStatements');
- const _queryFile = this._queryFileHelper();
- this.statement = this._pgp.utils.enumSql(path.join(sqlPath, 'sql'), {}, _queryFile);
- this.logger.debug(_scope, 'statements initialized', { statements: Object.keys(this.statement).length });
- }
-
-
- async _currentSchema() {
- return this.db.one('SELECT major, minor, patch FROM _meta_schema_version ORDER BY major DESC, minor DESC, patch DESC LIMIT 1');
- }
-
-
- async context(fn) {
- return this.db.task(async (t) => fn(t));
- }
-
-
- // eslint-disable-next-line class-methods-use-this
- async transaction(dbCtx, fn) {
- return dbCtx.txIf(async (t) => fn(t));
- }
-
-
- async healthCheck() {
- const _scope = _fileScope('healthCheck');
- this.logger.debug(_scope, 'called', {});
- const c = await this.db.connect();
- c.done();
- return { serverVersion: c.client.serverVersion };
- }
-
-
- async _closeConnection() {
- const _scope = _fileScope('_closeConnection');
- try {
- await this._pgp.end();
- } catch (e) {
- this.logger.error(_scope, 'failed', { error: e });
- throw e;
- }
- }
-
-
- /* istanbul ignore next */
- async _purgeTables(really = false) {
- const _scope = _fileScope('_purgeTables');
- try {
- if (really) {
- await this.db.tx(async (t) => {
- await t.batch(this._tableNames.map(async (table) => t.query('TRUNCATE TABLE $(table:name) CASCADE', { table })));
- });
- }
- } catch (e) {
- this.logger.error(_scope, 'failed', { error: e });
- throw e;
- }
- }
-
-}
-
-module.exports = DatabasePostgres;
--- /dev/null
+'use strict';
+
+const SQLite = require('better-sqlite3');
+const fs = require('fs');
+const path = require('path');
+const { performance } = require('perf_hooks');
+const Database = require('./abstract');
+const DBErrors = require('./errors');
+const { unappliedSchemaVersions } = require('./schema-version-helper');
+const { fileScope } = require('@squeep/log-helper');
+
+const _fileScope = fileScope(__filename);
+
+const SQLiteCreator = (Abstract) => {
+
+ class DatabaseSQLite extends Abstract {
+ // eslint-disable-next-line class-methods-use-this
+ get schemaVersionsSupported() {
+ return {
+ min: {
+ major: 0,
+ minor: 0,
+ patch: 0,
+ },
+ max: {
+ major: 0,
+ minor: 0,
+ patch: 0,
+ },
+ };
+ }
+
+ /**
+ *
+ * @param {ConsoleLike} logger
+ * @param {Object} options
+ * @param {Object} options.db
+ * @param {String} options.db.connectionString
+ * @param {String=} options.db.queryLogLevel
+ * @param {BigInt=} options.db.sqliteOptimizeAfterChanges
+ */
+ constructor(logger, options) {
+ super(logger, options);
+
+ const connectionString = options.db.connectionString || 'sqlite://:memory:';
+ const csDelim = '://';
+ const dbFilename = connectionString.slice(connectionString.indexOf(csDelim) + csDelim.length);
+
+ const queryLogLevel = options.db.queryLogLevel;
+
+ const sqliteOptions = {
+ ...(queryLogLevel && {
+ // eslint-disable-next-line security/detect-object-injection
+ verbose: (query) => this.logger[queryLogLevel](_fileScope('SQLite:verbose'), '', { query }),
+ }),
+ };
+ this.db = new SQLite(dbFilename, sqliteOptions);
+ this.changesSinceLastOptimize = BigInt(0);
+ this.optimizeAfterChanges = options.db.sqliteOptimizeAfterChanges || 0; // Default to no periodic optimization.
+ this.db.pragma('foreign_keys = on'); // Enforce consistency.
+ this.db.pragma('journal_mode = WAL'); // Be faster, expect local filesystem.
+ this.db.defaultSafeIntegers(true); // This probably isn't necessary, but by using these BigInts we keep weird floats out of the query logs.
+ }
+
+
+ initialize(sqlPath = __dirname, applyMigrations = true) {
+ const _scope = _fileScope('initialize');
+ this.logger.debug(_scope, 'called', { applyMigrations });
+ if (applyMigrations) {
+ this._initTables(sqlPath);
+ }
+ super.initialize();
+ this._initStatements(sqlPath);
+ }
+
+
+ _tableExists(tableName) {
+ return this.db.prepare('SELECT name FROM sqlite_master WHERE type=:type AND name=:tableName')
+ .pluck(true)
+ .bind({ type: 'table', tableName })
+ .get();
+ }
+
+
+ /**
+ * Create schema version tracking table and other common stuff.
+ */
+ _createMetaVersionTable() {
+ return this.db.exec(`BEGIN;
+CREATE TABLE _meta_schema_version (
+ major INTEGER NOT NULL CHECK (typeof(major) = 'integer'),
+ minor INTEGER NOT NULL CHECK (typeof(minor) = 'integer'),
+ patch INTEGER NOT NULL CHECK (typeof(patch) = 'integer'),
+ applied INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) CHECK (typeof(applied) = 'integer'),
+ PRIMARY KEY (major DESC, minor DESC, patch DESC)
+) WITHOUT ROWID;
+INSERT INTO _meta_schema_version (major, minor, patch) VALUES (0, 0, 0);
+COMMIT;`);
+ }
+
+
+ /**
+ * SQLite cannot prepare its statements without a schema, ensure such exists.
+ */
+ _initTables(sqlPath = __dirname) {
+ const _scope = _fileScope('_initTables');
+
+ // Migrations rely upon this table, ensure it exists.
+ const metaVersionTable = '_meta_schema_version';
+ let metaExists = this._tableExists(metaVersionTable);
+ if (metaExists === undefined) {
+ this._createMetaVersionTable();
+ metaExists = this._tableExists(metaVersionTable);
+ /* istanbul ignore if */
+ if (metaExists === undefined) {
+ throw new DBErrors.UnexpectedResult(`did not create ${metaVersionTable} table`);
+ }
+ this.logger.info(_scope, 'created schema version table', { metaVersionTable });
+ }
+
+ // Apply migrations
+ const currentSchema = this._currentSchema();
+ const migrationsWanted = unappliedSchemaVersions(sqlPath, currentSchema, this.schemaVersionsSupported);
+ this.logger.debug(_scope, 'schema migrations wanted', { migrationsWanted });
+ migrationsWanted.forEach((v) => {
+ const fPath = path.join(sqlPath, 'sql', 'schema', v, 'apply.sql');
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
+ const fSql = fs.readFileSync(fPath, { encoding: 'utf8' });
+ this.logger.info(_scope, 'applying migration', { version: v });
+ this.db.exec(fSql);
+ });
+ }
+
+
+ _initStatements(sqlPath = __dirname) {
+ const _scope = _fileScope('_initStatements');
+ const sqlDir = path.join(sqlPath, 'sql');
+ this.statement = {};
+
+ // Decorate the statement calls we use with timing and logging.
+ const wrapFetch = (logName, statementName, fn) => {
+ const _wrapScope = _fileScope(logName);
+ return (...args) => {
+ const startTimestampMs = performance.now();
+ const rows = fn(...args);
+ DatabaseSQLite._deOphidiate(rows);
+ const elapsedTimeMs = performance.now() - startTimestampMs;
+ this.logger.debug(_wrapScope, 'complete', { statementName, elapsedTimeMs });
+ return rows;
+ };
+ };
+ const wrapRun = (logName, statementName, fn) => {
+ const _wrapScope = _fileScope(logName);
+ return (...args) => {
+ const startTimestampMs = performance.now();
+ const result = fn(...args);
+ const elapsedTimeMs = performance.now() - startTimestampMs;
+ this._updateChanges(result);
+ this.logger.debug(_wrapScope, 'complete', { ...result, statementName, elapsedTimeMs });
+ result.duration = elapsedTimeMs;
+ return result;
+ };
+ };
+
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
+ for (const f of fs.readdirSync(sqlDir)) {
+ const fPath = path.join(sqlDir, f);
+ const { name: fName, ext: fExt } = path.parse(f);
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
+ const stat = fs.statSync(fPath);
+ if (!stat.isFile()
+ || fExt.toLowerCase() !== '.sql') {
+ continue;
+ }
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
+ const fSql = fs.readFileSync(fPath, { encoding: 'utf8' });
+ const statementName = Database._camelfy(fName.toLowerCase(), '-');
+ let statement;
+ try {
+ statement = this.db.prepare(fSql);
+ } catch (e) /* istanbul ignore next */ {
+ this.logger.error(_scope, 'failed to prepare statement', { error: e, file: f });
+ throw e;
+ }
+ // eslint-disable-next-line security/detect-object-injection
+ this.statement[statementName] = statement;
+ const { get: origGet, all: origAll, run: origRun } = statement;
+ statement.get = wrapFetch('SQLite:get', statementName, origGet.bind(statement));
+ statement.all = wrapFetch('SQLite:all', statementName, origAll.bind(statement));
+ statement.run = wrapRun('SQLite:run', statementName, origRun.bind(statement));
+ }
+ this.statement._optimize = this.db.prepare('SELECT * FROM pragma_optimize(0xffff)');
+
+ this.logger.debug(_scope, 'statements initialized', { statements: Object.keys(this.statement).length });
+ }
+
+
+ static _deOphidiate(rows) {
+ const rowsIsArray = Array.isArray(rows);
+ if (!rowsIsArray) {
+ rows = [rows];
+ }
+ const exemplaryRow = rows[0];
+ for (const prop in exemplaryRow) {
+ const camel = Database._camelfy(prop);
+ if (!(camel in exemplaryRow)) {
+ for (const d of rows) {
+ d[camel] = d[prop]; // eslint-disable-line security/detect-object-injection
+ delete d[prop]; // eslint-disable-line security/detect-object-injection
+ }
+ }
+ }
+ return rowsIsArray ? rows : rows[0];
+ }
+
+
+ _currentSchema() {
+ return this.db.prepare('SELECT major, minor, patch FROM _meta_schema_version ORDER BY major DESC, minor DESC, patch DESC LIMIT 1').get();
+ }
+
+
+ healthCheck() {
+ const _scope = _fileScope('healthCheck');
+ this.logger.debug(_scope, 'called', {});
+ if (!this.db.open) {
+ throw new DBErrors.UnexpectedResult('database is not open');
+ }
+ return { open: this.db.open };
+ }
+
+
+ _closeConnection() {
+ this.db.close();
+ }
+
+
+ _optimize() {
+ const _scope = _fileScope('_optimize');
+
+ const optimize = this.statement._optimize.all();
+ this.logger.debug(_scope, 'optimize', { optimize, changes: this.changesSinceLastOptimize });
+ this.db.pragma('optimize');
+ this.changesSinceLastOptimize = BigInt(0);
+ }
+
+
+ _updateChanges(dbResult) {
+ if (this.optimizeAfterChanges) {
+ this.changesSinceLastOptimize += BigInt(dbResult.changes);
+ if (this.changesSinceLastOptimize >= this.optimizeAfterChanges) {
+ this._optimize();
+ }
+ }
+ }
+
+
+ _purgeTables(really) {
+ if (really) {
+ this._tableNames.forEach((table) => {
+ const result = this.db.prepare(`DELETE FROM ${table}`).run();
+ this.logger.debug(_fileScope('_purgeTables'), 'success', { table, result });
+ });
+ }
+ }
+
+
+ context(fn) {
+ return fn(this.db);
+ }
+
+
+ transaction(dbCtx, fn) {
+ dbCtx = dbCtx || this.db;
+ return dbCtx.transaction(fn)();
+ }
+
+
+ /**
+ * Boolean to 0/1 representation for SQLite params.
+ * @param {Boolean} bool
+ * @returns {Number}
+ */
+ static _booleanToNumeric(bool) {
+ // eslint-disable-next-line security/detect-object-injection
+ return {
+ true: 1,
+ false: 0,
+ }[bool];
+ }
+
+
+ /**
+ * Epoch seconds to Date.
+ * @param {Number} epoch
+ * @returns {Date}
+ */
+ static _epochToDate(epoch) {
+ return new Date(Number(epoch) * 1000);
+ }
+
+
+ /**
+ * Date to epoch seconds.
+ * @param {Date} date
+ * @returns {Number}
+ */
+ static _dateToEpoch(date) {
+ return Math.ceil(date.getTime() / 1000);
+ }
+
+ }
+
+ return DatabaseSQLite;
+};
+
+module.exports = SQLiteCreator;
+++ /dev/null
-'use strict';
-
-const SQLite = require('better-sqlite3');
-const fs = require('fs');
-const path = require('path');
-const { performance } = require('perf_hooks');
-const Database = require('./abstract');
-const DBErrors = require('./errors');
-const { unappliedSchemaVersions } = require('./schema-version-helper');
-const { fileScope } = require('@squeep/log-helper');
-
-const _fileScope = fileScope(__filename);
-
-class DatabaseSQLite extends Database {
- // eslint-disable-next-line class-methods-use-this
- get schemaVersionsSupported() {
- return {
- min: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- max: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- };
- }
-
- /**
- *
- * @param {ConsoleLike} logger
- * @param {Object} options
- * @param {Object} options.db
- * @param {String} options.db.connectionString
- * @param {String=} options.db.queryLogLevel
- * @param {BigInt=} options.db.sqliteOptimizeAfterChanges
- */
- constructor(logger, options) {
- super(logger, options);
-
- const connectionString = options.db.connectionString || 'sqlite://:memory:';
- const csDelim = '://';
- const dbFilename = connectionString.slice(connectionString.indexOf(csDelim) + csDelim.length);
-
- const queryLogLevel = options.db.queryLogLevel;
-
- const sqliteOptions = {
- ...(queryLogLevel && {
- // eslint-disable-next-line security/detect-object-injection
- verbose: (query) => this.logger[queryLogLevel](_fileScope('SQLite:verbose'), '', { query }),
- }),
- };
- this.db = new SQLite(dbFilename, sqliteOptions);
- this.changesSinceLastOptimize = BigInt(0);
- this.optimizeAfterChanges = options.db.sqliteOptimizeAfterChanges || 0; // Default to no periodic optimization.
- this.db.pragma('foreign_keys = on'); // Enforce consistency.
- this.db.pragma('journal_mode = WAL'); // Be faster, expect local filesystem.
- this.db.defaultSafeIntegers(true); // This probably isn't necessary, but by using these BigInts we keep weird floats out of the query logs.
- }
-
-
- initialize(sqlPath = __dirname, applyMigrations = true) {
- const _scope = _fileScope('initialize');
- this.logger.debug(_scope, 'called', { applyMigrations });
- if (applyMigrations) {
- this._initTables(sqlPath);
- }
- super.initialize();
- this._initStatements(sqlPath);
- }
-
-
- _tableExists(tableName) {
- return this.db.prepare('SELECT name FROM sqlite_master WHERE type=:type AND name=:tableName')
- .pluck(true)
- .bind({ type: 'table', tableName })
- .get();
- }
-
-
- /**
- * Create schema version tracking table and other common stuff.
- */
- _createMetaVersionTable() {
- return this.db.exec(`BEGIN;
-CREATE TABLE _meta_schema_version (
- major INTEGER NOT NULL CHECK (typeof(major) = 'integer'),
- minor INTEGER NOT NULL CHECK (typeof(minor) = 'integer'),
- patch INTEGER NOT NULL CHECK (typeof(patch) = 'integer'),
- applied INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) CHECK (typeof(applied) = 'integer'),
- PRIMARY KEY (major DESC, minor DESC, patch DESC)
-) WITHOUT ROWID;
-INSERT INTO _meta_schema_version (major, minor, patch) VALUES (0, 0, 0);
-COMMIT;`);
- }
-
-
- /**
- * SQLite cannot prepare its statements without a schema, ensure such exists.
- */
- _initTables(sqlPath = __dirname) {
- const _scope = _fileScope('_initTables');
-
- // Migrations rely upon this table, ensure it exists.
- const metaVersionTable = '_meta_schema_version';
- let metaExists = this._tableExists(metaVersionTable);
- if (metaExists === undefined) {
- this._createMetaVersionTable();
- metaExists = this._tableExists(metaVersionTable);
- /* istanbul ignore if */
- if (metaExists === undefined) {
- throw new DBErrors.UnexpectedResult(`did not create ${metaVersionTable} table`);
- }
- this.logger.info(_scope, 'created schema version table', { metaVersionTable });
- }
-
- // Apply migrations
- const currentSchema = this._currentSchema();
- const migrationsWanted = unappliedSchemaVersions(sqlPath, currentSchema, this.schemaVersionsSupported);
- this.logger.debug(_scope, 'schema migrations wanted', { migrationsWanted });
- migrationsWanted.forEach((v) => {
- const fPath = path.join(sqlPath, 'sql', 'schema', v, 'apply.sql');
- // eslint-disable-next-line security/detect-non-literal-fs-filename
- const fSql = fs.readFileSync(fPath, { encoding: 'utf8' });
- this.logger.info(_scope, 'applying migration', { version: v });
- this.db.exec(fSql);
- });
- }
-
-
- _initStatements(sqlPath = __dirname) {
- const _scope = _fileScope('_initStatements');
- const sqlDir = path.join(sqlPath, 'sql');
- this.statement = {};
-
- // Decorate the statement calls we use with timing and logging.
- const wrapFetch = (logName, statementName, fn) => {
- const _wrapScope = _fileScope(logName);
- return (...args) => {
- const startTimestampMs = performance.now();
- const rows = fn(...args);
- DatabaseSQLite._deOphidiate(rows);
- const elapsedTimeMs = performance.now() - startTimestampMs;
- this.logger.debug(_wrapScope, 'complete', { statementName, elapsedTimeMs });
- return rows;
- };
- };
- const wrapRun = (logName, statementName, fn) => {
- const _wrapScope = _fileScope(logName);
- return (...args) => {
- const startTimestampMs = performance.now();
- const result = fn(...args);
- const elapsedTimeMs = performance.now() - startTimestampMs;
- this._updateChanges(result);
- this.logger.debug(_wrapScope, 'complete', { ...result, statementName, elapsedTimeMs });
- result.duration = elapsedTimeMs;
- return result;
- };
- };
-
- // eslint-disable-next-line security/detect-non-literal-fs-filename
- for (const f of fs.readdirSync(sqlDir)) {
- const fPath = path.join(sqlDir, f);
- const { name: fName, ext: fExt } = path.parse(f);
- // eslint-disable-next-line security/detect-non-literal-fs-filename
- const stat = fs.statSync(fPath);
- if (!stat.isFile()
- || fExt.toLowerCase() !== '.sql') {
- continue;
- }
- // eslint-disable-next-line security/detect-non-literal-fs-filename
- const fSql = fs.readFileSync(fPath, { encoding: 'utf8' });
- const statementName = Database._camelfy(fName.toLowerCase(), '-');
- let statement;
- try {
- statement = this.db.prepare(fSql);
- } catch (e) /* istanbul ignore next */ {
- this.logger.error(_scope, 'failed to prepare statement', { error: e, file: f });
- throw e;
- }
- // eslint-disable-next-line security/detect-object-injection
- this.statement[statementName] = statement;
- const { get: origGet, all: origAll, run: origRun } = statement;
- statement.get = wrapFetch('SQLite:get', statementName, origGet.bind(statement));
- statement.all = wrapFetch('SQLite:all', statementName, origAll.bind(statement));
- statement.run = wrapRun('SQLite:run', statementName, origRun.bind(statement));
- }
- this.statement._optimize = this.db.prepare('SELECT * FROM pragma_optimize(0xffff)');
-
- this.logger.debug(_scope, 'statements initialized', { statements: Object.keys(this.statement).length });
- }
-
-
- static _deOphidiate(rows) {
- const rowsIsArray = Array.isArray(rows);
- if (!rowsIsArray) {
- rows = [rows];
- }
- const exemplaryRow = rows[0];
- for (const prop in exemplaryRow) {
- const camel = Database._camelfy(prop);
- if (!(camel in exemplaryRow)) {
- for (const d of rows) {
- d[camel] = d[prop]; // eslint-disable-line security/detect-object-injection
- delete d[prop]; // eslint-disable-line security/detect-object-injection
- }
- }
- }
- return rowsIsArray ? rows : rows[0];
- }
-
-
- _currentSchema() {
- return this.db.prepare('SELECT major, minor, patch FROM _meta_schema_version ORDER BY major DESC, minor DESC, patch DESC LIMIT 1').get();
- }
-
-
- healthCheck() {
- const _scope = _fileScope('healthCheck');
- this.logger.debug(_scope, 'called', {});
- if (!this.db.open) {
- throw new DBErrors.UnexpectedResult('database is not open');
- }
- return { open: this.db.open };
- }
-
-
- _closeConnection() {
- this.db.close();
- }
-
-
- _optimize() {
- const _scope = _fileScope('_optimize');
-
- const optimize = this.statement._optimize.all();
- this.logger.debug(_scope, 'optimize', { optimize, changes: this.changesSinceLastOptimize });
- this.db.pragma('optimize');
- this.changesSinceLastOptimize = BigInt(0);
- }
-
-
- _updateChanges(dbResult) {
- if (this.optimizeAfterChanges) {
- this.changesSinceLastOptimize += BigInt(dbResult.changes);
- if (this.changesSinceLastOptimize >= this.optimizeAfterChanges) {
- this._optimize();
- }
- }
- }
-
-
- _purgeTables(really) {
- if (really) {
- this._tableNames.forEach((table) => {
- const result = this.db.prepare(`DELETE FROM ${table}`).run();
- this.logger.debug(_fileScope('_purgeTables'), 'success', { table, result });
- });
- }
- }
-
-
- context(fn) {
- return fn(this.db);
- }
-
-
- transaction(dbCtx, fn) {
- dbCtx = dbCtx || this.db;
- return dbCtx.transaction(fn)();
- }
-
-
- /**
- * Boolean to 0/1 representation for SQLite params.
- * @param {Boolean} bool
- * @returns {Number}
- */
- static _booleanToNumeric(bool) {
- // eslint-disable-next-line security/detect-object-injection
- return {
- true: 1,
- false: 0,
- }[bool];
- }
-
-
- /**
- * Epoch seconds to Date.
- * @param {Number} epoch
- * @returns {Date}
- */
- static _epochToDate(epoch) {
- return new Date(Number(epoch) * 1000);
- }
-
-
- /**
- * Date to epoch seconds.
- * @param {Date} date
- * @returns {Number}
- */
- static _dateToEpoch(date) {
- return Math.ceil(date.getTime() / 1000);
- }
-
-}
-
-module.exports = DatabaseSQLite;
--- /dev/null
+'use strict';
+
+const Abstract = require('../lib/abstract');
+
+class AbstractIntegration extends Abstract {
+ constructor(...args) {
+ super(...args);
+ if (!this._isProduction) {
+ this._abstractMethods.push(...[
+ 'almanacGetAll',
+ 'almanacGet',
+ 'almanacUpsert',
+ ]);
+ this._tableNames.push(...[
+ 'almanac',
+ ]);
+ }
+ }
+
+ almanacGetAll(dbCtx) {
+ this._notImplemented('almanacGetAll', arguments);
+ }
+
+ almanacGet(dbCtx, event) {
+ this._notImplemented('almanacGetAll', arguments);
+ }
+
+ almanacUpsert(dbCtx, event, date) {
+ this._notImplemented('almanacGetAll', arguments);
+ }
+
+}
+
+module.exports = AbstractIntegration;
/* eslint-env mocha */
'use strict';
-const DatabasePostgresBase = require('../../lib/postgres');
+const Abstract = require('../abstract');
+const PostgresCreator = require('../../lib/postgres-creator');
const DBErrors = require('../../lib/errors');
const assert = require('node:assert');
/**
* A minimal but actual db class implementation.
*/
-class DB extends DatabasePostgresBase {
+class PostgresDB extends PostgresCreator(Abstract) {
// eslint-disable-next-line class-methods-use-this
get schemaVersionsSupported() {
}
-module.exports = DB;
\ No newline at end of file
+module.exports = PostgresDB;
/* eslint-env mocha */
'use strict';
-const DatabaseSQLiteBase = require('../../lib/sqlite');
+const assert = require('node:assert');
+const SQLiteCreator = require('../../lib/sqlite-creator');
+const Abstract = require('../abstract');
const DBErrors = require('../../lib/errors');
/**
* A minimal but actual db class implementation.
*/
-class DB extends DatabaseSQLiteBase {
+class SQLiteDB extends SQLiteCreator(Abstract) {
// eslint-disable-next-line class-methods-use-this
get schemaVersionsSupported() {
this.logger.debug(_scope, 'called');
try {
const entries = this.statement.almanacGetAll.all();
- return entries.map((entry) => DB._almanacEntryToNative(entry));
+ return entries.map((entry) => this.constructor._almanacEntryToNative(entry));
} catch (error) {
this.logger.error(_scope, 'failed', { error });
throw error;
static _almanacEntryToNative(entry) {
return {
event: entry.event,
- date: DatabaseSQLiteBase._epochToDate(entry.epoch),
+ date: super._epochToDate(entry.epoch),
};
}
this.logger.debug(_scope, 'called', { event });
try {
const entry = this.statement.almanacGet.get({ event });
- const { date } = DB._almanacEntryToNative(entry);
+ const { date } = this.constructor._almanacEntryToNative(entry);
return date;
} catch (error) {
this.logger.error(_scope, 'failed', { error, event });
const _scope = 'almanacUpsert';
this.logger.debug(_scope, 'called', { event, date });
try {
- const epoch = DatabaseSQLiteBase._dateToEpoch(date);
+ const epoch = this.constructor._dateToEpoch(date);
const result = this.statement.almanacUpsert.run({ event, epoch });
if (result.changes != 1) {
throw new DBErrors.UnexpectedResult('did not update almanac');
}
-module.exports = DB;
\ No newline at end of file
+module.exports = SQLiteDB;
const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
const DBErrors = require('../lib/errors');
-const nop = () => { /* */ };
+const nop = () => undefined;
class StubLogger {
constructor(backend) {
StubLogger,
stubPgp,
itChecksImplementation,
-};
\ No newline at end of file
+};
step('fetch all records', async function () {
const expected = events;
await db.context(async (dbCtx) => {
- const events = await db.almanacGetAll(dbCtx);
- assert.deepStrictEqual(events, expected);
+ const allEvents = await db.almanacGetAll(dbCtx);
+ assert.deepStrictEqual(allEvents, expected);
});
});
+
+ step('transaction', async function () {
+ const expected = events;
+ await db.context(async (dbCtx) => {
+ await db.transaction(dbCtx, async (txCtx) => {
+ for await (const event of events) {
+ await db.almanacUpsert(txCtx, event.event, event.date);
+ }
+ }); // txCtx
+ const allEvents = await db.almanacGetAll(dbCtx);
+ assert.deepStrictEqual(allEvents, expected);
+ });
+ });
+
}); // Almanac
}); // specific implementation
const assert = require('node:assert');
const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
const Factory = require('../../lib/factory');
+const Abstract = require('../../lib/abstract');
const { UnsupportedEngine } = require('../../lib/errors');
const pgp = require('pg-promise');
-const nop = () => {};
+const nop = () => unknown;
describe('Factory', function () {
let logger, options;
it('covers postgres', function () {
sinon.stub(pgp.utils, 'enumSql').returns({});
options.db.connectionString = 'postgresql://example';
- const factory = new Factory(logger, options);
+ const factory = new Factory(Abstract, logger, options);
assert(factory);
});
it('covers sqlite', function () {
options.db.connectionString = 'sqlite://:memory:';
- const factory = new Factory(logger, options);
+ const factory = new Factory(Abstract, logger, options);
assert(factory);
});
it('covers unknown', function () {
options.db.connectionString = 'redis://example';
- assert.throws(() => new Factory(logger, options), UnsupportedEngine);
+ assert.throws(() => new Factory(Abstract, logger, options), UnsupportedEngine);
});
-
it('covers empty', function () {
delete options.db.connectionString;
- assert.throws(() => new Factory(logger, options), UnsupportedEngine);
+ assert.throws(() => new Factory(Abstract, logger, options), UnsupportedEngine);
});
+
}); // Factory
const assert = require('node:assert');
const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
const fs = require('node:fs');
-const DatabasePostgres = require('../../lib/postgres');
+const Abstract = require('../../lib/abstract');
+const PostgresCreator = require('../../lib/postgres-creator');
const DBErrors = require('../../lib/errors');
const { StubLogger, stubPgp, nop, itChecksImplementation } = require('../helpers');
-describe('DatabasePostgres', function () {
+describe('Postgres Creator', function () {
let db, options, stubLogger;
const expectedException = new Error('oh no');
+ const DatabasePostgres = PostgresCreator(Abstract);
beforeEach(function () {
options = {
}); // receive
}); // pgpInitOptions
+ describe('_tableExists', function () {
+ beforeEach(function () {
+ sinon.stub(db.db, 'oneOrNone');
+ });
+ it('covers no pgSchema', async function () {
+ await db._tableExists('almanac', db.pgSchema);
+ assert(db.db.oneOrNone.called);
+ });
+ it('covers pgSchema', async function () {
+ db.pgSchema = 'some_schema';
+ await db._tableExists('almanac', db.pgSchema);
+ assert(db.db.oneOrNone.called);
+ });
+ }); // _tableExists
+
+ describe('_currentSchema', function () {
+ beforeEach(function () {
+ sinon.stub(db.db, 'one');
+ });
+ it('covers no pgSchema', async function () {
+ await db._currentSchema();
+ assert(db.db.one.called);
+ });
+ it('covers pgSchema', async function () {
+ db.pgSchema = 'some_schema';
+ await db._currentSchema();
+ assert(db.db.one.called);
+ });
+ }); // _currentSchema
+
describe('_initTables', function () {
beforeEach(function () {
sinon.stub(db.db, 'multiResult').resolves();
.onCall(1).resolves({ 'table_name': '_meta_schema_version' });
await db._initTables();
});
+ it('covers apply with pgSchema', async function () {
+ db.pgSchema = 'some_schema';
+ db._currentSchema.resolves({ major: 0, minor: 0, patch: 0 });
+ db._tableExists
+ .onCall(0).resolves()
+ .onCall(1).resolves({ 'table_name': '_meta_schema_version' });
+ await db._initTables();
+ });
it('covers exists', async function () {
db._tableExists.resolves({ 'table_name': '_meta_schema_version' });
db._currentSchema.resolves(db.schemaVersionsSupported.max);
const assert = require('node:assert');
const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
const fs = require('node:fs');
-const DatabaseSQLite = require('../../lib/sqlite');
+const Abstract = require('../../lib/abstract');
+const SQLiteCreator = require('../../lib/sqlite-creator');
const DBErrors = require('../../lib/errors');
const { StubLogger, stubPgp, nop, itChecksImplementation } = require('../helpers');
describe('DatabaseSQLite', function () {
let db, options, stubLogger;
const expectedException = new Error('oh no');
+ const DatabaseSQLite = SQLiteCreator(Abstract);
beforeEach(function () {
options = {