Datanet: SQL Unsafe Function Detector

Detect unsafe functions in SQL queries on DataCentral ETL pages

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Datanet: SQL Unsafe Function Detector
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Detect unsafe functions in SQL queries on DataCentral ETL pages
// @author       [email protected]
// @match        https://datacentral.a2z.com/dw-platform/servlet/dwp/template/EtlViewExtractJobs.vm/job_profile_id/*
// @grant        none
// ==/UserScript==

/*
REVISION HISTORY:
1 - 2025-09-16 - zhjy@ - Initial version
1.1 - 2025-09-16 - zhjy@ - Updated "match" to make it available to new Datanet UI
1.2 - 2025-09-16 - zhjy@ - Revert to 1.0
1.3 - 2025-09-16 - zhjy@ - Fixed performance issues by removing automatic monitoring, detection now only runs on button click
1.4 - 2025-09-16 - zhjy@ - Enhanced WHERE clause detection to identify complete WHERE clauses and JOIN ON clause extraction for improved accuracy.
1.5 - 2025-09-18 - zhjy@ - Fixed SQL text extraction; Modernized UI design; Refactored unsafe function detection logic
*/

(function() {
    'use strict';

    // Define unsafe functions by category
    const UNSAFE_FUNCTIONS = {
        'Date/Time Functions': [
            'DATE_TRUNC', 'DATE_PART', 'EXTRACT', 'TO_CHAR', 'ADD_MONTHS',
            'CONVERT_TIMEZONE', 'CURRENT_DATE', 'INTERVAL', 'TRUNC', '::DATE'
        ],
        'String Functions': [
            'LENGTH', 'LEFT', 'RIGHT', 'SUBSTR', 'POSITION', 'CONCAT', 'REPLACE'
        ],
        'String Matching Functions': [
            'SIMILAR TO', 'REGEXP_COUNT', 'LIKE'
        ],
        'Type Conversion Functions': [
            'CAST', 'TO_NUMBER', 'TO_DATE',
            '::DATE', '::TIMESTAMP', '::TIME', '::INTEGER', '::NUMERIC', '::TEXT', '::VARCHAR', '::CHAR', '::BOOLEAN'
        ],
        'Time Comparison Functions': [
            'TIME BETWEEN', 'EXTRACT.*EPOCH'
        ]
    };

    // Flatten all unsafe functions for easy lookup
    const ALL_UNSAFE_FUNCTIONS = Object.values(UNSAFE_FUNCTIONS).flat();

    // Define safe TO_DATE patterns that should not be flagged as unsafe
    const SAFE_TO_DATE_PATTERNS = [
        /to_date\s*\(\s*['"]\{RUN_DATE_YYYYMMDD\}['"],\s*['"]YYYYMMDD['"]\s*\)/gi,
        /to_date\s*\(\s*['"]\{RUN_DATE_YYYY-MM-DD\}['"],\s*['"]YYYY-MM-DD['"]\s*\)/gi,
        /to_date\s*\(\s*['"]\{RUN_DATE_YYYY\/MM\/DD\}['"],\s*['"]YYYY\/MM\/DD['"]\s*\)/gi
    ];

    // CSS styles for UI and highlighting
    const styles = `
        .unsafe-function-highlight {
            background-color: #fef3c7 !important;
            border-radius: 4px !important;
            padding: 1px 3px !important;
            font-weight: 600 !important;
            user-select: text !important;
            -webkit-user-select: text !important;
            -moz-user-select: text !important;
            -ms-user-select: text !important;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
        }

        .unsafe-function-panel {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 380px;
            max-height: 520px;
            background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
            border: 1px solid #e2e8f0;
            border-radius: 12px;
            box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
            z-index: 9999;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            overflow: hidden;
            backdrop-filter: blur(10px);
            border-top: 3px solid #334155;
        }

        .unsafe-function-panel-header {
            background: linear-gradient(135deg, #334155 0%, #475569 100%);
            color: white;
            padding: 16px 20px;
            font-weight: 600;
            font-size: 15px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            letter-spacing: -0.025em;
        }

        .unsafe-function-panel-content {
            padding: 20px;
            max-height: 420px;
            overflow-y: auto;
            scrollbar-width: thin;
            scrollbar-color: #cbd5e1 #f1f5f9;
        }

        .unsafe-function-panel-content::-webkit-scrollbar {
            width: 6px;
        }

        .unsafe-function-panel-content::-webkit-scrollbar-track {
            background: #f1f5f9;
            border-radius: 3px;
        }

        .unsafe-function-panel-content::-webkit-scrollbar-thumb {
            background: #cbd5e1;
            border-radius: 3px;
        }

        .unsafe-function-panel-content::-webkit-scrollbar-thumb:hover {
            background: #94a3b8;
        }

        .unsafe-function-category {
            margin-bottom: 18px;
        }

        .unsafe-function-category:last-child {
            margin-bottom: 0;
        }

        .unsafe-function-category-title {
            font-weight: 600;
            color: #1e293b;
            margin-bottom: 8px;
            border-bottom: 2px solid #e2e8f0;
            padding-bottom: 6px;
            font-size: 14px;
            letter-spacing: -0.025em;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .unsafe-function-category-title::before {
            content: "⚠️";
            font-size: 16px;
        }

        .unsafe-function-item {
            margin: 6px 0;
            padding: 12px 16px;
            background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
            border: 1px solid #e2e8f0;
            border-left: 4px solid #f59e0b;
            border-radius: 8px;
            font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
            font-size: 13px;
            cursor: pointer;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
            position: relative;
            line-height: 1.5;
        }

        .unsafe-function-item:hover {
            background: linear-gradient(135deg, #fef3c7 0%, #fef9e7 100%);
            border-color: #f59e0b;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
            transform: translateY(-1px);
        }

        .unsafe-function-item strong {
            color: #dc2626;
            font-weight: 700;
            font-size: 14px;
        }

        .unsafe-function-item small {
            color: #64748b;
            font-size: 12px;
            line-height: 1.4;
            margin-top: 4px;
            display: block;
        }

        .unsafe-function-item::after {
            content: "🔍";
            position: absolute;
            right: 12px;
            top: 50%;
            transform: translateY(-50%);
            opacity: 0;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
            font-size: 16px;
        }

        .unsafe-function-item:hover::after {
            opacity: 1;
            transform: translateY(-50%) scale(1.1);
        }

        .temporary-line-flash {
            animation: lineFlash 2s ease-out;
        }

        @keyframes lineFlash {
            0% {
                background-color: rgba(51, 65, 85, 0.3) !important;
                box-shadow: 0 0 0 4px rgba(51, 65, 85, 0.2) !important;
            }
            50% {
                background-color: rgba(51, 65, 85, 0.2) !important;
                box-shadow: 0 0 0 2px rgba(51, 65, 85, 0.1) !important;
            }
            100% {
                background-color: rgba(245, 158, 11, 0.15) !important;
                box-shadow: none !important;
            }
        }

        .close-btn {
            background: rgba(255, 255, 255, 0.2);
            border: none;
            color: white;
            font-size: 20px;
            cursor: pointer;
            padding: 0;
            width: 28px;
            height: 28px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 6px;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
        }

        .close-btn:hover {
            background: rgba(255, 255, 255, 0.3);
            transform: scale(1.05);
        }

        .toggle-btn {
            position: fixed;
            bottom: 24px;
            right: 24px;
            background: linear-gradient(135deg, #334155 0%, #475569 100%);
            color: white;
            border: none;
            padding: 14px 20px;
            border-radius: 12px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            z-index: 9998;
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            letter-spacing: -0.025em;
            min-width: 200px;
            text-align: center;
        }

        .toggle-btn:hover {
            background: linear-gradient(135deg, #475569 0%, #64748b 100%);
            transform: translateY(-2px);
            box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
        }

        .toggle-btn:active {
            transform: translateY(0);
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
        }

        .no-unsafe-functions {
            color: #059669;
            font-style: normal;
            text-align: center;
            padding: 32px 20px;
            font-size: 15px;
            font-weight: 500;
            line-height: 1.6;
            background: linear-gradient(135deg, #ecfdf5 0%, #f0fdf4 100%);
            border-radius: 8px;
            border: 1px solid #bbf7d0;
        }

        .no-unsafe-functions::before {
            content: "✅";
            display: block;
            font-size: 32px;
            margin-bottom: 12px;
        }
    `;

    // Add styles to page
    const styleSheet = document.createElement('style');
    styleSheet.textContent = styles;
    document.head.appendChild(styleSheet);

    let detectedFunctions = {};
    let panelVisible = false;
    let panel = null;
    let toggleBtn = null;
    let isScanning = false;
    let scanTimeout = null;
    let highlightStyleSheet = null; // Store dynamic highlight styles
    let highlightedLines = new Map(); // Store highlighted line information for persistence
    let highlightProtectionInterval = null; // Interval for highlight protection

    // Create unique key for deduplication
    function createUniqueKey(func) {
        return `${func.function}-${func.line}-${func.column}-${func.context.trim()}`;
    }

    // Deduplicate detected functions using unique keys
    function deduplicateDetections(detectedFunctions) {
        const deduplicatedFunctions = {};
        const seenKeys = new Set();

        Object.entries(detectedFunctions).forEach(([category, functions]) => {
            const uniqueFunctions = [];

            functions.forEach(func => {
                const uniqueKey = createUniqueKey(func);
                if (!seenKeys.has(uniqueKey)) {
                    seenKeys.add(uniqueKey);
                    uniqueFunctions.push(func);
                } else {
                    // Duplicate detection skipped
                }
            });

            if (uniqueFunctions.length > 0) {
                deduplicatedFunctions[category] = uniqueFunctions;
            }
        });

        return deduplicatedFunctions;
    }

    // Jump to line functionality - completely independent functions
    function jumpToLine(lineNumber, elementIndex) {
        try {
            // Find the correct CodeMirror instance
            const codeMirrorInstance = findCodeMirrorForLine(lineNumber, elementIndex);
            if (!codeMirrorInstance) {
                return false;
            }

            // Get the target line element
            const lines = codeMirrorInstance.querySelectorAll('.CodeMirror-line');
            const targetLineIndex = lineNumber - 1; // Convert to 0-based index

            if (targetLineIndex >= 0 && targetLineIndex < lines.length) {
                const targetLine = lines[targetLineIndex];

                // Scroll to the line with smooth behavior
                targetLine.scrollIntoView({
                    behavior: 'smooth',
                    block: 'center',
                    inline: 'nearest'
                });

                // Add temporary flash effect
                addTemporaryHighlight(targetLine);

                return true;
            } else {
                return false;
            }
        } catch (error) {
            console.error('Error in jumpToLine:', error);
            return false;
        }
    }

    function findCodeMirrorForLine(lineNumber, elementIndex) {
        try {
            // Strategy 1: Use elementIndex if provided
            if (typeof elementIndex === 'number') {
                const codeMirrorElements = document.querySelectorAll('.CodeMirror-scroll');
                if (elementIndex >= 0 && elementIndex < codeMirrorElements.length) {
                    return codeMirrorElements[elementIndex];
                }
            }

            // Strategy 2: Find CodeMirror instance that contains the line
            const allCodeMirrorElements = document.querySelectorAll('.CodeMirror-scroll');

            for (let element of allCodeMirrorElements) {
                const lines = element.querySelectorAll('.CodeMirror-line');
                if (lineNumber <= lines.length) {
                    return element;
                }
            }

            // Strategy 3: Return the first available CodeMirror instance
            return allCodeMirrorElements[0] || null;
        } catch (error) {
            console.error('Error in findCodeMirrorForLine:', error);
            return null;
        }
    }

    function addTemporaryHighlight(lineElement) {
        try {
            if (!lineElement) return;

            // Add flash animation class
            lineElement.classList.add('temporary-line-flash');

            // Remove the class after animation completes
            setTimeout(() => {
                if (lineElement && lineElement.classList) {
                    lineElement.classList.remove('temporary-line-flash');
                }
            }, 1500); // Match the animation duration
        } catch (error) {
            console.error('Error in addTemporaryHighlight:', error);
        }
    }

    function setupSummaryClickHandlers() {
        try {
            if (!panel) return;

            // Use event delegation to handle clicks on summary items
            panel.addEventListener('click', function(event) {
                // Check if clicked element is an unsafe function item
                const clickedItem = event.target.closest('.unsafe-function-item');
                if (!clickedItem) return;

                // Prevent event bubbling
                event.stopPropagation();

                // Extract line number from the item content
                const lineInfo = extractLineInfoFromItem(clickedItem);
                if (lineInfo) {
                    jumpToLine(lineInfo.line, lineInfo.elementIndex);
                }
            });
        } catch (error) {
            console.error('Error in setupSummaryClickHandlers:', error);
        }
    }

    function extractLineInfoFromItem(itemElement) {
        try {
            const text = itemElement.textContent || '';

            // Extract line number from text like "REPLACE at line 15:23"
            const lineMatch = text.match(/at line (\d+):(\d+)/);
            if (lineMatch) {
                const lineNumber = parseInt(lineMatch[1], 10);
                const columnNumber = parseInt(lineMatch[2], 10);

                // Try to find the corresponding elementIndex from detectedFunctions
                let elementIndex = 0; // Default to first element

                // Search through detectedFunctions to find matching entry
                Object.values(detectedFunctions).forEach(functions => {
                    functions.forEach(func => {
                        if (func.line === lineNumber && func.column === columnNumber) {
                            // Try to find elementIndex from highlightedLines
                            highlightedLines.forEach((highlightInfo, lineKey) => {
                                if (highlightInfo.lineNumber === lineNumber) {
                                    elementIndex = highlightInfo.elementIndex;
                                }
                            });
                        }
                    });
                });

                return {
                    line: lineNumber,
                    column: columnNumber,
                    elementIndex: elementIndex
                };
            }

            return null;
        } catch (error) {
            console.error('Error in extractLineInfoFromItem:', error);
            return null;
        }
    }

    // Split SQL text into individual statements by semicolon, handling quotes and comments
    function splitSQLStatements(sqlText) {
        const statements = [];
        let currentStatement = '';
        let i = 0;
        let inSingleQuote = false;
        let inDoubleQuote = false;
        let inMultiLineComment = false;
        let inSingleLineComment = false;
        let statementStartPosition = 0;

        // Clean the SQL text first - remove zero-width characters and normalize
        sqlText = sqlText.replace(/[\u200B-\u200D\uFEFF]/g, ''); // Remove zero-width characters
        sqlText = sqlText.replace(/\u00A0/g, ' '); // Replace non-breaking spaces with regular spaces

        while (i < sqlText.length) {
            const char = sqlText[i];
            const nextChar = i + 1 < sqlText.length ? sqlText[i + 1] : '';

            // Handle multi-line comments /* */
            if (!inSingleQuote && !inDoubleQuote && !inSingleLineComment) {
                if (char === '/' && nextChar === '*') {
                    inMultiLineComment = true;
                    currentStatement += char + nextChar;
                    i += 2;
                    continue;
                }
            }

            if (inMultiLineComment) {
                currentStatement += char;
                if (char === '*' && nextChar === '/') {
                    inMultiLineComment = false;
                    currentStatement += nextChar;
                    i += 2;
                    continue;
                }
                i++;
                continue;
            }

            // Handle single-line comments -- (improved to handle multiple dashes)
            if (!inSingleQuote && !inDoubleQuote && !inSingleLineComment) {
                if (char === '-' && nextChar === '-') {
                    inSingleLineComment = true;
                    currentStatement += char + nextChar;
                    i += 2;
                    continue;
                }
            }

            if (inSingleLineComment) {
                currentStatement += char;
                if (char === '\n' || char === '\r') {
                    inSingleLineComment = false;
                } else if (char === ';') {
                    // Special case: semicolon can end a single-line comment if there's no newline
                    // This handles cases where SQL statements are on the same line
                    inSingleLineComment = false;
                    // Don't increment i here, let the semicolon be processed normally
                    continue;
                }
                i++;
                continue;
            }

            // Handle string literals with escape sequence support
            if (!inMultiLineComment && !inSingleLineComment) {
                if (char === "'" && !inDoubleQuote) {
                    // Check for escaped quote
                    if (i > 0 && sqlText[i-1] === '\\') {
                        currentStatement += char;
                        i++;
                        continue;
                    }
                    inSingleQuote = !inSingleQuote;
                    currentStatement += char;
                    i++;
                    continue;
                } else if (char === '"' && !inSingleQuote) {
                    // Check for escaped quote
                    if (i > 0 && sqlText[i-1] === '\\') {
                        currentStatement += char;
                        i++;
                        continue;
                    }
                    inDoubleQuote = !inDoubleQuote;
                    currentStatement += char;
                    i++;
                    continue;
                }
            }

            // Handle semicolon (statement separator)
            if (!inSingleQuote && !inDoubleQuote && !inMultiLineComment && !inSingleLineComment && char === ';') {
                currentStatement += char;

                // Trim and add statement if it's not empty
                const trimmedStatement = currentStatement.trim();

                if (trimmedStatement && trimmedStatement !== ';') {
                    statements.push({
                        text: trimmedStatement,
                        startPosition: statementStartPosition,
                        endPosition: i + 1
                    });
                }

                // Reset for next statement - skip whitespace after semicolon
                currentStatement = '';
                i++;
                // Skip whitespace and newlines to find the start of next statement
                while (i < sqlText.length && /\s/.test(sqlText[i])) {
                    i++;
                }
                statementStartPosition = i;
                continue;
            }

            // Normal character
            currentStatement += char;
            i++;
        }

        // Add the last statement if it exists (improved handling)
        const trimmedStatement = currentStatement.trim();
        if (trimmedStatement) {
            statements.push({
                text: trimmedStatement,
                startPosition: statementStartPosition,
                endPosition: sqlText.length
            });
        }

        return statements;
    }

    // Remove SQL comments from text while preserving line structure
    function removeComments(sqlText) {
        let result = '';
        let i = 0;
        let inSingleQuote = false;
        let inDoubleQuote = false;
        let inMultiLineComment = false;

        while (i < sqlText.length) {
            const char = sqlText[i];
            const nextChar = i + 1 < sqlText.length ? sqlText[i + 1] : '';

            // Handle string literals (don't process comments inside strings)
            if (!inMultiLineComment) {
                if (char === "'" && !inDoubleQuote) {
                    inSingleQuote = !inSingleQuote;
                    result += char;
                    i++;
                    continue;
                } else if (char === '"' && !inSingleQuote) {
                    inDoubleQuote = !inDoubleQuote;
                    result += char;
                    i++;
                    continue;
                }
            }

            // Skip comment processing if we're inside a string literal
            if (inSingleQuote || inDoubleQuote) {
                result += char;
                i++;
                continue;
            }

            // Handle multi-line comments /* */
            if (!inMultiLineComment && char === '/' && nextChar === '*') {
                inMultiLineComment = true;
                result += '  '; // Replace with spaces to maintain positioning
                i += 2;
                continue;
            }

            if (inMultiLineComment) {
                if (char === '*' && nextChar === '/') {
                    inMultiLineComment = false;
                    result += '  '; // Replace with spaces
                    i += 2;
                    continue;
                } else if (char === '\n') {
                    result += char; // Preserve line breaks
                    i++;
                    continue;
                } else {
                    result += ' '; // Replace comment content with space
                    i++;
                    continue;
                }
            }

            // Handle single-line comments --
            if (char === '-' && nextChar === '-') {
                // Replace everything from -- to end of line with spaces
                while (i < sqlText.length && sqlText[i] !== '\n') {
                    result += ' ';
                    i++;
                }
                // Don't increment i here as we want to process the \n normally
                continue;
            }

            // Normal character
            result += char;
            i++;
        }

        return result;
    }

    // Check if a TO_DATE function call matches safe patterns
    function isSafeToDatePattern(expression) {
        // Reset all regex patterns before testing
        SAFE_TO_DATE_PATTERNS.forEach(pattern => {
            pattern.lastIndex = 0;
        });

        // Test against all safe patterns
        return SAFE_TO_DATE_PATTERNS.some(pattern => {
            pattern.lastIndex = 0; // Reset regex state
            return pattern.test(expression);
        });
    }

    // Extract complete TO_DATE function call from text around a match position
    function extractToDateFunctionCall(text, matchIndex) {
        // Find the start of the function call
        let start = matchIndex;
        while (start > 0 && text[start - 1] !== ' ' && text[start - 1] !== '\t' && text[start - 1] !== '\n' && text[start - 1] !== '(' && text[start - 1] !== ',') {
            start--;
        }

        // Find the end of the function call by matching parentheses
        let parenCount = 0;
        let end = matchIndex;
        let foundOpenParen = false;

        // Move to the opening parenthesis
        while (end < text.length) {
            if (text[end] === '(') {
                foundOpenParen = true;
                parenCount++;
                end++;
                break;
            } else if (text[end] === ' ' || text[end] === '\t') {
                end++;
            } else {
                end++;
            }
        }

        if (!foundOpenParen) {
            return text.substring(start, Math.min(start + 50, text.length)); // Fallback
        }

        // Find the matching closing parenthesis
        while (end < text.length && parenCount > 0) {
            if (text[end] === '(') {
                parenCount++;
            } else if (text[end] === ')') {
                parenCount--;
            }
            end++;
        }

        return text.substring(start, end).trim();
    }

    // Create regex patterns for detecting unsafe functions
    function createFunctionRegex(functionName) {
        // Handle special cases
        if (functionName === 'SIMILAR TO') {
            return new RegExp('\\bSIMILAR\\s+TO\\b', 'gi');
        }
        if (functionName === 'TIME BETWEEN') {
            return new RegExp('\\bTIME\\s+BETWEEN\\b', 'gi');
        }
        if (functionName === 'EXTRACT.*EPOCH') {
            return new RegExp('\\bEXTRACT\\s*\\([^)]*EPOCH[^)]*\\)', 'gi');
        }

        // Handle POSITION function which can have different syntax: POSITION(x IN y) or POSITION(x,y)
        if (functionName === 'POSITION') {
            return new RegExp('\\bPOSITION\\s*\\(', 'gi');
        }

        // Handle INTERVAL function - supports both INTERVAL(...) and INTERVAL 'value' formats
        if (functionName === 'INTERVAL') {
            return new RegExp('\\bINTERVAL\\s*(?:\\(|\'|\\d)', 'gi');
        }

        // Handle :: type conversion patterns (e.g., ::DATE, ::TEXT, ::INTEGER)
        // Allow any non-whitespace character before :: to handle cases like (expression)::date
        if (functionName.startsWith('::')) {
            const typeName = functionName.substring(2); // Remove the ::
            return new RegExp('[^\\s]::\\s*' + typeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'gi');
        }

        // Standard function pattern: FUNCTION_NAME followed by opening parenthesis
        return new RegExp('\\b' + functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*\\(', 'gi');
    }

    // Parse comparison expressions and extract only the left side (field being compared)
    function parseComparisonExpression(condition) {
        const leftSideExpressions = [];

        // First, remove any nested SELECT...FROM statements from the condition
        const cleanedCondition = removeNestedSelectStatements(condition);

        // Handle BETWEEN operator specially
        const betweenMatch = cleanedCondition.match(/^(.+?)\s+BETWEEN\s+/i);
        if (betweenMatch) {
            leftSideExpressions.push(betweenMatch[1].trim());
            return leftSideExpressions;
        }

        // Handle IN operator with potential subqueries
        const inMatch = cleanedCondition.match(/^(.+?)\s+(?:NOT\s+)?IN\s*\(/i);
        if (inMatch) {
            leftSideExpressions.push(inMatch[1].trim());
            return leftSideExpressions;
        }

        // Handle EXISTS operator (left side is empty for EXISTS)
        if (cleanedCondition.match(/^\s*(?:NOT\s+)?EXISTS\s*\(/i)) {
            return leftSideExpressions; // No left side for EXISTS
        }

        // Handle standard comparison operators (=, >, <, >=, <=, !=, <>)
        // Use a more robust approach to handle nested parentheses in complex expressions
        let parenDepth = 0;
        let inQuotes = false;
        let quoteChar = '';
        let operatorIndex = -1;

        // Find the comparison operator at the top level (not inside parentheses or quotes)
        for (let i = 0; i < cleanedCondition.length; i++) {
            const char = cleanedCondition[i];

            // Handle quotes (including escaped quotes)
            if ((char === '"' || char === "'") && !inQuotes) {
                inQuotes = true;
                quoteChar = char;
            } else if (char === quoteChar && inQuotes) {
                // Check if it's escaped
                if (i > 0 && cleanedCondition[i-1] === '\\') {
                    continue; // Skip escaped quote
                }
                inQuotes = false;
                quoteChar = '';
            } else if (!inQuotes) {
                // Handle parentheses
                if (char === '(') {
                    parenDepth++;
                } else if (char === ')') {
                    parenDepth--;
                } else if (parenDepth === 0) {
                    // Check for comparison operators at top level
                    const remaining = cleanedCondition.substring(i);
                    // Include PostgreSQL regex operators: ~, ~*, !~, !~*
                    // Also include LIKE, ILIKE, SIMILAR TO
                    if (remaining.match(/^(>=|<=|<>|!=|!~\*|!~|~\*|~|>|<|=|\s+(?:NOT\s+)?(?:LIKE|ILIKE)\s+|\s+SIMILAR\s+TO\s+)/i)) {
                        operatorIndex = i;
                        break;
                    }
                }
            }
        }

        if (operatorIndex !== -1) {
            const leftSide = cleanedCondition.substring(0, operatorIndex).trim();
            if (leftSide) {
                leftSideExpressions.push(leftSide);
            }
        }

        return leftSideExpressions;
    }

    // Remove nested SELECT...FROM statements from a condition to avoid detecting unsafe functions within them
    function removeNestedSelectStatements(condition) {
        let result = condition;
        let changed = true;

        // Keep removing nested SELECT statements until no more are found
        while (changed) {
            changed = false;
            let parenDepth = 0;
            let inQuotes = false;
            let quoteChar = '';
            let selectStart = -1;

            for (let i = 0; i < result.length; i++) {
                const char = result[i];

                // Handle quotes
                if ((char === '"' || char === "'") && !inQuotes) {
                    inQuotes = true;
                    quoteChar = char;
                } else if (char === quoteChar && inQuotes) {
                    if (i > 0 && result[i-1] === '\\') {
                        continue; // Skip escaped quote
                    }
                    inQuotes = false;
                    quoteChar = '';
                } else if (!inQuotes) {
                    // Handle parentheses
                    if (char === '(') {
                        parenDepth++;
                        // Check if this starts a SELECT statement
                        const remaining = result.substring(i + 1).trim();
                        if (remaining.match(/^SELECT\s+/i) && selectStart === -1) {
                            selectStart = i;
                        }
                    } else if (char === ')') {
                        parenDepth--;
                        // If we're closing a SELECT statement
                        if (selectStart !== -1 && parenDepth === 0) {
                            // Replace the SELECT statement with placeholder
                            const beforeSelect = result.substring(0, selectStart);
                            const afterSelect = result.substring(i + 1);
                            result = beforeSelect + '(SUBQUERY_PLACEHOLDER)' + afterSelect;
                            changed = true;
                            break;
                        }
                    }
                }
            }
        }

        return result;
    }

    // Split WHERE clause into individual conditions, handling nested parentheses
    function splitWhereConditions(whereClause) {
        const conditions = [];
        let current = '';
        let parenDepth = 0;
        let inQuotes = false;
        let quoteChar = '';

        for (let i = 0; i < whereClause.length; i++) {
            const char = whereClause[i];
            const nextChars = whereClause.substr(i, 4).toUpperCase();

            // Handle quotes
            if ((char === '"' || char === "'") && !inQuotes) {
                inQuotes = true;
                quoteChar = char;
                current += char;
            } else if (char === quoteChar && inQuotes) {
                inQuotes = false;
                quoteChar = '';
                current += char;
            } else if (inQuotes) {
                current += char;
            } else {
                // Handle parentheses
                if (char === '(') {
                    parenDepth++;
                    current += char;
                } else if (char === ')') {
                    parenDepth--;
                    current += char;
                } else if (parenDepth === 0 && (nextChars === 'AND ' || nextChars === 'OR ')) {
                    // Found AND/OR at top level
                    if (current.trim()) {
                        conditions.push(current.trim());
                    }
                    current = '';
                    i += 3; // Skip past 'AND' or 'OR '
                } else {
                    current += char;
                }
            }
        }

        // Add the last condition
        if (current.trim()) {
            conditions.push(current.trim());
        }

        return conditions;
    }

    // Remove SELECT...FROM portion of statement to reduce false positives
    function removeSelectFromPortion(statement) {
        // Remove SELECT...FROM between content, keep FROM and after
        return statement.replace(/SELECT[\s\S]*?FROM\s+/i, 'FROM ');
    }

    function extractCompleteWhereClauses(statement) {
        // Remove SELECT...FROM part to reduce false positives
        let cleanedStatement = removeSelectFromPortion(statement);
        // Use non-greedy matching to extract WHERE clauses
        const whereRegex = /\bWHERE\s+([\s\S]*?)(?=\s+(?:GROUP\s+BY|ORDER\s+BY|HAVING|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|;|$))/gi;

        const whereClauses = [];
        let match;
        while ((match = whereRegex.exec(cleanedStatement)) !== null) {
            const whereClause = match[1].trim();
            if (whereClause) {
                const parts = splitAtUnmatchedParen(whereClause);
                whereClauses.push(...parts);
            }
        }

        return whereClauses;
    }

    // Helper function: Calculate statement line range boundaries
    function getStatementLineRange(statement, fullSqlText, lines) {
        try {
            // Strategy 1: Use character positions to find line boundaries
            const startPos = statement.startPosition;
            const endPos = statement.endPosition;

            let startLine = 1;
            let endLine = lines.length;
            let currentPos = 0;

            // Find start line by counting characters
            for (let i = 0; i < lines.length; i++) {
                const lineText = lines[i].textContent || '';
                const lineLength = lineText.length + 1; // +1 for newline character

                if (currentPos <= startPos && currentPos + lineLength > startPos) {
                    startLine = i + 1;
                }

                if (currentPos < endPos && currentPos + lineLength >= endPos) {
                    endLine = i + 1;
                    break;
                }

                currentPos += lineLength;
            }

            // Strategy 2: Fallback using content matching if position-based fails
            if (startLine === 1 && endLine === lines.length) {
                const statementLines = statement.text.split('\n');
                const firstStatementLine = statementLines[0].trim();
                const lastStatementLine = statementLines[statementLines.length - 1].trim();

                if (firstStatementLine) {
                    for (let i = 0; i < lines.length; i++) {
                        const lineText = (lines[i].textContent || '').trim();
                        if (lineText.includes(firstStatementLine) || firstStatementLine.includes(lineText)) {
                            startLine = i + 1;
                            break;
                        }
                    }
                }

                if (lastStatementLine && lastStatementLine !== firstStatementLine) {
                    for (let i = startLine - 1; i < lines.length; i++) {
                        const lineText = (lines[i].textContent || '').trim();
                        if (lineText.includes(lastStatementLine) || lastStatementLine.includes(lineText)) {
                            endLine = i + 1;
                            break;
                        }
                    }
                }
            }

            // Ensure valid range
            startLine = Math.max(1, startLine);
            endLine = Math.min(lines.length, Math.max(startLine, endLine));

            return { startLine, endLine };
        } catch (error) {
            console.error('Error calculating statement line range:', error);
            // Fallback to full range
            return { startLine: 1, endLine: lines.length };
        }
    }

    // Optimized function: Extract WHERE clauses with line number information (limited to statement range)
    function extractWhereClausesWithLineNumbers(statement, fullSqlText, lines) {
        const whereClauses = extractCompleteWhereClauses(statement.text);
        const clausesWithLineNumbers = [];

        // Get the line range for this statement to limit search scope
        const { startLine, endLine } = getStatementLineRange(statement, fullSqlText, lines);

        whereClauses.forEach(clause => {
            const clauseInfo = {
                text: clause,
                lineNumbers: []
            };

            // Only search within the statement's line range for better performance and accuracy
            for (let lineIndex = startLine - 1; lineIndex < endLine && lineIndex < lines.length; lineIndex++) {
                const line = lines[lineIndex];
                const lineText = line.textContent || '';
                const lineNumber = lineIndex + 1;

                // Check if this line belongs to the current statement and clause
                // Since we're already within the statement range, we can skip the expensive isLineInStatement check
                if (isLineMatchingClause(lineText, clause)) {
                    clauseInfo.lineNumbers.push({
                        lineNumber: lineNumber,
                        lineText: lineText.trim(),
                        lineElement: line
                    });
                }
            }

            if (clauseInfo.lineNumbers.length > 0) {
                clausesWithLineNumbers.push(clauseInfo);
            }
        });

        return clausesWithLineNumbers;
    }

    // Optimized function: Extract JOIN ON clauses with line number information (limited to statement range)
    function extractJoinOnClausesWithLineNumbers(statement, fullSqlText, lines) {
        const joinOnClauses = extractCompleteJoinOnClauses(statement.text);
        const clausesWithLineNumbers = [];

        // Get the line range for this statement to limit search scope
        const { startLine, endLine } = getStatementLineRange(statement, fullSqlText, lines);

        joinOnClauses.forEach(clause => {
            const clauseInfo = {
                text: clause,
                lineNumbers: []
            };

            // Only search within the statement's line range for better performance and accuracy
            for (let lineIndex = startLine - 1; lineIndex < endLine && lineIndex < lines.length; lineIndex++) {
                const line = lines[lineIndex];
                const lineText = line.textContent || '';
                const lineNumber = lineIndex + 1;

                // Check if this line belongs to the current statement and clause
                // Since we're already within the statement range, we can skip the expensive isLineInStatement check
                if (isLineMatchingClause(lineText, clause)) {
                    clauseInfo.lineNumbers.push({
                        lineNumber: lineNumber,
                        lineText: lineText.trim(),
                        lineElement: line
                    });
                }
            }

            if (clauseInfo.lineNumbers.length > 0) {
                clausesWithLineNumbers.push(clauseInfo);
            }
        });

        return clausesWithLineNumbers;
    }

    function splitAtUnmatchedParen(clause) {
        let results = [];
        let start = 0;
        let depth = 0;

        for (let i = 0; i < clause.length; i++) {
            const ch = clause[i];
            if (ch === "(") {
                depth++;
            } else if (ch === ")") {
                if (depth === 0) {
                    // find single ),end WHERE clause
                    results.push(clause.slice(start, i).trim());
                    start = i + 1;
                } else {
                    depth--;
                }
            }
        }

        const rest = clause.slice(start).trim();
        if (rest) results.push(rest);

        return results;
    }

    // Extract complete JOIN ON clauses from a SQL statement using regex
    function extractCompleteJoinOnClauses(statement) {
        // Remove SELECT...FROM part to reduce false positives
        let cleanedStatement = removeSelectFromPortion(statement);
        //console.log(cleanedStatement);
        cleanedStatement = cleanedStatement.replace(
            /\b(LEFT\s+JOIN|RIGHT\s+JOIN|INNER\s+JOIN|JOIN)\s+([\s\S]*?)\s+ON\s+/gi,
            '$1 TABLE_PLACEHOLDER ON '
        );

        //console.log(cleanedStatement);

        // Match various JOIN...ON patterns
        const joinOnRegex = /\b(INNER\s+JOIN|LEFT\s+(?:OUTER\s+)?JOIN|RIGHT\s+(?:OUTER\s+)?JOIN|FULL\s+(?:OUTER\s+)?JOIN|CROSS\s+JOIN|JOIN)\s+[\w.]+(?:\s+AS\s+\w+)?\s+ON\s+([\s\S]*?)(?=\s+(?:INNER\s+JOIN|LEFT\s+JOIN|RIGHT\s+JOIN|FULL\s+JOIN|CROSS\s+JOIN|JOIN|WHERE|GROUP\s+BY|ORDER\s+BY|HAVING|LIMIT|UNION|;|$))/gi;

        const joinOnClauses = [];
        let match;
        while ((match = joinOnRegex.exec(cleanedStatement)) !== null) {
            const onCondition = match[2].trim();
            if (onCondition) {
                joinOnClauses.push(onCondition);
            }
        }

        return joinOnClauses;
    }

    // Split clause into individual lines while preserving logical completeness
    function splitClauseIntoLines(clause) {
        if (!clause) return [];

        // Split by newlines first
        const rawLines = clause.split('\n');
        const processedLines = [];

        rawLines.forEach(line => {
            const trimmedLine = line.trim();

            // Skip empty lines and pure comment lines
            if (!trimmedLine || trimmedLine.startsWith('--')) {
                return;
            }

            // Remove inline comments but keep the main content
            const cleanedLine = trimmedLine.replace(/--.*$/, '').trim();
            if (cleanedLine) {
                processedLines.push(cleanedLine);
            }
        });

        return processedLines;
    }

    // Enhanced line matching with multiple strategies
    function isLineMatchingClause(lineText, clause) {

        const trimmedLine = lineText.trim();
        if (!trimmedLine) return false;

        // Remove comments from the line for comparison
        const cleanedLine = trimmedLine.replace(/--.*$/, '').trim();
        if (!cleanedLine) return false;
        const clauseLines = splitClauseIntoLines(clause);
        const filteredClauseLines = clauseLines.filter(line => line.length >= 5);

        /*
        // Strategy 1: Direct content matching in the complete clause
        if (clause.includes(trimmedLine) || clause.includes(cleanedLine)) {
            return true;
        }

        // Strategy 2: Normalized whitespace matching
        const normalizedLine = cleanedLine.replace(/\s+/g, ' ');
        const normalizedClause = clause.replace(/\s+/g, ' ');
        if (normalizedClause.includes(normalizedLine)) {
            return true;
        }

        // Strategy 3: Split clause into lines and check each line

        for (const clauseLine of clauseLines) {
            // Direct match
            if (clauseLine === cleanedLine || clauseLine === trimmedLine) {
                return true;
            }

            // Normalized match
            const normalizedClauseLine = clauseLine.replace(/\s+/g, ' ');
            if (normalizedClauseLine === normalizedLine) {
                return true;
            }

            // Partial match - check if the line is contained within a clause line
            if (clauseLine.includes(cleanedLine) || cleanedLine.includes(clauseLine)) {
                return true;
            }

            // Handle cases where the line might be a subset of a larger expression
            // For example, line: "user_id = 123" and clauseLine: "AND user_id = 123"
            if (clauseLine.includes(cleanedLine) &&
                (clauseLine.startsWith('AND ') || clauseLine.startsWith('OR ') ||
                 clauseLine.endsWith(' AND') || clauseLine.endsWith(' OR'))) {
                return true;
            }
        }*/

        // Strategy 4: Reverse check - see if any clause line is contained in the current line
        // This handles cases where the line has extra content like comments
        //console.log(`cleanedLine: ${cleanedLine}`);
        for (const clauseLine of filteredClauseLines) {
            if (cleanedLine.includes(clauseLine)) {
                return true;
            }
        }

        return false;
    }

    // Check if a line is within a complete WHERE clause
    function isLineInCompleteWhereClause(lineText, whereClauses) {
        const trimmedLine = lineText.trim();
        if (!trimmedLine) return false;

        return whereClauses.some(whereClause => {
            return isLineMatchingClause(lineText, whereClause);
        });
    }

    // Check if a line is within a complete JOIN ON clause
    function isLineInCompleteJoinOnClause(lineText, joinOnClauses) {
        const trimmedLine = lineText.trim();
        if (!trimmedLine) return false;

        return joinOnClauses.some(joinOnClause => {
            return isLineMatchingClause(lineText, joinOnClause);
        });
    }


    // Detect unsafe functions in SQL text (in WHERE and JOIN ON clauses) with accurate line numbers
    function detectUnsafeFunctions(sqlText, codeMirrorElement = null) {
        const detected = {};

        if (!codeMirrorElement) {
            return detected;
        }

        // Step 1: Split SQL text into individual statements
        const sqlStatements = splitSQLStatements(sqlText);
        console.log(`Split SQL into ${sqlStatements.length} statements:`, sqlStatements.map(s => s.text.substring(0, 50) + '...')); // Debug log

        // Get all CodeMirror lines
        const lines = codeMirrorElement.querySelectorAll('.CodeMirror-line');

        // Step 2: Process each SQL statement separately
        sqlStatements.forEach((statement, statementIndex) => {
            // Check if this statement contains any WHERE or JOIN ON patterns
            const hasRelevantClauses = /\b(WHERE|JOIN[\s\S]*?ON)\b/gi.test(statement.text);

            if (!hasRelevantClauses) {
                return; // No relevant clauses found in this statement
            }

            // Extract WHERE and JOIN ON clauses with line number information
            const whereClausesWithLines = extractWhereClausesWithLineNumbers(statement, sqlText, lines);
            const joinOnClausesWithLines = extractJoinOnClausesWithLineNumbers(statement, sqlText, lines);

            console.log(`Statement ${statementIndex + 1}: Found ${whereClausesWithLines.length} WHERE clauses and ${joinOnClausesWithLines.length} JOIN ON clauses`);

            // Process WHERE clauses
            whereClausesWithLines.forEach(clauseInfo => {
                clauseInfo.lineNumbers.forEach(lineInfo => {
                    // Remove comments from the line before processing
                    const cleanedLineText = removeComments(lineInfo.lineText);

                    // For WHERE clauses: extract only left-side expressions
                    const expressionsToCheck = extractLeftSideExpressionsFromLine(cleanedLineText);

                    // Check each expression for unsafe functions
                    expressionsToCheck.forEach(expression => {
                        Object.entries(UNSAFE_FUNCTIONS).forEach(([category, functions]) => {
                            functions.forEach(functionName => {
                                const regex = createFunctionRegex(functionName);
                                let match;

                                regex.lastIndex = 0; // Reset regex
                                while ((match = regex.exec(expression)) !== null) {
                                    // Special handling for TO_DATE function - check if it matches safe patterns
                                    if (functionName === 'TO_DATE') {
                                        // Extract the complete TO_DATE function call
                                        const toDateCall = extractToDateFunctionCall(expression, match.index);

                                        // Check if this TO_DATE call matches any safe patterns
                                        if (isSafeToDatePattern(toDateCall)) {
                                            //console.log(`Skipping safe TO_DATE pattern: ${toDateCall}`);
                                            continue; // Skip this detection as it's a safe pattern
                                        }
                                    }

                                    if (!detected[category]) {
                                        detected[category] = [];
                                    }

                                    // Calculate the actual column position in the original line
                                    const expressionStartInLine = cleanedLineText.indexOf(expression);
                                    const actualColumn = expressionStartInLine >= 0 ?
                                        expressionStartInLine + match.index + 1 :
                                        match.index + 1;

                                    detected[category].push({
                                        function: functionName,
                                        line: lineInfo.lineNumber,
                                        column: actualColumn,
                                        context: lineInfo.lineText, // Keep original line text for context display
                                        statementIndex: statementIndex // Track which statement this detection belongs to
                                    });
                                }
                            });
                        });
                    });
                });
            });

            // Process JOIN ON clauses
            joinOnClausesWithLines.forEach(clauseInfo => {
                clauseInfo.lineNumbers.forEach(lineInfo => {
                    // Remove comments from the line before processing
                    const cleanedLineText = removeComments(lineInfo.lineText);

                    // For JOIN ON clauses: check the entire line for unsafe functions
                    const expressionsToCheck = [cleanedLineText];

                    // Check each expression for unsafe functions
                    expressionsToCheck.forEach(expression => {
                        Object.entries(UNSAFE_FUNCTIONS).forEach(([category, functions]) => {
                            functions.forEach(functionName => {
                                const regex = createFunctionRegex(functionName);
                                let match;

                                regex.lastIndex = 0; // Reset regex
                                while ((match = regex.exec(expression)) !== null) {
                                    // Special handling for TO_DATE function - check if it matches safe patterns
                                    if (functionName === 'TO_DATE') {
                                        // Extract the complete TO_DATE function call
                                        const toDateCall = extractToDateFunctionCall(expression, match.index);

                                        // Check if this TO_DATE call matches any safe patterns
                                        if (isSafeToDatePattern(toDateCall)) {
                                            //console.log(`Skipping safe TO_DATE pattern: ${toDateCall}`);
                                            continue; // Skip this detection as it's a safe pattern
                                        }
                                    }

                                    if (!detected[category]) {
                                        detected[category] = [];
                                    }

                                    // Calculate the actual column position in the original line
                                    const expressionStartInLine = cleanedLineText.indexOf(expression);
                                    const actualColumn = expressionStartInLine >= 0 ?
                                        expressionStartInLine + match.index + 1 :
                                        match.index + 1;

                                    detected[category].push({
                                        function: functionName,
                                        line: lineInfo.lineNumber,
                                        column: actualColumn,
                                        context: lineInfo.lineText, // Keep original line text for context display
                                        statementIndex: statementIndex // Track which statement this detection belongs to
                                    });
                                }
                            });
                        });
                    });
                });
            });
        });

        return detected;
    }

    // Check if a line belongs to a specific SQL statement using improved content matching
    function isLineInStatement(lineText, fullSqlText, statement) {
        const trimmedLineText = lineText.trim();
        if (!trimmedLineText) return false;

        // Strategy 1: Check if the line content exists within the statement text
        if (statement.text.includes(trimmedLineText)) {
            return true;
        }

        // Strategy 2: Try normalized whitespace matching within the statement
        const normalizedLine = trimmedLineText.replace(/\s+/g, ' ');
        const normalizedStatement = statement.text.replace(/\s+/g, ' ');
        if (normalizedStatement.includes(normalizedLine)) {
            return true;
        }

        // Strategy 3: Check for partial matches with key SQL keywords from the line
        const sqlKeywords = ['SELECT', 'FROM', 'WHERE', 'JOIN', 'ON', 'AND', 'OR', 'CREATE', 'INSERT', 'UPDATE', 'DELETE'];
        const lineKeywords = sqlKeywords.filter(keyword => trimmedLineText.toUpperCase().includes(keyword));

        if (lineKeywords.length > 0) {
            // If the line contains SQL keywords, check if those keywords exist in the statement
            const hasMatchingKeywords = lineKeywords.every(keyword =>
                statement.text.toUpperCase().includes(keyword)
            );

            if (hasMatchingKeywords) {
                return true;
            }
        }

        // Strategy 4: Fallback to position-based matching (improved)
        let lineIndex = fullSqlText.indexOf(trimmedLineText);
        if (lineIndex === -1) {
            lineIndex = fullSqlText.replace(/\s+/g, ' ').indexOf(normalizedLine);
        }

        if (lineIndex !== -1) {
            const inRange = lineIndex >= statement.startPosition && lineIndex < statement.endPosition;
            return inRange;
        }

        return false;
    }

    // Extract left-side expressions from a single line of SQL
    function extractLeftSideExpressionsFromLine(lineText) {
        const leftSideExpressions = [];

        // Split the line by common logical operators while preserving the structure
        const conditions = splitLineConditions(lineText);

        conditions.forEach(condition => {
            // Check if condition contains comparison operators (including regex operators)
            if (/[=<>~]|BETWEEN/i.test(condition)) {
                // Parse the comparison and extract only the left side
                const leftSides = parseComparisonExpression(condition);
                leftSideExpressions.push(...leftSides);
            }
        });

        return leftSideExpressions;
    }

    // Split a line into individual conditions, similar to splitWhereConditions but for single lines
    function splitLineConditions(lineText) {
        const conditions = [];
        let current = '';
        let parenDepth = 0;
        let inQuotes = false;
        let quoteChar = '';

        for (let i = 0; i < lineText.length; i++) {
            const char = lineText[i];
            const nextChars = lineText.substr(i, 4).toUpperCase();

            // Handle quotes
            if ((char === '"' || char === "'") && !inQuotes) {
                inQuotes = true;
                quoteChar = char;
                current += char;
            } else if (char === quoteChar && inQuotes) {
                inQuotes = false;
                quoteChar = '';
                current += char;
            } else if (inQuotes) {
                current += char;
            } else {
                // Handle parentheses
                if (char === '(') {
                    parenDepth++;
                    current += char;
                } else if (char === ')') {
                    parenDepth--;
                    current += char;
                } else if (parenDepth === 0 && (nextChars === 'AND ' || nextChars === 'OR ')) {
                    // Found AND/OR at top level
                    if (current.trim()) {
                        conditions.push(current.trim());
                    }
                    current = '';
                    i += 3; // Skip past 'AND' or 'OR '
                } else {
                    current += char;
                }
            }
        }

        // Add the last condition
        if (current.trim()) {
            conditions.push(current.trim());
        }

        // If no conditions were found, return the entire line as a single condition
        if (conditions.length === 0 && lineText.trim()) {
            conditions.push(lineText.trim());
        }

        return conditions;
    }


    // Create summary panel
    function createSummaryPanel() {
        if (panel) {
            panel.remove();
        }

        panel = document.createElement('div');
        panel.className = 'unsafe-function-panel';
        panel.style.display = panelVisible ? 'block' : 'none';

        const header = document.createElement('div');
        header.className = 'unsafe-function-panel-header';
        header.innerHTML = `
            <span>Unsafe Functions</span>
            <button class="close-btn" onclick="this.parentElement.parentElement.style.display='none'">×</button>
        `;

        const content = document.createElement('div');
        content.className = 'unsafe-function-panel-content';

        const hasDetections = Object.keys(detectedFunctions).length > 0;

        if (!hasDetections) {
            content.innerHTML = '<div class="no-unsafe-functions">No unsafe functions detected in SQL queries.</div>';
        } else {
            Object.entries(detectedFunctions).forEach(([category, functions]) => {
                const categoryDiv = document.createElement('div');
                categoryDiv.className = 'unsafe-function-category';

                const titleDiv = document.createElement('div');
                titleDiv.className = 'unsafe-function-category-title';
                titleDiv.textContent = `${category} (${functions.length})`;
                categoryDiv.appendChild(titleDiv);

                functions.forEach(func => {
                    const itemDiv = document.createElement('div');
                    itemDiv.className = 'unsafe-function-item';
                    itemDiv.innerHTML = `
                        <strong>${func.function}</strong> at line ${func.line}:${func.column}<br>
                        <small>${func.context}</small>
                    `;
                    categoryDiv.appendChild(itemDiv);
                });

                content.appendChild(categoryDiv);
            });
        }

        panel.appendChild(header);
        panel.appendChild(content);
        document.body.appendChild(panel);

        // Set up click handlers for jump-to-line functionality (non-intrusive addition)
        setupSummaryClickHandlers();
    }

    // Create toggle button
    function createToggleButton() {
        if (toggleBtn) {
            toggleBtn.remove();
        }

        toggleBtn = document.createElement('button');
        toggleBtn.className = 'toggle-btn';
        toggleBtn.textContent = 'Unsafe Functions Detector';
        toggleBtn.title = 'Built by ARTS DE'
        toggleBtn.onclick = () => {
            panelVisible = !panelVisible;
            if (panel) {
                panel.style.display = panelVisible ? 'block' : 'none';
            }
            if (panelVisible) {
                scanForUnsafeFunctions();
            }
        };
        document.body.appendChild(toggleBtn);
    }

    // Extract SQL text from CodeMirror element, excluding gutter content
    function extractSQLText(codeMirrorElement) {
        // Get all CodeMirror-line elements directly from the original element
        const lines = codeMirrorElement.querySelectorAll('.CodeMirror-line');

        // Extract text from each line and join with newlines to preserve line structure
        const lineTexts = [];
        lines.forEach(line => {
            const lineText = line.textContent || line.innerText || '';
            lineTexts.push(lineText);
        });

        // Join lines with newline characters to maintain proper word boundaries
        return lineTexts.join('\n');
    }


    // Clear existing CSS-only highlights
    function clearCSSHighlights() {
        // Remove existing highlight stylesheet
        if (highlightStyleSheet) {
            highlightStyleSheet.remove();
            highlightStyleSheet = null;
        }

        // Remove ALL highlight classes and data attributes from ALL CodeMirror lines
        const allLines = document.querySelectorAll('.CodeMirror-line');
        allLines.forEach(line => {
            line.classList.remove('has-unsafe-function');
            line.removeAttribute('data-unsafe-function');
        });

        // Remove data attributes from CodeMirror elements
        const codeMirrorElements = document.querySelectorAll('.CodeMirror-scroll');
        codeMirrorElements.forEach(element => {
            element.removeAttribute('data-unsafe-functions');
        });

        // Force remove any remaining highlight styles by creating an empty stylesheet
        const cleanupStyleSheet = document.createElement('style');
        cleanupStyleSheet.id = 'unsafe-function-cleanup';
        cleanupStyleSheet.textContent = `
            .CodeMirror-line {
                background-color: transparent !important;
                border-left: none !important;
                padding-left: 0 !important;
            }
            .CodeMirror-line::after {
                display: none !important;
            }
        `;
        document.head.appendChild(cleanupStyleSheet);

        // Remove cleanup stylesheet after a brief moment
        setTimeout(() => {
            if (cleanupStyleSheet && cleanupStyleSheet.parentNode) {
                cleanupStyleSheet.remove();
            }
        }, 100);
    }

    // Apply robust highlighting with persistence and text selectability
    function applyCSSOnlyHighlighting(codeMirrorElement, elementIndex) {
        const fullSqlText = extractSQLText(codeMirrorElement);
        if (!fullSqlText) return;

        ////console.log('Full SQL Text:', fullSqlText.substring(0, 200) + '...'); // Debug log

        // Use the SAME logic as detectUnsafeFunctions to ensure consistency
        const detectedFunctions = detectUnsafeFunctions(fullSqlText, codeMirrorElement);

        ////console.log('Detection results:', detectedFunctions); // Debug log

        // If no unsafe functions detected, don't highlight anything
        if (Object.keys(detectedFunctions).length === 0) {
            ////console.log('No unsafe functions detected - skipping highlighting'); // Debug log
            return;
        }

        // Find all text nodes within CodeMirror lines
        const lines = codeMirrorElement.querySelectorAll('.CodeMirror-line');

        // Clear previous highlight tracking for this element
        highlightedLines.clear();

        // Split SQL into statements to get statement-specific clause ranges
        const sqlStatements = splitSQLStatements(fullSqlText);
        ////console.log(`Highlighting: Split SQL into ${sqlStatements.length} statements`); // Debug log

        // Directly highlight lines based on detection results
        Object.entries(detectedFunctions).forEach(([category, functions]) => {
            functions.forEach(func => {
                ////console.log(`Highlighting function: ${func.function} at line ${func.line} (statement ${func.statementIndex + 1})`); // Debug log

                // Find the specific line by line number (func.line is 1-based, array is 0-based)
                const targetLineIndex = func.line - 1;
                if (targetLineIndex >= 0 && targetLineIndex < lines.length) {
                    const line = lines[targetLineIndex];
                    const lineText = line.textContent || '';

                    // Get the statement this function belongs to
                    const statement = sqlStatements[func.statementIndex];
                    if (!statement) {
                        ////console.log(`Statement ${func.statementIndex} not found, skipping highlight`); // Debug log
                        return;
                    }

                    // Verify this line contains the function and is in relevant clause of the correct statement
                    const cleanedLineText = removeComments(lineText);
                    const functionRegex = createFunctionRegex(func.function);

                    // Extract complete WHERE and JOIN ON clauses for this statement
                    const whereClauses = extractCompleteWhereClauses(statement.text);
                    const joinOnClauses = extractCompleteJoinOnClauses(statement.text);

                    if (functionRegex.test(cleanedLineText) &&
                        isLineInStatement(lineText, fullSqlText, statement) &&
                        isLineInRelevantClause(lineText, statement.text, statement, whereClauses, joinOnClauses)) {

                        ////console.log(`Highlighting line ${func.line}: ${lineText.trim()} for function: ${func.function}`); // Debug log

                        // Apply both CSS class and inline styles for maximum persistence
                        line.classList.add('has-unsafe-function');

                        // Apply inline styles that are harder to override
                        line.style.setProperty('background-color', 'rgba(255, 235, 59, 0.3)', 'important');
                        line.style.setProperty('border-left', '3px solid #ff9800', 'important');
                        line.style.setProperty('padding-left', '5px', 'important');
                        line.style.setProperty('user-select', 'text', 'important');
                        line.style.setProperty('-webkit-user-select', 'text', 'important');
                        line.style.setProperty('-moz-user-select', 'text', 'important');
                        line.style.setProperty('-ms-user-select', 'text', 'important');

                        // Store multiple functions if they exist on the same line
                        const existingFunctions = line.getAttribute('data-unsafe-function') || '';
                        const functionList = existingFunctions ? existingFunctions.split(',') : [];
                        if (!functionList.includes(func.function)) {
                            functionList.push(func.function);
                            line.setAttribute('data-unsafe-function', functionList.join(','));
                        }

                        // Store highlight information for persistence tracking
                        const lineKey = `${elementIndex}-${func.line}`;
                        highlightedLines.set(lineKey, {
                            element: line,
                            functions: functionList,
                            lineNumber: func.line,
                            elementIndex: elementIndex
                        });
                    } else {
                        ////console.log(`Skipping highlight for line ${func.line}: function test=${functionRegex.test(cleanedLineText)}, inStatement=${isLineInStatement(lineText, fullSqlText, statement)}, inRelevantClause=${isLineInRelevantClause(lineText, statement.text, null)}`); // Debug log
                    }
                }
            });
        });

        // Generate CSS rules for highlighting
        generateHighlightCSS();

        // Start highlight protection if not already running
        startHighlightProtection();
    }

    // Note: findRelevantClauseRanges method removed - we now use semantic detection
    // instead of position-based range matching for better accuracy and maintainability

    // Check if a line is within a relevant clause (WHERE or JOIN ON) using content-based semantic detection
    function isLineInRelevantClause(lineText, fullSqlText, statement, whereClauses, joinOnClauses) {
        const cleanedLineText = removeComments(lineText.trim());
        if (!cleanedLineText) return false;

        // Method 1: Complete clause matching (more accurate)
        if (whereClauses && isLineInCompleteWhereClause(lineText, whereClauses)) {
            return true;
        }

        if (joinOnClauses && isLineInCompleteJoinOnClause(lineText, joinOnClauses)) {
            return true;
        }

        // Method 2: Fallback to semantic detection
        if (isWhereConditionLine(cleanedLineText)) {
            return true;
        }

        if (isJoinOnConditionLine(cleanedLineText)) {
            return true;
        }

        return false;
    }

    // Check if a line contains WHERE condition expressions
    function isWhereConditionLine(lineText) {
        const upperLineText = lineText.toUpperCase();

        // Skip if this looks like a SELECT statement or FROM clause
        if (upperLineText.includes('SELECT ') || upperLineText.includes('FROM ')) {
            // But allow if it's clearly a WHERE condition with SELECT in a subquery
            if (!upperLineText.includes('WHERE ')) {
                return false;
            }
        }

        // Skip if this is obviously a JOIN ON context
        if (isObviousJoinOnContext(lineText)) {
            return false;
        }

        // Skip if this is part of a CASE expression
        if (isCaseExpressionLine(lineText)) {
            return false;
        }

        // Skip if this is part of CREATE TABLE or other DDL statements
        if (isDDLContextLine(lineText)) {
            return false;
        }

        // Check for comparison operators
        const hasComparisonOperators = /[=<>!~]|BETWEEN|IN\s*\(|EXISTS\s*\(|LIKE|ILIKE|SIMILAR\s+TO/i.test(lineText);

        // Check for logical operators (but not in SELECT context)
        const hasLogicalOperators = /\b(AND|OR)\b/i.test(lineText) && !upperLineText.includes('SELECT ');

        // Check for WHERE keyword
        const hasWhereKeyword = /\bWHERE\b/i.test(lineText);

        // Check for common WHERE condition patterns
        const hasConditionPatterns = /\b(TRUNC|TO_DATE|EXTRACT|DATE_PART|CAST)\s*\(/i.test(lineText);

        const isWhereCondition = hasComparisonOperators || hasLogicalOperators || hasWhereKeyword || hasConditionPatterns;

        return isWhereCondition;
    }

    // Check if a line is obviously in JOIN ON context
    function isObviousJoinOnContext(lineText) {
        const upperLineText = lineText.toUpperCase();

        // Direct JOIN keywords with ON in the same line
        if (/\b(INNER\s+JOIN|LEFT\s+(?:OUTER\s+)?JOIN|RIGHT\s+(?:OUTER\s+)?JOIN|FULL\s+(?:OUTER\s+)?JOIN|CROSS\s+JOIN|JOIN)\b.*\bON\b/i.test(lineText)) {
            return true;
        }

        // Lines that start with JOIN keywords
        if (/^\s*(INNER\s+JOIN|LEFT\s+(?:OUTER\s+)?JOIN|RIGHT\s+(?:OUTER\s+)?JOIN|FULL\s+(?:OUTER\s+)?JOIN|CROSS\s+JOIN|JOIN)\b/i.test(lineText)) {
            return true;
        }

        // Lines that contain table.column = table.column patterns (typical JOIN conditions)
        // This pattern is more specific than general comparisons and strongly suggests JOIN context
        if (/\b\w+\.\w+\s*[=<>!]\s*\w+\.\w+\b/i.test(lineText)) {
            // Additional check: if the line also contains JOIN or ON keywords, it's definitely JOIN context
            if (/\b(JOIN|ON)\b/i.test(lineText)) {
                return true;
            }

            // Even without explicit JOIN/ON keywords, table.column = table.column is typically JOIN
            // But be more conservative - only if it doesn't contain WHERE keyword
            if (!upperLineText.includes('WHERE')) {
                return true;
            }
        }

        return false;
    }

    // Check if a line is part of a CASE expression
    function isCaseExpressionLine(lineText) {
        const upperLineText = lineText.toUpperCase().trim();

        // Direct CASE keywords
        if (/\b(CASE|WHEN|THEN|ELSE|END)\b/i.test(lineText)) {
            return true;
        }

        // Lines that are likely part of CASE expression context
        // Look for patterns like "WHEN condition" or "AND condition" following CASE
        if (/^\s*(WHEN\s+|AND\s+|OR\s+).*[=<>!~]/i.test(lineText)) {
            return true;
        }

        // Lines with comparison operators that start with logical operators (likely CASE conditions)
        if (/^\s*(AND|OR)\s+.*[=<>!~]/i.test(lineText)) {
            return true;
        }

        return false;
    }

    // Check if a line is in DDL (Data Definition Language) context
    function isDDLContextLine(lineText) {
        const upperLineText = lineText.toUpperCase();

        // DDL statement keywords
        if (/\b(CREATE\s+TABLE|ALTER\s+TABLE|DROP\s+TABLE|CREATE\s+INDEX|CREATE\s+VIEW)\b/i.test(lineText)) {
            return true;
        }

        // Column definition patterns in CREATE TABLE
        if (/\b(DISTKEY|SORTKEY|ENCODE|NOT\s+NULL|PRIMARY\s+KEY|FOREIGN\s+KEY)\b/i.test(lineText)) {
            return true;
        }

        // Table constraint patterns
        if (/^\s*--.*\b(table|column|constraint)\b/i.test(lineText)) {
            return true;
        }

        return false;
    }

    // Check if a line contains JOIN ON condition expressions
    function isJoinOnConditionLine(lineText) {
        const upperLineText = lineText.toUpperCase();

        // Check for JOIN ON keywords
        const hasJoinOnKeywords = /\b(JOIN|ON)\b/i.test(lineText);

        // Check for table join patterns (table.column = table.column)
        const hasJoinPatterns = /\w+\.\w+\s*[=<>!]\s*\w+\.\w+/i.test(lineText);

        // Check for comparison operators in JOIN context
        const hasComparisonInJoin = hasJoinOnKeywords && /[=<>!~]/i.test(lineText);

        const isJoinOnCondition = hasJoinOnKeywords || hasJoinPatterns || hasComparisonInJoin;

        return isJoinOnCondition;
    }


    // Generate dynamic CSS rules for highlighting unsafe functions
    function generateHighlightCSS() {
        // Remove existing highlight stylesheet
        if (highlightStyleSheet) {
            highlightStyleSheet.remove();
        }

        // Create new stylesheet for highlights
        highlightStyleSheet = document.createElement('style');
        highlightStyleSheet.id = 'unsafe-function-highlights';

        // Create CSS rules that highlight lines containing unsafe functions
        let cssRules = `
            .CodeMirror-line.has-unsafe-function {
                background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(251, 191, 36, 0.1) 100%) !important;
                border-left: 4px solid #f59e0b !important;
                padding-left: 8px !important;
                user-select: text !important;
                -webkit-user-select: text !important;
                -moz-user-select: text !important;
                -ms-user-select: text !important;
                position: relative !important;
                transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
            }

            .CodeMirror-line.has-unsafe-function:hover {
                background: linear-gradient(135deg, rgba(245, 158, 11, 0.25) 0%, rgba(251, 191, 36, 0.2) 100%) !important;
                border-left-color: #d97706 !important;
                box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.06) !important;
            }
        `;

        // Add specific highlighting for each unsafe function type
        ALL_UNSAFE_FUNCTIONS.forEach(functionName => {
            const escapedName = functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            cssRules += `
                .CodeMirror-line[data-unsafe-function*="${functionName}"] {
                    position: relative;
                }

                .CodeMirror-line[data-unsafe-function*="${functionName}"]::after {
                    content: "⚠️ ${functionName}";
                    position: absolute;
                    right: 12px;
                    top: 50%;
                    transform: translateY(-50%);
                    background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
                    color: white;
                    padding: 3px 8px;
                    border-radius: 6px;
                    font-size: 11px;
                    font-weight: 600;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                    pointer-events: none;
                    z-index: 10;
                    box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.06);
                    opacity: 0.9;
                    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
                }

                .CodeMirror-line[data-unsafe-function*="${functionName}"]:hover::after {
                    opacity: 1;
                    transform: translateY(-50%) scale(1.05);
                }
            `;
        });

        highlightStyleSheet.textContent = cssRules;
        document.head.appendChild(highlightStyleSheet);
    }

    // Main scanning function
    function scanForUnsafeFunctions() {
        if (isScanning) {
            return; // Prevent concurrent scans
        }

        isScanning = true;
        detectedFunctions = {};

        try {
            // Clear existing CSS highlights
            clearCSSHighlights();

            // Find all CodeMirror-scroll elements (top-level containers only to avoid duplicate scanning)
            const codeMirrorElements = document.querySelectorAll('.CodeMirror-scroll');

            codeMirrorElements.forEach((element, index) => {
                const sqlText = extractSQLText(element);
                if (sqlText && sqlText.trim()) {
                    // Pass the CodeMirror element to detectUnsafeFunctions for accurate line numbers
                    const detected = detectUnsafeFunctions(sqlText, element);

                    // Merge detected functions with comprehensive deduplication
                    Object.entries(detected).forEach(([category, functions]) => {
                        if (!detectedFunctions[category]) {
                            detectedFunctions[category] = [];
                        }

                        // Add all functions from this element
                        functions.forEach(func => {
                            detectedFunctions[category].push(func);
                        });
                    });

                    // Apply CSS-only highlighting to WHERE/AND clauses only
                    applyCSSOnlyHighlighting(element, index);
                }
            });

            // Apply comprehensive deduplication to the final results
            detectedFunctions = deduplicateDetections(detectedFunctions);

            createSummaryPanel();
        } finally {
            isScanning = false;
        }
    }

    // Highlight protection system - restores lost highlights
    function startHighlightProtection() {
        if (highlightProtectionInterval) {
            return; // Already running
        }

        highlightProtectionInterval = setInterval(() => {
            if (!panelVisible || highlightedLines.size === 0) {
                return;
            }

            // Check each highlighted line and restore if needed
            highlightedLines.forEach((highlightInfo, lineKey) => {
                const { element, functions, lineNumber, elementIndex } = highlightInfo;

                // Check if element still exists in DOM
                if (!document.contains(element)) {
                    highlightedLines.delete(lineKey);
                    return;
                }

                // Check if highlight styles are still applied
                const hasClass = element.classList.contains('has-unsafe-function');
                const hasInlineStyle = element.style.backgroundColor &&
                    element.style.backgroundColor.includes('255, 235, 59');

                if (!hasClass || !hasInlineStyle) {
                    // Restore CSS class
                    element.classList.add('has-unsafe-function');

                    // Restore inline styles
                    element.style.setProperty('background-color', 'rgba(255, 235, 59, 0.3)', 'important');
                    element.style.setProperty('border-left', '3px solid #ff9800', 'important');
                    element.style.setProperty('padding-left', '5px', 'important');
                    element.style.setProperty('user-select', 'text', 'important');
                    element.style.setProperty('-webkit-user-select', 'text', 'important');
                    element.style.setProperty('-moz-user-select', 'text', 'important');
                    element.style.setProperty('-ms-user-select', 'text', 'important');

                    // Restore data attribute
                    element.setAttribute('data-unsafe-function', functions.join(','));
                }
            });
        }, 500); // Check every 500ms
    }


    // Handle click events on CodeMirror lines to prevent highlight loss
    function setupClickProtection() {
        document.addEventListener('click', function(event) {
            const clickedLine = event.target.closest('.CodeMirror-line');
            if (clickedLine && clickedLine.classList.contains('has-unsafe-function')) {
                // Schedule highlight restoration after a short delay
                setTimeout(() => {
                    if (clickedLine.classList.contains('has-unsafe-function')) {
                        // Ensure inline styles are still applied
                        clickedLine.style.setProperty('background-color', 'rgba(255, 235, 59, 0.3)', 'important');
                        clickedLine.style.setProperty('border-left', '3px solid #ff9800', 'important');
                        clickedLine.style.setProperty('padding-left', '5px', 'important');
                        clickedLine.style.setProperty('user-select', 'text', 'important');
                        clickedLine.style.setProperty('-webkit-user-select', 'text', 'important');
                        clickedLine.style.setProperty('-moz-user-select', 'text', 'important');
                        clickedLine.style.setProperty('-ms-user-select', 'text', 'important');
                    }
                }, 50);
            }
        }, true); // Use capture phase to catch events early
    }

    // Debounced scan function
    function debouncedScan() {
        if (scanTimeout) {
            clearTimeout(scanTimeout);
        }
        scanTimeout = setTimeout(() => {
            if (panelVisible && !isScanning) {
                scanForUnsafeFunctions();
            }
        }, 300);
    }

    // Initialize the script
    function initialize() {
        createToggleButton();
        setupClickProtection();
    }

    // Wait for page to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();