Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for better-sqlite3 Driver #4871

Merged
merged 13 commits into from Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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