metal-orm 1.1.8 → 1.1.10

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 (75) hide show
  1. package/README.md +769 -764
  2. package/dist/index.cjs +2352 -226
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +605 -40
  5. package/dist/index.d.ts +605 -40
  6. package/dist/index.js +2324 -226
  7. package/dist/index.js.map +1 -1
  8. package/package.json +22 -17
  9. package/src/bulk/bulk-context.ts +83 -0
  10. package/src/bulk/bulk-delete-executor.ts +89 -0
  11. package/src/bulk/bulk-executor.base.ts +73 -0
  12. package/src/bulk/bulk-insert-executor.ts +74 -0
  13. package/src/bulk/bulk-types.ts +70 -0
  14. package/src/bulk/bulk-update-executor.ts +192 -0
  15. package/src/bulk/bulk-upsert-executor.ts +95 -0
  16. package/src/bulk/bulk-utils.ts +91 -0
  17. package/src/bulk/index.ts +18 -0
  18. package/src/codegen/typescript.ts +30 -21
  19. package/src/core/ast/expression-builders.ts +107 -10
  20. package/src/core/ast/expression-nodes.ts +52 -22
  21. package/src/core/ast/expression-visitor.ts +23 -13
  22. package/src/core/dialect/abstract.ts +30 -17
  23. package/src/core/dialect/mysql/index.ts +20 -5
  24. package/src/core/execution/db-executor.ts +96 -64
  25. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
  26. package/src/core/execution/executors/mssql-executor.ts +66 -34
  27. package/src/core/execution/executors/mysql-executor.ts +98 -66
  28. package/src/core/execution/executors/postgres-executor.ts +33 -11
  29. package/src/core/execution/executors/sqlite-executor.ts +86 -30
  30. package/src/decorators/bootstrap.ts +482 -398
  31. package/src/decorators/column-decorator.ts +87 -96
  32. package/src/decorators/decorator-metadata.ts +100 -24
  33. package/src/decorators/entity.ts +27 -24
  34. package/src/decorators/relations.ts +231 -149
  35. package/src/decorators/transformers/transformer-decorators.ts +26 -29
  36. package/src/decorators/validators/country-validators-decorators.ts +9 -15
  37. package/src/dto/apply-filter.ts +568 -551
  38. package/src/index.ts +16 -9
  39. package/src/orm/entity-hydration.ts +116 -72
  40. package/src/orm/entity-metadata.ts +347 -301
  41. package/src/orm/entity-relations.ts +264 -207
  42. package/src/orm/entity.ts +199 -199
  43. package/src/orm/execute.ts +13 -13
  44. package/src/orm/lazy-batch/morph-many.ts +70 -0
  45. package/src/orm/lazy-batch/morph-one.ts +69 -0
  46. package/src/orm/lazy-batch/morph-to.ts +59 -0
  47. package/src/orm/lazy-batch.ts +4 -1
  48. package/src/orm/orm-session.ts +170 -104
  49. package/src/orm/pooled-executor-factory.ts +99 -58
  50. package/src/orm/query-logger.ts +49 -40
  51. package/src/orm/relation-change-processor.ts +198 -96
  52. package/src/orm/relations/belongs-to.ts +143 -143
  53. package/src/orm/relations/has-many.ts +204 -204
  54. package/src/orm/relations/has-one.ts +174 -174
  55. package/src/orm/relations/many-to-many.ts +288 -288
  56. package/src/orm/relations/morph-many.ts +156 -0
  57. package/src/orm/relations/morph-one.ts +151 -0
  58. package/src/orm/relations/morph-to.ts +162 -0
  59. package/src/orm/save-graph.ts +116 -1
  60. package/src/query-builder/expression-table-mapper.ts +5 -0
  61. package/src/query-builder/hydration-manager.ts +345 -345
  62. package/src/query-builder/hydration-planner.ts +178 -148
  63. package/src/query-builder/relation-conditions.ts +171 -151
  64. package/src/query-builder/relation-cte-builder.ts +5 -1
  65. package/src/query-builder/relation-filter-utils.ts +9 -6
  66. package/src/query-builder/relation-include-strategies.ts +44 -2
  67. package/src/query-builder/relation-join-strategies.ts +8 -1
  68. package/src/query-builder/relation-service.ts +250 -241
  69. package/src/query-builder/select/cursor-pagination.ts +323 -0
  70. package/src/query-builder/select/select-operations.ts +110 -105
  71. package/src/query-builder/select.ts +42 -1
  72. package/src/query-builder/update-include.ts +4 -0
  73. package/src/schema/relation.ts +296 -188
  74. package/src/schema/types.ts +138 -123
  75. package/src/tree/tree-decorator.ts +127 -137
@@ -32,11 +32,16 @@ import { runInTransaction } from './transaction-runner.js';
32
32
  import { saveGraphInternal, patchGraphInternal, SaveGraphOptions } from './save-graph.js';
33
33
  import type { SaveGraphInputPayload, PatchGraphInputPayload } from './save-graph-types.js';
34
34
  import type { QueryCacheManager } from '../cache/query-cache-manager.js';
35
-
36
- /**
37
- * Interface for ORM interceptors that allow hooking into the flush lifecycle.
38
- */
39
- export interface OrmInterceptor {
35
+
36
+ const NESTED_TRANSACTIONS_REQUIRE_SAVEPOINTS =
37
+ 'Nested session.transaction calls require savepoint support in this executor';
38
+ const ROLLBACK_ONLY_TRANSACTION =
39
+ 'Cannot commit transaction because an inner transaction failed';
40
+
41
+ /**
42
+ * Interface for ORM interceptors that allow hooking into the flush lifecycle.
43
+ */
44
+ export interface OrmInterceptor {
40
45
  /**
41
46
  * Called before the flush operation begins.
42
47
  * @param ctx - The entity context
@@ -54,22 +59,22 @@ export interface OrmInterceptor {
54
59
  * Options for creating an OrmSession instance.
55
60
  * @template E - The domain event type
56
61
  */
57
- export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
58
- /** The ORM instance */
59
- orm: Orm<E>;
60
- /** The database executor */
61
- executor: DbExecutor;
62
- /** Optional query logger for debugging */
63
- queryLogger?: QueryLogger;
64
- /** Optional interceptors for flush lifecycle hooks */
65
- interceptors?: OrmInterceptor[];
66
- /** Optional domain event handlers */
67
- domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
68
- /** Optional cache manager for query caching */
69
- cacheManager?: QueryCacheManager;
70
- /** Optional tenant ID for multi-tenancy */
71
- tenantId?: string | number;
72
- }
62
+ export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
63
+ /** The ORM instance */
64
+ orm: Orm<E>;
65
+ /** The database executor */
66
+ executor: DbExecutor;
67
+ /** Optional query logger for debugging */
68
+ queryLogger?: QueryLogger;
69
+ /** Optional interceptors for flush lifecycle hooks */
70
+ interceptors?: OrmInterceptor[];
71
+ /** Optional domain event handlers */
72
+ domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
73
+ /** Optional cache manager for query caching */
74
+ cacheManager?: QueryCacheManager;
75
+ /** Optional tenant ID for multi-tenancy */
76
+ tenantId?: string | number;
77
+ }
73
78
 
74
79
  export interface SaveGraphSessionOptions extends SaveGraphOptions {
75
80
  /** Wrap the save operation in a transaction (default: true). */
@@ -93,15 +98,18 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
93
98
  readonly unitOfWork: UnitOfWork;
94
99
  /** The domain event bus */
95
100
  readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
96
- /** The relation change processor */
97
- readonly relationChanges: RelationChangeProcessor;
98
- /** The cache manager for query caching */
99
- readonly cacheManager?: QueryCacheManager;
100
- /** The tenant ID for multi-tenancy support */
101
- readonly tenantId?: string | number;
102
-
101
+ /** The relation change processor */
102
+ readonly relationChanges: RelationChangeProcessor;
103
+ /** The cache manager for query caching */
104
+ readonly cacheManager?: QueryCacheManager;
105
+ /** The tenant ID for multi-tenancy support */
106
+ readonly tenantId?: string | number;
107
+
103
108
  private readonly interceptors: OrmInterceptor[];
104
109
  private saveGraphDefaults?: SaveGraphSessionOptions;
110
+ private transactionDepth = 0;
111
+ private savepointCounter = 0;
112
+ private rollbackOnly = false;
105
113
 
106
114
  /**
107
115
  * Creates a new OrmSession instance.
@@ -112,13 +120,13 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
112
120
  this.executor = createQueryLoggingExecutor(opts.executor, opts.queryLogger);
113
121
  this.interceptors = [...(opts.interceptors ?? [])];
114
122
 
115
- this.identityMap = new IdentityMap();
116
- this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
117
- this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
118
- this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
119
- this.cacheManager = opts.cacheManager;
120
- this.tenantId = opts.tenantId;
121
- }
123
+ this.identityMap = new IdentityMap();
124
+ this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
125
+ this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
126
+ this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
127
+ this.cacheManager = opts.cacheManager;
128
+ this.tenantId = opts.tenantId;
129
+ }
122
130
 
123
131
  /**
124
132
  * Releases resources associated with this session (executor/pool leases) and resets tracking.
@@ -533,37 +541,71 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
533
541
  * @returns The result of the function
534
542
  * @throws If the transaction fails
535
543
  */
536
- async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
537
- // If the executor can't do transactions, just run and commit once.
538
- if (!this.executor.capabilities.transactions) {
539
- const result = await fn(this);
540
- await this.commit();
541
- return result;
542
- }
543
-
544
- await this.executor.beginTransaction();
545
- try {
546
- const result = await fn(this);
547
- await this.flushWithHooks();
548
- await this.executor.commitTransaction();
549
- await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
550
- return result;
551
- } catch (err) {
552
- await this.rollback();
553
- throw err;
554
- }
555
- }
544
+ async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
545
+ // If the executor can't do transactions, just run and commit once.
546
+ if (!this.executor.capabilities.transactions) {
547
+ const result = await fn(this);
548
+ await this.commit();
549
+ return result;
550
+ }
551
+
552
+ const isOutermost = this.transactionDepth === 0;
553
+ let savepointName: string | null = null;
554
+
555
+ if (isOutermost) {
556
+ this.rollbackOnly = false;
557
+ await this.executor.beginTransaction();
558
+ } else {
559
+ this.assertSavepointSupport();
560
+ savepointName = this.nextSavepointName();
561
+ await this.executor.savepoint!(savepointName);
562
+ }
563
+
564
+ this.transactionDepth += 1;
565
+ try {
566
+ const result = await fn(this);
567
+ this.throwIfRollbackOnly();
568
+ await this.flushWithHooks();
569
+ this.throwIfRollbackOnly();
570
+
571
+ if (isOutermost) {
572
+ await this.executor.commitTransaction();
573
+ await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
574
+ } else {
575
+ await this.executor.releaseSavepoint!(savepointName!);
576
+ }
577
+
578
+ return result;
579
+ } catch (err) {
580
+ if (isOutermost) {
581
+ await this.rollback();
582
+ } else {
583
+ this.rollbackOnly = true;
584
+ await this.executor.rollbackToSavepoint!(savepointName!);
585
+ }
586
+ throw err;
587
+ } finally {
588
+ this.transactionDepth = Math.max(0, this.transactionDepth - 1);
589
+ if (this.transactionDepth === 0) {
590
+ this.rollbackOnly = false;
591
+ this.savepointCounter = 0;
592
+ }
593
+ }
594
+ }
556
595
 
557
596
  /**
558
597
  * Rolls back the current transaction.
559
598
  */
560
- async rollback(): Promise<void> {
561
- if (this.executor.capabilities.transactions) {
562
- await this.executor.rollbackTransaction();
563
- }
564
- this.unitOfWork.reset();
565
- this.relationChanges.reset();
566
- }
599
+ async rollback(): Promise<void> {
600
+ if (this.executor.capabilities.transactions) {
601
+ await this.executor.rollbackTransaction();
602
+ }
603
+ this.transactionDepth = 0;
604
+ this.savepointCounter = 0;
605
+ this.rollbackOnly = false;
606
+ this.unitOfWork.reset();
607
+ this.relationChanges.reset();
608
+ }
567
609
 
568
610
  /**
569
611
  * Gets the execution context.
@@ -591,55 +633,79 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
591
633
  };
592
634
  }
593
635
 
594
- /**
595
- * Invalidates cache by specific tags.
596
- * @param tags - Tags to invalidate
597
- * @throws Error if no cache manager is configured
598
- * @example
599
- * await session.invalidateCacheTags(['users', 'dashboard']);
600
- */
601
- async invalidateCacheTags(tags: string[]): Promise<void> {
602
- if (!this.cacheManager) {
603
- throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
604
- }
605
- await this.cacheManager.invalidateTags(tags);
636
+ /**
637
+ * Invalidates cache by specific tags.
638
+ * @param tags - Tags to invalidate
639
+ * @throws Error if no cache manager is configured
640
+ * @example
641
+ * await session.invalidateCacheTags(['users', 'dashboard']);
642
+ */
643
+ async invalidateCacheTags(tags: string[]): Promise<void> {
644
+ if (!this.cacheManager) {
645
+ throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
646
+ }
647
+ await this.cacheManager.invalidateTags(tags);
648
+ }
649
+
650
+ /**
651
+ * Invalidates cache by key prefix (useful for multi-tenancy).
652
+ * @param prefix - Prefix to match cache keys
653
+ * @throws Error if no cache manager is configured
654
+ * @example
655
+ * await session.invalidateCachePrefix('tenant:123:');
656
+ */
657
+ async invalidateCachePrefix(prefix: string): Promise<void> {
658
+ if (!this.cacheManager) {
659
+ throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
660
+ }
661
+ await this.cacheManager.invalidatePrefix(prefix);
662
+ }
663
+
664
+ /**
665
+ * Invalidates a specific cache key.
666
+ * @param key - Cache key to invalidate
667
+ * @throws Error if no cache manager is configured
668
+ * @example
669
+ * await session.invalidateCacheKey('active_users');
670
+ */
671
+ async invalidateCacheKey(key: string): Promise<void> {
672
+ if (!this.cacheManager) {
673
+ throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
674
+ }
675
+ await this.cacheManager.invalidateKey(key, this.tenantId);
676
+ }
677
+
678
+ /**
679
+ * Merges session defaults with per-call saveGraph options.
680
+ * @param options - Per-call saveGraph options
681
+ * @returns Combined options with per-call values taking precedence
682
+ */
683
+ private resolveSaveGraphOptions(options?: SaveGraphSessionOptions): SaveGraphSessionOptions {
684
+ return { ...(this.saveGraphDefaults ?? {}), ...(options ?? {}) };
606
685
  }
607
686
 
608
- /**
609
- * Invalidates cache by key prefix (useful for multi-tenancy).
610
- * @param prefix - Prefix to match cache keys
611
- * @throws Error if no cache manager is configured
612
- * @example
613
- * await session.invalidateCachePrefix('tenant:123:');
614
- */
615
- async invalidateCachePrefix(prefix: string): Promise<void> {
616
- if (!this.cacheManager) {
617
- throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
687
+ private assertSavepointSupport(): void {
688
+ if (!this.executor.capabilities.savepoints) {
689
+ throw new Error(NESTED_TRANSACTIONS_REQUIRE_SAVEPOINTS);
690
+ }
691
+ if (
692
+ typeof this.executor.savepoint !== 'function' ||
693
+ typeof this.executor.releaseSavepoint !== 'function' ||
694
+ typeof this.executor.rollbackToSavepoint !== 'function'
695
+ ) {
696
+ throw new Error(NESTED_TRANSACTIONS_REQUIRE_SAVEPOINTS);
618
697
  }
619
- await this.cacheManager.invalidatePrefix(prefix);
620
698
  }
621
699
 
622
- /**
623
- * Invalidates a specific cache key.
624
- * @param key - Cache key to invalidate
625
- * @throws Error if no cache manager is configured
626
- * @example
627
- * await session.invalidateCacheKey('active_users');
628
- */
629
- async invalidateCacheKey(key: string): Promise<void> {
630
- if (!this.cacheManager) {
631
- throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
632
- }
633
- await this.cacheManager.invalidateKey(key, this.tenantId);
700
+ private nextSavepointName(): string {
701
+ this.savepointCounter += 1;
702
+ return `metalorm_sp_${this.savepointCounter}`;
634
703
  }
635
704
 
636
- /**
637
- * Merges session defaults with per-call saveGraph options.
638
- * @param options - Per-call saveGraph options
639
- * @returns Combined options with per-call values taking precedence
640
- */
641
- private resolveSaveGraphOptions(options?: SaveGraphSessionOptions): SaveGraphSessionOptions {
642
- return { ...(this.saveGraphDefaults ?? {}), ...(options ?? {}) };
705
+ private throwIfRollbackOnly(): void {
706
+ if (this.rollbackOnly) {
707
+ throw new Error(ROLLBACK_ONLY_TRANSACTION);
708
+ }
643
709
  }
644
710
  }
645
711
 
@@ -1,39 +1,42 @@
1
- import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
2
- import { toExecutionPayload } from '../core/execution/db-executor.js';
3
- import { rowsToQueryResult } from '../core/execution/db-executor.js';
1
+ import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
2
+ import { toExecutionPayload } from '../core/execution/db-executor.js';
3
+ import { rowsToQueryResult } from '../core/execution/db-executor.js';
4
4
  import type { Pool } from '../core/execution/pooling/pool.js';
5
5
  import type { DbExecutorFactory } from './orm.js';
6
6
 
7
7
  export interface PooledConnectionAdapter<TConn> {
8
- query(
9
- conn: TConn,
10
- sql: string,
11
- params?: unknown[]
12
- ): Promise<
13
- | Array<Record<string, unknown>>
14
- | Array<Array<Record<string, unknown>>>
15
- | QueryResult[]
16
- >;
17
-
18
- beginTransaction(conn: TConn): Promise<void>;
19
- commitTransaction(conn: TConn): Promise<void>;
20
- rollbackTransaction(conn: TConn): Promise<void>;
21
- }
22
-
23
- type PooledExecutorFactoryOptions<TConn> = {
24
- pool: Pool<TConn>;
25
- adapter: PooledConnectionAdapter<TConn>;
26
- };
27
-
28
- const isQueryResult = (value: unknown): value is QueryResult =>
29
- typeof value === 'object' &&
30
- value !== null &&
31
- Array.isArray((value as QueryResult).columns) &&
32
- Array.isArray((value as QueryResult).values);
33
-
34
- const isRowArray = (value: unknown): value is Array<Record<string, unknown>> =>
35
- Array.isArray(value) &&
36
- value.every(item => typeof item === 'object' && item !== null && !Array.isArray(item));
8
+ query(
9
+ conn: TConn,
10
+ sql: string,
11
+ params?: unknown[]
12
+ ): Promise<
13
+ | Array<Record<string, unknown>>
14
+ | Array<Array<Record<string, unknown>>>
15
+ | QueryResult[]
16
+ >;
17
+
18
+ beginTransaction(conn: TConn): Promise<void>;
19
+ commitTransaction(conn: TConn): Promise<void>;
20
+ rollbackTransaction(conn: TConn): Promise<void>;
21
+ savepoint?(conn: TConn, name: string): Promise<void>;
22
+ releaseSavepoint?(conn: TConn, name: string): Promise<void>;
23
+ rollbackToSavepoint?(conn: TConn, name: string): Promise<void>;
24
+ }
25
+
26
+ type PooledExecutorFactoryOptions<TConn> = {
27
+ pool: Pool<TConn>;
28
+ adapter: PooledConnectionAdapter<TConn>;
29
+ };
30
+
31
+ const isQueryResult = (value: unknown): value is QueryResult =>
32
+ typeof value === 'object' &&
33
+ value !== null &&
34
+ Array.isArray((value as QueryResult).columns) &&
35
+ Array.isArray((value as QueryResult).values);
36
+
37
+ const isRowArray = (value: unknown): value is Array<Record<string, unknown>> =>
38
+ Array.isArray(value) &&
39
+ value.every(item => typeof item === 'object' && item !== null && !Array.isArray(item));
37
40
 
38
41
  /**
39
42
  * Creates a first-class DbExecutorFactory backed by MetalORM's Pool.
@@ -45,11 +48,15 @@ const isRowArray = (value: unknown): value is Array<Record<string, unknown>> =>
45
48
  */
46
49
  export function createPooledExecutorFactory<TConn>(
47
50
  opts: PooledExecutorFactoryOptions<TConn>
48
- ): DbExecutorFactory {
49
- const { pool, adapter } = opts;
50
-
51
- const makeExecutor = (mode: 'session' | 'sticky'): DbExecutor => {
52
- let lease: Awaited<ReturnType<typeof pool.acquire>> | null = null;
51
+ ): DbExecutorFactory {
52
+ const { pool, adapter } = opts;
53
+ const supportsSavepoints =
54
+ typeof adapter.savepoint === 'function' &&
55
+ typeof adapter.releaseSavepoint === 'function' &&
56
+ typeof adapter.rollbackToSavepoint === 'function';
57
+
58
+ const makeExecutor = (mode: 'session' | 'sticky'): DbExecutor => {
59
+ let lease: Awaited<ReturnType<typeof pool.acquire>> | null = null;
53
60
 
54
61
  const getLease = async () => {
55
62
  if (lease) return lease;
@@ -61,19 +68,29 @@ export function createPooledExecutorFactory<TConn>(
61
68
  conn: TConn,
62
69
  sql: string,
63
70
  params?: unknown[]
64
- ) => {
65
- const rows = await adapter.query(conn, sql, params);
66
- if (Array.isArray(rows) && rows.length > 0 && rows.every(isQueryResult)) {
67
- return toExecutionPayload(rows);
68
- }
69
- if (Array.isArray(rows) && rows.length > 0 && rows.every(isRowArray)) {
70
- return toExecutionPayload(rows.map(set => rowsToQueryResult(set)));
71
+ ) => {
72
+ const rows = await adapter.query(conn, sql, params);
73
+ if (Array.isArray(rows) && rows.length > 0 && rows.every(isQueryResult)) {
74
+ return toExecutionPayload(rows);
75
+ }
76
+ if (Array.isArray(rows) && rows.length > 0 && rows.every(isRowArray)) {
77
+ return toExecutionPayload(rows.map(set => rowsToQueryResult(set)));
71
78
  }
72
79
  return toExecutionPayload([rowsToQueryResult(rows as Array<Record<string, unknown>>)]);
73
80
  };
74
-
75
- return {
76
- capabilities: { transactions: true },
81
+
82
+ const requireActiveTransactionLease = () => {
83
+ if (!lease) {
84
+ throw new Error('savepoint operation called without an active transaction');
85
+ }
86
+ return lease;
87
+ };
88
+
89
+ return {
90
+ capabilities: {
91
+ transactions: true,
92
+ ...(supportsSavepoints ? { savepoints: true } : {}),
93
+ },
77
94
 
78
95
  async executeSql(sql, params) {
79
96
  // Sticky mode: always reuse a leased connection.
@@ -114,22 +131,46 @@ export function createPooledExecutorFactory<TConn>(
114
131
  }
115
132
  },
116
133
 
117
- async rollbackTransaction() {
118
- if (!lease) {
119
- // Nothing to rollback; keep idempotent semantics.
120
- return;
121
- }
134
+ async rollbackTransaction() {
135
+ if (!lease) {
136
+ // Nothing to rollback; keep idempotent semantics.
137
+ return;
138
+ }
122
139
  const l = lease;
123
140
  try {
124
141
  await adapter.rollbackTransaction(l.resource);
125
142
  } finally {
126
143
  lease = null;
127
- await l.release();
128
- }
129
- },
130
-
131
- async dispose() {
132
- if (!lease) return;
144
+ await l.release();
145
+ }
146
+ },
147
+
148
+ async savepoint(name: string) {
149
+ if (!supportsSavepoints) {
150
+ throw new Error('Savepoints are not supported by this executor');
151
+ }
152
+ const l = requireActiveTransactionLease();
153
+ await adapter.savepoint!(l.resource, name);
154
+ },
155
+
156
+ async releaseSavepoint(name: string) {
157
+ if (!supportsSavepoints) {
158
+ throw new Error('Savepoints are not supported by this executor');
159
+ }
160
+ const l = requireActiveTransactionLease();
161
+ await adapter.releaseSavepoint!(l.resource, name);
162
+ },
163
+
164
+ async rollbackToSavepoint(name: string) {
165
+ if (!supportsSavepoints) {
166
+ throw new Error('Savepoints are not supported by this executor');
167
+ }
168
+ const l = requireActiveTransactionLease();
169
+ await adapter.rollbackToSavepoint!(l.resource, name);
170
+ },
171
+
172
+ async dispose() {
173
+ if (!lease) return;
133
174
  const l = lease;
134
175
  lease = null;
135
176
  await l.release();
@@ -1,47 +1,56 @@
1
- import type { DbExecutor } from '../core/execution/db-executor.js';
2
-
3
- /**
4
- * Represents a single SQL query log entry
5
- */
6
- export interface QueryLogEntry {
7
- /** The SQL query that was executed */
8
- sql: string;
9
- /** Parameters used in the query */
10
- params?: unknown[];
11
- }
12
-
13
- /**
14
- * Function type for query logging callbacks
15
- * @param entry - The query log entry to process
16
- */
17
- export type QueryLogger = (entry: QueryLogEntry) => void;
18
-
19
- /**
20
- * Creates a wrapped database executor that logs all SQL queries
21
- * @param executor - Original database executor to wrap
22
- * @param logger - Optional logger function to receive query log entries
23
- * @returns Wrapped executor that logs queries before execution
24
- */
25
- export const createQueryLoggingExecutor = (
26
- executor: DbExecutor,
27
- logger?: QueryLogger
28
- ): DbExecutor => {
29
- if (!logger) {
30
- return executor;
31
- }
32
-
33
- const wrapped: DbExecutor = {
34
- capabilities: executor.capabilities,
35
- async executeSql(sql, params) {
36
- logger({ sql, params });
37
- return executor.executeSql(sql, params);
1
+ import type { DbExecutor } from '../core/execution/db-executor.js';
2
+
3
+ /**
4
+ * Represents a single SQL query log entry
5
+ */
6
+ export interface QueryLogEntry {
7
+ /** The SQL query that was executed */
8
+ sql: string;
9
+ /** Parameters used in the query */
10
+ params?: unknown[];
11
+ }
12
+
13
+ /**
14
+ * Function type for query logging callbacks
15
+ * @param entry - The query log entry to process
16
+ */
17
+ export type QueryLogger = (entry: QueryLogEntry) => void;
18
+
19
+ /**
20
+ * Creates a wrapped database executor that logs all SQL queries
21
+ * @param executor - Original database executor to wrap
22
+ * @param logger - Optional logger function to receive query log entries
23
+ * @returns Wrapped executor that logs queries before execution
24
+ */
25
+ export const createQueryLoggingExecutor = (
26
+ executor: DbExecutor,
27
+ logger?: QueryLogger
28
+ ): DbExecutor => {
29
+ if (!logger) {
30
+ return executor;
31
+ }
32
+
33
+ const wrapped: DbExecutor = {
34
+ capabilities: executor.capabilities,
35
+ async executeSql(sql, params) {
36
+ logger({ sql, params });
37
+ return executor.executeSql(sql, params);
38
38
  }
39
39
  ,
40
40
  beginTransaction: () => executor.beginTransaction(),
41
41
  commitTransaction: () => executor.commitTransaction(),
42
42
  rollbackTransaction: () => executor.rollbackTransaction(),
43
+ savepoint: executor.savepoint
44
+ ? (name: string) => executor.savepoint!(name)
45
+ : undefined,
46
+ releaseSavepoint: executor.releaseSavepoint
47
+ ? (name: string) => executor.releaseSavepoint!(name)
48
+ : undefined,
49
+ rollbackToSavepoint: executor.rollbackToSavepoint
50
+ ? (name: string) => executor.rollbackToSavepoint!(name)
51
+ : undefined,
43
52
  dispose: () => executor.dispose(),
44
53
  };
45
-
46
- return wrapped;
47
- };
54
+
55
+ return wrapped;
56
+ };