项目 6 数据存储:仿冒 SqlClient(T-SQL 转写成 SQLite)、数据库页面、DataGridView、DateTimePicker
- 每个学生一套数据库,长期保存(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:
1 parent
6b088240da
commit
670abe5921
72 files changed
+7253
-51
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user