| 596 | |
| 597 | |
| 598 | async executeSQL(sql: string, options: ExecuteOptions, parameters?: any[]): Promise<SQLResult> { |
| 599 | if (!this.pool) { |
| 600 | throw new Error("Not connected to database"); |
| 601 | } |
| 602 | |
| 603 | const client = await this.pool.connect(); |
| 604 | try { |
| 605 | // Check if this is a multi-statement query |
| 606 | const statements = splitSQLStatements(sql, "postgres"); |
| 607 | |
| 608 | if (statements.length === 1) { |
| 609 | // Single statement - apply maxRows if applicable |
| 610 | const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows); |
| 611 | |
| 612 | // Engine-level read-only enforcement: when the tool is read-only, run the |
| 613 | // statement inside a READ ONLY transaction so the database itself rejects any |
| 614 | // write, even one the keyword classifier failed to catch (e.g. SELECT setval()). |
| 615 | if (options.readonly) { |
| 616 | await client.query('BEGIN READ ONLY'); |
| 617 | try { |
| 618 | const result = parameters && parameters.length > 0 |
| 619 | ? await client.query(processedStatement, parameters) |
| 620 | : await client.query(processedStatement); |
| 621 | await client.query('COMMIT'); |
| 622 | return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length }; |
| 623 | } catch (error) { |
| 624 | // Best-effort rollback so a failed ROLLBACK (e.g. dropped connection) |
| 625 | // can't mask the original query error. |
| 626 | try { |
| 627 | await client.query('ROLLBACK'); |
| 628 | } catch { |
| 629 | // ignore; the original error is more useful |
| 630 | } |
| 631 | console.error(`[PostgreSQL executeSQL] ERROR: ${(error as Error).message}`); |
| 632 | console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`); |
| 633 | if (parameters && parameters.length > 0) { |
| 634 | console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`); |
| 635 | } |
| 636 | throw error; |
| 637 | } |
| 638 | } |
| 639 | |
| 640 | // Use parameters if provided |
| 641 | let result; |
| 642 | if (parameters && parameters.length > 0) { |
| 643 | try { |
| 644 | result = await client.query(processedStatement, parameters); |
| 645 | } catch (error) { |
| 646 | console.error(`[PostgreSQL executeSQL] ERROR: ${(error as Error).message}`); |
| 647 | console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`); |
| 648 | console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`); |
| 649 | throw error; |
| 650 | } |
| 651 | } else { |
| 652 | result = await client.query(processedStatement); |
| 653 | } |
| 654 | // Explicitly return rows and rowCount to ensure rowCount is preserved |
| 655 | return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length }; |