diff --git a/docs/superpowers/plans/2026-07-05-sql-data-table-view.md b/docs/superpowers/plans/2026-07-05-sql-data-table-view.md new file mode 100644 index 0000000..9b0a0e8 --- /dev/null +++ b/docs/superpowers/plans/2026-07-05-sql-data-table-view.md @@ -0,0 +1,446 @@ +# SQL 数据表格化展示 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the raw-SQL-text preview and plain-text output in the SQL practice feature with real data tables, restructure the SQL layout to "code on top, two data tables side by side below", and eliminate the duplicate-output bug that occurs when a student's own SQL is itself a SELECT. + +**Architecture:** Candidate table data moves from a literal SQL string to structured `columns`/`rows`; the setup SQL sent to Judge0 is generated from that structured data. The submitted script now wraps the student's own statement between `.output /dev/null` and `.output stdout` so only the final auto-appended `SELECT * FROM ;` ever reaches stdout — eliminating the "two copies of data" bug. `Content.vue` grows a dedicated vertical-split branch for the `sql` language (code on top, `SqlSection` below); `SqlSection.vue` becomes a horizontal split of two `n-data-table`s (initial data vs. post-run result, with the existing error UI as fallback when the run failed). + +**Tech Stack:** Vue 3 + TypeScript, naive-ui's `n-data-table` (already a dependency, no new package needed). + +Spec: `codenext/docs/superpowers/specs/2026-07-05-sql-data-table-view-design.md` + +## Global Constraints + +- No `codeapinew/` changes — this is 100% frontend, same Judge0 SQLite (`language_id=82`) submission path as before. +- `codenext/` has no frontend test framework — verify with `npm run build` (must succeed) plus manual browser verification via `npm run start`, same convention as the rest of this codebase. +- The generated setup SQL only needs to be semantically equivalent to the old literal strings, not byte-identical — e.g. a `REAL` column value written as `299` instead of `299.0` is an accepted cosmetic difference (SQLite has no functional distinction between them for a `REAL`-affinity column). +- Every preset table's first column is `id` — this plan uses that as the `n-data-table` row key. Do not add a preset without an `id` first column without revisiting this assumption. +- No automated grading, no diff highlighting — carried over from the original feature spec, unaffected by this plan. + +--- + +### Task 1: Structured candidate-table data model + +**Files:** +- Modify: `codenext/src/data/sqlTables.ts` (full rewrite) + +**Interfaces:** +- Produces: + - `interface SqlColumn { name: string; type: string }` + - `interface SqlTablePreset { id: string; label: string; tableName: string; columns: SqlColumn[]; rows: (string | number)[][] }` (replaces the old `setupSql: string` field) + - `toSqlLiteral(value: string | number): string` + - `buildSetupSql(table: SqlTablePreset): string` + - `sqlTables: SqlTablePreset[]` (same 3 presets: students/employees/products, same data values, now structured) + - `defaultSqlTableId: string` (unchanged, `"students"`) +- Consumes: nothing from other tasks (foundational, same as before). + +- [ ] **Step 1: Rewrite `sqlTables.ts` with the structured model** + +Replace the full contents of `codenext/src/data/sqlTables.ts` with: + +```typescript +export interface SqlColumn { + name: string + type: string +} + +export interface SqlTablePreset { + id: string + label: string + tableName: string + columns: SqlColumn[] + rows: (string | number)[][] +} + +export function toSqlLiteral(value: string | number): string { + if (typeof value === "number") return String(value) + return `'${value.replace(/'/g, "''")}'` +} + +export function buildSetupSql(table: SqlTablePreset): string { + const columnDefs = table.columns + .map((column) => `${column.name} ${column.type}`) + .join(",\n ") + const columnNames = table.columns.map((column) => column.name).join(", ") + const valuesList = table.rows + .map((row) => `(${row.map(toSqlLiteral).join(", ")})`) + .join(",\n ") + return `CREATE TABLE ${table.tableName} (\n ${columnDefs}\n);\n\nINSERT INTO ${table.tableName} (${columnNames}) VALUES\n ${valuesList};` +} + +export const sqlTables: SqlTablePreset[] = [ + { + id: "students", + label: "学生成绩表", + tableName: "students", + columns: [ + { name: "id", type: "INTEGER PRIMARY KEY" }, + { name: "name", type: "TEXT NOT NULL" }, + { name: "class", type: "TEXT NOT NULL" }, + { name: "score", type: "INTEGER NOT NULL" }, + ], + rows: [ + [1, "张伟", "一班", 92], + [2, "王芳", "一班", 58], + [3, "李娜", "二班", 76], + [4, "刘洋", "二班", 45], + [5, "陈静", "一班", 88], + [6, "杨帆", "三班", 63], + [7, "赵敏", "二班", 39], + [8, "孙涛", "三班", 81], + ], + }, + { + id: "employees", + label: "员工工资表", + tableName: "employees", + columns: [ + { name: "id", type: "INTEGER PRIMARY KEY" }, + { name: "name", type: "TEXT NOT NULL" }, + { name: "department", type: "TEXT NOT NULL" }, + { name: "salary", type: "INTEGER NOT NULL" }, + { name: "hire_date", type: "TEXT NOT NULL" }, + ], + rows: [ + [1, "周明", "技术部", 12000, "2019-03-01"], + [2, "吴倩", "市场部", 8000, "2021-07-15"], + [3, "郑凯", "技术部", 15500, "2017-11-20"], + [4, "钱多多", "财务部", 9200, "2020-01-10"], + [5, "孙丽", "市场部", 7600, "2022-05-30"], + [6, "李强", "技术部", 10800, "2023-09-01"], + [7, "林小雨", "财务部", 8900, "2018-06-12"], + [8, "黄河", "市场部", 6800, "2024-02-18"], + ], + }, + { + id: "products", + label: "商品库存表", + tableName: "products", + columns: [ + { name: "id", type: "INTEGER PRIMARY KEY" }, + { name: "name", type: "TEXT NOT NULL" }, + { name: "category", type: "TEXT NOT NULL" }, + { name: "price", type: "REAL NOT NULL" }, + { name: "stock", type: "INTEGER NOT NULL" }, + ], + rows: [ + [1, "无线鼠标", "电子产品", 59.9, 120], + [2, "机械键盘", "电子产品", 299.0, 45], + [3, "保温杯", "生活用品", 39.5, 0], + [4, "笔记本", "文具", 12.0, 300], + [5, "蓝牙耳机", "电子产品", 199.0, 0], + [6, "台灯", "生活用品", 89.0, 60], + [7, "钢笔", "文具", 25.0, 150], + [8, "充电宝", "电子产品", 129.0, 8], + ], + }, +] + +export const defaultSqlTableId = sqlTables[0].id +``` + +- [ ] **Step 2: Fix the one compile error this rewrite creates** + +`src/composables/sqlTable.ts` currently reads `table.setupSql`, a field that no longer exists on `SqlTablePreset` after Step 1. Make a **minimal** compatibility edit: change that one reference to `buildSetupSql(table)`, and add `buildSetupSql` to its existing import from `"../data/sqlTables"`. Do not touch anything else in `sqlTable.ts` — the rest of its rework happens in Task 2. + +- [ ] **Step 3: Verify the build succeeds** + +Run: `cd codenext && npm run build` +Expected: build completes successfully with no TypeScript errors. + +- [ ] **Step 4: Commit** + +```bash +cd codenext +git add src/data/sqlTables.ts src/composables/sqlTable.ts +git commit -m "feat: structure candidate SQL table data as columns/rows" +``` + +--- + +### Task 2: Script building with output isolation + result parsing + +**Files:** +- Modify: `codenext/src/composables/sqlTable.ts` (full rewrite) + +**Interfaces:** +- Consumes: `buildSetupSql`, `SqlColumn`, `defaultSqlTableId`, `sqlTables` from Task 1's `../data/sqlTables`. +- Produces: + - `selectedTableId: Ref` (unchanged from before) + - `resetSqlTableSelection(): void` (unchanged from before) + - `buildSqlScript(studentSql: string): string` — same name/signature as before, new internals (wraps student SQL with `.output /dev/null` / `.output stdout`) + - `parseResultRows(output: string, columns: SqlColumn[]): Record[]` (new — used by Task 3's `SqlSection.vue`) + +- [ ] **Step 1: Rewrite `sqlTable.ts`** + +Replace the full contents of `codenext/src/composables/sqlTable.ts` with: + +```typescript +import { ref } from "vue" +import { + buildSetupSql, + defaultSqlTableId, + sqlTables, + type SqlColumn, +} from "../data/sqlTables" + +export const selectedTableId = ref(defaultSqlTableId) + +export function resetSqlTableSelection() { + selectedTableId.value = defaultSqlTableId +} + +export function buildSqlScript(studentSql: string) { + const table = + sqlTables.find((item) => item.id === selectedTableId.value) ?? + sqlTables[0] + const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";") + return [ + buildSetupSql(table), + ".output /dev/null", + normalizedSql, + ".output stdout", + `SELECT * FROM ${table.tableName};`, + ].join("\n\n") +} + +export function parseResultRows( + output: string, + columns: SqlColumn[], +): Record[] { + return output + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const cells = line.split("|") + return Object.fromEntries( + columns.map((column, index) => [column.name, cells[index] ?? ""]), + ) + }) +} +``` + +This supersedes Task 1's minimal compatibility edit to this same file — the end state of this file is exactly the code above, nothing carried over from the old version except `selectedTableId`/`resetSqlTableSelection` (unchanged) and the `buildSqlScript` name/signature (unchanged externally, new internals). + +- [ ] **Step 2: Fix the one compile error this rewrite creates** + +`SqlSection.vue` currently passes `selectedTable.setupSql` (the field Task 1's Step 2 compatibility edit changed to `buildSetupSql(table)` at the call site inside `sqlTable.ts` — but `SqlSection.vue` itself still separately reads `selectedTable.setupSql` directly for its read-only preview). Make a **minimal** patch in `codenext/src/desktop/SqlSection.vue`: change that one reference to `buildSetupSql(selectedTable)`, importing `buildSetupSql` from `"../data/sqlTables"`. Do not otherwise change `SqlSection.vue`'s structure — the full rewrite happens in Task 3. + +- [ ] **Step 3: Verify the build succeeds** + +Run: `cd codenext && npm run build` +Expected: build completes successfully with no TypeScript errors. + +- [ ] **Step 4: Commit** + +```bash +cd codenext +git add src/composables/sqlTable.ts src/desktop/SqlSection.vue +git commit -m "feat: isolate student SQL output and add result-row parsing" +``` + +--- + +### Task 3: Two-table layout (Content.vue + SqlSection.vue rewrite) + +**Files:** +- Modify: `codenext/src/desktop/Content.vue` (full rewrite) +- Modify: `codenext/src/desktop/SqlSection.vue` (full rewrite) + +**Interfaces:** +- Consumes: `parseResultRows`, `selectedTableId` from Task 2's `../composables/sqlTable`; `sqlTables` (with `columns`/`rows`) from Task 1's `../data/sqlTables`; `output`, `status` from `../composables/code` (pre-existing, unchanged); `Status` enum from `../types` (pre-existing, unchanged). +- Produces: the full end-to-end two-table SQL UI. + +- [ ] **Step 1: Rewrite `Content.vue`** + +Replace the full contents of `codenext/src/desktop/Content.vue` with: + +```vue + + + + + +``` + +Note the simplification: since the outer `v-if` now fully separates `sql` from everything else, the inner split's condition goes back to a simple `code.language !== 'turtle'` (no longer needs the `&& code.language !== 'sql'` clause, because `sql` never reaches this branch at all). + +- [ ] **Step 2: Rewrite `SqlSection.vue`** + +Replace the full contents of `codenext/src/desktop/SqlSection.vue` with: + +```vue + + + + + +``` + +Note the `watch(selectedTableId, ...)` now resets both `output` AND `status` (the pre-existing version, from the previous plan's review-fix round, only reset `output`). This is necessary now: without resetting `status`, switching tables while the previous table's last run was `Accepted` would leave the right pane trying to render `resultRows` — which re-parses the *old* `output` text against the *newly selected* table's columns, producing garbage. Resetting `status` to `NotStarted` correctly falls back to the (also-cleared) `OutputSection` instead. + +- [ ] **Step 3: Verify the build succeeds** + +Run: `cd codenext && npm run build` +Expected: build completes successfully with no TypeScript/Vue errors. + +- [ ] **Step 4: Manually verify the full end-to-end flow** + +Run: `cd codenext && npm run start`, open `http://localhost:3000`. + +1. Switch language to SQL. Confirm the layout is now top/bottom: a code editor on top (roughly 60% height), and below it two side-by-side panels — left "原始数据(学生成绩表)" showing an 8-row table with columns id/name/class/score matching the preset data, right panel empty (no run yet, falls back to the empty output box). +2. In the code editor, type `SELECT * FROM students;` (the student's own query is itself a SELECT — this is the exact scenario that used to produce duplicate output) and run. +3. **Confirm the right panel shows exactly one 8-row table** (not two copies concatenated, not 16 rows) — this is the specific bug this plan fixes. Cross-check the row count and values match the preset exactly (张伟 92, 王芳 58, ... 孙涛 81). +4. Type `UPDATE students SET score = 100 WHERE name = '王芳';` and run. Confirm the right table shows 王芳 with score 100 and all other rows unchanged, still exactly one table (not duplicated). +5. Switch the table dropdown (next to the language selector, top bar) to "员工工资表". Confirm: left panel immediately updates to the `employees` preset data (8 rows, id/name/department/salary/hire_date), right panel goes back to the empty fallback (proves the `status`/`output` reset-on-switch works — it should NOT show stale `students` data re-parsed against `employees` columns). +6. Type `DELETE FROM employees WHERE salary < 8000;` and run. Confirm the right table shows exactly 6 rows (孙丽 7600 and 黄河 6800 removed, 吴倩 8000 stays), still exactly one table. +7. Type intentionally broken SQL (e.g. `SELCT * FROM employees;`) and run. Confirm the right panel falls back to the existing error UI (运行失败 tag + 推测原因 button), NOT an empty or broken table — and the left panel's initial-data table is unaffected. +8. Resize the vertical split (drag the divider between code and tables) and the horizontal split (drag the divider between the two tables) — confirm both are draggable and don't break rendering. +9. Switch to a non-SQL language (e.g. C++) and confirm its layout (code left, input/output right) is completely unaffected by these changes. + +If step 3 or 6 shows duplicated/doubled data, or step 7 shows a broken table instead of the error fallback, STOP and report BLOCKED/DONE_WITH_CONCERNS with exact details — these are the specific defects this plan exists to fix. + +- [ ] **Step 5: Commit** + +```bash +cd codenext +git add src/desktop/Content.vue src/desktop/SqlSection.vue +git commit -m "feat: render SQL initial/result data as tables in a two-pane layout" +```