feat(f36): sandboxed SQL query workspace - #50
Conversation
Front end for the sandboxed SQL query workspace: a dialog (palette "SQL workspace…") that composes open documents, approved local files and one approved SQLite database into read-only SQL run through the F35 SafeQueryEngine. - src/lib/sqlWorkspace.ts: dependency-free logic — `:param` detection (skips strings/comments/`::`), typed-value validation mirroring the engine binder, the history ring reducer, and schema-driven prefix autocomplete. Covered by src/lib/sqlWorkspace.test.ts (34 tests). - src/components/SqlWorkspaceDialog.tsx: monospace editor (no editor deps) with a suggestion list, a typed :param table, Validate / Explain / Run with rows-produced progress + cancel, a bounded results grid with windowed load-more, an EXPLAIN QUERY PLAN tree, row/byte/time limits, the persisted history dropdown (reload only, never auto-run), save/load query definitions to the project, materialize-to-document (editable or indexed) and direct CSV export, and an inline approved- files manager (add via picker -> approved-source registry; remove revokes). - store slice, typed invoke wrappers (src/lib/tauri.ts), DTO mirrors (src/types.ts), and the command entry. A run/export learns its job id from the first progress event (the commands are awaited) to enable cancel; materialize rides the shared derive job slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Land the F36 sandboxed SQL workspace engine backend alongside the UI committed in bab906e, run the full gate suite green, and update docs. Engine backend (previously uncommitted): sql_workspace.rs, plus the safe_query generic read-only tabular virtual table (ceesvee_src) over any approved TabularSource with fingerprint-pinned windowed reads, lib.rs command registration, the project `queries` section, and the settings-persisted query-history ring. Docs: - CHANGELOG: one consolidated user-facing "Sandboxed SQL query workspace" entry under [Unreleased] > Added (engine + UI merged into one bullet). - README: "SQL query workspace" bullet in the Navigate & analyze group. Gates (all green): npm lint / format:check / typecheck / build, vitest 492 passed, cargo fmt --check, cargo clippy -D warnings, cargo test --lib 857 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cap SQLITE_LIMIT_LENGTH on every guarded connection so a single hostile scalar cannot force SQLite to materialise a ~1GB value before the row/byte budget reacts; bound EXPLAIN QUERY PLAN with the job registry (cancel + deadline + node cap) like every other steppable path; and refuse queries where a temp-schema document/file vtab alias would silently shadow a same-named table in the selected database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c2ba99d31
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| fn fingerprint(&self) -> ContentFingerprint { | ||
| match &self.backing { | ||
| FileBacking::Doc(d) => DocumentSource::new(d).fingerprint(), |
There was a problem hiding this comment.
Recheck file-backed snapshots against the file
For FileBacking::Doc, which is used by small CSV files and JSON imports in open_backing, the internal Document has a fixed revision and no stored file fingerprint, so delegating to DocumentSource::fingerprint() pins the in-memory snapshot rather than the approved path. If an approved CSV/JSON file is rewritten after registration or between query windows, the workspace keeps returning stale rows instead of aborting or refreshing the file source. Store and compare the canonical file fingerprint for these file backings too.
Useful? React with 👍 / 👎.
|
|
||
| sqlCancelRun: async () => { | ||
| const ws = get().sqlWorkspace; | ||
| if (ws.runJobId != null) await api.cancelJob(ws.runJobId).catch(() => undefined); |
There was a problem hiding this comment.
Make Cancel work before the first progress event
runJobId is only learned from job-progress, but SQL runs emit progress from ctx.advance() after rows are produced. For long statements that do all work before returning a row, such as SELECT count(*) FROM huge CROSS JOIN huge, runJobId remains null while the Cancel button is visible, so this guard makes Cancel a no-op until the query finishes or times out. Emit/store the job id when the run starts, or return it before awaiting completion.
Useful? React with 👍 / 👎.
| try { | ||
| const window = await api.sqlResultRows(ws.result.resultId, loaded, 500); | ||
| const cur = get().sqlWorkspace; | ||
| if (cur.result?.resultId !== ws.result.resultId) return; |
There was a problem hiding this comment.
Clear the load-more flag on stale responses
If the user clicks Load more and then starts another query before this request resolves, this early return leaves loadingMore set to true; sqlRun resets result and extraRows but not loadingMore. The next result then renders with Load more disabled permanently, preventing the user from paging additional rows until the workspace state is reset. Clear loadingMore before returning, or reset it when a new run starts.
Useful? React with 👍 / 👎.
| patchWs({ | ||
| sql: query.sql, | ||
| params: mergeDetectedParams(query.params, query.sql), |
There was a problem hiding this comment.
Restore saved query sources when loading
Saved queries persist a sources list when saved, but loading one only restores the SQL text and parameters. If the current workspace has different documents, files, or database selected, the loaded query runs against the wrong source set or fails even though the project entry carries the intended sources. Apply the saved source refs, or at least surface unresolved refs, when loading the saved query.
Useful? React with 👍 / 👎.
# Conflicts: # src-tauri/src/settings.rs
Summary
F36 adds a sandboxed SQL query workspace (palette -> "SQL workspace...") that lets you query your open documents, approved local files, and an F35-approved SQLite database together with read-only SQL — under CEESVEE's local controlled-execution model. Nothing leaves the device, there is no scripting or network surface, and files outside the ones you explicitly approve are never readable.
Built on the F35
SafeQueryEngine:ceesvee_srctabular virtual table does windowed reads over any approvedTabularSource, fingerprint-pinned at connect and re-checked on every window refill. A file is a table without loading it whole.query_only) connection.SELECT/WITH/VALUES/EXPLAINstatements run. Multi-statement input, DDL/DML (including CTE- orEXPLAIN-wrapped),ATTACH/DETACH, PRAGMA writes and extension loading are rejected before execution (statement-level pre-validation layered on the SQLite authorizer and thequery_onlypragma).:name— text / integer / decimal / float / boolean / date / datetime / null) are validated up front and always bound through the driver, never spliced into SQL, so a hostile string parameter is inert data.SQLITE_LIMIT_LENGTHto the per-run hard byte cap, so an oversized scalar (zeroblob(9e8),printf('%.*c',9e8,'x'), ...) raisesSQLITE_TOOBIGat construction rather than OOMing the 6 GB target after the fact.The dialog provides: a schema browser over every source, a dependency-free monospace editor with prefix-matched column/table autocomplete, a typed
:paramtable that tracks the query and flags bad values before a run, prepare-only validation (errors + output columns), anEXPLAIN QUERY PLANtree, and Run with rows-produced progress, a working cancel, and configurable row / byte / time limits enforced during streaming. A document edited mid-query reads its current revision consistently (F35 snapshot semantics); an approved file rewritten mid-query aborts the query instead of mixing versions. Results stream into a bounded grid you page with "load more", and can be materialized as a new derived document (editable or indexed by size) or exported to CSV. A settings-persisted query-history ring (capped, click to reload, never auto-executed) and the project file'squeriessection (name + sql + params + sources — never results, never auto-run) round out the workspace; approved files are managed inline.Backend adds 7
sql_*Tauri commands (register/unregister/list files, schema, validate, explain, run, result_rows, materialize, export, history, history_clear).Test counts (all gates green)
src/lib/sqlWorkspace.test.ts(34 tests).sql_workspace.rs+safe_query.rsunit tests, incl.oversized_single_value_is_refused_before_materialisingand the mid-query file-rewrite abort).npm run lint/format:check/typecheck/build: OK.cargo fmt --all --check/cargo clippy --all-targets --all-features -D warnings: OK.cargo testruns on CI (windows MSVC + ubuntu); local caps at--libper the GNU-ld cdylib export-ordinal limit.Acceptance mapping
sql_workspace.rssources model;safe_query.rsgenericceesvee_srcvtabceesvee_srctabular vtab, fingerprint-pinned + re-checked per windowSELECT/WITH/VALUES/EXPLAIN; reject DDL/DML/ATTACH/PRAGMA/ext-load pre-execquery_onlysqlWorkspace.ts/sql_workspace.rsparam typing + driver bindingSQLITE_LIMIT_LENGTHcap at both connect chokepointsSqlWorkspaceDialog.tsx,sql_validate/sql_explainsql_run; bounded windows to Reactsql_result_rowspagingsql_materialize(derived doc) /sql_exportqueriessectionsettings.rsring;project.rsqueriesSTACKED PR
This is a stacked PR. Base is
feat/f39-facets, notmain— review only the delta vsfeat/f39-facets(the 15 files / +5570 shown here are exactly F36's changes). The parent chain (main <- f31 <- infra <- f33 <- f37 <- f38 <- f48 <- f40 <- f41 <- f42 <- f32 <- f34 <- f35 <- f39) is reviewed in its own PRs. Rebased cleanly ontofeat/f39-facets(which picked up its Codex-fix commitcae7ee1); no conflicts.Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Generated with Claude Code.