@zhin.js/database 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1360 -34
- package/lib/base/database.d.ts +71 -13
- package/lib/base/database.d.ts.map +1 -1
- package/lib/base/database.js +128 -4
- package/lib/base/database.js.map +1 -1
- package/lib/base/dialect.d.ts +27 -10
- package/lib/base/dialect.d.ts.map +1 -1
- package/lib/base/dialect.js +32 -0
- package/lib/base/dialect.js.map +1 -1
- package/lib/base/index.d.ts +1 -0
- package/lib/base/index.d.ts.map +1 -1
- package/lib/base/index.js +1 -0
- package/lib/base/index.js.map +1 -1
- package/lib/base/model.d.ts +105 -12
- package/lib/base/model.d.ts.map +1 -1
- package/lib/base/model.js +224 -3
- package/lib/base/model.js.map +1 -1
- package/lib/base/query-classes.d.ts +204 -33
- package/lib/base/query-classes.d.ts.map +1 -1
- package/lib/base/query-classes.js +276 -0
- package/lib/base/query-classes.js.map +1 -1
- package/lib/base/thenable.d.ts +7 -7
- package/lib/base/thenable.d.ts.map +1 -1
- package/lib/base/thenable.js +5 -4
- package/lib/base/thenable.js.map +1 -1
- package/lib/base/transaction.d.ts +46 -0
- package/lib/base/transaction.d.ts.map +1 -0
- package/lib/base/transaction.js +186 -0
- package/lib/base/transaction.js.map +1 -0
- package/lib/dialects/memory.d.ts +13 -8
- package/lib/dialects/memory.d.ts.map +1 -1
- package/lib/dialects/memory.js +10 -7
- package/lib/dialects/memory.js.map +1 -1
- package/lib/dialects/mongodb.d.ts +12 -8
- package/lib/dialects/mongodb.d.ts.map +1 -1
- package/lib/dialects/mongodb.js +21 -18
- package/lib/dialects/mongodb.js.map +1 -1
- package/lib/dialects/mysql.d.ts +36 -7
- package/lib/dialects/mysql.d.ts.map +1 -1
- package/lib/dialects/mysql.js +140 -21
- package/lib/dialects/mysql.js.map +1 -1
- package/lib/dialects/pg.d.ts +36 -7
- package/lib/dialects/pg.d.ts.map +1 -1
- package/lib/dialects/pg.js +140 -21
- package/lib/dialects/pg.js.map +1 -1
- package/lib/dialects/redis.d.ts +13 -8
- package/lib/dialects/redis.d.ts.map +1 -1
- package/lib/dialects/redis.js +14 -11
- package/lib/dialects/redis.js.map +1 -1
- package/lib/dialects/sqlite.d.ts +20 -7
- package/lib/dialects/sqlite.d.ts.map +1 -1
- package/lib/dialects/sqlite.js +66 -13
- package/lib/dialects/sqlite.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/migration.d.ts +132 -0
- package/lib/migration.d.ts.map +1 -0
- package/lib/migration.js +475 -0
- package/lib/migration.js.map +1 -0
- package/lib/registry.d.ts +26 -23
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +1 -5
- package/lib/registry.js.map +1 -1
- package/lib/type/document/database.d.ts +12 -12
- package/lib/type/document/database.d.ts.map +1 -1
- package/lib/type/document/database.js +1 -1
- package/lib/type/document/database.js.map +1 -1
- package/lib/type/document/model.d.ts +8 -8
- package/lib/type/document/model.d.ts.map +1 -1
- package/lib/type/document/model.js +1 -1
- package/lib/type/document/model.js.map +1 -1
- package/lib/type/keyvalue/database.d.ts +12 -12
- package/lib/type/keyvalue/database.d.ts.map +1 -1
- package/lib/type/keyvalue/database.js +1 -1
- package/lib/type/keyvalue/database.js.map +1 -1
- package/lib/type/keyvalue/model.d.ts +3 -3
- package/lib/type/keyvalue/model.d.ts.map +1 -1
- package/lib/type/keyvalue/model.js +1 -1
- package/lib/type/keyvalue/model.js.map +1 -1
- package/lib/type/related/database.d.ts +49 -14
- package/lib/type/related/database.d.ts.map +1 -1
- package/lib/type/related/database.js +259 -28
- package/lib/type/related/database.js.map +1 -1
- package/lib/type/related/model.d.ts +252 -16
- package/lib/type/related/model.d.ts.map +1 -1
- package/lib/type/related/model.js +648 -23
- package/lib/type/related/model.js.map +1 -1
- package/lib/types.d.ts +475 -37
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +6 -0
- package/lib/types.js.map +1 -1
- package/package.json +10 -5
- package/src/base/database.ts +168 -24
- package/src/base/dialect.ts +49 -10
- package/src/base/index.ts +2 -1
- package/src/base/model.ts +258 -18
- package/src/base/query-classes.ts +471 -63
- package/src/base/thenable.ts +12 -11
- package/src/base/transaction.ts +213 -0
- package/src/dialects/memory.ts +17 -16
- package/src/dialects/mongodb.ts +44 -42
- package/src/dialects/mysql.ts +155 -26
- package/src/dialects/pg.ts +152 -25
- package/src/dialects/redis.ts +45 -43
- package/src/dialects/sqlite.ts +77 -19
- package/src/index.ts +1 -2
- package/src/migration.ts +544 -0
- package/src/registry.ts +33 -33
- package/src/type/document/database.ts +33 -33
- package/src/type/document/model.ts +15 -15
- package/src/type/keyvalue/database.ts +33 -33
- package/src/type/keyvalue/model.ts +19 -19
- package/src/type/related/database.ts +309 -34
- package/src/type/related/model.ts +801 -34
- package/src/types.ts +559 -44
- package/tests/database.test.ts +1738 -0
package/src/dialects/sqlite.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
-
import {Dialect} from "../base";
|
|
3
|
-
import {Registry} from "../registry";
|
|
4
|
-
import {Database} from "../base";
|
|
5
|
-
import {Column} from "../types";
|
|
6
|
-
import {RelatedDatabase} from "../type/related/database";
|
|
2
|
+
import {Dialect} from "../base/index.js";
|
|
3
|
+
import {Registry} from "../registry.js";
|
|
4
|
+
import {Database} from "../base/index.js";
|
|
5
|
+
import {Column, Transaction, TransactionOptions} from "../types.js";
|
|
6
|
+
import {RelatedDatabase} from "../type/related/database.js";
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@ export interface SQLiteDialectConfig {
|
|
|
12
12
|
mode?:string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export class SQLiteDialect extends Dialect<SQLiteDialectConfig, string> {
|
|
15
|
+
export class SQLiteDialect<S extends Record<string, object> = Record<string, object>> extends Dialect<SQLiteDialectConfig, S, string> {
|
|
16
16
|
private db: any = null;
|
|
17
17
|
|
|
18
18
|
constructor(config: SQLiteDialectConfig) {
|
|
@@ -40,7 +40,18 @@ export class SQLiteDialect extends Dialect<SQLiteDialectConfig, string> {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async disconnect(): Promise<void> {
|
|
43
|
+
if (this.db) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
this.db.close((err: any) => {
|
|
46
|
+
if (err) {
|
|
47
|
+
reject(err);
|
|
48
|
+
} else {
|
|
43
49
|
this.db = null;
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
async healthCheck(): Promise<boolean> {
|
|
@@ -65,17 +76,26 @@ export class SQLiteDialect extends Dialect<SQLiteDialectConfig, string> {
|
|
|
65
76
|
}
|
|
66
77
|
});
|
|
67
78
|
}
|
|
68
|
-
// INSERT
|
|
69
|
-
else if (isInsertQuery
|
|
79
|
+
// INSERT 使用 db.run 执行,返回 { lastID, changes }
|
|
80
|
+
else if (isInsertQuery) {
|
|
70
81
|
this.db.run(sql, params, function(this: any, err: any) {
|
|
71
82
|
if (err) {
|
|
72
83
|
reject(err);
|
|
73
84
|
} else {
|
|
74
|
-
// 返回插入的行 ID 和影响的行数
|
|
75
85
|
resolve({ lastID: this.lastID, changes: this.changes } as U);
|
|
76
86
|
}
|
|
77
87
|
});
|
|
78
88
|
}
|
|
89
|
+
// UPDATE/DELETE 返回受影响的行数
|
|
90
|
+
else if (trimmedSql.startsWith('update') || trimmedSql.startsWith('delete')) {
|
|
91
|
+
this.db.run(sql, params, function(this: any, err: any) {
|
|
92
|
+
if (err) {
|
|
93
|
+
reject(err);
|
|
94
|
+
} else {
|
|
95
|
+
resolve(this.changes as U);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
79
99
|
// 其他查询(CREATE TABLE, ALTER TABLE 等)使用 db.run
|
|
80
100
|
else {
|
|
81
101
|
this.db.run(sql, params, (err: any) => {
|
|
@@ -222,8 +242,8 @@ export class SQLiteDialect extends Dialect<SQLiteDialectConfig, string> {
|
|
|
222
242
|
return `LIMIT ${limit} OFFSET ${offset}`;
|
|
223
243
|
}
|
|
224
244
|
|
|
225
|
-
formatCreateTable(tableName:
|
|
226
|
-
return `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(tableName)} (${columns.join(', ')})`;
|
|
245
|
+
formatCreateTable<T extends keyof S>(tableName: T, columns: string[]): string {
|
|
246
|
+
return `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(String(tableName))} (${columns.join(', ')})`;
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
formatColumnDefinition(field: string, column: Column<any>): string {
|
|
@@ -240,20 +260,58 @@ export class SQLiteDialect extends Dialect<SQLiteDialectConfig, string> {
|
|
|
240
260
|
return `${name} ${type}${length}${primary}${unique}${nullable}${defaultVal}`;
|
|
241
261
|
}
|
|
242
262
|
|
|
243
|
-
formatAlterTable(tableName:
|
|
244
|
-
return `ALTER TABLE ${this.quoteIdentifier(tableName)} ${alterations.join(', ')}`;
|
|
263
|
+
formatAlterTable<T extends keyof S>(tableName: T, alterations: string[]): string {
|
|
264
|
+
return `ALTER TABLE ${this.quoteIdentifier(String(tableName))} ${alterations.join(', ')}`;
|
|
245
265
|
}
|
|
246
266
|
|
|
247
|
-
formatDropTable(tableName:
|
|
267
|
+
formatDropTable<T extends keyof S>(tableName: T, ifExists?: boolean): string {
|
|
248
268
|
const ifExistsClause = ifExists ? 'IF EXISTS ' : '';
|
|
249
|
-
return `DROP TABLE ${ifExistsClause}${this.quoteIdentifier(tableName)}`;
|
|
269
|
+
return `DROP TABLE ${ifExistsClause}${this.quoteIdentifier(String(tableName))}`;
|
|
250
270
|
}
|
|
251
271
|
|
|
252
|
-
formatDropIndex(indexName: string, tableName:
|
|
272
|
+
formatDropIndex<T extends keyof S>(indexName: string, tableName: T, ifExists?: boolean): string {
|
|
253
273
|
const ifExistsClause = ifExists ? 'IF EXISTS ' : '';
|
|
254
274
|
return `DROP INDEX ${ifExistsClause}${this.quoteIdentifier(indexName)}`;
|
|
255
275
|
}
|
|
276
|
+
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// Transaction Support
|
|
279
|
+
// ============================================================================
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* SQLite 支持事务
|
|
283
|
+
*/
|
|
284
|
+
supportsTransactions(): boolean {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 开始事务
|
|
290
|
+
*/
|
|
291
|
+
async beginTransaction(options?: TransactionOptions): Promise<Transaction> {
|
|
292
|
+
const dialect = this;
|
|
293
|
+
|
|
294
|
+
// 开始事务
|
|
295
|
+
await this.query('BEGIN TRANSACTION');
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
async commit(): Promise<void> {
|
|
299
|
+
await dialect.query('COMMIT');
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
async rollback(): Promise<void> {
|
|
303
|
+
await dialect.query('ROLLBACK');
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
async query<T = any>(sql: string, params?: any[]): Promise<T> {
|
|
307
|
+
return dialect.query<T>(sql, params);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export class Sqlite<S extends Record<string, object> = Record<string, object>> extends RelatedDatabase<SQLiteDialectConfig, S> {
|
|
313
|
+
constructor(config: SQLiteDialectConfig, definitions?: Database.DefinitionObj<S>) {
|
|
314
|
+
super(new SQLiteDialect<S>(config), definitions);
|
|
315
|
+
}
|
|
256
316
|
}
|
|
257
|
-
Registry.register('sqlite',
|
|
258
|
-
return new RelatedDatabase(new SQLiteDialect(config), definitions);
|
|
259
|
-
});
|
|
317
|
+
Registry.register('sqlite', Sqlite);
|
package/src/index.ts
CHANGED
package/src/migration.ts
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Migration,
|
|
3
|
+
MigrationContext,
|
|
4
|
+
MigrationRecord,
|
|
5
|
+
MigrationStatus,
|
|
6
|
+
MigrationRunnerConfig,
|
|
7
|
+
MigrationOperation,
|
|
8
|
+
Column
|
|
9
|
+
} from './types.js';
|
|
10
|
+
import { RelatedDatabase } from './type/related/database.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 记录型迁移上下文
|
|
14
|
+
* 在执行操作的同时记录所有操作,用于自动生成 down
|
|
15
|
+
*/
|
|
16
|
+
class RecordingMigrationContext implements MigrationContext {
|
|
17
|
+
private _operations: MigrationOperation[] = [];
|
|
18
|
+
|
|
19
|
+
constructor(private readonly baseContext: MigrationContext) {}
|
|
20
|
+
|
|
21
|
+
get operations(): MigrationOperation[] {
|
|
22
|
+
return this._operations;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async createTable(tableName: string, columns: Record<string, Column>): Promise<void> {
|
|
26
|
+
this._operations.push({ type: 'createTable', tableName, columns });
|
|
27
|
+
await this.baseContext.createTable(tableName, columns);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async dropTable(tableName: string): Promise<void> {
|
|
31
|
+
this._operations.push({ type: 'dropTable', tableName });
|
|
32
|
+
await this.baseContext.dropTable(tableName);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async addColumn(tableName: string, columnName: string, column: Column): Promise<void> {
|
|
36
|
+
this._operations.push({ type: 'addColumn', tableName, columnName, column });
|
|
37
|
+
await this.baseContext.addColumn(tableName, columnName, column);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async dropColumn(tableName: string, columnName: string): Promise<void> {
|
|
41
|
+
this._operations.push({ type: 'dropColumn', tableName, columnName });
|
|
42
|
+
await this.baseContext.dropColumn(tableName, columnName);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async modifyColumn(tableName: string, columnName: string, column: Column): Promise<void> {
|
|
46
|
+
// modifyColumn 不能自动反向,需要原始列定义
|
|
47
|
+
await this.baseContext.modifyColumn(tableName, columnName, column);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async renameColumn(tableName: string, oldName: string, newName: string): Promise<void> {
|
|
51
|
+
this._operations.push({ type: 'renameColumn', tableName, oldName, newName });
|
|
52
|
+
await this.baseContext.renameColumn(tableName, oldName, newName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async addIndex(tableName: string, indexName: string, columns: string[], unique?: boolean): Promise<void> {
|
|
56
|
+
this._operations.push({ type: 'addIndex', tableName, indexName, columns, unique });
|
|
57
|
+
await this.baseContext.addIndex(tableName, indexName, columns, unique);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async dropIndex(tableName: string, indexName: string): Promise<void> {
|
|
61
|
+
this._operations.push({ type: 'dropIndex', tableName, indexName });
|
|
62
|
+
await this.baseContext.dropIndex(tableName, indexName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async query<T = any>(sql: string, params?: any[]): Promise<T> {
|
|
66
|
+
// query 操作记录但无法自动反向
|
|
67
|
+
this._operations.push({ type: 'query', sql, params });
|
|
68
|
+
return this.baseContext.query<T>(sql, params);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 根据 up 操作自动生成 down 操作
|
|
74
|
+
*/
|
|
75
|
+
function generateReverseOperations(operations: MigrationOperation[]): MigrationOperation[] {
|
|
76
|
+
const reversed: MigrationOperation[] = [];
|
|
77
|
+
|
|
78
|
+
// 反向遍历操作列表
|
|
79
|
+
for (let i = operations.length - 1; i >= 0; i--) {
|
|
80
|
+
const op = operations[i];
|
|
81
|
+
|
|
82
|
+
switch (op.type) {
|
|
83
|
+
case 'createTable':
|
|
84
|
+
// createTable -> dropTable
|
|
85
|
+
reversed.push({ type: 'dropTable', tableName: op.tableName });
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'dropTable':
|
|
89
|
+
// dropTable 无法自动反向(需要原始表结构)
|
|
90
|
+
throw new Error(`Cannot auto-reverse 'dropTable("${op.tableName}")'. Please provide explicit 'down' function.`);
|
|
91
|
+
|
|
92
|
+
case 'addColumn':
|
|
93
|
+
// addColumn -> dropColumn
|
|
94
|
+
reversed.push({ type: 'dropColumn', tableName: op.tableName, columnName: op.columnName });
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case 'dropColumn':
|
|
98
|
+
// dropColumn 无法自动反向(需要原始列定义)
|
|
99
|
+
throw new Error(`Cannot auto-reverse 'dropColumn("${op.tableName}", "${op.columnName}")'. Please provide explicit 'down' function.`);
|
|
100
|
+
|
|
101
|
+
case 'addIndex':
|
|
102
|
+
// addIndex -> dropIndex
|
|
103
|
+
reversed.push({ type: 'dropIndex', tableName: op.tableName, indexName: op.indexName });
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'dropIndex':
|
|
107
|
+
// dropIndex 无法自动反向(需要原始索引定义)
|
|
108
|
+
throw new Error(`Cannot auto-reverse 'dropIndex("${op.tableName}", "${op.indexName}")'. Please provide explicit 'down' function.`);
|
|
109
|
+
|
|
110
|
+
case 'renameColumn':
|
|
111
|
+
// renameColumn -> renameColumn (反向)
|
|
112
|
+
reversed.push({ type: 'renameColumn', tableName: op.tableName, oldName: op.newName, newName: op.oldName });
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'query':
|
|
116
|
+
// query 无法自动反向
|
|
117
|
+
throw new Error(`Cannot auto-reverse raw query. Please provide explicit 'down' function.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return reversed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 执行反向操作
|
|
126
|
+
*/
|
|
127
|
+
async function executeReverseOperations(context: MigrationContext, operations: MigrationOperation[]): Promise<void> {
|
|
128
|
+
for (const op of operations) {
|
|
129
|
+
switch (op.type) {
|
|
130
|
+
case 'dropTable':
|
|
131
|
+
await context.dropTable(op.tableName);
|
|
132
|
+
break;
|
|
133
|
+
case 'dropColumn':
|
|
134
|
+
await context.dropColumn(op.tableName, op.columnName);
|
|
135
|
+
break;
|
|
136
|
+
case 'dropIndex':
|
|
137
|
+
await context.dropIndex(op.tableName, op.indexName);
|
|
138
|
+
break;
|
|
139
|
+
case 'renameColumn':
|
|
140
|
+
await context.renameColumn(op.tableName, op.oldName, op.newName);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 迁移运行器
|
|
148
|
+
* 管理数据库迁移的执行、回滚和状态跟踪
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* const runner = new MigrationRunner(db);
|
|
153
|
+
*
|
|
154
|
+
* // 添加迁移
|
|
155
|
+
* runner.add({
|
|
156
|
+
* name: '001_create_users',
|
|
157
|
+
* up: async (ctx) => {
|
|
158
|
+
* await ctx.createTable('users', {
|
|
159
|
+
* id: { type: 'integer', primary: true, autoIncrement: true },
|
|
160
|
+
* name: { type: 'text', nullable: false }
|
|
161
|
+
* });
|
|
162
|
+
* },
|
|
163
|
+
* down: async (ctx) => {
|
|
164
|
+
* await ctx.dropTable('users');
|
|
165
|
+
* }
|
|
166
|
+
* });
|
|
167
|
+
*
|
|
168
|
+
* // 运行所有待执行的迁移
|
|
169
|
+
* await runner.migrate();
|
|
170
|
+
*
|
|
171
|
+
* // 回滚最后一批迁移
|
|
172
|
+
* await runner.rollback();
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export class MigrationRunner<D = any, S extends Record<string, object> = Record<string, object>> {
|
|
176
|
+
private migrations: Migration[] = [];
|
|
177
|
+
private tableName: string;
|
|
178
|
+
private currentBatch: number = 0;
|
|
179
|
+
|
|
180
|
+
constructor(
|
|
181
|
+
private readonly database: RelatedDatabase<D, S>,
|
|
182
|
+
config?: MigrationRunnerConfig
|
|
183
|
+
) {
|
|
184
|
+
this.tableName = config?.tableName || '_migrations';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 添加迁移
|
|
189
|
+
*/
|
|
190
|
+
add(migration: Migration): this {
|
|
191
|
+
this.migrations.push(migration);
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 批量添加迁移
|
|
197
|
+
*/
|
|
198
|
+
addAll(migrations: Migration[]): this {
|
|
199
|
+
this.migrations.push(...migrations);
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 初始化迁移表
|
|
205
|
+
*/
|
|
206
|
+
private async ensureMigrationTable(): Promise<void> {
|
|
207
|
+
const sql = `
|
|
208
|
+
CREATE TABLE IF NOT EXISTS "${this.tableName}" (
|
|
209
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
210
|
+
name TEXT NOT NULL UNIQUE,
|
|
211
|
+
batch INTEGER NOT NULL,
|
|
212
|
+
operations TEXT,
|
|
213
|
+
executed_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
214
|
+
)
|
|
215
|
+
`;
|
|
216
|
+
await this.database.query(sql);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 获取已执行的迁移
|
|
221
|
+
*/
|
|
222
|
+
private async getExecutedMigrations(): Promise<MigrationRecord[]> {
|
|
223
|
+
const sql = `SELECT id, name, batch, executed_at as executedAt FROM "${this.tableName}" ORDER BY id ASC`;
|
|
224
|
+
return this.database.query<MigrationRecord[]>(sql);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 获取当前批次号
|
|
229
|
+
*/
|
|
230
|
+
private async getCurrentBatch(): Promise<number> {
|
|
231
|
+
const sql = `SELECT MAX(batch) as maxBatch FROM "${this.tableName}"`;
|
|
232
|
+
const result = await this.database.query<{ maxBatch: number | null }[]>(sql);
|
|
233
|
+
return result[0]?.maxBatch || 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 记录迁移执行
|
|
238
|
+
*/
|
|
239
|
+
private async recordMigration(name: string, batch: number, operations?: MigrationOperation[]): Promise<void> {
|
|
240
|
+
const operationsJson = operations ? JSON.stringify(operations) : null;
|
|
241
|
+
const sql = `INSERT INTO "${this.tableName}" (name, batch, operations) VALUES (?, ?, ?)`;
|
|
242
|
+
await this.database.query(sql, [name, batch, operationsJson]);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 获取迁移记录的操作
|
|
247
|
+
*/
|
|
248
|
+
private async getMigrationOperations(name: string): Promise<MigrationOperation[] | null> {
|
|
249
|
+
const sql = `SELECT operations FROM "${this.tableName}" WHERE name = ?`;
|
|
250
|
+
const result = await this.database.query<{ operations: string | null }[]>(sql, [name]);
|
|
251
|
+
if (result.length === 0 || !result[0].operations) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return JSON.parse(result[0].operations);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 删除迁移记录
|
|
259
|
+
*/
|
|
260
|
+
private async removeMigrationRecord(name: string): Promise<void> {
|
|
261
|
+
const sql = `DELETE FROM "${this.tableName}" WHERE name = ?`;
|
|
262
|
+
await this.database.query(sql, [name]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 创建迁移上下文
|
|
267
|
+
*/
|
|
268
|
+
private createContext(): MigrationContext {
|
|
269
|
+
const db = this.database;
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
async createTable(tableName: string, columns: Record<string, Column>): Promise<void> {
|
|
273
|
+
const columnDefs = Object.entries(columns).map(([name, col]) => {
|
|
274
|
+
let def = `"${name}" ${mapColumnType(col.type)}`;
|
|
275
|
+
if (col.primary) def += ' PRIMARY KEY';
|
|
276
|
+
if (col.autoIncrement) def += ' AUTOINCREMENT';
|
|
277
|
+
if (col.unique) def += ' UNIQUE';
|
|
278
|
+
if (!col.nullable) def += ' NOT NULL';
|
|
279
|
+
if (col.default !== undefined) def += ` DEFAULT ${formatDefault(col.default)}`;
|
|
280
|
+
return def;
|
|
281
|
+
}).join(', ');
|
|
282
|
+
|
|
283
|
+
const sql = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs})`;
|
|
284
|
+
await db.query(sql);
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
async dropTable(tableName: string): Promise<void> {
|
|
288
|
+
const sql = `DROP TABLE IF EXISTS "${tableName}"`;
|
|
289
|
+
await db.query(sql);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async addColumn(tableName: string, columnName: string, column: Column): Promise<void> {
|
|
293
|
+
let def = `"${columnName}" ${mapColumnType(column.type)}`;
|
|
294
|
+
if (column.unique) def += ' UNIQUE';
|
|
295
|
+
if (!column.nullable) def += ' NOT NULL';
|
|
296
|
+
if (column.default !== undefined) def += ` DEFAULT ${formatDefault(column.default)}`;
|
|
297
|
+
|
|
298
|
+
const sql = `ALTER TABLE "${tableName}" ADD COLUMN ${def}`;
|
|
299
|
+
await db.query(sql);
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
async dropColumn(tableName: string, columnName: string): Promise<void> {
|
|
303
|
+
const sql = `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}"`;
|
|
304
|
+
await db.query(sql);
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
async modifyColumn(tableName: string, columnName: string, column: Column): Promise<void> {
|
|
308
|
+
// 注意:SQLite 不支持直接 MODIFY COLUMN,需要重建表
|
|
309
|
+
// 这里提供一个简化的实现,完整实现需要更复杂的逻辑
|
|
310
|
+
throw new Error('modifyColumn is not supported in SQLite. Consider recreating the table.');
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
async renameColumn(tableName: string, oldName: string, newName: string): Promise<void> {
|
|
314
|
+
const sql = `ALTER TABLE "${tableName}" RENAME COLUMN "${oldName}" TO "${newName}"`;
|
|
315
|
+
await db.query(sql);
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
async addIndex(tableName: string, indexName: string, columns: string[], unique = false): Promise<void> {
|
|
319
|
+
const uniqueStr = unique ? 'UNIQUE ' : '';
|
|
320
|
+
const colStr = columns.map(c => `"${c}"`).join(', ');
|
|
321
|
+
const sql = `CREATE ${uniqueStr}INDEX IF NOT EXISTS "${indexName}" ON "${tableName}" (${colStr})`;
|
|
322
|
+
await db.query(sql);
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
async dropIndex(tableName: string, indexName: string): Promise<void> {
|
|
326
|
+
const sql = `DROP INDEX IF EXISTS "${indexName}"`;
|
|
327
|
+
await db.query(sql);
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
async query<T = any>(sql: string, params?: any[]): Promise<T> {
|
|
331
|
+
return db.query<T>(sql, params);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 获取迁移状态
|
|
338
|
+
*/
|
|
339
|
+
async status(): Promise<MigrationStatus[]> {
|
|
340
|
+
await this.ensureMigrationTable();
|
|
341
|
+
const executed = await this.getExecutedMigrations();
|
|
342
|
+
const executedNames = new Set(executed.map(m => m.name));
|
|
343
|
+
|
|
344
|
+
const result: MigrationStatus[] = [];
|
|
345
|
+
|
|
346
|
+
// 已执行的迁移
|
|
347
|
+
for (const record of executed) {
|
|
348
|
+
result.push({
|
|
349
|
+
name: record.name,
|
|
350
|
+
status: 'executed',
|
|
351
|
+
batch: record.batch,
|
|
352
|
+
executedAt: record.executedAt
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 待执行的迁移
|
|
357
|
+
for (const migration of this.migrations) {
|
|
358
|
+
if (!executedNames.has(migration.name)) {
|
|
359
|
+
result.push({
|
|
360
|
+
name: migration.name,
|
|
361
|
+
status: 'pending'
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 运行所有待执行的迁移
|
|
371
|
+
*/
|
|
372
|
+
async migrate(): Promise<string[]> {
|
|
373
|
+
await this.ensureMigrationTable();
|
|
374
|
+
const executed = await this.getExecutedMigrations();
|
|
375
|
+
const executedNames = new Set(executed.map(m => m.name));
|
|
376
|
+
|
|
377
|
+
const pending = this.migrations.filter(m => !executedNames.has(m.name));
|
|
378
|
+
|
|
379
|
+
if (pending.length === 0) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const batch = (await this.getCurrentBatch()) + 1;
|
|
384
|
+
const baseContext = this.createContext();
|
|
385
|
+
const migrated: string[] = [];
|
|
386
|
+
|
|
387
|
+
for (const migration of pending) {
|
|
388
|
+
try {
|
|
389
|
+
// 如果没有显式的 down,使用记录型上下文来记录操作
|
|
390
|
+
if (!migration.down) {
|
|
391
|
+
const recordingContext = new RecordingMigrationContext(baseContext);
|
|
392
|
+
await migration.up(recordingContext);
|
|
393
|
+
// 验证操作可以反向(提前检查)
|
|
394
|
+
generateReverseOperations(recordingContext.operations);
|
|
395
|
+
await this.recordMigration(migration.name, batch, recordingContext.operations);
|
|
396
|
+
} else {
|
|
397
|
+
await migration.up(baseContext);
|
|
398
|
+
await this.recordMigration(migration.name, batch);
|
|
399
|
+
}
|
|
400
|
+
migrated.push(migration.name);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
// 迁移失败,抛出错误
|
|
403
|
+
throw new Error(`Migration "${migration.name}" failed: ${(error as Error).message}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return migrated;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 回滚最后一批迁移
|
|
412
|
+
*/
|
|
413
|
+
async rollback(): Promise<string[]> {
|
|
414
|
+
await this.ensureMigrationTable();
|
|
415
|
+
const currentBatch = await this.getCurrentBatch();
|
|
416
|
+
|
|
417
|
+
if (currentBatch === 0) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 获取最后一批的迁移
|
|
422
|
+
const sql = `SELECT name FROM "${this.tableName}" WHERE batch = ? ORDER BY id DESC`;
|
|
423
|
+
const lastBatch = await this.database.query<{ name: string }[]>(sql, [currentBatch]);
|
|
424
|
+
|
|
425
|
+
const context = this.createContext();
|
|
426
|
+
const rolledBack: string[] = [];
|
|
427
|
+
|
|
428
|
+
for (const record of lastBatch) {
|
|
429
|
+
const migration = this.migrations.find(m => m.name === record.name);
|
|
430
|
+
if (!migration) {
|
|
431
|
+
throw new Error(`Migration "${record.name}" not found in registered migrations`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
if (migration.down) {
|
|
436
|
+
// 使用显式的 down 函数
|
|
437
|
+
await migration.down(context);
|
|
438
|
+
} else {
|
|
439
|
+
// 自动生成并执行反向操作
|
|
440
|
+
const operations = await this.getMigrationOperations(record.name);
|
|
441
|
+
if (!operations) {
|
|
442
|
+
throw new Error(`No recorded operations found for migration "${record.name}". Cannot auto-rollback.`);
|
|
443
|
+
}
|
|
444
|
+
const reverseOps = generateReverseOperations(operations);
|
|
445
|
+
await executeReverseOperations(context, reverseOps);
|
|
446
|
+
}
|
|
447
|
+
await this.removeMigrationRecord(record.name);
|
|
448
|
+
rolledBack.push(record.name);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
throw new Error(`Rollback "${record.name}" failed: ${(error as Error).message}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return rolledBack;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 回滚所有迁移
|
|
459
|
+
*/
|
|
460
|
+
async reset(): Promise<string[]> {
|
|
461
|
+
const allRolledBack: string[] = [];
|
|
462
|
+
|
|
463
|
+
let batch = await this.getCurrentBatch();
|
|
464
|
+
while (batch > 0) {
|
|
465
|
+
const rolledBack = await this.rollback();
|
|
466
|
+
allRolledBack.push(...rolledBack);
|
|
467
|
+
batch = await this.getCurrentBatch();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return allRolledBack;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 重新运行所有迁移(reset + migrate)
|
|
475
|
+
*/
|
|
476
|
+
async refresh(): Promise<{ rolledBack: string[]; migrated: string[] }> {
|
|
477
|
+
const rolledBack = await this.reset();
|
|
478
|
+
const migrated = await this.migrate();
|
|
479
|
+
return { rolledBack, migrated };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 映射列类型到 SQL 类型
|
|
485
|
+
*/
|
|
486
|
+
function mapColumnType(type: string): string {
|
|
487
|
+
const typeMap: Record<string, string> = {
|
|
488
|
+
'text': 'TEXT',
|
|
489
|
+
'integer': 'INTEGER',
|
|
490
|
+
'float': 'REAL',
|
|
491
|
+
'boolean': 'INTEGER',
|
|
492
|
+
'date': 'DATETIME',
|
|
493
|
+
'json': 'TEXT'
|
|
494
|
+
};
|
|
495
|
+
return typeMap[type] || type.toUpperCase();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* 格式化默认值
|
|
500
|
+
*/
|
|
501
|
+
function formatDefault(value: any): string {
|
|
502
|
+
if (value === null) return 'NULL';
|
|
503
|
+
if (typeof value === 'string') return `'${value}'`;
|
|
504
|
+
if (typeof value === 'boolean') return value ? '1' : '0';
|
|
505
|
+
return String(value);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 创建迁移辅助函数
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* // 只需要定义 up,down 会自动生成
|
|
513
|
+
* defineMigration({
|
|
514
|
+
* name: '001_create_users',
|
|
515
|
+
* up: async (ctx) => {
|
|
516
|
+
* await ctx.createTable('users', {
|
|
517
|
+
* id: { type: 'integer', primary: true, autoIncrement: true },
|
|
518
|
+
* name: { type: 'text', nullable: false }
|
|
519
|
+
* });
|
|
520
|
+
* await ctx.addIndex('users', 'idx_name', ['name']);
|
|
521
|
+
* }
|
|
522
|
+
* // down 自动生成: dropIndex + dropTable
|
|
523
|
+
* });
|
|
524
|
+
*
|
|
525
|
+
* // 如果需要自定义 down,可以显式提供
|
|
526
|
+
* defineMigration({
|
|
527
|
+
* name: '002_custom_migration',
|
|
528
|
+
* up: async (ctx) => {
|
|
529
|
+
* await ctx.query('INSERT INTO settings VALUES (?, ?)', ['key', 'value']);
|
|
530
|
+
* },
|
|
531
|
+
* down: async (ctx) => {
|
|
532
|
+
* await ctx.query('DELETE FROM settings WHERE key = ?', ['key']);
|
|
533
|
+
* }
|
|
534
|
+
* });
|
|
535
|
+
*/
|
|
536
|
+
export function defineMigration(config: {
|
|
537
|
+
name: string;
|
|
538
|
+
version?: string | number;
|
|
539
|
+
up: (context: MigrationContext) => Promise<void>;
|
|
540
|
+
down?: (context: MigrationContext) => Promise<void>;
|
|
541
|
+
}): Migration {
|
|
542
|
+
return config as Migration;
|
|
543
|
+
}
|
|
544
|
+
|