-
-
Notifications
You must be signed in to change notification settings - Fork 499
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add better-sqlite driver (#2792)
- Loading branch information
Showing
20 changed files
with
703 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
64
packages/better-sqlite/src/BetterSqliteExceptionConverter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
Oops, something went wrong.