- 每个学生一套数据库,长期保存(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>
170 lines
6.5 KiB
C#
170 lines
6.5 KiB
C#
using System.Collections.Generic;
|
|
using System.Data.Common;
|
|
using Sim.Data.Engine;
|
|
|
|
namespace System.Data.SqlClient;
|
|
|
|
/// <summary>
|
|
/// 数据库连接。连接字符串里的服务器名、账号、密码都不检查(书上的长服务器名、"." 都能连上),
|
|
/// 只按 Initial Catalog(或 Database)打开这个学生自己的同名数据库;不写时连到 master。
|
|
/// </summary>
|
|
public sealed class SqlConnection : DbConnection, ICloneable
|
|
{
|
|
private static readonly HashSet<string> KnownKeywords = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"data source", "server", "address", "addr", "network address", "initial catalog", "database", "user id", "uid", "user",
|
|
"password", "pwd", "integrated security", "trusted_connection", "persist security info", "persistsecurityinfo",
|
|
"connect timeout", "connection timeout", "timeout", "pooling", "max pool size", "min pool size", "multipleactiveresultsets",
|
|
"encrypt", "trustservercertificate", "application name", "app", "workstation id", "wsid", "packet size",
|
|
"asynchronous processing", "async", "user instance", "attachdbfilename", "connection lifetime", "load balance timeout", "current language", "language",
|
|
};
|
|
|
|
private string _connectionString = "";
|
|
private ConnectionState _state = ConnectionState.Closed;
|
|
private Session? _session;
|
|
|
|
public SqlConnection() { }
|
|
|
|
public SqlConnection(string connectionString) => ConnectionString = connectionString;
|
|
|
|
[Diagnostics.CodeAnalysis.AllowNull]
|
|
public override string ConnectionString
|
|
{
|
|
get => _connectionString;
|
|
set
|
|
{
|
|
if (_state != ConnectionState.Closed) throw new InvalidOperationException("不允许更改“ConnectionString”属性。连接的当前状态为打开。");
|
|
_connectionString = value ?? "";
|
|
}
|
|
}
|
|
|
|
public override string Database => _session?.Database ?? Setting("initial catalog", "database") ?? DatabaseStore.Master;
|
|
public override string DataSource => Setting("data source", "server", "address", "addr", "network address") ?? "";
|
|
public override string ServerVersion => "10.50.1600";
|
|
public override ConnectionState State => _state;
|
|
public override int ConnectionTimeout => 15;
|
|
public string WorkstationId => Environment.MachineName;
|
|
|
|
/// <summary>连接打开时的数据库会话(平台内部使用)</summary>
|
|
internal Session Session(string operation) =>
|
|
_state == ConnectionState.Open && _session != null
|
|
? _session
|
|
: throw new InvalidOperationException($"{operation} 要求已打开且可用的连接。连接的当前状态为已关闭。");
|
|
|
|
public override void Open()
|
|
{
|
|
if (_state == ConnectionState.Open) throw new InvalidOperationException("连接未关闭。连接的当前状态为打开。");
|
|
CheckKeywords();
|
|
var store = DatabaseStore.FromEnvironment()
|
|
?? throw new SqlException(-1, "在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误:这次运行没有准备数据库。");
|
|
try
|
|
{
|
|
_session = new Session(store, Database);
|
|
}
|
|
catch (SqlException ex)
|
|
{
|
|
throw ex.Rethrow();
|
|
}
|
|
SetState(ConnectionState.Open);
|
|
}
|
|
|
|
public override void Close()
|
|
{
|
|
if (_session == null) return;
|
|
_session.Dispose();
|
|
_session = null;
|
|
SetState(ConnectionState.Closed);
|
|
}
|
|
|
|
public override void ChangeDatabase(string databaseName) => Session("ChangeDatabase").ChangeDatabase(databaseName);
|
|
|
|
public new SqlCommand CreateCommand() => new() { Connection = this };
|
|
|
|
protected override DbCommand CreateDbCommand() => CreateCommand();
|
|
|
|
public new SqlTransaction BeginTransaction() => BeginTransaction(IsolationLevel.ReadCommitted);
|
|
|
|
public new SqlTransaction BeginTransaction(IsolationLevel iso)
|
|
{
|
|
Session("BeginTransaction").BeginTransaction();
|
|
return new SqlTransaction(this, iso);
|
|
}
|
|
|
|
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => BeginTransaction(isolationLevel);
|
|
|
|
public object Clone() => new SqlConnection(_connectionString);
|
|
|
|
public static void ClearAllPools() { }
|
|
public static void ClearPool(SqlConnection connection) { }
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing) Close();
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
private void SetState(ConnectionState state)
|
|
{
|
|
var original = _state;
|
|
_state = state;
|
|
OnStateChange(new StateChangeEventArgs(original, state));
|
|
}
|
|
|
|
private string? Setting(params string[] keys)
|
|
{
|
|
var builder = Parse();
|
|
foreach (var key in keys)
|
|
if (builder.TryGetValue(key, out var value) && value?.ToString() is { Length: > 0 } text) return text.Trim();
|
|
return null;
|
|
}
|
|
|
|
private DbConnectionStringBuilder Parse()
|
|
{
|
|
try
|
|
{
|
|
return new DbConnectionStringBuilder { ConnectionString = _connectionString };
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
throw new ArgumentException("初始化字符串的格式不符合从索引 0 处开始的规范。");
|
|
}
|
|
}
|
|
|
|
private void CheckKeywords()
|
|
{
|
|
foreach (string key in Parse().Keys)
|
|
if (!KnownKeywords.Contains(key)) throw new ArgumentException($"不支持关键字:“{key}”。");
|
|
if (Setting("attachdbfilename") != null) throw new SqlException(-1, "平台不支持 AttachDbFilename:请用 Initial Catalog 指定数据库名。");
|
|
}
|
|
}
|
|
|
|
public sealed class SqlTransaction : DbTransaction
|
|
{
|
|
private readonly SqlConnection _connection;
|
|
private bool _done;
|
|
|
|
internal SqlTransaction(SqlConnection connection, IsolationLevel isolationLevel)
|
|
{
|
|
_connection = connection;
|
|
IsolationLevel = isolationLevel;
|
|
}
|
|
|
|
public new SqlConnection Connection => _connection;
|
|
protected override DbConnection DbConnection => _connection;
|
|
public override IsolationLevel IsolationLevel { get; }
|
|
|
|
public override void Commit()
|
|
{
|
|
if (_done) throw new InvalidOperationException("此 SqlTransaction 已完成;它再也无法使用。");
|
|
_done = true;
|
|
_connection.Session("Commit").Commit();
|
|
}
|
|
|
|
public override void Rollback()
|
|
{
|
|
if (_done) throw new InvalidOperationException("此 SqlTransaction 已完成;它再也无法使用。");
|
|
_done = true;
|
|
_connection.Session("Rollback").Rollback();
|
|
}
|
|
}
|