Historical plan (the three PRs shipped). Paths and constraints below are stale:
packages/coreis nowpackages/sql+packages/db; the API lives inpackages/server(notapps/web/src/backend); SQLite is read-write; ports are 3210 / 5173. Current map: CODE_MAP.md. User walkthrough: user-guide.md. Agent notes:.cursor/rules/sql-editor-agent-memory.mdc.
FoxSchema can compare/migrate schemas but has no way to run ad-hoc SQL and look at data. The user wants a SQL Editor view in the web UI (not CLI): browse-style layout (schema objects left, editor right), multi-tab, per-tab credential checklist (run the same query against 1..N saved connections and compare data across environments), per-statement syntax indicators + run checkboxes, schema-aware autocomplete with alias support, and result grids groupable by credential (vertical) or side-by-side (horizontal).
Decisions locked with the user: any SQL allowed with a confirmation dialog when a write/DDL statement is included (SQLite/ClickHouse writes fail with friendly per-cell errors — their adapters are SELECT-only); per-statement checkboxes live in a statement strip below the editor (gutter keeps the green-✓/amber-⚠ status icons); delivery as 3 phased PRs, each leaving gates green.
Verified reuse: ConnectionFactory.executeQuery<T>() returns rows for all 14
dialects; resolveRef closure in routes.ts decrypts saved connections server-side;
rateLimit() in api/rate-limit.ts; Monaco via @monaco-editor/react +
monaco-setup.ts (monacoLanguage(), themes); sql-formatter + utils/formatSql.ts;
SavedConnectionSummary list in useSyncStore.connections; zustand persist +
partialize pattern (never persist credentials); vite aliases @foxschema/core →
packages/core/src/browser.ts, so pure core modules are shared frontend/backend.
Gaps (net-new): statement splitter, syntax heuristic, data-execution endpoint,
tabs UI, data grid, completion provider. No timeouts/cancel (deferred, documented).
Ships: switch to "SQL Editor" view, type SQL, check N credentials, Run → all statements execute against every checked credential, results in by-credential layout.
splitSqlStatements(sql): SplitStatement[]whereSplitStatement = { text, start, end, startLine, endLine, terminated }. Single-pass scanner aware of:--/#line comments,/* */block comments,'…'/"…"/backtick/[bracket]quoting, Postgres$tag$…$tag$dollar quotes,;terminators. NOT handled v1 (document): procedural BEGIN…END bodies with internal semicolons.checkStatement(stmt): { level: 'ok'|'warn'; reasons: string[] }— heuristic only: terminated with;, balanced quotes/parens, starts with known SQL keyword. UI copy must say "looks complete", not "valid".isWriteStatement(stmt): boolean— leading-keyword check (INSERT/UPDATE/DELETE/ CREATE/ALTER/DROP/TRUNCATE/MERGE/GRANT/REVOKE) for the confirm dialog.- Export all from
packages/core/src/browser.ts(andindex.tsfor backend). - Tests:
packages/core/src/modules/sql-splitter.test.ts— comments, dollar quotes, quoted semicolons, unterminated final statement, CRLF, empty input, check matrix.
- NEW
apps/web/src/backend/api/sql-execute.ts: pure helpersrunStatements(dialect, option, statements, maxRows)+shapeRows(rows, maxRows). Route registered insidecreateApiRoutes(routes.ts) so it closes overresolveRef. - Request:
{ ...ConnectionRef, statements: string[], maxRows?: number }. Clamp maxRows to [1, 5000], default 500; max 25 statements; reject empty statements. Server trusts the client split (same as/migration/execute). - Fan-out is client-side — one request per credential,
Promise.allSettledin the store. Progressive arrival for free; dead DB can't stall others; no new protocol. - Statements run sequentially via
ConnectionFactory.executeQuery; per-statement error isolation:results: Array<{ok:true, columns, rows, rowCount, truncated, durationMs} | {ok:false, error, durationMs}>(always 200 once ref resolves). - Row shaping: columns = union of
Object.keysover first 50 rows; rows as arrays in column order; BigInt → string, Date → ISO server-side (JSON.stringify throws on BigInt). Empty set →columns: [](v1 limitation, UI shows "0 rows"). - Row cap = slice-after-fetch +
truncated: trueflag (no LIMIT injection — dialect- variant, needs a parser). Truncation notice suggests adding LIMIT. rateLimit({ windowMs: 60_000, max: 60 }).- Tests: shaping/clamping helpers pure-unit;
runStatementsagainst a seeded better-sqlite3 file (no HTTP harness exists in repo — test the helper, not Express).
activeView: 'sync' | 'sqlEditor'inapps/web/src/frontend/store/uiStore.ts(already persists); view switcher buttons inTopToolbar.tsx;App.tsxWorkspacerenders<SqlEditorView/>instead of tree+detail panels whensqlEditor.- NEW
apps/web/src/frontend/store/useSqlEditorStore.ts(single tab in PR 1):{ sql, selectedConnectionIds, resultsByConnection, running }+execute()(split → confirm-if-isWriteStatement→ fan out), readinguseSyncStore.connections. - NEW
apps/web/src/frontend/components/sql-editor/:SqlEditorView.tsx— left:ConnectionChecklist; right: editor pane + Run buttonResultsPanel.
ConnectionChecklist.tsx— checkboxes overSavedConnectionSummary([DIALECT] namestyling + missing-password session-prompt pattern from TopToolbar).SqlEditorPane.tsx— NEW Monaco wrapper (copy BASE_OPTIONS/theme wiring fromSqlEditor.tsx, leave that file untouched),glyphMargin: true, editable; on change (debounced ~200ms) split+check →createDecorationsCollectionwith glyph classes for ✓/⚠ per statement start line. Lazy + Suspense like existing.ResultsPanel.tsx(byCredential layout only in PR 1) +DataGrid.tsx— plain<table>, sticky header,NULLdim-italic, ~200-char cell truncation w/ tooltip, footerN rows · X ms+ amber truncation notice; rose error card per failed cell.
- New frontend
api/sqlApi.tsfollowingschemaApi.tsconventions (parseJson,{error}shape). - Write-confirm dialog: portal modal listing the write statements + credential count,
styled like
DeployConfirmDialog.tsx.
- Multi-tab:
tabs: SqlTab[]({id, title, sql, selectedConnectionIds, checkedStatements, layout}),activeTabId, tab bar (add/close/rename via double-click), results keyed per tab (not persisted). StatementStrip.tsxbetween editor and results: one row per split statement — status icon, checkbox, truncated preview (#1 ✓ SELECT * FROM users…); click row → reveal statement in editor. Execute runs checked statements; none checked → first statement (the agreed default).setSqlthat changes statement count resets checks.- Layout toggle per tab:
byCredential(vertical stack of credential rows, each row's statement results horizontally scrollable) |sideBySide(one section per statement, credential results as columns). Same data, orientation flip. - Persistence:
zustand/persistkeyfoxschema-sql-editor, partialize →{tabs: [{id,title,sql,selectedConnectionIds,layout}], activeTabId}. Connection ids are safe (not credentials); drop stale ids againstuseSyncStore.connectionson render. Never persist results/schemaCache/checkedStatements. - CSV export per grid:
utils/exportCsv.ts(quote-escaped join + Blob download). - Format button reusing
utils/formatSql.ts. - Store reducer tests (tab add/close/toggle/check-reset).
SqlSchemaExplorer.tsx— NEW slim left tree (do NOT reuse SchemaTreePanel — it's hard-wired to compareResult/diff state): connection picker on top, tables → columns, click-to-insert identifier at cursor; reuseTYPE_METAicons. Data via existingloadSchema()(POST /schema/load), cached in storeschemaCache[connectionId].extractTableAliases(sql)insql-splitter.ts(+tests): regex overFROM|JOIN|UPDATE|INTO <ident> [AS] <alias>with keyword blacklist (where/on/set/join/left/right/inner/outer/group/order/limit/using).completion.ts— onemonaco.languages.registerCompletionItemProviderper language id (mysql/pgsql/sql), registered once at first mount, reading active tab's schemaCache via module-level getter (prevents duplicate-provider leaks). Logic:alias.→ that table's columns;table.→ its columns; otherwise table names + light keywords. Suggestions from all checked connections' schemas, deduped.- Pre-flight friendly warning when write statements target sqlite/clickhouse credentials (their adapters can't execute writes).
cd apps/web && npx tsc --noEmitand rootnpx vitest rungreen.- Browser (vite 5199 + API 3001, per-session
preview_start name:"web"+ tsx API): seed two SQLite files with the same table but divergent rows (sqlite3 /tmp/foxa.db//tmp/foxb.db— pattern proven this session), save both as connections, then: type 2 SELECTs + 1 broken statement → gutter shows ✓✓⚠; check both credentials → Run → by-credential layout shows 2 rows × per-statement grids with the divergent data visible; broken statement shows rose error card; PR 2: toggle side-by-side, reload page → tabs/sql persist; PR 3: typeSELECT u.afterFROM t1 u→ column suggestions appear. - Write-confirm: type an UPDATE, Run → confirmation dialog appears; on the SQLite credential the cell shows the friendly readonly error.
- CLI / TUI — the interactive SQL editor is web-only. No
foxschema sqlcommand, Ink screen, or CLI execute path in this plan or follow-ups unless revisited.
- No query timeout/cancel (only clickhouse/sqlserver honor
timeout.queryMs); runaway queries hold a pooled connection. Needs its own follow-up. - No cross-statement session state (each statement may get a different pooled
connection — temp tables/SET/transactions don't carry).
executeOnConnectionis the later fix if needed. - Empty result sets show no column names (rows-derived metadata).
- Syntax indicator is a heuristic ("looks complete"), not a parser.
- Row cap slices after fetch — driver still transfers the full result.