(sql: string, options: ExecuteOptions, parameters?: any[])
| 449 | |
| 450 | |
| 451 | async executeSQL(sql: string, options: ExecuteOptions, parameters?: any[]): Promise<SQLResult> { |
| 452 | if (!this.db) { |
| 453 | throw new Error("Not connected to SQLite database"); |
| 454 | } |
| 455 | |
| 456 | // Engine-level read-only backstop: PRAGMA query_only=ON makes SQLite reject any |
| 457 | // write (including header-writing pragmas like `PRAGMA user_version=N` and |
| 458 | // `wal_checkpoint(...)`) regardless of what the keyword classifier allowed. The |
| 459 | // body below runs synchronously (node:sqlite is sync), so no other executeSQL |
| 460 | // call can interleave between toggling the flag on and restoring it. |
| 461 | if (options.readonly) { |
| 462 | this.db.exec("PRAGMA query_only = ON"); |
| 463 | } |
| 464 | |
| 465 | try { |
| 466 | // Check if this is a multi-statement query |
| 467 | const statements = splitSQLStatements(sql, "sqlite"); |
| 468 | |
| 469 | if (statements.length === 1) { |
| 470 | // Single statement - determine if it returns data |
| 471 | let processedStatement = statements[0]; |
| 472 | const trimmedStatement = statements[0].toLowerCase().trim(); |
| 473 | const isReadStatement = trimmedStatement.startsWith('select') || |
| 474 | trimmedStatement.startsWith('with') || |
| 475 | trimmedStatement.startsWith('explain') || |
| 476 | trimmedStatement.startsWith('analyze') || |
| 477 | (trimmedStatement.startsWith('pragma') && |
| 478 | (trimmedStatement.includes('table_info') || |
| 479 | trimmedStatement.includes('index_info') || |
| 480 | trimmedStatement.includes('index_list') || |
| 481 | trimmedStatement.includes('foreign_key_list'))); |
| 482 | |
| 483 | // Apply maxRows limit to SELECT queries if specified (not PRAGMA/ANALYZE) |
| 484 | if (options.maxRows) { |
| 485 | processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows); |
| 486 | } |
| 487 | |
| 488 | if (isReadStatement) { |
| 489 | // Pass parameters if provided |
| 490 | if (parameters && parameters.length > 0) { |
| 491 | try { |
| 492 | const rows = this.prepare(processedStatement).all(...parameters); |
| 493 | return { rows, rowCount: rows.length }; |
| 494 | } catch (error) { |
| 495 | console.error(`[SQLite executeSQL] ERROR: ${(error as Error).message}`); |
| 496 | console.error(`[SQLite executeSQL] SQL: ${processedStatement}`); |
| 497 | console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`); |
| 498 | throw error; |
| 499 | } |
| 500 | } else { |
| 501 | const rows = this.prepare(processedStatement).all(); |
| 502 | return { rows, rowCount: rows.length }; |
| 503 | } |
| 504 | } else { |
| 505 | // Use run() for statements that don't return data |
| 506 | let result; |
| 507 | if (parameters && parameters.length > 0) { |
| 508 | try { |
nothing calls this directly
no test coverage detected