UNPKG

rawsql-ts

Version:

High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

331 lines 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MultiQueryUtils = exports.MultiQuerySplitter = void 0; const TextPositionUtils_1 = require("./TextPositionUtils"); /** * Splits SQL text containing multiple queries separated by semicolons * * Provides sophisticated query boundary detection that properly handles: * - String literals containing semicolons * - Comments containing semicolons * - Nested structures and complex SQL * - Empty queries and whitespace handling * * @example * ```typescript * const multiSQL = ` * -- First query * SELECT 'hello;world' FROM users; * * // Second query with comment * SELECT id FROM orders WHERE status = 'active'; * * -- Empty query * ; * `; * * const queries = MultiQuerySplitter.split(multiSQL); * console.log(queries.queries.length); // 3 queries * * // Find query at cursor position * const active = queries.getActive(150); * console.log(active?.sql); // Query containing position 150 * ``` */ class MultiQuerySplitter { /** * Split multi-query SQL text into individual queries * * @param text - SQL text that may contain multiple queries separated by semicolons * @returns Collection of individual queries with position information */ static split(text) { const queries = []; // Handle completely empty or whitespace-only text if (!text || text.trim() === '') { return { queries: [], originalText: text, getActive: () => undefined, getQuery: () => undefined, getNonEmpty: () => [] }; } const rawBoundaries = this.splitRespectingQuotesAndComments(text); const boundaries = this.mergeTrailingCommentSegments(rawBoundaries, text); let queryIndex = 0; for (const boundary of boundaries) { const rawSql = boundary.text.trim(); const isEmpty = this.isEmptyQuery(rawSql); // Use raw SQL as-is - boundaries are already correctly split by valid semicolons const sql = rawSql; const startLineCol = TextPositionUtils_1.TextPositionUtils.charOffsetToLineColumn(text, boundary.start); const endLineCol = TextPositionUtils_1.TextPositionUtils.charOffsetToLineColumn(text, boundary.end); queries.push({ sql, start: boundary.start, end: boundary.end, startLine: (startLineCol === null || startLineCol === void 0 ? void 0 : startLineCol.line) || 1, endLine: (endLineCol === null || endLineCol === void 0 ? void 0 : endLineCol.line) || 1, index: queryIndex++, isEmpty }); } return { queries, originalText: text, getActive: (cursorPosition) => { const charPos = typeof cursorPosition === 'number' ? cursorPosition : TextPositionUtils_1.TextPositionUtils.lineColumnToCharOffset(text, cursorPosition); if (charPos === -1) return undefined; return queries.find(query => charPos >= query.start && charPos <= query.end); }, getQuery: (index) => { return queries[index]; }, getNonEmpty: () => { return queries.filter(q => !q.isEmpty); } }; } /** * Split text by semicolons while respecting quotes and comments. * Single-pass O(n) implementation with support for: * - Single-quoted strings ('...' with '' escaping) * - Double-quoted identifiers ("..." with "" escaping) * - Line comments (--) * - Block comments (/* ... *\/) * - PostgreSQL dollar-quoted strings ($$...$$ or $tag$...$tag$) */ static splitRespectingQuotesAndComments(text) { const segments = []; let currentStart = 0; let i = 0; let inSingleQuote = false; let inDoubleQuote = false; let inLineComment = false; let inBlockComment = false; let inDollarQuote = false; let dollarTag = ''; while (i < text.length) { const char = text[i]; // --- inside line comment --- if (inLineComment) { if (char === '\n') inLineComment = false; i++; continue; } // --- inside block comment --- if (inBlockComment) { if (char === '*' && text[i + 1] === '/') { inBlockComment = false; i += 2; } else { i++; } continue; } // --- inside dollar-quoted string --- if (inDollarQuote) { const closing = '$' + dollarTag + '$'; if (text.startsWith(closing, i)) { inDollarQuote = false; dollarTag = ''; i += closing.length; } else { i++; } continue; } // --- inside single-quoted string --- if (inSingleQuote) { if (char === "'" && text[i + 1] === "'") { i += 2; // escaped quote } else if (char === "'") { inSingleQuote = false; i++; } else { i++; } continue; } // --- inside double-quoted identifier --- if (inDoubleQuote) { if (char === '"' && text[i + 1] === '"') { i += 2; // escaped quote } else if (char === '"') { inDoubleQuote = false; i++; } else { i++; } continue; } // --- unquoted context --- // line comment if (char === '-' && text[i + 1] === '-') { inLineComment = true; i += 2; continue; } // block comment if (char === '/' && text[i + 1] === '*') { inBlockComment = true; i += 2; continue; } // dollar-quoted string: $tag$ where tag is empty or [a-zA-Z_]\w* if (char === '$') { const tagEnd = text.indexOf('$', i + 1); if (tagEnd !== -1) { const tag = text.substring(i + 1, tagEnd); if (/^([a-zA-Z_]\w*)?$/.test(tag)) { inDollarQuote = true; dollarTag = tag; i = tagEnd + 1; continue; } } } // single quote if (char === "'") { inSingleQuote = true; i++; continue; } // double quote if (char === '"') { inDoubleQuote = true; i++; continue; } // valid semicolon split point if (char === ';') { segments.push({ text: text.substring(currentStart, i), start: currentStart, end: i }); currentStart = i + 1; i++; continue; } i++; } // remaining text after last semicolon (or entire text if no semicolon) const remaining = text.substring(currentStart); if (remaining.length > 0 || segments.length === 0) { segments.push({ text: remaining, start: currentStart, end: text.length }); } return segments; } /** * Merge comment-only segments with previous executable segments */ static mergeTrailingCommentSegments(segments, fullText) { const merged = []; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; const segmentText = segment.text.trim(); // Check if this segment contains only comments/whitespace (no executable SQL) const isCommentOnly = this.isEmptyQuery(segmentText); if (isCommentOnly && merged.length > 0) { // Only merge if this appears to be a trailing line comment (starts with --) // and the previous segment contains executable SQL const lastSegmentText = merged[merged.length - 1].text.trim(); const isTrailingLineComment = segmentText.startsWith('--'); const previousHasSQL = !this.isEmptyQuery(lastSegmentText); if (isTrailingLineComment && previousHasSQL) { // Merge trailing line comment with previous SQL segment const lastSegment = merged[merged.length - 1]; merged[merged.length - 1] = { text: fullText.substring(lastSegment.start, segment.end), start: lastSegment.start, end: segment.end }; } else { // Keep as separate segment (empty query or standalone comment) merged.push(segment); } } else { // Add as new segment merged.push(segment); } } return merged; } /** * Clean SQL comments and extract SQL statements * * @param sql - SQL query text * @returns Cleaned SQL text or null if no SQL remains */ static cleanSqlComments(sql) { if (!sql) return null; // Remove comments and extract SQL let cleaned = sql; // Remove line comments - standard SQL behavior: -- comments out to end of line cleaned = cleaned.split('\n').map(line => { const commentStart = line.indexOf('--'); if (commentStart >= 0) { return line.substring(0, commentStart); } return line; }).join('\n'); // Remove block comments cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, ''); const result = cleaned.trim(); return result.length > 0 ? result : null; } static isEmptyQuery(sql) { if (!sql) return true; return this.cleanSqlComments(sql) === null; } } exports.MultiQuerySplitter = MultiQuerySplitter; /** * Utility functions for working with query collections */ class MultiQueryUtils { /** * Get context information for IntelliSense at a cursor position * * @param text - Multi-query SQL text * @param cursorPosition - Cursor position * @returns Active query and position within that query */ static getContextAt(text, cursorPosition) { const queries = MultiQuerySplitter.split(text); const activeQuery = queries.getActive(cursorPosition); if (!activeQuery) return undefined; const charPos = typeof cursorPosition === 'number' ? cursorPosition : TextPositionUtils_1.TextPositionUtils.lineColumnToCharOffset(text, cursorPosition); if (charPos === -1) return undefined; const relativePosition = charPos - activeQuery.start; return { query: activeQuery, relativePosition }; } /** * Extract all non-empty queries from multi-query text * * @param text - Multi-query SQL text * @returns Array of query SQL strings */ static extractQueries(text) { const queries = MultiQuerySplitter.split(text); return queries.getNonEmpty().map(q => q.sql); } } exports.MultiQueryUtils = MultiQueryUtils; //# sourceMappingURL=MultiQuerySplitter.js.map