(db: DatabaseAdapter, sql: string, options?: SqlExecutionOptions)
| 391 | * falls back to keyword denylist for adapters that don't expose it. |
| 392 | */ |
| 393 | export function executeSql(db: DatabaseAdapter, sql: string, options?: SqlExecutionOptions): SqlExecutionResult { |
| 394 | const maxRows = options?.maxRows ?? 1000 |
| 395 | const columnar = options?.columnar ?? false |
| 396 | const timing = options?.timing ?? false |
| 397 | |
| 398 | const trimmed = sql.trim() |
| 399 | |
| 400 | const startTime = timing ? Date.now() : 0 |
| 401 | |
| 402 | const stmt = db.prepare(trimmed) |
| 403 | |
| 404 | if (stmt.readonly === false) { |
| 405 | throw new Error('Only read-only statements are allowed (SELECT / WITH)') |
| 406 | } |
| 407 | |
| 408 | if (stmt.readonly === undefined) { |
| 409 | const forbidden = /^\s*(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|ATTACH|DETACH|REINDEX|VACUUM|PRAGMA)/i |
| 410 | if (forbidden.test(trimmed)) { |
| 411 | throw new Error('Only SELECT queries are allowed') |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | const needsLimit = maxRows > 0 && !/\bLIMIT\b/i.test(trimmed) |
| 416 | let allRows: Record<string, unknown>[] |
| 417 | |
| 418 | if (needsLimit) { |
| 419 | const safeSql = `${trimmed} LIMIT ${maxRows + 1}` |
| 420 | allRows = db.prepare(safeSql).all() as Record<string, unknown>[] |
| 421 | } else { |
| 422 | allRows = stmt.all() as Record<string, unknown>[] |
| 423 | } |
| 424 | |
| 425 | const truncated = maxRows > 0 && allRows.length > maxRows |
| 426 | const resultRows = truncated ? allRows.slice(0, maxRows) : allRows |
| 427 | const columns = resultRows.length > 0 ? Object.keys(resultRows[0]) : [] |
| 428 | |
| 429 | const duration = timing ? Date.now() - startTime : undefined |
| 430 | |
| 431 | if (columnar) { |
| 432 | const rows2d = resultRows.map((row) => columns.map((col) => row[col])) |
| 433 | return { columns, rows: rows2d, rowCount: rows2d.length, truncated, duration } |
| 434 | } |
| 435 | |
| 436 | return { columns, rows: resultRows, rowCount: resultRows.length, truncated, duration } |
| 437 | } |
| 438 | |
| 439 | /** Table schema with column details */ |
| 440 | export interface TableSchema { |
no test coverage detected