DEV Community

Query Filter
Query Filter

Posted on

form9


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Offline SQL Formatter</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f5f5f5;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            text-align: center;
        }
        .editor-container {
            display: flex;
            gap: 20px;
            margin-bottom: 20px;
        }
        @media (max-width: 768px) {
            .editor-container {
                flex-direction: column;
            }
        }
        .editor-panel {
            flex: 1;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            color: #555;
        }
        textarea {
            width: 100%;
            height: 300px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-family: 'Courier New', monospace;
            font-size: 14px;
            resize: vertical;
        }
        .controls {
            margin: 20px 0;
            text-align: center;
        }
        button {
            background-color: #007cba;
            color: white;
            border: none;
            padding: 10px 20px;
            margin: 0 5px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        button:hover {
            background-color: #005a87;
        }
        .options {
            margin: 10px 0;
            text-align: center;
        }
        select {
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            margin: 0 5px;
        }
        .status {
            text-align: center;
            margin: 10px 0;
            color: #666;
            font-style: italic;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Offline SQL Formatter</h1>

        <div class="options">
            <label for="dialect">SQL Dialect:</label>
            <select id="dialect">
                <option value="sql">Standard SQL</option>
                <option value="mysql">MySQL</option>
                <option value="postgresql">PostgreSQL</option>
            </select>

            <label for="indent">Indent:</label>
            <select id="indent">
                <option value="2">2 spaces</option>
                <option value="4" selected>4 spaces</option>
                <option value="tab">Tabs</option>
            </select>
        </div>

        <div class="editor-container">
            <div class="editor-panel">
                <label for="inputSQL">Input SQL:</label>
                <textarea id="inputSQL" placeholder="Enter your SQL query here..."></textarea>
            </div>
            <div class="editor-panel">
                <label for="outputSQL">Formatted SQL:</label>
                <textarea id="outputSQL" readonly placeholder="Formatted SQL will appear here..."></textarea>
            </div>
        </div>

        <div class="controls">
            <button id="formatBtn">Format SQL</button>
            <button id="clearBtn">Clear</button>
            <button id="copyBtn">Copy Formatted</button>
        </div>

        <div class="status" id="status">Ready</div>
    </div>

    <script>
        // Robust SQL Formatter with proper formatting
        const SQLFormatter = (function() {
            'use strict';

            const reservedWords = new Set([
                'SELECT', 'FROM', 'WHERE', 'GROUP', 'BY', 'HAVING', 'ORDER', 'LIMIT', 'UNION', 'ALL',
                'INSERT', 'INTO', 'UPDATE', 'DELETE', 'CREATE', 'TABLE', 'ALTER', 'DROP', 'VALUES',
                'SET', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS', 'NULL', 'JOIN', 'LEFT',
                'RIGHT', 'INNER', 'OUTER', 'ON', 'AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
                'EXISTS', 'DISTINCT', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'ASC', 'DESC'
            ]);

            const topLevelKeywords = new Set(['SELECT', 'FROM', 'WHERE', 'GROUP BY', 'HAVING', 'ORDER BY', 'LIMIT', 'UNION', 'INSERT', 'UPDATE', 'DELETE']);
            const newlineKeywords = new Set(['AND', 'OR', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'OUTER JOIN', 'ON']);

            function tokenize(sql) {
                const tokens = [];
                let i = 0;
                const n = sql.length;

                while (i < n) {
                    const char = sql[i];

                    // Whitespace
                    if (/\s/.test(char)) {
                        let whitespace = '';
                        while (i < n && /\s/.test(sql[i])) {
                            whitespace += sql[i];
                            i++;
                        }
                        if (whitespace.includes('\n')) {
                            tokens.push({ type: 'NEWLINE', value: '\n' });
                        } else if (whitespace) {
                            tokens.push({ type: 'WHITESPACE', value: whitespace });
                        }
                        continue;
                    }

                    // Comments
                    if (char === '-' && sql[i + 1] === '-') {
                        let comment = '';
                        while (i < n && sql[i] !== '\n') {
                            comment += sql[i];
                            i++;
                        }
                        tokens.push({ type: 'COMMENT', value: comment });
                        continue;
                    }

                    if (char === '/' && sql[i + 1] === '*') {
                        let comment = '';
                        while (i < n && !(sql[i] === '*' && sql[i + 1] === '/')) {
                            comment += sql[i];
                            i++;
                        }
                        if (i < n - 1) {
                            comment += sql[i] + sql[i + 1];
                            i += 2;
                        }
                        tokens.push({ type: 'COMMENT', value: comment });
                        continue;
                    }

                    // Strings
                    if (char === "'" || char === '"' || char === '`') {
                        let str = char;
                        i++;
                        while (i < n && sql[i] !== char) {
                            if (sql[i] === '\\') {
                                str += sql[i];
                                i++;
                                if (i < n) {
                                    str += sql[i];
                                    i++;
                                }
                            } else {
                                str += sql[i];
                                i++;
                            }
                        }
                        if (i < n) {
                            str += sql[i];
                            i++;
                        }
                        tokens.push({ type: 'STRING', value: str });
                        continue;
                    }

                    // Numbers
                    if (/\d/.test(char)) {
                        let num = '';
                        while (i < n && /[\d.]/.test(sql[i])) {
                            num += sql[i];
                            i++;
                        }
                        tokens.push({ type: 'NUMBER', value: num });
                        continue;
                    }

                    // Special characters
                    if ('(),;.=<>!+-*/%'.includes(char)) {
                        tokens.push({ type: 'SYMBOL', value: char });
                        i++;
                        continue;
                    }

                    // Words and identifiers
                    if (/[a-zA-Z_]/.test(char)) {
                        let word = '';
                        while (i < n && /[\w]/.test(sql[i])) {
                            word += sql[i];
                            i++;
                        }
                        const upperWord = word.toUpperCase();
                        if (reservedWords.has(upperWord)) {
                            tokens.push({ type: 'KEYWORD', value: word, upper: upperWord });
                        } else {
                            tokens.push({ type: 'IDENTIFIER', value: word });
                        }
                        continue;
                    }

                    // Unknown character
                    tokens.push({ type: 'SYMBOL', value: char });
                    i++;
                }

                return tokens;
            }

            function formatSQL(sql, options = {}) {
                const {
                    indent: indentStr = '    ',
                    uppercase = true
                } = options;

                const tokens = tokenize(sql);
                let output = '';
                let indentLevel = 0;
                let inSelectList = false;
                let needsIndent = true;

                for (let i = 0; i < tokens.length; i++) {
                    const token = tokens[i];
                    const prevToken = i > 0 ? tokens[i - 1] : null;
                    const nextToken = i < tokens.length - 1 ? tokens[i + 1] : null;

                    // Handle indentation
                    if (needsIndent && token.type !== 'NEWLINE' && token.type !== 'WHITESPACE') {
                        output += indentStr.repeat(indentLevel);
                        needsIndent = false;
                    }

                    switch (token.type) {
                        case 'NEWLINE':
                            output += '\n';
                            needsIndent = true;
                            break;

                        case 'WHITESPACE':
                            // Only add space if not at start of line and not before certain symbols
                            if (!needsIndent && nextToken && 
                                ![')', ',', ';', '.'].includes(nextToken.value)) {
                                output += ' ';
                            }
                            break;

                        case 'COMMENT':
                            output += token.value;
                            if (token.value.includes('\n')) {
                                needsIndent = true;
                            }
                            break;

                        case 'STRING':
                        case 'NUMBER':
                            output += token.value;
                            break;

                        case 'IDENTIFIER':
                            output += token.value;
                            break;

                        case 'KEYWORD':
                            const keyword = uppercase ? token.upper : token.value;
                            output += keyword;

                            // Handle top-level keywords
                            if (topLevelKeywords.has(keyword)) {
                                if (keyword === 'SELECT') {
                                    inSelectList = true;
                                    output += ' ';
                                } else if (keyword === 'FROM') {
                                    inSelectList = false;
                                    output += '\n';
                                    needsIndent = true;
                                } else {
                                    output += '\n';
                                    needsIndent = true;
                                }
                            } else if (newlineKeywords.has(keyword)) {
                                output += '\n';
                                needsIndent = true;
                            } else {
                                output += ' ';
                            }
                            break;

                        case 'SYMBOL':
                            switch (token.value) {
                                case '(':
                                    output += token.value;
                                    if (!inSelectList) {
                                        indentLevel++;
                                        output += '\n';
                                        needsIndent = true;
                                    }
                                    break;
                                case ')':
                                    if (!inSelectList) {
                                        indentLevel = Math.max(0, indentLevel - 1);
                                        output += '\n' + indentStr.repeat(indentLevel) + token.value;
                                    } else {
                                        output += token.value;
                                    }
                                    break;
                                case ',':
                                    output += token.value;
                                    if (inSelectList) {
                                        output += ' ';
                                    } else {
                                        output += '\n';
                                        needsIndent = true;
                                    }
                                    break;
                                case '.':
                                    output += token.value;
                                    break;
                                case ';':
                                    output += token.value + '\n\n';
                                    indentLevel = 0;
                                    needsIndent = true;
                                    break;
                                default:
                                    output += ' ' + token.value + ' ';
                            }
                            break;
                    }
                }

                return output.trim();
            }

            return {
                format: formatSQL
            };
        })();

        // DOM handling
        document.addEventListener('DOMContentLoaded', function() {
            const inputSQL = document.getElementById('inputSQL');
            const outputSQL = document.getElementById('outputSQL');
            const formatBtn = document.getElementById('formatBtn');
            const clearBtn = document.getElementById('clearBtn');
            const copyBtn = document.getElementById('copyBtn');
            const dialectSelect = document.getElementById('dialect');
            const indentSelect = document.getElementById('indent');
            const status = document.getElementById('status');

            function formatSQL() {
                const sql = inputSQL.value.trim();
                if (!sql) {
                    outputSQL.value = '';
                    status.textContent = 'Please enter SQL to format';
                    return;
                }

                try {
                    const indent = indentSelect.value === 'tab' ? '\t' : ' '.repeat(parseInt(indentSelect.value));
                    const formatted = SQLFormatter.format(sql, {
                        indent: indent,
                        uppercase: true
                    });

                    outputSQL.value = formatted;
                    status.textContent = 'SQL formatted successfully';
                } catch (error) {
                    outputSQL.value = 'Error formatting SQL: ' + error.message;
                    status.textContent = 'Formatting failed';
                }
            }

            function clearAll() {
                inputSQL.value = '';
                outputSQL.value = '';
                status.textContent = 'Cleared';
            }

            function copyFormatted() {
                if (!outputSQL.value.trim()) {
                    status.textContent = 'No formatted SQL to copy';
                    return;
                }

                outputSQL.select();
                outputSQL.setSelectionRange(0, 99999);

                try {
                    navigator.clipboard.writeText(outputSQL.value);
                    status.textContent = 'Formatted SQL copied to clipboard';
                } catch (err) {
                    document.execCommand('copy');
                    status.textContent = 'Formatted SQL copied to clipboard';
                }
            }

            formatBtn.addEventListener('click', formatSQL);
            clearBtn.addEventListener('click', clearAll);
            copyBtn.addEventListener('click', copyFormatted);

            inputSQL.addEventListener('keydown', function(e) {
                if (e.ctrlKey && e.key === 'Enter') {
                    formatSQL();
                }
            });

            // Test SQL
            inputSQL.value = `SELECT c.COLUMN, t.ANOTHER_COLUMN, COUNT(*) as total_count FROM table1 c JOIN table2 t ON c.id = t.ref_id WHERE c.status = 'ACTIVE' AND t.value > 100 GROUP BY c.COLUMN, t.ANOTHER_COLUMN ORDER BY total_count DESC`;
        });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)