UNPKG

rawsql-ts

Version:

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

292 lines 11.1 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); } }; } /** * Get query boundaries from SQL text with proper semicolon handling * * @param text - SQL text to analyze * @returns Array of boundary positions */ /** * Split text by semicolons while respecting quotes and comments */ static splitRespectingQuotesAndComments(text) { const segments = []; let currentStart = 0; let i = 0; while (i <= text.length) { // Check if we're at a valid semicolon or end of text const isValidBreakpoint = (i === text.length) || (i < text.length && this.isValidSemicolon(text, i)); if (isValidBreakpoint) { const segmentText = text.substring(currentStart, i); if (segmentText.length > 0 || i < text.length) { segments.push({ text: segmentText, start: currentStart, end: i }); } currentStart = i + 1; } i++; } return segments; } /** * Check if character at position is a valid semicolon (not in quotes/comments) */ static isValidSemicolon(text, pos) { if (text[pos] !== ';') return false; // Check if this semicolon is inside quotes or comments by scanning from start let inSingleQuote = false; let inDoubleQuote = false; let inLineComment = false; let inBlockComment = false; for (let i = 0; i < pos; i++) { const char = text[i]; const nextChar = i + 1 < text.length ? text[i + 1] : ''; // Handle line comments if (!inSingleQuote && !inDoubleQuote && !inBlockComment && char === '-' && nextChar === '-') { inLineComment = true; i++; // Skip next character continue; } if (inLineComment && char === '\n') { inLineComment = false; continue; } // Handle block comments if (!inSingleQuote && !inDoubleQuote && !inLineComment && char === '/' && nextChar === '*') { inBlockComment = true; i++; // Skip next character continue; } if (inBlockComment && char === '*' && nextChar === '/') { inBlockComment = false; i++; // Skip next character continue; } // Skip if in any comment if (inLineComment || inBlockComment) { continue; } // Handle quotes if (char === "'" && !inDoubleQuote) { if (inSingleQuote && nextChar === "'") { i++; // Skip escaped quote } else { inSingleQuote = !inSingleQuote; } continue; } if (char === '"' && !inSingleQuote) { if (inDoubleQuote && nextChar === '"') { i++; // Skip escaped quote } else { inDoubleQuote = !inDoubleQuote; } continue; } } // Return false if we're inside quotes or comments at this position return !inSingleQuote && !inDoubleQuote && !inLineComment && !inBlockComment; } /** * 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