"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ChevronLeft, ChevronRight, Database, FileUp, Play, RotateCcw, Rows3, Table as TableIcon, Terminal, Upload, XCircle, } from "lucide-react"; import { Footer } from "./components/Footer"; import GridTable from "./components/GridTable"; const DEFAULT_QUERY = "SELECT name FROM sqlite_master WHERE type='table';"; const INITIAL_IMPORT_PROGRESS = { processed: 0, totalStatements: 0, applied: 0, skipped: 0, }; export default function HomePage() { const workerRef = useRef(null); const pendingRequestsRef = useRef(new Map()); const requestIdRef = useRef(1); const selectedTableRef = useRef(null); const [engineStatus, setEngineStatus] = useState("Initializing..."); const [engineTone, setEngineTone] = useState(""); const [isEngineReady, setIsEngineReady] = useState(false); const [isBusy, setIsBusy] = useState(true); const [isImporting, setIsImporting] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [importSummary, setImportSummary] = useState( "Waiting for SQL engine...", ); const [importLogLines, setImportLogLines] = useState([]); const [importProgress, setImportProgress] = useState(INITIAL_IMPORT_PROGRESS); const [tables, setTables] = useState([]); const [selectedTable, setSelectedTable] = useState(null); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(50); const [tableMeta, setTableMeta] = useState("No table selected."); const [tableColumns, setTableColumns] = useState([]); const [tableRows, setTableRows] = useState([]); const [canPrevPage, setCanPrevPage] = useState(false); const [canNextPage, setCanNextPage] = useState(false); const [queryInput, setQueryInput] = useState(DEFAULT_QUERY); const [queryMeta, setQueryMeta] = useState(""); const [queryColumns, setQueryColumns] = useState([]); const [queryRows, setQueryRows] = useState([]); useEffect(() => { selectedTableRef.current = selectedTable; }, [selectedTable]); const importLog = useMemo(() => importLogLines.join("\n"), [importLogLines]); const appendLog = useCallback((line) => { setImportLogLines((prev) => [...prev, line]); }, []); const clearQueryResults = useCallback(() => { setQueryMeta(""); setQueryColumns([]); setQueryRows([]); }, []); const clearTableSelection = useCallback(() => { setTableMeta("No table selected."); setTableColumns([]); setTableRows([]); setCanPrevPage(false); setCanNextPage(false); }, []); const rejectPendingRequests = useCallback((error) => { for (const pending of pendingRequestsRef.current.values()) { pending.reject(error); } pendingRequestsRef.current.clear(); }, []); const callWorker = useCallback((type, payload = {}) => { const worker = workerRef.current; if (!worker) { return Promise.reject(new Error("SQL worker is not initialized.")); } return new Promise((resolve, reject) => { const requestId = requestIdRef.current; requestIdRef.current += 1; pendingRequestsRef.current.set(requestId, { resolve, reject }); worker.postMessage({ type, requestId, ...payload }); }); }, []); const handleWorkerEvent = useCallback( (eventName, data = {}) => { switch (eventName) { case "log": if (data.line) { appendLog(data.line); } break; case "import-started": { const totalStatements = Number(data.totalStatements || 0); setImportProgress({ processed: 0, totalStatements, applied: 0, skipped: 0, }); setImportSummary( `Importing... 0/${totalStatements.toLocaleString()}`, ); break; } case "import-progress": { const processed = Number(data.processed || 0); const totalStatements = Number(data.totalStatements || 0); const applied = Number(data.applied || 0); const skipped = Number(data.skipped || 0); const percent = totalStatements > 0 ? Math.floor((processed / totalStatements) * 100) : 0; setImportProgress({ processed, totalStatements, applied, skipped, }); setImportSummary( `Importing... ${processed.toLocaleString()}/${totalStatements.toLocaleString()} (${percent}%)`, ); break; } case "import-cancel-requested": setImportSummary("Cancelling import..."); break; case "import-cancelled": { const processed = Number(data.processed || 0); const totalStatements = Number(data.totalStatements || 0); const applied = Number(data.applied || 0); const skipped = Number(data.skipped || 0); setImportProgress({ processed, totalStatements, applied, skipped, }); setImportSummary( `Import cancelled at ${processed.toLocaleString()}/${totalStatements.toLocaleString()} statements.`, ); break; } default: break; } }, [appendLog], ); const refreshTables = useCallback( async ({ keepSelection = false } = {}) => { const data = await callWorker("getTables"); const nextTables = data.tables || []; const currentSelection = selectedTableRef.current; const shouldKeepCurrent = keepSelection && currentSelection && nextTables.includes(currentSelection); setTables(nextTables); if (!nextTables.length) { setSelectedTable(null); setPage(0); clearTableSelection(); setTableMeta("No tables were created from this dump."); return; } if (shouldKeepCurrent) { setSelectedTable(currentSelection); } else { setSelectedTable(nextTables[0]); setPage(0); } }, [callWorker, clearTableSelection], ); useEffect(() => { const worker = new Worker( new URL("./workers/sqlWorker.js", import.meta.url), { type: "module", }, ); workerRef.current = worker; worker.onmessage = (event) => { const message = event.data || {}; if (message.type === "event") { handleWorkerEvent(message.event, message.data || {}); return; } if (message.type !== "response") { return; } const pending = pendingRequestsRef.current.get(message.requestId); if (!pending) { return; } pendingRequestsRef.current.delete(message.requestId); if (message.ok) { pending.resolve(message.data); } else { pending.reject( new Error(message.error?.message || "Worker command failed."), ); } }; worker.onerror = (event) => { const message = event.message || "Unknown worker error."; appendLog(`[fatal] ${message}`); setEngineStatus(`Worker error: ${message}`); setEngineTone("bad"); setIsEngineReady(false); setIsBusy(false); setIsImporting(false); rejectPendingRequests(new Error(message)); }; let cancelled = false; async function initEngine() { setIsBusy(true); try { await callWorker("init"); if (cancelled) { return; } setIsEngineReady(true); setEngineStatus("SQLite WASM ready (worker)"); setEngineTone("ok"); setImportSummary( "Engine ready. Choose a .sql or .sql.gz dump to import.", ); await refreshTables(); } catch (error) { if (cancelled) { return; } appendLog(`[fatal] ${error.stack || error.message}`); setEngineStatus(`Failed to initialize SQL worker: ${error.message}`); setEngineTone("bad"); setIsEngineReady(false); } finally { if (!cancelled) { setIsBusy(false); } } } void initEngine(); return () => { cancelled = true; worker.terminate(); workerRef.current = null; rejectPendingRequests(new Error("SQL worker terminated.")); }; }, [ appendLog, callWorker, handleWorkerEvent, refreshTables, rejectPendingRequests, ]); useEffect(() => { if (!isEngineReady || !selectedTable) { clearTableSelection(); return; } let cancelled = false; async function loadTablePage() { try { const data = await callWorker("getTablePage", { tableName: selectedTable, page, pageSize, }); if (cancelled) { return; } const rows = data.rows || []; const columns = data.columns || []; const totalRows = Number(data.totalRows || 0); const safePage = Number(data.page || 0); const safePageSize = Number(data.pageSize || pageSize); const offset = safePage * safePageSize; const start = totalRows === 0 ? 0 : offset + 1; const end = Math.min(offset + rows.length, totalRows); if (safePage !== page) { setPage(safePage); } setTableColumns(columns); setTableRows(rows); setCanPrevPage(Boolean(data.canPrevPage)); setCanNextPage(Boolean(data.canNextPage)); setTableMeta( `${selectedTable} • rows ${start}-${end} of ${totalRows.toLocaleString()} • page ${safePage + 1}`, ); } catch (error) { if (cancelled) { return; } clearTableSelection(); setTableMeta(`Failed to render table: ${error.message}`); } } void loadTablePage(); return () => { cancelled = true; }; }, [ callWorker, clearTableSelection, isEngineReady, page, pageSize, selectedTable, ]); const handleImportDump = useCallback(async () => { if (!selectedFile) { setImportSummary("Pick a .sql or .sql.gz file first."); return; } if (!isEngineReady) { setImportSummary("Database engine is not ready yet."); return; } setIsBusy(true); setIsImporting(true); setImportLogLines([]); setImportProgress(INITIAL_IMPORT_PROGRESS); setImportSummary("Preparing import..."); try { const result = await callWorker("importDump", { file: selectedFile, }); if (result.cancelled) { setImportSummary( `Import cancelled (${Number(result.applied || 0).toLocaleString()} applied, ${Number( result.skipped || 0, ).toLocaleString()} skipped).`, ); } else { setImportSummary( `Imported ${Number(result.applied || 0).toLocaleString()} statements (${Number( result.skipped || 0, ).toLocaleString()} skipped).`, ); } await refreshTables(); } catch (error) { appendLog(`[error] ${error.stack || error.message}`); setImportSummary(`Import failed: ${error.message}`); } finally { setIsImporting(false); setIsBusy(false); } }, [appendLog, callWorker, isEngineReady, refreshTables, selectedFile]); const handleCancelImport = useCallback(() => { if (!isImporting || !workerRef.current) { return; } setImportSummary("Cancelling import..."); workerRef.current.postMessage({ type: "cancelImport" }); }, [isImporting]); const handleReset = useCallback(async () => { if (!isEngineReady) { return; } setIsBusy(true); try { await callWorker("reset"); setTables([]); setSelectedTable(null); setPage(0); setPageSize(50); clearTableSelection(); clearQueryResults(); setImportProgress(INITIAL_IMPORT_PROGRESS); appendLog("[reset] in-memory database cleared"); setImportSummary("Database reset. Import another dump."); } catch (error) { setImportSummary(`Reset failed: ${error.message}`); } finally { setIsBusy(false); } }, [ appendLog, callWorker, clearQueryResults, clearTableSelection, isEngineReady, ]); const handleRunQuery = useCallback(async () => { if (!isEngineReady) { setQueryMeta("Database engine is not ready yet."); setQueryColumns([]); setQueryRows([]); return; } const sql = queryInput.trim(); if (!sql) { setQueryMeta("Enter a query first."); setQueryColumns([]); setQueryRows([]); return; } setQueryMeta("Running query..."); try { const data = await callWorker("runQuery", { sql }); setQueryColumns(data.columns || []); setQueryRows(data.rows || []); setQueryMeta(data.meta || "Query completed."); } catch (error) { setQueryMeta(`Query error: ${error.message}`); setQueryColumns([]); setQueryRows([]); } }, [callWorker, isEngineReady, queryInput]); const importDisabled = isBusy || !isEngineReady || isImporting; const resetDisabled = isBusy || !isEngineReady || isImporting; const runQueryDisabled = !isEngineReady || isImporting; const statusClasses = engineTone === "ok" ? "border-accent-500/35 bg-accent-500/15 text-accent-300" : engineTone === "bad" ? "border-red-400/35 bg-red-500/14 text-red-300" : "border-zinc-700/60 bg-zinc-800/60 text-zinc-400"; const progressPercent = importProgress.totalStatements > 0 ? Math.floor( (importProgress.processed / importProgress.totalStatements) * 100, ) : 0; return (
Skip to content {/* Header */}
MySQL Dump Browser Import{" "} .sql {" "} or{" "} .sql.gz {" "} and explore in-browser
{engineStatus}
{/* Import Section */}
Import

Load a SQL dump

{importSummary}

{importProgress.totalStatements > 0 && (

{importProgress.processed.toLocaleString()}/ {importProgress.totalStatements.toLocaleString()} statements · {importProgress.applied.toLocaleString()} applied · {importProgress.skipped.toLocaleString()} skipped

)}
{isImporting && ( )}
{importLog && (
              {importLog}
            
)}
{/* Tables + Data */}
{/* Sidebar */} {/* Data panel */}

{tableMeta}

{/* SQL Query */}