@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.
Files changed (96) hide show
  1. package/README.md +186 -10
  2. package/dist/db-manager/index.d.ts +7 -0
  3. package/dist/db-manager/index.d.ts.map +1 -0
  4. package/dist/db-manager/index.js +7 -0
  5. package/dist/db-manager/index.js.map +1 -0
  6. package/dist/db-manager/validation.d.ts +35 -0
  7. package/dist/db-manager/validation.d.ts.map +1 -0
  8. package/dist/db-manager/validation.js +54 -0
  9. package/dist/db-manager/validation.js.map +1 -0
  10. package/dist/db-manager.d.ts +175 -5
  11. package/dist/db-manager.d.ts.map +1 -1
  12. package/dist/db-manager.js +589 -26
  13. package/dist/db-manager.js.map +1 -1
  14. package/dist/index.js +141 -11
  15. package/dist/index.js.map +1 -1
  16. package/dist/tools/analysis-tools.d.ts.map +1 -1
  17. package/dist/tools/analysis-tools.js +53 -49
  18. package/dist/tools/analysis-tools.js.map +1 -1
  19. package/dist/tools/schema-tools.d.ts +40 -1
  20. package/dist/tools/schema-tools.d.ts.map +1 -1
  21. package/dist/tools/schema-tools.js +174 -92
  22. package/dist/tools/schema-tools.js.map +1 -1
  23. package/dist/tools/server-tools.d.ts +1 -0
  24. package/dist/tools/server-tools.d.ts.map +1 -1
  25. package/dist/tools/server-tools.js +10 -6
  26. package/dist/tools/server-tools.js.map +1 -1
  27. package/dist/tools/sql/utils/connection-utils.d.ts +79 -0
  28. package/dist/tools/sql/utils/connection-utils.d.ts.map +1 -0
  29. package/dist/tools/sql/utils/connection-utils.js +129 -0
  30. package/dist/tools/sql/utils/connection-utils.js.map +1 -0
  31. package/dist/tools/sql/utils/constants.d.ts +55 -0
  32. package/dist/tools/sql/utils/constants.d.ts.map +1 -0
  33. package/dist/tools/sql/utils/constants.js +55 -0
  34. package/dist/tools/sql/utils/constants.js.map +1 -0
  35. package/dist/tools/sql/utils/dry-run-utils.d.ts +31 -0
  36. package/dist/tools/sql/utils/dry-run-utils.d.ts.map +1 -0
  37. package/dist/tools/sql/utils/dry-run-utils.js +173 -0
  38. package/dist/tools/sql/utils/dry-run-utils.js.map +1 -0
  39. package/dist/tools/sql/utils/file-handler.d.ts +57 -0
  40. package/dist/tools/sql/utils/file-handler.d.ts.map +1 -0
  41. package/dist/tools/sql/utils/file-handler.js +150 -0
  42. package/dist/tools/sql/utils/file-handler.js.map +1 -0
  43. package/dist/tools/sql/utils/index.d.ts +12 -0
  44. package/dist/tools/sql/utils/index.d.ts.map +1 -0
  45. package/dist/tools/sql/utils/index.js +12 -0
  46. package/dist/tools/sql/utils/index.js.map +1 -0
  47. package/dist/tools/sql/utils/result-formatter.d.ts +94 -0
  48. package/dist/tools/sql/utils/result-formatter.d.ts.map +1 -0
  49. package/dist/tools/sql/utils/result-formatter.js +154 -0
  50. package/dist/tools/sql/utils/result-formatter.js.map +1 -0
  51. package/dist/tools/sql/utils/sql-parser.d.ts +125 -0
  52. package/dist/tools/sql/utils/sql-parser.d.ts.map +1 -0
  53. package/dist/tools/sql/utils/sql-parser.js +468 -0
  54. package/dist/tools/sql/utils/sql-parser.js.map +1 -0
  55. package/dist/tools/sql-tools.d.ts +21 -0
  56. package/dist/tools/sql-tools.d.ts.map +1 -1
  57. package/dist/tools/sql-tools.js +383 -532
  58. package/dist/tools/sql-tools.js.map +1 -1
  59. package/dist/types.d.ts +38 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/utils/retry.d.ts +1 -1
  62. package/dist/utils/retry.d.ts.map +1 -1
  63. package/dist/utils/retry.js.map +1 -1
  64. package/dist/utils/validation.d.ts +45 -9
  65. package/dist/utils/validation.d.ts.map +1 -1
  66. package/dist/utils/validation.js +335 -72
  67. package/dist/utils/validation.js.map +1 -1
  68. package/package.json +9 -2
  69. package/dist/__tests__/analysis-tools.test.d.ts +0 -2
  70. package/dist/__tests__/analysis-tools.test.d.ts.map +0 -1
  71. package/dist/__tests__/analysis-tools.test.js +0 -294
  72. package/dist/__tests__/analysis-tools.test.js.map +0 -1
  73. package/dist/__tests__/db-manager.test.d.ts +0 -2
  74. package/dist/__tests__/db-manager.test.d.ts.map +0 -1
  75. package/dist/__tests__/db-manager.test.js +0 -410
  76. package/dist/__tests__/db-manager.test.js.map +0 -1
  77. package/dist/__tests__/mcp-server.test.d.ts +0 -13
  78. package/dist/__tests__/mcp-server.test.d.ts.map +0 -1
  79. package/dist/__tests__/mcp-server.test.js +0 -146
  80. package/dist/__tests__/mcp-server.test.js.map +0 -1
  81. package/dist/__tests__/schema-tools.test.d.ts +0 -2
  82. package/dist/__tests__/schema-tools.test.d.ts.map +0 -1
  83. package/dist/__tests__/schema-tools.test.js +0 -171
  84. package/dist/__tests__/schema-tools.test.js.map +0 -1
  85. package/dist/__tests__/server-tools.test.d.ts +0 -2
  86. package/dist/__tests__/server-tools.test.d.ts.map +0 -1
  87. package/dist/__tests__/server-tools.test.js +0 -113
  88. package/dist/__tests__/server-tools.test.js.map +0 -1
  89. package/dist/__tests__/sql-tools.test.d.ts +0 -2
  90. package/dist/__tests__/sql-tools.test.d.ts.map +0 -1
  91. package/dist/__tests__/sql-tools.test.js +0 -1912
  92. package/dist/__tests__/sql-tools.test.js.map +0 -1
  93. package/dist/__tests__/validation.test.d.ts +0 -2
  94. package/dist/__tests__/validation.test.d.ts.map +0 -1
  95. package/dist/__tests__/validation.test.js +0 -203
  96. package/dist/__tests__/validation.test.js.map +0 -1
@@ -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
- const MAX_OUTPUT_CHARS = 50000; // Maximum characters before writing to file
8
- const MAX_ROWS_DEFAULT = 1000; // Default max rows in direct response
9
- const MAX_ROWS_LIMIT = 100000; // Absolute maximum rows
10
- const DEFAULT_SQL_LENGTH_LIMIT = 100000; // Default SQL query length limit (100KB)
11
- const MAX_PARAMS = 100; // Maximum number of query parameters
12
- const MAX_SQL_FILE_SIZE = 50 * 1024 * 1024; // Maximum SQL file size (50MB)
13
- const MAX_DRY_RUN_SAMPLE_ROWS = 10; // Maximum sample rows to return in dry-run
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 === undefined || args.sql === null) {
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
- // Get schema hints if requested
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 = process.hrtime.bigint();
206
- // Execute query with optional parameters (supports transaction)
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 endTime = process.hrtime.bigint();
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: Math.round(executionTimeMs * 100) / 100,
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: Math.round(executionTimeMs * 100) / 100,
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: Math.round(executionTimeMs * 100) / 100,
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 = process.hrtime.bigint();
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
- for (let i = 0; i < executableStatements.length; i++) {
289
- const stmt = executableStatements[i];
290
- const stmtResult = {
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 result;
298
- if (transactionId) {
299
- result = await dbManager.queryInTransaction(transactionId, stmt.sql);
300
- }
301
- else {
302
- result = await dbManager.query(stmt.sql);
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
- catch (error) {
310
- stmtResult.success = false;
311
- stmtResult.error = error instanceof Error ? error.message : String(error);
312
- failureCount++;
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 endTime = process.hrtime.bigint();
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: Math.round(executionTimeMs * 100) / 100,
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 client = await dbManager.getClient();
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 > 10) {
349
- throw new Error('Maximum 10 hypothetical indexes allowed');
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
- // Ignore if hypopg not available
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.release();
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
- catch (error) {
461
- // Invalid regex - skip this pattern
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
- const startTime = process.hrtime.bigint();
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 endTime = process.hrtime.bigint();
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 > 300 ? stmt.sql.substring(0, 300) + '...' : stmt.sql,
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: Math.round(executionTimeMs * 100) / 100,
434
+ executionTimeMs,
539
435
  rowsAffected: 0,
540
436
  validateOnly: true,
541
437
  preview
542
438
  };
543
439
  }
544
- const client = await dbManager.getClient();
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 > 200 ? trimmed.substring(0, 200) + '...' : trimmed,
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 > 200 ? trimmed.substring(0, 200) + '...' : trimmed,
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 endTime = process.hrtime.bigint();
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: Math.round(executionTimeMs * 100) / 100,
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 endTime = process.hrtime.bigint();
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: Math.round(executionTimeMs * 100) / 100,
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.release();
634
- }
635
- }
636
- /**
637
- * Detect the type of SQL statement (SELECT, INSERT, UPDATE, DELETE, CREATE, etc.)
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 || 20, 100);
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 > 300 ? stmt.sql.substring(0, 300) + '...' : stmt.sql,
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
- const fileSizeFormatted = stats.size < 1024
747
- ? `${stats.size} bytes`
748
- : stats.size < 1024 * 1024
749
- ? `${(stats.size / 1024).toFixed(1)} KB`
750
- : `${(stats.size / (1024 * 1024)).toFixed(2)} MB`;
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 || 50, 200);
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
- const fileSizeFormatted = stats.size < 1024
831
- ? `${stats.size} bytes`
832
- : stats.size < 1024 * 1024
833
- ? `${(stats.size / 1024).toFixed(1)} KB`
834
- : `${(stats.size / (1024 * 1024)).toFixed(2)} MB`;
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
- const client = await dbManager.getClient();
837
- const startTime = process.hrtime.bigint();
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 = process.hrtime.bigint();
753
+ const stmtStartTime = getStartTime();
853
754
  const result = {
854
755
  index: idx + 1,
855
756
  lineNumber: stmt.lineNumber,
856
- sql: stmt.sql.length > 300 ? stmt.sql.substring(0, 300) + '...' : stmt.sql,
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
- const stmtEndTime = process.hrtime.bigint();
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.release();
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 endTime = process.hrtime.bigint();
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
- * Strips leading line comments and block comments from SQL to check if there's actual SQL.
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 splitSqlStatementsWithLineNumbers(sql) {
1003
- const statements = [];
1004
- let current = '';
1005
- let currentLineNumber = 1;
1006
- let statementStartLine = 1;
1007
- let inString = false;
1008
- let stringChar = '';
1009
- let inLineComment = false;
1010
- let inBlockComment = false;
1011
- let i = 0;
1012
- while (i < sql.length) {
1013
- const char = sql[i];
1014
- const nextChar = sql[i + 1] || '';
1015
- // Track line numbers
1016
- if (char === '\n') {
1017
- currentLineNumber++;
1018
- }
1019
- // If starting a new statement (current is empty/whitespace), record line number
1020
- if (current.trim() === '' && char.trim() !== '') {
1021
- statementStartLine = currentLineNumber;
1022
- }
1023
- // Handle line comments
1024
- if (!inString && !inBlockComment && char === '-' && nextChar === '-') {
1025
- inLineComment = true;
1026
- current += char;
1027
- i++;
1028
- continue;
1029
- }
1030
- if (inLineComment && (char === '\n' || char === '\r')) {
1031
- inLineComment = false;
1032
- current += char;
1033
- i++;
1034
- continue;
1035
- }
1036
- // Handle block comments
1037
- if (!inString && !inLineComment && char === '/' && nextChar === '*') {
1038
- inBlockComment = true;
1039
- current += char + nextChar;
1040
- i += 2;
1041
- continue;
1042
- }
1043
- if (inBlockComment && char === '*' && nextChar === '/') {
1044
- inBlockComment = false;
1045
- current += char + nextChar;
1046
- i += 2;
1047
- continue;
1048
- }
1049
- // Handle string literals
1050
- if (!inLineComment && !inBlockComment && (char === "'" || char === '"')) {
1051
- if (!inString) {
1052
- inString = true;
1053
- stringChar = char;
1054
- }
1055
- else if (char === stringChar) {
1056
- if (nextChar === stringChar) {
1057
- current += char + nextChar;
1058
- i += 2;
1059
- continue;
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
- inString = false;
1062
- stringChar = '';
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
- // Handle statement separator
1083
- if (!inString && !inLineComment && !inBlockComment && char === ';') {
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
- return tables;
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 || 5, 20); // Default 5, max 20
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 = 'UNKNOWN';
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.query(`EXPLAIN (FORMAT JSON) ${sql}`);
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 = sql.match(/UPDATE\s+(["`]?[\w.]+["`]?)\s+SET/i);
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 = sql.match(/DELETE\s+FROM\s+(["`]?[\w.]+["`]?)/i);
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.query(`EXPLAIN (FORMAT JSON) ${sql}`);
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.query(selectSql);
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.query(countSql);
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, 20);
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 = 'UNKNOWN';
1360
- if (upperSql.startsWith('UPDATE') || upperSql.match(/^WITH\b.*\bUPDATE\b/s)) {
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') || upperSql.match(/^WITH\b.*\bDELETE\b/s)) {
1185
+ else if (upperSql.startsWith('DELETE') || withDeletePattern.test(upperSql)) {
1364
1186
  mutationType = 'DELETE';
1365
1187
  }
1366
- else if (upperSql.startsWith('INSERT') || upperSql.match(/^WITH\b.*\bINSERT\b/s)) {
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.query(`EXPLAIN (FORMAT JSON) ${sql}`);
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 = sql.match(/UPDATE\s+(["`]?[\w.]+["`]?)\s+SET/i);
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 = sql.match(/DELETE\s+FROM\s+(["`]?[\w.]+["`]?)/i);
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 = sql.match(/INSERT\s+INTO\s+(["`]?[\w.]+["`]?)/i);
1252
+ const insertMatch = insertTablePattern.exec(sql);
1418
1253
  targetTable = insertMatch?.[1]?.replace(/["`]/g, '');
1419
1254
  }
1420
- const dbManager = getDbManager();
1421
- const client = await dbManager.getClient();
1422
- const startTime = process.hrtime.bigint();
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.release();
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 endTime = process.hrtime.bigint();
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: Math.round(executionTimeMs * 100) / 100
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 > 20) {
1545
- throw new Error('Maximum 20 queries allowed in a batch');
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 = process.hrtime.bigint();
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 = process.hrtime.bigint();
1422
+ const queryStartTime = getStartTime();
1570
1423
  try {
1571
- const result = await dbManager.query(query.sql, query.params);
1572
- const queryEndTime = process.hrtime.bigint();
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: Math.round(executionTimeMs * 100) / 100
1432
+ executionTimeMs
1581
1433
  }
1582
1434
  };
1583
1435
  }
1584
1436
  catch (error) {
1585
- const queryEndTime = process.hrtime.bigint();
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: Math.round(executionTimeMs * 100) / 100
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 endTime = process.hrtime.bigint();
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: Math.round(totalExecutionTimeMs * 100) / 100,
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${info.name ? ` "${info.name}"` : ''} started. Use transactionId "${info.transactionId}" with execute_sql or commit/rollback.`
1484
+ message: `Transaction${namePart} started. Use transactionId "${info.transactionId}" with execute_sql or commit/rollback.`
1634
1485
  };
1635
1486
  }
1636
1487
  /**