@tejasanik/postgres-mcp-server 1.7.0 → 2.0.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 +200 -7
- package/dist/__tests__/db-manager.test.js +44 -0
- package/dist/__tests__/db-manager.test.js.map +1 -1
- package/dist/__tests__/sql-tools.test.js +746 -0
- package/dist/__tests__/sql-tools.test.js.map +1 -1
- package/dist/db-manager.d.ts +31 -1
- package/dist/db-manager.d.ts.map +1 -1
- package/dist/db-manager.js +110 -2
- package/dist/db-manager.js.map +1 -1
- package/dist/index.js +129 -11
- package/dist/index.js.map +1 -1
- package/dist/tools/server-tools.d.ts +2 -0
- package/dist/tools/server-tools.d.ts.map +1 -1
- package/dist/tools/server-tools.js +2 -1
- package/dist/tools/server-tools.js.map +1 -1
- package/dist/tools/sql-tools.d.ts +62 -2
- package/dist/tools/sql-tools.d.ts.map +1 -1
- package/dist/tools/sql-tools.js +537 -38
- package/dist/tools/sql-tools.js.map +1 -1
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +60 -60
package/dist/tools/sql-tools.js
CHANGED
|
@@ -26,8 +26,8 @@ export async function executeSql(args) {
|
|
|
26
26
|
if (!args.allowLargeScript && sql.length > DEFAULT_SQL_LENGTH_LIMIT) {
|
|
27
27
|
throw new Error(`SQL query exceeds ${DEFAULT_SQL_LENGTH_LIMIT} characters. Use allowLargeScript=true for deployment scripts.`);
|
|
28
28
|
}
|
|
29
|
-
// Validate params if provided
|
|
30
|
-
if (args.params !== undefined) {
|
|
29
|
+
// Validate params if provided (only for single statement)
|
|
30
|
+
if (args.params !== undefined && !args.allowMultipleStatements) {
|
|
31
31
|
if (!Array.isArray(args.params)) {
|
|
32
32
|
throw new Error('params must be an array');
|
|
33
33
|
}
|
|
@@ -35,22 +35,46 @@ export async function executeSql(args) {
|
|
|
35
35
|
throw new Error(`Maximum ${MAX_PARAMS} parameters allowed`);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
// Params not supported with multiple statements
|
|
39
|
+
if (args.allowMultipleStatements && args.params && args.params.length > 0) {
|
|
40
|
+
throw new Error('params not supported with allowMultipleStatements. Use separate execute_sql calls for parameterized queries.');
|
|
41
|
+
}
|
|
38
42
|
const dbManager = getDbManager();
|
|
39
43
|
const maxRows = validatePositiveInteger(args.maxRows, 'maxRows', 1, MAX_ROWS_LIMIT) || MAX_ROWS_DEFAULT;
|
|
40
44
|
const offset = args.offset !== undefined ? validatePositiveInteger(args.offset, 'offset', 0, Number.MAX_SAFE_INTEGER) : 0;
|
|
45
|
+
// Get schema hints if requested
|
|
46
|
+
let schemaHint;
|
|
47
|
+
if (args.includeSchemaHint) {
|
|
48
|
+
schemaHint = await getSchemaHintForSql(sql);
|
|
49
|
+
}
|
|
50
|
+
// Handle multi-statement execution
|
|
51
|
+
if (args.allowMultipleStatements) {
|
|
52
|
+
return executeMultipleStatements(sql, schemaHint, args.transactionId);
|
|
53
|
+
}
|
|
41
54
|
// Record start time for execution timing
|
|
42
55
|
const startTime = process.hrtime.bigint();
|
|
43
|
-
// Execute query with optional parameters
|
|
44
|
-
|
|
56
|
+
// Execute query with optional parameters (supports transaction)
|
|
57
|
+
let result;
|
|
58
|
+
if (args.transactionId) {
|
|
59
|
+
result = await dbManager.queryInTransaction(args.transactionId, sql, args.params);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
result = await dbManager.query(sql, args.params);
|
|
63
|
+
}
|
|
45
64
|
// Calculate execution time in milliseconds
|
|
46
65
|
const endTime = process.hrtime.bigint();
|
|
47
66
|
const executionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
67
|
+
// Defensive: ensure result has expected structure
|
|
68
|
+
if (!result || typeof result !== 'object') {
|
|
69
|
+
throw new Error('Query returned invalid result');
|
|
70
|
+
}
|
|
48
71
|
const fields = result.fields?.map(f => f.name) || [];
|
|
49
|
-
const
|
|
72
|
+
const rows = result.rows || [];
|
|
73
|
+
const totalRows = rows.length;
|
|
50
74
|
// Apply offset and limit to the results
|
|
51
75
|
const startIndex = Math.min(offset, totalRows);
|
|
52
76
|
const endIndex = Math.min(startIndex + maxRows, totalRows);
|
|
53
|
-
const paginatedRows =
|
|
77
|
+
const paginatedRows = rows.slice(startIndex, endIndex);
|
|
54
78
|
const returnedRows = paginatedRows.length;
|
|
55
79
|
// Calculate actual output size
|
|
56
80
|
const outputJson = JSON.stringify(paginatedRows);
|
|
@@ -78,7 +102,8 @@ export async function executeSql(args) {
|
|
|
78
102
|
truncated: true,
|
|
79
103
|
executionTimeMs: Math.round(executionTimeMs * 100) / 100,
|
|
80
104
|
offset: startIndex,
|
|
81
|
-
hasMore: endIndex < totalRows
|
|
105
|
+
hasMore: endIndex < totalRows,
|
|
106
|
+
...(schemaHint && { schemaHint })
|
|
82
107
|
};
|
|
83
108
|
}
|
|
84
109
|
return {
|
|
@@ -87,7 +112,66 @@ export async function executeSql(args) {
|
|
|
87
112
|
fields,
|
|
88
113
|
executionTimeMs: Math.round(executionTimeMs * 100) / 100,
|
|
89
114
|
offset: startIndex,
|
|
90
|
-
hasMore: endIndex < totalRows
|
|
115
|
+
hasMore: endIndex < totalRows,
|
|
116
|
+
...(schemaHint && { schemaHint })
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Execute multiple SQL statements and return results for each
|
|
121
|
+
*/
|
|
122
|
+
async function executeMultipleStatements(sql, schemaHint, transactionId) {
|
|
123
|
+
const dbManager = getDbManager();
|
|
124
|
+
const startTime = process.hrtime.bigint();
|
|
125
|
+
// Parse statements with line numbers
|
|
126
|
+
const parsedStatements = splitSqlStatementsWithLineNumbers(sql);
|
|
127
|
+
// Filter out empty statements and comments-only
|
|
128
|
+
const executableStatements = parsedStatements.filter(stmt => {
|
|
129
|
+
const trimmed = stmt.sql.trim();
|
|
130
|
+
if (!trimmed)
|
|
131
|
+
return false;
|
|
132
|
+
const withoutComments = stripLeadingComments(trimmed);
|
|
133
|
+
return withoutComments.length > 0;
|
|
134
|
+
});
|
|
135
|
+
const results = [];
|
|
136
|
+
let successCount = 0;
|
|
137
|
+
let failureCount = 0;
|
|
138
|
+
for (let i = 0; i < executableStatements.length; i++) {
|
|
139
|
+
const stmt = executableStatements[i];
|
|
140
|
+
const stmtResult = {
|
|
141
|
+
statementIndex: i + 1,
|
|
142
|
+
sql: stmt.sql.length > 200 ? stmt.sql.substring(0, 200) + '...' : stmt.sql,
|
|
143
|
+
lineNumber: stmt.lineNumber,
|
|
144
|
+
success: false,
|
|
145
|
+
};
|
|
146
|
+
try {
|
|
147
|
+
let result;
|
|
148
|
+
if (transactionId) {
|
|
149
|
+
result = await dbManager.queryInTransaction(transactionId, stmt.sql);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
result = await dbManager.query(stmt.sql);
|
|
153
|
+
}
|
|
154
|
+
stmtResult.success = true;
|
|
155
|
+
stmtResult.rows = result.rows?.slice(0, 100); // Limit rows per statement
|
|
156
|
+
stmtResult.rowCount = result.rowCount ?? result.rows?.length ?? 0;
|
|
157
|
+
successCount++;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
stmtResult.success = false;
|
|
161
|
+
stmtResult.error = error instanceof Error ? error.message : String(error);
|
|
162
|
+
failureCount++;
|
|
163
|
+
}
|
|
164
|
+
results.push(stmtResult);
|
|
165
|
+
}
|
|
166
|
+
const endTime = process.hrtime.bigint();
|
|
167
|
+
const executionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
168
|
+
return {
|
|
169
|
+
results,
|
|
170
|
+
totalStatements: executableStatements.length,
|
|
171
|
+
successCount,
|
|
172
|
+
failureCount,
|
|
173
|
+
executionTimeMs: Math.round(executionTimeMs * 100) / 100,
|
|
174
|
+
...(schemaHint && { schemaHint })
|
|
91
175
|
};
|
|
92
176
|
}
|
|
93
177
|
export async function explainQuery(args) {
|
|
@@ -243,30 +327,23 @@ export async function executeSqlFile(args) {
|
|
|
243
327
|
let totalRowsAffected = 0;
|
|
244
328
|
let rolledBack = false;
|
|
245
329
|
const collectedErrors = [];
|
|
246
|
-
// Split SQL into statements
|
|
247
|
-
const
|
|
248
|
-
const executableStatements =
|
|
249
|
-
const trimmed =
|
|
250
|
-
|
|
330
|
+
// Split SQL into statements with line number tracking
|
|
331
|
+
const parsedStatements = splitSqlStatementsWithLineNumbers(sqlContent);
|
|
332
|
+
const executableStatements = parsedStatements.filter(stmt => {
|
|
333
|
+
const trimmed = stmt.sql.trim();
|
|
334
|
+
if (!trimmed)
|
|
335
|
+
return false;
|
|
336
|
+
const withoutComments = stripLeadingComments(trimmed);
|
|
337
|
+
return withoutComments.length > 0;
|
|
251
338
|
});
|
|
252
339
|
const totalStatements = executableStatements.length;
|
|
253
340
|
try {
|
|
254
341
|
if (useTransaction) {
|
|
255
342
|
await client.query('BEGIN');
|
|
256
343
|
}
|
|
257
|
-
let statementIndex = 0;
|
|
258
|
-
|
|
259
|
-
const trimmed =
|
|
260
|
-
// Skip empty statements
|
|
261
|
-
if (!trimmed) {
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
// Skip pure comment-only statements (just -- comments with no SQL after)
|
|
265
|
-
const withoutComments = stripLeadingComments(trimmed);
|
|
266
|
-
if (!withoutComments) {
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
statementIndex++;
|
|
344
|
+
for (let statementIndex = 0; statementIndex < executableStatements.length; statementIndex++) {
|
|
345
|
+
const stmt = executableStatements[statementIndex];
|
|
346
|
+
const trimmed = stmt.sql.trim();
|
|
270
347
|
try {
|
|
271
348
|
const result = await client.query(trimmed);
|
|
272
349
|
statementsExecuted++;
|
|
@@ -284,7 +361,8 @@ export async function executeSqlFile(args) {
|
|
|
284
361
|
}
|
|
285
362
|
// Add the error to collection before throwing
|
|
286
363
|
collectedErrors.push({
|
|
287
|
-
statementIndex,
|
|
364
|
+
statementIndex: statementIndex + 1,
|
|
365
|
+
lineNumber: stmt.lineNumber,
|
|
288
366
|
sql: trimmed.length > 200 ? trimmed.substring(0, 200) + '...' : trimmed,
|
|
289
367
|
error: errorMessage
|
|
290
368
|
});
|
|
@@ -292,11 +370,12 @@ export async function executeSqlFile(args) {
|
|
|
292
370
|
}
|
|
293
371
|
// If stopOnError is false, collect error and continue
|
|
294
372
|
collectedErrors.push({
|
|
295
|
-
statementIndex,
|
|
373
|
+
statementIndex: statementIndex + 1,
|
|
374
|
+
lineNumber: stmt.lineNumber,
|
|
296
375
|
sql: trimmed.length > 200 ? trimmed.substring(0, 200) + '...' : trimmed,
|
|
297
376
|
error: errorMessage
|
|
298
377
|
});
|
|
299
|
-
console.error(`Warning: Statement ${statementIndex} failed: ${errorMessage}`);
|
|
378
|
+
console.error(`Warning: Statement ${statementIndex + 1} at line ${stmt.lineNumber} failed: ${errorMessage}`);
|
|
300
379
|
}
|
|
301
380
|
}
|
|
302
381
|
if (useTransaction && !rolledBack) {
|
|
@@ -375,12 +454,14 @@ function stripLeadingComments(sql) {
|
|
|
375
454
|
return result;
|
|
376
455
|
}
|
|
377
456
|
/**
|
|
378
|
-
* Split SQL content into individual statements.
|
|
379
|
-
*
|
|
457
|
+
* Split SQL content into individual statements with line number tracking.
|
|
458
|
+
* Returns ParsedStatement objects with SQL and line number info.
|
|
380
459
|
*/
|
|
381
|
-
function
|
|
460
|
+
function splitSqlStatementsWithLineNumbers(sql) {
|
|
382
461
|
const statements = [];
|
|
383
462
|
let current = '';
|
|
463
|
+
let currentLineNumber = 1;
|
|
464
|
+
let statementStartLine = 1;
|
|
384
465
|
let inString = false;
|
|
385
466
|
let stringChar = '';
|
|
386
467
|
let inLineComment = false;
|
|
@@ -389,6 +470,14 @@ function splitSqlStatements(sql) {
|
|
|
389
470
|
while (i < sql.length) {
|
|
390
471
|
const char = sql[i];
|
|
391
472
|
const nextChar = sql[i + 1] || '';
|
|
473
|
+
// Track line numbers
|
|
474
|
+
if (char === '\n') {
|
|
475
|
+
currentLineNumber++;
|
|
476
|
+
}
|
|
477
|
+
// If starting a new statement (current is empty/whitespace), record line number
|
|
478
|
+
if (current.trim() === '' && char.trim() !== '') {
|
|
479
|
+
statementStartLine = currentLineNumber;
|
|
480
|
+
}
|
|
392
481
|
// Handle line comments
|
|
393
482
|
if (!inString && !inBlockComment && char === '-' && nextChar === '-') {
|
|
394
483
|
inLineComment = true;
|
|
@@ -422,7 +511,6 @@ function splitSqlStatements(sql) {
|
|
|
422
511
|
stringChar = char;
|
|
423
512
|
}
|
|
424
513
|
else if (char === stringChar) {
|
|
425
|
-
// Check for escaped quote (doubled)
|
|
426
514
|
if (nextChar === stringChar) {
|
|
427
515
|
current += char + nextChar;
|
|
428
516
|
i += 2;
|
|
@@ -439,7 +527,11 @@ function splitSqlStatements(sql) {
|
|
|
439
527
|
const dollarTag = dollarMatch[1];
|
|
440
528
|
const endIndex = sql.indexOf(dollarTag, i + dollarTag.length);
|
|
441
529
|
if (endIndex !== -1) {
|
|
442
|
-
|
|
530
|
+
const dollarContent = sql.slice(i, endIndex + dollarTag.length);
|
|
531
|
+
// Count newlines in dollar-quoted content
|
|
532
|
+
const newlines = (dollarContent.match(/\n/g) || []).length;
|
|
533
|
+
currentLineNumber += newlines;
|
|
534
|
+
current += dollarContent;
|
|
443
535
|
i = endIndex + dollarTag.length;
|
|
444
536
|
continue;
|
|
445
537
|
}
|
|
@@ -448,8 +540,12 @@ function splitSqlStatements(sql) {
|
|
|
448
540
|
// Handle statement separator
|
|
449
541
|
if (!inString && !inLineComment && !inBlockComment && char === ';') {
|
|
450
542
|
current += char;
|
|
451
|
-
|
|
543
|
+
const trimmed = current.trim();
|
|
544
|
+
if (trimmed) {
|
|
545
|
+
statements.push({ sql: trimmed, lineNumber: statementStartLine });
|
|
546
|
+
}
|
|
452
547
|
current = '';
|
|
548
|
+
statementStartLine = currentLineNumber;
|
|
453
549
|
i++;
|
|
454
550
|
continue;
|
|
455
551
|
}
|
|
@@ -457,9 +553,412 @@ function splitSqlStatements(sql) {
|
|
|
457
553
|
i++;
|
|
458
554
|
}
|
|
459
555
|
// Add remaining content if any
|
|
460
|
-
|
|
461
|
-
|
|
556
|
+
const trimmed = current.trim();
|
|
557
|
+
if (trimmed) {
|
|
558
|
+
statements.push({ sql: trimmed, lineNumber: statementStartLine });
|
|
559
|
+
}
|
|
560
|
+
return statements;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Extracts table names from a SQL query.
|
|
564
|
+
* Handles common patterns: FROM, JOIN, INTO, UPDATE, DELETE FROM
|
|
565
|
+
*/
|
|
566
|
+
function extractTablesFromSql(sql) {
|
|
567
|
+
const tables = [];
|
|
568
|
+
const seen = new Set();
|
|
569
|
+
// Normalize SQL: remove comments and extra whitespace
|
|
570
|
+
const normalized = sql
|
|
571
|
+
.replace(/--[^\n]*/g, '') // Remove line comments
|
|
572
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
573
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
574
|
+
.trim();
|
|
575
|
+
// Patterns to find table references
|
|
576
|
+
const patterns = [
|
|
577
|
+
/\bFROM\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
578
|
+
/\bJOIN\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
579
|
+
/\bINTO\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
580
|
+
/\bUPDATE\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
581
|
+
/\bDELETE\s+FROM\s+(["`]?[\w]+["`]?(?:\s*\.\s*["`]?[\w]+["`]?)?)/gi,
|
|
582
|
+
];
|
|
583
|
+
for (const pattern of patterns) {
|
|
584
|
+
let match;
|
|
585
|
+
while ((match = pattern.exec(normalized)) !== null) {
|
|
586
|
+
const tableRef = match[1].replace(/["`]/g, '').trim();
|
|
587
|
+
// Skip common SQL keywords that might be matched
|
|
588
|
+
if (['SELECT', 'WHERE', 'SET', 'VALUES', 'AND', 'OR'].includes(tableRef.toUpperCase())) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
let schema = 'public';
|
|
592
|
+
let table = tableRef;
|
|
593
|
+
if (tableRef.includes('.')) {
|
|
594
|
+
const parts = tableRef.split('.');
|
|
595
|
+
schema = parts[0].trim();
|
|
596
|
+
table = parts[1].trim();
|
|
597
|
+
}
|
|
598
|
+
const key = `${schema}.${table}`.toLowerCase();
|
|
599
|
+
if (!seen.has(key)) {
|
|
600
|
+
seen.add(key);
|
|
601
|
+
tables.push({ schema, table });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return tables;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Gets schema hints for tables mentioned in SQL
|
|
609
|
+
*/
|
|
610
|
+
async function getSchemaHintForSql(sql) {
|
|
611
|
+
const dbManager = getDbManager();
|
|
612
|
+
const tables = extractTablesFromSql(sql);
|
|
613
|
+
const tableHints = [];
|
|
614
|
+
for (const { schema, table } of tables.slice(0, 10)) { // Limit to 10 tables
|
|
615
|
+
try {
|
|
616
|
+
// Get columns
|
|
617
|
+
const columnsResult = await dbManager.query(`
|
|
618
|
+
SELECT
|
|
619
|
+
column_name as name,
|
|
620
|
+
data_type as type,
|
|
621
|
+
is_nullable = 'YES' as nullable
|
|
622
|
+
FROM information_schema.columns
|
|
623
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
624
|
+
ORDER BY ordinal_position
|
|
625
|
+
`, [schema, table]);
|
|
626
|
+
// Get primary key
|
|
627
|
+
const pkResult = await dbManager.query(`
|
|
628
|
+
SELECT a.attname as column_name
|
|
629
|
+
FROM pg_index i
|
|
630
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
631
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
632
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
633
|
+
WHERE i.indisprimary
|
|
634
|
+
AND n.nspname = $1
|
|
635
|
+
AND c.relname = $2
|
|
636
|
+
`, [schema, table]);
|
|
637
|
+
// Get foreign keys
|
|
638
|
+
const fkResult = await dbManager.query(`
|
|
639
|
+
SELECT
|
|
640
|
+
kcu.column_name,
|
|
641
|
+
ccu.table_schema || '.' || ccu.table_name as referenced_table,
|
|
642
|
+
ccu.column_name as referenced_column
|
|
643
|
+
FROM information_schema.table_constraints tc
|
|
644
|
+
JOIN information_schema.key_column_usage kcu
|
|
645
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
646
|
+
AND tc.table_schema = kcu.table_schema
|
|
647
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
648
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
649
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
650
|
+
AND tc.table_schema = $1
|
|
651
|
+
AND tc.table_name = $2
|
|
652
|
+
`, [schema, table]);
|
|
653
|
+
// Get row count estimate
|
|
654
|
+
const countResult = await dbManager.query(`
|
|
655
|
+
SELECT reltuples::bigint as estimate
|
|
656
|
+
FROM pg_class c
|
|
657
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
658
|
+
WHERE n.nspname = $1 AND c.relname = $2
|
|
659
|
+
`, [schema, table]);
|
|
660
|
+
const hint = {
|
|
661
|
+
schema,
|
|
662
|
+
table,
|
|
663
|
+
columns: columnsResult.rows.map(r => ({
|
|
664
|
+
name: r.name,
|
|
665
|
+
type: r.type,
|
|
666
|
+
nullable: r.nullable
|
|
667
|
+
})),
|
|
668
|
+
primaryKey: pkResult.rows.map(r => r.column_name),
|
|
669
|
+
rowCountEstimate: countResult.rows[0]?.estimate || 0
|
|
670
|
+
};
|
|
671
|
+
// Group foreign keys by constraint
|
|
672
|
+
if (fkResult.rows.length > 0) {
|
|
673
|
+
const fkMap = new Map();
|
|
674
|
+
for (const row of fkResult.rows) {
|
|
675
|
+
const key = row.referenced_table;
|
|
676
|
+
if (!fkMap.has(key)) {
|
|
677
|
+
fkMap.set(key, { columns: [], referencedColumns: [] });
|
|
678
|
+
}
|
|
679
|
+
fkMap.get(key).columns.push(row.column_name);
|
|
680
|
+
fkMap.get(key).referencedColumns.push(row.referenced_column);
|
|
681
|
+
}
|
|
682
|
+
hint.foreignKeys = Array.from(fkMap.entries()).map(([refTable, data]) => ({
|
|
683
|
+
columns: data.columns,
|
|
684
|
+
referencedTable: refTable,
|
|
685
|
+
referencedColumns: data.referencedColumns
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
tableHints.push(hint);
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
// Skip tables that don't exist or have permission issues
|
|
692
|
+
console.error(`Could not get schema hint for ${schema}.${table}: ${error}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { tables: tableHints };
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Preview the effect of a mutation (INSERT/UPDATE/DELETE) without executing it.
|
|
699
|
+
* Returns estimated rows affected and sample of rows that would be affected.
|
|
700
|
+
*/
|
|
701
|
+
export async function mutationPreview(args) {
|
|
702
|
+
if (!args.sql || typeof args.sql !== 'string') {
|
|
703
|
+
throw new Error('sql parameter is required');
|
|
704
|
+
}
|
|
705
|
+
const sql = args.sql.trim();
|
|
706
|
+
const sampleSize = Math.min(args.sampleSize || 5, 20); // Default 5, max 20
|
|
707
|
+
// Detect mutation type
|
|
708
|
+
const upperSql = sql.toUpperCase();
|
|
709
|
+
let mutationType = 'UNKNOWN';
|
|
710
|
+
if (upperSql.startsWith('UPDATE')) {
|
|
711
|
+
mutationType = 'UPDATE';
|
|
712
|
+
}
|
|
713
|
+
else if (upperSql.startsWith('DELETE')) {
|
|
714
|
+
mutationType = 'DELETE';
|
|
715
|
+
}
|
|
716
|
+
else if (upperSql.startsWith('INSERT')) {
|
|
717
|
+
mutationType = 'INSERT';
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
throw new Error('SQL must be an INSERT, UPDATE, or DELETE statement');
|
|
721
|
+
}
|
|
722
|
+
const dbManager = getDbManager();
|
|
723
|
+
// For INSERT, we can't preview affected rows
|
|
724
|
+
if (mutationType === 'INSERT') {
|
|
725
|
+
// Use EXPLAIN to estimate rows
|
|
726
|
+
const explainResult = await dbManager.query(`EXPLAIN (FORMAT JSON) ${sql}`);
|
|
727
|
+
const plan = explainResult.rows[0]['QUERY PLAN'][0];
|
|
728
|
+
return {
|
|
729
|
+
mutationType,
|
|
730
|
+
estimatedRowsAffected: plan?.Plan?.['Plan Rows'] || 1,
|
|
731
|
+
sampleAffectedRows: [],
|
|
732
|
+
warning: 'INSERT preview cannot show affected rows - they do not exist yet'
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// For UPDATE and DELETE, extract WHERE clause and table
|
|
736
|
+
let targetTable;
|
|
737
|
+
let whereClause;
|
|
738
|
+
if (mutationType === 'UPDATE') {
|
|
739
|
+
// Pattern: UPDATE table SET ... WHERE ...
|
|
740
|
+
const updateMatch = sql.match(/UPDATE\s+(["`]?[\w.]+["`]?)\s+SET/i);
|
|
741
|
+
const whereMatch = sql.match(/\bWHERE\s+(.+)$/is);
|
|
742
|
+
targetTable = updateMatch?.[1]?.replace(/["`]/g, '');
|
|
743
|
+
whereClause = whereMatch?.[1];
|
|
744
|
+
}
|
|
745
|
+
else if (mutationType === 'DELETE') {
|
|
746
|
+
// Pattern: DELETE FROM table WHERE ...
|
|
747
|
+
const deleteMatch = sql.match(/DELETE\s+FROM\s+(["`]?[\w.]+["`]?)/i);
|
|
748
|
+
const whereMatch = sql.match(/\bWHERE\s+(.+)$/is);
|
|
749
|
+
targetTable = deleteMatch?.[1]?.replace(/["`]/g, '');
|
|
750
|
+
whereClause = whereMatch?.[1];
|
|
751
|
+
}
|
|
752
|
+
if (!targetTable) {
|
|
753
|
+
throw new Error('Could not parse target table from SQL');
|
|
754
|
+
}
|
|
755
|
+
// Get estimated row count using EXPLAIN
|
|
756
|
+
let estimatedRowsAffected = 0;
|
|
757
|
+
try {
|
|
758
|
+
const explainResult = await dbManager.query(`EXPLAIN (FORMAT JSON) ${sql}`);
|
|
759
|
+
const plan = explainResult.rows[0]['QUERY PLAN'][0];
|
|
760
|
+
estimatedRowsAffected = plan?.Plan?.['Plan Rows'] || 0;
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
// EXPLAIN might fail, continue with count query
|
|
462
764
|
}
|
|
463
|
-
|
|
765
|
+
// Build SELECT query to get sample of affected rows
|
|
766
|
+
let sampleRows = [];
|
|
767
|
+
try {
|
|
768
|
+
const selectSql = whereClause
|
|
769
|
+
? `SELECT * FROM ${targetTable} WHERE ${whereClause} LIMIT ${sampleSize}`
|
|
770
|
+
: `SELECT * FROM ${targetTable} LIMIT ${sampleSize}`;
|
|
771
|
+
const sampleResult = await dbManager.query(selectSql);
|
|
772
|
+
sampleRows = sampleResult.rows;
|
|
773
|
+
// If EXPLAIN didn't work, get count
|
|
774
|
+
if (estimatedRowsAffected === 0) {
|
|
775
|
+
const countSql = whereClause
|
|
776
|
+
? `SELECT COUNT(*) as cnt FROM ${targetTable} WHERE ${whereClause}`
|
|
777
|
+
: `SELECT COUNT(*) as cnt FROM ${targetTable}`;
|
|
778
|
+
const countResult = await dbManager.query(countSql);
|
|
779
|
+
estimatedRowsAffected = parseInt(countResult.rows[0]?.cnt || '0', 10);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
throw new Error(`Could not preview affected rows: ${error instanceof Error ? error.message : String(error)}`);
|
|
784
|
+
}
|
|
785
|
+
const result = {
|
|
786
|
+
mutationType,
|
|
787
|
+
estimatedRowsAffected,
|
|
788
|
+
sampleAffectedRows: sampleRows,
|
|
789
|
+
targetTable
|
|
790
|
+
};
|
|
791
|
+
if (whereClause) {
|
|
792
|
+
result.whereClause = whereClause;
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
result.warning = 'No WHERE clause - ALL rows in the table will be affected!';
|
|
796
|
+
}
|
|
797
|
+
return result;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Execute multiple SQL queries in parallel.
|
|
801
|
+
* Returns results keyed by query name.
|
|
802
|
+
*/
|
|
803
|
+
export async function batchExecute(args) {
|
|
804
|
+
if (!args.queries || !Array.isArray(args.queries)) {
|
|
805
|
+
throw new Error('queries parameter is required and must be an array');
|
|
806
|
+
}
|
|
807
|
+
if (args.queries.length === 0) {
|
|
808
|
+
throw new Error('queries array cannot be empty');
|
|
809
|
+
}
|
|
810
|
+
if (args.queries.length > 20) {
|
|
811
|
+
throw new Error('Maximum 20 queries allowed in a batch');
|
|
812
|
+
}
|
|
813
|
+
// Validate each query
|
|
814
|
+
const seenNames = new Set();
|
|
815
|
+
for (const query of args.queries) {
|
|
816
|
+
if (!query.name || typeof query.name !== 'string') {
|
|
817
|
+
throw new Error('Each query must have a name');
|
|
818
|
+
}
|
|
819
|
+
if (!query.sql || typeof query.sql !== 'string') {
|
|
820
|
+
throw new Error(`Query "${query.name}" must have sql`);
|
|
821
|
+
}
|
|
822
|
+
if (seenNames.has(query.name)) {
|
|
823
|
+
throw new Error(`Duplicate query name: ${query.name}`);
|
|
824
|
+
}
|
|
825
|
+
seenNames.add(query.name);
|
|
826
|
+
}
|
|
827
|
+
const dbManager = getDbManager();
|
|
828
|
+
const stopOnError = args.stopOnError === true; // Default false
|
|
829
|
+
const startTime = process.hrtime.bigint();
|
|
830
|
+
const results = {};
|
|
831
|
+
let successCount = 0;
|
|
832
|
+
let failureCount = 0;
|
|
833
|
+
// Execute all queries in parallel
|
|
834
|
+
const promises = args.queries.map(async (query) => {
|
|
835
|
+
const queryStartTime = process.hrtime.bigint();
|
|
836
|
+
try {
|
|
837
|
+
const result = await dbManager.query(query.sql, query.params);
|
|
838
|
+
const queryEndTime = process.hrtime.bigint();
|
|
839
|
+
const executionTimeMs = Number(queryEndTime - queryStartTime) / 1_000_000;
|
|
840
|
+
return {
|
|
841
|
+
name: query.name,
|
|
842
|
+
result: {
|
|
843
|
+
success: true,
|
|
844
|
+
rows: result.rows,
|
|
845
|
+
rowCount: result.rowCount ?? result.rows.length,
|
|
846
|
+
executionTimeMs: Math.round(executionTimeMs * 100) / 100
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
const queryEndTime = process.hrtime.bigint();
|
|
852
|
+
const executionTimeMs = Number(queryEndTime - queryStartTime) / 1_000_000;
|
|
853
|
+
return {
|
|
854
|
+
name: query.name,
|
|
855
|
+
result: {
|
|
856
|
+
success: false,
|
|
857
|
+
error: error instanceof Error ? error.message : String(error),
|
|
858
|
+
executionTimeMs: Math.round(executionTimeMs * 100) / 100
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
// Wait for all queries
|
|
864
|
+
const queryResults = await Promise.all(promises);
|
|
865
|
+
// Collect results
|
|
866
|
+
for (const { name, result } of queryResults) {
|
|
867
|
+
results[name] = result;
|
|
868
|
+
if (result.success) {
|
|
869
|
+
successCount++;
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
failureCount++;
|
|
873
|
+
if (stopOnError) {
|
|
874
|
+
// Mark remaining as not executed
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const endTime = process.hrtime.bigint();
|
|
880
|
+
const totalExecutionTimeMs = Number(endTime - startTime) / 1_000_000;
|
|
881
|
+
return {
|
|
882
|
+
totalQueries: args.queries.length,
|
|
883
|
+
successCount,
|
|
884
|
+
failureCount,
|
|
885
|
+
totalExecutionTimeMs: Math.round(totalExecutionTimeMs * 100) / 100,
|
|
886
|
+
results
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Begin a new transaction. Returns a transactionId to use with subsequent queries.
|
|
891
|
+
*/
|
|
892
|
+
export async function beginTransaction(args) {
|
|
893
|
+
const dbManager = getDbManager();
|
|
894
|
+
const info = await dbManager.beginTransaction(args?.name);
|
|
895
|
+
return {
|
|
896
|
+
transactionId: info.transactionId,
|
|
897
|
+
name: info.name,
|
|
898
|
+
status: 'started',
|
|
899
|
+
message: `Transaction${info.name ? ` "${info.name}"` : ''} started. Use transactionId "${info.transactionId}" with execute_sql or commit/rollback.`
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Get information about an active transaction.
|
|
904
|
+
*/
|
|
905
|
+
export async function getTransactionInfo(args) {
|
|
906
|
+
if (!args.transactionId) {
|
|
907
|
+
throw new Error('transactionId parameter is required');
|
|
908
|
+
}
|
|
909
|
+
const dbManager = getDbManager();
|
|
910
|
+
const info = dbManager.getTransactionInfo(args.transactionId);
|
|
911
|
+
if (!info) {
|
|
912
|
+
return { error: `Transaction not found: ${args.transactionId}` };
|
|
913
|
+
}
|
|
914
|
+
return info;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* List all active transactions.
|
|
918
|
+
*/
|
|
919
|
+
export async function listActiveTransactions() {
|
|
920
|
+
const dbManager = getDbManager();
|
|
921
|
+
const transactions = dbManager.listActiveTransactions();
|
|
922
|
+
return {
|
|
923
|
+
transactions,
|
|
924
|
+
count: transactions.length
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Commit an active transaction.
|
|
929
|
+
*/
|
|
930
|
+
export async function commitTransaction(args) {
|
|
931
|
+
if (!args.transactionId || typeof args.transactionId !== 'string') {
|
|
932
|
+
throw new Error('transactionId is required');
|
|
933
|
+
}
|
|
934
|
+
const dbManager = getDbManager();
|
|
935
|
+
await dbManager.commitTransaction(args.transactionId);
|
|
936
|
+
return {
|
|
937
|
+
transactionId: args.transactionId,
|
|
938
|
+
status: 'committed',
|
|
939
|
+
message: 'Transaction committed successfully.'
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Rollback an active transaction.
|
|
944
|
+
*/
|
|
945
|
+
export async function rollbackTransaction(args) {
|
|
946
|
+
if (!args.transactionId || typeof args.transactionId !== 'string') {
|
|
947
|
+
throw new Error('transactionId is required');
|
|
948
|
+
}
|
|
949
|
+
const dbManager = getDbManager();
|
|
950
|
+
await dbManager.rollbackTransaction(args.transactionId);
|
|
951
|
+
return {
|
|
952
|
+
transactionId: args.transactionId,
|
|
953
|
+
status: 'rolled_back',
|
|
954
|
+
message: 'Transaction rolled back successfully.'
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Get connection context for including in tool responses
|
|
959
|
+
*/
|
|
960
|
+
export function getConnectionContext() {
|
|
961
|
+
const dbManager = getDbManager();
|
|
962
|
+
return dbManager.getConnectionContext();
|
|
464
963
|
}
|
|
465
964
|
//# sourceMappingURL=sql-tools.js.map
|