"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 */}
{/* 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 */}
Data
{tableMeta}
{/* SQL Query */}
SQL Query
);
}