(sqlQuery: string, options: ExecuteOptions, parameters?: any[])
| 607 | } |
| 608 | |
| 609 | async executeSQL(sqlQuery: string, options: ExecuteOptions, parameters?: any[]): Promise<SQLResult> { |
| 610 | if (!this.connection) { |
| 611 | throw new Error("Not connected to SQL Server database"); |
| 612 | } |
| 613 | |
| 614 | // SQL Server has no native EXPLAIN statement. Translate a leading `EXPLAIN` |
| 615 | // into a SHOWPLAN_XML request so callers get a Postgres/MySQL-like |
| 616 | // experience. SHOWPLAN_XML compiles the statement without executing it, so |
| 617 | // this is read-only safe (further enforced in explainQuery). |
| 618 | const afterNoise = sqlQuery.slice( |
| 619 | sqlQuery.match(SQLServerConnector.LEADING_NOISE)![0].length |
| 620 | ); |
| 621 | if (/^explain\b/i.test(afterNoise)) { |
| 622 | return this.explainQuery(afterNoise.slice("explain".length).trim()); |
| 623 | } |
| 624 | |
| 625 | try { |
| 626 | // Apply maxRows limit to SELECT queries if specified |
| 627 | let processedSQL = sqlQuery; |
| 628 | if (options.maxRows) { |
| 629 | processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows); |
| 630 | } |
| 631 | |
| 632 | // Engine-level read-only enforcement: SQL Server has no |
| 633 | // BEGIN TRANSACTION READ ONLY, so we wrap in a transaction and |
| 634 | // unconditionally ROLLBACK to prevent any modifications from persisting. |
| 635 | // This is defense-in-depth behind the keyword classifier. |
| 636 | if (options.readonly) { |
| 637 | return await this.executeReadOnly(processedSQL, parameters); |
| 638 | } |
| 639 | |
| 640 | // Create request and collect informational messages (e.g. SET STATISTICS TIME/IO, PRINT) |
| 641 | const request = this.connection.request(); |
| 642 | const messages: DatabaseMessage[] = []; |
| 643 | request.on( |
| 644 | 'info', |
| 645 | (info: { message: string; number?: number; class?: number; lineNumber?: number }) => { |
| 646 | messages.push({ |
| 647 | text: info.message, |
| 648 | // SQL Server reports severity as a numeric class; info messages are < 10. |
| 649 | severity: info.class !== undefined ? String(info.class) : undefined, |
| 650 | code: info.number, |
| 651 | line: info.lineNumber, |
| 652 | }); |
| 653 | } |
| 654 | ); |
| 655 | |
| 656 | if (parameters && parameters.length > 0) { |
| 657 | // SQL Server uses @p1, @p2, etc. for parameters |
| 658 | parameters.forEach((param, index) => { |
| 659 | const paramName = `p${index + 1}`; |
| 660 | // Infer SQL Server type from JavaScript type |
| 661 | if (typeof param === 'string') { |
| 662 | request.input(paramName, sql.VarChar, param); |
| 663 | } else if (typeof param === 'number') { |
| 664 | if (Number.isInteger(param)) { |
| 665 | request.input(paramName, sql.Int, param); |
| 666 | } else { |
nothing calls this directly
no test coverage detected