@tejasanik/postgres-mcp-server 2.1.0 → 2.2.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/README.md +186 -10
- package/dist/db-manager/index.d.ts +7 -0
- package/dist/db-manager/index.d.ts.map +1 -0
- package/dist/db-manager/index.js +7 -0
- package/dist/db-manager/index.js.map +1 -0
- package/dist/db-manager/validation.d.ts +35 -0
- package/dist/db-manager/validation.d.ts.map +1 -0
- package/dist/db-manager/validation.js +54 -0
- package/dist/db-manager/validation.js.map +1 -0
- package/dist/db-manager.d.ts +175 -5
- package/dist/db-manager.d.ts.map +1 -1
- package/dist/db-manager.js +589 -26
- package/dist/db-manager.js.map +1 -1
- package/dist/index.js +141 -11
- package/dist/index.js.map +1 -1
- package/dist/tools/analysis-tools.d.ts.map +1 -1
- package/dist/tools/analysis-tools.js +53 -49
- package/dist/tools/analysis-tools.js.map +1 -1
- package/dist/tools/schema-tools.d.ts +40 -1
- package/dist/tools/schema-tools.d.ts.map +1 -1
- package/dist/tools/schema-tools.js +174 -92
- package/dist/tools/schema-tools.js.map +1 -1
- package/dist/tools/server-tools.d.ts +1 -0
- package/dist/tools/server-tools.d.ts.map +1 -1
- package/dist/tools/server-tools.js +10 -6
- package/dist/tools/server-tools.js.map +1 -1
- package/dist/tools/sql/utils/connection-utils.d.ts +79 -0
- package/dist/tools/sql/utils/connection-utils.d.ts.map +1 -0
- package/dist/tools/sql/utils/connection-utils.js +129 -0
- package/dist/tools/sql/utils/connection-utils.js.map +1 -0
- package/dist/tools/sql/utils/constants.d.ts +55 -0
- package/dist/tools/sql/utils/constants.d.ts.map +1 -0
- package/dist/tools/sql/utils/constants.js +55 -0
- package/dist/tools/sql/utils/constants.js.map +1 -0
- package/dist/tools/sql/utils/dry-run-utils.d.ts +31 -0
- package/dist/tools/sql/utils/dry-run-utils.d.ts.map +1 -0
- package/dist/tools/sql/utils/dry-run-utils.js +173 -0
- package/dist/tools/sql/utils/dry-run-utils.js.map +1 -0
- package/dist/tools/sql/utils/file-handler.d.ts +57 -0
- package/dist/tools/sql/utils/file-handler.d.ts.map +1 -0
- package/dist/tools/sql/utils/file-handler.js +150 -0
- package/dist/tools/sql/utils/file-handler.js.map +1 -0
- package/dist/tools/sql/utils/index.d.ts +12 -0
- package/dist/tools/sql/utils/index.d.ts.map +1 -0
- package/dist/tools/sql/utils/index.js +12 -0
- package/dist/tools/sql/utils/index.js.map +1 -0
- package/dist/tools/sql/utils/result-formatter.d.ts +94 -0
- package/dist/tools/sql/utils/result-formatter.d.ts.map +1 -0
- package/dist/tools/sql/utils/result-formatter.js +154 -0
- package/dist/tools/sql/utils/result-formatter.js.map +1 -0
- package/dist/tools/sql/utils/sql-parser.d.ts +125 -0
- package/dist/tools/sql/utils/sql-parser.d.ts.map +1 -0
- package/dist/tools/sql/utils/sql-parser.js +468 -0
- package/dist/tools/sql/utils/sql-parser.js.map +1 -0
- package/dist/tools/sql-tools.d.ts +21 -0
- package/dist/tools/sql-tools.d.ts.map +1 -1
- package/dist/tools/sql-tools.js +383 -532
- package/dist/tools/sql-tools.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/retry.d.ts +1 -1
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/validation.d.ts +45 -9
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +335 -72
- package/dist/utils/validation.js.map +1 -1
- package/package.json +9 -2
- package/dist/__tests__/analysis-tools.test.d.ts +0 -2
- package/dist/__tests__/analysis-tools.test.d.ts.map +0 -1
- package/dist/__tests__/analysis-tools.test.js +0 -294
- package/dist/__tests__/analysis-tools.test.js.map +0 -1
- package/dist/__tests__/db-manager.test.d.ts +0 -2
- package/dist/__tests__/db-manager.test.d.ts.map +0 -1
- package/dist/__tests__/db-manager.test.js +0 -410
- package/dist/__tests__/db-manager.test.js.map +0 -1
- package/dist/__tests__/mcp-server.test.d.ts +0 -13
- package/dist/__tests__/mcp-server.test.d.ts.map +0 -1
- package/dist/__tests__/mcp-server.test.js +0 -146
- package/dist/__tests__/mcp-server.test.js.map +0 -1
- package/dist/__tests__/schema-tools.test.d.ts +0 -2
- package/dist/__tests__/schema-tools.test.d.ts.map +0 -1
- package/dist/__tests__/schema-tools.test.js +0 -171
- package/dist/__tests__/schema-tools.test.js.map +0 -1
- package/dist/__tests__/server-tools.test.d.ts +0 -2
- package/dist/__tests__/server-tools.test.d.ts.map +0 -1
- package/dist/__tests__/server-tools.test.js +0 -113
- package/dist/__tests__/server-tools.test.js.map +0 -1
- package/dist/__tests__/sql-tools.test.d.ts +0 -2
- package/dist/__tests__/sql-tools.test.d.ts.map +0 -1
- package/dist/__tests__/sql-tools.test.js +0 -1912
- package/dist/__tests__/sql-tools.test.js.map +0 -1
- package/dist/__tests__/validation.test.d.ts +0 -2
- package/dist/__tests__/validation.test.d.ts.map +0 -1
- package/dist/__tests__/validation.test.js +0 -203
- package/dist/__tests__/validation.test.js.map +0 -1
package/dist/tools/sql-tools.js
CHANGED
|
@@ -4,169 +4,17 @@ import * as path from 'path';
|
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
import { validateIdentifier, validateIndexType, isReadOnlySql, validatePositiveInteger } from '../utils/validation.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Extract detailed error information from a PostgreSQL error.
|
|
16
|
-
* Captures all available fields to help AI quickly identify and fix issues.
|
|
17
|
-
*/
|
|
18
|
-
function extractDryRunError(error) {
|
|
19
|
-
const result = {
|
|
20
|
-
message: error instanceof Error ? error.message : String(error)
|
|
21
|
-
};
|
|
22
|
-
// PostgreSQL errors have additional properties
|
|
23
|
-
if (error && typeof error === 'object') {
|
|
24
|
-
const pgError = error;
|
|
25
|
-
if (pgError.code)
|
|
26
|
-
result.code = String(pgError.code);
|
|
27
|
-
if (pgError.severity)
|
|
28
|
-
result.severity = String(pgError.severity);
|
|
29
|
-
if (pgError.detail)
|
|
30
|
-
result.detail = String(pgError.detail);
|
|
31
|
-
if (pgError.hint)
|
|
32
|
-
result.hint = String(pgError.hint);
|
|
33
|
-
if (pgError.position)
|
|
34
|
-
result.position = Number(pgError.position);
|
|
35
|
-
if (pgError.internalPosition)
|
|
36
|
-
result.internalPosition = Number(pgError.internalPosition);
|
|
37
|
-
if (pgError.internalQuery)
|
|
38
|
-
result.internalQuery = String(pgError.internalQuery);
|
|
39
|
-
if (pgError.where)
|
|
40
|
-
result.where = String(pgError.where);
|
|
41
|
-
if (pgError.schema)
|
|
42
|
-
result.schema = String(pgError.schema);
|
|
43
|
-
if (pgError.table)
|
|
44
|
-
result.table = String(pgError.table);
|
|
45
|
-
if (pgError.column)
|
|
46
|
-
result.column = String(pgError.column);
|
|
47
|
-
if (pgError.dataType)
|
|
48
|
-
result.dataType = String(pgError.dataType);
|
|
49
|
-
if (pgError.constraint)
|
|
50
|
-
result.constraint = String(pgError.constraint);
|
|
51
|
-
if (pgError.file)
|
|
52
|
-
result.file = String(pgError.file);
|
|
53
|
-
if (pgError.line)
|
|
54
|
-
result.line = String(pgError.line);
|
|
55
|
-
if (pgError.routine)
|
|
56
|
-
result.routine = String(pgError.routine);
|
|
57
|
-
}
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Check if a SQL statement contains operations that cannot be fully rolled back
|
|
62
|
-
* or have side effects even within a transaction.
|
|
63
|
-
*/
|
|
64
|
-
function detectNonRollbackableOperations(sql, statementIndex, lineNumber) {
|
|
65
|
-
const warnings = [];
|
|
66
|
-
const upperSql = sql.toUpperCase().trim();
|
|
67
|
-
// Operations that cannot run inside a transaction at all - MUST SKIP
|
|
68
|
-
if (upperSql.match(/\bVACUUM\b/)) {
|
|
69
|
-
warnings.push({
|
|
70
|
-
operation: 'VACUUM',
|
|
71
|
-
message: 'VACUUM cannot run inside a transaction block. Statement skipped.',
|
|
72
|
-
statementIndex,
|
|
73
|
-
lineNumber,
|
|
74
|
-
mustSkip: true
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
if (upperSql.match(/\bCLUSTER\b/) && !upperSql.includes('CREATE')) {
|
|
78
|
-
warnings.push({
|
|
79
|
-
operation: 'CLUSTER',
|
|
80
|
-
message: 'CLUSTER cannot run inside a transaction block. Statement skipped.',
|
|
81
|
-
statementIndex,
|
|
82
|
-
lineNumber,
|
|
83
|
-
mustSkip: true
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
if (upperSql.match(/\bREINDEX\b.*\bCONCURRENTLY\b/)) {
|
|
87
|
-
warnings.push({
|
|
88
|
-
operation: 'REINDEX_CONCURRENTLY',
|
|
89
|
-
message: 'REINDEX CONCURRENTLY cannot run inside a transaction block. Statement skipped.',
|
|
90
|
-
statementIndex,
|
|
91
|
-
lineNumber,
|
|
92
|
-
mustSkip: true
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
if (upperSql.match(/\bCREATE\s+INDEX\b.*\bCONCURRENTLY\b/)) {
|
|
96
|
-
warnings.push({
|
|
97
|
-
operation: 'CREATE_INDEX_CONCURRENTLY',
|
|
98
|
-
message: 'CREATE INDEX CONCURRENTLY cannot run inside a transaction block. Statement skipped.',
|
|
99
|
-
statementIndex,
|
|
100
|
-
lineNumber,
|
|
101
|
-
mustSkip: true
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
if (upperSql.match(/\bCREATE\s+DATABASE\b/)) {
|
|
105
|
-
warnings.push({
|
|
106
|
-
operation: 'CREATE_DATABASE',
|
|
107
|
-
message: 'CREATE DATABASE cannot run inside a transaction block. Statement skipped.',
|
|
108
|
-
statementIndex,
|
|
109
|
-
lineNumber,
|
|
110
|
-
mustSkip: true
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
if (upperSql.match(/\bDROP\s+DATABASE\b/)) {
|
|
114
|
-
warnings.push({
|
|
115
|
-
operation: 'DROP_DATABASE',
|
|
116
|
-
message: 'DROP DATABASE cannot run inside a transaction block. Statement skipped.',
|
|
117
|
-
statementIndex,
|
|
118
|
-
lineNumber,
|
|
119
|
-
mustSkip: true
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
// Operations that have side effects even when rolled back - MUST SKIP
|
|
123
|
-
if (upperSql.match(/\bNEXTVAL\s*\(/)) {
|
|
124
|
-
warnings.push({
|
|
125
|
-
operation: 'SEQUENCE',
|
|
126
|
-
message: 'NEXTVAL increments sequence even when transaction is rolled back. Statement skipped to prevent sequence consumption.',
|
|
127
|
-
statementIndex,
|
|
128
|
-
lineNumber,
|
|
129
|
-
mustSkip: true
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
if (upperSql.match(/\bSETVAL\s*\(/)) {
|
|
133
|
-
warnings.push({
|
|
134
|
-
operation: 'SEQUENCE',
|
|
135
|
-
message: 'SETVAL modifies sequence. Statement skipped to prevent side effects.',
|
|
136
|
-
statementIndex,
|
|
137
|
-
lineNumber,
|
|
138
|
-
mustSkip: true
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
// INSERT with SERIAL/BIGSERIAL columns may consume sequence values - WARNING ONLY (not skipped)
|
|
142
|
-
if (upperSql.match(/\bINSERT\s+INTO\b/)) {
|
|
143
|
-
warnings.push({
|
|
144
|
-
operation: 'SEQUENCE',
|
|
145
|
-
message: 'INSERT may consume sequence values (for SERIAL/BIGSERIAL columns) even when rolled back.',
|
|
146
|
-
statementIndex,
|
|
147
|
-
lineNumber,
|
|
148
|
-
mustSkip: false // Warning only, do not skip
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
// NOTIFY only sends on commit, so safe in dry-run (rollback prevents notification)
|
|
152
|
-
if (upperSql.match(/\bNOTIFY\b/)) {
|
|
153
|
-
warnings.push({
|
|
154
|
-
operation: 'NOTIFY',
|
|
155
|
-
message: 'NOTIFY sends notifications on commit. Since dry-run rolls back, notifications will NOT be sent.',
|
|
156
|
-
statementIndex,
|
|
157
|
-
lineNumber,
|
|
158
|
-
mustSkip: false // Safe to execute in dry-run since we rollback
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
return warnings;
|
|
162
|
-
}
|
|
7
|
+
// Import utilities from modular structure
|
|
8
|
+
import { MAX_OUTPUT_CHARS, MAX_ROWS_DEFAULT, MAX_ROWS_LIMIT, DEFAULT_SQL_LENGTH_LIMIT, MAX_PARAMS, MAX_SQL_FILE_SIZE, MAX_DRY_RUN_SAMPLE_ROWS, MAX_BATCH_QUERIES, MAX_MUTATION_SAMPLE_SIZE, DEFAULT_MUTATION_SAMPLE_SIZE, MAX_HYPOTHETICAL_INDEXES, MAX_DRY_RUN_STATEMENTS, DEFAULT_DRY_RUN_STATEMENTS, MAX_PREVIEW_STATEMENTS, DEFAULT_PREVIEW_STATEMENTS, MAX_ROWS_PER_STATEMENT, SQL_TRUNCATION_SHORT, SQL_TRUNCATION_LONG, MAX_TABLES_TO_ANALYZE, } from './sql/utils/constants.js';
|
|
9
|
+
import { extractDryRunError, detectNonRollbackableOperations, } from './sql/utils/dry-run-utils.js';
|
|
10
|
+
import { splitSqlStatementsWithLineNumbers, stripLeadingComments, detectStatementType, extractTablesFromSql, } from './sql/utils/sql-parser.js';
|
|
11
|
+
import { preprocessSqlContent, } from './sql/utils/file-handler.js';
|
|
12
|
+
// Connection utilities are handled inline for explicit control flow
|
|
13
|
+
import { calculateExecutionTime, getStartTime, } from './sql/utils/result-formatter.js';
|
|
163
14
|
export async function executeSql(args) {
|
|
164
15
|
// Validate SQL input
|
|
165
|
-
if (args.sql
|
|
166
|
-
throw new Error('sql parameter is required');
|
|
167
|
-
}
|
|
168
|
-
if (typeof args.sql !== 'string') {
|
|
169
|
-
throw new Error('sql parameter must be a string');
|
|
16
|
+
if (!args.sql || typeof args.sql !== 'string') {
|
|
17
|
+
throw new Error('sql parameter is required and must be a string');
|
|
170
18
|
}
|
|
171
19
|
const sql = args.sql.trim();
|
|
172
20
|
if (sql.length === 0) {
|
|
@@ -189,31 +37,53 @@ export async function executeSql(args) {
|
|
|
189
37
|
if (args.allowMultipleStatements && args.params && args.params.length > 0) {
|
|
190
38
|
throw new Error('params not supported with allowMultipleStatements. Use separate execute_sql calls for parameterized queries.');
|
|
191
39
|
}
|
|
40
|
+
// Connection override and transaction are mutually exclusive
|
|
41
|
+
const hasOverride = args.server || args.database || args.schema;
|
|
42
|
+
if (hasOverride && args.transactionId) {
|
|
43
|
+
throw new Error('Connection override (server/database/schema) cannot be used with transactions. Transactions are bound to the main connection.');
|
|
44
|
+
}
|
|
192
45
|
const dbManager = getDbManager();
|
|
193
46
|
const maxRows = validatePositiveInteger(args.maxRows, 'maxRows', 1, MAX_ROWS_LIMIT) || MAX_ROWS_DEFAULT;
|
|
194
47
|
const offset = args.offset !== undefined ? validatePositiveInteger(args.offset, 'offset', 0, Number.MAX_SAFE_INTEGER) : 0;
|
|
195
|
-
//
|
|
48
|
+
// Build connection override if specified
|
|
49
|
+
const override = hasOverride
|
|
50
|
+
? { server: args.server, database: args.database, schema: args.schema }
|
|
51
|
+
: undefined;
|
|
52
|
+
// Get schema hints if requested (uses current connection for schema lookup)
|
|
196
53
|
let schemaHint;
|
|
197
54
|
if (args.includeSchemaHint) {
|
|
198
|
-
schemaHint = await getSchemaHintForSql(sql);
|
|
55
|
+
schemaHint = await getSchemaHintForSql(sql, override);
|
|
199
56
|
}
|
|
200
57
|
// Handle multi-statement execution
|
|
201
58
|
if (args.allowMultipleStatements) {
|
|
202
|
-
return executeMultipleStatements(sql, schemaHint, args.transactionId);
|
|
59
|
+
return executeMultipleStatements(sql, schemaHint, args.transactionId, override);
|
|
203
60
|
}
|
|
204
61
|
// Record start time for execution timing
|
|
205
|
-
const startTime =
|
|
206
|
-
// Execute query with optional parameters
|
|
62
|
+
const startTime = getStartTime();
|
|
63
|
+
// Execute query with optional parameters
|
|
207
64
|
let result;
|
|
208
65
|
if (args.transactionId) {
|
|
66
|
+
// Transaction-based execution (no override support)
|
|
209
67
|
result = await dbManager.queryInTransaction(args.transactionId, sql, args.params);
|
|
210
68
|
}
|
|
69
|
+
else if (override) {
|
|
70
|
+
// Use connection override for one-time execution
|
|
71
|
+
const { client, release, server, database, schema } = await dbManager.getClientWithOverride(override);
|
|
72
|
+
try {
|
|
73
|
+
result = await client.query(sql, args.params);
|
|
74
|
+
// Add connection info to result for transparency
|
|
75
|
+
result._connectionInfo = { server, database, schema };
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
release();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
211
81
|
else {
|
|
82
|
+
// Default: use main connection
|
|
212
83
|
result = await dbManager.query(sql, args.params);
|
|
213
84
|
}
|
|
214
|
-
// Calculate execution time in milliseconds
|
|
215
|
-
const
|
|
216
|
-
const executionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
85
|
+
// Calculate execution time in milliseconds (already rounded to 2 decimal places)
|
|
86
|
+
const executionTimeMs = calculateExecutionTime(startTime, getStartTime());
|
|
217
87
|
// Defensive: ensure result has expected structure
|
|
218
88
|
if (!result || typeof result !== 'object') {
|
|
219
89
|
throw new Error('Query returned invalid result');
|
|
@@ -240,7 +110,7 @@ export async function executeSql(args) {
|
|
|
240
110
|
offset: startIndex,
|
|
241
111
|
fields,
|
|
242
112
|
rows: paginatedRows,
|
|
243
|
-
executionTimeMs
|
|
113
|
+
executionTimeMs,
|
|
244
114
|
generatedAt: new Date().toISOString()
|
|
245
115
|
};
|
|
246
116
|
fs.writeFileSync(filePath, JSON.stringify(outputData, null, 2), { mode: 0o600 });
|
|
@@ -250,7 +120,7 @@ export async function executeSql(args) {
|
|
|
250
120
|
fields,
|
|
251
121
|
outputFile: filePath,
|
|
252
122
|
truncated: true,
|
|
253
|
-
executionTimeMs
|
|
123
|
+
executionTimeMs,
|
|
254
124
|
offset: startIndex,
|
|
255
125
|
hasMore: endIndex < totalRows,
|
|
256
126
|
...(schemaHint && { schemaHint })
|
|
@@ -260,7 +130,7 @@ export async function executeSql(args) {
|
|
|
260
130
|
rows: paginatedRows,
|
|
261
131
|
rowCount: totalRows,
|
|
262
132
|
fields,
|
|
263
|
-
executionTimeMs
|
|
133
|
+
executionTimeMs,
|
|
264
134
|
offset: startIndex,
|
|
265
135
|
hasMore: endIndex < totalRows,
|
|
266
136
|
...(schemaHint && { schemaHint })
|
|
@@ -269,9 +139,9 @@ export async function executeSql(args) {
|
|
|
269
139
|
/**
|
|
270
140
|
* Execute multiple SQL statements and return results for each
|
|
271
141
|
*/
|
|
272
|
-
async function executeMultipleStatements(sql, schemaHint, transactionId) {
|
|
142
|
+
async function executeMultipleStatements(sql, schemaHint, transactionId, override) {
|
|
273
143
|
const dbManager = getDbManager();
|
|
274
|
-
const startTime =
|
|
144
|
+
const startTime = getStartTime();
|
|
275
145
|
// Parse statements with line numbers
|
|
276
146
|
const parsedStatements = splitSqlStatementsWithLineNumbers(sql);
|
|
277
147
|
// Filter out empty statements and comments-only
|
|
@@ -285,42 +155,75 @@ async function executeMultipleStatements(sql, schemaHint, transactionId) {
|
|
|
285
155
|
const results = [];
|
|
286
156
|
let successCount = 0;
|
|
287
157
|
let failureCount = 0;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
statementIndex: i + 1,
|
|
292
|
-
sql: stmt.sql.length > 200 ? stmt.sql.substring(0, 200) + '...' : stmt.sql,
|
|
293
|
-
lineNumber: stmt.lineNumber,
|
|
294
|
-
success: false,
|
|
295
|
-
};
|
|
158
|
+
// For override, get a single client and execute all statements
|
|
159
|
+
if (override) {
|
|
160
|
+
const { client, release } = await dbManager.getClientWithOverride(override);
|
|
296
161
|
try {
|
|
297
|
-
let
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
162
|
+
for (let i = 0; i < executableStatements.length; i++) {
|
|
163
|
+
const stmt = executableStatements[i];
|
|
164
|
+
const stmtResult = {
|
|
165
|
+
statementIndex: i + 1,
|
|
166
|
+
sql: stmt.sql.length > SQL_TRUNCATION_SHORT ? stmt.sql.substring(0, SQL_TRUNCATION_SHORT) + '...' : stmt.sql,
|
|
167
|
+
lineNumber: stmt.lineNumber,
|
|
168
|
+
success: false,
|
|
169
|
+
};
|
|
170
|
+
try {
|
|
171
|
+
const result = await client.query(stmt.sql);
|
|
172
|
+
stmtResult.success = true;
|
|
173
|
+
stmtResult.rows = result.rows?.slice(0, MAX_ROWS_PER_STATEMENT);
|
|
174
|
+
stmtResult.rowCount = result.rowCount ?? result.rows?.length ?? 0;
|
|
175
|
+
successCount++;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
stmtResult.success = false;
|
|
179
|
+
stmtResult.error = error instanceof Error ? error.message : String(error);
|
|
180
|
+
failureCount++;
|
|
181
|
+
}
|
|
182
|
+
results.push(stmtResult);
|
|
303
183
|
}
|
|
304
|
-
stmtResult.success = true;
|
|
305
|
-
stmtResult.rows = result.rows?.slice(0, 100); // Limit rows per statement
|
|
306
|
-
stmtResult.rowCount = result.rowCount ?? result.rows?.length ?? 0;
|
|
307
|
-
successCount++;
|
|
308
184
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
185
|
+
finally {
|
|
186
|
+
release();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Default execution path
|
|
191
|
+
for (let i = 0; i < executableStatements.length; i++) {
|
|
192
|
+
const stmt = executableStatements[i];
|
|
193
|
+
const stmtResult = {
|
|
194
|
+
statementIndex: i + 1,
|
|
195
|
+
sql: stmt.sql.length > SQL_TRUNCATION_SHORT ? stmt.sql.substring(0, SQL_TRUNCATION_SHORT) + '...' : stmt.sql,
|
|
196
|
+
lineNumber: stmt.lineNumber,
|
|
197
|
+
success: false,
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
let result;
|
|
201
|
+
if (transactionId) {
|
|
202
|
+
result = await dbManager.queryInTransaction(transactionId, stmt.sql);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
result = await dbManager.query(stmt.sql);
|
|
206
|
+
}
|
|
207
|
+
stmtResult.success = true;
|
|
208
|
+
stmtResult.rows = result.rows?.slice(0, MAX_ROWS_PER_STATEMENT); // Limit rows per statement
|
|
209
|
+
stmtResult.rowCount = result.rowCount ?? result.rows?.length ?? 0;
|
|
210
|
+
successCount++;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
stmtResult.success = false;
|
|
214
|
+
stmtResult.error = error instanceof Error ? error.message : String(error);
|
|
215
|
+
failureCount++;
|
|
216
|
+
}
|
|
217
|
+
results.push(stmtResult);
|
|
313
218
|
}
|
|
314
|
-
results.push(stmtResult);
|
|
315
219
|
}
|
|
316
|
-
const
|
|
317
|
-
const executionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
220
|
+
const executionTimeMs = calculateExecutionTime(startTime, getStartTime());
|
|
318
221
|
return {
|
|
319
222
|
results,
|
|
320
223
|
totalStatements: executableStatements.length,
|
|
321
224
|
successCount,
|
|
322
225
|
failureCount,
|
|
323
|
-
executionTimeMs
|
|
226
|
+
executionTimeMs,
|
|
324
227
|
...(schemaHint && { schemaHint })
|
|
325
228
|
};
|
|
326
229
|
}
|
|
@@ -340,13 +243,26 @@ export async function explainQuery(args) {
|
|
|
340
243
|
}
|
|
341
244
|
}
|
|
342
245
|
const dbManager = getDbManager();
|
|
343
|
-
const
|
|
246
|
+
const hasOverride = args.server || args.database || args.schema;
|
|
247
|
+
const override = hasOverride
|
|
248
|
+
? { server: args.server, database: args.database, schema: args.schema }
|
|
249
|
+
: undefined;
|
|
250
|
+
// Get client - either from override or main connection
|
|
251
|
+
let clientResult = null;
|
|
252
|
+
let client;
|
|
253
|
+
if (override) {
|
|
254
|
+
clientResult = await dbManager.getClientWithOverride(override);
|
|
255
|
+
client = clientResult.client;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
client = await dbManager.getClient();
|
|
259
|
+
}
|
|
344
260
|
try {
|
|
345
261
|
// If hypothetical indexes are specified, validate and create them
|
|
346
262
|
if (args.hypotheticalIndexes && args.hypotheticalIndexes.length > 0) {
|
|
347
263
|
// Limit number of hypothetical indexes
|
|
348
|
-
if (args.hypotheticalIndexes.length >
|
|
349
|
-
throw new Error(
|
|
264
|
+
if (args.hypotheticalIndexes.length > MAX_HYPOTHETICAL_INDEXES) {
|
|
265
|
+
throw new Error(`Maximum ${MAX_HYPOTHETICAL_INDEXES} hypothetical indexes allowed`);
|
|
350
266
|
}
|
|
351
267
|
// Check if hypopg extension is available
|
|
352
268
|
const hypopgCheck = await client.query(`
|
|
@@ -417,7 +333,8 @@ export async function explainQuery(args) {
|
|
|
417
333
|
await client.query('SELECT hypopg_reset()');
|
|
418
334
|
}
|
|
419
335
|
catch (e) {
|
|
420
|
-
//
|
|
336
|
+
// hypopg extension may not be available
|
|
337
|
+
console.debug('Could not reset hypopg:', e);
|
|
421
338
|
}
|
|
422
339
|
}
|
|
423
340
|
if (format === 'json') {
|
|
@@ -430,39 +347,14 @@ export async function explainQuery(args) {
|
|
|
430
347
|
};
|
|
431
348
|
}
|
|
432
349
|
finally {
|
|
433
|
-
client
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Preprocess SQL content by removing patterns.
|
|
438
|
-
* Supports both literal string matching and regex patterns.
|
|
439
|
-
*
|
|
440
|
-
* @param sql - The SQL content to preprocess
|
|
441
|
-
* @param patterns - Array of patterns to remove from SQL content
|
|
442
|
-
* @param isRegex - If true, patterns are treated as regex; if false, as literal strings
|
|
443
|
-
*/
|
|
444
|
-
function preprocessSqlContent(sql, patterns, isRegex = false) {
|
|
445
|
-
let result = sql;
|
|
446
|
-
for (const pattern of patterns) {
|
|
447
|
-
try {
|
|
448
|
-
if (isRegex) {
|
|
449
|
-
// Treat as regex pattern (multiline by default)
|
|
450
|
-
const regex = new RegExp(pattern, 'gm');
|
|
451
|
-
result = result.replace(regex, '');
|
|
452
|
-
}
|
|
453
|
-
else {
|
|
454
|
-
// Treat as literal string - escape and match on its own line
|
|
455
|
-
const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
456
|
-
const regex = new RegExp(`^\\s*${escapedPattern}\\s*$`, 'gm');
|
|
457
|
-
result = result.replace(regex, '');
|
|
458
|
-
}
|
|
350
|
+
// Release client properly based on connection type
|
|
351
|
+
if (clientResult) {
|
|
352
|
+
clientResult.release();
|
|
459
353
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
console.error(`Warning: Invalid pattern "${pattern}": ${error instanceof Error ? error.message : String(error)}`);
|
|
354
|
+
else {
|
|
355
|
+
client.release();
|
|
463
356
|
}
|
|
464
357
|
}
|
|
465
|
-
return result;
|
|
466
358
|
}
|
|
467
359
|
/**
|
|
468
360
|
* Execute a SQL file from the filesystem.
|
|
@@ -506,7 +398,12 @@ export async function executeSqlFile(args) {
|
|
|
506
398
|
const useTransaction = args.useTransaction !== false; // Default to true
|
|
507
399
|
const stopOnError = args.stopOnError !== false; // Default to true
|
|
508
400
|
const validateOnly = args.validateOnly === true; // Default to false
|
|
509
|
-
|
|
401
|
+
// Build connection override if specified
|
|
402
|
+
const hasOverride = args.server || args.database || args.schema;
|
|
403
|
+
const override = hasOverride
|
|
404
|
+
? { server: args.server, database: args.database, schema: args.schema }
|
|
405
|
+
: undefined;
|
|
406
|
+
const startTime = getStartTime();
|
|
510
407
|
// Split SQL into statements with line number tracking
|
|
511
408
|
const parsedStatements = splitSqlStatementsWithLineNumbers(sqlContent);
|
|
512
409
|
const executableStatements = parsedStatements.filter(stmt => {
|
|
@@ -519,13 +416,12 @@ export async function executeSqlFile(args) {
|
|
|
519
416
|
const totalStatements = executableStatements.length;
|
|
520
417
|
// If validateOnly mode, return preview without execution
|
|
521
418
|
if (validateOnly) {
|
|
522
|
-
const
|
|
523
|
-
const executionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
419
|
+
const executionTimeMs = calculateExecutionTime(startTime, getStartTime());
|
|
524
420
|
// Create preview of statements
|
|
525
421
|
const preview = executableStatements.map((stmt, idx) => ({
|
|
526
422
|
index: idx + 1,
|
|
527
423
|
lineNumber: stmt.lineNumber,
|
|
528
|
-
sql: stmt.sql.length >
|
|
424
|
+
sql: stmt.sql.length > SQL_TRUNCATION_LONG ? stmt.sql.substring(0, SQL_TRUNCATION_LONG) + '...' : stmt.sql,
|
|
529
425
|
type: detectStatementType(stmt.sql)
|
|
530
426
|
}));
|
|
531
427
|
return {
|
|
@@ -535,13 +431,22 @@ export async function executeSqlFile(args) {
|
|
|
535
431
|
totalStatements,
|
|
536
432
|
statementsExecuted: 0,
|
|
537
433
|
statementsFailed: 0,
|
|
538
|
-
executionTimeMs
|
|
434
|
+
executionTimeMs,
|
|
539
435
|
rowsAffected: 0,
|
|
540
436
|
validateOnly: true,
|
|
541
437
|
preview
|
|
542
438
|
};
|
|
543
439
|
}
|
|
544
|
-
|
|
440
|
+
// Get client - either from override or main connection
|
|
441
|
+
let clientResult = null;
|
|
442
|
+
let client;
|
|
443
|
+
if (override) {
|
|
444
|
+
clientResult = await dbManager.getClientWithOverride(override);
|
|
445
|
+
client = clientResult.client;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
client = await dbManager.getClient();
|
|
449
|
+
}
|
|
545
450
|
let statementsExecuted = 0;
|
|
546
451
|
let statementsFailed = 0;
|
|
547
452
|
let totalRowsAffected = 0;
|
|
@@ -573,7 +478,7 @@ export async function executeSqlFile(args) {
|
|
|
573
478
|
collectedErrors.push({
|
|
574
479
|
statementIndex: statementIndex + 1,
|
|
575
480
|
lineNumber: stmt.lineNumber,
|
|
576
|
-
sql: trimmed.length >
|
|
481
|
+
sql: trimmed.length > SQL_TRUNCATION_SHORT ? trimmed.substring(0, SQL_TRUNCATION_SHORT) + '...' : trimmed,
|
|
577
482
|
error: errorMessage
|
|
578
483
|
});
|
|
579
484
|
throw error;
|
|
@@ -582,7 +487,7 @@ export async function executeSqlFile(args) {
|
|
|
582
487
|
collectedErrors.push({
|
|
583
488
|
statementIndex: statementIndex + 1,
|
|
584
489
|
lineNumber: stmt.lineNumber,
|
|
585
|
-
sql: trimmed.length >
|
|
490
|
+
sql: trimmed.length > SQL_TRUNCATION_SHORT ? trimmed.substring(0, SQL_TRUNCATION_SHORT) + '...' : trimmed,
|
|
586
491
|
error: errorMessage
|
|
587
492
|
});
|
|
588
493
|
console.error(`Warning: Statement ${statementIndex + 1} at line ${stmt.lineNumber} failed: ${errorMessage}`);
|
|
@@ -591,8 +496,7 @@ export async function executeSqlFile(args) {
|
|
|
591
496
|
if (useTransaction && !rolledBack) {
|
|
592
497
|
await client.query('COMMIT');
|
|
593
498
|
}
|
|
594
|
-
const
|
|
595
|
-
const executionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
499
|
+
const executionTimeMs = calculateExecutionTime(startTime, getStartTime());
|
|
596
500
|
const result = {
|
|
597
501
|
success: statementsFailed === 0,
|
|
598
502
|
filePath: resolvedPath,
|
|
@@ -600,7 +504,7 @@ export async function executeSqlFile(args) {
|
|
|
600
504
|
totalStatements,
|
|
601
505
|
statementsExecuted,
|
|
602
506
|
statementsFailed,
|
|
603
|
-
executionTimeMs
|
|
507
|
+
executionTimeMs,
|
|
604
508
|
rowsAffected: totalRowsAffected
|
|
605
509
|
};
|
|
606
510
|
// Include errors array if there were any failures (when stopOnError=false)
|
|
@@ -610,8 +514,7 @@ export async function executeSqlFile(args) {
|
|
|
610
514
|
return result;
|
|
611
515
|
}
|
|
612
516
|
catch (error) {
|
|
613
|
-
const
|
|
614
|
-
const executionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
517
|
+
const executionTimeMs = calculateExecutionTime(startTime, getStartTime());
|
|
615
518
|
const result = {
|
|
616
519
|
success: false,
|
|
617
520
|
filePath: resolvedPath,
|
|
@@ -619,7 +522,7 @@ export async function executeSqlFile(args) {
|
|
|
619
522
|
totalStatements,
|
|
620
523
|
statementsExecuted,
|
|
621
524
|
statementsFailed,
|
|
622
|
-
executionTimeMs
|
|
525
|
+
executionTimeMs,
|
|
623
526
|
rowsAffected: totalRowsAffected,
|
|
624
527
|
error: error instanceof Error ? error.message : String(error),
|
|
625
528
|
rollback: rolledBack
|
|
@@ -630,40 +533,14 @@ export async function executeSqlFile(args) {
|
|
|
630
533
|
return result;
|
|
631
534
|
}
|
|
632
535
|
finally {
|
|
633
|
-
client
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
function detectStatementType(sql) {
|
|
640
|
-
const trimmed = stripLeadingComments(sql).toUpperCase();
|
|
641
|
-
// Common statement types
|
|
642
|
-
const types = [
|
|
643
|
-
'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP',
|
|
644
|
-
'TRUNCATE', 'GRANT', 'REVOKE', 'BEGIN', 'COMMIT', 'ROLLBACK',
|
|
645
|
-
'SET', 'SHOW', 'EXPLAIN', 'ANALYZE', 'VACUUM', 'REINDEX',
|
|
646
|
-
'COMMENT', 'WITH', 'DO', 'CALL', 'EXECUTE'
|
|
647
|
-
];
|
|
648
|
-
for (const type of types) {
|
|
649
|
-
if (trimmed.startsWith(type + ' ') || trimmed.startsWith(type + '\n') ||
|
|
650
|
-
trimmed.startsWith(type + '\t') || trimmed === type) {
|
|
651
|
-
// Special case for WITH - check if it's a CTE followed by SELECT/INSERT/UPDATE/DELETE
|
|
652
|
-
if (type === 'WITH') {
|
|
653
|
-
if (trimmed.includes('SELECT'))
|
|
654
|
-
return 'WITH SELECT';
|
|
655
|
-
if (trimmed.includes('INSERT'))
|
|
656
|
-
return 'WITH INSERT';
|
|
657
|
-
if (trimmed.includes('UPDATE'))
|
|
658
|
-
return 'WITH UPDATE';
|
|
659
|
-
if (trimmed.includes('DELETE'))
|
|
660
|
-
return 'WITH DELETE';
|
|
661
|
-
return 'WITH';
|
|
662
|
-
}
|
|
663
|
-
return type;
|
|
536
|
+
// Release client properly based on connection type
|
|
537
|
+
if (clientResult) {
|
|
538
|
+
clientResult.release();
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
client.release();
|
|
664
542
|
}
|
|
665
543
|
}
|
|
666
|
-
return 'UNKNOWN';
|
|
667
544
|
}
|
|
668
545
|
/**
|
|
669
546
|
* Preview a SQL file without executing.
|
|
@@ -703,7 +580,7 @@ export async function previewSqlFile(args) {
|
|
|
703
580
|
if (args.stripPatterns && args.stripPatterns.length > 0) {
|
|
704
581
|
sqlContent = preprocessSqlContent(sqlContent, args.stripPatterns, args.stripAsRegex === true);
|
|
705
582
|
}
|
|
706
|
-
const maxStatements = Math.min(args.maxStatements ||
|
|
583
|
+
const maxStatements = Math.min(args.maxStatements || DEFAULT_PREVIEW_STATEMENTS, MAX_PREVIEW_STATEMENTS);
|
|
707
584
|
// Split SQL into statements with line number tracking
|
|
708
585
|
const parsedStatements = splitSqlStatementsWithLineNumbers(sqlContent);
|
|
709
586
|
const executableStatements = parsedStatements.filter(stmt => {
|
|
@@ -739,15 +616,20 @@ export async function previewSqlFile(args) {
|
|
|
739
616
|
const statements = executableStatements.slice(0, maxStatements).map((stmt, idx) => ({
|
|
740
617
|
index: idx + 1,
|
|
741
618
|
lineNumber: stmt.lineNumber,
|
|
742
|
-
sql: stmt.sql.length >
|
|
619
|
+
sql: stmt.sql.length > SQL_TRUNCATION_LONG ? stmt.sql.substring(0, SQL_TRUNCATION_LONG) + '...' : stmt.sql,
|
|
743
620
|
type: detectStatementType(stmt.sql)
|
|
744
621
|
}));
|
|
745
622
|
// Format file size
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
623
|
+
let fileSizeFormatted;
|
|
624
|
+
if (stats.size < 1024) {
|
|
625
|
+
fileSizeFormatted = `${stats.size} bytes`;
|
|
626
|
+
}
|
|
627
|
+
else if (stats.size < 1024 * 1024) {
|
|
628
|
+
fileSizeFormatted = `${(stats.size / 1024).toFixed(1)} KB`;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
fileSizeFormatted = `${(stats.size / (1024 * 1024)).toFixed(2)} MB`;
|
|
632
|
+
}
|
|
751
633
|
// Generate summary
|
|
752
634
|
const typeEntries = Object.entries(statementsByType).sort((a, b) => b[1] - a[1]);
|
|
753
635
|
const typeSummary = typeEntries.map(([type, count]) => `${count} ${type}`).join(', ');
|
|
@@ -808,7 +690,7 @@ export async function dryRunSqlFile(args) {
|
|
|
808
690
|
if (args.stripPatterns && args.stripPatterns.length > 0) {
|
|
809
691
|
sqlContent = preprocessSqlContent(sqlContent, args.stripPatterns, args.stripAsRegex === true);
|
|
810
692
|
}
|
|
811
|
-
const maxStatements = Math.min(args.maxStatements ||
|
|
693
|
+
const maxStatements = Math.min(args.maxStatements || DEFAULT_DRY_RUN_STATEMENTS, MAX_DRY_RUN_STATEMENTS);
|
|
812
694
|
const stopOnError = args.stopOnError === true;
|
|
813
695
|
// Split SQL into statements with line number tracking
|
|
814
696
|
const parsedStatements = splitSqlStatementsWithLineNumbers(sqlContent);
|
|
@@ -827,14 +709,33 @@ export async function dryRunSqlFile(args) {
|
|
|
827
709
|
nonRollbackableWarnings.push(...warnings);
|
|
828
710
|
});
|
|
829
711
|
// Format file size
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
712
|
+
let fileSizeFormatted;
|
|
713
|
+
if (stats.size < 1024) {
|
|
714
|
+
fileSizeFormatted = `${stats.size} bytes`;
|
|
715
|
+
}
|
|
716
|
+
else if (stats.size < 1024 * 1024) {
|
|
717
|
+
fileSizeFormatted = `${(stats.size / 1024).toFixed(1)} KB`;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
fileSizeFormatted = `${(stats.size / (1024 * 1024)).toFixed(2)} MB`;
|
|
721
|
+
}
|
|
835
722
|
const dbManager = getDbManager();
|
|
836
|
-
|
|
837
|
-
const
|
|
723
|
+
// Build connection override if specified
|
|
724
|
+
const hasOverride = args.server || args.database || args.schema;
|
|
725
|
+
const override = hasOverride
|
|
726
|
+
? { server: args.server, database: args.database, schema: args.schema }
|
|
727
|
+
: undefined;
|
|
728
|
+
// Get client - either from override or main connection
|
|
729
|
+
let clientResult = null;
|
|
730
|
+
let client;
|
|
731
|
+
if (override) {
|
|
732
|
+
clientResult = await dbManager.getClientWithOverride(override);
|
|
733
|
+
client = clientResult.client;
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
client = await dbManager.getClient();
|
|
737
|
+
}
|
|
738
|
+
const startTime = getStartTime();
|
|
838
739
|
const statementResults = [];
|
|
839
740
|
const statementsByType = {};
|
|
840
741
|
let successCount = 0;
|
|
@@ -849,11 +750,11 @@ export async function dryRunSqlFile(args) {
|
|
|
849
750
|
const stmt = executableStatements[idx];
|
|
850
751
|
const stmtType = detectStatementType(stmt.sql);
|
|
851
752
|
statementsByType[stmtType] = (statementsByType[stmtType] || 0) + 1;
|
|
852
|
-
const stmtStartTime =
|
|
753
|
+
const stmtStartTime = getStartTime();
|
|
853
754
|
const result = {
|
|
854
755
|
index: idx + 1,
|
|
855
756
|
lineNumber: stmt.lineNumber,
|
|
856
|
-
sql: stmt.sql.length >
|
|
757
|
+
sql: stmt.sql.length > SQL_TRUNCATION_LONG ? stmt.sql.substring(0, SQL_TRUNCATION_LONG) + '...' : stmt.sql,
|
|
857
758
|
type: stmtType,
|
|
858
759
|
success: false
|
|
859
760
|
};
|
|
@@ -908,8 +809,7 @@ export async function dryRunSqlFile(args) {
|
|
|
908
809
|
}
|
|
909
810
|
}
|
|
910
811
|
}
|
|
911
|
-
|
|
912
|
-
result.executionTimeMs = Math.round(Number(stmtEndTime - stmtStartTime) / 1_000_000 * 100) / 100;
|
|
812
|
+
result.executionTimeMs = calculateExecutionTime(stmtStartTime, getStartTime());
|
|
913
813
|
// Only include results up to maxStatements
|
|
914
814
|
if (statementResults.length < maxStatements) {
|
|
915
815
|
statementResults.push(result);
|
|
@@ -929,10 +829,15 @@ export async function dryRunSqlFile(args) {
|
|
|
929
829
|
throw e;
|
|
930
830
|
}
|
|
931
831
|
finally {
|
|
932
|
-
client
|
|
832
|
+
// Release client properly based on connection type
|
|
833
|
+
if (clientResult) {
|
|
834
|
+
clientResult.release();
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
client.release();
|
|
838
|
+
}
|
|
933
839
|
}
|
|
934
|
-
const
|
|
935
|
-
const executionTimeMs = Math.round(Number(endTime - startTime) / 1_000_000 * 100) / 100;
|
|
840
|
+
const executionTimeMs = calculateExecutionTime(startTime, getStartTime());
|
|
936
841
|
// Generate summary
|
|
937
842
|
const typeEntries = Object.entries(statementsByType).sort((a, b) => b[1] - a[1]);
|
|
938
843
|
const typeSummary = typeEntries.map(([type, count]) => `${count} ${type}`).join(', ');
|
|
@@ -966,194 +871,99 @@ export async function dryRunSqlFile(args) {
|
|
|
966
871
|
};
|
|
967
872
|
}
|
|
968
873
|
/**
|
|
969
|
-
*
|
|
970
|
-
* Returns empty string if the entire content is just comments.
|
|
971
|
-
*/
|
|
972
|
-
function stripLeadingComments(sql) {
|
|
973
|
-
let result = sql.trim();
|
|
974
|
-
while (result.length > 0) {
|
|
975
|
-
// Strip leading line comments
|
|
976
|
-
if (result.startsWith('--')) {
|
|
977
|
-
const newlineIndex = result.indexOf('\n');
|
|
978
|
-
if (newlineIndex === -1) {
|
|
979
|
-
return ''; // Entire string is a line comment
|
|
980
|
-
}
|
|
981
|
-
result = result.substring(newlineIndex + 1).trim();
|
|
982
|
-
continue;
|
|
983
|
-
}
|
|
984
|
-
// Strip leading block comments
|
|
985
|
-
if (result.startsWith('/*')) {
|
|
986
|
-
const endIndex = result.indexOf('*/');
|
|
987
|
-
if (endIndex === -1) {
|
|
988
|
-
return ''; // Unclosed block comment
|
|
989
|
-
}
|
|
990
|
-
result = result.substring(endIndex + 2).trim();
|
|
991
|
-
continue;
|
|
992
|
-
}
|
|
993
|
-
// No more leading comments
|
|
994
|
-
break;
|
|
995
|
-
}
|
|
996
|
-
return result;
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* Split SQL content into individual statements with line number tracking.
|
|
1000
|
-
* Returns ParsedStatement objects with SQL and line number info.
|
|
874
|
+
* Gets schema hints for tables mentioned in SQL
|
|
1001
875
|
*/
|
|
1002
|
-
function
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
i
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
876
|
+
async function getSchemaHintForSql(sql, override) {
|
|
877
|
+
const dbManager = getDbManager();
|
|
878
|
+
const tables = extractTablesFromSql(sql);
|
|
879
|
+
const tableHints = [];
|
|
880
|
+
// If override is specified, use a single client for all queries
|
|
881
|
+
if (override) {
|
|
882
|
+
const { client, release } = await dbManager.getClientWithOverride(override);
|
|
883
|
+
try {
|
|
884
|
+
for (const { schema, table } of tables.slice(0, MAX_TABLES_TO_ANALYZE)) {
|
|
885
|
+
try {
|
|
886
|
+
const columnsResult = await client.query(`
|
|
887
|
+
SELECT
|
|
888
|
+
column_name as name,
|
|
889
|
+
data_type as type,
|
|
890
|
+
is_nullable = 'YES' as nullable
|
|
891
|
+
FROM information_schema.columns
|
|
892
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
893
|
+
ORDER BY ordinal_position
|
|
894
|
+
`, [schema, table]);
|
|
895
|
+
const pkResult = await client.query(`
|
|
896
|
+
SELECT a.attname as column_name
|
|
897
|
+
FROM pg_index i
|
|
898
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
899
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
900
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
901
|
+
WHERE i.indisprimary
|
|
902
|
+
AND n.nspname = $1
|
|
903
|
+
AND c.relname = $2
|
|
904
|
+
`, [schema, table]);
|
|
905
|
+
const fkResult = await client.query(`
|
|
906
|
+
SELECT
|
|
907
|
+
kcu.column_name,
|
|
908
|
+
ccu.table_schema || '.' || ccu.table_name as referenced_table,
|
|
909
|
+
ccu.column_name as referenced_column
|
|
910
|
+
FROM information_schema.table_constraints tc
|
|
911
|
+
JOIN information_schema.key_column_usage kcu
|
|
912
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
913
|
+
AND tc.table_schema = kcu.table_schema
|
|
914
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
915
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
916
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
917
|
+
AND tc.table_schema = $1
|
|
918
|
+
AND tc.table_name = $2
|
|
919
|
+
`, [schema, table]);
|
|
920
|
+
const countResult = await client.query(`
|
|
921
|
+
SELECT reltuples::bigint as estimate
|
|
922
|
+
FROM pg_class c
|
|
923
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
924
|
+
WHERE n.nspname = $1 AND c.relname = $2
|
|
925
|
+
`, [schema, table]);
|
|
926
|
+
const hint = {
|
|
927
|
+
schema,
|
|
928
|
+
table,
|
|
929
|
+
columns: columnsResult.rows.map(r => ({
|
|
930
|
+
name: r.name,
|
|
931
|
+
type: r.type,
|
|
932
|
+
nullable: r.nullable
|
|
933
|
+
})),
|
|
934
|
+
primaryKey: pkResult.rows.map(r => r.column_name),
|
|
935
|
+
rowCountEstimate: countResult.rows[0]?.estimate || 0
|
|
936
|
+
};
|
|
937
|
+
if (fkResult.rows.length > 0) {
|
|
938
|
+
const fkMap = new Map();
|
|
939
|
+
for (const row of fkResult.rows) {
|
|
940
|
+
const key = row.referenced_table;
|
|
941
|
+
if (!fkMap.has(key)) {
|
|
942
|
+
fkMap.set(key, { columns: [], referencedColumns: [] });
|
|
943
|
+
}
|
|
944
|
+
fkMap.get(key).columns.push(row.column_name);
|
|
945
|
+
fkMap.get(key).referencedColumns.push(row.referenced_column);
|
|
946
|
+
}
|
|
947
|
+
hint.foreignKeys = Array.from(fkMap.entries()).map(([refTable, data]) => ({
|
|
948
|
+
columns: data.columns,
|
|
949
|
+
referencedTable: refTable,
|
|
950
|
+
referencedColumns: data.referencedColumns
|
|
951
|
+
}));
|
|
952
|
+
}
|
|
953
|
+
tableHints.push(hint);
|
|
1060
954
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
// Handle dollar-quoted strings (PostgreSQL specific)
|
|
1066
|
-
if (!inString && !inLineComment && !inBlockComment && char === '$') {
|
|
1067
|
-
const dollarMatch = sql.slice(i).match(/^(\$[a-zA-Z0-9_]*\$)/);
|
|
1068
|
-
if (dollarMatch) {
|
|
1069
|
-
const dollarTag = dollarMatch[1];
|
|
1070
|
-
const endIndex = sql.indexOf(dollarTag, i + dollarTag.length);
|
|
1071
|
-
if (endIndex !== -1) {
|
|
1072
|
-
const dollarContent = sql.slice(i, endIndex + dollarTag.length);
|
|
1073
|
-
// Count newlines in dollar-quoted content
|
|
1074
|
-
const newlines = (dollarContent.match(/\n/g) || []).length;
|
|
1075
|
-
currentLineNumber += newlines;
|
|
1076
|
-
current += dollarContent;
|
|
1077
|
-
i = endIndex + dollarTag.length;
|
|
1078
|
-
continue;
|
|
955
|
+
catch (error) {
|
|
956
|
+
console.error(`Could not get schema hint for ${schema}.${table}: ${error}`);
|
|
1079
957
|
}
|
|
1080
958
|
}
|
|
1081
959
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
current += char;
|
|
1085
|
-
const trimmed = current.trim();
|
|
1086
|
-
if (trimmed) {
|
|
1087
|
-
statements.push({ sql: trimmed, lineNumber: statementStartLine });
|
|
1088
|
-
}
|
|
1089
|
-
current = '';
|
|
1090
|
-
statementStartLine = currentLineNumber;
|
|
1091
|
-
i++;
|
|
1092
|
-
continue;
|
|
1093
|
-
}
|
|
1094
|
-
current += char;
|
|
1095
|
-
i++;
|
|
1096
|
-
}
|
|
1097
|
-
// Add remaining content if any
|
|
1098
|
-
const trimmed = current.trim();
|
|
1099
|
-
if (trimmed) {
|
|
1100
|
-
statements.push({ sql: trimmed, lineNumber: statementStartLine });
|
|
1101
|
-
}
|
|
1102
|
-
return statements;
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Extracts table names from a SQL query.
|
|
1106
|
-
* Handles common patterns: FROM, JOIN, INTO, UPDATE, DELETE FROM
|
|
1107
|
-
*/
|
|
1108
|
-
function extractTablesFromSql(sql) {
|
|
1109
|
-
const tables = [];
|
|
1110
|
-
const seen = new Set();
|
|
1111
|
-
// Normalize SQL: remove comments and extra whitespace
|
|
1112
|
-
const normalized = sql
|
|
1113
|
-
.replace(/--[^\n]*/g, '') // Remove line comments
|
|
1114
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
1115
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
1116
|
-
.trim();
|
|
1117
|
-
// Patterns to find table references
|
|
1118
|
-
const patterns = [
|
|
1119
|
-
/\bFROM\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
1120
|
-
/\bJOIN\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
1121
|
-
/\bINTO\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
1122
|
-
/\bUPDATE\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
1123
|
-
/\bDELETE\s+FROM\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
1124
|
-
];
|
|
1125
|
-
for (const pattern of patterns) {
|
|
1126
|
-
let match;
|
|
1127
|
-
while ((match = pattern.exec(normalized)) !== null) {
|
|
1128
|
-
const tableRef = match[1].replace(/["`]/g, '').trim();
|
|
1129
|
-
// Skip common SQL keywords that might be matched
|
|
1130
|
-
if (['SELECT', 'WHERE', 'SET', 'VALUES', 'AND', 'OR'].includes(tableRef.toUpperCase())) {
|
|
1131
|
-
continue;
|
|
1132
|
-
}
|
|
1133
|
-
let schema = 'public';
|
|
1134
|
-
let table = tableRef;
|
|
1135
|
-
if (tableRef.includes('.')) {
|
|
1136
|
-
const parts = tableRef.split('.');
|
|
1137
|
-
schema = parts[0].trim();
|
|
1138
|
-
table = parts[1].trim();
|
|
1139
|
-
}
|
|
1140
|
-
const key = `${schema}.${table}`.toLowerCase();
|
|
1141
|
-
if (!seen.has(key)) {
|
|
1142
|
-
seen.add(key);
|
|
1143
|
-
tables.push({ schema, table });
|
|
1144
|
-
}
|
|
960
|
+
finally {
|
|
961
|
+
release();
|
|
1145
962
|
}
|
|
963
|
+
return { tables: tableHints };
|
|
1146
964
|
}
|
|
1147
|
-
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Gets schema hints for tables mentioned in SQL
|
|
1151
|
-
*/
|
|
1152
|
-
async function getSchemaHintForSql(sql) {
|
|
1153
|
-
const dbManager = getDbManager();
|
|
1154
|
-
const tables = extractTablesFromSql(sql);
|
|
1155
|
-
const tableHints = [];
|
|
1156
|
-
for (const { schema, table } of tables.slice(0, 10)) { // Limit to 10 tables
|
|
965
|
+
// Default path: use main connection
|
|
966
|
+
for (const { schema, table } of tables.slice(0, MAX_TABLES_TO_ANALYZE)) { // Limit to 10 tables
|
|
1157
967
|
try {
|
|
1158
968
|
// Get columns
|
|
1159
969
|
const columnsResult = await dbManager.query(`
|
|
@@ -1245,10 +1055,10 @@ export async function mutationPreview(args) {
|
|
|
1245
1055
|
throw new Error('sql parameter is required');
|
|
1246
1056
|
}
|
|
1247
1057
|
const sql = args.sql.trim();
|
|
1248
|
-
const sampleSize = Math.min(args.sampleSize ||
|
|
1058
|
+
const sampleSize = Math.min(args.sampleSize || DEFAULT_MUTATION_SAMPLE_SIZE, MAX_MUTATION_SAMPLE_SIZE);
|
|
1249
1059
|
// Detect mutation type
|
|
1250
1060
|
const upperSql = sql.toUpperCase();
|
|
1251
|
-
let mutationType
|
|
1061
|
+
let mutationType;
|
|
1252
1062
|
if (upperSql.startsWith('UPDATE')) {
|
|
1253
1063
|
mutationType = 'UPDATE';
|
|
1254
1064
|
}
|
|
@@ -1262,10 +1072,15 @@ export async function mutationPreview(args) {
|
|
|
1262
1072
|
throw new Error('SQL must be an INSERT, UPDATE, or DELETE statement');
|
|
1263
1073
|
}
|
|
1264
1074
|
const dbManager = getDbManager();
|
|
1075
|
+
// Build connection override if specified
|
|
1076
|
+
const hasOverride = args.server || args.database || args.schema;
|
|
1077
|
+
const override = hasOverride
|
|
1078
|
+
? { server: args.server, database: args.database, schema: args.schema }
|
|
1079
|
+
: undefined;
|
|
1265
1080
|
// For INSERT, we can't preview affected rows
|
|
1266
1081
|
if (mutationType === 'INSERT') {
|
|
1267
1082
|
// Use EXPLAIN to estimate rows
|
|
1268
|
-
const explainResult = await dbManager.
|
|
1083
|
+
const explainResult = await dbManager.queryWithOverride(`EXPLAIN (FORMAT JSON) ${sql}`, undefined, override);
|
|
1269
1084
|
const plan = explainResult.rows[0]['QUERY PLAN'][0];
|
|
1270
1085
|
return {
|
|
1271
1086
|
mutationType,
|
|
@@ -1277,19 +1092,22 @@ export async function mutationPreview(args) {
|
|
|
1277
1092
|
// For UPDATE and DELETE, extract WHERE clause and table
|
|
1278
1093
|
let targetTable;
|
|
1279
1094
|
let whereClause;
|
|
1095
|
+
// Extract WHERE clause using string search (avoids regex backtracking)
|
|
1096
|
+
const whereIdx = sql.toUpperCase().lastIndexOf('WHERE');
|
|
1097
|
+
if (whereIdx !== -1) {
|
|
1098
|
+
whereClause = sql.substring(whereIdx + 5).trim();
|
|
1099
|
+
}
|
|
1100
|
+
const updatePattern = /UPDATE\s+(["`]?\w[\w.]*["`]?)\s+SET/i;
|
|
1101
|
+
const deletePattern = /DELETE\s+FROM\s+(["`]?\w[\w.]*["`]?)/i;
|
|
1280
1102
|
if (mutationType === 'UPDATE') {
|
|
1281
1103
|
// Pattern: UPDATE table SET ... WHERE ...
|
|
1282
|
-
const updateMatch =
|
|
1283
|
-
const whereMatch = sql.match(/\bWHERE\s+(.+)$/is);
|
|
1104
|
+
const updateMatch = updatePattern.exec(sql);
|
|
1284
1105
|
targetTable = updateMatch?.[1]?.replace(/["`]/g, '');
|
|
1285
|
-
whereClause = whereMatch?.[1];
|
|
1286
1106
|
}
|
|
1287
1107
|
else if (mutationType === 'DELETE') {
|
|
1288
1108
|
// Pattern: DELETE FROM table WHERE ...
|
|
1289
|
-
const deleteMatch =
|
|
1290
|
-
const whereMatch = sql.match(/\bWHERE\s+(.+)$/is);
|
|
1109
|
+
const deleteMatch = deletePattern.exec(sql);
|
|
1291
1110
|
targetTable = deleteMatch?.[1]?.replace(/["`]/g, '');
|
|
1292
|
-
whereClause = whereMatch?.[1];
|
|
1293
1111
|
}
|
|
1294
1112
|
if (!targetTable) {
|
|
1295
1113
|
throw new Error('Could not parse target table from SQL');
|
|
@@ -1297,12 +1115,13 @@ export async function mutationPreview(args) {
|
|
|
1297
1115
|
// Get estimated row count using EXPLAIN
|
|
1298
1116
|
let estimatedRowsAffected = 0;
|
|
1299
1117
|
try {
|
|
1300
|
-
const explainResult = await dbManager.
|
|
1118
|
+
const explainResult = await dbManager.queryWithOverride(`EXPLAIN (FORMAT JSON) ${sql}`, undefined, override);
|
|
1301
1119
|
const plan = explainResult.rows[0]['QUERY PLAN'][0];
|
|
1302
1120
|
estimatedRowsAffected = plan?.Plan?.['Plan Rows'] || 0;
|
|
1303
1121
|
}
|
|
1304
1122
|
catch (error) {
|
|
1305
|
-
// EXPLAIN might fail, continue with count query
|
|
1123
|
+
// EXPLAIN might fail for complex queries, continue with count query
|
|
1124
|
+
console.debug('Could not get EXPLAIN plan:', error);
|
|
1306
1125
|
}
|
|
1307
1126
|
// Build SELECT query to get sample of affected rows
|
|
1308
1127
|
let sampleRows = [];
|
|
@@ -1310,14 +1129,14 @@ export async function mutationPreview(args) {
|
|
|
1310
1129
|
const selectSql = whereClause
|
|
1311
1130
|
? `SELECT * FROM ${targetTable} WHERE ${whereClause} LIMIT ${sampleSize}`
|
|
1312
1131
|
: `SELECT * FROM ${targetTable} LIMIT ${sampleSize}`;
|
|
1313
|
-
const sampleResult = await dbManager.
|
|
1132
|
+
const sampleResult = await dbManager.queryWithOverride(selectSql, undefined, override);
|
|
1314
1133
|
sampleRows = sampleResult.rows;
|
|
1315
1134
|
// If EXPLAIN didn't work, get count
|
|
1316
1135
|
if (estimatedRowsAffected === 0) {
|
|
1317
1136
|
const countSql = whereClause
|
|
1318
1137
|
? `SELECT COUNT(*) as cnt FROM ${targetTable} WHERE ${whereClause}`
|
|
1319
1138
|
: `SELECT COUNT(*) as cnt FROM ${targetTable}`;
|
|
1320
|
-
const countResult = await dbManager.
|
|
1139
|
+
const countResult = await dbManager.queryWithOverride(countSql, undefined, override);
|
|
1321
1140
|
estimatedRowsAffected = parseInt(countResult.rows[0]?.cnt || '0', 10);
|
|
1322
1141
|
}
|
|
1323
1142
|
}
|
|
@@ -1353,17 +1172,20 @@ export async function mutationDryRun(args) {
|
|
|
1353
1172
|
throw new Error('sql parameter is required');
|
|
1354
1173
|
}
|
|
1355
1174
|
const sql = args.sql.trim();
|
|
1356
|
-
const sampleSize = Math.min(args.sampleSize || MAX_DRY_RUN_SAMPLE_ROWS,
|
|
1175
|
+
const sampleSize = Math.min(args.sampleSize || MAX_DRY_RUN_SAMPLE_ROWS, MAX_MUTATION_SAMPLE_SIZE);
|
|
1357
1176
|
// Detect mutation type
|
|
1358
1177
|
const upperSql = sql.toUpperCase();
|
|
1359
|
-
let mutationType
|
|
1360
|
-
|
|
1178
|
+
let mutationType;
|
|
1179
|
+
const withUpdatePattern = /^WITH\b.*\bUPDATE\b/s;
|
|
1180
|
+
const withDeletePattern = /^WITH\b.*\bDELETE\b/s;
|
|
1181
|
+
const withInsertPattern = /^WITH\b.*\bINSERT\b/s;
|
|
1182
|
+
if (upperSql.startsWith('UPDATE') || withUpdatePattern.test(upperSql)) {
|
|
1361
1183
|
mutationType = 'UPDATE';
|
|
1362
1184
|
}
|
|
1363
|
-
else if (upperSql.startsWith('DELETE') ||
|
|
1185
|
+
else if (upperSql.startsWith('DELETE') || withDeletePattern.test(upperSql)) {
|
|
1364
1186
|
mutationType = 'DELETE';
|
|
1365
1187
|
}
|
|
1366
|
-
else if (upperSql.startsWith('INSERT') ||
|
|
1188
|
+
else if (upperSql.startsWith('INSERT') || withInsertPattern.test(upperSql)) {
|
|
1367
1189
|
mutationType = 'INSERT';
|
|
1368
1190
|
}
|
|
1369
1191
|
else {
|
|
@@ -1372,14 +1194,19 @@ export async function mutationDryRun(args) {
|
|
|
1372
1194
|
// Check for non-rollbackable operations
|
|
1373
1195
|
const nonRollbackableWarnings = detectNonRollbackableOperations(sql);
|
|
1374
1196
|
const mustSkipWarnings = nonRollbackableWarnings.filter(w => w.mustSkip);
|
|
1197
|
+
const dbManager = getDbManager();
|
|
1198
|
+
// Build connection override if specified
|
|
1199
|
+
const hasOverride = args.server || args.database || args.schema;
|
|
1200
|
+
const override = hasOverride
|
|
1201
|
+
? { server: args.server, database: args.database, schema: args.schema }
|
|
1202
|
+
: undefined;
|
|
1375
1203
|
// Skip only if there are mustSkip warnings
|
|
1376
1204
|
if (mustSkipWarnings.length > 0) {
|
|
1377
1205
|
const skipReason = mustSkipWarnings.map(w => w.message).join('; ');
|
|
1378
1206
|
// Run EXPLAIN to show query plan without executing
|
|
1379
1207
|
let explainPlan;
|
|
1380
|
-
const dbManager = getDbManager();
|
|
1381
1208
|
try {
|
|
1382
|
-
const explainResult = await dbManager.
|
|
1209
|
+
const explainResult = await dbManager.queryWithOverride(`EXPLAIN (FORMAT JSON) ${sql}`, undefined, override);
|
|
1383
1210
|
if (explainResult.rows && explainResult.rows.length > 0) {
|
|
1384
1211
|
explainPlan = explainResult.rows[0]['QUERY PLAN'];
|
|
1385
1212
|
}
|
|
@@ -1401,25 +1228,41 @@ export async function mutationDryRun(args) {
|
|
|
1401
1228
|
// Extract table and WHERE clause
|
|
1402
1229
|
let targetTable;
|
|
1403
1230
|
let whereClause;
|
|
1231
|
+
// Extract WHERE clause using string search (avoids regex backtracking)
|
|
1232
|
+
const upperSqlForWhere = sql.toUpperCase();
|
|
1233
|
+
const whereIdx = upperSqlForWhere.lastIndexOf('WHERE');
|
|
1234
|
+
if (whereIdx !== -1) {
|
|
1235
|
+
let endIdx = upperSqlForWhere.indexOf('RETURNING', whereIdx);
|
|
1236
|
+
if (endIdx === -1)
|
|
1237
|
+
endIdx = sql.length;
|
|
1238
|
+
whereClause = sql.substring(whereIdx + 5, endIdx).trim();
|
|
1239
|
+
}
|
|
1240
|
+
const updateTablePattern = /UPDATE\s+(["`]?\w[\w.]*["`]?)\s+SET/i;
|
|
1241
|
+
const deleteTablePattern = /DELETE\s+FROM\s+(["`]?\w[\w.]*["`]?)/i;
|
|
1242
|
+
const insertTablePattern = /INSERT\s+INTO\s+(["`]?\w[\w.]*["`]?)/i;
|
|
1404
1243
|
if (mutationType === 'UPDATE') {
|
|
1405
|
-
const updateMatch =
|
|
1406
|
-
const whereMatch = sql.match(/\bWHERE\s+(.+?)(?:RETURNING|$)/is);
|
|
1244
|
+
const updateMatch = updateTablePattern.exec(sql);
|
|
1407
1245
|
targetTable = updateMatch?.[1]?.replace(/["`]/g, '');
|
|
1408
|
-
whereClause = whereMatch?.[1]?.trim();
|
|
1409
1246
|
}
|
|
1410
1247
|
else if (mutationType === 'DELETE') {
|
|
1411
|
-
const deleteMatch =
|
|
1412
|
-
const whereMatch = sql.match(/\bWHERE\s+(.+?)(?:RETURNING|$)/is);
|
|
1248
|
+
const deleteMatch = deleteTablePattern.exec(sql);
|
|
1413
1249
|
targetTable = deleteMatch?.[1]?.replace(/["`]/g, '');
|
|
1414
|
-
whereClause = whereMatch?.[1]?.trim();
|
|
1415
1250
|
}
|
|
1416
1251
|
else if (mutationType === 'INSERT') {
|
|
1417
|
-
const insertMatch =
|
|
1252
|
+
const insertMatch = insertTablePattern.exec(sql);
|
|
1418
1253
|
targetTable = insertMatch?.[1]?.replace(/["`]/g, '');
|
|
1419
1254
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1255
|
+
// Get client - either from override or main connection
|
|
1256
|
+
let clientResult = null;
|
|
1257
|
+
let client;
|
|
1258
|
+
if (override) {
|
|
1259
|
+
clientResult = await dbManager.getClientWithOverride(override);
|
|
1260
|
+
client = clientResult.client;
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
client = await dbManager.getClient();
|
|
1264
|
+
}
|
|
1265
|
+
const startTime = getStartTime();
|
|
1423
1266
|
let beforeRows;
|
|
1424
1267
|
let affectedRows = [];
|
|
1425
1268
|
let rowsAffected = 0;
|
|
@@ -1494,15 +1337,20 @@ export async function mutationDryRun(args) {
|
|
|
1494
1337
|
error = extractDryRunError(e);
|
|
1495
1338
|
}
|
|
1496
1339
|
finally {
|
|
1497
|
-
client
|
|
1340
|
+
// Release client properly based on connection type
|
|
1341
|
+
if (clientResult) {
|
|
1342
|
+
clientResult.release();
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
client.release();
|
|
1346
|
+
}
|
|
1498
1347
|
}
|
|
1499
|
-
const
|
|
1500
|
-
const executionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
1348
|
+
const executionTimeMs = calculateExecutionTime(startTime, getStartTime());
|
|
1501
1349
|
const result = {
|
|
1502
1350
|
mutationType,
|
|
1503
1351
|
success,
|
|
1504
1352
|
rowsAffected,
|
|
1505
|
-
executionTimeMs
|
|
1353
|
+
executionTimeMs
|
|
1506
1354
|
};
|
|
1507
1355
|
if (beforeRows && beforeRows.length > 0) {
|
|
1508
1356
|
result.beforeRows = beforeRows;
|
|
@@ -1541,8 +1389,8 @@ export async function batchExecute(args) {
|
|
|
1541
1389
|
if (args.queries.length === 0) {
|
|
1542
1390
|
throw new Error('queries array cannot be empty');
|
|
1543
1391
|
}
|
|
1544
|
-
if (args.queries.length >
|
|
1545
|
-
throw new Error(
|
|
1392
|
+
if (args.queries.length > MAX_BATCH_QUERIES) {
|
|
1393
|
+
throw new Error(`Maximum ${MAX_BATCH_QUERIES} queries allowed in a batch`);
|
|
1546
1394
|
}
|
|
1547
1395
|
// Validate each query
|
|
1548
1396
|
const seenNames = new Set();
|
|
@@ -1560,36 +1408,39 @@ export async function batchExecute(args) {
|
|
|
1560
1408
|
}
|
|
1561
1409
|
const dbManager = getDbManager();
|
|
1562
1410
|
const stopOnError = args.stopOnError === true; // Default false
|
|
1563
|
-
const startTime =
|
|
1411
|
+
const startTime = getStartTime();
|
|
1412
|
+
// Build connection override if specified
|
|
1413
|
+
const hasOverride = args.server || args.database || args.schema;
|
|
1414
|
+
const override = hasOverride
|
|
1415
|
+
? { server: args.server, database: args.database, schema: args.schema }
|
|
1416
|
+
: undefined;
|
|
1564
1417
|
const results = {};
|
|
1565
1418
|
let successCount = 0;
|
|
1566
1419
|
let failureCount = 0;
|
|
1567
1420
|
// Execute all queries in parallel
|
|
1568
1421
|
const promises = args.queries.map(async (query) => {
|
|
1569
|
-
const queryStartTime =
|
|
1422
|
+
const queryStartTime = getStartTime();
|
|
1570
1423
|
try {
|
|
1571
|
-
const result = await dbManager.
|
|
1572
|
-
const
|
|
1573
|
-
const executionTimeMs = Number(queryEndTime - queryStartTime) / 1_000_000;
|
|
1424
|
+
const result = await dbManager.queryWithOverride(query.sql, query.params, override);
|
|
1425
|
+
const executionTimeMs = calculateExecutionTime(queryStartTime, getStartTime());
|
|
1574
1426
|
return {
|
|
1575
1427
|
name: query.name,
|
|
1576
1428
|
result: {
|
|
1577
1429
|
success: true,
|
|
1578
1430
|
rows: result.rows,
|
|
1579
1431
|
rowCount: result.rowCount ?? result.rows.length,
|
|
1580
|
-
executionTimeMs
|
|
1432
|
+
executionTimeMs
|
|
1581
1433
|
}
|
|
1582
1434
|
};
|
|
1583
1435
|
}
|
|
1584
1436
|
catch (error) {
|
|
1585
|
-
const
|
|
1586
|
-
const executionTimeMs = Number(queryEndTime - queryStartTime) / 1_000_000;
|
|
1437
|
+
const executionTimeMs = calculateExecutionTime(queryStartTime, getStartTime());
|
|
1587
1438
|
return {
|
|
1588
1439
|
name: query.name,
|
|
1589
1440
|
result: {
|
|
1590
1441
|
success: false,
|
|
1591
1442
|
error: error instanceof Error ? error.message : String(error),
|
|
1592
|
-
executionTimeMs
|
|
1443
|
+
executionTimeMs
|
|
1593
1444
|
}
|
|
1594
1445
|
};
|
|
1595
1446
|
}
|
|
@@ -1610,13 +1461,12 @@ export async function batchExecute(args) {
|
|
|
1610
1461
|
}
|
|
1611
1462
|
}
|
|
1612
1463
|
}
|
|
1613
|
-
const
|
|
1614
|
-
const totalExecutionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
1464
|
+
const totalExecutionTimeMs = calculateExecutionTime(startTime, getStartTime());
|
|
1615
1465
|
return {
|
|
1616
1466
|
totalQueries: args.queries.length,
|
|
1617
1467
|
successCount,
|
|
1618
1468
|
failureCount,
|
|
1619
|
-
totalExecutionTimeMs
|
|
1469
|
+
totalExecutionTimeMs,
|
|
1620
1470
|
results
|
|
1621
1471
|
};
|
|
1622
1472
|
}
|
|
@@ -1626,11 +1476,12 @@ export async function batchExecute(args) {
|
|
|
1626
1476
|
export async function beginTransaction(args) {
|
|
1627
1477
|
const dbManager = getDbManager();
|
|
1628
1478
|
const info = await dbManager.beginTransaction(args?.name);
|
|
1479
|
+
const namePart = info.name ? ` "${info.name}"` : '';
|
|
1629
1480
|
return {
|
|
1630
1481
|
transactionId: info.transactionId,
|
|
1631
1482
|
name: info.name,
|
|
1632
1483
|
status: 'started',
|
|
1633
|
-
message: `Transaction${
|
|
1484
|
+
message: `Transaction${namePart} started. Use transactionId "${info.transactionId}" with execute_sql or commit/rollback.`
|
|
1634
1485
|
};
|
|
1635
1486
|
}
|
|
1636
1487
|
/**
|