refactor(契约): SQL 展示数据进契约,topReaction 收成枚举
d3b05b8 把 SQLDisplay* 列进「保留不动的窄化」,理由是契约那边就是
Record<string, unknown>。这次把那个碗底扫了 —— 因为「契约说不知道形状、
前端手抄一份来渲染」正是那个 commit 修的五处分歧的同一个模子。
SQL 题两个 JSONB 列现在有精确 schema:sqlConfigSchema、sqlDisplaySchema、
sqlDisplayTableSchema、sqlDisplayColumnSchema。键名保持 snake_case,是
problem.sql_config / sql_display 的原文,回滚时旧后端要读同一份。前端
utils/types.ts 里手抄的 SQLConfig / SQLDisplay / SQLDisplayTable /
SQLDisplayColumn 共 30 行删掉改成 re-export,Problem 和 AdminProblem 的
Omit 列表各短两项。手抄那份把 SQLDisplayColumn.type 写成了可选,后端一直
是必有的空串。
后端跟着收紧:commonChecks / generateSqlDisplay 的 Record<string, unknown>
换成 SqlConfig,`sqlConfig.mode === "modify" ? "modify" : "query"` 这句防御
删了 —— 现在类型上就只有那两个值。请求侧 createProblemRequestSchema.sqlConfig
也不再是 record:mode 是枚举、order_sensitive 缺省补 false,正好是旧后端
SQLConfigSerializer 的口径(新后端之前反而比旧的松,什么都收)。
顺带修 adminProblemListItemSchema.topReaction 的注释:写着「当前后端恒传
null,get_top_reactions 没跟着迁过来」,但 admin/problem.ts:257 早就在调了,
只有比赛题列表恒 null。这条注释会骗人去补一个已经存在的实现。类型同时从
z.string() 收成 reactionKeySchema —— getTopReactions 本来就把库里认不出的
类型滤掉了,只是类型上没体现,前端因此得在 transforms.ts 写
`as AdminProblemFiltered["topReaction"]`,现在那个强转和 `?? null` 一起没了。
服务层里的 `as ReactionKey` 换成 isReactionKey 类型守卫。
**收紧 JSONB 的 schema 会把「存量数据形状不对」从静默降级变成 500,所以实打了:**
- 从生产库备份捞出 9 道 SQL 题的 sql_config / sql_display 原文,逐条过新
schema,9/9 通过;键集与旧后端 judge/sql_runner.py:build_display 的产出
逐字一致(columns/name/rows/total_rows/truncated,expected 两形态)。
- 把其中 query 形态、modify 形态各一条种进本地库,起 API 打 oj 详情和后台
详情共四个端点,全 200,expected 两种分支都正确解析。
- topReaction:插两条并列票,按 reactionKeySchema 顺序正确取到 confusing;
再插一条库里已下掉的类型,被守卫滤掉返回 null。
另:`bunx vue-tsc --noEmit` 不带 -p 是**无效的**,根 tsconfig.json 是
"files": [],塞个类型错误进去照样 exit 0。要跑 `bun run type-check`
(-p tsconfig.app.json),这次的结论出自它。tsc(apps/api) 0 error、
check:routes 168 条无遮蔽、vite build 通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
sqlPreviewRequestSchema,
|
sqlPreviewRequestSchema,
|
||||||
sqlTestCaseScriptSchema,
|
sqlTestCaseScriptSchema,
|
||||||
uploadTestCaseResponseSchema,
|
uploadTestCaseResponseSchema,
|
||||||
|
type SqlConfig,
|
||||||
} from "@oj2/contract"
|
} from "@oj2/contract"
|
||||||
import { and, count, desc, eq, ilike, inArray, isNull, ne, or, sql } from "drizzle-orm"
|
import { and, count, desc, eq, ilike, inArray, isNull, ne, or, sql } from "drizzle-orm"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
@@ -138,7 +139,7 @@ function commonChecks(data: {
|
|||||||
inputDescription: string
|
inputDescription: string
|
||||||
outputDescription: string
|
outputDescription: string
|
||||||
samples: unknown[]
|
samples: unknown[]
|
||||||
sqlConfig: Record<string, unknown> | null
|
sqlConfig: SqlConfig | null
|
||||||
answers: Record<string, unknown>[]
|
answers: Record<string, unknown>[]
|
||||||
}): { error: string } | { sql: boolean } {
|
}): { error: string } | { sql: boolean } {
|
||||||
if (data.languages.includes("SQL")) {
|
if (data.languages.includes("SQL")) {
|
||||||
@@ -164,7 +165,7 @@ function commonChecks(data: {
|
|||||||
async function generateSqlDisplay(
|
async function generateSqlDisplay(
|
||||||
testCaseId: string,
|
testCaseId: string,
|
||||||
answers: Record<string, unknown>[],
|
answers: Record<string, unknown>[],
|
||||||
sqlConfig: Record<string, unknown>,
|
sqlConfig: SqlConfig,
|
||||||
): Promise<{ error: string } | { display: unknown }> {
|
): Promise<{ error: string } | { display: unknown }> {
|
||||||
const info = await readInfo(testCaseId)
|
const info = await readInfo(testCaseId)
|
||||||
if (!info) return { error: "测试点信息读取失败,请重新上传测试点" }
|
if (!info) return { error: "测试点信息读取失败,请重新上传测试点" }
|
||||||
@@ -183,8 +184,7 @@ async function generateSqlDisplay(
|
|||||||
(item) => item.language === "SQL" && typeof item.code === "string" && item.code.trim(),
|
(item) => item.language === "SQL" && typeof item.code === "string" && item.code.trim(),
|
||||||
)?.code
|
)?.code
|
||||||
if (typeof refSql !== "string") return { error: "题目缺少 SQL 标准答案" }
|
if (typeof refSql !== "string") return { error: "题目缺少 SQL 标准答案" }
|
||||||
const mode = sqlConfig.mode === "modify" ? "modify" as const : "query" as const
|
const outcome = await buildSqlDisplay(initSql, refSql, sqlConfig.mode)
|
||||||
const outcome = await buildSqlDisplay(initSql, refSql, mode)
|
|
||||||
if (!outcome.ok) return { error: `SQL 展示数据生成失败: ${outcome.message}` }
|
if (!outcome.ok) return { error: `SQL 展示数据生成失败: ${outcome.message}` }
|
||||||
return { display: outcome.value }
|
return { display: outcome.value }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { reactionKeySchema } from "@oj2/contract"
|
import { reactionKeySchema, type ReactionKey } from "@oj2/contract"
|
||||||
import { count, inArray } from "drizzle-orm"
|
import { count, inArray } from "drizzle-orm"
|
||||||
|
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
@@ -12,8 +12,13 @@ const TYPE_ORDER = new Map<string, number>(
|
|||||||
reactionKeySchema.options.map((key, index) => [key, index]),
|
reactionKeySchema.options.map((key, index) => [key, index]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 库里的 type 是裸字符串;在 TYPE_ORDER 里就等价于「契约认得的类型」。 */
|
||||||
|
function isReactionKey(value: string): value is ReactionKey {
|
||||||
|
return TYPE_ORDER.has(value)
|
||||||
|
}
|
||||||
|
|
||||||
export interface TopReaction {
|
export interface TopReaction {
|
||||||
type: string
|
type: ReactionKey
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,10 +43,10 @@ export async function getTopReactions(problemIds: number[]) {
|
|||||||
.groupBy(schema.reaction.problemId, schema.reaction.type)
|
.groupBy(schema.reaction.problemId, schema.reaction.type)
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const order = TYPE_ORDER.get(row.type)
|
|
||||||
// 库里可能残留前端已经下掉的旧类型。跳过而不是当成并列最优 ——
|
// 库里可能残留前端已经下掉的旧类型。跳过而不是当成并列最优 ——
|
||||||
// 否则一个已经不展示的类型会把真正的最高票挤掉。
|
// 否则一个已经不展示的类型会把真正的最高票挤掉。
|
||||||
if (order === undefined) continue
|
if (!isReactionKey(row.type)) continue
|
||||||
|
const order = TYPE_ORDER.get(row.type)!
|
||||||
const current = top.get(row.problemId)
|
const current = top.get(row.problemId)
|
||||||
if (
|
if (
|
||||||
!current ||
|
!current ||
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import type {
|
|||||||
Contest,
|
Contest,
|
||||||
Exercise,
|
Exercise,
|
||||||
ExerciseType,
|
ExerciseType,
|
||||||
SQLDisplay,
|
SqlDisplay,
|
||||||
TestcaseUploadedReturns,
|
TestcaseUploadedReturns,
|
||||||
Tutorial,
|
Tutorial,
|
||||||
User,
|
User,
|
||||||
@@ -237,7 +237,7 @@ export function previewSQLTestcase(data: {
|
|||||||
refSql: string
|
refSql: string
|
||||||
mode: "query" | "modify"
|
mode: "query" | "modify"
|
||||||
}) {
|
}) {
|
||||||
return api2.post<SQLDisplay>("admin/sql-test-cases/preview", data)
|
return api2.post<SqlDisplay>("admin/sql-test-cases/preview", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序)
|
// 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { LANGUAGE, SQLDisplay, Testcase } from "utils/types"
|
import type { LANGUAGE, SqlDisplay, Testcase } from "utils/types"
|
||||||
import { createZipBlob } from "utils/functions"
|
import { createZipBlob } from "utils/functions"
|
||||||
import SQLDataTable from "oj/problem/components/SQLDataTable.vue"
|
import SQLDataTable from "oj/problem/components/SQLDataTable.vue"
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
interface ScriptEntry {
|
interface ScriptEntry {
|
||||||
id: number
|
id: number
|
||||||
sql: string
|
sql: string
|
||||||
display: SQLDisplay | null
|
display: SqlDisplay | null
|
||||||
error: string
|
error: string
|
||||||
// 标准答案或题型改过之后,旧预览结果作废,需重新预览才能上传
|
// 标准答案或题型改过之后,旧预览结果作废,需重新预览才能上传
|
||||||
stale: boolean
|
stale: boolean
|
||||||
@@ -109,11 +109,11 @@ function reset() {
|
|||||||
scripts.value = [blankEntry(), blankEntry(), blankEntry()]
|
scripts.value = [blankEntry(), blankEntry(), blankEntry()]
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectedQuery(d: SQLDisplay) {
|
function expectedQuery(d: SqlDisplay) {
|
||||||
return "columns" in d.expected ? d.expected : null
|
return "columns" in d.expected ? d.expected : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function changedTables(d: SQLDisplay) {
|
function changedTables(d: SqlDisplay) {
|
||||||
return "changed_tables" in d.expected ? d.expected.changed_tables : []
|
return "changed_tables" in d.expected ? d.expected.changed_tables : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ export function toProblemListItem(
|
|||||||
hasAstRules: result.hasAstRules,
|
hasAstRules: result.hasAstRules,
|
||||||
allowFlowchart: result.allowFlowchart,
|
allowFlowchart: result.allowFlowchart,
|
||||||
showFlowchart: result.showFlowchart,
|
showFlowchart: result.showFlowchart,
|
||||||
// 比赛题目列表接口不返回这个字段
|
topReaction: result.topReaction,
|
||||||
topReaction: (result.topReaction ??
|
|
||||||
null) as AdminProblemFiltered["topReaction"],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SQLDisplayColumn } from "utils/types"
|
import type { SqlDisplayColumn } from "utils/types"
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
columns: SQLDisplayColumn[]
|
columns: SqlDisplayColumn[]
|
||||||
rows: (string | number | null)[][]
|
rows: (string | number | null)[][]
|
||||||
totalRows?: number
|
totalRows?: number
|
||||||
truncated?: boolean
|
truncated?: boolean
|
||||||
|
|||||||
@@ -52,36 +52,17 @@ export type LANGUAGE =
|
|||||||
| "Flowchart"
|
| "Flowchart"
|
||||||
| "SQL"
|
| "SQL"
|
||||||
|
|
||||||
export interface SQLConfig {
|
/**
|
||||||
mode: "query" | "modify"
|
* SQL 题的配置与展示数据。形状在契约里 —— 原来这里手抄了一份,
|
||||||
order_sensitive: boolean
|
* 而契约那边是 `Record<string, unknown>`,等于渲染表格的那段代码全靠手抄件兜底。
|
||||||
}
|
* 键名的 snake_case 是 JSONB 原文,见契约 sqlDisplaySchema 的注释。
|
||||||
|
*/
|
||||||
export interface SQLDisplayColumn {
|
export type {
|
||||||
name: string
|
SqlConfig,
|
||||||
type?: string
|
SqlDisplay,
|
||||||
}
|
SqlDisplayTable,
|
||||||
|
SqlDisplayColumn,
|
||||||
export interface SQLDisplayTable {
|
} from "@oj2/contract"
|
||||||
name: string
|
|
||||||
columns: SQLDisplayColumn[]
|
|
||||||
rows: (string | number | null)[][]
|
|
||||||
total_rows: number
|
|
||||||
truncated: boolean
|
|
||||||
dropped?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SQLDisplay {
|
|
||||||
tables: SQLDisplayTable[]
|
|
||||||
expected:
|
|
||||||
| {
|
|
||||||
columns: SQLDisplayColumn[]
|
|
||||||
rows: (string | number | null)[][]
|
|
||||||
total_rows: number
|
|
||||||
truncated: boolean
|
|
||||||
}
|
|
||||||
| { changed_tables: SQLDisplayTable[] }
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SUBMISSION_RESULT =
|
export type SUBMISSION_RESULT =
|
||||||
-2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
|
-2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
|
||||||
@@ -119,17 +100,11 @@ export type Testcase = TestCaseEntry & { score: string }
|
|||||||
/**
|
/**
|
||||||
* 题目详情。以契约的 ProblemDetail 为准,只在这里补两处前端自己的窄化:
|
* 题目详情。以契约的 ProblemDetail 为准,只在这里补两处前端自己的窄化:
|
||||||
* - `languages` / `template` 的键窄化成 LANGUAGE,组件按语言查模板要靠它
|
* - `languages` / `template` 的键窄化成 LANGUAGE,组件按语言查模板要靠它
|
||||||
* - `astRules` / `sqlConfig` / `sqlDisplay` 契约里是 Record<string, unknown>,
|
* - `astRules` 契约里是 unknown,这里给出组件实际读的形状
|
||||||
* 这里给出组件实际读的形状
|
|
||||||
*/
|
*/
|
||||||
export type Problem = Omit<
|
export type Problem = Omit<ProblemDetail, "languages" | "template"> & {
|
||||||
ProblemDetail,
|
|
||||||
"languages" | "template" | "sqlConfig" | "sqlDisplay"
|
|
||||||
> & {
|
|
||||||
languages: LANGUAGE[]
|
languages: LANGUAGE[]
|
||||||
template: { [key in LANGUAGE]?: string }
|
template: { [key in LANGUAGE]?: string }
|
||||||
sqlConfig?: SQLConfig | null
|
|
||||||
sqlDisplay?: SQLDisplay | null
|
|
||||||
astRules?: AstRules | null
|
astRules?: AstRules | null
|
||||||
hasAstRules?: boolean
|
hasAstRules?: boolean
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
@@ -159,8 +134,6 @@ export type AdminProblem = Omit<
|
|||||||
| "languages"
|
| "languages"
|
||||||
| "template"
|
| "template"
|
||||||
| "testCaseScore"
|
| "testCaseScore"
|
||||||
| "sqlConfig"
|
|
||||||
| "sqlDisplay"
|
|
||||||
| "samples"
|
| "samples"
|
||||||
| "answers"
|
| "answers"
|
||||||
| "astRules"
|
| "astRules"
|
||||||
@@ -171,8 +144,6 @@ export type AdminProblem = Omit<
|
|||||||
testCaseScore: Testcase[]
|
testCaseScore: Testcase[]
|
||||||
samples: { input: string; output: string }[]
|
samples: { input: string; output: string }[]
|
||||||
answers: { language: LANGUAGE; code: string }[]
|
answers: { language: LANGUAGE; code: string }[]
|
||||||
sqlConfig?: SQLConfig | null
|
|
||||||
sqlDisplay?: SQLDisplay | null
|
|
||||||
astRules?: AstRules | null
|
astRules?: AstRules | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,8 +198,8 @@ export interface AdminProblemFiltered {
|
|||||||
hasAstRules: boolean
|
hasAstRules: boolean
|
||||||
allowFlowchart: boolean
|
allowFlowchart: boolean
|
||||||
showFlowchart: boolean
|
showFlowchart: boolean
|
||||||
// 比赛题目列表接口不返回这个字段
|
// 比赛题目列表恒为 null —— 只有公开题列表下发最高票评价
|
||||||
topReaction?: { type: ReactionKey; count: number } | null
|
topReaction: { type: ReactionKey; count: number } | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 题单相关类型
|
// 题单相关类型
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { z } from "zod"
|
|||||||
import { achievementRaritySchema } from "./achievement"
|
import { achievementRaritySchema } from "./achievement"
|
||||||
import { rankProfileSchema } from "./account"
|
import { rankProfileSchema } from "./account"
|
||||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||||
import { problemDifficultySchema } from "./problem"
|
import { reactionKeySchema } from "./content"
|
||||||
|
import { problemDifficultySchema, sqlConfigSchema, sqlDisplaySchema } from "./problem"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台侧的契约。与 oj 侧分开放:同一张表在两侧下发的字段集通常不同
|
* 后台侧的契约。与 oj 侧分开放:同一张表在两侧下发的字段集通常不同
|
||||||
@@ -513,10 +514,10 @@ export const adminProblemListItemSchema = z.object({
|
|||||||
hasAstRules: z.boolean(),
|
hasAstRules: z.boolean(),
|
||||||
allowFlowchart: z.boolean(),
|
allowFlowchart: z.boolean(),
|
||||||
showFlowchart: z.boolean(),
|
showFlowchart: z.boolean(),
|
||||||
// 最高票评价 {type, count}。**当前后端恒传 null** —— 旧后端的
|
// 最高票评价 {type, count}。**只有公开题列表下发**,比赛题列表恒传 null ——
|
||||||
// reaction/services.py:get_top_reactions 没有跟着迁过来,这一列现在是空的。
|
// 与旧后端 reaction/services.py:get_top_reactions 的口径一致。
|
||||||
topReaction: z
|
topReaction: z
|
||||||
.object({ type: z.string(), count: z.number().int() })
|
.object({ type: reactionKeySchema, count: z.number().int() })
|
||||||
.nullable(),
|
.nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -558,8 +559,8 @@ export const adminProblemSchema = z.object({
|
|||||||
astRules: z.unknown(),
|
astRules: z.unknown(),
|
||||||
answers: z.array(z.record(z.string(), z.unknown())),
|
answers: z.array(z.record(z.string(), z.unknown())),
|
||||||
prompt: z.string().nullable(),
|
prompt: z.string().nullable(),
|
||||||
sqlConfig: z.record(z.string(), z.unknown()).nullable(),
|
sqlConfig: sqlConfigSchema.nullable(),
|
||||||
sqlDisplay: z.record(z.string(), z.unknown()).nullable(),
|
sqlDisplay: sqlDisplaySchema.nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createProblemRequestSchema = z.object({
|
export const createProblemRequestSchema = z.object({
|
||||||
@@ -588,7 +589,12 @@ export const createProblemRequestSchema = z.object({
|
|||||||
mermaidCode: z.string().nullable().default(null),
|
mermaidCode: z.string().nullable().default(null),
|
||||||
flowchartHint: z.string().nullable().default(null),
|
flowchartHint: z.string().nullable().default(null),
|
||||||
astRules: z.unknown().default(null),
|
astRules: z.unknown().default(null),
|
||||||
sqlConfig: z.record(z.string(), z.unknown()).nullable().default(null),
|
// order_sensitive 缺省补 false —— 与旧后端 SQLConfigSerializer 的
|
||||||
|
// `BooleanField(default=False)` 一致
|
||||||
|
sqlConfig: sqlConfigSchema
|
||||||
|
.extend({ order_sensitive: z.boolean().default(false) })
|
||||||
|
.nullable()
|
||||||
|
.default(null),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateProblemRequestSchema = createProblemRequestSchema
|
export const updateProblemRequestSchema = createProblemRequestSchema
|
||||||
|
|||||||
@@ -8,6 +8,53 @@ import { paginatedSchema, sampleUserSchema } from "./common"
|
|||||||
* 多出来的值会静默渲染成 undefined。
|
* 多出来的值会静默渲染成 undefined。
|
||||||
*/
|
*/
|
||||||
export const problemDifficultySchema = z.enum(["Low", "Mid", "High"])
|
export const problemDifficultySchema = z.enum(["Low", "Mid", "High"])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL 题配置与展示数据。两者都是 `problem.sql_config` / `problem.sql_display`
|
||||||
|
* 的 **JSONB 原文**,所以键名保持 snake_case —— 回滚时旧后端要读同一份,
|
||||||
|
* 且旧后端 `judge/sql_runner.py:build_display` 产出的就是这个形状
|
||||||
|
* (生产库 9 道 SQL 题逐条比对过,键集完全一致)。
|
||||||
|
*
|
||||||
|
* 写成精确 schema 而不是 `z.record(z.unknown())`:前端原来得自己手抄一份
|
||||||
|
* SQLDisplay 接口才能渲染表格,抄错了没人拦得住。
|
||||||
|
*/
|
||||||
|
export const sqlConfigSchema = z.object({
|
||||||
|
mode: z.enum(["query", "modify"]),
|
||||||
|
order_sensitive: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 表格里的单元格。SQLite 只会给出这三种;BLOB 在落库前已转成十六进制字符串 */
|
||||||
|
const sqlCellSchema = z.union([z.string(), z.number(), z.null()])
|
||||||
|
|
||||||
|
const sqlDisplayColumnSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
/** 表达式/聚合列(COUNT(*)、别名)在数据表里无同名列,类型为空串,前端据此隐藏 */
|
||||||
|
type: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const sqlResultSetSchema = z.object({
|
||||||
|
columns: z.array(sqlDisplayColumnSchema),
|
||||||
|
rows: z.array(z.array(sqlCellSchema)),
|
||||||
|
total_rows: z.number().int().nonnegative(),
|
||||||
|
truncated: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const sqlDisplayTableSchema = sqlResultSetSchema.extend({
|
||||||
|
name: z.string(),
|
||||||
|
/** 被标准答案 DROP 的表:条目用初始数据补齐、rows 清空,前端提示「表已删除」 */
|
||||||
|
dropped: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const sqlDisplaySchema = z.object({
|
||||||
|
tables: z.array(sqlDisplayTableSchema),
|
||||||
|
// query 题给结果集,modify 题给改动后的表 —— 两种形态,前端按有没有
|
||||||
|
// changed_tables 分支
|
||||||
|
expected: z.union([
|
||||||
|
sqlResultSetSchema,
|
||||||
|
z.object({ changed_tables: z.array(sqlDisplayTableSchema) }),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
export const problemDetailSchema = z.object({
|
export const problemDetailSchema = z.object({
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
_id: z.string(),
|
_id: z.string(),
|
||||||
@@ -49,8 +96,8 @@ export const problemDetailSchema = z.object({
|
|||||||
mermaidCode: z.string().nullable(),
|
mermaidCode: z.string().nullable(),
|
||||||
flowchartData: z.record(z.string(), z.unknown()).nullable(),
|
flowchartData: z.record(z.string(), z.unknown()).nullable(),
|
||||||
flowchartHint: z.string().nullable(),
|
flowchartHint: z.string().nullable(),
|
||||||
sqlConfig: z.record(z.string(), z.unknown()).nullable(),
|
sqlConfig: sqlConfigSchema.nullable(),
|
||||||
sqlDisplay: z.record(z.string(), z.unknown()).nullable(),
|
sqlDisplay: sqlDisplaySchema.nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type ProblemDetail = z.infer<typeof problemDetailSchema>
|
export type ProblemDetail = z.infer<typeof problemDetailSchema>
|
||||||
@@ -96,4 +143,8 @@ export type ProblemListItem = z.infer<typeof problemListItemSchema>
|
|||||||
export type ProblemList = z.infer<typeof problemListSchema>
|
export type ProblemList = z.infer<typeof problemListSchema>
|
||||||
export type Tag = z.infer<typeof tagSchema>
|
export type Tag = z.infer<typeof tagSchema>
|
||||||
export type ProblemAuthor = z.infer<typeof problemAuthorSchema>
|
export type ProblemAuthor = z.infer<typeof problemAuthorSchema>
|
||||||
|
export type SqlConfig = z.infer<typeof sqlConfigSchema>
|
||||||
|
export type SqlDisplay = z.infer<typeof sqlDisplaySchema>
|
||||||
|
export type SqlDisplayTable = z.infer<typeof sqlDisplayTableSchema>
|
||||||
|
export type SqlDisplayColumn = z.infer<typeof sqlDisplayColumnSchema>
|
||||||
export type YearlyAc = z.infer<typeof yearlyAcSchema>
|
export type YearlyAc = z.infer<typeof yearlyAcSchema>
|
||||||
|
|||||||
Reference in New Issue
Block a user