@memberjunction/sqlserver-dataprovider 2.52.0 → 2.54.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/dist/SQLServerDataProvider.d.ts +46 -3
- package/dist/SQLServerDataProvider.d.ts.map +1 -1
- package/dist/SQLServerDataProvider.js +338 -127
- package/dist/SQLServerDataProvider.js.map +1 -1
- package/dist/SQLServerTransactionGroup.d.ts.map +1 -1
- package/dist/SQLServerTransactionGroup.js +48 -3
- package/dist/SQLServerTransactionGroup.js.map +1 -1
- package/package.json +9 -9
|
@@ -51,6 +51,82 @@ const SQLServerTransactionGroup_1 = require("./SQLServerTransactionGroup");
|
|
|
51
51
|
const SqlLogger_js_1 = require("./SqlLogger.js");
|
|
52
52
|
const actions_1 = require("@memberjunction/actions");
|
|
53
53
|
const uuid_1 = require("uuid");
|
|
54
|
+
/**
|
|
55
|
+
* Core SQL execution function - handles the actual database query execution
|
|
56
|
+
* This is outside the class to allow both static and instance methods to use it
|
|
57
|
+
* without creating circular dependencies or forcing everything to be static
|
|
58
|
+
*/
|
|
59
|
+
async function executeSQLCore(query, parameters, context, options) {
|
|
60
|
+
// Determine which connection source to use
|
|
61
|
+
let connectionSource;
|
|
62
|
+
if (context.transaction) {
|
|
63
|
+
// Use the transaction if provided
|
|
64
|
+
// Note: We no longer test the transaction validity here because:
|
|
65
|
+
// 1. It could cause race conditions with concurrent queries
|
|
66
|
+
// 2. If the transaction is invalid, we'll get a proper error when trying to use it
|
|
67
|
+
// 3. We should never silently fall back to the pool when a transaction is expected
|
|
68
|
+
connectionSource = context.transaction;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
connectionSource = context.pool;
|
|
72
|
+
}
|
|
73
|
+
// Check if the pool is connected before attempting to execute
|
|
74
|
+
if (connectionSource === context.pool && !context.pool.connected) {
|
|
75
|
+
const errorMessage = 'Connection pool is closed. Cannot execute SQL query.';
|
|
76
|
+
const error = new Error(errorMessage);
|
|
77
|
+
error.code = 'POOL_CLOSED';
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
// Handle logging
|
|
81
|
+
let logPromise;
|
|
82
|
+
if (options && !options.ignoreLogging && context.logSqlStatement) {
|
|
83
|
+
logPromise = context.logSqlStatement(query, parameters, options.description, options.ignoreLogging, options.isMutation, options.simpleSQLFallback, options.contextUser);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
logPromise = Promise.resolve();
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
// Create a new request object for this query
|
|
90
|
+
let request;
|
|
91
|
+
if (connectionSource instanceof sql.Transaction) {
|
|
92
|
+
request = new sql.Request(connectionSource);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
request = new sql.Request(connectionSource);
|
|
96
|
+
}
|
|
97
|
+
// Add parameters if provided
|
|
98
|
+
let processedQuery = query;
|
|
99
|
+
if (parameters) {
|
|
100
|
+
if (Array.isArray(parameters)) {
|
|
101
|
+
// Handle positional parameters (legacy TypeORM style)
|
|
102
|
+
parameters.forEach((value, index) => {
|
|
103
|
+
request.input(`p${index}`, value);
|
|
104
|
+
});
|
|
105
|
+
// Replace ? with @p0, @p1, etc. in the query
|
|
106
|
+
let paramIndex = 0;
|
|
107
|
+
processedQuery = query.replace(/\?/g, () => `@p${paramIndex++}`);
|
|
108
|
+
}
|
|
109
|
+
else if (typeof parameters === 'object') {
|
|
110
|
+
// Handle named parameters
|
|
111
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
112
|
+
request.input(key, value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Execute query and logging in parallel
|
|
117
|
+
const [result] = await Promise.all([
|
|
118
|
+
request.query(processedQuery),
|
|
119
|
+
logPromise
|
|
120
|
+
]);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
// Log all errors
|
|
125
|
+
(0, core_1.LogError)(error);
|
|
126
|
+
// Re-throw all errors
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
54
130
|
/**
|
|
55
131
|
* SQL Server implementation of the MemberJunction data provider interfaces.
|
|
56
132
|
*
|
|
@@ -70,13 +146,19 @@ const uuid_1 = require("uuid");
|
|
|
70
146
|
* await provider.Config();
|
|
71
147
|
* ```
|
|
72
148
|
*/
|
|
73
|
-
class SQLServerDataProvider extends core_1.
|
|
149
|
+
class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
74
150
|
constructor() {
|
|
75
151
|
super(...arguments);
|
|
152
|
+
this._transactionDepth = 0;
|
|
153
|
+
this._savepointCounter = 0;
|
|
154
|
+
this._savepointStack = [];
|
|
76
155
|
this._bAllowRefresh = true;
|
|
77
156
|
this._needsDatetimeOffsetAdjustment = false;
|
|
78
157
|
this._datetimeOffsetTestComplete = false;
|
|
79
158
|
this._sqlLoggingSessions = new Map();
|
|
159
|
+
// Instance SQL execution queue for serializing transaction queries
|
|
160
|
+
// Non-transactional queries bypass this queue for maximum parallelism
|
|
161
|
+
this._sqlQueue$ = new rxjs_1.Subject();
|
|
80
162
|
// Transaction state management
|
|
81
163
|
this._transactionState$ = new rxjs_1.BehaviorSubject(false);
|
|
82
164
|
this._deferredTasks = [];
|
|
@@ -92,6 +174,32 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
92
174
|
get transactionState$() {
|
|
93
175
|
return this._transactionState$.asObservable();
|
|
94
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Gets the current transaction nesting depth
|
|
179
|
+
* 0 = no transaction, 1 = first level, 2+ = nested transactions
|
|
180
|
+
*/
|
|
181
|
+
get transactionDepth() {
|
|
182
|
+
return this._transactionDepth;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Checks if we're currently in a transaction (at any depth)
|
|
186
|
+
*/
|
|
187
|
+
get inTransaction() {
|
|
188
|
+
return this._transactionDepth > 0;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Checks if we're currently in a nested transaction (depth > 1)
|
|
192
|
+
*/
|
|
193
|
+
get inNestedTransaction() {
|
|
194
|
+
return this._transactionDepth > 1;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Gets the current savepoint names in the stack (for debugging)
|
|
198
|
+
* Returns a copy to prevent external modification
|
|
199
|
+
*/
|
|
200
|
+
get savepointStack() {
|
|
201
|
+
return [...this._savepointStack];
|
|
202
|
+
}
|
|
95
203
|
/**
|
|
96
204
|
* Gets whether a transaction is currently active
|
|
97
205
|
*/
|
|
@@ -113,6 +221,8 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
113
221
|
async Config(configData) {
|
|
114
222
|
try {
|
|
115
223
|
this._pool = configData.DataSource; // Now expects a ConnectionPool instead of DataSource
|
|
224
|
+
// Initialize the instance queue processor
|
|
225
|
+
this.initializeQueueProcessor();
|
|
116
226
|
return super.Config(configData); // now parent class can do it's config
|
|
117
227
|
}
|
|
118
228
|
catch (e) {
|
|
@@ -120,6 +230,21 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
120
230
|
throw e;
|
|
121
231
|
}
|
|
122
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Initialize the SQL queue processor for this instance
|
|
235
|
+
* This ensures all queries within a transaction execute sequentially
|
|
236
|
+
*/
|
|
237
|
+
initializeQueueProcessor() {
|
|
238
|
+
// Each instance gets its own queue processor
|
|
239
|
+
this._queueSubscription = this._sqlQueue$.pipe((0, rxjs_1.concatMap)(item => (0, rxjs_1.from)(executeSQLCore(item.query, item.parameters, item.context, item.options)).pipe(
|
|
240
|
+
// Handle success
|
|
241
|
+
(0, rxjs_1.tap)(result => item.resolve(result)),
|
|
242
|
+
// Handle errors
|
|
243
|
+
(0, rxjs_1.catchError)(error => {
|
|
244
|
+
item.reject(error);
|
|
245
|
+
return (0, rxjs_1.of)(null); // Continue processing queue even on errors
|
|
246
|
+
})))).subscribe();
|
|
247
|
+
}
|
|
123
248
|
/**
|
|
124
249
|
* Gets the underlying SQL Server connection pool
|
|
125
250
|
* @returns The mssql ConnectionPool object
|
|
@@ -236,6 +361,25 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
236
361
|
await Promise.all(disposePromises);
|
|
237
362
|
this._sqlLoggingSessions.clear();
|
|
238
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Dispose of this provider instance and clean up resources.
|
|
366
|
+
* This should be called when the provider is no longer needed.
|
|
367
|
+
*/
|
|
368
|
+
async Dispose() {
|
|
369
|
+
// Dispose all SQL logging sessions
|
|
370
|
+
await this.DisposeAllSqlLoggingSessions();
|
|
371
|
+
// Unsubscribe from the SQL queue
|
|
372
|
+
if (this._queueSubscription) {
|
|
373
|
+
this._queueSubscription.unsubscribe();
|
|
374
|
+
this._queueSubscription = null;
|
|
375
|
+
}
|
|
376
|
+
// Complete the queue subject
|
|
377
|
+
if (this._sqlQueue$) {
|
|
378
|
+
this._sqlQueue$.complete();
|
|
379
|
+
}
|
|
380
|
+
// Note: We don't close the pool here as it might be shared
|
|
381
|
+
// The caller is responsible for closing the pool when appropriate
|
|
382
|
+
}
|
|
239
383
|
/**
|
|
240
384
|
* Internal method to log SQL statement to all active logging sessions.
|
|
241
385
|
* This is called automatically by ExecuteSQL methods.
|
|
@@ -2581,100 +2725,42 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2581
2725
|
* @returns Promise<sql.IResult<any>> - Query result
|
|
2582
2726
|
* @private
|
|
2583
2727
|
*/
|
|
2728
|
+
/**
|
|
2729
|
+
* Internal SQL execution method for instance calls - routes queries based on transaction state
|
|
2730
|
+
* - Queries without transactions execute directly (allowing parallelism)
|
|
2731
|
+
* - Queries within transactions go through the instance queue (ensuring serialization)
|
|
2732
|
+
*/
|
|
2733
|
+
async _internalExecuteSQLInstance(query, parameters, context, options) {
|
|
2734
|
+
// If no transaction is active, execute directly without queuing
|
|
2735
|
+
// This allows maximum parallelism for non-transactional queries
|
|
2736
|
+
if (!context.transaction) {
|
|
2737
|
+
return executeSQLCore(query, parameters, context, options);
|
|
2738
|
+
}
|
|
2739
|
+
// For transactional queries, use the instance queue to ensure serialization
|
|
2740
|
+
// This prevents EREQINPROG errors when multiple queries try to use the same transaction
|
|
2741
|
+
return new Promise((resolve, reject) => {
|
|
2742
|
+
this._sqlQueue$.next({
|
|
2743
|
+
id: (0, uuid_1.v4)(),
|
|
2744
|
+
query,
|
|
2745
|
+
parameters,
|
|
2746
|
+
context,
|
|
2747
|
+
options,
|
|
2748
|
+
resolve,
|
|
2749
|
+
reject
|
|
2750
|
+
});
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Static SQL execution method - for static methods like ExecuteSQLWithPool
|
|
2755
|
+
* Static methods don't have access to instance queues, so they execute directly
|
|
2756
|
+
* Transactions are not supported in static context
|
|
2757
|
+
*/
|
|
2584
2758
|
static async _internalExecuteSQLStatic(query, parameters, context, options) {
|
|
2585
|
-
// Determine which connection source to use
|
|
2586
|
-
let connectionSource;
|
|
2587
2759
|
if (context.transaction) {
|
|
2588
|
-
|
|
2589
|
-
try {
|
|
2590
|
-
// Test if the transaction is still valid by creating a request
|
|
2591
|
-
const testRequest = new sql.Request(context.transaction);
|
|
2592
|
-
connectionSource = context.transaction;
|
|
2593
|
-
}
|
|
2594
|
-
catch (error) {
|
|
2595
|
-
// Transaction is no longer valid, clear it and use the pool
|
|
2596
|
-
if (context.clearTransaction) {
|
|
2597
|
-
context.clearTransaction();
|
|
2598
|
-
}
|
|
2599
|
-
connectionSource = context.pool;
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
else {
|
|
2603
|
-
connectionSource = context.pool;
|
|
2604
|
-
}
|
|
2605
|
-
// Check if the pool is connected before attempting to execute
|
|
2606
|
-
if (connectionSource === context.pool && !context.pool.connected) {
|
|
2607
|
-
const errorMessage = 'Connection pool is closed. Cannot execute SQL query.';
|
|
2608
|
-
const error = new Error(errorMessage);
|
|
2609
|
-
error.code = 'POOL_CLOSED';
|
|
2610
|
-
throw error;
|
|
2611
|
-
}
|
|
2612
|
-
// Handle logging
|
|
2613
|
-
let logPromise;
|
|
2614
|
-
if (options && !options.ignoreLogging && context.logSqlStatement) {
|
|
2615
|
-
logPromise = context.logSqlStatement(query, parameters, options.description, options.ignoreLogging, options.isMutation, options.simpleSQLFallback, options.contextUser);
|
|
2616
|
-
}
|
|
2617
|
-
else {
|
|
2618
|
-
logPromise = Promise.resolve();
|
|
2619
|
-
}
|
|
2620
|
-
try {
|
|
2621
|
-
// Create a new request object for this query
|
|
2622
|
-
let request;
|
|
2623
|
-
if (connectionSource instanceof sql.Transaction) {
|
|
2624
|
-
request = new sql.Request(connectionSource);
|
|
2625
|
-
}
|
|
2626
|
-
else {
|
|
2627
|
-
request = new sql.Request(connectionSource);
|
|
2628
|
-
}
|
|
2629
|
-
// Add parameters if provided
|
|
2630
|
-
let processedQuery = query;
|
|
2631
|
-
if (parameters) {
|
|
2632
|
-
if (Array.isArray(parameters)) {
|
|
2633
|
-
// Handle positional parameters (legacy TypeORM style)
|
|
2634
|
-
parameters.forEach((value, index) => {
|
|
2635
|
-
request.input(`p${index}`, value);
|
|
2636
|
-
});
|
|
2637
|
-
// Replace ? with @p0, @p1, etc. in the query
|
|
2638
|
-
let paramIndex = 0;
|
|
2639
|
-
processedQuery = query.replace(/\?/g, () => `@p${paramIndex++}`);
|
|
2640
|
-
}
|
|
2641
|
-
else if (typeof parameters === 'object') {
|
|
2642
|
-
// Handle named parameters
|
|
2643
|
-
for (const [key, value] of Object.entries(parameters)) {
|
|
2644
|
-
request.input(key, value);
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
// Execute query and logging in parallel
|
|
2649
|
-
const [result] = await Promise.all([
|
|
2650
|
-
request.query(processedQuery),
|
|
2651
|
-
logPromise
|
|
2652
|
-
]);
|
|
2653
|
-
return result;
|
|
2654
|
-
}
|
|
2655
|
-
catch (error) {
|
|
2656
|
-
// If we get an EREQINPROG error and we were using a transaction, retry with the pool
|
|
2657
|
-
if (error?.code === 'EREQINPROG' && connectionSource === context.transaction) {
|
|
2658
|
-
// Silently retry with pool connection - this is expected behavior during concurrent operations
|
|
2659
|
-
// LogDebug('Transaction connection busy (EREQINPROG) - retrying with pool connection');
|
|
2660
|
-
// Clear the transaction reference
|
|
2661
|
-
if (context.clearTransaction) {
|
|
2662
|
-
context.clearTransaction();
|
|
2663
|
-
}
|
|
2664
|
-
// Retry using the pool connection
|
|
2665
|
-
return SQLServerDataProvider._internalExecuteSQLStatic(query, parameters, {
|
|
2666
|
-
...context,
|
|
2667
|
-
transaction: null // Force use of pool
|
|
2668
|
-
}, options ? {
|
|
2669
|
-
...options,
|
|
2670
|
-
description: options.description + ' (retry with pool)'
|
|
2671
|
-
} : undefined);
|
|
2672
|
-
}
|
|
2673
|
-
// Log other errors
|
|
2674
|
-
(0, core_1.LogError)(error);
|
|
2675
|
-
// Re-throw all errors
|
|
2676
|
-
throw error;
|
|
2760
|
+
throw new Error('Transactions are not supported in static SQL execution context. Use instance methods for transactional queries.');
|
|
2677
2761
|
}
|
|
2762
|
+
// Static calls always execute directly (no queue needed since no transactions)
|
|
2763
|
+
return executeSQLCore(query, parameters, context, options);
|
|
2678
2764
|
}
|
|
2679
2765
|
/**
|
|
2680
2766
|
* Internal centralized method for executing SQL queries with consistent transaction and connection handling.
|
|
@@ -2724,8 +2810,8 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2724
2810
|
simpleSQLFallback: loggingOptions.simpleSQLFallback,
|
|
2725
2811
|
contextUser: loggingOptions.contextUser
|
|
2726
2812
|
} : undefined;
|
|
2727
|
-
// Delegate to
|
|
2728
|
-
return
|
|
2813
|
+
// Delegate to instance method
|
|
2814
|
+
return this._internalExecuteSQLInstance(query, parameters, context, options);
|
|
2729
2815
|
}
|
|
2730
2816
|
/**
|
|
2731
2817
|
* This method can be used to execute raw SQL statements outside of the MJ infrastructure.
|
|
@@ -2840,31 +2926,43 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2840
2926
|
batchSQL += ';\n';
|
|
2841
2927
|
}
|
|
2842
2928
|
});
|
|
2843
|
-
//
|
|
2844
|
-
let
|
|
2929
|
+
// Create execution context for batch SQL
|
|
2930
|
+
let pool;
|
|
2931
|
+
let transaction = null;
|
|
2845
2932
|
if (connectionSource instanceof sql.Request) {
|
|
2846
|
-
|
|
2933
|
+
throw new Error('Request objects are not supported for batch execution. Use ConnectionPool or Transaction.');
|
|
2847
2934
|
}
|
|
2848
2935
|
else if (connectionSource instanceof sql.Transaction) {
|
|
2849
|
-
|
|
2936
|
+
transaction = connectionSource;
|
|
2937
|
+
// Get pool from transaction's internal connection
|
|
2938
|
+
pool = connectionSource._pool || connectionSource.parent;
|
|
2939
|
+
if (!pool) {
|
|
2940
|
+
throw new Error('Unable to get connection pool from transaction');
|
|
2941
|
+
}
|
|
2850
2942
|
}
|
|
2851
2943
|
else if (connectionSource instanceof sql.ConnectionPool) {
|
|
2852
|
-
|
|
2944
|
+
pool = connectionSource;
|
|
2853
2945
|
}
|
|
2854
2946
|
else {
|
|
2855
2947
|
throw new Error('Invalid connection source type');
|
|
2856
2948
|
}
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2949
|
+
// Create context for executeSQLCore
|
|
2950
|
+
const context = {
|
|
2951
|
+
pool: pool,
|
|
2952
|
+
transaction: transaction,
|
|
2953
|
+
logSqlStatement: async (q, p, d, i, m, s, u) => {
|
|
2954
|
+
await SQLServerDataProvider.LogSQLStatement(q, p, d || 'Batch execution', m || false, s, u);
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
// Use named parameters for batch SQL
|
|
2958
|
+
const namedParams = batchParameters;
|
|
2959
|
+
// Execute using the centralized core function
|
|
2960
|
+
const result = await executeSQLCore(batchSQL, namedParams, context, {
|
|
2961
|
+
description: 'Batch execution',
|
|
2962
|
+
ignoreLogging: false,
|
|
2963
|
+
isMutation: false,
|
|
2964
|
+
contextUser: contextUser
|
|
2965
|
+
});
|
|
2868
2966
|
// Return array of recordsets - one for each query
|
|
2869
2967
|
// Handle both single and multiple recordsets
|
|
2870
2968
|
if (result.recordsets && Array.isArray(result.recordsets)) {
|
|
@@ -2895,17 +2993,66 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2895
2993
|
*/
|
|
2896
2994
|
async ExecuteSQLBatch(queries, parameters, options, contextUser) {
|
|
2897
2995
|
try {
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2996
|
+
// Build combined batch SQL and parameters (same as static method)
|
|
2997
|
+
let batchSQL = '';
|
|
2998
|
+
const batchParameters = {};
|
|
2999
|
+
let globalParamIndex = 0;
|
|
3000
|
+
queries.forEach((query, queryIndex) => {
|
|
3001
|
+
let processedQuery = query;
|
|
3002
|
+
// Add parameters for this query if provided
|
|
3003
|
+
if (parameters && parameters[queryIndex]) {
|
|
3004
|
+
const queryParams = parameters[queryIndex];
|
|
3005
|
+
if (Array.isArray(queryParams)) {
|
|
3006
|
+
// Handle positional parameters
|
|
3007
|
+
queryParams.forEach((value, localIndex) => {
|
|
3008
|
+
const paramName = `p${globalParamIndex}`;
|
|
3009
|
+
batchParameters[paramName] = value;
|
|
3010
|
+
globalParamIndex++;
|
|
3011
|
+
});
|
|
3012
|
+
// Replace ? placeholders with parameter names
|
|
3013
|
+
let localParamIndex = globalParamIndex - queryParams.length;
|
|
3014
|
+
processedQuery = processedQuery.replace(/\?/g, () => `@p${localParamIndex++}`);
|
|
3015
|
+
}
|
|
3016
|
+
else if (typeof queryParams === 'object') {
|
|
3017
|
+
// Handle named parameters - prefix with query index to avoid conflicts
|
|
3018
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
3019
|
+
const paramName = `q${queryIndex}_${key}`;
|
|
3020
|
+
batchParameters[paramName] = value;
|
|
3021
|
+
// Replace parameter references in query
|
|
3022
|
+
processedQuery = processedQuery.replace(new RegExp(`@${key}\\b`, 'g'), `@${paramName}`);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
batchSQL += processedQuery;
|
|
3027
|
+
if (queryIndex < queries.length - 1) {
|
|
3028
|
+
batchSQL += ';\n';
|
|
3029
|
+
}
|
|
3030
|
+
});
|
|
3031
|
+
// Create execution context
|
|
3032
|
+
const context = {
|
|
3033
|
+
pool: this._pool,
|
|
3034
|
+
transaction: this._transaction,
|
|
3035
|
+
logSqlStatement: this._logSqlStatement.bind(this),
|
|
3036
|
+
clearTransaction: () => { this._transaction = null; }
|
|
3037
|
+
};
|
|
3038
|
+
// Execute using instance method (which handles queue for transactions)
|
|
3039
|
+
const result = await this._internalExecuteSQLInstance(batchSQL, batchParameters, context, {
|
|
3040
|
+
description: options?.description || 'Batch execution',
|
|
3041
|
+
ignoreLogging: options?.ignoreLogging || false,
|
|
3042
|
+
isMutation: options?.isMutation || false,
|
|
3043
|
+
contextUser: contextUser
|
|
3044
|
+
});
|
|
3045
|
+
// Return array of recordsets - one for each query
|
|
3046
|
+
// Handle both single and multiple recordsets
|
|
3047
|
+
if (result.recordsets && Array.isArray(result.recordsets)) {
|
|
3048
|
+
return result.recordsets;
|
|
3049
|
+
}
|
|
3050
|
+
else if (result.recordset) {
|
|
3051
|
+
return [result.recordset];
|
|
2902
3052
|
}
|
|
2903
3053
|
else {
|
|
2904
|
-
|
|
2905
|
-
connectionSource = this._pool;
|
|
3054
|
+
return [];
|
|
2906
3055
|
}
|
|
2907
|
-
// ExecuteSQLBatchStatic handles its own logging, so we don't need to duplicate it here
|
|
2908
|
-
return await SQLServerDataProvider.ExecuteSQLBatchStatic(connectionSource, queries, parameters, contextUser);
|
|
2909
3056
|
}
|
|
2910
3057
|
catch (e) {
|
|
2911
3058
|
(0, core_1.LogError)(e);
|
|
@@ -2995,29 +3142,57 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2995
3142
|
}
|
|
2996
3143
|
async BeginTransaction() {
|
|
2997
3144
|
try {
|
|
2998
|
-
|
|
2999
|
-
this.
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3145
|
+
this._transactionDepth++;
|
|
3146
|
+
if (this._transactionDepth === 1) {
|
|
3147
|
+
// First transaction - actually begin using mssql Transaction object
|
|
3148
|
+
this._transaction = new sql.Transaction(this._pool);
|
|
3149
|
+
await this._transaction.begin();
|
|
3150
|
+
// Emit transaction state change
|
|
3151
|
+
this._transactionState$.next(true);
|
|
3152
|
+
}
|
|
3153
|
+
else {
|
|
3154
|
+
// Nested transaction - create a savepoint
|
|
3155
|
+
const savepointName = `SavePoint_${++this._savepointCounter}`;
|
|
3156
|
+
this._savepointStack.push(savepointName);
|
|
3157
|
+
// Create savepoint for nested transaction
|
|
3158
|
+
await this.ExecuteSQL(`SAVE TRANSACTION ${savepointName}`, null, {
|
|
3159
|
+
description: `Creating savepoint ${savepointName} at depth ${this._transactionDepth}`,
|
|
3160
|
+
ignoreLogging: true
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3005
3163
|
}
|
|
3006
3164
|
catch (e) {
|
|
3165
|
+
this._transactionDepth--; // Restore depth on error
|
|
3007
3166
|
(0, core_1.LogError)(e);
|
|
3008
3167
|
throw e; // force caller to handle
|
|
3009
3168
|
}
|
|
3010
3169
|
}
|
|
3011
3170
|
async CommitTransaction() {
|
|
3012
3171
|
try {
|
|
3013
|
-
if (this._transaction) {
|
|
3172
|
+
if (!this._transaction) {
|
|
3173
|
+
throw new Error('No active transaction to commit');
|
|
3174
|
+
}
|
|
3175
|
+
if (this._transactionDepth === 0) {
|
|
3176
|
+
throw new Error('Transaction depth mismatch - no transaction to commit');
|
|
3177
|
+
}
|
|
3178
|
+
this._transactionDepth--;
|
|
3179
|
+
if (this._transactionDepth === 0) {
|
|
3180
|
+
// Outermost transaction - use mssql Transaction object to commit
|
|
3014
3181
|
await this._transaction.commit();
|
|
3015
3182
|
this._transaction = null;
|
|
3183
|
+
// Clear savepoint tracking
|
|
3184
|
+
this._savepointStack = [];
|
|
3185
|
+
this._savepointCounter = 0;
|
|
3016
3186
|
// Emit transaction state change
|
|
3017
3187
|
this._transactionState$.next(false);
|
|
3018
3188
|
// Process any deferred tasks after successful commit
|
|
3019
3189
|
await this.processDeferredTasks();
|
|
3020
3190
|
}
|
|
3191
|
+
else {
|
|
3192
|
+
// Nested transaction - just remove the savepoint from stack
|
|
3193
|
+
// The savepoint remains valid in SQL Server until the outer transaction commits or rolls back
|
|
3194
|
+
this._savepointStack.pop();
|
|
3195
|
+
}
|
|
3021
3196
|
}
|
|
3022
3197
|
catch (e) {
|
|
3023
3198
|
(0, core_1.LogError)(e);
|
|
@@ -3026,9 +3201,20 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
3026
3201
|
}
|
|
3027
3202
|
async RollbackTransaction() {
|
|
3028
3203
|
try {
|
|
3029
|
-
if (this._transaction) {
|
|
3204
|
+
if (!this._transaction) {
|
|
3205
|
+
throw new Error('No active transaction to rollback');
|
|
3206
|
+
}
|
|
3207
|
+
if (this._transactionDepth === 0) {
|
|
3208
|
+
throw new Error('Transaction depth mismatch - no transaction to rollback');
|
|
3209
|
+
}
|
|
3210
|
+
if (this._transactionDepth === 1) {
|
|
3211
|
+
// Outermost transaction - use mssql Transaction object to rollback everything
|
|
3030
3212
|
await this._transaction.rollback();
|
|
3031
3213
|
this._transaction = null;
|
|
3214
|
+
this._transactionDepth = 0;
|
|
3215
|
+
// Clear savepoint tracking
|
|
3216
|
+
this._savepointStack = [];
|
|
3217
|
+
this._savepointCounter = 0;
|
|
3032
3218
|
// Emit transaction state change
|
|
3033
3219
|
this._transactionState$.next(false);
|
|
3034
3220
|
// Clear deferred tasks after rollback (don't process them)
|
|
@@ -3038,8 +3224,33 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
3038
3224
|
(0, core_1.LogStatus)(`Cleared ${deferredCount} deferred tasks after transaction rollback`);
|
|
3039
3225
|
}
|
|
3040
3226
|
}
|
|
3227
|
+
else {
|
|
3228
|
+
// Nested transaction - rollback to the last savepoint
|
|
3229
|
+
const savepointName = this._savepointStack[this._savepointStack.length - 1];
|
|
3230
|
+
if (!savepointName) {
|
|
3231
|
+
throw new Error('Savepoint stack mismatch - no savepoint to rollback to');
|
|
3232
|
+
}
|
|
3233
|
+
// Rollback to the savepoint (this preserves the outer transaction)
|
|
3234
|
+
// Note: We use ROLLBACK TRANSACTION SavePointName (without TO) as per SQL Server syntax
|
|
3235
|
+
await this.ExecuteSQL(`ROLLBACK TRANSACTION ${savepointName}`, null, {
|
|
3236
|
+
description: `Rolling back to savepoint ${savepointName}`,
|
|
3237
|
+
ignoreLogging: true
|
|
3238
|
+
});
|
|
3239
|
+
// Remove the savepoint from stack and decrement depth
|
|
3240
|
+
this._savepointStack.pop();
|
|
3241
|
+
this._transactionDepth--;
|
|
3242
|
+
}
|
|
3041
3243
|
}
|
|
3042
3244
|
catch (e) {
|
|
3245
|
+
// On error in nested transaction, maintain state
|
|
3246
|
+
// On error in outer transaction, reset everything
|
|
3247
|
+
if (this._transactionDepth === 1 || !this._transaction) {
|
|
3248
|
+
this._transaction = null;
|
|
3249
|
+
this._transactionDepth = 0;
|
|
3250
|
+
this._savepointStack = [];
|
|
3251
|
+
this._savepointCounter = 0;
|
|
3252
|
+
this._transactionState$.next(false);
|
|
3253
|
+
}
|
|
3043
3254
|
(0, core_1.LogError)(e);
|
|
3044
3255
|
throw e; // force caller to handle
|
|
3045
3256
|
}
|