@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.
@@ -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
- const result = await dbManager.query(sql, args.params);
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 totalRows = result.rows.length;
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 = result.rows.slice(startIndex, endIndex);
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 first to get total count
247
- const statements = splitSqlStatements(sqlContent);
248
- const executableStatements = statements.filter(s => {
249
- const trimmed = s.trim();
250
- return trimmed && !trimmed.startsWith('--');
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
- for (const statement of statements) {
259
- const trimmed = statement.trim();
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
- * Handles basic cases like semicolons, comments, and string literals.
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 splitSqlStatements(sql) {
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
- current += sql.slice(i, endIndex + dollarTag.length);
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
- statements.push(current.trim());
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
- if (current.trim()) {
461
- statements.push(current.trim());
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
- return statements.filter(s => s.length > 0);
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