Skip to content

Commit

Permalink
feat: add better-sqlite driver (#2792)
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Feb 18, 2022
1 parent 64bc99d commit 1b39d66
Show file tree
Hide file tree
Showing 20 changed files with 703 additions and 15 deletions.
1 change: 1 addition & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const packages = [
'entity-generator',
'reflection',
'sqlite',
'better-sqlite',
'mariadb',
'mongodb',
'mysql',
Expand Down
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Config } from '@jest/types';

// Sync object
const config: Config.InitialOptions = {
testTimeout: 30000,
preset: 'ts-jest',
Expand All @@ -20,6 +19,7 @@ const config: Config.InitialOptions = {
],
coveragePathIgnorePatterns: [
'<rootDir>/packages/cli/src/cli.ts',
'<rootDir>/packages/better-sqlite',
],
setupFiles: [
'<rootDir>/tests/setup.ts',
Expand Down
8 changes: 8 additions & 0 deletions packages/better-sqlite/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
src
tests
coverage
temp
yarn-error.log
data
tsconfig.*
84 changes: 84 additions & 0 deletions packages/better-sqlite/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"name": "@mikro-orm/better-sqlite",
"version": "5.0.2",
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
"main": "dist/index.js",
"module": "dist/index.mjs",
"typings": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/mikro-orm/mikro-orm.git"
},
"keywords": [
"orm",
"mongo",
"mongodb",
"mysql",
"mariadb",
"postgresql",
"sqlite",
"sqlite3",
"ts",
"typescript",
"js",
"javascript",
"entity",
"ddd",
"mikro-orm",
"unit-of-work",
"data-mapper",
"identity-map"
],
"author": "Martin Adámek",
"license": "MIT",
"bugs": {
"url": "https://github.com/mikro-orm/mikro-orm/issues"
},
"homepage": "https://mikro-orm.io",
"engines": {
"node": ">= 14.0.0"
},
"scripts": {
"build": "yarn clean && yarn compile && yarn copy",
"postbuild": "yarn gen-esm-wrapper dist/index.js dist/index.mjs",
"clean": "rimraf ./dist",
"compile": "tsc -p tsconfig.build.json",
"copy": "ts-node -T ../../scripts/copy.ts"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@mikro-orm/knex": "^5.0.2",
"better-sqlite3": "7.5.0",
"fs-extra": "10.0.0",
"sqlstring-sqlite": "0.1.1"
},
"devDependencies": {
"@mikro-orm/core": "^5.0.2"
},
"peerDependencies": {
"@mikro-orm/core": "^5.0.0",
"@mikro-orm/entity-generator": "^5.0.0",
"@mikro-orm/migrations": "^5.0.0",
"@mikro-orm/seeder": "^5.0.0"
},
"peerDependenciesMeta": {
"@mikro-orm/entity-generator": {
"optional": true
},
"@mikro-orm/migrations": {
"optional": true
},
"@mikro-orm/seeder": {
"optional": true
}
}
}
155 changes: 155 additions & 0 deletions packages/better-sqlite/src/BetterSqliteConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { ensureDir, readFile } from 'fs-extra';
import { dirname } from 'path';
import type { Knex } from '@mikro-orm/knex';
import { AbstractSqlConnection, MonkeyPatchable } from '@mikro-orm/knex';
import type { Dictionary } from '@mikro-orm/core';
import { Utils } from '@mikro-orm/core';

export class BetterSqliteConnection extends AbstractSqlConnection {

static readonly RUN_QUERY_RE = /^insert into|^update|^delete|^truncate/;

async connect(): Promise<void> {
await ensureDir(dirname(this.config.get('dbName')!));
this.getPatchedDialect();
this.client = this.createKnexClient('better-sqlite3');
await this.client.raw('pragma foreign_keys = on');
}

getDefaultClientUrl(): string {
return '';
}

getClientUrl(): string {
return '';
}

async loadFile(path: string): Promise<void> {
const conn = await this.client.client.acquireConnection();
await conn.exec((await readFile(path)).toString());
await this.client.client.releaseConnection(conn);
}

protected getKnexOptions(type: string): Knex.Config {
return Utils.merge({
client: type,
connection: {
filename: this.config.get('dbName'),
},
pool: this.config.get('pool'),
useNullAsDefault: true,
}, this.config.get('driverOptions'));
}

protected transformRawResult<T>(res: any, method: 'all' | 'get' | 'run'): T {
if (method === 'get') {
return res[0];
}

if (method === 'all') {
return res;
}

return {
insertId: res.lastID,
affectedRows: res.changes,
} as unknown as T;
}

/**
* monkey patch knex' BetterSqlite Dialect so it returns inserted id when doing raw insert query
*/
private getPatchedDialect() {
const { Sqlite3Dialect, Sqlite3DialectTableCompiler } = MonkeyPatchable;

if (Sqlite3Dialect.prototype.__patched) {
return Sqlite3Dialect;
}

const processResponse = Sqlite3Dialect.prototype.processResponse;
Sqlite3Dialect.prototype.__patched = true;
Sqlite3Dialect.prototype.processResponse = (obj: any, runner: any) => {
if (obj.method === 'raw' && obj.sql.trim().match(BetterSqliteConnection.RUN_QUERY_RE)) {
return obj.context;
}

return processResponse(obj, runner);
};

Sqlite3Dialect.prototype._query = (connection: any, obj: any) => {
const callMethod = this.getCallMethod(obj);

return new Promise((resolve: any, reject: any) => {
/* istanbul ignore if */
if (!connection || !connection[callMethod]) {
return reject(new Error(`Error calling ${callMethod} on connection.`));
}

connection[callMethod](obj.sql, obj.bindings, function (this: any, err: any, response: any) {
if (err) {
return reject(err);
}

obj.response = response;
obj.context = this;

return resolve(obj);
});
});
};

/* istanbul ignore next */
Sqlite3DialectTableCompiler.prototype.foreign = function (this: typeof Sqlite3DialectTableCompiler, foreignInfo: Dictionary) {
foreignInfo.column = this.formatter.columnize(foreignInfo.column);
foreignInfo.column = Array.isArray(foreignInfo.column)
? foreignInfo.column
: [foreignInfo.column];
foreignInfo.inTable = this.formatter.columnize(foreignInfo.inTable);
foreignInfo.references = this.formatter.columnize(foreignInfo.references);

const addColumnQuery = this.sequence.find((query: { sql: string }) => query.sql.includes(`add column ${foreignInfo.column[0]}`));

// no need for temp tables if we just add a column
if (addColumnQuery) {
const onUpdate = foreignInfo.onUpdate ? ` on update ${foreignInfo.onUpdate}` : '';
const onDelete = foreignInfo.onDelete ? ` on delete ${foreignInfo.onDelete}` : '';
addColumnQuery.sql += ` constraint ${foreignInfo.keyName} references ${foreignInfo.inTable} (${foreignInfo.references})${onUpdate}${onDelete}`;
return;
}

// eslint-disable-next-line @typescript-eslint/no-this-alias
const compiler = this;

if (this.method !== 'create' && this.method !== 'createIfNot') {
this.pushQuery({
sql: `PRAGMA table_info(${this.tableName()})`,
statementsProducer(pragma: any, connection: any) {
return compiler.client
.ddl(compiler, pragma, connection)
.foreign(foreignInfo);
},
});
}
};

return Sqlite3Dialect;
}

private getCallMethod(obj: any): string {
if (obj.method === 'raw' && obj.sql.trim().match(BetterSqliteConnection.RUN_QUERY_RE)) {
return 'run';
}

/* istanbul ignore next */
switch (obj.method) {
case 'insert':
case 'update':
case 'counter':
case 'del':
return 'run';
default:
return 'all';
}
}

}
24 changes: 24 additions & 0 deletions packages/better-sqlite/src/BetterSqliteDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { AnyEntity, Configuration, EntityDictionary, NativeInsertUpdateManyOptions, QueryResult } from '@mikro-orm/core';
import { AbstractSqlDriver } from '@mikro-orm/knex';
import { BetterSqliteConnection } from './BetterSqliteConnection';
import { BetterSqlitePlatform } from './BetterSqlitePlatform';

export class BetterSqliteDriver extends AbstractSqlDriver<BetterSqliteConnection> {

constructor(config: Configuration) {
super(config, new BetterSqlitePlatform(), BetterSqliteConnection, ['knex', 'BetterSqlite3']);
}

async nativeInsertMany<T extends AnyEntity<T>>(entityName: string, data: EntityDictionary<T>[], options: NativeInsertUpdateManyOptions<T> = {}): Promise<QueryResult<T>> {
options.processCollections ??= true;
const res = await super.nativeInsertMany(entityName, data, options);
const pks = this.getPrimaryKeyFields(entityName);
const first = res.insertId as number - data.length + 1;
res.rows ??= [];
data.forEach((item, idx) => res.rows![idx] = { [pks[0]]: item[pks[0]] ?? first + idx });
res.row = res.rows![0];

return res;
}

}
64 changes: 64 additions & 0 deletions packages/better-sqlite/src/BetterSqliteExceptionConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Dictionary, DriverException } from '@mikro-orm/core';
import {
ConnectionException, ExceptionConverter, InvalidFieldNameException, LockWaitTimeoutException, NonUniqueFieldNameException,
NotNullConstraintViolationException, ReadOnlyException, SyntaxErrorException, TableExistsException, TableNotFoundException, UniqueConstraintViolationException,
} from '@mikro-orm/core';

export class BetterSqliteExceptionConverter extends ExceptionConverter {

/* istanbul ignore next */
/**
* @inheritDoc
* @link http://www.sqlite.org/c3ref/c_abort.html
* @link https://github.com/doctrine/dbal/blob/master/src/Driver/AbstractSQLiteDriver.php
*/
convertException(exception: Error & Dictionary): DriverException {
if (exception.message.includes('database is locked')) {
return new LockWaitTimeoutException(exception);
}

if (
exception.message.includes('must be unique') ||
exception.message.includes('is not unique') ||
exception.message.includes('are not unique') ||
exception.message.includes('UNIQUE constraint failed')
) {
return new UniqueConstraintViolationException(exception);
}

if (exception.message.includes('may not be NULL') || exception.message.includes('NOT NULL constraint failed')) {
return new NotNullConstraintViolationException(exception);
}

if (exception.message.includes('no such table:')) {
return new TableNotFoundException(exception);
}

if (exception.message.includes('already exists')) {
return new TableExistsException(exception);
}

if (exception.message.includes('no such column:')) {
return new InvalidFieldNameException(exception);
}

if (exception.message.includes('ambiguous column name')) {
return new NonUniqueFieldNameException(exception);
}

if (exception.message.includes('syntax error')) {
return new SyntaxErrorException(exception);
}

if (exception.message.includes('attempt to write a readonly database')) {
return new ReadOnlyException(exception);
}

if (exception.message.includes('unable to open database file')) {
return new ConnectionException(exception);
}

return super.convertException(exception);
}

}
Loading

0 comments on commit 1b39d66

Please sign in to comment.