|
| 1 | +// --- Global Variables --- |
| 2 | +let db = null; // To hold the database instance |
| 3 | +let editor; // To hold the CodeMirror editor instance |
| 4 | + |
| 5 | +// --- DOM Element References --- |
| 6 | +const dbFileInput = document.getElementById('dbFile'); |
| 7 | +const messageElem = document.getElementById('message'); |
| 8 | +const tableListElem = document.getElementById('table-list'); |
| 9 | +const executeQueryBtn = document.getElementById('execute-query'); |
| 10 | +const resultsElem = document.getElementById('results'); |
| 11 | +const dbViewerContainer = document.getElementById('db-viewer-container'); |
| 12 | +const queryStatusElem = document.getElementById('query-status'); |
| 13 | +const themeToggle = document.getElementById('theme-toggle'); |
| 14 | + |
| 15 | +// --- Initialization on DOM Load --- |
| 16 | +document.addEventListener('DOMContentLoaded', () => { |
| 17 | + // Initialize the CodeMirror SQL editor |
| 18 | + editor = CodeMirror(document.getElementById('sqlQueryContainer'), { |
| 19 | + mode: 'text/x-sql', |
| 20 | + lineNumbers: true, |
| 21 | + theme: 'default', |
| 22 | + value: '-- Load a database and click a table to start exploring', |
| 23 | + readOnly: 'nocursor' // Initially disable the editor |
| 24 | + }); |
| 25 | + |
| 26 | + // Set up the theme toggler |
| 27 | + themeToggle.addEventListener('click', toggleTheme); |
| 28 | + |
| 29 | + // Set initial theme based on user's system preference |
| 30 | + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { |
| 31 | + document.body.classList.add('dark-mode'); |
| 32 | + updateTheme(); |
| 33 | + } |
| 34 | +}); |
| 35 | + |
| 36 | + |
| 37 | +// --- Event Listeners --- |
| 38 | +dbFileInput.addEventListener('change', handleFileSelect); |
| 39 | +executeQueryBtn.addEventListener('click', executeQuery); |
| 40 | + |
| 41 | + |
| 42 | +// --- Core Functions --- |
| 43 | + |
| 44 | +/** |
| 45 | + * Toggles the color theme between light and dark mode. |
| 46 | + */ |
| 47 | +function toggleTheme() { |
| 48 | + document.body.classList.toggle('dark-mode'); |
| 49 | + updateTheme(); |
| 50 | +} |
| 51 | + |
| 52 | +/** |
| 53 | + * Updates UI elements based on the current theme (e.g., editor, icon). |
| 54 | + */ |
| 55 | +function updateTheme() { |
| 56 | + const isDarkMode = document.body.classList.contains('dark-mode'); |
| 57 | + editor.setOption('theme', isDarkMode ? 'material-darker' : 'default'); |
| 58 | + themeToggle.innerHTML = isDarkMode ? '<i class="fa-solid fa-sun"></i>' : '<i class="fa-solid fa-moon"></i>'; |
| 59 | +} |
| 60 | + |
| 61 | +/** |
| 62 | + * Handles the database file selection, loading, and initialization. |
| 63 | + */ |
| 64 | +async function handleFileSelect(event) { |
| 65 | + const file = event.target.files[0]; |
| 66 | + if (!file) return; |
| 67 | + |
| 68 | + resetUI(); |
| 69 | + messageElem.textContent = `Loading ${file.name}...`; |
| 70 | + |
| 71 | + try { |
| 72 | + // Initialize sql.js library, pointing to the wasm file |
| 73 | + const SQL = await initSqlJs({ locateFile: () => `sql-wasm.wasm` }); |
| 74 | + |
| 75 | + const reader = new FileReader(); |
| 76 | + reader.onload = async (e) => { |
| 77 | + try { |
| 78 | + const Uints = new Uint8Array(e.target.result); |
| 79 | + db = new SQL.Database(Uints); |
| 80 | + |
| 81 | + // Enable UI elements now that the database is loaded |
| 82 | + editor.setOption('readOnly', false); |
| 83 | + executeQueryBtn.disabled = false; |
| 84 | + |
| 85 | + messageElem.textContent = `${file.name} loaded successfully!`; |
| 86 | + messageElem.style.color = 'var(--color-success)'; |
| 87 | + |
| 88 | + loadTables(); |
| 89 | + |
| 90 | + } catch (err) { |
| 91 | + showError(`Error loading database: ${err.message}`); |
| 92 | + } |
| 93 | + }; |
| 94 | + reader.readAsArrayBuffer(file); |
| 95 | + |
| 96 | + } catch (err) { |
| 97 | + showError(`Failed to initialize SQL.js: ${err.message}. Ensure sql-wasm.wasm is in the same folder.`); |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +/** |
| 102 | + * Fetches and displays the list of tables from the loaded database. |
| 103 | + */ |
| 104 | +function loadTables() { |
| 105 | + if (!db) return; |
| 106 | + try { |
| 107 | + const stmt = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"); |
| 108 | + const tables = []; |
| 109 | + while (stmt.step()) { |
| 110 | + tables.push(stmt.get()[0]); |
| 111 | + } |
| 112 | + stmt.free(); |
| 113 | + |
| 114 | + tableListElem.innerHTML = ''; |
| 115 | + if (tables.length === 0) { |
| 116 | + tableListElem.innerHTML = '<li class="no-data">No tables found.</li>'; |
| 117 | + return; |
| 118 | + } |
| 119 | + |
| 120 | + tables.forEach(tableName => { |
| 121 | + const li = document.createElement('li'); |
| 122 | + li.innerHTML = `<i class="fa-solid fa-table"></i>${tableName}`; |
| 123 | + li.addEventListener('click', (e) => { |
| 124 | + document.querySelectorAll('#table-list li').forEach(item => item.classList.remove('active')); |
| 125 | + e.currentTarget.classList.add('active'); |
| 126 | + editor.setValue(`SELECT * FROM "${tableName}" LIMIT 100;`); |
| 127 | + executeQuery(); |
| 128 | + }); |
| 129 | + tableListElem.appendChild(li); |
| 130 | + }); |
| 131 | + |
| 132 | + // Automatically click the first table to show its contents |
| 133 | + if (tables.length > 0) { |
| 134 | + tableListElem.firstElementChild.click(); |
| 135 | + } |
| 136 | + |
| 137 | + } catch (error) { |
| 138 | + showError(`Error loading tables: ${error.message}`); |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +/** |
| 143 | + * Executes the SQL query from the editor and displays the results. |
| 144 | + */ |
| 145 | +function executeQuery() { |
| 146 | + if (!db) { |
| 147 | + showError("No database loaded."); |
| 148 | + return; |
| 149 | + } |
| 150 | + |
| 151 | + const query = editor.getValue().trim(); |
| 152 | + if (!query) { |
| 153 | + showError("Please enter a SQL query."); |
| 154 | + return; |
| 155 | + } |
| 156 | + |
| 157 | + resultsElem.innerHTML = ''; |
| 158 | + queryStatusElem.textContent = 'Executing...'; |
| 159 | + |
| 160 | + // Use a small timeout to allow the UI to update before a long query blocks the main thread |
| 161 | + setTimeout(() => { |
| 162 | + try { |
| 163 | + const startTime = performance.now(); |
| 164 | + const res = db.exec(query); |
| 165 | + const endTime = performance.now(); |
| 166 | + const duration = (endTime - startTime).toFixed(2); |
| 167 | + |
| 168 | + if (!res.length) { |
| 169 | + resultsElem.innerHTML = '<p class="no-data">Query executed successfully, but returned no data.</p>'; |
| 170 | + queryStatusElem.textContent = `Query took ${duration} ms`; |
| 171 | + return; |
| 172 | + } |
| 173 | + |
| 174 | + const { columns, values } = res[0]; |
| 175 | + resultsElem.innerHTML = createTableHTML(columns, values); |
| 176 | + queryStatusElem.textContent = `${values.length} rows returned in ${duration} ms.`; |
| 177 | + |
| 178 | + } catch (error) { |
| 179 | + resultsElem.innerHTML = `<p class="no-data" style="color: var(--color-danger);">${error.message}</p>`; |
| 180 | + queryStatusElem.textContent = `Error executing query.`; |
| 181 | + } |
| 182 | + }, 10); |
| 183 | +} |
| 184 | + |
| 185 | + |
| 186 | +// --- UI Helper Functions --- |
| 187 | + |
| 188 | +/** |
| 189 | + * Resets the UI to its initial state when a new file is loaded. |
| 190 | + */ |
| 191 | +function resetUI() { |
| 192 | + db = null; |
| 193 | + tableListElem.innerHTML = '<li class="no-data">Load a database to see tables</li>'; |
| 194 | + resultsElem.innerHTML = '<p class="no-data">Results will appear here</p>'; |
| 195 | + queryStatusElem.textContent = ''; |
| 196 | + editor.setValue('-- Load a database and click a table to start exploring'); |
| 197 | + editor.setOption('readOnly', 'nocursor'); |
| 198 | + executeQueryBtn.disabled = true; |
| 199 | +} |
| 200 | + |
| 201 | +/** |
| 202 | + * Displays an error message to the user. |
| 203 | + * @param {string} message The error message to display. |
| 204 | + */ |
| 205 | +function showError(message) { |
| 206 | + messageElem.textContent = message; |
| 207 | + messageElem.style.color = 'var(--color-danger)'; |
| 208 | + console.error(message); |
| 209 | +} |
| 210 | + |
| 211 | +/** |
| 212 | + * Generates an HTML table from SQL query results. |
| 213 | + * @param {string[]} columns Array of column names. |
| 214 | + * @param {Array<any[]>} values 2D array of row values. |
| 215 | + * @returns {string} The HTML string for the results table. |
| 216 | + */ |
| 217 | +function createTableHTML(columns, values) { |
| 218 | + let tableHtml = '<table><thead><tr>'; |
| 219 | + columns.forEach(col => tableHtml += `<th>${col}</th>`); |
| 220 | + tableHtml += '</tr></thead><tbody>'; |
| 221 | + values.forEach(row => { |
| 222 | + tableHtml += '<tr>'; |
| 223 | + row.forEach(cell => tableHtml += `<td>${cell !== null ? cell : 'NULL'}</td>`); |
| 224 | + tableHtml += '</tr>'; |
| 225 | + }); |
| 226 | + tableHtml += '</tbody></table>'; |
| 227 | + return tableHtml; |
| 228 | +} |
0 commit comments