Skip to content

feat(f36): sandboxed SQL query workspace - #50

Merged
soldforaloss merged 5 commits into
mainfrom
feat/f36-sql
Aug 4, 2026
Merged

feat(f36): sandboxed SQL query workspace#50
soldforaloss merged 5 commits into
mainfrom
feat/f36-sql

Conversation

@soldforaloss

Copy link
Copy Markdown
Owner

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:

  • Approved CSV / JSON / Parquet / Arrow files become queryable tables without a full import — a generic read-only ceesvee_src tabular virtual table does windowed reads over any approved TabularSource, fingerprint-pinned at connect and re-checked on every window refill. A file is a table without loading it whole.
  • Every source is reached through the authorizer-guarded, read-only (query_only) connection.
  • Only SELECT / WITH / VALUES / EXPLAIN statements run. Multi-statement input, DDL/DML (including CTE- or EXPLAIN-wrapped), ATTACH/DETACH, PRAGMA writes and extension loading are rejected before execution (statement-level pre-validation layered on the SQLite authorizer and the query_only pragma).
  • Typed named parameters (: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.
  • A per-value size ceiling caps SQLITE_LIMIT_LENGTH to the per-run hard byte cap, so an oversized scalar (zeroblob(9e8), printf('%.*c',9e8,'x'), ...) raises SQLITE_TOOBIG at 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 :param table that tracks the query and flags bad values before a run, prepare-only validation (errors + output columns), an EXPLAIN QUERY PLAN tree, 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's queries section (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)

  • vitest: 493 passed / 493 (46 files) — incl. new src/lib/sqlWorkspace.test.ts (34 tests).
  • cargo test --lib: 864 passed / 0 failed (sql_workspace.rs + safe_query.rs unit tests, incl. oversized_single_value_is_refused_before_materialising and 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.
  • Full cargo test runs on CI (windows MSVC + ubuntu); local caps at --lib per the GNU-ld cdylib export-ordinal limit.

Acceptance mapping

Acceptance criterion Where
Query open docs + approved files + approved SQLite together sql_workspace.rs sources model; safe_query.rs generic ceesvee_src vtab
Files queryable without full import (windowed reads) ceesvee_src tabular vtab, fingerprint-pinned + re-checked per window
Read-only only: SELECT/WITH/VALUES/EXPLAIN; reject DDL/DML/ATTACH/PRAGMA/ext-load pre-exec statement pre-validation + authorizer + query_only
Typed named params validated up front, always bound (never spliced) sqlWorkspace.ts / sql_workspace.rs param typing + driver binding
Per-value size ceiling (no single-scalar OOM) SQLITE_LIMIT_LENGTH cap at both connect chokepoints
Schema browser + editor + autocomplete + prepare-only validate + EXPLAIN tree SqlWorkspaceDialog.tsx, sql_validate / sql_explain
Run with progress + working cancel + row/byte/time limits during streaming job-registry-backed sql_run; bounded windows to React
Revision-consistent reads; abort on approved-file rewrite mid-query F35 snapshot semantics; fingerprint re-check
Results in bounded grid with load-more sql_result_rows paging
Materialize to new derived document (editable/indexed) or export CSV sql_materialize (derived doc) / sql_export
Persisted history ring (never auto-run); project queries section settings.rs ring; project.rs queries
No data leaves device; no scripting/SQL-extension/exec surface; atomic saves authorizer sandbox + atomic-save infra

STACKED PR

This is a stacked PR. Base is feat/f39-facets, not main — review only the delta vs feat/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 onto feat/f39-facets (which picked up its Codex-fix commit cae7ee1); no conflicts.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Generated with Claude Code.

soldforaloss and others added 4 commits July 18, 2026 05:36
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/store/useStore.ts

sqlCancelRun: async () => {
const ws = get().sqlWorkspace;
if (ws.runJobId != null) await api.cancelJob(ws.runJobId).catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/store/useStore.ts
try {
const window = await api.sqlResultRows(ws.result.resultId, loaded, 500);
const cur = get().sqlWorkspace;
if (cur.result?.resultId !== ws.result.resultId) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/store/useStore.ts
Comment on lines +6808 to +6810
patchWs({
sql: query.sql,
params: mergeDetectedParams(query.params, query.sql),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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
@soldforaloss
soldforaloss changed the base branch from feat/f39-facets to main August 4, 2026 20:00
@soldforaloss
soldforaloss merged commit a848658 into main Aug 4, 2026
2 checks passed
@soldforaloss
soldforaloss deleted the feat/f36-sql branch August 4, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant