(sql: string)
| 33 | |
| 34 | // Returns alias → ParsedTableRef. Handles quoted identifiers, schema.table, and comma-separated FROM. |
| 35 | export const parseTablesFromQuery = (sql: string): Map<string, ParsedTableRef> | null => { |
| 36 | if (!sql || sql.length === 0) return null; |
| 37 | |
| 38 | const fromSection = extractFromSection(sql); |
| 39 | if (!fromSection) return null; |
| 40 | |
| 41 | const tableMap = new Map<string, ParsedTableRef>(); |
| 42 | const fromPattern = |
| 43 | /(?:from|join|,)\s+("(?:[^"]|"")*"|`[^`]+`|[a-zA-Z_][a-zA-Z0-9_]*)(?:\.("(?:[^"]|"")*"|`[^`]+`|[a-zA-Z_][a-zA-Z0-9_]*))?(?:\s+(?:as\s+)?("(?:[^"]|"")*"|`[^`]+`|(?!(?:join|left|right|inner|outer|cross|natural|full|on|using|where|group|order|having|limit|offset|union|intersect|except|for|fetch|window|lateral|tablesample|qualify|straight_join)\b)[a-zA-Z_][a-zA-Z0-9_]*))?/gi; |
| 44 | |
| 45 | let match; |
| 46 | let matchCount = 0; |
| 47 | const MAX_MATCHES = 10; |
| 48 | |
| 49 | while ((match = fromPattern.exec(fromSection)) !== null && matchCount++ < MAX_MATCHES) { |
| 50 | const schemaToken = match[2] ? match[1] : undefined; |
| 51 | const tableToken = match[2] ?? match[1]; |
| 52 | if (!tableToken) continue; |
| 53 | |
| 54 | const tableName = stripIdentifierQuotes(tableToken); |
| 55 | const schema = schemaToken ? stripIdentifierQuotes(schemaToken) : undefined; |
| 56 | const aliasToken = match[3]; |
| 57 | const alias = aliasToken ? stripIdentifierQuotes(aliasToken) : tableName; |
| 58 | tableMap.set(alias, { name: tableName, schema }); |
| 59 | } |
| 60 | |
| 61 | return tableMap.size > 0 ? tableMap : null; |
| 62 | }; |
| 63 | |
| 64 | // Optimized statement extractor - avoid full text scan when possible |
| 65 | export const getCurrentStatement = (model: { getValue: () => string; getOffsetAt: (position: { lineNumber: number; column: number }) => number }, position: { lineNumber: number; column: number }): string => { |
no test coverage detected