Compare commits
3 Commits
527127d9c7
...
be1b9a97a0
| Author | SHA1 | Date | |
|---|---|---|---|
| be1b9a97a0 | |||
| 02c523b32f | |||
| f6d4d9bcb2 |
@@ -0,0 +1,28 @@
|
||||
# SQL 题强制至少 2 个测试点 — 设计
|
||||
|
||||
日期:2026-07-05
|
||||
|
||||
## 背景
|
||||
|
||||
题目页的 `sql_display` 用**测试点 1** 的数据生成期望结果展示(截断到 20 行)。如果 SQL 题只有 1 个测试点且结果 ≤20 行,页面展示的期望结果就是完整答案输出,学生可用 `SELECT ... UNION ALL ...`(query 模式)或硬编码 INSERT/UPDATE(modify 模式)对照抄写直接 AC。多测试点时数据不同,硬编码只能过测试点 1。
|
||||
|
||||
## 决定
|
||||
|
||||
SQL 题**强制至少 2 个数据不同的初始化脚本**,前后端双重拦截:
|
||||
|
||||
- **前端** `ojnext/src/admin/problem/components/SQLTestcaseEditor.vue`:
|
||||
- `canUpload` 要求非空脚本数 ≥ 2,不满足时上传按钮禁用;
|
||||
- 上传按钮 tooltip 在脚本不足时显示原因("SQL 题至少需要 2 个数据不同的测试点,防止硬编码期望结果")。
|
||||
- **后端** `OnlineJudge/problem/views/admin.py` 的 `TestCaseZipProcessor.process_zip`:
|
||||
- `sql=True` 且测试点数 < 2 时 `raise APIError(...)`,兜底直接调 API 的情况。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 只在**重新上传/保存测试点**时拦截,已有的单测试点老题目不受影响、不回溯校验。
|
||||
- 非 SQL 题(.in/.out 沙箱判题)不受影响。
|
||||
- "数据不同"不做内容级校验(两个脚本内容相同也能过),只保证数量下限,YAGNI。
|
||||
|
||||
## 验证
|
||||
|
||||
前端 `vue-tsc --noEmit`、Prettier 通过;后端 `ruff check` / `ruff format --check` 通过。
|
||||
人工验证:出题页只填 1 个脚本 → 上传按钮禁用且 tooltip 说明原因;填 2 个并预览通过 → 可上传。
|
||||
@@ -0,0 +1,50 @@
|
||||
# SQL 题目表名/字段名自动补全 — 设计
|
||||
|
||||
日期:2026-07-05
|
||||
|
||||
## 目标
|
||||
|
||||
学生在 SQL 题目的代码编辑器里输入时,自动补全列表中除现有的 SQL 关键字/函数外,还出现**当前题目的表名和字段名**(带类型提示),减少抄写表名字段名的负担和拼写错误。
|
||||
|
||||
## 背景
|
||||
|
||||
- SQL 题目详情页已下发 `problem.sql_display`(`SQLDisplay` 类型),其中 `tables: SQLDisplayTable[]` 包含每张表的 `name`、`columns[{name, type}]`。数据在前端齐全,**无需后端改动**。
|
||||
- 编辑器补全入口是 `shared/extensions/autocompletion.ts` 的 `enhanceCompletion(language)`,`CodeEditor.vue` 和 `SyncCodeEditor.vue` 都用它,且都叠加了 `completeAnyWord`。
|
||||
- SQL 静态关键字补全表在 `shared/extensions/sql.ts`。
|
||||
- `shared` 直接 import `oj/store/problem` 已有先例(`FlowchartEditor/index.vue`)。
|
||||
|
||||
## 方案(已选:方案 A)
|
||||
|
||||
在 `enhanceCompletion` 中,当 `language === "SQL"` 时,从 `useProblemStore().problem?.sql_display?.tables` 动态生成补全项,追加到静态关键字列表后:
|
||||
|
||||
- **表名**:`type: "class"`,`detail: "数据表"`,`info` 列出该表全部字段(如 `字段:id INTEGER, name TEXT, score REAL`),`boost` 高于所有关键字(如 110)。
|
||||
- **字段名**:`type: "property"`,`detail` 标注来源表和类型(如 `students 的字段 · TEXT`),`boost` 略低于表名、高于关键字(如 105)。
|
||||
- **同名字段每表一条**,靠 detail 区分来源表。
|
||||
- store 在补全回调内惰性读取(每次按键执行),题目切换后自动反映最新表结构。
|
||||
- 非 SQL 语言、无题目上下文(如 admin/tutorial/learn 页面)或 `sql_display` 为空时,不追加任何项,行为与现状一致。
|
||||
|
||||
### 不做的事(YAGNI)
|
||||
|
||||
- 不改后端;不改题目描述展示(`SQLDataTable` 已展示表结构)。
|
||||
- 管理端出题的 SQL 编辑器(`SQLTestcaseEditor`)不接入。
|
||||
- 不做基于 SQL 语法位置的智能上下文补全(如 FROM 后只补表名)。
|
||||
|
||||
## 改动文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/shared/extensions/autocompletion.ts` | SQL 分支追加由 `sql_display.tables` 生成的动态补全项 |
|
||||
|
||||
(如生成逻辑较长,可拆一个小函数放同文件或 `sql.ts`,保持单一职责。)
|
||||
|
||||
## 错误处理
|
||||
|
||||
- `problem`、`sql_display`、`tables` 任一为空 → 返回纯静态列表(可选链兜底)。
|
||||
- Pinia store 在组件上下文外调用的风险:补全回调在编辑器运行期触发,此时 Pinia 已安装;与 FlowchartEditor 的既有用法一致。
|
||||
|
||||
## 测试
|
||||
|
||||
项目无测试套件(政策:不写新测试)。人工验证:
|
||||
1. 打开一道 SQL 题,编辑器中输入表名/字段名前缀,确认补全项出现且 detail/info 正确。
|
||||
2. 打开非 SQL 题,确认补全行为无变化。
|
||||
3. 协作编辑(SyncCodeEditor)场景同样生效。
|
||||
@@ -51,11 +51,16 @@ const isGenerating = ref(false)
|
||||
const hasAnyScript = computed(() => scripts.value.some((s) => s.sql.trim()))
|
||||
const hasBlankScript = computed(() => scripts.value.some((s) => !s.sql.trim()))
|
||||
|
||||
const filledCount = computed(
|
||||
() => scripts.value.filter((s) => s.sql.trim()).length,
|
||||
)
|
||||
|
||||
const canUpload = computed(() => {
|
||||
const filled = scripts.value.filter((s) => s.sql.trim())
|
||||
return (
|
||||
!isPreviewing.value &&
|
||||
filled.length > 0 &&
|
||||
// 至少 2 个数据不同的测试点,防止学生对照题目页的期望结果硬编码
|
||||
filled.length >= 2 &&
|
||||
filled.every((s) => s.display && !s.error && !s.stale)
|
||||
)
|
||||
})
|
||||
@@ -241,7 +246,11 @@ async function upload() {
|
||||
</n-button>
|
||||
</span>
|
||||
</template>
|
||||
所有脚本预览验证通过后才能上传
|
||||
{{
|
||||
filledCount < 2
|
||||
? "SQL 题至少需要 2 个数据不同的测试点,防止硬编码期望结果"
|
||||
: "所有脚本预览验证通过后才能上传"
|
||||
}}
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
CompletionSource,
|
||||
} from "@codemirror/autocomplete"
|
||||
import type { EditorView } from "@codemirror/view"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import type { LANGUAGE } from "utils/types"
|
||||
import { c } from "./c"
|
||||
import { python } from "./python"
|
||||
@@ -22,6 +23,30 @@ const chineseAnnotations: Record<string, ChineseCompletion[]> = {
|
||||
sql,
|
||||
}
|
||||
|
||||
// SQL 题:当前题目的表名和字段名补全,数据来自 sql_display
|
||||
function sqlSchemaCompletions(): Completion[] {
|
||||
const tables = useProblemStore().problem?.sql_display?.tables ?? []
|
||||
return tables.flatMap((table) => [
|
||||
{
|
||||
label: table.name,
|
||||
detail: "数据表",
|
||||
type: "class",
|
||||
info: `字段:${table.columns
|
||||
.map((col) => (col.type ? `${col.name} ${col.type}` : col.name))
|
||||
.join(", ")}`,
|
||||
boost: 110,
|
||||
},
|
||||
...table.columns.map((col) => ({
|
||||
label: col.name,
|
||||
detail: col.type
|
||||
? `${table.name} 的字段 · ${col.type}`
|
||||
: `${table.name} 的字段`,
|
||||
type: "property",
|
||||
boost: 105,
|
||||
})),
|
||||
])
|
||||
}
|
||||
|
||||
export function enhanceCompletion(language: LANGUAGE): CompletionSource {
|
||||
return async function (
|
||||
context: CompletionContext,
|
||||
@@ -69,6 +94,10 @@ export function enhanceCompletion(language: LANGUAGE): CompletionSource {
|
||||
return completion
|
||||
})
|
||||
|
||||
if (trulyLanguage === "sql") {
|
||||
completions.push(...sqlSchemaCompletions())
|
||||
}
|
||||
|
||||
return {
|
||||
from: word ? word.from : context.pos,
|
||||
options: completions,
|
||||
|
||||
Reference in New Issue
Block a user