- 每个学生一套数据库,长期保存(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>
175 lines
7.6 KiB
C#
175 lines
7.6 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data.SqlClient;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Text.Encodings.Web;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Nodes;
|
||
using System.Text.RegularExpressions;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Sim.Data.Engine;
|
||
|
||
namespace Sim.Runner;
|
||
|
||
/// <summary>
|
||
/// 常驻 SQL 服务:数据库页面(代替 SSMS)的查询、对象资源管理器,以及评分前按实验预置脚本准备数据库。
|
||
/// 请求里的目录由服务器给出(学生自己的数据库目录或评分用的临时目录),学生的 SQL 经转写后只能操作这个目录里的库。
|
||
/// 每行一个请求 {"id":…,"type":"exec"|"script"|"explorer",…},回复带同样的 id;请求可以并发处理。
|
||
/// </summary>
|
||
internal static class SqlService
|
||
{
|
||
private static readonly JsonSerializerOptions JsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
|
||
private static readonly TimeSpan TimeLimit = TimeSpan.FromSeconds(5);
|
||
/// <summary>查询窗口最多显示的行数</summary>
|
||
private const int MaxRows = 1000;
|
||
|
||
public static int Run()
|
||
{
|
||
// 日期等按中文 Windows 上 SSMS 的样子显示
|
||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.GetCultureInfo("zh-CN");
|
||
using var input = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
|
||
var output = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)) { AutoFlush = true };
|
||
var writeLock = new object();
|
||
using var slots = new SemaphoreSlim(4);
|
||
|
||
output.WriteLine(new JsonObject { ["type"] = "ready" }.ToJsonString(JsonOptions));
|
||
while (input.ReadLine() is { } line)
|
||
{
|
||
slots.Wait();
|
||
Task.Run(() =>
|
||
{
|
||
JsonObject reply;
|
||
JsonNode? id = null;
|
||
try
|
||
{
|
||
var request = JsonNode.Parse(line)!.AsObject();
|
||
id = request["id"]?.DeepClone();
|
||
reply = Handle(request);
|
||
}
|
||
catch (SqlException ex)
|
||
{
|
||
reply = Error(ex);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
reply = new JsonObject { ["type"] = "error", ["number"] = 0, ["message"] = ex.Message, ["line"] = 1 };
|
||
}
|
||
finally
|
||
{
|
||
slots.Release();
|
||
}
|
||
reply["id"] = id;
|
||
lock (writeLock) output.WriteLine(reply.ToJsonString(JsonOptions));
|
||
});
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
private static JsonObject Handle(JsonObject request)
|
||
{
|
||
var dir = (string?)request["dir"] ?? throw new ArgumentException("缺少 dir");
|
||
var store = new DatabaseStore(dir);
|
||
return (string?)request["type"] switch
|
||
{
|
||
"exec" => Exec(store, (string?)request["database"] ?? DatabaseStore.Master, (string?)request["sql"] ?? ""),
|
||
"script" => Script(store, (string?)request["sql"] ?? ""),
|
||
"explorer" => Explorer(store),
|
||
var type => throw new ArgumentException($"不认识的请求 {type}"),
|
||
};
|
||
}
|
||
|
||
/// <summary>查询窗口执行一批语句:返回各个结果集(最多 1000 行)、受影响的行数,以及执行后的当前库(USE 可能切换了)</summary>
|
||
private static JsonObject Exec(DatabaseStore store, string database, string sql)
|
||
{
|
||
var started = Environment.TickCount64;
|
||
using var session = new Session(store, database, TimeLimit);
|
||
var batch = session.Execute(sql, name => (false, null));
|
||
var results = new JsonArray();
|
||
foreach (var result in batch.Results)
|
||
{
|
||
var columns = new JsonArray();
|
||
foreach (var column in result.Columns) columns.Add(new JsonObject { ["name"] = column.Name, ["type"] = column.Type?.ToString() });
|
||
var rows = new JsonArray();
|
||
foreach (var row in result.Rows.Take(MaxRows))
|
||
{
|
||
var cells = new JsonArray();
|
||
for (var i = 0; i < row.Length; i++) cells.Add(Display(row[i], result.Columns[i].Type?.IsDate == true));
|
||
rows.Add(cells);
|
||
}
|
||
results.Add(new JsonObject { ["columns"] = columns, ["rows"] = rows, ["total"] = result.Rows.Count });
|
||
}
|
||
return new JsonObject
|
||
{
|
||
["type"] = "result",
|
||
["database"] = session.Database,
|
||
["results"] = results,
|
||
["affected"] = batch.RecordsAffected,
|
||
["ms"] = Environment.TickCount64 - started,
|
||
};
|
||
}
|
||
|
||
/// <summary>执行一个脚本(多批语句,用单独一行的 GO 分隔),从 master 开始,USE 对后面的批次有效</summary>
|
||
private static JsonObject Script(DatabaseStore store, string sql)
|
||
{
|
||
var batches = Regex.Split(sql, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase).Where(b => !string.IsNullOrWhiteSpace(b));
|
||
using var session = new Session(store, DatabaseStore.Master, TimeSpan.FromSeconds(30));
|
||
foreach (var batch in batches) session.Execute(batch, name => (false, null));
|
||
return new JsonObject { ["type"] = "done" };
|
||
}
|
||
|
||
/// <summary>对象资源管理器:所有库、库里的表、表的列</summary>
|
||
private static JsonObject Explorer(DatabaseStore store)
|
||
{
|
||
var databases = new JsonArray();
|
||
foreach (var name in store.List())
|
||
{
|
||
var tables = new JsonArray();
|
||
using (var connection = store.Open(name))
|
||
{
|
||
foreach (var table in DatabaseStore.ListTables(connection))
|
||
{
|
||
var info = DatabaseStore.GetTable(connection, table);
|
||
var columns = new JsonArray();
|
||
foreach (var column in info?.Columns ?? Array.Empty<ColumnInfo>())
|
||
{
|
||
columns.Add(new JsonObject
|
||
{
|
||
["name"] = column.Name,
|
||
["type"] = column.Type.ToString(),
|
||
["nullable"] = column.Nullable,
|
||
["identity"] = column.Identity,
|
||
["primaryKey"] = column.PrimaryKey,
|
||
});
|
||
}
|
||
tables.Add(new JsonObject { ["name"] = table, ["columns"] = columns });
|
||
}
|
||
}
|
||
databases.Add(new JsonObject { ["name"] = name, ["tables"] = tables });
|
||
}
|
||
return new JsonObject { ["type"] = "explorer", ["databases"] = databases };
|
||
}
|
||
|
||
/// <summary>结果网格里显示的样子,和 SSMS 一致:日期 2024-01-16 09:05:03.000,bit 显示 1 / 0,二进制显示 0x…</summary>
|
||
private static JsonNode? Display(object value, bool dateOnly) => value switch
|
||
{
|
||
DBNull => null,
|
||
DateTime d when dateOnly => d.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||
DateTime d => d.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture),
|
||
bool b => b ? 1 : 0,
|
||
byte[] bytes => "0x" + Convert.ToHexString(bytes.AsSpan(0, Math.Min(bytes.Length, 32))) + (bytes.Length > 32 ? "…" : ""),
|
||
int i => i,
|
||
long l => l,
|
||
double x => x,
|
||
decimal m => m,
|
||
IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
|
||
_ => value.ToString(),
|
||
};
|
||
|
||
private static JsonObject Error(SqlException ex) =>
|
||
new() { ["type"] = "error", ["number"] = ex.Number, ["message"] = ex.Message, ["line"] = ex.LineNumber };
|
||
}
|