Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3de784a
fix(sql-editor): rename page wrap alias — DB2 rejects leading _
cursoragent Jul 29, 2026
81e065b
fix(sql-editor): include wrapped SQL in paging failure messages
cursoragent Jul 29, 2026
808ebd2
fix(sql-editor): Data Peek fit + clearer number/link colors on paper
cursoragent Jul 29, 2026
43e2c71
feat(sql-editor): multi FK Data Peek panels + readable number links
cursoragent Jul 29, 2026
8d1d36d
feat(sql-editor): Data Peek filters, resize, and rust FK IDs
cursoragent Jul 29, 2026
b09385a
fix(sql-editor): stack By cred Out grids vertically
cursoragent Jul 29, 2026
35d9d41
fix(sql-editor): stack Data Peek FK panels vertically
cursoragent Jul 29, 2026
e61a2c9
fix(sql-editor): enlarge Data Peek drill grids
cursoragent Jul 29, 2026
a5a6eb7
fix(sql-editor): main vertical scroll for many Data Peek grids
cursoragent Jul 29, 2026
343a3b6
feat(sql-editor): Prev/Next paging on Data Peek grids
cursoragent Jul 29, 2026
5f84933
feat(sql-editor): drag to rearrange Data Peek grids
cursoragent Jul 29, 2026
2dfed00
docs(sql-editor): mention arrange handle in Data Peek hint
cursoragent Jul 29, 2026
3ba94a9
feat(sql-editor): Data Peek from result FK cells + user tips
cursoragent Jul 29, 2026
d772e7b
chore: drop unused ident in tableNamesFromSql
cursoragent Jul 29, 2026
525452a
chore: assert copyright NOTICE and SPDX headers
cursoragent Jul 29, 2026
1e133f5
chore: align LICENSE copyright notice with NOTICE
cursoragent Jul 29, 2026
f64d50b
chore: correct copyright name to Huy Phan
cursoragent Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .cursor/rules/sql-editor-agent-memory.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ FoxSchema = schema diff/migration + SQL Editor. Destinations checklist, Schema e
- **Cell banding** in Monaco: `.fox-cell-band-sql` / `.fox-cell-band-code` + left edge.
- **Statement strip**: Jupyter-like `In [n]:`, kind badge, per-cell **▶** → `execute({ statementIndices: [i] })`.
- Toolbar Run still uses strip checkboxes / selection; per-cell Play ignores both.
- Results side-by-side labels: `Out [n]: …` (`sql-result-stmt-${i}`).
- Results labels: `Out [n]: …` (`sql-result-stmt-${i}` in side-by-side; By cred stacks Out grids vertically per credential).
- Password / Safe-mode confirm preserve `statementIndices` on resume.

### 1. Data peek (Cmd/Ctrl-click)
- **UI**: `DataPeekPanel.tsx` — modal grids + breadcrumb; Esc closes.
- **Trigger**: Schema explorer Cmd/Ctrl-click on TABLE / VIEW / MQT → `openDataPeek`.
- **Queries**: `tablePreview.ts` builds SQL via `sqlTag` / `renderSqlQuery` (bound params, never string paste).
- `buildTablePreview` → `SELECT * FROM <quoted id>` (paging/cap from `/sql/execute`).
- `buildForeignKeyDrilldown` → parent WHERE with bound cell values; null if FK cols unusable.
- `foreignKeyLinksFor` — case-insensitive column match (Oracle/Db2 fold).
- **Drill**: FK cell click → `drillDataPeek` stacks another grid; use **row index** (not 1-based display #).
- **Store**: `useSqlEditorStore` `dataPeek` / `openDataPeek` / `drillDataPeek` / `closeDataPeek` / `closeDataPeekFrom`.
- Match drilled table names both qualified (`public.customers`) and bare against schema cache.
### 1. Data peek (Cmd/Ctrl-click + result FK)
- **UI**: `DataPeekPanel.tsx` — modal grids; Esc closes. Each panel: WHERE / ORDER BY / LIMIT + drag resize + arrange grip. Mounted from `SqlEditorView` (not only Schema).
- **Triggers**:
- Schema explorer Cmd/Ctrl-click TABLE / VIEW / MQT → `openDataPeek`.
- Editor result FK cell (rust underline) → `openDataPeekFromFk` (`ResultsPanel` / `foreignKeyLinksForSql`).
- **Queries**: `tablePreview.ts` — `buildTablePreview`, `buildForeignKeyDrilldown`, `composePeekSql`, `foreignKeyLinksFor` / `foreignKeyLinksForSql` / `tablesFromSql`.
- **Drill**: FK cell → `drillDataPeek` stacks panels (`drillKey` siblings); use **row index**.
- **Store**: `dataPeek` / `openDataPeek` / `openDataPeekFromFk` / `drillDataPeek` / `updateDataPeekFilters` / `pageDataPeekEntry` / `reorderDataPeekEntries` / `setDataPeekPanelHeight` / close APIs.
- **FK color**: `--fox-grid-link` rust `#9a3412` + solid underline.
- Match drilled table names both qualified and bare against schema cache.

### 2. Node `sql` bridge (`-- @node` / `-- @nodets`)
**Design**: worker has **no DB handle**. Cell calls `sql\`...\``; worker renders to `{ text, params }` and posts `cell-query` to the API parent; parent runs SQL and replies `cell-query-result`.
Expand Down
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@ A few rules that have bitten people before (the full set is in [CLAUDE.md](CLAUD
1. Branch off `main`.
2. Keep the change focused; update docs and tests alongside code.
3. Ensure the correctness gates pass (`tsc --noEmit` + `vitest run`).
4. Write a clear PR description: what changed, why, and how you verified it.
4. Write a **short, user-facing** PR description: what the user gets, why it
matters, and how you verified it. Prefer a limited summary on GitHub — do
**not** paste long internal implementation plans, agent transcripts, or
speculative design dumps into public PR bodies.
5. Keep copyright / `NOTICE` / `LICENSE` intact. Prefer the SPDX header in
[docs/COPYRIGHT_HEADER.txt](docs/COPYRIGHT_HEADER.txt) on new source files.
Authorship: Huy Phan `<huyplb@gmail.com>` (see [NOTICE](NOTICE)).

Bug reports and feature ideas → open a GitHub issue. Security vulnerabilities →
**do not** open a public issue; follow [SECURITY.md](SECURITY.md).
14 changes: 10 additions & 4 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ END OF TERMS AND CONDITIONS
## Copyright Notice

```
Copyright 2026 huyplb
Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
Copyright 2024-2026 tedious-code contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -195,16 +196,19 @@ See the License for the specific language governing permissions and
limitations under the License.
```

Also see the project root `NOTICE` file.

---

## How to Apply This License to Your Files

Add the following boilerplate notice at the top of each source file:
Add the following boilerplate notice at the top of each source file
(also in `docs/COPYRIGHT_HEADER.txt`):

```
/*
* foxSchema
* Copyright 2026 huyplb@gmail.com
* Fox Schema (foxschema)
* Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -217,6 +221,8 @@ Add the following boilerplate notice at the top of each source file:
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
```

Expand Down
15 changes: 15 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Fox Schema (foxschema)
Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
Copyright 2024-2026 tedious-code contributors

This product includes software developed by Huy Phan and contributors
to the Fox Schema project (https://github.com/tedious-code/foxschema).

Licensed under the Apache License, Version 2.0.
See the LICENSE file in the project root for full license text.

Attribution notice
------------------
Redistribution or reuse of this work must retain copyright notices and
the Apache License terms. Removing copyright, NOTICE, or LICENSE files
to conceal authorship is not permitted under the License.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,8 @@ Maintainers: [docs/PUBLISH.md](docs/PUBLISH.md) · [docs/ARCHITECTURE.md](docs/A

## License

Apache-2.0
Copyright 2024–2026 Huy Phan `<huyplb@gmail.com>` and [tedious-code](https://github.com/tedious-code)
contributors. See [NOTICE](NOTICE) and [LICENSE](LICENSE) (Apache-2.0).

Keep copyright / NOTICE / LICENSE when you fork or redistribute — stripping
authorship notices is not allowed under the License.
3 changes: 2 additions & 1 deletion apps/web/src/backend/api/sql-execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ describe('runStatements against a real SQLite file', () => {
expect(results[0].error).toMatch(/wrap boom/);
}
expect(spy).toHaveBeenCalledTimes(1);
expect(String(spy.mock.calls[0]![2])).toContain('_fox_page');
expect(String(spy.mock.calls[0]![2])).toContain('fox_page');
expect(String(spy.mock.calls[0]![2])).not.toContain('_fox_page');
} finally {
spy.mockRestore();
}
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/backend/api/sql-execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ export async function runStatements(
continue;
}

const paged = wrapSqlForPage(sql, dialect, offset, maxRows);
try {
const paged = wrapSqlForPage(sql, dialect, offset, maxRows);
const raw = await ConnectionFactory.executeOnConnection<Record<string, unknown>>(
dialect,
connection,
Expand All @@ -167,7 +167,9 @@ export async function runStatements(
const wrapMsg = error instanceof Error ? error.message : String(error);
// Fail closed for all pageable statements: raw fallback can materialize
// an unbounded result set before shapeRows truncates, and ignores OFFSET.
pushErr(`Paging failed: ${wrapMsg}`);
// Include the wrapped SQL so DB2 alias / dialect issues are diagnosable.
const preview = paged.length > 240 ? `${paged.slice(0, 240)}…` : paged;
pushErr(`Paging failed: ${wrapMsg}\nWrapped SQL: ${preview}`);
}
}
return results;
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/backend/api/sql-page-wrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ import {
describe('sql-page-wrap', () => {
it('wraps postgres-style with LIMIT/OFFSET and +1 probe', () => {
expect(wrapSqlForPage('SELECT 1;', 'postgres', 40, 20)).toBe(
'SELECT * FROM (SELECT 1) AS _fox_page LIMIT 21 OFFSET 40'
'SELECT * FROM (SELECT 1) AS fox_page LIMIT 21 OFFSET 40'
);
});

it('wraps db2 without a leading-underscore alias (SQL20521N)', () => {
const sql = wrapSqlForPage('select * from USER order by id DESC', 'db2', 0, 200);
expect(sql).toBe(
'SELECT * FROM (select * from USER order by id DESC) AS fox_page OFFSET 0 ROWS FETCH FIRST 201 ROWS ONLY'
);
expect(sql).not.toMatch(/_fox_page/);
});

it('wraps sqlserver without ORDER BY using dummy ORDER BY + OFFSET FETCH', () => {
const sql = wrapSqlForPage('SELECT id FROM t', 'sqlserver', 0, 10);
expect(sql).toContain('OFFSET 0 ROWS FETCH NEXT 11 ROWS ONLY');
Expand Down
18 changes: 14 additions & 4 deletions apps/web/src/backend/api/sql-page-wrap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/**
* Fox Schema (foxschema)
* Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*
* Wrap a statement so the engine returns a page (LIMIT/OFFSET).
* Fetches `limit + 1` rows so the caller can detect `hasNext` without a COUNT.
*
* Alias must not start with `_` — DB2 treats `_…` as a conditional-compilation
* directive (SQL20521N). Keep a plain letter-led name for all dialects.
*/

import { statementVerb } from '@foxschema/core';
Expand All @@ -11,6 +18,9 @@ const PAGEABLE_VERBS = new Set(['select', 'values']);
/** Dialects that use T-SQL OFFSET/FETCH (not MySQL/Postgres LIMIT). */
const TSQL_DIALECTS = new Set(['sqlserver', 'mssql', 'azuresql']);

/** Derived-table alias for page wraps (no leading underscore — see file header). */
const PAGE_ALIAS = 'fox_page';

export function clampOffset(v: unknown): number {
const n = typeof v === 'number' ? Math.floor(v) : Number.NaN;
if (!Number.isFinite(n) || n < 0) return 0;
Expand Down Expand Up @@ -135,16 +145,16 @@ export function wrapSqlForPage(
if (hasTopLevelOrderBy(inner)) {
return `${inner} ${fetch}`;
}
return `SELECT * FROM (${inner}) AS _fox_page ORDER BY (SELECT NULL) ${fetch}`;
return `SELECT * FROM (${inner}) AS ${PAGE_ALIAS} ORDER BY (SELECT NULL) ${fetch}`;
}
if (d === 'oracle') {
return `SELECT * FROM (${inner}) _fox_page OFFSET ${offset} ROWS FETCH NEXT ${fetchLimit} ROWS ONLY`;
return `SELECT * FROM (${inner}) ${PAGE_ALIAS} OFFSET ${offset} ROWS FETCH NEXT ${fetchLimit} ROWS ONLY`;
}
if (d === 'db2') {
return `SELECT * FROM (${inner}) AS _fox_page OFFSET ${offset} ROWS FETCH FIRST ${fetchLimit} ROWS ONLY`;
return `SELECT * FROM (${inner}) AS ${PAGE_ALIAS} OFFSET ${offset} ROWS FETCH FIRST ${fetchLimit} ROWS ONLY`;
}
// Postgres, MySQL, MariaDB, SQLite, Cockroach, Yugabyte, TiDB, DuckDB, ClickHouse-ish
return `SELECT * FROM (${inner}) AS _fox_page LIMIT ${fetchLimit} OFFSET ${offset}`;
return `SELECT * FROM (${inner}) AS ${PAGE_ALIAS} LIMIT ${fetchLimit} OFFSET ${offset}`;
}

/** After shaping, drop the probe row and set truncated/hasNext. */
Expand Down
32 changes: 20 additions & 12 deletions apps/web/src/frontend/components/sql-editor/DataGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* Fox Schema (foxschema)
* Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*
* Result DataGrid (paper surface, paging, FK drill links for Data Peek).
*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AlertTriangle, Download, GripVertical, RefreshCw } from 'lucide-react';
import type { SqlStatementResult } from '../../api/sqlApi';
Expand All @@ -24,20 +31,20 @@ const LONG_TEXT_NAME =
type CellKind = 'null' | 'number' | 'boolean' | 'datetime' | 'binary' | 'string';

const KIND_CELL_CLASS: Record<CellKind, string> = {
null: 'italic text-[#94a3b8]',
number: 'text-[#1d4ed8]',
boolean: 'text-[#7c3aed]',
datetime: 'text-[#0f766e]',
binary: 'text-[#b45309]',
string: 'text-[#1e293b]',
null: 'italic text-[var(--fox-grid-muted)]',
number: 'tabular-nums text-[var(--fox-grid-number)] font-semibold',
boolean: 'text-[var(--fox-grid-boolean)]',
datetime: 'text-[var(--fox-grid-datetime)]',
binary: 'text-[var(--fox-grid-binary)]',
string: 'text-[var(--fox-grid-ink)]',
};

const KIND_HEADER_CLASS: Record<Exclude<CellKind, 'null'>, string> = {
number: 'text-[#1d4ed8]',
boolean: 'text-[#7c3aed]',
datetime: 'text-[#0f766e]',
binary: 'text-[#b45309]',
string: 'text-[#64748b]',
number: 'text-[var(--fox-grid-number)]',
boolean: 'text-[var(--fox-grid-boolean)]',
datetime: 'text-[var(--fox-grid-datetime)]',
binary: 'text-[var(--fox-grid-binary)]',
string: 'text-[var(--fox-grid-muted)]',
};

const KIND_LABEL: Record<Exclude<CellKind, 'null'>, string> = {
Expand Down Expand Up @@ -682,7 +689,8 @@ export const DataGrid: React.FC<{
// 1-based number shown in the # column and would
// read the NEXT row's value.
onClick={() => onLinkClick?.(colIdx, i)}
className="underline decoration-dotted underline-offset-2 text-cyan-300 hover:text-cyan-200"
// Rust ID color + solid underline — pale cyan was unreadable.
className="underline decoration-solid underline-offset-2 decoration-2 decoration-[var(--fox-grid-link)] text-[var(--fox-grid-link)] font-bold hover:brightness-110"
>
{text}
</button>
Expand Down
Loading
Loading