Conversation
- define SqliteExplorerAdapter contract for DB-agnostic exploration - add Drizzle adapter implementation using sqlite_master and PRAGMA introspection - include component state types (DbExplorerState, DbExplorerAction)
- add useDbExplorerState hook with reducer for table browsing, pagination, and custom SQL execution - add useColumnResize hook for interactive column width adjustment via mouse drag
- display tables and views grouped with icons and row counts - highlight selected object and support click-to-select
- Add SqlEditor component with CodeMirror 6, SQLite dialect, and dark mode support - Install codemirror, @codemirror/lang-sql, @codemirror/state, @codemirror/view, and @codemirror/theme-one-dark - Support Cmd+Enter to execute queries, display execution time and errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Resizable columns via useColumnResize hook - Click-to-copy cell values with visual feedback - Pagination controls with configurable page sizes - NULL value styling and empty state handling
- add DbExplorerPage with page header and adapter wiring - add DbExplorer component with resizable sidebar, SQL editor, and data table - register /db-explorer route in app.tsx
- adapter now supports both array and object row formats since PowerSync returns objects while wa-sqlite returns arrays - filter out internal ps_ prefixed tables from the object list - add debug logging for object loading
- the ps_ exclusion was too broad and hid PowerSync data tables that users need to inspect in the db-explorer
- show all tables and views in sqlite_master without filtering
- display indexes and triggers alongside tables and views - group objects by type with distinct icons - only fetch row counts for tables and views - extract DbObjectType and use config-driven grouping to reduce duplication
- Fetch sqlDefinition and tblName from sqlite_master for all objects - Show parent table name for indexes/triggers in the object list - Display PRAGMA index_info with CREATE statement for indexes - Display CREATE TRIGGER definition for triggers - Remove default SELECT * query when selecting non-table objects
- groups start collapsed by default to reduce visual noise - add chevron indicator with rotate animation on expand - show item count badge in group header
… SQL - display sqlDefinition directly in the result grid instead of querying sqlite_master - eliminates unnecessary database round-trip for trigger inspection
… or execution - Add dedicated SqlDefinition component with CodeMirror read-only editor - Introduce viewMode state to switch between query and definition views - Unify index and trigger selection into a single code path
- rename SCREAMING_CASE constants to camelCase per project conventions - add braces to single-line if/return statements - remove stale console.log in object loading - fix long lines and improve formatting consistency
cjroth
left a comment
There was a problem hiding this comment.
I love this - I think we should either find a way to make this only bundle in dev builds or maybe even put it into a browser extension (btw is there a major benefit to using this vs an existing sqlite browser extension?)
If we are going to go forward with it, I'd want to add tests.
Here's Claude's code review:
Overview
A new devtools page that lets developers explore the in-browser SQLite database. It features:
- A collapsible sidebar listing tables, views, indexes, and triggers
- A CodeMirror-based SQL editor with Cmd+Enter execution
- A paginated data table with resizable columns and click-to-copy
- An adapter pattern (SqliteExplorerAdapter) making the component DB-agnostic
- Read-only SQL definition view for indexes/triggers
The architecture is solid — clean adapter pattern, proper use of useReducer for complex state, and
state/logic extracted into a custom hook per project conventions. Well-researched PR description too.
Issues
- SQL Injection via unescaped identifiers in use-db-explorer-state.ts
The adapter has a proper escapeId() helper, but the state hook constructs SQL with raw template
literals:
// use-db-explorer-state.ts:152
adapter.execute(SELECT * FROM "${name}" LIMIT ${defaultPageSize} OFFSET 0)
// use-db-explorer-state.ts:206
adapter.execute(SELECT * FROM "${state.selectedObject}" LIMIT ${state.pageSize} OFFSET ${offset})
A table named foo"bar would break the query. Either expose escapeId from the adapter or move these
queries inside the adapter itself (e.g., adapter.getPage(name, limit, offset)).
- Duplicated useIsDarkMode hook
Identical function in both sql-definition.tsx:13-18 and sql-editor.tsx:18-23. Extract to a shared
file.
Also, when theme === 'system', this reads window.matchMedia at render time but doesn't subscribe to
changes. Per project conventions, useSyncExternalStore would be the right fit here, since this is
exactly "subscribing to a browser API."
- Sequential row count loading
object-list.tsx:29-39 fetches row counts in a serial for loop:
for (const obj of countable) {
counts.set(obj.name, await adapter.getRowCount(obj.name))
}
With many tables, this is noticeably slow. Promise.all (or batched parallel) would be faster.
- columns stored in state but never read
state.columns is populated in SELECT_OBJECT (use-db-explorer-state.ts:77) but no component ever reads
it. The DataTable uses result.columns from the query result instead. Dead state — remove it or use
it.
- resetWidths defined but unused
use-column-resize.ts:37 exports resetWidths, which is never called. Remove it.
- Custom SQL runs without pagination
runQuery (use-db-explorer-state.ts:175-192) sets totalRows: result.rows.length, meaning all rows are
fetched into memory. For a SELECT * FROM on a large table, this could be expensive. Consider wrapping
custom queries in a COUNT(*) subquery or at least adding a default LIMIT.
- No production guard
The PR description mentions "auto-exclude from production via process.env.NODE_ENV", but the route in
app.tsx:115 is unconditional. This will ship to production. Should be gated like the tasks route:
{isDev && <Route path="db-explorer" element={} />}
- No tests
No test files included. The drizzle-adapter.ts (SQL parsing, row normalization, column extraction)
and use-db-explorer-state.ts (reducer logic) are both very testable and have meaningful edge cases
worth covering.
Minor / Style
- data-table.tsx:66 — (row as unknown[]): The QueryResult type already declares rows: unknown[][], so
this cast shouldn't be needed. If it is, the type might not be flowing right. - Double border on sidebar: db-explorer.tsx:29 has border-r on the sidebar container, but the
ResizableHandle already provides a visual separator. - setPageSize async pattern (use-db-explorer-state.ts:221-243): The inner run() function is defined
and immediately called. An async IIFE or extracting to a separate function would be cleaner. - Hardcoded Cmd+Enter in sql-editor.tsx:108: On Windows/Linux this would be Ctrl+Enter. The keymap
binding uses Mod-Enter correctly, but the displayed hint says Cmd+Enter.
What's good
- The adapter interface is well-designed and genuinely DB-agnostic
- useReducer + extracted state hook follows project conventions
- CodeMirror integration is clean — callback refs avoid stale closures, theme changes handled
properly - Column resize hook is a nice touch
- The temp view approach for extracting column names from arbitrary queries is clever
- PR description with landscape research is thorough
Verdict
Good foundation for a dev tool. The main things to address before merging: production guard on the
route, extract/fix useIsDarkMode, escape identifiers consistently, and remove dead code (columns
state, resetWidths). Tests would be nice but less critical for an experimental devtool behind a
feature gate.
Landscape Research
Existing Solutions
NPM Packages (embeddable)
@drizzle-team/studio)@electric-sql/pglite-repl)Browser Extensions
Standalone Web Apps
Key Finding
There is a clear gap: no open-source, embeddable React component exists for exploring in-browser SQLite databases (wa-sqlite, sql.js, PowerSync). Every existing solution is either:
npm Package vs Browser Extension
npm package is the clear winner. Here's why:
Live database access required — a browser extension communicates via message passing and cannot access WASM memory or JS objects directly. The adapter pattern requires a JS reference to the DB.
Established pattern — React Query DevTools, TanStack Router DevTools, and LiveStore Devtools all ship as npm packages with a React component. This is the standard React ecosystem approach for dev tooling.
Distribution model:
@thunderbolt/sqlite-explorer(or similar)SqliteExplorerAdapteras prop (existing pattern)process.env.NODE_ENVAdapter pattern already solves the hard problem —
SqliteExplorerAdapteris DB-agnostic. Users can implement it for wa-sqlite, sql.js, PowerSync, SQLocal, or any other browser SQLite. Ship a few built-in adapters (createDrizzleExplorerAdapter,createSqlJsAdapter, etc.)The browser-sqlite-editor project (React + wa-sqlite + CodeMirror 6) validates the tech stack but is a standalone PWA — extracting it as a component would be significant work. We're already building the component-first version.
Market Opportunity
The local-first / offline-first ecosystem (PowerSync, wa-sqlite, sql.js, SQLocal, Electric SQL) is growing fast and has zero embeddable debugging tools. Publishing this as an npm package following the TanStack DevTools pattern would fill a genuine unserved niche.
Sources