Skip to content

feat(sql-editor): data peek, safe SQL bridge for Node cells, FK correctness fixes - #121

Merged
huyplb merged 1 commit into
mainfrom
feat/sql-editor-data-peek-and-sql-bridge
Jul 28, 2026
Merged

feat(sql-editor): data peek, safe SQL bridge for Node cells, FK correctness fixes#121
huyplb merged 1 commit into
mainfrom
feat/sql-editor-data-peek-and-sql-bridge

Conversation

@huyplb

@huyplb huyplb commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Three strands of SQL-editor / core work. Each is independently reviewable; the FK fixes underpin the other two.

Data peek (new)

Cmd/Ctrl-click a table in the schema explorer to see its rows without writing a query. Foreign-key cells render as links — clicking one appends the parent's rows as a grid below, so a relationship can be followed a couple of hops. A breadcrumb walks back.

Answers the "I just want a quick look before I write the real query" case.

sql bridge for -- @node cells (new)

Node code cells can now run SQL against the run's credential:

-- @node
const rows = [{ id: 2, email: "o'brien@x.com", note: null }];
await sql`INSERT INTO ${sql.id('accounts')} ${sql.values(rows)}`;
return await sql`SELECT id, email FROM accounts WHERE id IN ${[2, 3]}`;
-- @end

Interpolations become bind parameters, never text. String interpolation was requested and deliberately not used: an apostrophe in a value breaks the statement, null becomes the bare word null, Date becomes a locale string, and an object becomes [object Object]. sql.raw() is the explicit escape hatch.

Cells run in an isolated worker with no DB handle, so sql proxies to the API process over a request/response bridge. Three consequences reviewers should know:

  • The cell timeout pauses while a query is in flight. Database time is not the cell's time; without this any real migration was killed mid-statement. The budget restarts fresh after each query — total DB time is intentionally unbounded, a runaway loop between queries is still caught.
  • Safe mode is enforced server-side, per statement. A cell can't be statically analyzed for the SQL it will build at runtime.
  • Each await sql is its own round trip on a pooled connection, so transactions and temp tables do not carry across calls. sql.transaction() is the follow-up if that's needed.

/sql/execute now accepts bind parameters (a thin passthrough — executeOnConnection already took them).

Foreign-key correctness (fixes)

  • resolveFkReferencedColumns requires every catalog entry to be a real identifier, not just matching arity. SQLite's PRAGMA foreign_key_list reports NULL for to, which arrived as [null] and produced REFERENCES parent ().
  • Shared groupForeignKeyRows replaces the same hand-rolled fold in five providers, so that rule lives in one place instead of being re-derived per dialect.
  • SQLite could never execute a write: the adapter called .all() for every statement and opened the file read-only. No SQLite connection could run DDL or DML through any path, not just this feature.
  • Blueprint modal: silent wrong-column FK guess, stripped schema qualifier, self-referencing FKs unauthorable, misleading "applied" on skipped statements, Oracle trigger timing missing EACH ROW.

Samples

Five sql-bridge examples, plus a show/hide toggle for the built-in ★ samples (a view filter — they stay installed). Ten existing samples crashed with a raw TypeError when run standalone from the statement strip, where last is null; all now guard, with a test that runs every browser sample with last: null.

Rebase note

This work was developed against bd614e1605 and rebased onto current main, which had independently refactored several of the same files. Two resolutions worth a look:

  • sql-generator.module.ts — main added addForeignKeySql, which handles the empty-parent-columns case by emitting a -- review: skip FK comment. I took main's version and dropped my referencesClause (which emitted a bare REFERENCES parent). Main's is the more conservative choice; my upstream fixes make its skip path trigger far less often.
  • SqlSchemaExplorer.tsx — main refactored it substantially, so I took main's file wholesale and re-applied only the peek feature, dropping my preferredIds memo tweak in favour of main's equivalent.

Verification

  • cd apps/web && npx tsc --noEmit clean; npx vitest run (repo root) 607 passed, 2 skipped; ESLint clean on touched files.
  • The bridge and the peek were driven against a real SQLite database, not mocks: bulk insert with an apostrophe and a NULL landed correctly, Safe mode blocked a DELETE and the rows were verified still present, and an infinite-loop cell was terminated by the timeout.
  • Post-rebase the app was re-checked in the browser: Cmd-click → peek opens → FK link drills to customers · id = 3.

One caught in self-review worth flagging: the FK link handler was passing the 1-based display row number where a row index was expected, so most links silently drilled the wrong row or did nothing. The original fixture masked it (two rows shared a customer_id); reseeding with distinct values exposed it. Fixed and verified.

🤖 Generated with Claude Code


Note

High Risk
Changes user-code SQL execution (worker bridge, unbounded DB time during paused timeouts), ad-hoc query binding, and SQLite write behavior—security- and data-integrity sensitive paths that need careful review.

Overview
Data peek — Cmd/Ctrl-click a table/view/MQT in the schema explorer opens a modal that loads up to 50 rows via parameterized SELECT *. FK columns render as links; clicking stacks another grid below with a breadcrumb to walk back. DataGrid gains optional FK link rendering; the store owns peek state and uses tablePreview helpers.

Node sql bridge — New @foxschema/core sqlTag / renderSqlQuery turns tagged-template values into bind parameters per dialect. The code-cell worker exposes sql to user code but runs statements in the API via cell-query messages; makeCellQueryRunner enforces Safe mode writes, caps rows, and the cell timeout pauses while queries are in flight. /sql/code-cell resolves a connection ref when present; the editor passes ref and allowWrites on runs. /sql/execute and runStatements accept optional params per statement.

FK & provider fixesresolveFkReferencedColumns ignores NULL/empty catalog parent columns; shared groupForeignKeyRows replaces duplicated provider logic (incl. Redshift FK query fallback). SQLite adapter opens read-write, uses fileMustExist, and runs non-reader statements with run().

Editor polish — Blueprint modal FK/Oracle trigger/apply messaging fixes; bookmark sample hide toggle; sample cells guard last === null; new Node sql samples and user-guide docs; .gitignore for local foxflow.sqlite*.

Reviewed by Cursor Bugbot for commit 0d03e73. Bugbot is set up for automated code reviews on this repo. Configure here.

Three strands of work, all in the SQL editor / core provider layer.

**Data peek (new).** Cmd/Ctrl-click a table in the schema explorer to see
its rows without writing a query. Foreign-key cells render as links; clicking
one appends the parent's rows as another grid below, so a relationship can be
followed a couple of hops. Breadcrumb navigates back. Answers "I just want a
quick look before I write the real query".

**`sql` bridge for `-- @node` cells (new).** Node code cells can now run SQL
against the run's credential via a tagged template whose interpolations become
bind parameters, never text:

    await sql`INSERT INTO ${sql.id('accounts')} ${sql.values(rows)}`;

String interpolation was considered and rejected: a value containing an
apostrophe breaks the statement, null becomes the bare word null, Date becomes
a locale string, and an object becomes "[object Object]". Cells run in an
isolated worker with no DB handle, so `sql` proxies to the API process over a
request/response bridge. Three consequences worth knowing:

- The cell timeout pauses while a bridged query is in flight — database time
  is not the cell's time, and without this any real migration was killed
  mid-statement.
- Safe mode is enforced server-side per statement, since a cell cannot be
  statically analyzed for the SQL it will build at runtime.
- Each `await sql` is its own round trip on a pooled connection, so
  transactions and temp tables do not carry across calls.

**Foreign-key correctness (fixes).** `resolveFkReferencedColumns` now requires
every catalog entry to be a real identifier, not just matching arity — SQLite's
PRAGMA reports NULL for `to`, which arrived as `[null]` and produced
`REFERENCES parent ()`. A shared `groupForeignKeyRows` replaces the same
hand-rolled fold in five providers so that rule lives in one place. The
generator's new `referencesClause` emits a bare `REFERENCES parent` when the
parent columns are unknown, which every engine reads as "the parent's PK".

Also fixed along the way:

- SQLite adapter could never execute a write: it called `.all()` for every
  statement and opened the file read-only. No SQLite connection could run DDL
  or DML through any path.
- Blueprint modal: silent wrong-column FK guess, stripped schema qualifier,
  self-referencing FKs unauthorable, misleading "applied" on skipped
  statements, Oracle trigger timing missing EACH ROW.
- Ten sample bookmarks crashed with a raw TypeError when run standalone from
  the statement strip (`last` is null there).

Samples gained five `sql`-bridge examples and a show/hide toggle. Every sample
is executed against a real database by its test, and `/sql/execute` now accepts
bind parameters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_69e45237-1dc0-4fba-ad16-7ae3b58fcaed)


it('keeps null / Date / object as bound values, not text', () => {
const d = new Date('2026-01-02T03:04:05Z');
const out = renderSqlQuery(sql`INSERT INTO t VALUES (${null}, ${d}, ${{ a: 1 }})`, 'mysql');
@huyplb
huyplb merged commit feda77f into main Jul 28, 2026
6 of 7 checks passed
@huyplb
huyplb deleted the feat/sql-editor-data-peek-and-sql-bridge branch July 28, 2026 18:21
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