Files
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

175 lines
7.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}