@memberjunction/sqlserver-dataprovider 2.53.0 → 2.55.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 +344 -128
- 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.
|
|
@@ -688,8 +832,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
688
832
|
return Promise.all(promises);
|
|
689
833
|
}
|
|
690
834
|
validateUserProvidedSQLClause(clause) {
|
|
835
|
+
// First, remove all string literals from the clause to avoid false positives
|
|
836
|
+
// This regex matches both single and double quoted strings, handling escaped quotes
|
|
837
|
+
const stringLiteralPattern = /(['"])(?:(?=(\\?))\2[\s\S])*?\1/g;
|
|
838
|
+
// Replace all string literals with empty strings for validation purposes
|
|
839
|
+
const clauseWithoutStrings = clause.replace(stringLiteralPattern, '');
|
|
691
840
|
// convert the clause to lower case to make the keyword search case-insensitive
|
|
692
|
-
const lowerClause =
|
|
841
|
+
const lowerClause = clauseWithoutStrings.toLowerCase();
|
|
693
842
|
// Define forbidden keywords and characters as whole words using regular expressions
|
|
694
843
|
const forbiddenPatterns = [
|
|
695
844
|
/\binsert\b/,
|
|
@@ -2581,100 +2730,42 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2581
2730
|
* @returns Promise<sql.IResult<any>> - Query result
|
|
2582
2731
|
* @private
|
|
2583
2732
|
*/
|
|
2733
|
+
/**
|
|
2734
|
+
* Internal SQL execution method for instance calls - routes queries based on transaction state
|
|
2735
|
+
* - Queries without transactions execute directly (allowing parallelism)
|
|
2736
|
+
* - Queries within transactions go through the instance queue (ensuring serialization)
|
|
2737
|
+
*/
|
|
2738
|
+
async _internalExecuteSQLInstance(query, parameters, context, options) {
|
|
2739
|
+
// If no transaction is active, execute directly without queuing
|
|
2740
|
+
// This allows maximum parallelism for non-transactional queries
|
|
2741
|
+
if (!context.transaction) {
|
|
2742
|
+
return executeSQLCore(query, parameters, context, options);
|
|
2743
|
+
}
|
|
2744
|
+
// For transactional queries, use the instance queue to ensure serialization
|
|
2745
|
+
// This prevents EREQINPROG errors when multiple queries try to use the same transaction
|
|
2746
|
+
return new Promise((resolve, reject) => {
|
|
2747
|
+
this._sqlQueue$.next({
|
|
2748
|
+
id: (0, uuid_1.v4)(),
|
|
2749
|
+
query,
|
|
2750
|
+
parameters,
|
|
2751
|
+
context,
|
|
2752
|
+
options,
|
|
2753
|
+
resolve,
|
|
2754
|
+
reject
|
|
2755
|
+
});
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Static SQL execution method - for static methods like ExecuteSQLWithPool
|
|
2760
|
+
* Static methods don't have access to instance queues, so they execute directly
|
|
2761
|
+
* Transactions are not supported in static context
|
|
2762
|
+
*/
|
|
2584
2763
|
static async _internalExecuteSQLStatic(query, parameters, context, options) {
|
|
2585
|
-
// Determine which connection source to use
|
|
2586
|
-
let connectionSource;
|
|
2587
2764
|
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;
|
|
2765
|
+
throw new Error('Transactions are not supported in static SQL execution context. Use instance methods for transactional queries.');
|
|
2677
2766
|
}
|
|
2767
|
+
// Static calls always execute directly (no queue needed since no transactions)
|
|
2768
|
+
return executeSQLCore(query, parameters, context, options);
|
|
2678
2769
|
}
|
|
2679
2770
|
/**
|
|
2680
2771
|
* Internal centralized method for executing SQL queries with consistent transaction and connection handling.
|
|
@@ -2724,8 +2815,8 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2724
2815
|
simpleSQLFallback: loggingOptions.simpleSQLFallback,
|
|
2725
2816
|
contextUser: loggingOptions.contextUser
|
|
2726
2817
|
} : undefined;
|
|
2727
|
-
// Delegate to
|
|
2728
|
-
return
|
|
2818
|
+
// Delegate to instance method
|
|
2819
|
+
return this._internalExecuteSQLInstance(query, parameters, context, options);
|
|
2729
2820
|
}
|
|
2730
2821
|
/**
|
|
2731
2822
|
* This method can be used to execute raw SQL statements outside of the MJ infrastructure.
|
|
@@ -2840,31 +2931,43 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2840
2931
|
batchSQL += ';\n';
|
|
2841
2932
|
}
|
|
2842
2933
|
});
|
|
2843
|
-
//
|
|
2844
|
-
let
|
|
2934
|
+
// Create execution context for batch SQL
|
|
2935
|
+
let pool;
|
|
2936
|
+
let transaction = null;
|
|
2845
2937
|
if (connectionSource instanceof sql.Request) {
|
|
2846
|
-
|
|
2938
|
+
throw new Error('Request objects are not supported for batch execution. Use ConnectionPool or Transaction.');
|
|
2847
2939
|
}
|
|
2848
2940
|
else if (connectionSource instanceof sql.Transaction) {
|
|
2849
|
-
|
|
2941
|
+
transaction = connectionSource;
|
|
2942
|
+
// Get pool from transaction's internal connection
|
|
2943
|
+
pool = connectionSource._pool || connectionSource.parent;
|
|
2944
|
+
if (!pool) {
|
|
2945
|
+
throw new Error('Unable to get connection pool from transaction');
|
|
2946
|
+
}
|
|
2850
2947
|
}
|
|
2851
2948
|
else if (connectionSource instanceof sql.ConnectionPool) {
|
|
2852
|
-
|
|
2949
|
+
pool = connectionSource;
|
|
2853
2950
|
}
|
|
2854
2951
|
else {
|
|
2855
2952
|
throw new Error('Invalid connection source type');
|
|
2856
2953
|
}
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2954
|
+
// Create context for executeSQLCore
|
|
2955
|
+
const context = {
|
|
2956
|
+
pool: pool,
|
|
2957
|
+
transaction: transaction,
|
|
2958
|
+
logSqlStatement: async (q, p, d, i, m, s, u) => {
|
|
2959
|
+
await SQLServerDataProvider.LogSQLStatement(q, p, d || 'Batch execution', m || false, s, u);
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
// Use named parameters for batch SQL
|
|
2963
|
+
const namedParams = batchParameters;
|
|
2964
|
+
// Execute using the centralized core function
|
|
2965
|
+
const result = await executeSQLCore(batchSQL, namedParams, context, {
|
|
2966
|
+
description: 'Batch execution',
|
|
2967
|
+
ignoreLogging: false,
|
|
2968
|
+
isMutation: false,
|
|
2969
|
+
contextUser: contextUser
|
|
2970
|
+
});
|
|
2868
2971
|
// Return array of recordsets - one for each query
|
|
2869
2972
|
// Handle both single and multiple recordsets
|
|
2870
2973
|
if (result.recordsets && Array.isArray(result.recordsets)) {
|
|
@@ -2895,17 +2998,66 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2895
2998
|
*/
|
|
2896
2999
|
async ExecuteSQLBatch(queries, parameters, options, contextUser) {
|
|
2897
3000
|
try {
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3001
|
+
// Build combined batch SQL and parameters (same as static method)
|
|
3002
|
+
let batchSQL = '';
|
|
3003
|
+
const batchParameters = {};
|
|
3004
|
+
let globalParamIndex = 0;
|
|
3005
|
+
queries.forEach((query, queryIndex) => {
|
|
3006
|
+
let processedQuery = query;
|
|
3007
|
+
// Add parameters for this query if provided
|
|
3008
|
+
if (parameters && parameters[queryIndex]) {
|
|
3009
|
+
const queryParams = parameters[queryIndex];
|
|
3010
|
+
if (Array.isArray(queryParams)) {
|
|
3011
|
+
// Handle positional parameters
|
|
3012
|
+
queryParams.forEach((value, localIndex) => {
|
|
3013
|
+
const paramName = `p${globalParamIndex}`;
|
|
3014
|
+
batchParameters[paramName] = value;
|
|
3015
|
+
globalParamIndex++;
|
|
3016
|
+
});
|
|
3017
|
+
// Replace ? placeholders with parameter names
|
|
3018
|
+
let localParamIndex = globalParamIndex - queryParams.length;
|
|
3019
|
+
processedQuery = processedQuery.replace(/\?/g, () => `@p${localParamIndex++}`);
|
|
3020
|
+
}
|
|
3021
|
+
else if (typeof queryParams === 'object') {
|
|
3022
|
+
// Handle named parameters - prefix with query index to avoid conflicts
|
|
3023
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
3024
|
+
const paramName = `q${queryIndex}_${key}`;
|
|
3025
|
+
batchParameters[paramName] = value;
|
|
3026
|
+
// Replace parameter references in query
|
|
3027
|
+
processedQuery = processedQuery.replace(new RegExp(`@${key}\\b`, 'g'), `@${paramName}`);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
batchSQL += processedQuery;
|
|
3032
|
+
if (queryIndex < queries.length - 1) {
|
|
3033
|
+
batchSQL += ';\n';
|
|
3034
|
+
}
|
|
3035
|
+
});
|
|
3036
|
+
// Create execution context
|
|
3037
|
+
const context = {
|
|
3038
|
+
pool: this._pool,
|
|
3039
|
+
transaction: this._transaction,
|
|
3040
|
+
logSqlStatement: this._logSqlStatement.bind(this),
|
|
3041
|
+
clearTransaction: () => { this._transaction = null; }
|
|
3042
|
+
};
|
|
3043
|
+
// Execute using instance method (which handles queue for transactions)
|
|
3044
|
+
const result = await this._internalExecuteSQLInstance(batchSQL, batchParameters, context, {
|
|
3045
|
+
description: options?.description || 'Batch execution',
|
|
3046
|
+
ignoreLogging: options?.ignoreLogging || false,
|
|
3047
|
+
isMutation: options?.isMutation || false,
|
|
3048
|
+
contextUser: contextUser
|
|
3049
|
+
});
|
|
3050
|
+
// Return array of recordsets - one for each query
|
|
3051
|
+
// Handle both single and multiple recordsets
|
|
3052
|
+
if (result.recordsets && Array.isArray(result.recordsets)) {
|
|
3053
|
+
return result.recordsets;
|
|
3054
|
+
}
|
|
3055
|
+
else if (result.recordset) {
|
|
3056
|
+
return [result.recordset];
|
|
2902
3057
|
}
|
|
2903
3058
|
else {
|
|
2904
|
-
|
|
2905
|
-
connectionSource = this._pool;
|
|
3059
|
+
return [];
|
|
2906
3060
|
}
|
|
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
3061
|
}
|
|
2910
3062
|
catch (e) {
|
|
2911
3063
|
(0, core_1.LogError)(e);
|
|
@@ -2995,29 +3147,57 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2995
3147
|
}
|
|
2996
3148
|
async BeginTransaction() {
|
|
2997
3149
|
try {
|
|
2998
|
-
|
|
2999
|
-
this.
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3150
|
+
this._transactionDepth++;
|
|
3151
|
+
if (this._transactionDepth === 1) {
|
|
3152
|
+
// First transaction - actually begin using mssql Transaction object
|
|
3153
|
+
this._transaction = new sql.Transaction(this._pool);
|
|
3154
|
+
await this._transaction.begin();
|
|
3155
|
+
// Emit transaction state change
|
|
3156
|
+
this._transactionState$.next(true);
|
|
3157
|
+
}
|
|
3158
|
+
else {
|
|
3159
|
+
// Nested transaction - create a savepoint
|
|
3160
|
+
const savepointName = `SavePoint_${++this._savepointCounter}`;
|
|
3161
|
+
this._savepointStack.push(savepointName);
|
|
3162
|
+
// Create savepoint for nested transaction
|
|
3163
|
+
await this.ExecuteSQL(`SAVE TRANSACTION ${savepointName}`, null, {
|
|
3164
|
+
description: `Creating savepoint ${savepointName} at depth ${this._transactionDepth}`,
|
|
3165
|
+
ignoreLogging: true
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3005
3168
|
}
|
|
3006
3169
|
catch (e) {
|
|
3170
|
+
this._transactionDepth--; // Restore depth on error
|
|
3007
3171
|
(0, core_1.LogError)(e);
|
|
3008
3172
|
throw e; // force caller to handle
|
|
3009
3173
|
}
|
|
3010
3174
|
}
|
|
3011
3175
|
async CommitTransaction() {
|
|
3012
3176
|
try {
|
|
3013
|
-
if (this._transaction) {
|
|
3177
|
+
if (!this._transaction) {
|
|
3178
|
+
throw new Error('No active transaction to commit');
|
|
3179
|
+
}
|
|
3180
|
+
if (this._transactionDepth === 0) {
|
|
3181
|
+
throw new Error('Transaction depth mismatch - no transaction to commit');
|
|
3182
|
+
}
|
|
3183
|
+
this._transactionDepth--;
|
|
3184
|
+
if (this._transactionDepth === 0) {
|
|
3185
|
+
// Outermost transaction - use mssql Transaction object to commit
|
|
3014
3186
|
await this._transaction.commit();
|
|
3015
3187
|
this._transaction = null;
|
|
3188
|
+
// Clear savepoint tracking
|
|
3189
|
+
this._savepointStack = [];
|
|
3190
|
+
this._savepointCounter = 0;
|
|
3016
3191
|
// Emit transaction state change
|
|
3017
3192
|
this._transactionState$.next(false);
|
|
3018
3193
|
// Process any deferred tasks after successful commit
|
|
3019
3194
|
await this.processDeferredTasks();
|
|
3020
3195
|
}
|
|
3196
|
+
else {
|
|
3197
|
+
// Nested transaction - just remove the savepoint from stack
|
|
3198
|
+
// The savepoint remains valid in SQL Server until the outer transaction commits or rolls back
|
|
3199
|
+
this._savepointStack.pop();
|
|
3200
|
+
}
|
|
3021
3201
|
}
|
|
3022
3202
|
catch (e) {
|
|
3023
3203
|
(0, core_1.LogError)(e);
|
|
@@ -3026,9 +3206,20 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
3026
3206
|
}
|
|
3027
3207
|
async RollbackTransaction() {
|
|
3028
3208
|
try {
|
|
3029
|
-
if (this._transaction) {
|
|
3209
|
+
if (!this._transaction) {
|
|
3210
|
+
throw new Error('No active transaction to rollback');
|
|
3211
|
+
}
|
|
3212
|
+
if (this._transactionDepth === 0) {
|
|
3213
|
+
throw new Error('Transaction depth mismatch - no transaction to rollback');
|
|
3214
|
+
}
|
|
3215
|
+
if (this._transactionDepth === 1) {
|
|
3216
|
+
// Outermost transaction - use mssql Transaction object to rollback everything
|
|
3030
3217
|
await this._transaction.rollback();
|
|
3031
3218
|
this._transaction = null;
|
|
3219
|
+
this._transactionDepth = 0;
|
|
3220
|
+
// Clear savepoint tracking
|
|
3221
|
+
this._savepointStack = [];
|
|
3222
|
+
this._savepointCounter = 0;
|
|
3032
3223
|
// Emit transaction state change
|
|
3033
3224
|
this._transactionState$.next(false);
|
|
3034
3225
|
// Clear deferred tasks after rollback (don't process them)
|
|
@@ -3038,8 +3229,33 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
3038
3229
|
(0, core_1.LogStatus)(`Cleared ${deferredCount} deferred tasks after transaction rollback`);
|
|
3039
3230
|
}
|
|
3040
3231
|
}
|
|
3232
|
+
else {
|
|
3233
|
+
// Nested transaction - rollback to the last savepoint
|
|
3234
|
+
const savepointName = this._savepointStack[this._savepointStack.length - 1];
|
|
3235
|
+
if (!savepointName) {
|
|
3236
|
+
throw new Error('Savepoint stack mismatch - no savepoint to rollback to');
|
|
3237
|
+
}
|
|
3238
|
+
// Rollback to the savepoint (this preserves the outer transaction)
|
|
3239
|
+
// Note: We use ROLLBACK TRANSACTION SavePointName (without TO) as per SQL Server syntax
|
|
3240
|
+
await this.ExecuteSQL(`ROLLBACK TRANSACTION ${savepointName}`, null, {
|
|
3241
|
+
description: `Rolling back to savepoint ${savepointName}`,
|
|
3242
|
+
ignoreLogging: true
|
|
3243
|
+
});
|
|
3244
|
+
// Remove the savepoint from stack and decrement depth
|
|
3245
|
+
this._savepointStack.pop();
|
|
3246
|
+
this._transactionDepth--;
|
|
3247
|
+
}
|
|
3041
3248
|
}
|
|
3042
3249
|
catch (e) {
|
|
3250
|
+
// On error in nested transaction, maintain state
|
|
3251
|
+
// On error in outer transaction, reset everything
|
|
3252
|
+
if (this._transactionDepth === 1 || !this._transaction) {
|
|
3253
|
+
this._transaction = null;
|
|
3254
|
+
this._transactionDepth = 0;
|
|
3255
|
+
this._savepointStack = [];
|
|
3256
|
+
this._savepointCounter = 0;
|
|
3257
|
+
this._transactionState$.next(false);
|
|
3258
|
+
}
|
|
3043
3259
|
(0, core_1.LogError)(e);
|
|
3044
3260
|
throw e; // force caller to handle
|
|
3045
3261
|
}
|