Skip to content

Commit

Permalink
Support for better-sqlite3 Driver (#4871)
Browse files Browse the repository at this point in the history
Signed-off-by: blam <ben@blam.sh>

Co-authored-by: Igor Savin <iselwin@gmail.com>
  • Loading branch information
benjdlambert and kibertoad committed Dec 9, 2021
1 parent de1122a commit 2bd1811
Show file tree
Hide file tree
Showing 17 changed files with 4,648 additions and 25 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/integration-tests.yml
Expand Up @@ -19,7 +19,7 @@ jobs:
fail-fast: false
matrix:
node-version: [17.x, 16.x, 14.x, 12.x]
database-type: [postgres, pgnative, mysql, mssql, sqlite3, cockroachdb]
database-type: [postgres, pgnative, mysql, mssql, sqlite3, cockroachdb, better-sqlite3]

steps:
- name: Checkout Repository
Expand All @@ -41,7 +41,7 @@ jobs:
--detach \
--build \
"${{ matrix.database-type }}"
if: matrix.database-type != 'sqlite3'
if: matrix.database-type != 'sqlite3' && matrix.database-type != 'better-sqlite3'

- name: Initialize Database(s)
run: |
Expand All @@ -50,7 +50,7 @@ jobs:
up \
--detach \
"wait${{ matrix.database-type }}"
if: matrix.database-type != 'sqlite3'
if: matrix.database-type != 'sqlite3' && matrix.database-type != 'better-sqlite3'

- name: Run npm install
run: npm install
Expand All @@ -70,4 +70,4 @@ jobs:
docker-compose \
--file "scripts/docker-compose.yml" \
down
if: matrix.database-type != 'sqlite3'
if: matrix.database-type != 'sqlite3' && matrix.database-type != 'better-sqlite3'
1 change: 1 addition & 0 deletions lib/client.js
Expand Up @@ -51,6 +51,7 @@ class Client extends EventEmitter {
`Using 'this.dialect' to identify the client is deprecated and support for it will be removed in the future. Please use configuration option 'client' instead.`
);
}

const dbClient = this.config.client || this.dialect;
if (!dbClient) {
throw new Error(
Expand Down
2 changes: 2 additions & 0 deletions lib/constants.js
Expand Up @@ -16,6 +16,7 @@ const SUPPORTED_CLIENTS = Object.freeze(
'redshift',
'sqlite3',
'cockroachdb',
'better-sqlite3',
].concat(Object.keys(CLIENT_ALIASES))
);

Expand All @@ -29,6 +30,7 @@ const DRIVER_NAMES = Object.freeze({
Redshift: 'pg-redshift',
SQLite: 'sqlite3',
CockroachDB: 'cockroachdb',
BetterSQLite3: 'better-sqlite3',
});

const POOL_CONFIG_OPTIONS = Object.freeze([
Expand Down
72 changes: 72 additions & 0 deletions lib/dialects/better-sqlite3/index.js
@@ -0,0 +1,72 @@
// better-sqlite3 Client
// -------
const Client_SQLite3 = require('../sqlite3');

class Client_BetterSQLite3 extends Client_SQLite3 {
_driver() {
return require('better-sqlite3');
}

// Get a raw connection from the database, returning a promise with the connection object.
async acquireRawConnection() {
return new this.driver(this.connectionSettings.filename);
}

// Used to explicitly close a connection, called internally by the pool when
// a connection times out or the pool is shutdown.
async destroyRawConnection(connection) {
return connection.close();
}

// Runs the query on the specified connection, providing the bindings and any
// other necessary prep work.
async _query(connection, obj) {
if (!obj.sql) throw new Error('The query is empty');

if (!connection) {
throw new Error('No connection provided');
}

const statement = connection.prepare(obj.sql);
const bindings = this._formatBindings(obj.bindings);

if (statement.reader) {
const response = await statement.all(bindings);
obj.response = response;
return obj;
}

const response = await statement.run(bindings);
obj.response = response;
obj.context = {
lastID: response.lastInsertRowid,
changes: response.changes,
};

return obj;
}

_formatBindings(bindings) {
if (!bindings) {
return [];
}
return bindings.map((binding) => {
if (binding instanceof Date) {
return binding.valueOf();
}

if (typeof binding === 'boolean') {
return String(binding);
}

return binding;
});
}
}

Object.assign(Client_BetterSQLite3.prototype, {
// The "dialect", for reference .
driverName: 'better-sqlite3',
});

module.exports = Client_BetterSQLite3;
2 changes: 2 additions & 0 deletions lib/dialects/sqlite3/index.js
Expand Up @@ -132,6 +132,7 @@ class Client_SQLite3 extends Client {
// We need the context here, as it contains
// the "this.lastID" or "this.changes"
obj.context = this;

return resolver(obj);
});
});
Expand All @@ -144,6 +145,7 @@ class Client_SQLite3 extends Client {
return new Promise(function (resolver, rejecter) {
stream.on('error', rejecter);
stream.on('end', resolver);

return client
._query(connection, obj)
.then((obj) => obj.response)
Expand Down
6 changes: 3 additions & 3 deletions lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js
Expand Up @@ -11,15 +11,15 @@ function copyData(sourceTable, targetTable, columns) {
}

function dropOriginal(tableName) {
return `DROP TABLE "${tableName}"`;
return `DROP TABLE '${tableName}'`;
}

function renameTable(tableName, alteredName) {
return `ALTER TABLE "${tableName}" RENAME TO "${alteredName}"`;
return `ALTER TABLE '${tableName}' RENAME TO '${alteredName}'`;
}

function getTableSql(tableName) {
return `SELECT type, sql FROM sqlite_master WHERE (type="table" OR (type="index" AND sql IS NOT NULL)) AND tbl_name="${tableName}"`;
return `SELECT type, sql FROM sqlite_master WHERE (type='table' OR (type='index' AND sql IS NOT NULL)) AND tbl_name='${tableName}'`;
}

module.exports = {
Expand Down
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -28,6 +28,7 @@
"test:mysql2": "cross-env DB=mysql2 npm run test:db",
"test:oracledb": "cross-env DB=oracledb npm run test:db",
"test:sqlite": "cross-env DB=sqlite3 npm run test:db",
"test:better-sqlite3": "cross-env DB=better-sqlite3 npm run test:db",
"test:postgres": "cross-env DB=postgres npm run test:db",
"test:cockroachdb": "cross-env DB=cockroachdb npm run test:db",
"test:pgnative": "cross-env DB=pgnative npm run test:db",
Expand Down Expand Up @@ -85,6 +86,9 @@
},
"sqlite3": {
"optional": true
},
"better-sqlite3": {
"optional": true
}
},
"lint-staged": {
Expand All @@ -96,6 +100,7 @@
"devDependencies": {
"@types/node": "^16.11.6",
"@vscode/sqlite3": "^5.0.7",
"better-sqlite3": "^7.4.5",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-subset-in-order": "^3.1.0",
Expand Down
2 changes: 2 additions & 0 deletions test/integration2/query/misc/additional.spec.js
Expand Up @@ -350,6 +350,8 @@ describe('Additional', function () {
"SELECT table_name FROM information_schema.tables WHERE table_schema='public'",
[drivers.Redshift]:
"SELECT table_name FROM information_schema.tables WHERE table_schema='public'",
[drivers.BetterSQLite3]:
"SELECT name FROM sqlite_master WHERE type='table';",
[drivers.SQLite]:
"SELECT name FROM sqlite_master WHERE type='table';",
[drivers.Oracle]: 'select TABLE_NAME from USER_TABLES',
Expand Down
1 change: 1 addition & 0 deletions test/integration2/query/select/selects.spec.js
Expand Up @@ -1207,6 +1207,7 @@ describe('Selects', function () {
'sqlite3',
'oracledb',
'cockroachdb',
'better-sqlite3',
]);

if (knex.client.driverName !== 'cockroachdb') {
Expand Down
25 changes: 17 additions & 8 deletions test/integration2/schema/foreign-keys.spec.js
Expand Up @@ -7,6 +7,7 @@ const {
isPostgreSQL,
isSQLite,
isCockroachDB,
isBetterSQLite3,
} = require('../../util/db-helpers');

describe('Schema', () => {
Expand Down Expand Up @@ -76,8 +77,8 @@ describe('Schema', () => {
expect(queries.sql).to.eql([
'CREATE TABLE `_knex_temp_alter111` (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `fkey_two` integer NOT NULL, `fkey_three` integer NOT NULL, CONSTRAINT `fk_fkey_threeee` FOREIGN KEY (`fkey_three`) REFERENCES `foreign_keys_table_three` (`id`))',
'INSERT INTO _knex_temp_alter111 SELECT * FROM foreign_keys_table_one;',
'DROP TABLE "foreign_keys_table_one"',
'ALTER TABLE "_knex_temp_alter111" RENAME TO "foreign_keys_table_one"',
"DROP TABLE 'foreign_keys_table_one'",
"ALTER TABLE '_knex_temp_alter111' RENAME TO 'foreign_keys_table_one'",
]);
}

Expand Down Expand Up @@ -112,8 +113,8 @@ describe('Schema', () => {
expect(queries.sql).to.eql([
'CREATE TABLE `_knex_temp_alter111` (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `fkey_two` integer NOT NULL, `fkey_three` integer NOT NULL, CONSTRAINT `fk_fkey_threeee` FOREIGN KEY (`fkey_three`) REFERENCES `foreign_keys_table_three` (`id`) ON DELETE CASCADE)',
'INSERT INTO _knex_temp_alter111 SELECT * FROM foreign_keys_table_one;',
'DROP TABLE "foreign_keys_table_one"',
'ALTER TABLE "_knex_temp_alter111" RENAME TO "foreign_keys_table_one"',
"DROP TABLE 'foreign_keys_table_one'",
"ALTER TABLE '_knex_temp_alter111' RENAME TO 'foreign_keys_table_one'",
]);
});

Expand All @@ -138,8 +139,8 @@ describe('Schema', () => {
expect(queries.sql).to.eql([
'CREATE TABLE `_knex_temp_alter111` (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `fkey_two` integer NOT NULL, `fkey_three` integer NOT NULL, CONSTRAINT `fk_fkey_threeee` FOREIGN KEY (`fkey_three`) REFERENCES `foreign_keys_table_three` (`id`) ON UPDATE CASCADE)',
'INSERT INTO _knex_temp_alter111 SELECT * FROM foreign_keys_table_one;',
'DROP TABLE "foreign_keys_table_one"',
'ALTER TABLE "_knex_temp_alter111" RENAME TO "foreign_keys_table_one"',
"DROP TABLE 'foreign_keys_table_one'",
"ALTER TABLE '_knex_temp_alter111' RENAME TO 'foreign_keys_table_one'",
]);
});

Expand Down Expand Up @@ -199,7 +200,11 @@ describe('Schema', () => {
});
throw new Error("Shouldn't reach this");
} catch (err) {
if (isSQLite(knex)) {
if (isBetterSQLite3(knex)) {
expect(err.message).to.equal(
`insert into \`foreign_keys_table_one\` (\`fkey_three\`, \`fkey_two\`) values (99, 9999) - FOREIGN KEY constraint failed`
);
} else if (isSQLite(knex)) {
expect(err.message).to.equal(
`insert into \`foreign_keys_table_one\` (\`fkey_three\`, \`fkey_two\`) values (99, 9999) - SQLITE_CONSTRAINT_FOREIGNKEY: FOREIGN KEY constraint failed`
);
Expand Down Expand Up @@ -330,7 +335,11 @@ describe('Schema', () => {
});
throw new Error("Shouldn't reach this");
} catch (err) {
if (isSQLite(knex)) {
if (isBetterSQLite3(knex)) {
expect(err.message).to.equal(
`insert into \`foreign_keys_table_one\` (\`fkey_four_part1\`, \`fkey_four_part2\`, \`fkey_three\`, \`fkey_two\`) values ('a', 'b', 99, 9999) - FOREIGN KEY constraint failed`
);
} else if (isSQLite(knex)) {
expect(err.message).to.equal(
`insert into \`foreign_keys_table_one\` (\`fkey_four_part1\`, \`fkey_four_part2\`, \`fkey_three\`, \`fkey_two\`) values ('a', 'b', 99, 9999) - SQLITE_CONSTRAINT_FOREIGNKEY: FOREIGN KEY constraint failed`
);
Expand Down
13 changes: 13 additions & 0 deletions test/integration2/schema/misc.spec.js
Expand Up @@ -13,6 +13,7 @@ const {
isMssql,
isCockroachDB,
isPostgreSQL,
isBetterSQLite3,
} = require('../../util/db-helpers');
const { getAllDbs, getKnexForDb } = require('../util/knex-instance-provider');
const logger = require('../../integration/logger');
Expand Down Expand Up @@ -1305,6 +1306,9 @@ describe('Schema (misc)', () => {
tester('pg-redshift', [
'create table "bool_test" ("one" boolean, "two" boolean default \'0\', "three" boolean default \'1\', "four" boolean default \'1\', "five" boolean default \'0\')',
]);
tester('better-sqlite3', [
"create table `bool_test` (`one` boolean, `two` boolean default '0', `three` boolean default '1', `four` boolean default '1', `five` boolean default '0')",
]);
tester('sqlite3', [
"create table `bool_test` (`one` boolean, `two` boolean default '0', `three` boolean default '1', `four` boolean default '1', `five` boolean default '0')",
]);
Expand Down Expand Up @@ -1785,6 +1789,15 @@ describe('Schema (misc)', () => {

const autoinc = !!res.rows[0].ident;
expect(autoinc).to.equal(true);
} else if (isBetterSQLite3(knex)) {
const res = await knex.raw(
`SELECT 'is-autoincrement' as ident
FROM sqlite_master
WHERE tbl_name = ? AND sql LIKE '%AUTOINCREMENT%'`,
[tableName]
);
const autoinc = !!res[0].ident;
expect(autoinc).to.equal(true);
} else if (isSQLite(knex)) {
const res = await knex.raw(
`SELECT "is-autoincrement" as ident
Expand Down
25 changes: 21 additions & 4 deletions test/integration2/schema/primary-keys.spec.js
Expand Up @@ -4,6 +4,7 @@ const {
isSQLite,
isPgBased,
isCockroachDB,
isBetterSQLite3,
} = require('../../util/db-helpers');
const { getAllDbs, getKnexForDb } = require('../util/knex-instance-provider');

Expand Down Expand Up @@ -45,7 +46,11 @@ describe('Schema', () => {
await knex('primary_table').insert({ id_four: 1 });
throw new Error(`Shouldn't reach this`);
} catch (err) {
if (isSQLite(knex)) {
if (isBetterSQLite3(knex)) {
expect(err.message).to.equal(
'insert into `primary_table` (`id_four`) values (1) - UNIQUE constraint failed: primary_table.id_four'
);
} else if (isSQLite(knex)) {
expect(err.message).to.equal(
'insert into `primary_table` (`id_four`) values (1) - SQLITE_CONSTRAINT_PRIMARYKEY: UNIQUE constraint failed: primary_table.id_four'
);
Expand Down Expand Up @@ -76,7 +81,11 @@ describe('Schema', () => {
await knex('primary_table').insert({ id_four: 1 });
throw new Error(`Shouldn't reach this`);
} catch (err) {
if (isSQLite(knex)) {
if (isBetterSQLite3(knex)) {
expect(err.message).to.equal(
'insert into `primary_table` (`id_four`) values (1) - UNIQUE constraint failed: primary_table.id_four'
);
} else if (isSQLite(knex)) {
expect(err.message).to.equal(
'insert into `primary_table` (`id_four`) values (1) - SQLITE_CONSTRAINT_PRIMARYKEY: UNIQUE constraint failed: primary_table.id_four'
);
Expand Down Expand Up @@ -112,7 +121,11 @@ describe('Schema', () => {
try {
await knex('primary_table').insert({ id_two: 1, id_three: 1 });
} catch (err) {
if (isSQLite(knex)) {
if (isBetterSQLite3(knex)) {
expect(err.message).to.equal(
'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - UNIQUE constraint failed: primary_table.id_two, primary_table.id_three'
);
} else if (isSQLite(knex)) {
expect(err.message).to.equal(
'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - SQLITE_CONSTRAINT_PRIMARYKEY: UNIQUE constraint failed: primary_table.id_two, primary_table.id_three'
);
Expand Down Expand Up @@ -154,7 +167,11 @@ describe('Schema', () => {
try {
await knex('primary_table').insert({ id_two: 1, id_three: 1 });
} catch (err) {
if (isSQLite(knex)) {
if (isBetterSQLite3(knex)) {
expect(err.message).to.equal(
'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - UNIQUE constraint failed: primary_table.id_two, primary_table.id_three'
);
} else if (isSQLite(knex)) {
expect(err.message).to.equal(
'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - SQLITE_CONSTRAINT_PRIMARYKEY: UNIQUE constraint failed: primary_table.id_two, primary_table.id_three'
);
Expand Down
8 changes: 6 additions & 2 deletions test/integration2/schema/set-nullable.spec.js
Expand Up @@ -6,6 +6,7 @@ const {
isSQLite,
isPostgreSQL,
isMysql,
isBetterSQLite3,
isOracle,
} = require('../../util/db-helpers');
const { getAllDbs, getKnexForDb } = require('../util/knex-instance-provider');
Expand Down Expand Up @@ -48,8 +49,8 @@ describe('Schema', () => {
expect(queries.sql).to.eql([
'CREATE TABLE `_knex_temp_alter111` (`id_nullable` integer NULL, `id_not_nullable` integer)',
'INSERT INTO _knex_temp_alter111 SELECT * FROM primary_table;',
'DROP TABLE "primary_table"',
'ALTER TABLE "_knex_temp_alter111" RENAME TO "primary_table"',
"DROP TABLE 'primary_table'",
"ALTER TABLE '_knex_temp_alter111' RENAME TO 'primary_table'",
]);
}

Expand Down Expand Up @@ -88,6 +89,9 @@ describe('Schema', () => {
errorMessage = 'cannot be null';
} else if (isOracle(knex)) {
errorMessage = 'ORA-01400: cannot insert NULL into';
} else if (isBetterSQLite3(knex)) {
errorMessage =
'insert into `primary_table` (`id_not_nullable`, `id_nullable`) values (1, NULL) - NOT NULL constraint failed: primary_table.id_nullable';
} else if (isSQLite(knex)) {
errorMessage =
'insert into `primary_table` (`id_not_nullable`, `id_nullable`) values (1, NULL) - SQLITE_CONSTRAINT_NOTNULL: NOT NULL constraint failed: primary_table.id_nullable';
Expand Down

0 comments on commit 2bd1811

Please sign in to comment.