@zhin.js/database 1.0.4 → 1.0.6

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 (115) 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 +12 -7
  32. package/lib/dialects/memory.d.ts.map +1 -1
  33. package/lib/dialects/memory.js +7 -4
  34. package/lib/dialects/memory.js.map +1 -1
  35. package/lib/dialects/mongodb.d.ts +11 -7
  36. package/lib/dialects/mongodb.d.ts.map +1 -1
  37. package/lib/dialects/mongodb.js +18 -15
  38. package/lib/dialects/mongodb.js.map +1 -1
  39. package/lib/dialects/mysql.d.ts +35 -6
  40. package/lib/dialects/mysql.d.ts.map +1 -1
  41. package/lib/dialects/mysql.js +137 -18
  42. package/lib/dialects/mysql.js.map +1 -1
  43. package/lib/dialects/pg.d.ts +35 -6
  44. package/lib/dialects/pg.d.ts.map +1 -1
  45. package/lib/dialects/pg.js +137 -18
  46. package/lib/dialects/pg.js.map +1 -1
  47. package/lib/dialects/redis.d.ts +11 -6
  48. package/lib/dialects/redis.d.ts.map +1 -1
  49. package/lib/dialects/redis.js +11 -8
  50. package/lib/dialects/redis.js.map +1 -1
  51. package/lib/dialects/sqlite.d.ts +19 -6
  52. package/lib/dialects/sqlite.d.ts.map +1 -1
  53. package/lib/dialects/sqlite.js +63 -10
  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 +11 -11
  68. package/lib/type/document/database.d.ts.map +1 -1
  69. package/lib/type/document/database.js.map +1 -1
  70. package/lib/type/document/model.d.ts +7 -7
  71. package/lib/type/document/model.d.ts.map +1 -1
  72. package/lib/type/document/model.js.map +1 -1
  73. package/lib/type/keyvalue/database.d.ts +11 -11
  74. package/lib/type/keyvalue/database.d.ts.map +1 -1
  75. package/lib/type/keyvalue/database.js.map +1 -1
  76. package/lib/type/keyvalue/model.d.ts +2 -2
  77. package/lib/type/keyvalue/model.d.ts.map +1 -1
  78. package/lib/type/keyvalue/model.js.map +1 -1
  79. package/lib/type/related/database.d.ts +48 -13
  80. package/lib/type/related/database.d.ts.map +1 -1
  81. package/lib/type/related/database.js +258 -27
  82. package/lib/type/related/database.js.map +1 -1
  83. package/lib/type/related/model.d.ts +251 -15
  84. package/lib/type/related/model.d.ts.map +1 -1
  85. package/lib/type/related/model.js +647 -22
  86. package/lib/type/related/model.js.map +1 -1
  87. package/lib/types.d.ts +475 -37
  88. package/lib/types.d.ts.map +1 -1
  89. package/lib/types.js +6 -0
  90. package/lib/types.js.map +1 -1
  91. package/package.json +14 -5
  92. package/src/base/database.ts +168 -24
  93. package/src/base/dialect.ts +49 -10
  94. package/src/base/index.ts +2 -1
  95. package/src/base/model.ts +258 -18
  96. package/src/base/query-classes.ts +471 -63
  97. package/src/base/thenable.ts +12 -11
  98. package/src/base/transaction.ts +213 -0
  99. package/src/dialects/memory.ts +14 -13
  100. package/src/dialects/mongodb.ts +40 -38
  101. package/src/dialects/mysql.ts +151 -22
  102. package/src/dialects/pg.ts +148 -21
  103. package/src/dialects/redis.ts +40 -38
  104. package/src/dialects/sqlite.ts +73 -15
  105. package/src/index.ts +1 -2
  106. package/src/migration.ts +544 -0
  107. package/src/registry.ts +33 -33
  108. package/src/type/document/database.ts +32 -32
  109. package/src/type/document/model.ts +14 -14
  110. package/src/type/keyvalue/database.ts +32 -32
  111. package/src/type/keyvalue/model.ts +18 -18
  112. package/src/type/related/database.ts +309 -34
  113. package/src/type/related/model.ts +800 -33
  114. package/src/types.ts +559 -44
  115. package/tests/database.test.ts +1738 -0
@@ -0,0 +1,1738 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
2
+ import { Registry } from '../src/registry.js';
3
+ import { Sqlite } from '../src/dialects/sqlite.js';
4
+ import { MigrationRunner, defineMigration } from '../src/migration.js';
5
+
6
+ // 注册 SQLite dialect
7
+ Registry.register('sqlite', Sqlite);
8
+
9
+ // 定义测试 Schema
10
+ interface TestSchema extends Record<string, object> {
11
+ users: {
12
+ id: number;
13
+ name: string;
14
+ email: string;
15
+ status: string;
16
+ age: number;
17
+ deletedAt: Date | null;
18
+ createdAt: Date;
19
+ updatedAt: Date;
20
+ };
21
+ orders: {
22
+ id: number;
23
+ userId: number;
24
+ amount: number;
25
+ productName: string;
26
+ createdAt: Date;
27
+ };
28
+ products: {
29
+ id: number;
30
+ name: string;
31
+ price: number;
32
+ categoryId: number;
33
+ };
34
+ categories: {
35
+ id: number;
36
+ name: string;
37
+ active: boolean;
38
+ };
39
+ }
40
+
41
+ describe('Database Core Features', () => {
42
+ let db: Sqlite<TestSchema>;
43
+
44
+ beforeAll(async () => {
45
+ // 使用 Registry 创建 SQLite 内存数据库
46
+ db = Registry.create<TestSchema, 'sqlite'>('sqlite', { filename: ':memory:' });
47
+
48
+ db.enableLogging(); // 启用查询日志
49
+ await db.start();
50
+
51
+ // 创建测试表
52
+ await db.query(`
53
+ CREATE TABLE IF NOT EXISTS users (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ name TEXT NOT NULL,
56
+ email TEXT,
57
+ status TEXT DEFAULT 'active',
58
+ age INTEGER DEFAULT 0,
59
+ deletedAt DATETIME,
60
+ createdAt DATETIME,
61
+ updatedAt DATETIME
62
+ )
63
+ `);
64
+
65
+ await db.query(`
66
+ CREATE TABLE IF NOT EXISTS orders (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ userId INTEGER NOT NULL,
69
+ amount REAL NOT NULL,
70
+ productName TEXT,
71
+ createdAt DATETIME
72
+ )
73
+ `);
74
+
75
+ await db.query(`
76
+ CREATE TABLE IF NOT EXISTS products (
77
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ name TEXT NOT NULL,
79
+ price REAL NOT NULL,
80
+ categoryId INTEGER
81
+ )
82
+ `);
83
+
84
+ await db.query(`
85
+ CREATE TABLE IF NOT EXISTS categories (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ name TEXT NOT NULL,
88
+ active INTEGER DEFAULT 1
89
+ )
90
+ `);
91
+ });
92
+
93
+ afterAll(async () => {
94
+ await db.stop();
95
+ });
96
+
97
+ beforeEach(async () => {
98
+ // 清空测试数据
99
+ await db.query('DELETE FROM users');
100
+ await db.query('DELETE FROM orders');
101
+ await db.query('DELETE FROM products');
102
+ await db.query('DELETE FROM categories');
103
+ });
104
+
105
+ // ==========================================================================
106
+ // 基础 CRUD 测试
107
+ // ==========================================================================
108
+
109
+ describe('Basic CRUD', () => {
110
+ it('should insert and select data', async () => {
111
+ // 插入
112
+ await db.insert('users', {
113
+ id: 1,
114
+ name: 'John',
115
+ email: 'john@test.com',
116
+ status: 'active',
117
+ age: 25,
118
+ deletedAt: null,
119
+ createdAt: new Date(),
120
+ updatedAt: new Date()
121
+ });
122
+
123
+ // 查询
124
+ const users = await db.select('users', ['id', 'name', 'email']);
125
+
126
+ expect(users).toHaveLength(1);
127
+ expect(users[0].name).toBe('John');
128
+ expect(users[0].email).toBe('john@test.com');
129
+ });
130
+
131
+ it('should update data with where condition', async () => {
132
+ await db.insert('users', {
133
+ id: 1, name: 'John', email: 'john@test.com', status: 'active', age: 25,
134
+ deletedAt: null, createdAt: new Date(), updatedAt: new Date()
135
+ });
136
+
137
+ const affected = await db.update('users', { name: 'Jane' })
138
+ .where({ id: 1 });
139
+
140
+ expect(affected).toBe(1);
141
+
142
+ const users = await db.select('users', ['name']).where({ id: 1 });
143
+ expect(users[0].name).toBe('Jane');
144
+ });
145
+
146
+ it('should delete data', async () => {
147
+ await db.insert('users', {
148
+ id: 1, name: 'John', email: 'john@test.com', status: 'active', age: 25,
149
+ deletedAt: null, createdAt: new Date(), updatedAt: new Date()
150
+ });
151
+
152
+ await db.delete('users', { id: 1 });
153
+
154
+ const users = await db.select('users', ['id']);
155
+ expect(users).toHaveLength(0);
156
+ });
157
+ });
158
+
159
+ // ==========================================================================
160
+ // 链式查询测试
161
+ // ==========================================================================
162
+
163
+ describe('Chainable Query', () => {
164
+ beforeEach(async () => {
165
+ // 插入测试数据
166
+ await db.insertMany('users', [
167
+ { id: 1, name: 'Alice', email: 'alice@test.com', status: 'active', age: 25, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
168
+ { id: 2, name: 'Bob', email: 'bob@test.com', status: 'active', age: 30, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
169
+ { id: 3, name: 'Charlie', email: 'charlie@test.com', status: 'inactive', age: 35, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
170
+ { id: 4, name: 'David', email: 'david@test.com', status: 'active', age: 28, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
171
+ ]);
172
+ });
173
+
174
+ it('should support where with comparison operators', async () => {
175
+ const users = await db.select('users', ['name', 'age'])
176
+ .where({ age: { $gt: 28 } });
177
+
178
+ expect(users).toHaveLength(2);
179
+ expect(users.map(u => u.name).sort()).toEqual(['Bob', 'Charlie']);
180
+ });
181
+
182
+ it('should support orderBy', async () => {
183
+ const users = await db.select('users', ['name'])
184
+ .orderBy('name', 'ASC');
185
+
186
+ expect(users.map(u => u.name)).toEqual(['Alice', 'Bob', 'Charlie', 'David']);
187
+ });
188
+
189
+ it('should support limit and offset', async () => {
190
+ const users = await db.select('users', ['name'])
191
+ .orderBy('id', 'ASC')
192
+ .limit(2)
193
+ .offset(1);
194
+
195
+ expect(users).toHaveLength(2);
196
+ expect(users[0].name).toBe('Bob');
197
+ expect(users[1].name).toBe('Charlie');
198
+ });
199
+
200
+ it('should support $in operator', async () => {
201
+ const users = await db.select('users', ['name'])
202
+ .where({ status: { $in: ['active'] } });
203
+
204
+ expect(users).toHaveLength(3);
205
+ });
206
+
207
+ it('should support $like operator', async () => {
208
+ const users = await db.select('users', ['name'])
209
+ .where({ name: { $like: '%li%' } });
210
+
211
+ expect(users).toHaveLength(2); // Alice, Charlie
212
+ });
213
+
214
+ it('should support logical $or operator', async () => {
215
+ const users = await db.select('users', ['name'])
216
+ .where({
217
+ $or: [
218
+ { name: 'Alice' },
219
+ { name: 'Bob' }
220
+ ]
221
+ });
222
+
223
+ expect(users).toHaveLength(2);
224
+ });
225
+ });
226
+
227
+ // ==========================================================================
228
+ // 聚合查询测试
229
+ // ==========================================================================
230
+
231
+ describe('Aggregation', () => {
232
+ beforeEach(async () => {
233
+ await db.insertMany('users', [
234
+ { id: 1, name: 'Alice', email: 'a@t.com', status: 'active', age: 25, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
235
+ { id: 2, name: 'Bob', email: 'b@t.com', status: 'active', age: 30, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
236
+ { id: 3, name: 'Charlie', email: 'c@t.com', status: 'inactive', age: 35, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
237
+ ]);
238
+ });
239
+
240
+ it('should count records', async () => {
241
+ const result = await db.aggregate('users')
242
+ .count('*', 'total');
243
+
244
+ expect(result[0].total).toBe(3);
245
+ });
246
+
247
+ it('should calculate sum', async () => {
248
+ const result = await db.aggregate('users')
249
+ .sum('age', 'totalAge');
250
+
251
+ expect(result[0].totalAge).toBe(90);
252
+ });
253
+
254
+ it('should calculate avg', async () => {
255
+ const result = await db.aggregate('users')
256
+ .avg('age', 'avgAge');
257
+
258
+ expect(result[0].avgAge).toBe(30);
259
+ });
260
+
261
+ it('should support groupBy', async () => {
262
+ const result = await db.aggregate('users')
263
+ .count('*', 'count')
264
+ .groupBy('status');
265
+
266
+ expect(result).toHaveLength(2);
267
+ const active = result.find((r: any) => r.status === 'active');
268
+ expect(active?.count).toBe(2);
269
+ });
270
+ });
271
+
272
+ // ==========================================================================
273
+ // 批量插入测试
274
+ // ==========================================================================
275
+
276
+ describe('Batch Insert', () => {
277
+ it('should insert multiple records', async () => {
278
+ const result = await db.insertMany('users', [
279
+ { id: 1, name: 'User1', email: 'u1@t.com', status: 'active', age: 20, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
280
+ { id: 2, name: 'User2', email: 'u2@t.com', status: 'active', age: 21, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
281
+ { id: 3, name: 'User3', email: 'u3@t.com', status: 'active', age: 22, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
282
+ ]);
283
+
284
+ expect(result.affectedRows).toBe(3);
285
+
286
+ const users = await db.select('users', ['id']);
287
+ expect(users).toHaveLength(3);
288
+ });
289
+ });
290
+
291
+ // ==========================================================================
292
+ // 事务测试
293
+ // ==========================================================================
294
+
295
+ describe('Transaction', () => {
296
+ it('should commit transaction on success', async () => {
297
+ await db.transaction(async (trx) => {
298
+ await trx.insert('users', {
299
+ id: 1, name: 'TrxUser', email: 'trx@t.com', status: 'active', age: 25,
300
+ deletedAt: null, createdAt: new Date(), updatedAt: new Date()
301
+ });
302
+
303
+ await trx.update('users', { age: 30 }).where({ id: 1 });
304
+ });
305
+
306
+ const users = await db.select('users', ['name', 'age']).where({ id: 1 });
307
+ expect(users).toHaveLength(1);
308
+ expect(users[0].age).toBe(30);
309
+ });
310
+
311
+ it('should rollback transaction on error', async () => {
312
+ try {
313
+ await db.transaction(async (trx) => {
314
+ await trx.insert('users', {
315
+ id: 1, name: 'TrxUser', email: 'trx@t.com', status: 'active', age: 25,
316
+ deletedAt: null, createdAt: new Date(), updatedAt: new Date()
317
+ });
318
+
319
+ // 故意抛出错误
320
+ throw new Error('Intentional error');
321
+ });
322
+ } catch (e) {
323
+ // 预期会抛出错误
324
+ }
325
+
326
+ const users = await db.select('users', ['id']);
327
+ expect(users).toHaveLength(0); // 应该回滚
328
+ });
329
+
330
+ it('should support chainable queries in transaction', async () => {
331
+ await db.transaction(async (trx) => {
332
+ await trx.insert('users', {
333
+ id: 1, name: 'Alice', email: 'a@t.com', status: 'active', age: 25,
334
+ deletedAt: null, createdAt: new Date(), updatedAt: new Date()
335
+ });
336
+
337
+ const users = await trx.select('users', ['name'])
338
+ .where({ status: 'active' });
339
+
340
+ expect(users).toHaveLength(1);
341
+ expect(users[0].name).toBe('Alice');
342
+ });
343
+ });
344
+ });
345
+
346
+ // ==========================================================================
347
+ // 子查询测试
348
+ // ==========================================================================
349
+
350
+ describe('Subquery', () => {
351
+ beforeEach(async () => {
352
+ await db.insertMany('users', [
353
+ { id: 1, name: 'Alice', email: 'a@t.com', status: 'active', age: 25, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
354
+ { id: 2, name: 'Bob', email: 'b@t.com', status: 'active', age: 30, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
355
+ ]);
356
+
357
+ await db.insertMany('orders', [
358
+ { id: 1, userId: 1, amount: 100, productName: 'Product A', createdAt: new Date() },
359
+ { id: 2, userId: 1, amount: 200, productName: 'Product B', createdAt: new Date() },
360
+ ]);
361
+ });
362
+
363
+ it('should support $in with subquery', async () => {
364
+ // 查询有订单的用户
365
+ const users = await db.select('users', ['id', 'name'])
366
+ .where({
367
+ id: { $in: db.select('orders', ['userId']) }
368
+ });
369
+
370
+ expect(users).toHaveLength(1);
371
+ expect(users[0].name).toBe('Alice');
372
+ });
373
+
374
+ it('should support $nin with subquery', async () => {
375
+ // 查询没有订单的用户
376
+ const users = await db.select('users', ['id', 'name'])
377
+ .where({
378
+ id: { $nin: db.select('orders', ['userId']) }
379
+ });
380
+
381
+ expect(users).toHaveLength(1);
382
+ expect(users[0].name).toBe('Bob');
383
+ });
384
+ });
385
+
386
+ // ==========================================================================
387
+ // JOIN 测试
388
+ // ==========================================================================
389
+
390
+ describe('JOIN Query', () => {
391
+ beforeEach(async () => {
392
+ await db.insertMany('users', [
393
+ { id: 1, name: 'Alice', email: 'a@t.com', status: 'active', age: 25, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
394
+ { id: 2, name: 'Bob', email: 'b@t.com', status: 'active', age: 30, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
395
+ ]);
396
+
397
+ await db.insertMany('orders', [
398
+ { id: 1, userId: 1, amount: 100, productName: 'Product A', createdAt: new Date() },
399
+ { id: 2, userId: 1, amount: 200, productName: 'Product B', createdAt: new Date() },
400
+ ]);
401
+ });
402
+
403
+ it('should support INNER JOIN', async () => {
404
+ const result = await db.select('users', ['id', 'name'])
405
+ .join('orders', 'id', 'userId');
406
+
407
+ // Alice 有 2 个订单,所以返回 2 行
408
+ expect(result).toHaveLength(2);
409
+ });
410
+
411
+ it('should support LEFT JOIN', async () => {
412
+ const result = await db.select('users', ['id', 'name'])
413
+ .leftJoin('orders', 'id', 'userId');
414
+
415
+ // Alice 有 2 订单,Bob 没有订单但也返回
416
+ expect(result).toHaveLength(3);
417
+ });
418
+ });
419
+
420
+ // ==========================================================================
421
+ // 软删除测试
422
+ // ==========================================================================
423
+
424
+ describe('Soft Delete', () => {
425
+ let userModel: ReturnType<typeof db.model<'users'>>;
426
+
427
+ beforeAll(() => {
428
+ userModel = db.model('users', { softDelete: true });
429
+ });
430
+
431
+ beforeEach(async () => {
432
+ await db.insertMany('users', [
433
+ { id: 1, name: 'Alice', email: 'a@t.com', status: 'active', age: 25, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
434
+ { id: 2, name: 'Bob', email: 'b@t.com', status: 'active', age: 30, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
435
+ ]);
436
+ });
437
+
438
+ it('should soft delete (set deletedAt)', async () => {
439
+ await userModel.delete({ id: 1 } as any);
440
+
441
+ // 检查 deletedAt 被设置
442
+ const allUsers = await userModel.selectWithTrashed('id', 'name', 'deletedAt');
443
+ const alice = allUsers.find(u => u.id === 1);
444
+
445
+ expect(alice?.deletedAt).not.toBeNull();
446
+ });
447
+
448
+ it('should exclude soft deleted in normal select', async () => {
449
+ await userModel.delete({ id: 1 } as any);
450
+
451
+ const users = await userModel.select('id', 'name');
452
+ expect(users).toHaveLength(1);
453
+ expect(users[0].name).toBe('Bob');
454
+ });
455
+
456
+ it('should include soft deleted with selectWithTrashed', async () => {
457
+ await userModel.delete({ id: 1 } as any);
458
+
459
+ const users = await userModel.selectWithTrashed('id', 'name');
460
+ expect(users).toHaveLength(2);
461
+ });
462
+
463
+ it('should only get soft deleted with selectOnlyTrashed', async () => {
464
+ await userModel.delete({ id: 1 } as any);
465
+
466
+ const users = await userModel.selectOnlyTrashed('id', 'name');
467
+ expect(users).toHaveLength(1);
468
+ expect(users[0].name).toBe('Alice');
469
+ });
470
+
471
+ it('should restore soft deleted record', async () => {
472
+ await userModel.delete({ id: 1 } as any);
473
+ await userModel.restore({ id: 1 } as any);
474
+
475
+ const users = await userModel.select('id', 'name');
476
+ expect(users).toHaveLength(2);
477
+ });
478
+
479
+ it('should force delete (physical delete)', async () => {
480
+ await userModel.forceDelete({ id: 1 } as any);
481
+
482
+ const allUsers = await userModel.selectWithTrashed('id');
483
+ expect(allUsers).toHaveLength(1);
484
+ });
485
+ });
486
+
487
+ // ==========================================================================
488
+ // 边界情况测试
489
+ // ==========================================================================
490
+
491
+ describe('Edge Cases', () => {
492
+ it('should handle empty result set', async () => {
493
+ const users = await db.select('users', ['id', 'name'])
494
+ .where({ name: 'NonExistent' });
495
+
496
+ expect(users).toHaveLength(0);
497
+ });
498
+
499
+ it('should handle null values correctly', async () => {
500
+ await db.insert('users', {
501
+ id: 1, name: 'Test', email: null as any, status: 'active', age: 25,
502
+ deletedAt: null, createdAt: new Date(), updatedAt: new Date()
503
+ });
504
+
505
+ const users = await db.select('users', ['id', 'email'])
506
+ .where({ email: null as any });
507
+
508
+ expect(users).toHaveLength(1);
509
+ expect(users[0].email).toBeNull();
510
+ });
511
+
512
+ it('should handle complex nested conditions', async () => {
513
+ await db.insertMany('users', [
514
+ { id: 1, name: 'Alice', email: 'a@t.com', status: 'active', age: 25, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
515
+ { id: 2, name: 'Bob', email: 'b@t.com', status: 'inactive', age: 30, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
516
+ { id: 3, name: 'Charlie', email: 'c@t.com', status: 'active', age: 35, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
517
+ ]);
518
+
519
+ // (status = 'active' AND age > 26) OR (status = 'inactive')
520
+ const users = await db.select('users', ['name'])
521
+ .where({
522
+ $or: [
523
+ { status: 'active', age: { $gt: 26 } },
524
+ { status: 'inactive' }
525
+ ]
526
+ });
527
+
528
+ expect(users.map(u => u.name).sort()).toEqual(['Bob', 'Charlie']);
529
+ });
530
+
531
+ it('should handle date comparison', async () => {
532
+ const now = new Date();
533
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
534
+
535
+ await db.insertMany('users', [
536
+ { id: 1, name: 'OldUser', email: 'a@t.com', status: 'active', age: 25, deletedAt: null, createdAt: yesterday, updatedAt: yesterday },
537
+ { id: 2, name: 'NewUser', email: 'b@t.com', status: 'active', age: 30, deletedAt: null, createdAt: now, updatedAt: now },
538
+ ]);
539
+
540
+ // 比较日期
541
+ const users = await db.select('users', ['name', 'createdAt'])
542
+ .orderBy('id', 'ASC');
543
+
544
+ expect(users).toHaveLength(2);
545
+ });
546
+
547
+ it('should handle special characters in string', async () => {
548
+ await db.insert('users', {
549
+ id: 1, name: "O'Brien", email: 'test@test.com', status: 'active', age: 25,
550
+ deletedAt: null, createdAt: new Date(), updatedAt: new Date()
551
+ });
552
+
553
+ const users = await db.select('users', ['name'])
554
+ .where({ name: "O'Brien" });
555
+
556
+ expect(users).toHaveLength(1);
557
+ expect(users[0].name).toBe("O'Brien");
558
+ });
559
+ });
560
+
561
+ // ==========================================================================
562
+ // 查询日志测试
563
+ // ==========================================================================
564
+
565
+ describe('Query Logging', () => {
566
+ it('should log queries when enabled', async () => {
567
+ const logs: string[] = [];
568
+
569
+ // 先禁用再启用,确保使用新的 handler
570
+ db.disableLogging();
571
+ db.enableLogging(({ sql }) => {
572
+ logs.push(sql);
573
+ });
574
+
575
+ await db.select('users', ['id']).where({ status: 'active' });
576
+
577
+ expect(logs.length).toBeGreaterThan(0);
578
+ expect(logs[0]).toContain('SELECT');
579
+
580
+ // 测试后恢复默认日志
581
+ db.enableLogging();
582
+ });
583
+
584
+ it('should not log when disabled', async () => {
585
+ const logs: string[] = [];
586
+
587
+ db.disableLogging();
588
+ db.enableLogging(({ sql }) => {
589
+ logs.push(sql);
590
+ });
591
+ db.disableLogging();
592
+
593
+ await db.select('users', ['id']);
594
+
595
+ expect(logs).toHaveLength(0);
596
+
597
+ // 测试后恢复默认日志
598
+ db.enableLogging();
599
+ });
600
+ });
601
+
602
+ // ==========================================================================
603
+ // Relations 关联关系测试
604
+ // ==========================================================================
605
+
606
+ describe('Relations', () => {
607
+ beforeEach(async () => {
608
+ // 插入用户数据
609
+ await db.insertMany('users', [
610
+ { id: 1, name: 'Alice', email: 'alice@test.com', status: 'active', age: 25, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
611
+ { id: 2, name: 'Bob', email: 'bob@test.com', status: 'active', age: 30, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
612
+ ]);
613
+
614
+ // 插入订单数据
615
+ await db.insertMany('orders', [
616
+ { id: 1, userId: 1, amount: 100, productName: 'Product A', createdAt: new Date() },
617
+ { id: 2, userId: 1, amount: 200, productName: 'Product B', createdAt: new Date() },
618
+ { id: 3, userId: 2, amount: 150, productName: 'Product C', createdAt: new Date() },
619
+ ]);
620
+ });
621
+
622
+ it('should define hasMany relation', () => {
623
+ const userModel = db.model('users');
624
+ const orderModel = db.model('orders');
625
+
626
+ // 新 API:传入模型实例,自动推断关系名
627
+ userModel.hasMany(orderModel, 'userId');
628
+
629
+ expect(userModel.getRelationNames()).toContain('orders');
630
+ expect(userModel.getRelation('orders')?.type).toBe('hasMany');
631
+ });
632
+
633
+ it('should define belongsTo relation', () => {
634
+ const userModel = db.model('users');
635
+ const orderModel = db.model('orders');
636
+
637
+ // 新 API:传入模型实例
638
+ orderModel.belongsTo(userModel, 'userId');
639
+
640
+ expect(orderModel.getRelationNames()).toContain('users');
641
+ expect(orderModel.getRelation('users')?.type).toBe('belongsTo');
642
+ });
643
+
644
+ it('should load hasMany relation for single record', async () => {
645
+ const userModel = db.model('users');
646
+ const orderModel = db.model('orders');
647
+ userModel.hasMany(orderModel, 'userId');
648
+
649
+ const alice = await userModel.selectById(1);
650
+ const aliceWithOrders = await userModel.loadRelation(alice!, 'orders');
651
+
652
+ expect(aliceWithOrders.orders).toHaveLength(2);
653
+ expect((aliceWithOrders.orders as any[])[0].productName).toBe('Product A');
654
+ });
655
+
656
+ it('should load belongsTo relation for single record', async () => {
657
+ const userModel = db.model('users');
658
+ const orderModel = db.model('orders');
659
+ orderModel.belongsTo(userModel, 'userId');
660
+
661
+ const order = await orderModel.selectById(1);
662
+ const orderWithUser = await orderModel.loadRelation(order!, 'users');
663
+
664
+ expect(orderWithUser.users).not.toBeNull();
665
+ expect((orderWithUser.users as any).name).toBe('Alice');
666
+ });
667
+
668
+ it('should batch load relations (solve N+1)', async () => {
669
+ const userModel = db.model('users');
670
+ const orderModel = db.model('orders');
671
+ userModel.hasMany(orderModel, 'userId');
672
+
673
+ const users = await userModel.select();
674
+ const usersWithOrders = await userModel.loadRelations(users, ['orders']);
675
+
676
+ expect(usersWithOrders).toHaveLength(2);
677
+
678
+ const alice = usersWithOrders.find(u => u.name === 'Alice');
679
+ const bob = usersWithOrders.find(u => u.name === 'Bob');
680
+
681
+ expect(alice?.orders).toHaveLength(2);
682
+ expect(bob?.orders).toHaveLength(1);
683
+ });
684
+
685
+ it('should use .with() for eager loading', async () => {
686
+ const userModel = db.model('users');
687
+ const orderModel = db.model('orders');
688
+ userModel.hasMany(orderModel, 'userId');
689
+
690
+ const usersWithOrders = await userModel.with('orders')
691
+ .where({ status: 'active' })
692
+ .orderBy('id', 'ASC');
693
+
694
+ expect(usersWithOrders).toHaveLength(2);
695
+ expect(usersWithOrders[0].orders).toHaveLength(2); // Alice's orders
696
+ expect(usersWithOrders[1].orders).toHaveLength(1); // Bob's orders
697
+ });
698
+
699
+ it('should handle null belongsTo relation', async () => {
700
+ // 插入一个没有用户的订单
701
+ await db.insert('orders', { id: 99, userId: 999, amount: 50, productName: 'Orphan', createdAt: new Date() });
702
+
703
+ const userModel = db.model('users');
704
+ const orderModel = db.model('orders');
705
+ orderModel.belongsTo(userModel, 'userId');
706
+
707
+ const order = await orderModel.selectById(99);
708
+ const orderWithUser = await orderModel.loadRelation(order!, 'users');
709
+
710
+ expect(orderWithUser.users).toBeNull();
711
+ });
712
+
713
+ it('should handle empty hasMany relation', async () => {
714
+ // 插入一个没有订单的用户
715
+ await db.insert('users', { id: 99, name: 'NoOrders', email: 'no@test.com', status: 'active', age: 40, deletedAt: null, createdAt: new Date(), updatedAt: new Date() });
716
+
717
+ const userModel = db.model('users');
718
+ const orderModel = db.model('orders');
719
+ userModel.hasMany(orderModel, 'userId');
720
+
721
+ const user = await userModel.selectById(99);
722
+ const userWithOrders = await userModel.loadRelation(user!, 'orders');
723
+
724
+ expect(userWithOrders.orders).toHaveLength(0);
725
+ });
726
+ });
727
+
728
+ // ==========================================================================
729
+ // 预定义关系配置测试
730
+ // ==========================================================================
731
+
732
+ describe('Predefined Relations Config', () => {
733
+ beforeEach(async () => {
734
+ await db.insertMany('users', [
735
+ { id: 1, name: 'Alice', email: 'alice@test.com', status: 'active', age: 25, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
736
+ { id: 2, name: 'Bob', email: 'bob@test.com', status: 'active', age: 30, deletedAt: null, createdAt: new Date(), updatedAt: new Date() },
737
+ ]);
738
+
739
+ await db.insertMany('orders', [
740
+ { id: 1, userId: 1, amount: 100, productName: 'Product A', createdAt: new Date() },
741
+ { id: 2, userId: 1, amount: 200, productName: 'Product B', createdAt: new Date() },
742
+ ]);
743
+ });
744
+
745
+ it('should auto-apply relations from defineRelations()', async () => {
746
+ // 预定义关系配置
747
+ db.defineRelations({
748
+ users: {
749
+ hasMany: { orders: 'userId' }
750
+ },
751
+ orders: {
752
+ belongsTo: { users: 'userId' }
753
+ }
754
+ });
755
+
756
+ // 获取模型时自动应用关系
757
+ const userModel = db.model('users');
758
+
759
+ // 无需手动调用 hasMany,关系已自动配置
760
+ expect(userModel.getRelationNames()).toContain('orders');
761
+
762
+ // 直接使用 .with() 加载关联
763
+ const usersWithOrders = await userModel.with('orders');
764
+
765
+ expect(usersWithOrders).toHaveLength(2);
766
+ expect(usersWithOrders[0].orders).toHaveLength(2);
767
+ });
768
+
769
+ it('should support belongsTo from config', async () => {
770
+ db.defineRelations({
771
+ orders: {
772
+ belongsTo: { users: 'userId' }
773
+ }
774
+ });
775
+
776
+ const orderModel = db.model('orders');
777
+
778
+ expect(orderModel.getRelationNames()).toContain('users');
779
+
780
+ const order = await orderModel.selectById(1);
781
+ const orderWithUser = await orderModel.loadRelation(order!, 'users');
782
+
783
+ expect((orderWithUser.users as any).name).toBe('Alice');
784
+ });
785
+
786
+ it('should work with model options and relations', async () => {
787
+ db.defineRelations({
788
+ users: {
789
+ hasMany: { orders: 'userId' }
790
+ }
791
+ });
792
+
793
+ // 同时使用 options 和预定义关系
794
+ const userModel = db.model('users', { softDelete: true });
795
+
796
+ // 关系仍然被正确应用
797
+ expect(userModel.getRelationNames()).toContain('orders');
798
+ });
799
+ });
800
+ });
801
+
802
+ // =============================================================================
803
+ // Migration Tests (独立的 describe 块,使用独立的数据库实例)
804
+ // =============================================================================
805
+
806
+ describe('Migration Runner', () => {
807
+ let migrationDb: Sqlite<Record<string, object>>;
808
+ let runner: MigrationRunner;
809
+
810
+ beforeAll(async () => {
811
+ // 创建一个独立的数据库用于迁移测试
812
+ migrationDb = new Sqlite({ filename: ':memory:' });
813
+ await migrationDb.start();
814
+ });
815
+
816
+ afterAll(async () => {
817
+ await migrationDb.stop();
818
+ });
819
+
820
+ beforeEach(async () => {
821
+ // 清理迁移表和测试创建的表
822
+ await migrationDb.query("DELETE FROM _migrations").catch(() => {});
823
+
824
+ // 删除测试创建的表
825
+ const tables = ['test_table', 'another_table', 'table_a', 'table_b',
826
+ 'column_test', 'index_test', 'first_table', 'second_table',
827
+ 'refresh_table', 'only_once'];
828
+ for (const table of tables) {
829
+ await migrationDb.query(`DROP TABLE IF EXISTS "${table}"`).catch(() => {});
830
+ }
831
+
832
+ // 每个测试使用新的 runner
833
+ runner = new MigrationRunner(migrationDb as any);
834
+ });
835
+
836
+ it('should create migration table automatically', async () => {
837
+ const status = await runner.status();
838
+ expect(status).toHaveLength(0);
839
+
840
+ // 验证迁移表已创建
841
+ const tables = await migrationDb.query<{ name: string }[]>(
842
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='_migrations'"
843
+ );
844
+ expect(tables).toHaveLength(1);
845
+ });
846
+
847
+ it('should run migrations and track status', async () => {
848
+ runner.add(defineMigration({
849
+ name: '001_create_test_table',
850
+ up: async (ctx) => {
851
+ await ctx.createTable('test_table', {
852
+ id: { type: 'integer', primary: true, autoIncrement: true },
853
+ name: { type: 'text', nullable: false }
854
+ });
855
+ },
856
+ down: async (ctx) => {
857
+ await ctx.dropTable('test_table');
858
+ }
859
+ }));
860
+
861
+ // 检查初始状态
862
+ let status = await runner.status();
863
+ expect(status).toHaveLength(1);
864
+ expect(status[0].status).toBe('pending');
865
+
866
+ // 运行迁移
867
+ const migrated = await runner.migrate();
868
+ expect(migrated).toHaveLength(1);
869
+ expect(migrated[0]).toBe('001_create_test_table');
870
+
871
+ // 检查迁移后状态
872
+ status = await runner.status();
873
+ expect(status[0].status).toBe('executed');
874
+ expect(status[0].batch).toBe(1);
875
+
876
+ // 验证表已创建
877
+ const tables = await migrationDb.query<{ name: string }[]>(
878
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='test_table'"
879
+ );
880
+ expect(tables).toHaveLength(1);
881
+ });
882
+
883
+ it('should rollback last batch', async () => {
884
+ runner.add(defineMigration({
885
+ name: '002_create_another_table',
886
+ up: async (ctx) => {
887
+ await ctx.createTable('another_table', {
888
+ id: { type: 'integer', primary: true }
889
+ });
890
+ },
891
+ down: async (ctx) => {
892
+ await ctx.dropTable('another_table');
893
+ }
894
+ }));
895
+
896
+ // 运行迁移
897
+ await runner.migrate();
898
+
899
+ // 回滚
900
+ const rolledBack = await runner.rollback();
901
+ expect(rolledBack).toHaveLength(1);
902
+ expect(rolledBack[0]).toBe('002_create_another_table');
903
+
904
+ // 验证表已删除
905
+ const tables = await migrationDb.query<{ name: string }[]>(
906
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='another_table'"
907
+ );
908
+ expect(tables).toHaveLength(0);
909
+ });
910
+
911
+ it('should run multiple migrations in batch', async () => {
912
+ runner.addAll([
913
+ defineMigration({
914
+ name: '003_table_a',
915
+ up: async (ctx) => {
916
+ await ctx.createTable('table_a', {
917
+ id: { type: 'integer', primary: true }
918
+ });
919
+ },
920
+ down: async (ctx) => {
921
+ await ctx.dropTable('table_a');
922
+ }
923
+ }),
924
+ defineMigration({
925
+ name: '004_table_b',
926
+ up: async (ctx) => {
927
+ await ctx.createTable('table_b', {
928
+ id: { type: 'integer', primary: true }
929
+ });
930
+ },
931
+ down: async (ctx) => {
932
+ await ctx.dropTable('table_b');
933
+ }
934
+ })
935
+ ]);
936
+
937
+ const migrated = await runner.migrate();
938
+ expect(migrated).toHaveLength(2);
939
+
940
+ // 验证两个表都创建了
941
+ const tables = await migrationDb.query<{ name: string }[]>(
942
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('table_a', 'table_b')"
943
+ );
944
+ expect(tables).toHaveLength(2);
945
+
946
+ // 回滚应该一次性回滚两个(同一批次)
947
+ const rolledBack = await runner.rollback();
948
+ expect(rolledBack).toHaveLength(2);
949
+ });
950
+
951
+ it('should support addColumn and dropColumn', async () => {
952
+ runner.add(defineMigration({
953
+ name: '005_add_column',
954
+ up: async (ctx) => {
955
+ await ctx.createTable('column_test', {
956
+ id: { type: 'integer', primary: true }
957
+ });
958
+ await ctx.addColumn('column_test', 'email', {
959
+ type: 'text',
960
+ nullable: true
961
+ });
962
+ },
963
+ down: async (ctx) => {
964
+ await ctx.dropTable('column_test');
965
+ }
966
+ }));
967
+
968
+ await runner.migrate();
969
+
970
+ // 验证表已创建(通过插入数据测试)
971
+ await migrationDb.query("INSERT INTO column_test (id, email) VALUES (1, 'test@test.com')");
972
+ const rows = await migrationDb.query<any[]>("SELECT * FROM column_test");
973
+ expect(rows).toHaveLength(1);
974
+ expect(rows[0].email).toBe('test@test.com');
975
+ });
976
+
977
+ it('should support addIndex and dropIndex', async () => {
978
+ runner.add(defineMigration({
979
+ name: '006_add_index',
980
+ up: async (ctx) => {
981
+ await ctx.createTable('index_test', {
982
+ id: { type: 'integer', primary: true },
983
+ email: { type: 'text' }
984
+ });
985
+ await ctx.addIndex('index_test', 'idx_email', ['email'], true);
986
+ },
987
+ down: async (ctx) => {
988
+ await ctx.dropIndex('index_test', 'idx_email');
989
+ await ctx.dropTable('index_test');
990
+ }
991
+ }));
992
+
993
+ await runner.migrate();
994
+
995
+ // 验证索引已创建
996
+ const indexes = await migrationDb.query<{ name: string }[]>(
997
+ "SELECT name FROM sqlite_master WHERE type='index' AND name='idx_email'"
998
+ );
999
+ expect(indexes).toHaveLength(1);
1000
+ });
1001
+
1002
+ it('should support reset (rollback all)', async () => {
1003
+ // 添加多个迁移
1004
+ runner.addAll([
1005
+ defineMigration({
1006
+ name: '007_first',
1007
+ up: async (ctx) => {
1008
+ await ctx.createTable('first_table', { id: { type: 'integer', primary: true } });
1009
+ },
1010
+ down: async (ctx) => {
1011
+ await ctx.dropTable('first_table');
1012
+ }
1013
+ }),
1014
+ defineMigration({
1015
+ name: '008_second',
1016
+ up: async (ctx) => {
1017
+ await ctx.createTable('second_table', { id: { type: 'integer', primary: true } });
1018
+ },
1019
+ down: async (ctx) => {
1020
+ await ctx.dropTable('second_table');
1021
+ }
1022
+ })
1023
+ ]);
1024
+
1025
+ await runner.migrate();
1026
+
1027
+ // Reset 应该回滚所有
1028
+ const rolledBack = await runner.reset();
1029
+ expect(rolledBack).toHaveLength(2);
1030
+
1031
+ // 验证所有表都删除了
1032
+ const tables = await migrationDb.query<{ name: string }[]>(
1033
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('first_table', 'second_table')"
1034
+ );
1035
+ expect(tables).toHaveLength(0);
1036
+ });
1037
+
1038
+ it('should support refresh (reset + migrate)', async () => {
1039
+ runner.add(defineMigration({
1040
+ name: '009_refresh_test',
1041
+ up: async (ctx) => {
1042
+ await ctx.createTable('refresh_table', { id: { type: 'integer', primary: true } });
1043
+ },
1044
+ down: async (ctx) => {
1045
+ await ctx.dropTable('refresh_table');
1046
+ }
1047
+ }));
1048
+
1049
+ await runner.migrate();
1050
+
1051
+ const result = await runner.refresh();
1052
+ expect(result.rolledBack).toHaveLength(1);
1053
+ expect(result.migrated).toHaveLength(1);
1054
+ });
1055
+
1056
+ it('should skip already executed migrations', async () => {
1057
+ runner.add(defineMigration({
1058
+ name: '010_only_once',
1059
+ up: async (ctx) => {
1060
+ await ctx.createTable('only_once', { id: { type: 'integer', primary: true } });
1061
+ },
1062
+ down: async (ctx) => {
1063
+ await ctx.dropTable('only_once');
1064
+ }
1065
+ }));
1066
+
1067
+ // 第一次运行
1068
+ const first = await runner.migrate();
1069
+ expect(first).toHaveLength(1);
1070
+
1071
+ // 第二次运行应该跳过
1072
+ const second = await runner.migrate();
1073
+ expect(second).toHaveLength(0);
1074
+ });
1075
+
1076
+ // 自动生成 down 的测试
1077
+ it('should auto-generate down for createTable', async () => {
1078
+ // 只定义 up,不定义 down
1079
+ runner.add(defineMigration({
1080
+ name: '011_auto_down_table',
1081
+ up: async (ctx) => {
1082
+ await ctx.createTable('auto_table', {
1083
+ id: { type: 'integer', primary: true },
1084
+ name: { type: 'text' }
1085
+ });
1086
+ }
1087
+ // down 自动生成
1088
+ }));
1089
+
1090
+ await runner.migrate();
1091
+
1092
+ // 验证表创建成功
1093
+ const tables = await migrationDb.query<{ name: string }[]>(
1094
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='auto_table'"
1095
+ );
1096
+ expect(tables).toHaveLength(1);
1097
+
1098
+ // 回滚 - 应该自动调用 dropTable
1099
+ await runner.rollback();
1100
+
1101
+ // 验证表被删除
1102
+ const tablesAfter = await migrationDb.query<{ name: string }[]>(
1103
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='auto_table'"
1104
+ );
1105
+ expect(tablesAfter).toHaveLength(0);
1106
+ });
1107
+
1108
+ it('should auto-generate down for addColumn and addIndex', async () => {
1109
+ // 先创建基础表
1110
+ await migrationDb.query(`
1111
+ CREATE TABLE base_table (id INTEGER PRIMARY KEY)
1112
+ `);
1113
+
1114
+ runner.add(defineMigration({
1115
+ name: '012_auto_down_column_index',
1116
+ up: async (ctx) => {
1117
+ await ctx.addColumn('base_table', 'email', { type: 'text' });
1118
+ await ctx.addIndex('base_table', 'idx_email', ['email']);
1119
+ }
1120
+ // down 自动生成: dropIndex + dropColumn
1121
+ }));
1122
+
1123
+ await runner.migrate();
1124
+
1125
+ // 验证列和索引创建成功
1126
+ const indexes = await migrationDb.query<{ name: string }[]>(
1127
+ "SELECT name FROM sqlite_master WHERE type='index' AND name='idx_email'"
1128
+ );
1129
+ expect(indexes).toHaveLength(1);
1130
+
1131
+ // 回滚
1132
+ await runner.rollback();
1133
+
1134
+ // 验证索引被删除
1135
+ const indexesAfter = await migrationDb.query<{ name: string }[]>(
1136
+ "SELECT name FROM sqlite_master WHERE type='index' AND name='idx_email'"
1137
+ );
1138
+ expect(indexesAfter).toHaveLength(0);
1139
+ });
1140
+
1141
+ it('should auto-generate down for renameColumn', async () => {
1142
+ // 先创建表
1143
+ await migrationDb.query(`
1144
+ CREATE TABLE rename_test (id INTEGER PRIMARY KEY, old_name TEXT)
1145
+ `);
1146
+
1147
+ runner.add(defineMigration({
1148
+ name: '013_auto_down_rename',
1149
+ up: async (ctx) => {
1150
+ await ctx.renameColumn('rename_test', 'old_name', 'new_name');
1151
+ }
1152
+ // down 自动生成: renameColumn('new_name', 'old_name')
1153
+ }));
1154
+
1155
+ await runner.migrate();
1156
+
1157
+ // 验证列已重命名
1158
+ await migrationDb.query("INSERT INTO rename_test (id, new_name) VALUES (1, 'test')");
1159
+ const rows = await migrationDb.query<any[]>("SELECT new_name FROM rename_test");
1160
+ expect(rows[0].new_name).toBe('test');
1161
+
1162
+ // 回滚
1163
+ await runner.rollback();
1164
+
1165
+ // 验证列恢复原名
1166
+ await migrationDb.query("UPDATE rename_test SET old_name = 'restored' WHERE id = 1");
1167
+ const rowsAfter = await migrationDb.query<any[]>("SELECT old_name FROM rename_test");
1168
+ expect(rowsAfter[0].old_name).toBe('restored');
1169
+ });
1170
+
1171
+ it('should throw error for non-reversible operations', async () => {
1172
+ runner.add(defineMigration({
1173
+ name: '014_non_reversible',
1174
+ up: async (ctx) => {
1175
+ await ctx.dropTable('nonexistent'); // dropTable 无法自动反向
1176
+ }
1177
+ }));
1178
+
1179
+ // migrate 时应该抛出错误,因为 dropTable 无法自动反向
1180
+ await expect(runner.migrate()).rejects.toThrow(/Cannot auto-reverse/);
1181
+ });
1182
+
1183
+ it('should use explicit down when provided', async () => {
1184
+ let downCalled = false;
1185
+
1186
+ runner.add(defineMigration({
1187
+ name: '015_explicit_down',
1188
+ up: async (ctx) => {
1189
+ await ctx.createTable('explicit_down_table', {
1190
+ id: { type: 'integer', primary: true }
1191
+ });
1192
+ },
1193
+ down: async (ctx) => {
1194
+ downCalled = true;
1195
+ await ctx.dropTable('explicit_down_table');
1196
+ }
1197
+ }));
1198
+
1199
+ await runner.migrate();
1200
+ await runner.rollback();
1201
+
1202
+ // 验证显式 down 被调用
1203
+ expect(downCalled).toBe(true);
1204
+ });
1205
+ });
1206
+
1207
+ // ============================================================================
1208
+ // Lifecycle Hooks Tests
1209
+ // ============================================================================
1210
+ describe('Lifecycle Hooks', () => {
1211
+ let db: Sqlite<TestSchema>;
1212
+ let userModel: ReturnType<typeof db.model<'users'>>;
1213
+
1214
+ beforeEach(async () => {
1215
+ db = new Sqlite<TestSchema>({ filename: ':memory:' });
1216
+ await db.start();
1217
+
1218
+ // 创建测试表
1219
+ await db.query(`
1220
+ CREATE TABLE IF NOT EXISTS users (
1221
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1222
+ name TEXT NOT NULL,
1223
+ email TEXT,
1224
+ status TEXT DEFAULT 'active',
1225
+ age INTEGER DEFAULT 0,
1226
+ deletedAt DATETIME,
1227
+ createdAt DATETIME,
1228
+ updatedAt DATETIME
1229
+ )
1230
+ `);
1231
+
1232
+ userModel = db.model('users', { timestamps: true });
1233
+ });
1234
+
1235
+ afterEach(async () => {
1236
+ userModel.clearHooks();
1237
+ await db.stop();
1238
+ });
1239
+
1240
+ it('should call beforeCreate and afterCreate hooks', async () => {
1241
+ const hookCalls: string[] = [];
1242
+
1243
+ userModel
1244
+ .addHook('beforeCreate', (ctx) => {
1245
+ hookCalls.push('beforeCreate');
1246
+ expect(ctx.data).toBeDefined();
1247
+ expect(ctx.data?.name).toBe('TestUser');
1248
+ })
1249
+ .addHook('afterCreate', (ctx) => {
1250
+ hookCalls.push('afterCreate');
1251
+ expect(ctx.result).toBeDefined();
1252
+ });
1253
+
1254
+ const user = await userModel.create({ name: 'TestUser', email: 'test@test.com' });
1255
+
1256
+ expect(hookCalls).toEqual(['beforeCreate', 'afterCreate']);
1257
+ expect(user).toBeDefined();
1258
+ expect(user?.name).toBe('TestUser');
1259
+ });
1260
+
1261
+ it('should allow beforeCreate hook to cancel operation', async () => {
1262
+ userModel.addHook('beforeCreate', () => {
1263
+ return false; // 取消操作
1264
+ });
1265
+
1266
+ const result = await userModel.create({ name: 'Cancelled', email: 'cancel@test.com' });
1267
+
1268
+ expect(result).toBeNull();
1269
+
1270
+ // 验证数据没有被插入
1271
+ const count = await userModel.count();
1272
+ expect(count).toBe(0);
1273
+ });
1274
+
1275
+ it('should allow beforeCreate hook to modify data', async () => {
1276
+ userModel.addHook('beforeCreate', (ctx) => {
1277
+ if (ctx.data) {
1278
+ ctx.data.status = 'pending'; // 修改数据
1279
+ ctx.data.name = ctx.data.name?.toUpperCase();
1280
+ }
1281
+ });
1282
+
1283
+ const user = await userModel.create({ name: 'lowercase', email: 'test@test.com' });
1284
+
1285
+ expect(user?.name).toBe('LOWERCASE');
1286
+ expect(user?.status).toBe('pending');
1287
+ });
1288
+
1289
+ it('should call beforeFind and afterFind hooks', async () => {
1290
+ // 先创建数据
1291
+ await userModel.insert({ id: 1, name: 'FindMe', email: 'find@test.com' } as any);
1292
+
1293
+ const hookCalls: string[] = [];
1294
+
1295
+ userModel
1296
+ .addHook('beforeFind', (ctx) => {
1297
+ hookCalls.push('beforeFind');
1298
+ expect(ctx.where).toBeDefined();
1299
+ })
1300
+ .addHook('afterFind', (ctx) => {
1301
+ hookCalls.push('afterFind');
1302
+ expect(ctx.result).toBeDefined();
1303
+ });
1304
+
1305
+ const user = await userModel.findOne({ id: 1 });
1306
+
1307
+ expect(hookCalls).toEqual(['beforeFind', 'afterFind']);
1308
+ expect(user?.name).toBe('FindMe');
1309
+ });
1310
+
1311
+ it('should allow afterFind hook to transform result', async () => {
1312
+ await userModel.insert({ id: 1, name: 'Transform', email: 'transform@test.com' } as any);
1313
+
1314
+ userModel.addHook('afterFind', (ctx) => {
1315
+ if (ctx.result && !Array.isArray(ctx.result)) {
1316
+ (ctx.result as any).transformed = true;
1317
+ }
1318
+ });
1319
+
1320
+ const user = await userModel.findOne({ id: 1 });
1321
+
1322
+ expect((user as any)?.transformed).toBe(true);
1323
+ });
1324
+
1325
+ it('should call beforeUpdate and afterUpdate hooks', async () => {
1326
+ await userModel.insert({ id: 1, name: 'Original', email: 'update@test.com' } as any);
1327
+
1328
+ const hookCalls: string[] = [];
1329
+
1330
+ userModel
1331
+ .addHook('beforeUpdate', (ctx) => {
1332
+ hookCalls.push('beforeUpdate');
1333
+ expect(ctx.where).toBeDefined();
1334
+ expect(ctx.data).toBeDefined();
1335
+ })
1336
+ .addHook('afterUpdate', (ctx) => {
1337
+ hookCalls.push('afterUpdate');
1338
+ expect(ctx.result).toBeDefined();
1339
+ });
1340
+
1341
+ await userModel.updateById(1, { name: 'Updated' });
1342
+
1343
+ expect(hookCalls).toEqual(['beforeUpdate', 'afterUpdate']);
1344
+
1345
+ const user = await userModel.findById(1);
1346
+ expect(user?.name).toBe('Updated');
1347
+ });
1348
+
1349
+ it('should allow beforeUpdate hook to cancel operation', async () => {
1350
+ await userModel.insert({ id: 1, name: 'Protected', email: 'protect@test.com' } as any);
1351
+
1352
+ userModel.addHook('beforeUpdate', () => {
1353
+ return false; // 取消更新
1354
+ });
1355
+
1356
+ const result = await userModel.updateById(1, { name: 'Changed' });
1357
+
1358
+ expect(result).toBe(false);
1359
+
1360
+ // 验证数据没有被更新
1361
+ const user = await userModel.findById(1);
1362
+ expect(user?.name).toBe('Protected');
1363
+ });
1364
+
1365
+ it('should call beforeDelete and afterDelete hooks', async () => {
1366
+ await userModel.insert({ id: 1, name: 'ToDelete', email: 'delete@test.com' } as any);
1367
+
1368
+ const hookCalls: string[] = [];
1369
+
1370
+ userModel
1371
+ .addHook('beforeDelete', (ctx) => {
1372
+ hookCalls.push('beforeDelete');
1373
+ expect(ctx.where).toBeDefined();
1374
+ })
1375
+ .addHook('afterDelete', (ctx) => {
1376
+ hookCalls.push('afterDelete');
1377
+ expect(ctx.result).toBeDefined();
1378
+ });
1379
+
1380
+ await userModel.deleteById(1);
1381
+
1382
+ expect(hookCalls).toEqual(['beforeDelete', 'afterDelete']);
1383
+ });
1384
+
1385
+ it('should allow beforeDelete hook to cancel operation', async () => {
1386
+ await userModel.insert({ id: 1, name: 'Protected', email: 'nodelete@test.com' } as any);
1387
+
1388
+ userModel.addHook('beforeDelete', () => {
1389
+ return false; // 取消删除
1390
+ });
1391
+
1392
+ const result = await userModel.deleteById(1);
1393
+
1394
+ expect(result).toBe(false);
1395
+
1396
+ // 验证数据没有被删除
1397
+ const user = await userModel.findById(1);
1398
+ expect(user).toBeDefined();
1399
+ });
1400
+
1401
+ it('should support multiple hooks for same event', async () => {
1402
+ const order: number[] = [];
1403
+
1404
+ userModel
1405
+ .addHook('beforeCreate', () => { order.push(1); })
1406
+ .addHook('beforeCreate', () => { order.push(2); })
1407
+ .addHook('beforeCreate', () => { order.push(3); });
1408
+
1409
+ await userModel.create({ name: 'Multi', email: 'multi@test.com' });
1410
+
1411
+ expect(order).toEqual([1, 2, 3]);
1412
+ });
1413
+
1414
+ it('should support registerHooks for batch registration', async () => {
1415
+ const hookCalls: string[] = [];
1416
+
1417
+ userModel.registerHooks({
1418
+ beforeCreate: (ctx) => { hookCalls.push('beforeCreate'); },
1419
+ afterCreate: [
1420
+ () => { hookCalls.push('afterCreate1'); },
1421
+ () => { hookCalls.push('afterCreate2'); }
1422
+ ]
1423
+ });
1424
+
1425
+ await userModel.create({ name: 'Batch', email: 'batch@test.com' });
1426
+
1427
+ expect(hookCalls).toEqual(['beforeCreate', 'afterCreate1', 'afterCreate2']);
1428
+ });
1429
+
1430
+ it('should support removeHook', async () => {
1431
+ const hookCalls: string[] = [];
1432
+ const hook1 = () => { hookCalls.push('hook1'); };
1433
+ const hook2 = () => { hookCalls.push('hook2'); };
1434
+
1435
+ userModel
1436
+ .addHook('beforeCreate', hook1)
1437
+ .addHook('beforeCreate', hook2)
1438
+ .removeHook('beforeCreate', hook1);
1439
+
1440
+ await userModel.create({ name: 'Remove', email: 'remove@test.com' });
1441
+
1442
+ expect(hookCalls).toEqual(['hook2']);
1443
+ });
1444
+
1445
+ it('should support clearHooks', async () => {
1446
+ const hookCalls: string[] = [];
1447
+
1448
+ userModel
1449
+ .addHook('beforeCreate', () => { hookCalls.push('hook'); })
1450
+ .clearHooks();
1451
+
1452
+ await userModel.create({ name: 'Clear', email: 'clear@test.com' });
1453
+
1454
+ expect(hookCalls).toEqual([]);
1455
+ });
1456
+
1457
+ it('should support async hooks', async () => {
1458
+ const hookCalls: string[] = [];
1459
+
1460
+ userModel.addHook('beforeCreate', async (ctx) => {
1461
+ await new Promise(resolve => setTimeout(resolve, 10));
1462
+ hookCalls.push('asyncHook');
1463
+ });
1464
+
1465
+ await userModel.create({ name: 'Async', email: 'async@test.com' });
1466
+
1467
+ expect(hookCalls).toEqual(['asyncHook']);
1468
+ });
1469
+
1470
+ it('should work with findAll hook', async () => {
1471
+ await userModel.insertMany([
1472
+ { id: 1, name: 'User1', email: 'u1@test.com' },
1473
+ { id: 2, name: 'User2', email: 'u2@test.com' }
1474
+ ] as any);
1475
+
1476
+ const hookCalls: string[] = [];
1477
+
1478
+ userModel
1479
+ .addHook('beforeFind', () => { hookCalls.push('beforeFind'); })
1480
+ .addHook('afterFind', (ctx) => {
1481
+ hookCalls.push('afterFind');
1482
+ expect(Array.isArray(ctx.result)).toBe(true);
1483
+ expect((ctx.result as any[]).length).toBe(2);
1484
+ });
1485
+
1486
+ const users = await userModel.findAll();
1487
+
1488
+ expect(hookCalls).toEqual(['beforeFind', 'afterFind']);
1489
+ expect(users.length).toBe(2);
1490
+ });
1491
+ });
1492
+
1493
+ // ============================================================================
1494
+ // Many-to-Many (belongsToMany) Tests
1495
+ // ============================================================================
1496
+ describe('Many-to-Many Relations (belongsToMany)', () => {
1497
+ let db: Sqlite<TestSchema & {
1498
+ roles: { id: number; name: string };
1499
+ user_roles: { user_id: number; role_id: number; assigned_at: string };
1500
+ tags: { id: number; name: string };
1501
+ post_tags: { post_id: number; tag_id: number; sort_order: number };
1502
+ posts: { id: number; title: string; userId: number };
1503
+ }>;
1504
+
1505
+ beforeAll(async () => {
1506
+ db = new Sqlite({ filename: ':memory:' });
1507
+ await db.start();
1508
+
1509
+ // 创建用户表
1510
+ await db.query(`
1511
+ CREATE TABLE IF NOT EXISTS users (
1512
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1513
+ name TEXT NOT NULL,
1514
+ email TEXT
1515
+ )
1516
+ `);
1517
+
1518
+ // 创建角色表
1519
+ await db.query(`
1520
+ CREATE TABLE IF NOT EXISTS roles (
1521
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1522
+ name TEXT NOT NULL
1523
+ )
1524
+ `);
1525
+
1526
+ // 创建用户-角色中间表
1527
+ await db.query(`
1528
+ CREATE TABLE IF NOT EXISTS user_roles (
1529
+ user_id INTEGER NOT NULL,
1530
+ role_id INTEGER NOT NULL,
1531
+ assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1532
+ PRIMARY KEY (user_id, role_id)
1533
+ )
1534
+ `);
1535
+
1536
+ // 创建文章表
1537
+ await db.query(`
1538
+ CREATE TABLE IF NOT EXISTS posts (
1539
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1540
+ title TEXT NOT NULL,
1541
+ userId INTEGER
1542
+ )
1543
+ `);
1544
+
1545
+ // 创建标签表
1546
+ await db.query(`
1547
+ CREATE TABLE IF NOT EXISTS tags (
1548
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1549
+ name TEXT NOT NULL
1550
+ )
1551
+ `);
1552
+
1553
+ // 创建文章-标签中间表
1554
+ await db.query(`
1555
+ CREATE TABLE IF NOT EXISTS post_tags (
1556
+ post_id INTEGER NOT NULL,
1557
+ tag_id INTEGER NOT NULL,
1558
+ sort_order INTEGER DEFAULT 0,
1559
+ PRIMARY KEY (post_id, tag_id)
1560
+ )
1561
+ `);
1562
+ });
1563
+
1564
+ afterAll(async () => {
1565
+ await db.stop();
1566
+ });
1567
+
1568
+ beforeEach(async () => {
1569
+ // 清空数据
1570
+ await db.query('DELETE FROM user_roles');
1571
+ await db.query('DELETE FROM post_tags');
1572
+ await db.query('DELETE FROM users');
1573
+ await db.query('DELETE FROM roles');
1574
+ await db.query('DELETE FROM posts');
1575
+ await db.query('DELETE FROM tags');
1576
+ });
1577
+
1578
+ it('should define belongsToMany relation', async () => {
1579
+ const userModel = db.model('users');
1580
+ const roleModel = db.model('roles');
1581
+
1582
+ // 定义多对多关系
1583
+ userModel.belongsToMany(roleModel, 'user_roles', 'user_id', 'role_id');
1584
+
1585
+ // 验证关系已定义
1586
+ const relation = userModel.getRelation('roles');
1587
+ expect(relation).toBeDefined();
1588
+ expect(relation?.type).toBe('belongsToMany');
1589
+ expect(relation?.pivot?.table).toBe('user_roles');
1590
+ });
1591
+
1592
+ it('should load many-to-many relation for single record', async () => {
1593
+ // 插入测试数据
1594
+ await db.query("INSERT INTO users (id, name) VALUES (1, 'Alice')");
1595
+ await db.query("INSERT INTO roles (id, name) VALUES (1, 'admin'), (2, 'editor')");
1596
+ await db.query("INSERT INTO user_roles (user_id, role_id) VALUES (1, 1), (1, 2)");
1597
+
1598
+ const userModel = db.model('users');
1599
+ const roleModel = db.model('roles');
1600
+ userModel.belongsToMany(roleModel, 'user_roles', 'user_id', 'role_id');
1601
+
1602
+ // 获取用户并加载角色
1603
+ const user = await userModel.findById(1);
1604
+ expect(user).toBeDefined();
1605
+
1606
+ const userWithRoles = await userModel.loadRelation(user!, 'roles');
1607
+
1608
+ expect(userWithRoles.roles).toBeDefined();
1609
+ const roles = userWithRoles.roles as any[];
1610
+ expect(roles).toHaveLength(2);
1611
+ expect(roles.map(r => r.name)).toContain('admin');
1612
+ expect(roles.map(r => r.name)).toContain('editor');
1613
+ });
1614
+
1615
+ it('should load many-to-many relation with pivot data', async () => {
1616
+ // 插入测试数据
1617
+ await db.query("INSERT INTO posts (id, title) VALUES (1, 'Post 1')");
1618
+ await db.query("INSERT INTO tags (id, name) VALUES (1, 'TypeScript'), (2, 'JavaScript')");
1619
+ await db.query("INSERT INTO post_tags (post_id, tag_id, sort_order) VALUES (1, 1, 10), (1, 2, 20)");
1620
+
1621
+ const postModel = db.model('posts');
1622
+ const tagModel = db.model('tags');
1623
+
1624
+ // 定义关系,包含 pivot 字段
1625
+ postModel.belongsToMany(tagModel, 'post_tags', 'post_id', 'tag_id', 'id', 'id', ['sort_order']);
1626
+
1627
+ const post = await postModel.findById(1);
1628
+ const postWithTags = await postModel.loadRelation(post!, 'tags');
1629
+
1630
+ const tags = postWithTags.tags as any[];
1631
+ expect(tags).toHaveLength(2);
1632
+ // 验证 pivot 数据
1633
+ const tsTag = tags.find(t => t.name === 'TypeScript');
1634
+ expect(tsTag?.pivot?.sort_order).toBe(10);
1635
+ });
1636
+
1637
+ it('should batch load many-to-many relations (with())', async () => {
1638
+ // 插入测试数据
1639
+ await db.query("INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob')");
1640
+ await db.query("INSERT INTO roles (id, name) VALUES (1, 'admin'), (2, 'editor'), (3, 'viewer')");
1641
+ await db.query("INSERT INTO user_roles (user_id, role_id) VALUES (1, 1), (1, 2), (2, 2), (2, 3)");
1642
+
1643
+ const userModel = db.model('users');
1644
+ const roleModel = db.model('roles');
1645
+ userModel.belongsToMany(roleModel, 'user_roles', 'user_id', 'role_id');
1646
+
1647
+ // 使用 with() 批量加载
1648
+ const usersWithRoles = await userModel.with('roles');
1649
+
1650
+ expect(usersWithRoles).toHaveLength(2);
1651
+
1652
+ const alice = usersWithRoles.find((u: any) => u.name === 'Alice');
1653
+ const bob = usersWithRoles.find((u: any) => u.name === 'Bob');
1654
+
1655
+ expect(alice?.roles).toHaveLength(2);
1656
+ expect(bob?.roles).toHaveLength(2);
1657
+ });
1658
+
1659
+ it('should support bidirectional many-to-many', async () => {
1660
+ // 插入测试数据
1661
+ await db.query("INSERT INTO users (id, name) VALUES (1, 'Alice')");
1662
+ await db.query("INSERT INTO roles (id, name) VALUES (1, 'admin')");
1663
+ await db.query("INSERT INTO user_roles (user_id, role_id) VALUES (1, 1)");
1664
+
1665
+ const userModel = db.model('users');
1666
+ const roleModel = db.model('roles');
1667
+
1668
+ // 双向关系定义
1669
+ userModel.belongsToMany(roleModel, 'user_roles', 'user_id', 'role_id');
1670
+ roleModel.belongsToMany(userModel, 'user_roles', 'role_id', 'user_id');
1671
+
1672
+ // 从用户侧查询
1673
+ const user = await userModel.findById(1);
1674
+ const userWithRoles = await userModel.loadRelation(user!, 'roles');
1675
+ const roles = userWithRoles.roles as any[];
1676
+ expect(roles).toHaveLength(1);
1677
+ expect(roles[0].name).toBe('admin');
1678
+
1679
+ // 从角色侧查询
1680
+ const role = await roleModel.findById(1);
1681
+ const roleWithUsers = await roleModel.loadRelation(role!, 'users');
1682
+ const users = roleWithUsers.users as any[];
1683
+ expect(users).toHaveLength(1);
1684
+ expect(users[0].name).toBe('Alice');
1685
+ });
1686
+
1687
+ it('should return empty array for records without relations', async () => {
1688
+ await db.query("INSERT INTO users (id, name) VALUES (1, 'Alice')");
1689
+ // 不插入 user_roles
1690
+
1691
+ const userModel = db.model('users');
1692
+ const roleModel = db.model('roles');
1693
+ userModel.belongsToMany(roleModel, 'user_roles', 'user_id', 'role_id');
1694
+
1695
+ const user = await userModel.findById(1);
1696
+ const userWithRoles = await userModel.loadRelation(user!, 'roles');
1697
+
1698
+ expect(userWithRoles.roles).toEqual([]);
1699
+ });
1700
+
1701
+ it('should support schema-based belongsToMany definition', async () => {
1702
+ // 创建新的数据库实例
1703
+ const db2 = new Sqlite({ filename: ':memory:' });
1704
+ await db2.start();
1705
+
1706
+ // 创建表
1707
+ await db2.query(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`);
1708
+ await db2.query(`CREATE TABLE roles (id INTEGER PRIMARY KEY, name TEXT)`);
1709
+ await db2.query(`CREATE TABLE user_roles (user_id INTEGER, role_id INTEGER)`);
1710
+
1711
+ // 插入数据
1712
+ await db2.query("INSERT INTO users (id, name) VALUES (1, 'Alice')");
1713
+ await db2.query("INSERT INTO roles (id, name) VALUES (1, 'admin')");
1714
+ await db2.query("INSERT INTO user_roles (user_id, role_id) VALUES (1, 1)");
1715
+
1716
+ // 使用 schema 定义关系
1717
+ db2.defineRelations({
1718
+ users: {
1719
+ belongsToMany: {
1720
+ roles: {
1721
+ pivot: 'user_roles',
1722
+ foreignKey: 'user_id',
1723
+ relatedKey: 'role_id'
1724
+ }
1725
+ }
1726
+ }
1727
+ } as any);
1728
+
1729
+ const userModel = db2.model('users');
1730
+ const usersWithRoles = await userModel.with('roles');
1731
+
1732
+ expect(usersWithRoles).toHaveLength(1);
1733
+ expect(usersWithRoles[0].roles).toHaveLength(1);
1734
+
1735
+ await db2.stop();
1736
+ });
1737
+ });
1738
+