export interface SqlStatementRange { start: number; end: number; text: string; } export type SqlExecutionSelectionSource = 'selection' | 'statement' | 'line'; export interface SqlExecutionSelection { sql: string; source: SqlExecutionSelectionSource; } const isWhitespace = (ch: string): boolean => ( ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' ); const isSqlIdentifierStart = (ch: string): boolean => /^[A-Za-z_]$/.test(ch); const isSqlIdentifierPart = (ch: string): boolean => /^[A-Za-z0-9_$#]$/.test(ch); const skipSqlWhitespaceAndComments = (text: string, position: number): number => { let index = position; while (index < text.length) { const ch = text[index]; const next = index + 1 < text.length ? text[index + 1] : ''; if (isWhitespace(ch)) { index += 1; continue; } if (ch === '-' && next === '-') { index += 2; while (index < text.length && text[index] !== '\n') index += 1; continue; } if (ch === '/' && next === '*') { index += 2; while (index + 1 < text.length && !(text[index] === '*' && text[index + 1] === '/')) { index += 1; } if (index + 1 < text.length) index += 2; continue; } break; } return index; }; const nextSqlSignificantToken = (text: string, position: number): string => { const index = skipSqlWhitespaceAndComments(text, position); if (index >= text.length || !isSqlIdentifierStart(text[index])) return ''; let end = index + 1; while (end < text.length && isSqlIdentifierPart(text[end])) end += 1; return text.slice(index, end).toLowerCase(); }; const nextSqlSignificantChar = (text: string, position: number): string => { const index = skipSqlWhitespaceAndComments(text, position); return index >= text.length ? '' : text[index]; }; const shouldEnterPlsqlBeginBlock = (text: string, tokenEnd: number): boolean => { const nextChar = nextSqlSignificantChar(text, tokenEnd); if (!nextChar || nextChar === ';') return false; return !['transaction', 'work', 'isolation', 'read', 'write'].includes(nextSqlSignificantToken(text, tokenEnd)); }; const shouldEnterPlsqlDeclareBlock = (text: string, tokenEnd: number): boolean => Boolean(nextSqlSignificantToken(text, tokenEnd)); const isPlsqlControlEnd = (text: string, tokenEnd: number): boolean => ( ['if', 'loop', 'case'].includes(nextSqlSignificantToken(text, tokenEnd)) ); const trimStatementRange = (sql: string, start: number, end: number): SqlStatementRange | null => { let nextStart = Math.max(0, start); let nextEnd = Math.min(sql.length, Math.max(start, end)); while (nextStart < nextEnd && isWhitespace(sql[nextStart])) { nextStart++; } while (nextEnd > nextStart && isWhitespace(sql[nextEnd - 1])) { nextEnd--; } if (nextStart >= nextEnd) { return null; } return { start: nextStart, end: nextEnd, text: sql.slice(nextStart, nextEnd), }; }; export const findSqlStatementRanges = (sql: string): SqlStatementRange[] => { const text = String(sql || '').replace(/\r\n/g, '\n'); const ranges: SqlStatementRange[] = []; let statementStart = 0; let inSingle = false; let inDouble = false; let inBacktick = false; let escaped = false; let inLineComment = false; let inBlockComment = false; let dollarTag: string | null = null; let plsqlDepth = 0; let plsqlDeclareBeginSkips = 0; let justClosedPLSQLBlock = false; const push = (end: number) => { const range = trimStatementRange(text, statementStart, end); if (range) { ranges.push(range); } }; for (let index = 0; index < text.length; index++) { const ch = text[index]; const next = index + 1 < text.length ? text[index + 1] : ''; const prev = index > 0 ? text[index - 1] : ''; const next2 = index + 2 < text.length ? text[index + 2] : ''; if (dollarTag) { if (text.startsWith(dollarTag, index)) { index += dollarTag.length - 1; dollarTag = null; } continue; } if (inLineComment) { if (ch === '\n') { inLineComment = false; } continue; } if (inBlockComment) { if (ch === '*' && next === '/') { index++; inBlockComment = false; } continue; } if (!inSingle && !inDouble && !inBacktick) { if (ch === '/' && next === '*') { index++; inBlockComment = true; continue; } if (ch === '#') { inLineComment = true; continue; } if (ch === '-' && next === '-' && (index === 0 || isWhitespace(prev)) && (next2 === '' || isWhitespace(next2))) { index++; inLineComment = true; continue; } if (ch === '$') { const match = text.slice(index).match(/^\$[A-Za-z0-9_]*\$/); if (match?.[0]) { dollarTag = match[0]; index += dollarTag.length - 1; continue; } } } if (escaped) { escaped = false; continue; } if ((inSingle || inDouble) && ch === '\\') { escaped = true; continue; } if (!inDouble && !inBacktick && ch === "'") { inSingle = !inSingle; continue; } if (!inSingle && !inBacktick && ch === '"') { inDouble = !inDouble; continue; } if (!inSingle && !inDouble && ch === '`') { inBacktick = !inBacktick; continue; } if (!inSingle && !inDouble && !inBacktick && !dollarTag && isSqlIdentifierStart(ch)) { let tokenEnd = index + 1; while (tokenEnd < text.length && isSqlIdentifierPart(text[tokenEnd])) { tokenEnd++; } const token = text.slice(index, tokenEnd).toLowerCase(); if (token === 'begin' && plsqlDeclareBeginSkips > 0) { plsqlDeclareBeginSkips--; justClosedPLSQLBlock = false; } else if (token === 'begin' && shouldEnterPlsqlBeginBlock(text, tokenEnd)) { plsqlDepth++; justClosedPLSQLBlock = false; } else if (token === 'declare' && shouldEnterPlsqlDeclareBlock(text, tokenEnd)) { plsqlDepth++; plsqlDeclareBeginSkips++; justClosedPLSQLBlock = false; } else if (token === 'end' && plsqlDepth > 0 && !isPlsqlControlEnd(text, tokenEnd)) { plsqlDepth--; if (plsqlDeclareBeginSkips > plsqlDepth) { plsqlDeclareBeginSkips = plsqlDepth; } justClosedPLSQLBlock = plsqlDepth === 0; } index = tokenEnd - 1; continue; } if (!inSingle && !inDouble && !inBacktick && (ch === ';' || ch === ';')) { if (plsqlDepth > 0) { continue; } push(justClosedPLSQLBlock ? index + 1 : index); statementStart = index + 1; justClosedPLSQLBlock = false; continue; } } push(text.length); return ranges; }; export const resolveCurrentSqlStatementRange = (sql: string, cursorOffset: number): SqlStatementRange | null => { const text = String(sql || '').replace(/\r\n/g, '\n'); const offset = Math.max(0, Math.min(text.length, Number.isFinite(cursorOffset) ? cursorOffset : 0)); const ranges = findSqlStatementRanges(text); if (ranges.length === 0) { return null; } const containingRange = ranges.find((range) => offset >= range.start && offset <= range.end); if (containingRange) { return containingRange; } const nextRange = ranges.find((range) => offset < range.start); if (nextRange) { return nextRange; } return ranges[ranges.length - 1]; }; export const resolveExecutableSql = ( sql: string, cursorOffset: number, selectedSql = '', ): SqlExecutionSelection | null => { const selected = String(selectedSql || '').trim(); if (selected) { return { sql: selectedSql, source: 'selection' }; } const text = String(sql || '').replace(/\r\n/g, '\n'); const offset = Math.max(0, Math.min(text.length, Number.isFinite(cursorOffset) ? cursorOffset : 0)); const ranges = findSqlStatementRanges(text); const statement = ranges.find((range) => offset >= range.start && offset <= range.end); if (statement?.text.trim()) { return { sql: statement.text, source: 'statement' }; } const lineStart = text.lastIndexOf('\n', Math.max(0, offset - 1)) + 1; const nextLineBreak = text.indexOf('\n', offset); const lineEnd = nextLineBreak === -1 ? text.length : nextLineBreak; const line = text.slice(lineStart, lineEnd).trim(); if (line) { const lineStatement = [...ranges].reverse().find((range) => range.start < lineEnd && range.end >= lineStart); if (lineStatement?.text.trim()) { return { sql: lineStatement.text, source: 'statement' }; } } if (line) { return { sql: line, source: 'line' }; } return null; };