Files
IOT_SIM/server/src/databases.ts
T
xuyueandClaude Opus 5.5 670abe5921
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
项目 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>
2026-10-23 09:18:56 +08:00

72 lines
3.2 KiB
TypeScript

// 学生的数据库:每个用户一个目录(data/databases/<用户编号>),每个库一个 SQLite 文件,长期保存,
// 就像学生自己电脑上的 SQL Server。学生程序运行时挂进沙箱可读写;数据库页面通过 SQL 服务访问。
// 评分时不用学生自己的库,而是按实验的 database.sql 新建一套干净的库。
import { cp, mkdir, mkdtemp, readdir, rm, stat } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { config } from './config'
import { sqlService } from './sql-service'
/** 用户的数据库目录(不存在时创建) */
export async function userDatabaseDir(userId: number) {
const dir = resolve(config.dataDir, 'databases', String(userId))
await mkdir(dir, { recursive: true })
return dir
}
/** 实验预置的数据库脚本(samples/<实验>/database.sql),没有时返回 undefined */
export async function experimentScript(sampleId: string): Promise<string | undefined> {
const file = Bun.file(join(config.samplesDir, sampleId, 'database.sql'))
return (await file.exists()) ? file.text() : undefined
}
/** 按脚本建库:先清空目录再执行(脚本里 CREATE DATABASE、CREATE TABLE、INSERT 都可以写) */
export async function runScript(dir: string, script: string) {
await mkdir(dir, { recursive: true })
await sqlService.request({ type: 'script', dir, sql: script })
}
/**
* 把学生某个实验用到的库恢复成实验的初始数据:只替换脚本里建的库,学生的其他库不动。
* 返回恢复了哪些库。
*/
export async function restoreExperimentDatabases(userId: number, sampleId: string) {
const script = await experimentScript(sampleId)
if (!script) return []
const temp = await mkdtemp(join(config.runsDir, 'db-'))
try {
await runScript(temp, script)
const target = await userDatabaseDir(userId)
const files = (await readdir(temp)).filter((f) => f.endsWith('.db') && f !== 'master.db')
for (const file of files) {
for (const suffix of ['', '-wal', '-shm']) await rm(join(target, file + suffix), { force: true })
await cp(join(temp, file), join(target, file))
}
return files.map((f) => f.slice(0, -3))
} finally {
await rm(temp, { recursive: true, force: true })
}
}
/** 评分用:实验数据库的模板按实验缓存,每个检查项复制一份(互不影响) */
const templates = new Map<string, Promise<string | undefined>>()
export async function gradingDatabaseDir(sampleId: string): Promise<string | undefined> {
let template = templates.get(sampleId)
if (!template) {
template = (async () => {
const script = await experimentScript(sampleId)
if (script === undefined) return undefined
const dir = resolve(config.runsDir, 'db-templates', sampleId)
await rm(dir, { recursive: true, force: true })
await runScript(dir, script)
return dir
})()
templates.set(sampleId, template)
template.catch(() => templates.delete(sampleId))
}
const source = await template
const copy = await mkdtemp(join(config.runsDir, 'grade-db-'))
if (source && (await stat(source).catch(() => null))) await cp(source, copy, { recursive: true })
return copy
}