Compare commits
15
Commits
d798d56dbe
...
9b79cddfa6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b79cddfa6 | ||
|
|
edd2f706ac | ||
|
|
436f39d424 | ||
|
|
9a01d0e972 | ||
|
|
4abd5761a0 | ||
|
|
5e38c5c796 | ||
|
|
f8e59e9850 | ||
|
|
62ef50fb76 | ||
|
|
b30e8c9ee6 | ||
|
|
e5b2852fd1 | ||
|
|
89d42b8d0f | ||
|
|
fbe98c455b | ||
|
|
bc3308da51 | ||
|
|
b7c19ed5d5 | ||
|
|
c9eb5fa12f |
@@ -0,0 +1,651 @@
|
||||
# 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:** Let students pick one of three preset teaching tables, write SQL against it in the existing code editor, and see the table's resulting state after execution — with no automated grading, just observation for the teacher.
|
||||
|
||||
**Architecture:** SQL is wired in as a new language alongside python/c/cpp/turtle. Frontend composes `<preset setup SQL> + <student SQL> + SELECT * FROM <table>;` and submits it through the existing Judge0 pipeline (`language_id=82`, SQLite) — no backend changes for execution. A small backend addition (`sqlparse`) extends the existing generic `/format` endpoint to support SQL too.
|
||||
|
||||
**Tech Stack:** Vue 3 + TypeScript + CodeMirror 6 (`@codemirror/lang-sql`) on the frontend; Python `sqlparse` library on the backend (FastAPI).
|
||||
|
||||
Spec: `codenext/docs/superpowers/specs/2026-07-04-sql-crud-practice-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- SQL execution requires **zero** `codeapinew/` changes — it reuses the existing Judge0 direct-call path (`languageToId["sql"] = 82`).
|
||||
- Every run always starts from the preset's initial data (`setupSql`) — no persistent/accumulating session across runs.
|
||||
- After the student's SQL runs, always append `SELECT * FROM <table>;` to show the final table state, regardless of whether the student wrote SELECT/INSERT/UPDATE/DELETE.
|
||||
- No automated grading, no answer comparison, no before/after diff highlighting — just show the raw post-execution output text (reuse existing `OutputSection`, plain text).
|
||||
- Mobile excludes `"sql"` from the language selector, exactly like the existing `"turtle"` exclusion (mobile has no split-pane layout to host a table-picker + preview).
|
||||
- `codenext/` has no frontend test framework (no vitest/jest configured). Verify frontend tasks with `npm run build` (must succeed) plus manual browser verification via `npm run start` — do not introduce a new test runner.
|
||||
- `codeapinew/` already has `pytest` + `test_formatter.py` — use real TDD (red/green) for the backend task.
|
||||
- Risk carried from the spec: the self-hosted Judge0 instance's support for `language_id=82` (SQLite) has not been verified from code. This plan does not block on it, but Task 4's manual verification step is where it will surface if the image isn't enabled.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add `sql` language plumbing (types, Judge0 id, source, cache, share whitelist)
|
||||
|
||||
**Files:**
|
||||
- Modify: `codenext/src/types.ts:3`
|
||||
- Modify: `codenext/src/templates.ts:1-33`
|
||||
- Modify: `codenext/src/composables/code.ts:13-23,74-76`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `LANGUAGE` type now includes `"sql"`; `languageToId["sql"] === 82`; `sources.sql` (default placeholder source, used to seed `cache.code.sql` and `reset()`).
|
||||
- Consumes: nothing from other tasks (foundational).
|
||||
|
||||
- [ ] **Step 1: Add `"sql"` to the `LANGUAGE` union**
|
||||
|
||||
Edit `codenext/src/types.ts:3`:
|
||||
|
||||
```typescript
|
||||
export type LANGUAGE = "c" | "python" | "cpp" | "turtle" | "sql"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the Judge0 language id and default source for SQL**
|
||||
|
||||
Edit `codenext/src/templates.ts`. Add a new source constant near the top (after `turtleSource`):
|
||||
|
||||
```typescript
|
||||
const sqlSource = "-- 在这里编写你的 SQL 语句\n"
|
||||
```
|
||||
|
||||
Update `languageToId` and `sources`:
|
||||
|
||||
```typescript
|
||||
export const languageToId: { [key in string]: number } = {
|
||||
c: 50,
|
||||
cpp: 54,
|
||||
java: 62,
|
||||
python: 71,
|
||||
sql: 82,
|
||||
}
|
||||
|
||||
export const sources = {
|
||||
c: cSource,
|
||||
cpp: cppSource,
|
||||
java: javaSource,
|
||||
python: pythonSource,
|
||||
turtle: turtleSource,
|
||||
sql: sqlSource,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add `sql` to the code cache and the share-link language whitelist**
|
||||
|
||||
Edit `codenext/src/composables/code.ts`. In the `cache` object (around line 17-22), add:
|
||||
|
||||
```typescript
|
||||
code: {
|
||||
python: useStorage("code_python", sources["python"]),
|
||||
c: useStorage("code_c", sources["c"]),
|
||||
cpp: useStorage("code_cpp", sources["cpp"]),
|
||||
turtle: useStorage("code_turtle", sources["turtle"]),
|
||||
sql: useStorage("code_sql", sources["sql"]),
|
||||
},
|
||||
```
|
||||
|
||||
In `init()` (around line 74), extend the whitelist:
|
||||
|
||||
```typescript
|
||||
const lang = ["python", "c", "cpp", "turtle", "sql"].includes(data.lang)
|
||||
? (data.lang as LANGUAGE)
|
||||
: defaultLanguage
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify the build still succeeds**
|
||||
|
||||
Run: `cd codenext && npm run build`
|
||||
Expected: build completes successfully (same output shape as before — no new UI is reachable yet since `SelectLanguage.vue` doesn't offer `"sql"` until Task 3).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd codenext
|
||||
git add src/types.ts src/templates.ts src/composables/code.ts
|
||||
git commit -m "feat: add sql to LANGUAGE type, Judge0 mapping, and code cache"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Candidate table data + `sqlTable` composable
|
||||
|
||||
**Files:**
|
||||
- Create: `codenext/src/data/sqlTables.ts`
|
||||
- Create: `codenext/src/composables/sqlTable.ts`
|
||||
- Modify: `codenext/src/composables/code.ts:40-48,106-126`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `LANGUAGE` type from Task 1 (already includes `"sql"`).
|
||||
- Produces:
|
||||
- `SqlTablePreset` interface: `{ id: string; label: string; description: string; tableName: string; setupSql: string }`
|
||||
- `sqlTables: SqlTablePreset[]` (3 presets: `students`, `employees`, `products`)
|
||||
- `defaultSqlTableId: string` (equals `sqlTables[0].id`, i.e. `"students"`)
|
||||
- `selectedTableId: Ref<string>` (current candidate table selection, used by `SqlSection.vue` in Task 4)
|
||||
- `resetSqlTableSelection(): void`
|
||||
- `buildSqlScript(studentSql: string): string` — used by `run()` in this task
|
||||
|
||||
- [ ] **Step 1: Create the candidate table data file**
|
||||
|
||||
Create `codenext/src/data/sqlTables.ts`:
|
||||
|
||||
```typescript
|
||||
export interface SqlTablePreset {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
tableName: string
|
||||
setupSql: string
|
||||
}
|
||||
|
||||
const studentsSetupSql = `CREATE TABLE students (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
class TEXT NOT NULL,
|
||||
score INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO students (id, name, class, score) VALUES
|
||||
(1, '张伟', '一班', 92),
|
||||
(2, '王芳', '一班', 58),
|
||||
(3, '李娜', '二班', 76),
|
||||
(4, '刘洋', '二班', 45),
|
||||
(5, '陈静', '一班', 88),
|
||||
(6, '杨帆', '三班', 63),
|
||||
(7, '赵敏', '二班', 39),
|
||||
(8, '孙涛', '三班', 81);`
|
||||
|
||||
const employeesSetupSql = `CREATE TABLE employees (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
department TEXT NOT NULL,
|
||||
salary INTEGER NOT NULL,
|
||||
hire_date TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO employees (id, name, department, salary, hire_date) VALUES
|
||||
(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');`
|
||||
|
||||
const productsSetupSql = `CREATE TABLE products (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
stock INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO products (id, name, category, price, stock) VALUES
|
||||
(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 sqlTables: SqlTablePreset[] = [
|
||||
{
|
||||
id: "students",
|
||||
label: "学生成绩表",
|
||||
description: "适合练习按分数筛选、批量更新、删除不及格记录",
|
||||
tableName: "students",
|
||||
setupSql: studentsSetupSql,
|
||||
},
|
||||
{
|
||||
id: "employees",
|
||||
label: "员工工资表",
|
||||
description: "适合练习按部门分组调薪、按入职日期筛选",
|
||||
tableName: "employees",
|
||||
setupSql: employeesSetupSql,
|
||||
},
|
||||
{
|
||||
id: "products",
|
||||
label: "商品库存表",
|
||||
description: "适合练习新增商品、调整库存、下架缺货商品",
|
||||
tableName: "products",
|
||||
setupSql: productsSetupSql,
|
||||
},
|
||||
]
|
||||
|
||||
export const defaultSqlTableId = sqlTables[0].id
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the `sqlTable` composable**
|
||||
|
||||
Create `codenext/src/composables/sqlTable.ts`:
|
||||
|
||||
```typescript
|
||||
import { ref } from "vue"
|
||||
import { defaultSqlTableId, sqlTables } 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]
|
||||
return `${table.setupSql}\n\n${studentSql}\n\nSELECT * FROM ${table.tableName};`
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Wire the composable into `code.ts`'s language switch and `run()`**
|
||||
|
||||
Edit `codenext/src/composables/code.ts`. Add the import at the top:
|
||||
|
||||
```typescript
|
||||
import { buildSqlScript, resetSqlTableSelection } from "./sqlTable"
|
||||
```
|
||||
|
||||
Update the language watcher (around line 40-48) to reset the table selection when switching into SQL:
|
||||
|
||||
```typescript
|
||||
watch(
|
||||
() => code.language,
|
||||
(value: LANGUAGE) => {
|
||||
cache.language.value = value
|
||||
code.value = cache.code[value].value
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
if (value === "sql") resetSqlTableSelection()
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Update `run()` (around line 106-126) to build the combined script for SQL:
|
||||
|
||||
```typescript
|
||||
export async function run() {
|
||||
loading.value = true
|
||||
const cleanCode = code.value.trim()
|
||||
if (!cleanCode) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
if (code.language === "turtle") {
|
||||
turtleRunId.value++
|
||||
} else {
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
const sourceCode =
|
||||
code.language === "sql" ? buildSqlScript(cleanCode) : cleanCode
|
||||
const result = await submit(
|
||||
{ value: sourceCode, language: code.language },
|
||||
input.value.trim(),
|
||||
)
|
||||
output.value = result.output || ""
|
||||
status.value = result.status
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify the build still succeeds**
|
||||
|
||||
Run: `cd codenext && npm run build`
|
||||
Expected: build completes successfully. (Behavioral verification of `buildSqlScript`/`run()` happens end-to-end in Task 4, once the UI can reach the SQL language.)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd codenext
|
||||
git add src/data/sqlTables.ts src/composables/sqlTable.ts src/composables/code.ts
|
||||
git commit -m "feat: add candidate SQL table presets and script-building composable"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Editor & language selector UI (make SQL selectable + syntax highlighted)
|
||||
|
||||
**Files:**
|
||||
- Modify: `codenext/package.json`
|
||||
- Modify: `codenext/src/components/CodeEditor.vue:1-61`
|
||||
- Modify: `codenext/src/components/SelectLanguage.vue:7-25`
|
||||
- Create: `codenext/public/sql.svg`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `LANGUAGE` type from Task 1 (`"sql"` already valid).
|
||||
- Produces: `"sql"` becomes selectable in `SelectLanguage.vue`'s dropdown (desktop only); `CodeEditor.vue` renders SQL syntax highlighting when `language === "sql"`.
|
||||
|
||||
- [ ] **Step 1: Install the CodeMirror SQL language package**
|
||||
|
||||
Run: `cd codenext && npm install @codemirror/lang-sql`
|
||||
Expected: `package.json` gains a new dependency entry under `"@codemirror/lang-sql"` (npm resolves the version; do not hand-pick a version number).
|
||||
|
||||
- [ ] **Step 2: Add SQL syntax highlighting to `CodeEditor.vue`**
|
||||
|
||||
Edit `codenext/src/components/CodeEditor.vue`. Add the import near the other `@codemirror/lang-*` imports (line 2-3):
|
||||
|
||||
```typescript
|
||||
import { sql } from "@codemirror/lang-sql"
|
||||
```
|
||||
|
||||
Update `langExtension` (line 56-61):
|
||||
|
||||
```typescript
|
||||
const langExtension = computed(() => {
|
||||
if (props.language === "python" || props.language === "turtle") {
|
||||
return python()
|
||||
}
|
||||
if (props.language === "sql") {
|
||||
return sql()
|
||||
}
|
||||
return cpp()
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the SQL option to the language selector, excluded on mobile like turtle**
|
||||
|
||||
Edit `codenext/src/components/SelectLanguage.vue`. Update `allLangs` and the mobile filter (line 7-18):
|
||||
|
||||
```typescript
|
||||
const LANGS = computed(() => {
|
||||
const allLangs = [
|
||||
["python", "Python"],
|
||||
["turtle", "海龟绘图"],
|
||||
["c", "C 语言"],
|
||||
["cpp", "C++"],
|
||||
["sql", "SQL"],
|
||||
]
|
||||
if (isMobile.value) {
|
||||
return allLangs.filter(([lang]) => lang !== "turtle" && lang !== "sql")
|
||||
}
|
||||
return allLangs
|
||||
})
|
||||
```
|
||||
|
||||
Update the mobile auto-switch watcher (line 20-25) to also cover SQL:
|
||||
|
||||
```typescript
|
||||
// 如果当前在移动端且语言是海龟绘图或 SQL,自动切换到 Python
|
||||
watch(isMobile, (mobile) => {
|
||||
if (mobile && (code.language === "turtle" || code.language === "sql")) {
|
||||
code.language = "python"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the SQL language icon**
|
||||
|
||||
Create `codenext/public/sql.svg`:
|
||||
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="#a9b9cb" d="M12 3c-4.97 0-9 1.343-9 3v12c0 1.657 4.03 3 9 3s9-1.343 9-3V6c0-1.657-4.03-3-9-3z"/><path fill="#7f8b99" d="M3 6v3c0 1.657 4.03 3 9 3s9-1.343 9-3V6c0 1.657-4.03 3-9 3S3 7.657 3 6z"/><path fill="#7f8b99" d="M3 11v3c0 1.657 4.03 3 9 3s9-1.343 9-3v-3c0 1.657-4.03 3-9 3s-9-1.343-9-3z"/></svg>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify the build succeeds**
|
||||
|
||||
Run: `cd codenext && npm run build`
|
||||
Expected: build completes successfully.
|
||||
|
||||
- [ ] **Step 6: Manually verify the language selector and syntax highlighting**
|
||||
|
||||
Run: `cd codenext && npm run start`, open `http://localhost:3000` in a browser.
|
||||
- Open the language dropdown (desktop layout) — confirm "SQL" appears with the new icon and is selectable.
|
||||
- Select SQL — confirm the editor now does SQL syntax highlighting (keywords colored) instead of C++ highlighting.
|
||||
- Shrink the window to mobile width (or use device toolbar) — confirm "SQL" is absent from the dropdown, matching turtle's existing mobile behavior.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
cd codenext
|
||||
git add package.json package-lock.json src/components/CodeEditor.vue src/components/SelectLanguage.vue public/sql.svg
|
||||
git commit -m "feat: make SQL selectable as a language with syntax highlighting"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `SqlSection` UI + `Content.vue` wiring (end-to-end feature)
|
||||
|
||||
**Files:**
|
||||
- Create: `codenext/src/desktop/SqlSection.vue`
|
||||
- Modify: `codenext/src/desktop/Content.vue`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `selectedTableId`, `sqlTables` (Task 2); `code.language`, `run()` (Task 1/2, via `composables/code.ts`); `OutputSection.vue` (existing, unmodified).
|
||||
- Produces: fully working SQL practice flow reachable from the desktop UI.
|
||||
|
||||
- [ ] **Step 1: Create `SqlSection.vue`**
|
||||
|
||||
Create `codenext/src/desktop/SqlSection.vue`:
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import CodeEditor from "../components/CodeEditor.vue"
|
||||
import { selectedTableId } from "../composables/sqlTable"
|
||||
import { sqlTables } from "../data/sqlTables"
|
||||
import OutputSection from "./OutputSection.vue"
|
||||
|
||||
const tableOptions = computed(() =>
|
||||
sqlTables.map((table) => ({ value: table.id, label: table.label })),
|
||||
)
|
||||
|
||||
const selectedTable = computed(
|
||||
() =>
|
||||
sqlTables.find((table) => table.id === selectedTableId.value) ??
|
||||
sqlTables[0],
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical class="sql-section">
|
||||
<n-select
|
||||
class="table-select"
|
||||
v-model:value="selectedTableId"
|
||||
:options="tableOptions"
|
||||
/>
|
||||
<n-split
|
||||
direction="vertical"
|
||||
:default-size="1 / 3"
|
||||
:min="1 / 5"
|
||||
:max="3 / 5"
|
||||
>
|
||||
<template #1>
|
||||
<CodeEditor
|
||||
:model-value="selectedTable.setupSql"
|
||||
language="sql"
|
||||
readonly
|
||||
label="表结构与初始数据"
|
||||
/>
|
||||
</template>
|
||||
<template #2>
|
||||
<OutputSection />
|
||||
</template>
|
||||
</n-split>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sql-section {
|
||||
height: 100%;
|
||||
}
|
||||
.table-select {
|
||||
margin: 12px 20px 0;
|
||||
width: 160px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Wire `SqlSection` into `Content.vue`**
|
||||
|
||||
Edit `codenext/src/desktop/Content.vue`:
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { code } from "../composables/code"
|
||||
import CodeSection from "./CodeSection.vue"
|
||||
import InputSection from "./InputSection.vue"
|
||||
import OutputSection from "./OutputSection.vue"
|
||||
import SqlSection from "./SqlSection.vue"
|
||||
import TurtleSection from "./TurtleSection.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout-content class="container">
|
||||
<n-split direction="horizontal" :min="1 / 3" :max="4 / 5">
|
||||
<template #1>
|
||||
<CodeSection />
|
||||
</template>
|
||||
<template #2>
|
||||
<n-split
|
||||
v-if="code.language !== 'turtle' && code.language !== 'sql'"
|
||||
direction="vertical"
|
||||
:default-size="1 / 3"
|
||||
:min="1 / 5"
|
||||
:max="3 / 5"
|
||||
>
|
||||
<template #1>
|
||||
<InputSection />
|
||||
</template>
|
||||
<template #2>
|
||||
<OutputSection />
|
||||
</template>
|
||||
</n-split>
|
||||
<TurtleSection v-else-if="code.language === 'turtle'" />
|
||||
<SqlSection v-else />
|
||||
</template>
|
||||
</n-split>
|
||||
</n-layout-content>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the build succeeds**
|
||||
|
||||
Run: `cd codenext && npm run build`
|
||||
Expected: build completes successfully.
|
||||
|
||||
- [ ] **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 right pane now shows: a table dropdown (defaulting to "学生成绩表"), a read-only preview of the `students` setup SQL below it, and an (empty) output panel at the bottom.
|
||||
2. In the left editor, type: `UPDATE students SET score = 100 WHERE name = '王芳';`
|
||||
3. Click run (or press F5). Confirm the output panel shows the full `students` table with 王芳's score now `100` and all other rows unchanged.
|
||||
4. Switch the table dropdown to "员工工资表". Confirm the preview panel updates to show the `employees` setup SQL.
|
||||
5. Type `DELETE FROM employees WHERE salary < 8000;` and run. Confirm the output shows the `employees` table with the two lowest-salary rows (吴倩 8000 stays if not `<` 8000 — confirm rows below 8000, i.e. 孙丽 7600 and 黄河 6800, are gone) and the rest intact.
|
||||
6. Re-run without changing anything. Confirm the result is identical each time (proves it always restarts from the preset data rather than accumulating state).
|
||||
7. Type intentionally broken SQL (e.g. `SELCT * FROM students;`) and run. Confirm an error surfaces through the existing error/AI-analysis UI, the same way a C/C++ compile error would.
|
||||
8. **If this step fails with a Judge0 "language not supported" error:** this surfaces the risk noted in the spec — the self-hosted Judge0 instance needs `language_id=82` (SQLite) enabled. Stop and flag this to the user; it's an infra fix, not a code fix.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd codenext
|
||||
git add src/desktop/SqlSection.vue src/desktop/Content.vue
|
||||
git commit -m "feat: add SqlSection UI wired into the desktop editor layout"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Backend SQL formatting via `sqlparse`
|
||||
|
||||
**Files:**
|
||||
- Modify: `codeapinew/formatter.py`
|
||||
- Modify: `codeapinew/pyproject.toml`
|
||||
- Modify: `codeapinew/requirements.txt`
|
||||
- Modify: `codeapinew/test_formatter.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing from other tasks (independent of the frontend tasks).
|
||||
- Produces: `format_code(code, "sql")` returns sqlparse-formatted SQL instead of raising `FormatError`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Add to `codeapinew/test_formatter.py`:
|
||||
|
||||
```python
|
||||
def test_format_sql_uppercases_keywords():
|
||||
result = format_code("select * from students where score>60", "sql")
|
||||
assert result == "SELECT * FROM students WHERE score>60"
|
||||
|
||||
|
||||
def test_format_sql_splits_multiple_statements():
|
||||
result = format_code(
|
||||
"delete from students where score<60;insert into students (id) values (9);",
|
||||
"sql",
|
||||
)
|
||||
assert result == (
|
||||
"DELETE FROM students WHERE score<60;\n\n"
|
||||
"INSERT INTO students (id) VALUES (9);"
|
||||
)
|
||||
|
||||
|
||||
def test_format_sql_tolerates_syntax_errors():
|
||||
result = format_code("select from where", "sql")
|
||||
assert result == "SELECT FROM WHERE"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `cd codeapinew && uv run pytest test_formatter.py -v -k sql`
|
||||
Expected: 3 failures — `format_code` raises `FormatError: 不支持的语言: sql` (the `sql` branch doesn't exist yet).
|
||||
|
||||
- [ ] **Step 3: Add the `sqlparse` dependency**
|
||||
|
||||
Run: `cd codeapinew && uv add sqlparse`
|
||||
Expected: `pyproject.toml` gains `sqlparse` under `dependencies`, `uv.lock` updates.
|
||||
|
||||
Check the resolved version:
|
||||
|
||||
Run: `grep sqlparse pyproject.toml uv.lock | head -5`
|
||||
|
||||
Add the matching pinned entry to `codeapinew/requirements.txt` (insert alphabetically, matching the existing pin style, e.g. `sqlparse==<resolved-version>`).
|
||||
|
||||
- [ ] **Step 4: Implement the `sql` branch in `formatter.py`**
|
||||
|
||||
Edit `codeapinew/formatter.py`. Add the import at the top:
|
||||
|
||||
```python
|
||||
import sqlparse
|
||||
```
|
||||
|
||||
Add the helper function (near `_run`, before `format_code`):
|
||||
|
||||
```python
|
||||
def _format_with_sql(code: str) -> str:
|
||||
# sqlparse 对语法错误宽容,不会抛异常,语法问题留给判题阶段反馈
|
||||
# strip_whitespace 会把多条语句压成一行,先按分号拆分再逐条格式化
|
||||
statements = sqlparse.split(code)
|
||||
return "\n\n".join(
|
||||
sqlparse.format(s, strip_whitespace=True, keyword_case="upper")
|
||||
for s in statements
|
||||
)
|
||||
```
|
||||
|
||||
Add the branch inside `format_code` (before the final `raise FormatError`):
|
||||
|
||||
```python
|
||||
if language == "sql":
|
||||
return _format_with_sql(code)
|
||||
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the tests to verify they pass**
|
||||
|
||||
Run: `cd codeapinew && uv run pytest test_formatter.py -v`
|
||||
Expected: all tests pass, including the 3 new `sql` tests and the pre-existing python/c/cpp/unsupported-language tests (no regressions).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd codeapinew
|
||||
git add formatter.py pyproject.toml uv.lock requirements.txt test_formatter.py
|
||||
git commit -m "feat: format SQL code via sqlparse in the /format endpoint"
|
||||
```
|
||||
@@ -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 <table>;` 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<string>` (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<string, string | number>[]` (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<string, string | number>[] {
|
||||
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
|
||||
<script lang="ts" setup>
|
||||
import { code } from "../composables/code"
|
||||
import CodeSection from "./CodeSection.vue"
|
||||
import InputSection from "./InputSection.vue"
|
||||
import OutputSection from "./OutputSection.vue"
|
||||
import SqlSection from "./SqlSection.vue"
|
||||
import TurtleSection from "./TurtleSection.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout-content class="container">
|
||||
<n-split
|
||||
v-if="code.language === 'sql'"
|
||||
direction="vertical"
|
||||
:default-size="3 / 5"
|
||||
:min="1 / 4"
|
||||
:max="3 / 4"
|
||||
>
|
||||
<template #1>
|
||||
<CodeSection />
|
||||
</template>
|
||||
<template #2>
|
||||
<SqlSection />
|
||||
</template>
|
||||
</n-split>
|
||||
<n-split v-else direction="horizontal" :min="1 / 3" :max="4 / 5">
|
||||
<template #1>
|
||||
<CodeSection />
|
||||
</template>
|
||||
<template #2>
|
||||
<n-split
|
||||
v-if="code.language !== 'turtle'"
|
||||
direction="vertical"
|
||||
:default-size="1 / 3"
|
||||
:min="1 / 5"
|
||||
:max="3 / 5"
|
||||
>
|
||||
<template #1>
|
||||
<InputSection />
|
||||
</template>
|
||||
<template #2>
|
||||
<OutputSection />
|
||||
</template>
|
||||
</n-split>
|
||||
<TurtleSection v-else />
|
||||
</template>
|
||||
</n-split>
|
||||
</n-layout-content>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
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
|
||||
<script lang="ts" setup>
|
||||
import type { DataTableColumns } from "naive-ui"
|
||||
import { computed, watch } from "vue"
|
||||
import { output, status } from "../composables/code"
|
||||
import { parseResultRows, selectedTableId } from "../composables/sqlTable"
|
||||
import { sqlTables } from "../data/sqlTables"
|
||||
import { Status } from "../types"
|
||||
import OutputSection from "./OutputSection.vue"
|
||||
|
||||
const selectedTable = computed(
|
||||
() =>
|
||||
sqlTables.find((table) => table.id === selectedTableId.value) ??
|
||||
sqlTables[0],
|
||||
)
|
||||
|
||||
const tableColumns = computed<DataTableColumns>(() =>
|
||||
selectedTable.value.columns.map((column) => ({
|
||||
title: column.name,
|
||||
key: column.name,
|
||||
})),
|
||||
)
|
||||
|
||||
const initialRows = computed(() =>
|
||||
selectedTable.value.rows.map((row) =>
|
||||
Object.fromEntries(
|
||||
selectedTable.value.columns.map((column, index) => [
|
||||
column.name,
|
||||
row[index],
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const resultRows = computed(() =>
|
||||
parseResultRows(output.value, selectedTable.value.columns),
|
||||
)
|
||||
|
||||
watch(selectedTableId, () => {
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-split
|
||||
direction="horizontal"
|
||||
:default-size="1 / 2"
|
||||
:min="1 / 4"
|
||||
:max="3 / 4"
|
||||
>
|
||||
<template #1>
|
||||
<div class="table-panel">
|
||||
<div class="panel-title">原始数据({{ selectedTable.label }})</div>
|
||||
<n-data-table
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:columns="tableColumns"
|
||||
:data="initialRows"
|
||||
:row-key="(row: any) => row.id"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #2>
|
||||
<div class="table-panel" v-if="status === Status.Accepted">
|
||||
<div class="panel-title">运行后数据</div>
|
||||
<n-data-table
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:columns="tableColumns"
|
||||
:data="resultRows"
|
||||
:row-key="(row: any) => row.id"
|
||||
/>
|
||||
</div>
|
||||
<OutputSection v-else />
|
||||
</template>
|
||||
</n-split>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-panel {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 12px 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
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"
|
||||
```
|
||||
@@ -0,0 +1,93 @@
|
||||
# SQL 增删改查练习功能 设计文档
|
||||
|
||||
日期:2026-07-04
|
||||
|
||||
## 背景与目标
|
||||
|
||||
老师需要在现有在线代码编辑器(codenext)中考察学生的 SQL 增删改查(CRUD)能力。功能不是自动判题/评分系统,而是一个"候选数据表 + SQL 执行 + 结果展示"的沙箱:学生从几张预设的教学表中选一张,写 SQL 语句,执行后看到该表的最终状态,由老师人工判断学生写得对不对。
|
||||
|
||||
## 范围
|
||||
|
||||
- SQL 执行本身只涉及 `codenext/`(前端),复用现有的 Judge0 直连链路,与 C/C++ 的执行方式一致,不需要改动 `codeapinew/`。
|
||||
- 代码格式化("整理"按钮)需要给 `codeapinew/formatter.py` 新增 `sql` 分支(用 `sqlparse` 库),是本次唯一涉及后端的改动,见"代码格式化"一节。
|
||||
- 不做自动判题/评分、不做前后对比高亮、不做多语句持久会话。这些是明确排除的非目标(见"已排除的方案")。
|
||||
|
||||
## 架构与执行流程
|
||||
|
||||
将 SQL 作为一门新语言接入现有语言体系(与 python / c / cpp / turtle 同级),复用 `api.ts` 中现有的 Judge0 提交链路。
|
||||
|
||||
每次点击"运行"时:
|
||||
|
||||
1. 学生已从候选表下拉框选定一张表(如 `students`)。
|
||||
2. 学生在编辑器中只写自己的 SQL(如 `DELETE FROM students WHERE score < 60;`)。
|
||||
3. 前端拼接完整脚本:`<该候选表的建表+初始数据 SQL>` + `\n` + `<学生的 SQL>` + `\n` + `SELECT * FROM <该表名>;`(自动追加)。
|
||||
4. 整体脚本作为 source,以 `language_id = 82`(Judge0 的 SQLite)提交执行。
|
||||
5. 每次提交都是全新的临时 SQLite 数据库,因此天然满足"每次都从预设初始数据重新执行"的要求,不需要任何服务端状态管理。
|
||||
6. Judge0 返回的 stdout(即自动追加的那条 `SELECT *` 的结果)作为执行结果展示。
|
||||
|
||||
**统一处理原则**:无论学生写的是 INSERT / UPDATE / DELETE 还是 SELECT,都统一在脚本末尾追加一次针对该候选表的 `SELECT * FROM 表名;` 来展示表的最终状态。这样处理逻辑不需要按语句类型分支。如果学生自己写的也是 SELECT,输出里会同时包含学生查询的结果和追加查询的表状态,两者都展示,不做去重。
|
||||
|
||||
## 候选数据表内容
|
||||
|
||||
新增配置文件 `codenext/src/data/sqlTables.ts`(格式参考现有 `templates.ts`),包含 3 张独立的单表教学数据:
|
||||
|
||||
1. **`students`(学生成绩表)**:`id, name, class, score`,8 条示例数据,分数、班级有区分度,适合 WHERE / UPDATE / DELETE 练习。
|
||||
2. **`employees`(员工工资表)**:`id, name, department, salary, hire_date`,8 条示例数据,适合按部门分组、调整工资、按日期筛选。
|
||||
3. **`products`(商品库存表)**:`id, name, category, price, stock`,8 条示例数据,适合新增商品(INSERT)、调整库存(UPDATE)、下架缺货商品(DELETE)。
|
||||
|
||||
每张表都是独立单表,不带外键关联,保持题目简单易懂。每条配置包含:`{ id, label, description, tableName, setupSql, previewRows }`。
|
||||
|
||||
## UI 改动
|
||||
|
||||
沿用"每种语言一套专属布局"的现有模式(Turtle 已是先例):
|
||||
|
||||
- **`SelectLanguage.vue`**:语言列表新增 `["sql", "SQL"]`。
|
||||
- **`CodeEditor.vue`**:`language === "sql"` 时引入 `@codemirror/lang-sql`(新依赖)做语法高亮,替换现有 python()/cpp() 的判断分支。
|
||||
- **`Content.vue`**:新增分支 `v-else-if="code.language === 'sql'"`,渲染新组件 `SqlSection.vue`,替代原有的 Input/Output 分栏(SQL 场景不需要 stdin)。
|
||||
|
||||
**`SqlSection.vue` 内部结构**:
|
||||
1. 顶部:候选表下拉选择器。
|
||||
2. 表结构 + 初始数据只读预览(帮助学生了解字段和数据,避免瞎猜)。
|
||||
3. 中间:复用现有 `CodeSection`(学生写 SQL 的编辑器,本身不需要改动)。
|
||||
4. 底部:结果区,复用现有 `OutputSection`(纯文本展示 Judge0 返回的 stdout),不做高亮/diff,保持简单。
|
||||
|
||||
**其他必改点**(沿用现有语言接入的固定套路):
|
||||
- `types.ts`:`LANGUAGE` 联合类型新增 `"sql"`。
|
||||
- `templates.ts`:`languageToId` 新增 `sql: 82`。
|
||||
- `composables/code.ts`:`cache.code` 及分享链接(`?share=`)白名单加入 `sql`。
|
||||
- 新增 `composables/sqlTable.ts`:管理"当前选中的候选表"这一状态(选中的表 id,随语言切换重置为默认表)。
|
||||
|
||||
后端 `codeapinew` 不需要任何改动。
|
||||
|
||||
## 代码格式化("整理"按钮)
|
||||
|
||||
现有"整理"按钮(`CodeSection.vue:81`)已经是语言无关的通用功能:`code.ts` 的 `format()` 调用后端 `/format` 接口,`codeapinew/formatter.py` 的 `format_code()` 按 `language` 分发到不同的格式化工具(python/turtle 用 `ruff format`,c/cpp 用 `clang-format`)。前端不需要任何改动,只需给 `format_code()` 新增 `sql` 分支:
|
||||
|
||||
```python
|
||||
def _format_with_sql(code: str) -> str:
|
||||
# sqlparse 对语法错误宽容,不会抛异常,语法问题留给判题阶段反馈
|
||||
# strip_whitespace 会把多条语句压成一行,先按分号拆分再逐条格式化
|
||||
statements = sqlparse.split(code)
|
||||
return "\n\n".join(
|
||||
sqlparse.format(s, strip_whitespace=True, keyword_case="upper")
|
||||
for s in statements
|
||||
)
|
||||
```
|
||||
|
||||
在 `format_code()` 里加入 `if language == "sql": return _format_with_sql(code)` 分支即可。需要给 `codeapinew` 新增依赖 `sqlparse`。
|
||||
|
||||
## 已排除的方案(非目标)
|
||||
|
||||
- **不做自动判题/评分**:没有"标准答案"比对,考察由老师人工完成。
|
||||
- **不做持久会话/状态累积**:每次运行都基于预设初始数据重新执行,不支持像真实数据库会话那样跨多次提交累积状态(若未来需要,需引入客户端 SQLite WASM,如 sql.js,这是一次单独的架构变更)。
|
||||
- **不做前后对比高亮**:只展示执行后的最终表状态文本,不做逐行 diff 或颜色标注。
|
||||
|
||||
## 风险与待确认事项
|
||||
|
||||
- **需要确认自建 Judge0 实例是否已启用 `language_id = 82`(SQLite)**。这一点无法从代码层面确认,需要访问该 Judge0 实例的 `/languages` 接口核实;若未启用,需要找 Judge0 管理员开启对应语言镜像后此功能才能工作。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 手动验证:针对三张候选表分别执行 INSERT / UPDATE / DELETE / SELECT,确认输出符合预期。
|
||||
- 回归验证:语言切换、分享链接(`?share=`)、localStorage 缓存机制在新增 `sql` 语言后,对其他既有语言(python/c/cpp/turtle)无影响。
|
||||
- "整理"按钮:验证多语句(含分号分隔)、关键字大小写混用、含语法错误的 SQL 格式化后不报错、行为符合预期。
|
||||
@@ -0,0 +1,95 @@
|
||||
# SQL 数据表格化展示 设计文档
|
||||
|
||||
日期:2026-07-05
|
||||
|
||||
## 背景与目标
|
||||
|
||||
`2026-07-04-sql-crud-practice-design.md` 实现的 SQL 练习功能里,候选表的初始数据预览和执行结果都是纯文本/代码形式(读只 CodeEditor 显示 `CREATE TABLE`/`INSERT` 语句,输出框显示 Judge0 返回的纯文本)。但 SQL 练习真正关心的是"数据本身",纯文本不够直观。本设计把这两处都换成真正的数据表格,同时调整整体布局。
|
||||
|
||||
## 范围
|
||||
|
||||
仅涉及 `codenext/`(前端)。不需要改动 `codeapinew/` 后端或 Judge0 提交方式(仍然是同一个 Judge0 SQLite 直连链路)。
|
||||
|
||||
## 数据模型改造
|
||||
|
||||
`src/data/sqlTables.ts` 里 `SqlTablePreset` 从"一整段写死的 SQL 字符串"改为结构化数据:
|
||||
|
||||
```typescript
|
||||
export interface SqlColumn {
|
||||
name: string
|
||||
type: string // 如 "INTEGER PRIMARY KEY", "TEXT NOT NULL"
|
||||
}
|
||||
|
||||
export interface SqlTablePreset {
|
||||
id: string
|
||||
label: string
|
||||
tableName: string
|
||||
columns: SqlColumn[]
|
||||
rows: (string | number)[][] // 每行一个数组,顺序对应 columns
|
||||
}
|
||||
```
|
||||
|
||||
新增两个纯函数(同文件导出):
|
||||
|
||||
- `toSqlLiteral(value: string | number): string` — 数字原样转字符串,字符串加单引号并转义内部单引号。
|
||||
- `buildSetupSql(table: SqlTablePreset): string` — 用 `columns`/`rows` 拼出 `CREATE TABLE ... ; INSERT INTO ... VALUES ...;`,供提交 Judge0 时使用(原先这段是写死的字符串常量,现在改为运行时生成,生成结果在语义上与原常量等价)。
|
||||
|
||||
3 张候选表(students/employees/products)的列结构和数据值保持不变,只是从字符串形式换成结构化形式。
|
||||
|
||||
## 布局改造
|
||||
|
||||
SQL 语言不再复用其他语言共用的"左代码、右内容"横向分栏模板,改为在 `Content.vue` 里单独走一条纵向分栏分支:
|
||||
|
||||
```
|
||||
Content.vue(language === "sql" 分支):
|
||||
┌─────────────────────────────┐
|
||||
│ CodeSection │ ← 上:SQL 输入框(全宽),default-size 3/5
|
||||
├───────────────┬─────────────┤
|
||||
│ 原始数据表 │ 运行后数据表 │ ← 下:SqlSection 内部横向 n-split,各占一半
|
||||
└───────────────┴─────────────┘
|
||||
```
|
||||
|
||||
- 顶层 `n-split direction="vertical"`:`v-if="code.language === 'sql'"` 时 `#1=CodeSection`、`#2=SqlSection`;其余语言维持原有 `n-split direction="horizontal"` 分支(CodeSection + 各自的右侧内容),不受影响。
|
||||
- `SqlSection.vue` 内部改为横向 `n-split`:`#1` 渲染候选表的初始数据(`n-data-table`),`#2` 渲染运行结果(成功时 `n-data-table`,失败时回退现有 `OutputSection`)。
|
||||
- 表格组件统一用 naive-ui 的 `n-data-table`(已是现有依赖,自动适配深浅色主题)。
|
||||
- 顶部语言选择器旁边的候选表下拉框(`SelectSqlTable.vue`,已在上一版实现)位置不变。
|
||||
|
||||
## 运行结果解析与去重
|
||||
|
||||
**问题**:原先脚本拼接方式是 `setupSql + 学生SQL + 追加的 SELECT * FROM 表;`。如果学生自己写的语句也是 SELECT(对初学者来说非常常见,比如上来就写 `SELECT * FROM students;`),Judge0 返回的 stdout 会包含两段几乎重复的结果——学生自己查询的输出,加上自动追加查询的输出——纯文本时代不易察觉,表格化后会非常明显地表现为"重复的两组数据"。
|
||||
|
||||
**解决方案**:利用 sqlite3 CLI 支持在同一份输入脚本里混用点命令的特性,在学生语句执行前后临时静默输出:
|
||||
|
||||
```
|
||||
<setupSql>
|
||||
|
||||
.output /dev/null
|
||||
|
||||
<学生的 SQL(规范化,确保以且仅以一个分号结尾)>
|
||||
|
||||
.output stdout
|
||||
|
||||
SELECT * FROM <表名>;
|
||||
```
|
||||
|
||||
`.output /dev/null` 只重定向"查询结果"的输出目标,不影响 sqlite3 shell 报错信息(走 stderr,仍会被 Judge0 的 `redirect_stderr_to_stdout: true` 合并进 stdout)。这样无论学生写什么语句,stdout 里能看到的查询结果永远只有最后那条自动追加的 `SELECT * FROM 表;`,彻底消除重复问题,也不需要任何"猜哪一段是最终结果"的启发式解析。
|
||||
|
||||
`buildSqlScript`(`src/composables/sqlTable.ts`)改为按上述模板拼接。
|
||||
|
||||
**解析**(新增 `parseResultRows(output: string, columns: SqlColumn[])`,与 `buildSqlScript` 同文件):状态为 `Accepted` 时,把 `output` 按行分割、去空行,每行按 `|` 切开,按 `columns` 顺序映射成对象数组,直接喂给 `n-data-table`。状态非 `Accepted`(SQL 写错)时,右侧不解析、不渲染表格,回退显示现有的 `OutputSection`(错误文本 + "运行失败" 标签 + AI"推测原因"面板,和 C/C++ 报错一致)。
|
||||
|
||||
## 已排除的方案(非目标)
|
||||
|
||||
- 不再展示原始 `CREATE TABLE`/`INSERT` 语句文本(连列类型都不单独展示)——这是本次改造的明确取舍,学生只看数据本身,不看 DDL。
|
||||
- 不改变判题/评分方式——依然是纯观察沙箱,无自动判分。
|
||||
- 不改变执行方式——依然是每次从预设数据重新执行、不做多语句持久会话(沿用上一版设计)。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 手动验证(浏览器 + 真实 Judge0):
|
||||
- 切换到 SQL,确认布局为上下结构(上:代码框;下:左原始数据表、右空结果表)
|
||||
- 对 students 表执行 `UPDATE`,确认右侧表格只显示一份最终数据,且改动的行是对的
|
||||
- 学生自己写 `SELECT * FROM students;` 再运行,确认右侧表格**不再出现重复的两组数据**(这是本次改造要修的问题)
|
||||
- 切换候选表,确认左右两个表格都随之更新
|
||||
- 故意写错 SQL,确认右侧回退为现有错误文本/AI 推测面板,左侧原始数据表不受影响
|
||||
- 回归:确认其他语言(python/c/cpp/turtle)的布局和行为不受影响(`Content.vue` 的改动只新增了 sql 专属分支,其余分支原样保留)
|
||||
Generated
+52
-3
@@ -11,6 +11,7 @@
|
||||
"@codemirror/autocomplete": "^6.20.2",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"axios": "^1.16.0",
|
||||
"client-zip": "2.5.0",
|
||||
@@ -129,6 +130,20 @@
|
||||
"@lezer/python": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-sql": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
|
||||
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
|
||||
@@ -205,9 +220,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -490,6 +505,40 @@
|
||||
"@napi-rs/wasm-runtime": "1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-arm64-msvc": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.2.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@codemirror/autocomplete": "^6.20.2",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"axios": "^1.16.0",
|
||||
"client-zip": "2.5.0",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="#a9b9cb" d="M12 3c-4.97 0-9 1.343-9 3v12c0 1.657 4.03 3 9 3s9-1.343 9-3V6c0-1.657-4.03-3-9-3z"/><path fill="#7f8b99" d="M3 6v3c0 1.657 4.03 3 9 3s9-1.343 9-3V6c0 1.657-4.03 3-9 3S3 7.657 3 6z"/><path fill="#7f8b99" d="M3 11v3c0 1.657 4.03 3 9 3s9-1.343 9-3v-3c0 1.657-4.03 3-9 3s-9-1.343-9-3z"/></svg>
|
||||
|
After Width: | Height: | Size: 399 B |
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { cpp } from "@codemirror/lang-cpp"
|
||||
import { python } from "@codemirror/lang-python"
|
||||
import { sql } from "@codemirror/lang-sql"
|
||||
import { EditorState } from "@codemirror/state"
|
||||
import { EditorView } from "@codemirror/view"
|
||||
import { autocompletion, completeAnyWord } from "@codemirror/autocomplete"
|
||||
@@ -57,11 +58,13 @@ const langExtension = computed(() => {
|
||||
if (props.language === "python" || props.language === "turtle") {
|
||||
return python()
|
||||
}
|
||||
if (props.language === "sql") {
|
||||
return sql()
|
||||
}
|
||||
return cpp()
|
||||
})
|
||||
|
||||
const enhanceAutoCompletion = computed(() => {
|
||||
console.log(props.language)
|
||||
return autocompletion({
|
||||
override: [enhanceCompletion(props.language), completeAnyWord],
|
||||
})
|
||||
|
||||
@@ -10,16 +10,17 @@ const LANGS = computed(() => {
|
||||
["turtle", "海龟绘图"],
|
||||
["c", "C 语言"],
|
||||
["cpp", "C++"],
|
||||
["sql", "SQL"],
|
||||
]
|
||||
if (isMobile.value) {
|
||||
return allLangs.filter(([lang]) => lang !== "turtle")
|
||||
return allLangs.filter(([lang]) => lang !== "turtle" && lang !== "sql")
|
||||
}
|
||||
return allLangs
|
||||
})
|
||||
|
||||
// 如果当前在移动端且语言是海龟绘图,自动切换到 Python
|
||||
// 如果当前在移动端且语言是海龟绘图或 SQL,自动切换到 Python
|
||||
watch(isMobile, (mobile) => {
|
||||
if (mobile && code.language === "turtle") {
|
||||
if (mobile && (code.language === "turtle" || code.language === "sql")) {
|
||||
code.language = "python"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SelectOption } from "naive-ui"
|
||||
import { computed } from "vue"
|
||||
import { selectedTableId } from "../composables/sqlTable"
|
||||
import { sqlTables } from "../data/sqlTables"
|
||||
|
||||
const tableOptions = computed<SelectOption[]>(() =>
|
||||
sqlTables.map((table) => ({ value: table.id, label: table.label })),
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<n-select
|
||||
class="select"
|
||||
:options="tableOptions"
|
||||
v-model:value="selectedTableId"
|
||||
/>
|
||||
</template>
|
||||
<style scoped>
|
||||
.select {
|
||||
width: 125px;
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@ import { sources } from "../templates"
|
||||
import { Cache, Code, LANGUAGE, Status } from "../types"
|
||||
import { atou, utoa } from "../utils"
|
||||
import { isMobile } from "./breakpoints"
|
||||
import { buildSqlScript, resetSqlTableSelection } from "./sqlTable"
|
||||
|
||||
const defaultLanguage = "python"
|
||||
|
||||
@@ -19,6 +20,7 @@ const cache: Cache = {
|
||||
c: useStorage("code_c", sources["c"]),
|
||||
cpp: useStorage("code_cpp", sources["cpp"]),
|
||||
turtle: useStorage("code_turtle", sources["turtle"]),
|
||||
sql: useStorage("code_sql", sources["sql"]),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,6 +46,7 @@ watch(
|
||||
code.value = cache.code[value].value
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
if (value === "sql") resetSqlTableSelection()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -71,7 +74,7 @@ export async function init() {
|
||||
if (base64) {
|
||||
try {
|
||||
const data = JSON.parse(atou(base64))
|
||||
const lang = ["python", "c", "cpp", "turtle"].includes(data.lang)
|
||||
const lang = ["python", "c", "cpp", "turtle", "sql"].includes(data.lang)
|
||||
? (data.lang as LANGUAGE)
|
||||
: defaultLanguage
|
||||
const sharedCode = data.code ?? sources[lang]
|
||||
@@ -115,8 +118,10 @@ export async function run() {
|
||||
} else {
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
const sourceCode =
|
||||
code.language === "sql" ? buildSqlScript(cleanCode) : cleanCode
|
||||
const result = await submit(
|
||||
{ value: cleanCode, language: code.language },
|
||||
{ value: sourceCode, language: code.language },
|
||||
input.value.trim(),
|
||||
)
|
||||
output.value = result.output || ""
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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<string, string | number>[] {
|
||||
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] ?? ""]),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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
|
||||
+16
-1
@@ -3,12 +3,27 @@ import { code } from "../composables/code"
|
||||
import CodeSection from "./CodeSection.vue"
|
||||
import InputSection from "./InputSection.vue"
|
||||
import OutputSection from "./OutputSection.vue"
|
||||
import SqlSection from "./SqlSection.vue"
|
||||
import TurtleSection from "./TurtleSection.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout-content class="container">
|
||||
<n-split direction="horizontal" :min="1 / 3" :max="4 / 5">
|
||||
<n-split
|
||||
v-if="code.language === 'sql'"
|
||||
direction="vertical"
|
||||
:default-size="1 / 4"
|
||||
:min="1 / 4"
|
||||
:max="3 / 4"
|
||||
>
|
||||
<template #1>
|
||||
<CodeSection />
|
||||
</template>
|
||||
<template #2>
|
||||
<SqlSection />
|
||||
</template>
|
||||
</n-split>
|
||||
<n-split v-else direction="horizontal" :min="1 / 3" :max="4 / 5">
|
||||
<template #1>
|
||||
<CodeSection />
|
||||
</template>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { useMessage } from "naive-ui"
|
||||
import SelectLanguage from "../components/SelectLanguage.vue"
|
||||
import SelectSqlTable from "../components/SelectSqlTable.vue"
|
||||
import ThemeButton from "../components/ThemeButton.vue"
|
||||
import { loading, run, share, size } from "../composables/code"
|
||||
import { code, loading, run, share, size } from "../composables/code"
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
@@ -34,6 +35,7 @@ function handleShare() {
|
||||
<template #prefix>字号</template>
|
||||
</n-input-number>
|
||||
<SelectLanguage />
|
||||
<SelectSqlTable v-if="code.language === 'sql'" />
|
||||
<n-button type="primary" @click="run" :disabled="loading">
|
||||
运行 (F5)
|
||||
</n-button>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DataTableColumns } from "naive-ui"
|
||||
import { computed, watch } from "vue"
|
||||
import { output, status } from "../composables/code"
|
||||
import { parseResultRows, selectedTableId } from "../composables/sqlTable"
|
||||
import { sqlTables } from "../data/sqlTables"
|
||||
import { Status } from "../types"
|
||||
import OutputSection from "./OutputSection.vue"
|
||||
|
||||
const selectedTable = computed(
|
||||
() =>
|
||||
sqlTables.find((table) => table.id === selectedTableId.value) ??
|
||||
sqlTables[0],
|
||||
)
|
||||
|
||||
const tableColumns = computed<DataTableColumns>(() =>
|
||||
selectedTable.value.columns.map((column) => ({
|
||||
title: column.name,
|
||||
key: column.name,
|
||||
})),
|
||||
)
|
||||
|
||||
const initialRows = computed(() =>
|
||||
selectedTable.value.rows.map((row) =>
|
||||
Object.fromEntries(
|
||||
selectedTable.value.columns.map((column, index) => [
|
||||
column.name,
|
||||
row[index],
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const resultRows = computed(() =>
|
||||
parseResultRows(output.value, selectedTable.value.columns),
|
||||
)
|
||||
|
||||
watch(selectedTableId, () => {
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-split
|
||||
direction="horizontal"
|
||||
:default-size="1 / 2"
|
||||
:min="1 / 4"
|
||||
:max="3 / 4"
|
||||
>
|
||||
<template #1>
|
||||
<div class="table-panel">
|
||||
<div class="panel-title">原始数据({{ selectedTable.label }})</div>
|
||||
<n-data-table
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:columns="tableColumns"
|
||||
:data="initialRows"
|
||||
:row-key="(row: any) => row.id"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #2>
|
||||
<div class="table-panel" v-if="status === Status.Accepted">
|
||||
<div class="panel-title">运行后数据</div>
|
||||
<n-data-table
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:columns="tableColumns"
|
||||
:data="resultRows"
|
||||
:row-key="(row: any) => row.id"
|
||||
/>
|
||||
</div>
|
||||
<OutputSection v-else />
|
||||
</template>
|
||||
</n-split>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-panel {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 12px 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
NConfigProvider,
|
||||
NDataTable,
|
||||
NDropdown,
|
||||
NFlex,
|
||||
NIcon,
|
||||
@@ -40,6 +41,7 @@ const naive = create({
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
NConfigProvider,
|
||||
NDataTable,
|
||||
NMessageProvider,
|
||||
NLayout,
|
||||
NLayoutHeader,
|
||||
|
||||
@@ -17,11 +17,14 @@ for i in range(4):
|
||||
|
||||
turtle.done()`
|
||||
|
||||
const sqlSource = "-- 在这里编写你的 SQL 语句\n"
|
||||
|
||||
export const languageToId: { [key in string]: number } = {
|
||||
c: 50,
|
||||
cpp: 54,
|
||||
java: 62,
|
||||
python: 71,
|
||||
sql: 82,
|
||||
}
|
||||
|
||||
export const sources = {
|
||||
@@ -30,4 +33,5 @@ export const sources = {
|
||||
java: javaSource,
|
||||
python: pythonSource,
|
||||
turtle: turtleSource,
|
||||
sql: sqlSource,
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { RemovableRef } from "@vueuse/core"
|
||||
|
||||
export type LANGUAGE = "c" | "python" | "cpp" | "turtle"
|
||||
export type LANGUAGE = "c" | "python" | "cpp" | "turtle" | "sql"
|
||||
|
||||
export interface Code {
|
||||
value: string
|
||||
|
||||
Reference in New Issue
Block a user