@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.
Files changed (119) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1360 -34
  3. package/lib/base/database.d.ts +71 -13
  4. package/lib/base/database.d.ts.map +1 -1
  5. package/lib/base/database.js +128 -4
  6. package/lib/base/database.js.map +1 -1
  7. package/lib/base/dialect.d.ts +27 -10
  8. package/lib/base/dialect.d.ts.map +1 -1
  9. package/lib/base/dialect.js +32 -0
  10. package/lib/base/dialect.js.map +1 -1
  11. package/lib/base/index.d.ts +1 -0
  12. package/lib/base/index.d.ts.map +1 -1
  13. package/lib/base/index.js +1 -0
  14. package/lib/base/index.js.map +1 -1
  15. package/lib/base/model.d.ts +105 -12
  16. package/lib/base/model.d.ts.map +1 -1
  17. package/lib/base/model.js +224 -3
  18. package/lib/base/model.js.map +1 -1
  19. package/lib/base/query-classes.d.ts +204 -33
  20. package/lib/base/query-classes.d.ts.map +1 -1
  21. package/lib/base/query-classes.js +276 -0
  22. package/lib/base/query-classes.js.map +1 -1
  23. package/lib/base/thenable.d.ts +7 -7
  24. package/lib/base/thenable.d.ts.map +1 -1
  25. package/lib/base/thenable.js +5 -4
  26. package/lib/base/thenable.js.map +1 -1
  27. package/lib/base/transaction.d.ts +46 -0
  28. package/lib/base/transaction.d.ts.map +1 -0
  29. package/lib/base/transaction.js +186 -0
  30. package/lib/base/transaction.js.map +1 -0
  31. package/lib/dialects/memory.d.ts +13 -8
  32. package/lib/dialects/memory.d.ts.map +1 -1
  33. package/lib/dialects/memory.js +10 -7
  34. package/lib/dialects/memory.js.map +1 -1
  35. package/lib/dialects/mongodb.d.ts +12 -8
  36. package/lib/dialects/mongodb.d.ts.map +1 -1
  37. package/lib/dialects/mongodb.js +21 -18
  38. package/lib/dialects/mongodb.js.map +1 -1
  39. package/lib/dialects/mysql.d.ts +36 -7
  40. package/lib/dialects/mysql.d.ts.map +1 -1
  41. package/lib/dialects/mysql.js +140 -21
  42. package/lib/dialects/mysql.js.map +1 -1
  43. package/lib/dialects/pg.d.ts +36 -7
  44. package/lib/dialects/pg.d.ts.map +1 -1
  45. package/lib/dialects/pg.js +140 -21
  46. package/lib/dialects/pg.js.map +1 -1
  47. package/lib/dialects/redis.d.ts +13 -8
  48. package/lib/dialects/redis.d.ts.map +1 -1
  49. package/lib/dialects/redis.js +14 -11
  50. package/lib/dialects/redis.js.map +1 -1
  51. package/lib/dialects/sqlite.d.ts +20 -7
  52. package/lib/dialects/sqlite.d.ts.map +1 -1
  53. package/lib/dialects/sqlite.js +66 -13
  54. package/lib/dialects/sqlite.js.map +1 -1
  55. package/lib/index.d.ts +1 -0
  56. package/lib/index.d.ts.map +1 -1
  57. package/lib/index.js +1 -0
  58. package/lib/index.js.map +1 -1
  59. package/lib/migration.d.ts +132 -0
  60. package/lib/migration.d.ts.map +1 -0
  61. package/lib/migration.js +475 -0
  62. package/lib/migration.js.map +1 -0
  63. package/lib/registry.d.ts +26 -23
  64. package/lib/registry.d.ts.map +1 -1
  65. package/lib/registry.js +1 -5
  66. package/lib/registry.js.map +1 -1
  67. package/lib/type/document/database.d.ts +12 -12
  68. package/lib/type/document/database.d.ts.map +1 -1
  69. package/lib/type/document/database.js +1 -1
  70. package/lib/type/document/database.js.map +1 -1
  71. package/lib/type/document/model.d.ts +8 -8
  72. package/lib/type/document/model.d.ts.map +1 -1
  73. package/lib/type/document/model.js +1 -1
  74. package/lib/type/document/model.js.map +1 -1
  75. package/lib/type/keyvalue/database.d.ts +12 -12
  76. package/lib/type/keyvalue/database.d.ts.map +1 -1
  77. package/lib/type/keyvalue/database.js +1 -1
  78. package/lib/type/keyvalue/database.js.map +1 -1
  79. package/lib/type/keyvalue/model.d.ts +3 -3
  80. package/lib/type/keyvalue/model.d.ts.map +1 -1
  81. package/lib/type/keyvalue/model.js +1 -1
  82. package/lib/type/keyvalue/model.js.map +1 -1
  83. package/lib/type/related/database.d.ts +49 -14
  84. package/lib/type/related/database.d.ts.map +1 -1
  85. package/lib/type/related/database.js +259 -28
  86. package/lib/type/related/database.js.map +1 -1
  87. package/lib/type/related/model.d.ts +252 -16
  88. package/lib/type/related/model.d.ts.map +1 -1
  89. package/lib/type/related/model.js +648 -23
  90. package/lib/type/related/model.js.map +1 -1
  91. package/lib/types.d.ts +475 -37
  92. package/lib/types.d.ts.map +1 -1
  93. package/lib/types.js +6 -0
  94. package/lib/types.js.map +1 -1
  95. package/package.json +10 -5
  96. package/src/base/database.ts +168 -24
  97. package/src/base/dialect.ts +49 -10
  98. package/src/base/index.ts +2 -1
  99. package/src/base/model.ts +258 -18
  100. package/src/base/query-classes.ts +471 -63
  101. package/src/base/thenable.ts +12 -11
  102. package/src/base/transaction.ts +213 -0
  103. package/src/dialects/memory.ts +17 -16
  104. package/src/dialects/mongodb.ts +44 -42
  105. package/src/dialects/mysql.ts +155 -26
  106. package/src/dialects/pg.ts +152 -25
  107. package/src/dialects/redis.ts +45 -43
  108. package/src/dialects/sqlite.ts +77 -19
  109. package/src/index.ts +1 -2
  110. package/src/migration.ts +544 -0
  111. package/src/registry.ts +33 -33
  112. package/src/type/document/database.ts +33 -33
  113. package/src/type/document/model.ts +15 -15
  114. package/src/type/keyvalue/database.ts +33 -33
  115. package/src/type/keyvalue/model.ts +19 -19
  116. package/src/type/related/database.ts +309 -34
  117. package/src/type/related/model.ts +801 -34
  118. package/src/types.ts +559 -44
  119. package/tests/database.test.ts +1738 -0
@@ -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/UPDATE/DELETE 使用 db.run 执行
69
- else if (isInsertQuery || trimmedSql.startsWith('update') || trimmedSql.startsWith('delete')) {
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: string, columns: string[]): string {
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: string, alterations: string[]): string {
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: string, ifExists?: boolean): string {
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: string, ifExists?: boolean): string {
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', (config: SQLiteDialectConfig, definitions?: Database.Definitions<Record<string, object>>) => {
258
- return new RelatedDatabase(new SQLiteDialect(config), definitions);
259
- });
317
+ Registry.register('sqlite', Sqlite);
package/src/index.ts CHANGED
@@ -15,6 +15,5 @@ export * from './dialects/pg.js';
15
15
  export * from './dialects/redis.js';
16
16
  export * from './dialects/sqlite.js';
17
17
  export * from './dialects/mongodb.js';
18
-
19
-
18
+ export * from './migration.js';
20
19
 
@@ -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
+