项目 6 数据存储:仿冒 SqlClient(T-SQL 转写成 SQLite)、数据库页面、DataGridView、DateTimePicker
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- 每个学生一套数据库,长期保存(data/databases/<用户编号>/库名.db);学生程序运行时挂进沙箱可读写
- Sim.Data:SqlConnection / SqlCommand / SqlDataReader / SqlDataAdapter / SqlCommandBuilder / SqlParameter;
  连接字符串只按库名打开学生自己的库;ScriptDom 解析 T-SQL 后按白名单转写成 SQLite 语句,
  STRICT 表 + 元数据表还原 SQL Server 的列类型,报错编号和中文信息与 SQL Server 一致;
  INSERT 不写列名时跳过自增列,日期统一格式(书上 DateTime.ToString() 拼接的日期能正确比较)
- 常驻 SQL 服务(Sim.Runner sql-server):数据库页面的查询、对象资源管理器,评分前按实验 database.sql 建库
- 数据库页面(代替 SSMS):对象资源管理器、查询窗口、编辑前 200 行、表设计器、恢复实验初始数据
- DataGridView(绑定 DataView/DataTable/DataSet,单元格修改写回 DataTable)、DateTimePicker;设计器工具箱加入
- 评分:每个检查项一份新建的实验库;新增点 / 修改表格单元格、选日期、表格里有 / 没有某行的检查
- 示例:项目 6 任务 2、3;时间格式改成和中文 Windows 一致(9:05:03);异常堆栈不暴露服务器路径
- 测试:数据库端到端、接口(每人只看到自己的库、恢复实验数据)、评分出错情况、T-SQL 生成

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-23 09:18:56 +08:00
1 parent 6b088240da
commit 670abe5921
72 files changed
+7253 -51

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
import type { Subprocess } from 'bun'
import { config } from './config'
import { readLines } from './runner-process'
/** 查询窗口的一个结果集 */
export interface SqlResultSet {
columns: { name: string; type: string | null }[]
rows: unknown[][]
/** 实际行数(只返回前 1000 行) */
total: number
}
export type SqlReply =
| { type: 'result'; database: string; results: SqlResultSet[]; affected: number; ms: number }
| { type: 'done' }
| { type: 'explorer'; databases: { name: string; tables: { name: string; columns: SqlColumnInfo[] }[] }[] }
| { type: 'error'; number: number; message: string; line: number }
export interface SqlColumnInfo {
name: string
type: string
nullable: boolean
identity: boolean
primaryKey: boolean
}
export type SqlRequest =
| { type: 'exec'; dir: string; database: string; sql: string }
| { type: 'script'; dir: string; sql: string }
| { type: 'explorer'; dir: string }
/** SQL 报错(编号和信息与 SQL Server 一致) */
export class SqlError extends Error {
constructor(
readonly number: number,
message: string,
readonly line: number,
) {
super(message)
}
}
/**
* 常驻 SQL 服务(C# 进程,T-SQL 转写成 SQLite 执行):数据库页面的查询和对象资源管理器、评分前准备实验数据库。
* 进程意外退出时挂起的请求全部失败,下一次请求自动重启。
*/
class SqlService {
private proc?: Subprocess<'pipe', 'pipe', 'inherit'>
private ready?: Promise<void>
private nextId = 1
private readonly pending = new Map<number, { resolve: (r: SqlReply) => void; reject: (e: Error) => void }>()
/** 发出请求;SQL 报错时抛出 SqlError */
async request(request: SqlRequest): Promise<Exclude<SqlReply, { type: 'error' }>> {
await this.start()
const id = this.nextId++
const reply = await new Promise<SqlReply>((resolve, reject) => {
this.pending.set(id, { resolve, reject })
this.proc!.stdin.write(`${JSON.stringify({ id, ...request })}\n`)
this.proc!.stdin.flush()
})
if (reply.type === 'error') throw new SqlError(reply.number, reply.message, reply.line)
return reply
}
stop() {
const proc = this.proc
this.proc = undefined
this.ready = undefined
for (const waiter of this.pending.values()) waiter.reject(new Error('SQL 服务已停止'))
this.pending.clear()
proc?.kill()
}
start() {
if (this.ready) return this.ready
const proc = Bun.spawn(['dotnet', config.runnerDll, 'sql-server'], {
stdin: 'pipe',
stdout: 'pipe',
stderr: 'inherit',
env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: process.env.HOME ?? '/tmp', DOTNET_CLI_TELEMETRY_OPTOUT: '1', DOTNET_NOLOGO: '1', TZ: config.timeZone },
})
this.proc = proc
this.ready = new Promise<void>((resolveReady) => {
void readLines(proc.stdout, (line) => {
const message = JSON.parse(line) as ({ type: 'ready' } | SqlReply) & { id?: number }
if (message.type === 'ready') {
resolveReady()
return
}
const waiter = this.pending.get(message.id!)
if (!waiter) return
this.pending.delete(message.id!)
waiter.resolve(message)
})
})
proc.exited.then((code) => {
if (this.proc !== proc) return
this.proc = undefined
this.ready = undefined
for (const waiter of this.pending.values()) waiter.reject(new Error(`SQL 服务意外退出(${code})`))
this.pending.clear()
})
return this.ready
}
}
export const sqlService = new SqlService()