@geekmidas/db 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/kysely/pagination.cjs.map +1 -1
- package/dist/kysely/pagination.d.cts +1 -0
- package/dist/kysely/pagination.d.cts.map +1 -0
- package/dist/kysely/pagination.d.mts +1 -0
- package/dist/kysely/pagination.d.mts.map +1 -0
- package/dist/kysely/pagination.mjs.map +1 -1
- package/dist/kysely-8WPSKCZG.cjs.map +1 -1
- package/dist/{kysely-0FOi6ZdO.d.cts → kysely-D3eeuId4.d.cts} +3 -1
- package/dist/kysely-D3eeuId4.d.cts.map +1 -0
- package/dist/{kysely-Di1LVvL2.d.mts → kysely-DUdsB0BP.d.mts} +3 -1
- package/dist/kysely-DUdsB0BP.d.mts.map +1 -0
- package/dist/kysely-DmfA94RY.mjs.map +1 -1
- package/dist/kysely.d.cts +1 -1
- package/dist/kysely.d.mts +1 -1
- package/dist/rls.cjs.map +1 -1
- package/dist/rls.d.cts +2 -1
- package/dist/rls.d.cts.map +1 -0
- package/dist/rls.d.mts +2 -1
- package/dist/rls.d.mts.map +1 -0
- package/dist/rls.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/kysely.integration.spec.ts +729 -729
- package/src/__tests__/kysely.spec.ts +388 -388
- package/src/__tests__/rls.spec.ts +399 -399
- package/src/kysely/__tests__/pagination.integration.spec.ts +327 -327
- package/src/kysely/pagination.ts +84 -84
- package/src/kysely.ts +19 -19
- package/src/rls.ts +31 -31
- package/tsconfig.json +9 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
CamelCasePlugin,
|
|
3
|
+
type Generated,
|
|
4
|
+
Kysely,
|
|
5
|
+
PostgresDialect,
|
|
6
|
+
sql,
|
|
7
7
|
} from 'kysely';
|
|
8
8
|
import pg from 'pg';
|
|
9
9
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
@@ -11,731 +11,731 @@ import { TEST_DATABASE_CONFIG } from '../../../testkit/test/globalSetup';
|
|
|
11
11
|
import { withTransaction } from '../kysely';
|
|
12
12
|
|
|
13
13
|
interface TestDatabase {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
kyselyTrxUsers: {
|
|
15
|
+
id: Generated<number>;
|
|
16
|
+
name: string;
|
|
17
|
+
email: string;
|
|
18
|
+
createdAt: Generated<Date>;
|
|
19
|
+
};
|
|
20
|
+
kyselyTrxAccounts: {
|
|
21
|
+
id: Generated<number>;
|
|
22
|
+
userId: number;
|
|
23
|
+
balance: number;
|
|
24
|
+
version: number;
|
|
25
|
+
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe('Kysely Transaction Integration Tests', () => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
29
|
+
let db: Kysely<TestDatabase>;
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
db = new Kysely<TestDatabase>({
|
|
33
|
+
dialect: new PostgresDialect({
|
|
34
|
+
pool: new pg.Pool({
|
|
35
|
+
...TEST_DATABASE_CONFIG,
|
|
36
|
+
database: 'postgres',
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
plugins: [new CamelCasePlugin()],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Create users table
|
|
43
|
+
await db.schema
|
|
44
|
+
.createTable('kysely_trx_users')
|
|
45
|
+
.ifNotExists()
|
|
46
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
47
|
+
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
48
|
+
.addColumn('email', 'varchar', (col) => col.notNull().unique())
|
|
49
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
50
|
+
col.defaultTo(sql`now()`).notNull(),
|
|
51
|
+
)
|
|
52
|
+
.execute();
|
|
53
|
+
|
|
54
|
+
// Create accounts table
|
|
55
|
+
await db.schema
|
|
56
|
+
.createTable('kysely_trx_accounts')
|
|
57
|
+
.ifNotExists()
|
|
58
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
59
|
+
.addColumn('user_id', 'integer', (col) =>
|
|
60
|
+
col.notNull().references('kysely_trx_users.id').onDelete('cascade'),
|
|
61
|
+
)
|
|
62
|
+
.addColumn('balance', 'numeric(10, 2)', (col) =>
|
|
63
|
+
col.notNull().defaultTo(0),
|
|
64
|
+
)
|
|
65
|
+
.addColumn('version', 'integer', (col) => col.notNull().defaultTo(0))
|
|
66
|
+
.execute();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
// Clean up data after each test
|
|
71
|
+
await db.deleteFrom('kyselyTrxAccounts').execute();
|
|
72
|
+
await db.deleteFrom('kyselyTrxUsers').execute();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
// Drop tables and close connection
|
|
77
|
+
await db.schema.dropTable('kysely_trx_accounts').ifExists().execute();
|
|
78
|
+
await db.schema.dropTable('kysely_trx_users').ifExists().execute();
|
|
79
|
+
await db.destroy();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('withTransaction - Real Database Operations', () => {
|
|
83
|
+
it('should execute real insert and select within transaction', async () => {
|
|
84
|
+
const result = await withTransaction(db, async (trx) => {
|
|
85
|
+
// Insert a user
|
|
86
|
+
const user = await trx
|
|
87
|
+
.insertInto('kyselyTrxUsers')
|
|
88
|
+
.values({
|
|
89
|
+
name: 'John Doe',
|
|
90
|
+
email: 'john@example.com',
|
|
91
|
+
})
|
|
92
|
+
.returningAll()
|
|
93
|
+
.executeTakeFirstOrThrow();
|
|
94
|
+
|
|
95
|
+
// Verify we can select it within the same transaction
|
|
96
|
+
const foundUser = await trx
|
|
97
|
+
.selectFrom('kyselyTrxUsers')
|
|
98
|
+
.selectAll()
|
|
99
|
+
.where('id', '=', user.id)
|
|
100
|
+
.executeTakeFirstOrThrow();
|
|
101
|
+
|
|
102
|
+
expect(foundUser.name).toBe('John Doe');
|
|
103
|
+
expect(foundUser.email).toBe('john@example.com');
|
|
104
|
+
|
|
105
|
+
return user;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(result).toBeDefined();
|
|
109
|
+
expect(result.id).toBeDefined();
|
|
110
|
+
expect(result.name).toBe('John Doe');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should rollback on error', async () => {
|
|
114
|
+
const insertPromise = withTransaction(db, async (trx) => {
|
|
115
|
+
// Insert a user
|
|
116
|
+
await trx
|
|
117
|
+
.insertInto('kyselyTrxUsers')
|
|
118
|
+
.values({
|
|
119
|
+
name: 'Will Be Rolled Back',
|
|
120
|
+
email: 'rollback@example.com',
|
|
121
|
+
})
|
|
122
|
+
.execute();
|
|
123
|
+
|
|
124
|
+
// Throw an error
|
|
125
|
+
throw new Error('Transaction should rollback');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await expect(insertPromise).rejects.toThrow(
|
|
129
|
+
'Transaction should rollback',
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Verify user was not created
|
|
133
|
+
const users = await db
|
|
134
|
+
.selectFrom('kyselyTrxUsers')
|
|
135
|
+
.selectAll()
|
|
136
|
+
.where('email', '=', 'rollback@example.com')
|
|
137
|
+
.execute();
|
|
138
|
+
|
|
139
|
+
expect(users).toHaveLength(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should reuse existing transaction', async () => {
|
|
143
|
+
await withTransaction(db, async (trx1) => {
|
|
144
|
+
// Insert user in outer transaction
|
|
145
|
+
const user = await trx1
|
|
146
|
+
.insertInto('kyselyTrxUsers')
|
|
147
|
+
.values({
|
|
148
|
+
name: 'Outer Transaction',
|
|
149
|
+
email: 'outer@example.com',
|
|
150
|
+
})
|
|
151
|
+
.returningAll()
|
|
152
|
+
.executeTakeFirstOrThrow();
|
|
153
|
+
|
|
154
|
+
// Nested transaction should reuse the same transaction
|
|
155
|
+
await withTransaction(trx1, async (trx2) => {
|
|
156
|
+
// Should see the user from outer transaction
|
|
157
|
+
const foundUser = await trx2
|
|
158
|
+
.selectFrom('kyselyTrxUsers')
|
|
159
|
+
.selectAll()
|
|
160
|
+
.where('id', '=', user.id)
|
|
161
|
+
.executeTakeFirstOrThrow();
|
|
162
|
+
|
|
163
|
+
expect(foundUser.name).toBe('Outer Transaction');
|
|
164
|
+
|
|
165
|
+
// Insert an account
|
|
166
|
+
await trx2
|
|
167
|
+
.insertInto('kyselyTrxAccounts')
|
|
168
|
+
.values({
|
|
169
|
+
userId: user.id,
|
|
170
|
+
balance: 1000,
|
|
171
|
+
version: 0,
|
|
172
|
+
})
|
|
173
|
+
.execute();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Verify account was created in the same transaction
|
|
177
|
+
const accounts = await trx1
|
|
178
|
+
.selectFrom('kyselyTrxAccounts')
|
|
179
|
+
.selectAll()
|
|
180
|
+
.where('userId', '=', user.id)
|
|
181
|
+
.execute();
|
|
182
|
+
|
|
183
|
+
expect(accounts).toHaveLength(1);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('SELECT FOR UPDATE', () => {
|
|
189
|
+
it('should lock row with SELECT FOR UPDATE', async () => {
|
|
190
|
+
// Create a user and account
|
|
191
|
+
const user = await db
|
|
192
|
+
.insertInto('kyselyTrxUsers')
|
|
193
|
+
.values({
|
|
194
|
+
name: 'Test User',
|
|
195
|
+
email: 'test@example.com',
|
|
196
|
+
})
|
|
197
|
+
.returningAll()
|
|
198
|
+
.executeTakeFirstOrThrow();
|
|
199
|
+
|
|
200
|
+
const account = await db
|
|
201
|
+
.insertInto('kyselyTrxAccounts')
|
|
202
|
+
.values({
|
|
203
|
+
userId: user.id,
|
|
204
|
+
balance: 1000,
|
|
205
|
+
version: 0,
|
|
206
|
+
})
|
|
207
|
+
.returningAll()
|
|
208
|
+
.executeTakeFirstOrThrow();
|
|
209
|
+
|
|
210
|
+
// Start two concurrent transactions
|
|
211
|
+
const transaction1Promise = withTransaction(db, async (trx1) => {
|
|
212
|
+
// Lock the account row
|
|
213
|
+
const lockedAccount = await trx1
|
|
214
|
+
.selectFrom('kyselyTrxAccounts')
|
|
215
|
+
.selectAll()
|
|
216
|
+
.where('id', '=', account.id)
|
|
217
|
+
.forUpdate()
|
|
218
|
+
.executeTakeFirstOrThrow();
|
|
219
|
+
|
|
220
|
+
expect(lockedAccount.balance).toBe('1000.00');
|
|
221
|
+
|
|
222
|
+
// Simulate some processing time
|
|
223
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
224
|
+
|
|
225
|
+
// Update the balance
|
|
226
|
+
await trx1
|
|
227
|
+
.updateTable('kyselyTrxAccounts')
|
|
228
|
+
.set({
|
|
229
|
+
balance: sql`balance - 100`,
|
|
230
|
+
version: sql`version + 1`,
|
|
231
|
+
})
|
|
232
|
+
.where('id', '=', account.id)
|
|
233
|
+
.execute();
|
|
234
|
+
|
|
235
|
+
return 'transaction1-complete';
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Wait a bit to ensure transaction1 has acquired the lock
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
240
|
+
|
|
241
|
+
// This transaction should wait for the lock
|
|
242
|
+
const transaction2Promise = withTransaction(db, async (trx2) => {
|
|
243
|
+
// This will wait for transaction1 to release the lock
|
|
244
|
+
const lockedAccount = await trx2
|
|
245
|
+
.selectFrom('kyselyTrxAccounts')
|
|
246
|
+
.selectAll()
|
|
247
|
+
.where('id', '=', account.id)
|
|
248
|
+
.forUpdate()
|
|
249
|
+
.executeTakeFirstOrThrow();
|
|
250
|
+
|
|
251
|
+
// By the time we get here, transaction1 should have completed
|
|
252
|
+
expect(Number(lockedAccount.balance)).toBe(900);
|
|
253
|
+
|
|
254
|
+
// Update the balance again
|
|
255
|
+
await trx2
|
|
256
|
+
.updateTable('kyselyTrxAccounts')
|
|
257
|
+
.set({
|
|
258
|
+
balance: sql`balance - 50`,
|
|
259
|
+
version: sql`version + 1`,
|
|
260
|
+
})
|
|
261
|
+
.where('id', '=', account.id)
|
|
262
|
+
.execute();
|
|
263
|
+
|
|
264
|
+
return 'transaction2-complete';
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Wait for both transactions to complete
|
|
268
|
+
const results = await Promise.all([
|
|
269
|
+
transaction1Promise,
|
|
270
|
+
transaction2Promise,
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
expect(results).toEqual([
|
|
274
|
+
'transaction1-complete',
|
|
275
|
+
'transaction2-complete',
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
// Verify final balance
|
|
279
|
+
const finalAccount = await db
|
|
280
|
+
.selectFrom('kyselyTrxAccounts')
|
|
281
|
+
.selectAll()
|
|
282
|
+
.where('id', '=', account.id)
|
|
283
|
+
.executeTakeFirstOrThrow();
|
|
284
|
+
|
|
285
|
+
expect(Number(finalAccount.balance)).toBe(850);
|
|
286
|
+
expect(finalAccount.version).toBe(2);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should prevent concurrent updates with SELECT FOR UPDATE', async () => {
|
|
290
|
+
// Create user and account
|
|
291
|
+
const user = await db
|
|
292
|
+
.insertInto('kyselyTrxUsers')
|
|
293
|
+
.values({
|
|
294
|
+
name: 'Concurrent User',
|
|
295
|
+
email: 'concurrent@example.com',
|
|
296
|
+
})
|
|
297
|
+
.returningAll()
|
|
298
|
+
.executeTakeFirstOrThrow();
|
|
299
|
+
|
|
300
|
+
const account = await db
|
|
301
|
+
.insertInto('kyselyTrxAccounts')
|
|
302
|
+
.values({
|
|
303
|
+
userId: user.id,
|
|
304
|
+
balance: 500,
|
|
305
|
+
version: 0,
|
|
306
|
+
})
|
|
307
|
+
.returningAll()
|
|
308
|
+
.executeTakeFirstOrThrow();
|
|
309
|
+
|
|
310
|
+
// Perform multiple concurrent withdrawals
|
|
311
|
+
const withdrawals = [100, 150, 200];
|
|
312
|
+
const results = await Promise.all(
|
|
313
|
+
withdrawals.map((amount) =>
|
|
314
|
+
withTransaction(db, async (trx) => {
|
|
315
|
+
// Lock the row
|
|
316
|
+
const currentAccount = await trx
|
|
317
|
+
.selectFrom('kyselyTrxAccounts')
|
|
318
|
+
.selectAll()
|
|
319
|
+
.where('id', '=', account.id)
|
|
320
|
+
.forUpdate()
|
|
321
|
+
.executeTakeFirstOrThrow();
|
|
322
|
+
|
|
323
|
+
// Check if there's enough balance
|
|
324
|
+
if (Number(currentAccount.balance) >= amount) {
|
|
325
|
+
await trx
|
|
326
|
+
.updateTable('kyselyTrxAccounts')
|
|
327
|
+
.set({
|
|
328
|
+
balance: sql`balance - ${amount}`,
|
|
329
|
+
version: sql`version + 1`,
|
|
330
|
+
})
|
|
331
|
+
.where('id', '=', account.id)
|
|
332
|
+
.execute();
|
|
333
|
+
|
|
334
|
+
return { success: true, amount };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { success: false, amount };
|
|
338
|
+
}),
|
|
339
|
+
),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Verify that withdrawals were serialized correctly
|
|
343
|
+
const finalAccount = await db
|
|
344
|
+
.selectFrom('kyselyTrxAccounts')
|
|
345
|
+
.selectAll()
|
|
346
|
+
.where('id', '=', account.id)
|
|
347
|
+
.executeTakeFirstOrThrow();
|
|
348
|
+
|
|
349
|
+
const totalWithdrawn = results
|
|
350
|
+
.filter((r) => r.success)
|
|
351
|
+
.reduce((sum, r) => sum + r.amount, 0);
|
|
352
|
+
|
|
353
|
+
expect(Number(finalAccount.balance)).toBe(500 - totalWithdrawn);
|
|
354
|
+
expect(finalAccount.version).toBe(
|
|
355
|
+
results.filter((r) => r.success).length,
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('Isolation Levels', () => {
|
|
361
|
+
it('should set READ COMMITTED isolation level', async () => {
|
|
362
|
+
const result = await withTransaction(
|
|
363
|
+
db,
|
|
364
|
+
async (trx) => {
|
|
365
|
+
// Insert a user
|
|
366
|
+
const user = await trx
|
|
367
|
+
.insertInto('kyselyTrxUsers')
|
|
368
|
+
.values({
|
|
369
|
+
name: 'Read Committed User',
|
|
370
|
+
email: 'readcommitted@example.com',
|
|
371
|
+
})
|
|
372
|
+
.returningAll()
|
|
373
|
+
.executeTakeFirstOrThrow();
|
|
374
|
+
|
|
375
|
+
return user;
|
|
376
|
+
},
|
|
377
|
+
{ isolationLevel: 'read committed' },
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(result).toBeDefined();
|
|
381
|
+
expect(result.name).toBe('Read Committed User');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should set REPEATABLE READ isolation level', async () => {
|
|
385
|
+
const result = await withTransaction(
|
|
386
|
+
db,
|
|
387
|
+
async (trx) => {
|
|
388
|
+
// Insert a user
|
|
389
|
+
const user = await trx
|
|
390
|
+
.insertInto('kyselyTrxUsers')
|
|
391
|
+
.values({
|
|
392
|
+
name: 'Repeatable Read User',
|
|
393
|
+
email: 'repeatableread@example.com',
|
|
394
|
+
})
|
|
395
|
+
.returningAll()
|
|
396
|
+
.executeTakeFirstOrThrow();
|
|
397
|
+
|
|
398
|
+
return user;
|
|
399
|
+
},
|
|
400
|
+
{ isolationLevel: 'repeatable read' },
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(result).toBeDefined();
|
|
404
|
+
expect(result.name).toBe('Repeatable Read User');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should set SERIALIZABLE isolation level', async () => {
|
|
408
|
+
const result = await withTransaction(
|
|
409
|
+
db,
|
|
410
|
+
async (trx) => {
|
|
411
|
+
// Insert a user
|
|
412
|
+
const user = await trx
|
|
413
|
+
.insertInto('kyselyTrxUsers')
|
|
414
|
+
.values({
|
|
415
|
+
name: 'Serializable User',
|
|
416
|
+
email: 'serializable@example.com',
|
|
417
|
+
})
|
|
418
|
+
.returningAll()
|
|
419
|
+
.executeTakeFirstOrThrow();
|
|
420
|
+
|
|
421
|
+
return user;
|
|
422
|
+
},
|
|
423
|
+
{ isolationLevel: 'serializable' },
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
expect(result).toBeDefined();
|
|
427
|
+
expect(result.name).toBe('Serializable User');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should demonstrate READ COMMITTED allows non-repeatable reads', async () => {
|
|
431
|
+
// Create a user first
|
|
432
|
+
const user = await db
|
|
433
|
+
.insertInto('kyselyTrxUsers')
|
|
434
|
+
.values({
|
|
435
|
+
name: 'Original Name',
|
|
436
|
+
email: 'nonrepeatable@example.com',
|
|
437
|
+
})
|
|
438
|
+
.returningAll()
|
|
439
|
+
.executeTakeFirstOrThrow();
|
|
440
|
+
|
|
441
|
+
// Start a READ COMMITTED transaction
|
|
442
|
+
const readTransaction = withTransaction(
|
|
443
|
+
db,
|
|
444
|
+
async (trx) => {
|
|
445
|
+
// First read
|
|
446
|
+
const firstUser = await trx
|
|
447
|
+
.selectFrom('kyselyTrxUsers')
|
|
448
|
+
.selectAll()
|
|
449
|
+
.where('id', '=', user.id)
|
|
450
|
+
.executeTakeFirstOrThrow();
|
|
451
|
+
|
|
452
|
+
// Wait for concurrent update
|
|
453
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
454
|
+
|
|
455
|
+
// Second read (should see the update in READ COMMITTED)
|
|
456
|
+
const secondUser = await trx
|
|
457
|
+
.selectFrom('kyselyTrxUsers')
|
|
458
|
+
.selectAll()
|
|
459
|
+
.where('id', '=', user.id)
|
|
460
|
+
.executeTakeFirstOrThrow();
|
|
461
|
+
|
|
462
|
+
return { firstRead: firstUser.name, secondRead: secondUser.name };
|
|
463
|
+
},
|
|
464
|
+
{ isolationLevel: 'read committed' },
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// After first read, update the user in a separate transaction
|
|
468
|
+
setTimeout(() => {
|
|
469
|
+
db.updateTable('kyselyTrxUsers')
|
|
470
|
+
.set({ name: 'Updated Name' })
|
|
471
|
+
.where('id', '=', user.id)
|
|
472
|
+
.execute();
|
|
473
|
+
}, 25);
|
|
474
|
+
|
|
475
|
+
const result = await readTransaction;
|
|
476
|
+
|
|
477
|
+
// In READ COMMITTED, the second read sees the committed update
|
|
478
|
+
expect(result.firstRead).toBe('Original Name');
|
|
479
|
+
expect(result.secondRead).toBe('Updated Name');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should demonstrate REPEATABLE READ prevents non-repeatable reads', async () => {
|
|
483
|
+
// Create a user first
|
|
484
|
+
const user = await db
|
|
485
|
+
.insertInto('kyselyTrxUsers')
|
|
486
|
+
.values({
|
|
487
|
+
name: 'Repeatable Original',
|
|
488
|
+
email: 'repeatable@example.com',
|
|
489
|
+
})
|
|
490
|
+
.returningAll()
|
|
491
|
+
.executeTakeFirstOrThrow();
|
|
492
|
+
|
|
493
|
+
// Start a REPEATABLE READ transaction
|
|
494
|
+
const readTransaction = withTransaction(
|
|
495
|
+
db,
|
|
496
|
+
async (trx) => {
|
|
497
|
+
// First read
|
|
498
|
+
const firstUser = await trx
|
|
499
|
+
.selectFrom('kyselyTrxUsers')
|
|
500
|
+
.selectAll()
|
|
501
|
+
.where('id', '=', user.id)
|
|
502
|
+
.executeTakeFirstOrThrow();
|
|
503
|
+
|
|
504
|
+
// Wait for concurrent update
|
|
505
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
506
|
+
|
|
507
|
+
// Second read (should still see the same value in REPEATABLE READ)
|
|
508
|
+
const secondUser = await trx
|
|
509
|
+
.selectFrom('kyselyTrxUsers')
|
|
510
|
+
.selectAll()
|
|
511
|
+
.where('id', '=', user.id)
|
|
512
|
+
.executeTakeFirstOrThrow();
|
|
513
|
+
|
|
514
|
+
return { firstRead: firstUser.name, secondRead: secondUser.name };
|
|
515
|
+
},
|
|
516
|
+
{ isolationLevel: 'repeatable read' },
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
// After first read, update the user in a separate transaction
|
|
520
|
+
setTimeout(() => {
|
|
521
|
+
db.updateTable('kyselyTrxUsers')
|
|
522
|
+
.set({ name: 'Repeatable Updated' })
|
|
523
|
+
.where('id', '=', user.id)
|
|
524
|
+
.execute();
|
|
525
|
+
}, 25);
|
|
526
|
+
|
|
527
|
+
const result = await readTransaction;
|
|
528
|
+
|
|
529
|
+
// In REPEATABLE READ, both reads see the same value
|
|
530
|
+
expect(result.firstRead).toBe('Repeatable Original');
|
|
531
|
+
expect(result.secondRead).toBe('Repeatable Original');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should demonstrate SERIALIZABLE prevents phantom reads', async () => {
|
|
535
|
+
// Create initial users
|
|
536
|
+
await db
|
|
537
|
+
.insertInto('kyselyTrxUsers')
|
|
538
|
+
.values([
|
|
539
|
+
{ name: 'User 1', email: 'user1@example.com' },
|
|
540
|
+
{ name: 'User 2', email: 'user2@example.com' },
|
|
541
|
+
])
|
|
542
|
+
.execute();
|
|
543
|
+
|
|
544
|
+
// Start a SERIALIZABLE transaction
|
|
545
|
+
const readTransaction = withTransaction(
|
|
546
|
+
db,
|
|
547
|
+
async (trx) => {
|
|
548
|
+
// First count
|
|
549
|
+
const firstCount = await trx
|
|
550
|
+
.selectFrom('kyselyTrxUsers')
|
|
551
|
+
.select(sql<number>`count(*)`.as('count'))
|
|
552
|
+
.executeTakeFirstOrThrow();
|
|
553
|
+
|
|
554
|
+
// Wait for concurrent insert
|
|
555
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
556
|
+
|
|
557
|
+
// Second count (should be the same in SERIALIZABLE)
|
|
558
|
+
const secondCount = await trx
|
|
559
|
+
.selectFrom('kyselyTrxUsers')
|
|
560
|
+
.select(sql<number>`count(*)`.as('count'))
|
|
561
|
+
.executeTakeFirstOrThrow();
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
firstCount: Number(firstCount.count),
|
|
565
|
+
secondCount: Number(secondCount.count),
|
|
566
|
+
};
|
|
567
|
+
},
|
|
568
|
+
{ isolationLevel: 'serializable' },
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
// After first count, insert a new user in a separate transaction
|
|
572
|
+
setTimeout(() => {
|
|
573
|
+
db.insertInto('kyselyTrxUsers')
|
|
574
|
+
.values({ name: 'User 3', email: 'user3@example.com' })
|
|
575
|
+
.execute();
|
|
576
|
+
}, 25);
|
|
577
|
+
|
|
578
|
+
const result = await readTransaction;
|
|
579
|
+
|
|
580
|
+
// In SERIALIZABLE, both counts see the same number of rows
|
|
581
|
+
expect(result.firstCount).toBe(2);
|
|
582
|
+
expect(result.secondCount).toBe(2);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should not apply isolation level when reusing transaction', async () => {
|
|
586
|
+
await withTransaction(
|
|
587
|
+
db,
|
|
588
|
+
async (trx1) => {
|
|
589
|
+
// Insert user in outer transaction
|
|
590
|
+
const user = await trx1
|
|
591
|
+
.insertInto('kyselyTrxUsers')
|
|
592
|
+
.values({
|
|
593
|
+
name: 'Nested Transaction Test',
|
|
594
|
+
email: 'nested@example.com',
|
|
595
|
+
})
|
|
596
|
+
.returningAll()
|
|
597
|
+
.executeTakeFirstOrThrow();
|
|
598
|
+
|
|
599
|
+
// Nested transaction with different isolation level
|
|
600
|
+
// should be ignored since it reuses the outer transaction
|
|
601
|
+
await withTransaction(
|
|
602
|
+
trx1,
|
|
603
|
+
async (trx2) => {
|
|
604
|
+
const foundUser = await trx2
|
|
605
|
+
.selectFrom('kyselyTrxUsers')
|
|
606
|
+
.selectAll()
|
|
607
|
+
.where('id', '=', user.id)
|
|
608
|
+
.executeTakeFirstOrThrow();
|
|
609
|
+
|
|
610
|
+
expect(foundUser.name).toBe('Nested Transaction Test');
|
|
611
|
+
},
|
|
612
|
+
{ isolationLevel: 'serializable' },
|
|
613
|
+
);
|
|
614
|
+
},
|
|
615
|
+
{ isolationLevel: 'read committed' },
|
|
616
|
+
);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
describe('Complex Transaction Scenarios', () => {
|
|
621
|
+
it('should handle cascading deletes within transaction', async () => {
|
|
622
|
+
await withTransaction(db, async (trx) => {
|
|
623
|
+
// Create user and account
|
|
624
|
+
const user = await trx
|
|
625
|
+
.insertInto('kyselyTrxUsers')
|
|
626
|
+
.values({
|
|
627
|
+
name: 'Delete Test User',
|
|
628
|
+
email: 'delete@example.com',
|
|
629
|
+
})
|
|
630
|
+
.returningAll()
|
|
631
|
+
.executeTakeFirstOrThrow();
|
|
632
|
+
|
|
633
|
+
await trx
|
|
634
|
+
.insertInto('kyselyTrxAccounts')
|
|
635
|
+
.values({
|
|
636
|
+
userId: user.id,
|
|
637
|
+
balance: 1000,
|
|
638
|
+
version: 0,
|
|
639
|
+
})
|
|
640
|
+
.execute();
|
|
641
|
+
|
|
642
|
+
// Delete user (should cascade to accounts)
|
|
643
|
+
await trx
|
|
644
|
+
.deleteFrom('kyselyTrxUsers')
|
|
645
|
+
.where('id', '=', user.id)
|
|
646
|
+
.execute();
|
|
647
|
+
|
|
648
|
+
// Verify cascading delete worked
|
|
649
|
+
const accounts = await trx
|
|
650
|
+
.selectFrom('kyselyTrxAccounts')
|
|
651
|
+
.selectAll()
|
|
652
|
+
.where('userId', '=', user.id)
|
|
653
|
+
.execute();
|
|
654
|
+
|
|
655
|
+
expect(accounts).toHaveLength(0);
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should handle batch inserts within transaction', async () => {
|
|
660
|
+
const userCount = await withTransaction(db, async (trx) => {
|
|
661
|
+
// Batch insert users
|
|
662
|
+
const users = await trx
|
|
663
|
+
.insertInto('kyselyTrxUsers')
|
|
664
|
+
.values([
|
|
665
|
+
{ name: 'Batch User 1', email: 'batch1@example.com' },
|
|
666
|
+
{ name: 'Batch User 2', email: 'batch2@example.com' },
|
|
667
|
+
{ name: 'Batch User 3', email: 'batch3@example.com' },
|
|
668
|
+
{ name: 'Batch User 4', email: 'batch4@example.com' },
|
|
669
|
+
{ name: 'Batch User 5', email: 'batch5@example.com' },
|
|
670
|
+
])
|
|
671
|
+
.returningAll()
|
|
672
|
+
.execute();
|
|
673
|
+
|
|
674
|
+
expect(users).toHaveLength(5);
|
|
675
|
+
|
|
676
|
+
// Count users
|
|
677
|
+
const count = await trx
|
|
678
|
+
.selectFrom('kyselyTrxUsers')
|
|
679
|
+
.select(sql<number>`count(*)`.as('count'))
|
|
680
|
+
.where('email', 'like', 'batch%')
|
|
681
|
+
.executeTakeFirstOrThrow();
|
|
682
|
+
|
|
683
|
+
return Number(count.count);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
expect(userCount).toBe(5);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should handle complex queries with joins', async () => {
|
|
690
|
+
await withTransaction(db, async (trx) => {
|
|
691
|
+
// Create test data
|
|
692
|
+
const user1 = await trx
|
|
693
|
+
.insertInto('kyselyTrxUsers')
|
|
694
|
+
.values({ name: 'Author 1', email: 'author1@example.com' })
|
|
695
|
+
.returningAll()
|
|
696
|
+
.executeTakeFirstOrThrow();
|
|
697
|
+
|
|
698
|
+
const user2 = await trx
|
|
699
|
+
.insertInto('kyselyTrxUsers')
|
|
700
|
+
.values({ name: 'Author 2', email: 'author2@example.com' })
|
|
701
|
+
.returningAll()
|
|
702
|
+
.executeTakeFirstOrThrow();
|
|
703
|
+
|
|
704
|
+
await trx
|
|
705
|
+
.insertInto('kyselyTrxAccounts')
|
|
706
|
+
.values([
|
|
707
|
+
{ userId: user1.id, balance: 1000, version: 0 },
|
|
708
|
+
{ userId: user1.id, balance: 2000, version: 0 },
|
|
709
|
+
{ userId: user2.id, balance: 3000, version: 0 },
|
|
710
|
+
])
|
|
711
|
+
.execute();
|
|
712
|
+
|
|
713
|
+
// Complex query with joins
|
|
714
|
+
const results = await trx
|
|
715
|
+
.selectFrom('kyselyTrxUsers')
|
|
716
|
+
.leftJoin(
|
|
717
|
+
'kyselyTrxAccounts',
|
|
718
|
+
'kyselyTrxUsers.id',
|
|
719
|
+
'kyselyTrxAccounts.userId',
|
|
720
|
+
)
|
|
721
|
+
.select([
|
|
722
|
+
'kyselyTrxUsers.id as userId',
|
|
723
|
+
'kyselyTrxUsers.name',
|
|
724
|
+
sql<number>`count(kysely_trx_accounts.id)`.as('accountCount'),
|
|
725
|
+
sql<number>`coalesce(sum(kysely_trx_accounts.balance), 0)`.as(
|
|
726
|
+
'totalBalance',
|
|
727
|
+
),
|
|
728
|
+
])
|
|
729
|
+
.groupBy(['kyselyTrxUsers.id', 'kyselyTrxUsers.name'])
|
|
730
|
+
.orderBy('kyselyTrxUsers.id')
|
|
731
|
+
.execute();
|
|
732
|
+
|
|
733
|
+
expect(results).toHaveLength(2);
|
|
734
|
+
expect(Number(results[0].accountCount)).toBe(2);
|
|
735
|
+
expect(Number(results[0].totalBalance)).toBe(3000);
|
|
736
|
+
expect(Number(results[1].accountCount)).toBe(1);
|
|
737
|
+
expect(Number(results[1].totalBalance)).toBe(3000);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
741
|
});
|