| 604 | } |
| 605 | |
| 606 | async executeSQL(sql: string, options: ExecuteOptions, parameters?: any[]): Promise<SQLResult> { |
| 607 | if (!this.pool) { |
| 608 | throw new Error("Not connected to database"); |
| 609 | } |
| 610 | |
| 611 | // Get a dedicated connection from the pool to ensure session consistency |
| 612 | // This is critical for session-specific features like LAST_INSERT_ID() |
| 613 | const conn = await this.pool.getConnection(); |
| 614 | try { |
| 615 | // Engine-level read-only backstop: run the batch inside a READ ONLY |
| 616 | // transaction so MySQL rejects DML writes (INSERT/UPDATE/DELETE/REPLACE) |
| 617 | // that the keyword classifier missed (e.g. function-based writes). Note this |
| 618 | // does NOT stop DDL: statements like DROP/CREATE perform an implicit COMMIT |
| 619 | // that ends the read-only transaction first, so DDL escapes. Stacked-DDL |
| 620 | // payloads (e.g. `SELECT 1--1;DROP TABLE t`) are instead rejected upstream by |
| 621 | // the read-only classifier, which now splits `--`-hidden statements (see |
| 622 | // scanSingleLineCommentMySQL in sql-parser.ts). |
| 623 | if (options.readonly) { |
| 624 | await conn.query("START TRANSACTION READ ONLY"); |
| 625 | } |
| 626 | |
| 627 | // Apply maxRows limit to SELECT queries if specified |
| 628 | let processedSQL = sql; |
| 629 | if (options.maxRows) { |
| 630 | // Handle multi-statement SQL by processing each statement individually |
| 631 | const statements = splitSQLStatements(sql, "mysql"); |
| 632 | |
| 633 | const processedStatements = statements.map(statement => |
| 634 | SQLRowLimiter.applyMaxRows(statement, options.maxRows) |
| 635 | ); |
| 636 | |
| 637 | processedSQL = processedStatements.join('; '); |
| 638 | if (sql.trim().endsWith(';')) { |
| 639 | processedSQL += ';'; |
| 640 | } |
| 641 | } |
| 642 | |
| 643 | // Use dedicated connection with multipleStatements: true support |
| 644 | // Pass parameters if provided, with optional query timeout |
| 645 | let results: any; |
| 646 | if (parameters && parameters.length > 0) { |
| 647 | try { |
| 648 | results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs }, parameters); |
| 649 | } catch (error) { |
| 650 | console.error(`[MySQL executeSQL] ERROR: ${(error as Error).message}`); |
| 651 | console.error(`[MySQL executeSQL] SQL: ${processedSQL}`); |
| 652 | console.error(`[MySQL executeSQL] Parameters: ${JSON.stringify(parameters)}`); |
| 653 | throw error; |
| 654 | } |
| 655 | } else { |
| 656 | results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs }); |
| 657 | } |
| 658 | |
| 659 | // MySQL2 returns results in format [rows, fields] |
| 660 | // Extract the first element which contains the actual row data |
| 661 | const [firstResult] = results; |
| 662 | |
| 663 | // Parse results using shared utility that handles both single and multi-statement queries |