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; /// /// 常驻 SQL 服务:数据库页面(代替 SSMS)的查询、对象资源管理器,以及评分前按实验预置脚本准备数据库。 /// 请求里的目录由服务器给出(学生自己的数据库目录或评分用的临时目录),学生的 SQL 经转写后只能操作这个目录里的库。 /// 每行一个请求 {"id":…,"type":"exec"|"script"|"explorer",…},回复带同样的 id;请求可以并发处理。 /// internal static class SqlService { private static readonly JsonSerializerOptions JsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; private static readonly TimeSpan TimeLimit = TimeSpan.FromSeconds(5); /// 查询窗口最多显示的行数 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}"), }; } /// 查询窗口执行一批语句:返回各个结果集(最多 1000 行)、受影响的行数,以及执行后的当前库(USE 可能切换了) 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, }; } /// 执行一个脚本(多批语句,用单独一行的 GO 分隔),从 master 开始,USE 对后面的批次有效 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" }; } /// 对象资源管理器:所有库、库里的表、表的列 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()) { 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 }; } /// 结果网格里显示的样子,和 SSMS 一致:日期 2024-01-16 09:05:03.000,bit 显示 1 / 0,二进制显示 0x… 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 }; }