项目 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>
This commit is contained in:
1 parent
6b088240da
commit
670abe5921
72 files changed
+7253
-51
No files matched your search
@@ -0,0 +1,302 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using Sim.Data.Engine;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
/// <summary>执行 T-SQL 语句。一条命令里可以有多条语句(用分号或换行分开)</summary>
|
||||
public sealed class SqlCommand : DbCommand, ICloneable
|
||||
{
|
||||
private SqlConnection? _connection;
|
||||
private string _commandText = "";
|
||||
private CommandType _commandType = CommandType.Text;
|
||||
|
||||
public SqlCommand() { }
|
||||
|
||||
public SqlCommand(string cmdText) => CommandText = cmdText;
|
||||
|
||||
public SqlCommand(string cmdText, SqlConnection connection) : this(cmdText) => Connection = connection;
|
||||
|
||||
public SqlCommand(string cmdText, SqlConnection connection, SqlTransaction transaction) : this(cmdText, connection) => Transaction = transaction;
|
||||
|
||||
[Diagnostics.CodeAnalysis.AllowNull]
|
||||
public override string CommandText
|
||||
{
|
||||
get => _commandText;
|
||||
set => _commandText = value ?? "";
|
||||
}
|
||||
|
||||
public override int CommandTimeout { get; set; } = 30;
|
||||
|
||||
public override CommandType CommandType
|
||||
{
|
||||
get => _commandType;
|
||||
set => _commandType = value;
|
||||
}
|
||||
|
||||
public new SqlConnection? Connection
|
||||
{
|
||||
get => _connection;
|
||||
set => _connection = value;
|
||||
}
|
||||
|
||||
protected override DbConnection? DbConnection
|
||||
{
|
||||
get => _connection;
|
||||
set => _connection = (SqlConnection?)value;
|
||||
}
|
||||
|
||||
public new SqlParameterCollection Parameters { get; } = new();
|
||||
protected override DbParameterCollection DbParameterCollection => Parameters;
|
||||
|
||||
public new SqlTransaction? Transaction { get; set; }
|
||||
|
||||
protected override DbTransaction? DbTransaction
|
||||
{
|
||||
get => Transaction;
|
||||
set => Transaction = (SqlTransaction?)value;
|
||||
}
|
||||
|
||||
public override bool DesignTimeVisible { get; set; } = true;
|
||||
public override UpdateRowSource UpdatedRowSource { get; set; } = UpdateRowSource.Both;
|
||||
|
||||
public override int ExecuteNonQuery() => Run(nameof(ExecuteNonQuery)).RecordsAffected;
|
||||
|
||||
public override object? ExecuteScalar()
|
||||
{
|
||||
var result = Run(nameof(ExecuteScalar));
|
||||
return result.Results.Count > 0 && result.Results[0].Rows.Count > 0 ? result.Results[0].Rows[0][0] : null;
|
||||
}
|
||||
|
||||
public new SqlDataReader ExecuteReader() => ExecuteReader(CommandBehavior.Default);
|
||||
|
||||
public new SqlDataReader ExecuteReader(CommandBehavior behavior) =>
|
||||
new(Run(nameof(ExecuteReader)), behavior.HasFlag(CommandBehavior.CloseConnection) ? _connection : null);
|
||||
|
||||
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => ExecuteReader(behavior);
|
||||
|
||||
public override void Cancel() { }
|
||||
public override void Prepare() { }
|
||||
|
||||
public new SqlParameter CreateParameter() => new();
|
||||
protected override DbParameter CreateDbParameter() => CreateParameter();
|
||||
|
||||
public SqlCommand Clone()
|
||||
{
|
||||
var clone = new SqlCommand(_commandText, _connection!) { CommandType = _commandType, CommandTimeout = CommandTimeout, Transaction = Transaction };
|
||||
foreach (SqlParameter parameter in Parameters) clone.Parameters.Add(parameter.Clone());
|
||||
return clone;
|
||||
}
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
private BatchResult Run(string operation)
|
||||
{
|
||||
if (_connection == null) throw new InvalidOperationException($"{operation}: Connection 属性尚未初始化。");
|
||||
var session = _connection.Session(operation);
|
||||
if (string.IsNullOrWhiteSpace(_commandText)) throw new InvalidOperationException($"{operation}: CommandText 属性尚未初始化");
|
||||
if (_commandType == CommandType.StoredProcedure) throw SqlException.NotSupported("存储过程");
|
||||
try
|
||||
{
|
||||
return session.Execute(_commandText, name =>
|
||||
{
|
||||
var parameter = Parameters.Find(name);
|
||||
return parameter == null ? (false, null) : (true, parameter.Value);
|
||||
});
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
throw ex.Rethrow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SqlParameter : DbParameter, ICloneable
|
||||
{
|
||||
private SqlDbType _sqlDbType = SqlDbType.NVarChar;
|
||||
private bool _typeSet;
|
||||
private string _parameterName = "";
|
||||
private string _sourceColumn = "";
|
||||
|
||||
public SqlParameter() { }
|
||||
|
||||
public SqlParameter(string parameterName, object? value)
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public SqlParameter(string parameterName, SqlDbType dbType)
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
SqlDbType = dbType;
|
||||
}
|
||||
|
||||
public SqlParameter(string parameterName, SqlDbType dbType, int size) : this(parameterName, dbType) => Size = size;
|
||||
|
||||
public SqlParameter(string parameterName, SqlDbType dbType, int size, string sourceColumn) : this(parameterName, dbType, size) => SourceColumn = sourceColumn;
|
||||
|
||||
[Diagnostics.CodeAnalysis.AllowNull]
|
||||
public override string ParameterName
|
||||
{
|
||||
get => _parameterName;
|
||||
set => _parameterName = value ?? "";
|
||||
}
|
||||
|
||||
public override object? Value { get; set; }
|
||||
|
||||
public SqlDbType SqlDbType
|
||||
{
|
||||
get => _sqlDbType;
|
||||
set
|
||||
{
|
||||
_sqlDbType = value;
|
||||
_typeSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override DbType DbType
|
||||
{
|
||||
get => _sqlDbType switch
|
||||
{
|
||||
SqlDbType.Int => DbType.Int32,
|
||||
SqlDbType.BigInt => DbType.Int64,
|
||||
SqlDbType.SmallInt => DbType.Int16,
|
||||
SqlDbType.TinyInt => DbType.Byte,
|
||||
SqlDbType.Bit => DbType.Boolean,
|
||||
SqlDbType.Float => DbType.Double,
|
||||
SqlDbType.Real => DbType.Single,
|
||||
SqlDbType.Decimal or SqlDbType.Money => DbType.Decimal,
|
||||
SqlDbType.DateTime or SqlDbType.SmallDateTime or SqlDbType.Date => DbType.DateTime,
|
||||
SqlDbType.VarBinary or SqlDbType.Image or SqlDbType.Binary => DbType.Binary,
|
||||
SqlDbType.UniqueIdentifier => DbType.Guid,
|
||||
SqlDbType.VarChar or SqlDbType.Char or SqlDbType.Text => DbType.AnsiString,
|
||||
_ => DbType.String,
|
||||
};
|
||||
set => SqlDbType = value switch
|
||||
{
|
||||
DbType.Int32 => SqlDbType.Int,
|
||||
DbType.Int64 => SqlDbType.BigInt,
|
||||
DbType.Int16 => SqlDbType.SmallInt,
|
||||
DbType.Byte => SqlDbType.TinyInt,
|
||||
DbType.Boolean => SqlDbType.Bit,
|
||||
DbType.Double => SqlDbType.Float,
|
||||
DbType.Single => SqlDbType.Real,
|
||||
DbType.Decimal or DbType.Currency => SqlDbType.Decimal,
|
||||
DbType.DateTime or DbType.Date => SqlDbType.DateTime,
|
||||
DbType.Binary => SqlDbType.VarBinary,
|
||||
DbType.Guid => SqlDbType.UniqueIdentifier,
|
||||
DbType.AnsiString => SqlDbType.VarChar,
|
||||
_ => SqlDbType.NVarChar,
|
||||
};
|
||||
}
|
||||
|
||||
public override ParameterDirection Direction { get; set; } = ParameterDirection.Input;
|
||||
public override bool IsNullable { get; set; }
|
||||
public override int Size { get; set; }
|
||||
public new byte Precision { get; set; }
|
||||
public new byte Scale { get; set; }
|
||||
|
||||
[Diagnostics.CodeAnalysis.AllowNull]
|
||||
public override string SourceColumn
|
||||
{
|
||||
get => _sourceColumn;
|
||||
set => _sourceColumn = value ?? "";
|
||||
}
|
||||
|
||||
public override bool SourceColumnNullMapping { get; set; }
|
||||
public override DataRowVersion SourceVersion { get; set; } = DataRowVersion.Current;
|
||||
|
||||
public override void ResetDbType()
|
||||
{
|
||||
_sqlDbType = SqlDbType.NVarChar;
|
||||
_typeSet = false;
|
||||
}
|
||||
|
||||
internal bool TypeSet => _typeSet;
|
||||
|
||||
public SqlParameter Clone() => new(_parameterName, Value)
|
||||
{
|
||||
_sqlDbType = _sqlDbType,
|
||||
_typeSet = _typeSet,
|
||||
Direction = Direction,
|
||||
Size = Size,
|
||||
SourceColumn = SourceColumn,
|
||||
SourceVersion = SourceVersion,
|
||||
SourceColumnNullMapping = SourceColumnNullMapping,
|
||||
IsNullable = IsNullable,
|
||||
};
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
public override string ToString() => _parameterName;
|
||||
}
|
||||
|
||||
public sealed class SqlParameterCollection : DbParameterCollection
|
||||
{
|
||||
private readonly List<SqlParameter> _items = new();
|
||||
|
||||
public override int Count => _items.Count;
|
||||
public override object SyncRoot => this;
|
||||
|
||||
public new SqlParameter this[int index]
|
||||
{
|
||||
get => _items[index];
|
||||
set => _items[index] = value;
|
||||
}
|
||||
|
||||
public new SqlParameter this[string parameterName]
|
||||
{
|
||||
get => Find(parameterName) ?? throw new IndexOutOfRangeException($"此 SqlParameterCollection 中未包含带有 ParameterName“{parameterName}”的 SqlParameter。");
|
||||
set => _items[IndexOf(parameterName)] = value;
|
||||
}
|
||||
|
||||
/// <summary>按名字找参数,"@id" 和 "id" 视为同一个(和 SqlClient 一样不区分大小写)</summary>
|
||||
internal SqlParameter? Find(string name)
|
||||
{
|
||||
var bare = name.TrimStart('@');
|
||||
return _items.FirstOrDefault(p => string.Equals(p.ParameterName.TrimStart('@'), bare, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public SqlParameter Add(SqlParameter value)
|
||||
{
|
||||
_items.Add(value ?? throw new ArgumentNullException(nameof(value)));
|
||||
return value;
|
||||
}
|
||||
|
||||
public override int Add(object value)
|
||||
{
|
||||
Add(value as SqlParameter ?? throw new InvalidCastException("SqlParameterCollection 只接受非空的 SqlParameter 类型对象。"));
|
||||
return _items.Count - 1;
|
||||
}
|
||||
|
||||
public SqlParameter Add(string parameterName, SqlDbType sqlDbType) => Add(new SqlParameter(parameterName, sqlDbType));
|
||||
public SqlParameter Add(string parameterName, SqlDbType sqlDbType, int size) => Add(new SqlParameter(parameterName, sqlDbType, size));
|
||||
public SqlParameter Add(string parameterName, SqlDbType sqlDbType, int size, string sourceColumn) => Add(new SqlParameter(parameterName, sqlDbType, size, sourceColumn));
|
||||
public SqlParameter AddWithValue(string parameterName, object? value) => Add(new SqlParameter(parameterName, value));
|
||||
|
||||
public override void AddRange(Array values)
|
||||
{
|
||||
foreach (var value in values) Add(value);
|
||||
}
|
||||
|
||||
public void AddRange(SqlParameter[] values) => AddRange((Array)values);
|
||||
|
||||
public override void Clear() => _items.Clear();
|
||||
public override bool Contains(object value) => value is SqlParameter p && _items.Contains(p);
|
||||
public override bool Contains(string value) => Find(value) != null;
|
||||
public override void CopyTo(Array array, int index) => ((ICollection)_items).CopyTo(array, index);
|
||||
public override IEnumerator GetEnumerator() => _items.GetEnumerator();
|
||||
public override int IndexOf(object value) => value is SqlParameter p ? _items.IndexOf(p) : -1;
|
||||
public override int IndexOf(string parameterName) => Find(parameterName) is { } p ? _items.IndexOf(p) : -1;
|
||||
public override void Insert(int index, object value) => _items.Insert(index, (SqlParameter)value);
|
||||
public override void Remove(object value) => _items.Remove((SqlParameter)value);
|
||||
public override void RemoveAt(int index) => _items.RemoveAt(index);
|
||||
public override void RemoveAt(string parameterName) => _items.RemoveAt(IndexOf(parameterName));
|
||||
protected override DbParameter GetParameter(int index) => _items[index];
|
||||
protected override DbParameter GetParameter(string parameterName) => this[parameterName];
|
||||
protected override void SetParameter(int index, DbParameter value) => _items[index] = (SqlParameter)value;
|
||||
protected override void SetParameter(string parameterName, DbParameter value) => this[parameterName] = (SqlParameter)value;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Data.Common;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
public delegate void SqlRowUpdatingEventHandler(object sender, SqlRowUpdatingEventArgs e);
|
||||
public delegate void SqlRowUpdatedEventHandler(object sender, SqlRowUpdatedEventArgs e);
|
||||
|
||||
public sealed class SqlRowUpdatingEventArgs : RowUpdatingEventArgs
|
||||
{
|
||||
public SqlRowUpdatingEventArgs(DataRow row, IDbCommand? command, StatementType statementType, DataTableMapping tableMapping)
|
||||
: base(row, command, statementType, tableMapping) { }
|
||||
|
||||
public new SqlCommand? Command
|
||||
{
|
||||
get => (SqlCommand?)base.Command;
|
||||
set => base.Command = value;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SqlRowUpdatedEventArgs : RowUpdatedEventArgs
|
||||
{
|
||||
public SqlRowUpdatedEventArgs(DataRow row, IDbCommand? command, StatementType statementType, DataTableMapping tableMapping)
|
||||
: base(row, command, statementType, tableMapping) { }
|
||||
|
||||
public new SqlCommand? Command => (SqlCommand?)base.Command;
|
||||
}
|
||||
|
||||
/// <summary>在数据库和 DataSet 之间搬运数据:Fill 读出来,Update 把 DataTable 里改过的行写回去</summary>
|
||||
public sealed class SqlDataAdapter : DbDataAdapter, ICloneable
|
||||
{
|
||||
public SqlDataAdapter() { }
|
||||
|
||||
public SqlDataAdapter(SqlCommand selectCommand) => SelectCommand = selectCommand;
|
||||
|
||||
public SqlDataAdapter(string selectCommandText, SqlConnection selectConnection) => SelectCommand = new SqlCommand(selectCommandText, selectConnection);
|
||||
|
||||
public SqlDataAdapter(string selectCommandText, string selectConnectionString) =>
|
||||
SelectCommand = new SqlCommand(selectCommandText, new SqlConnection(selectConnectionString));
|
||||
|
||||
public new SqlCommand? SelectCommand
|
||||
{
|
||||
get => (SqlCommand?)base.SelectCommand;
|
||||
set => base.SelectCommand = value;
|
||||
}
|
||||
|
||||
public new SqlCommand? InsertCommand
|
||||
{
|
||||
get => (SqlCommand?)base.InsertCommand;
|
||||
set => base.InsertCommand = value;
|
||||
}
|
||||
|
||||
public new SqlCommand? UpdateCommand
|
||||
{
|
||||
get => (SqlCommand?)base.UpdateCommand;
|
||||
set => base.UpdateCommand = value;
|
||||
}
|
||||
|
||||
public new SqlCommand? DeleteCommand
|
||||
{
|
||||
get => (SqlCommand?)base.DeleteCommand;
|
||||
set => base.DeleteCommand = value;
|
||||
}
|
||||
|
||||
public event SqlRowUpdatingEventHandler? RowUpdating;
|
||||
public event SqlRowUpdatedEventHandler? RowUpdated;
|
||||
|
||||
protected override RowUpdatingEventArgs CreateRowUpdatingEvent(DataRow dataRow, IDbCommand? command, StatementType statementType, DataTableMapping tableMapping) =>
|
||||
new SqlRowUpdatingEventArgs(dataRow, command, statementType, tableMapping);
|
||||
|
||||
protected override RowUpdatedEventArgs CreateRowUpdatedEvent(DataRow dataRow, IDbCommand? command, StatementType statementType, DataTableMapping tableMapping) =>
|
||||
new SqlRowUpdatedEventArgs(dataRow, command, statementType, tableMapping);
|
||||
|
||||
protected override void OnRowUpdating(RowUpdatingEventArgs value) => RowUpdating?.Invoke(this, (SqlRowUpdatingEventArgs)value);
|
||||
|
||||
protected override void OnRowUpdated(RowUpdatedEventArgs value) => RowUpdated?.Invoke(this, (SqlRowUpdatedEventArgs)value);
|
||||
|
||||
object ICloneable.Clone() => new SqlDataAdapter { SelectCommand = SelectCommand, InsertCommand = InsertCommand, UpdateCommand = UpdateCommand, DeleteCommand = DeleteCommand };
|
||||
}
|
||||
|
||||
/// <summary>按 SqlDataAdapter 的查询语句自动生成 INSERT / UPDATE / DELETE(要求查询包含主键列)</summary>
|
||||
public sealed class SqlCommandBuilder : DbCommandBuilder
|
||||
{
|
||||
public SqlCommandBuilder()
|
||||
{
|
||||
QuotePrefix = "[";
|
||||
QuoteSuffix = "]";
|
||||
}
|
||||
|
||||
public SqlCommandBuilder(SqlDataAdapter adapter) : this() => DataAdapter = adapter;
|
||||
|
||||
public new SqlDataAdapter? DataAdapter
|
||||
{
|
||||
get => (SqlDataAdapter?)base.DataAdapter;
|
||||
set => base.DataAdapter = value;
|
||||
}
|
||||
|
||||
public new SqlCommand GetInsertCommand() => (SqlCommand)base.GetInsertCommand();
|
||||
public new SqlCommand GetInsertCommand(bool useColumnsForParameterNames) => (SqlCommand)base.GetInsertCommand(useColumnsForParameterNames);
|
||||
public new SqlCommand GetUpdateCommand() => (SqlCommand)base.GetUpdateCommand();
|
||||
public new SqlCommand GetUpdateCommand(bool useColumnsForParameterNames) => (SqlCommand)base.GetUpdateCommand(useColumnsForParameterNames);
|
||||
public new SqlCommand GetDeleteCommand() => (SqlCommand)base.GetDeleteCommand();
|
||||
public new SqlCommand GetDeleteCommand(bool useColumnsForParameterNames) => (SqlCommand)base.GetDeleteCommand(useColumnsForParameterNames);
|
||||
|
||||
protected override void ApplyParameterInfo(DbParameter parameter, DataRow row, StatementType statementType, bool whereClause) { }
|
||||
|
||||
protected override string GetParameterName(int parameterOrdinal) => "@p" + parameterOrdinal.ToString(Globalization.CultureInfo.InvariantCulture);
|
||||
protected override string GetParameterName(string parameterName) => "@" + parameterName;
|
||||
protected override string GetParameterPlaceholder(int parameterOrdinal) => GetParameterName(parameterOrdinal);
|
||||
|
||||
public override string QuoteIdentifier(string unquotedIdentifier) => "[" + unquotedIdentifier.Replace("]", "]]") + "]";
|
||||
|
||||
protected override void SetRowUpdatingHandler(DbDataAdapter adapter)
|
||||
{
|
||||
if (adapter is not SqlDataAdapter sql) throw new InvalidOperationException("SqlCommandBuilder 只能用于 SqlDataAdapter。");
|
||||
if (adapter == base.DataAdapter) sql.RowUpdating += Handler;
|
||||
else sql.RowUpdating -= Handler;
|
||||
}
|
||||
|
||||
private void Handler(object sender, SqlRowUpdatingEventArgs e) => RowUpdatingHandler(e);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using System.Collections;
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using Sim.Data.Engine;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
/// <summary>按行读取查询结果(结果已在内存里,读的时候不再访问数据库)</summary>
|
||||
public sealed class SqlDataReader : DbDataReader
|
||||
{
|
||||
private readonly BatchResult _batch;
|
||||
private readonly SqlConnection? _closeConnection;
|
||||
private int _resultIndex;
|
||||
private int _row = -1;
|
||||
private bool _closed;
|
||||
|
||||
internal SqlDataReader(BatchResult batch, SqlConnection? closeConnection)
|
||||
{
|
||||
_batch = batch;
|
||||
_closeConnection = closeConnection;
|
||||
}
|
||||
|
||||
private QueryResult? Current => _resultIndex < _batch.Results.Count ? _batch.Results[_resultIndex] : null;
|
||||
|
||||
private object[] Row
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_closed) throw new InvalidOperationException("阅读器关闭时尝试调用 Read 无效。");
|
||||
var current = Current;
|
||||
if (current == null || _row < 0 || _row >= current.Rows.Count) throw new InvalidOperationException("不存在数据时进行无效的读取尝试。");
|
||||
return current.Rows[_row];
|
||||
}
|
||||
}
|
||||
|
||||
public override int FieldCount => Current?.Columns.Count ?? 0;
|
||||
public override bool HasRows => Current?.Rows.Count > 0;
|
||||
public override bool IsClosed => _closed;
|
||||
public override int RecordsAffected => _batch.RecordsAffected;
|
||||
public override int Depth => 0;
|
||||
|
||||
public override object this[int ordinal] => GetValue(ordinal);
|
||||
public override object this[string name] => GetValue(GetOrdinal(name));
|
||||
|
||||
public override bool Read()
|
||||
{
|
||||
if (_closed) throw new InvalidOperationException("阅读器关闭时尝试调用 Read 无效。");
|
||||
var current = Current;
|
||||
if (current == null) return false;
|
||||
_row++;
|
||||
return _row < current.Rows.Count;
|
||||
}
|
||||
|
||||
public override bool NextResult()
|
||||
{
|
||||
_resultIndex++;
|
||||
_row = -1;
|
||||
return Current != null;
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (_closed) return;
|
||||
_closed = true;
|
||||
_closeConnection?.Close();
|
||||
}
|
||||
|
||||
public override string GetName(int ordinal) => Column(ordinal).Name;
|
||||
|
||||
public override int GetOrdinal(string name)
|
||||
{
|
||||
var columns = Current?.Columns ?? throw new IndexOutOfRangeException(name);
|
||||
for (var i = 0; i < columns.Count; i++)
|
||||
if (string.Equals(columns[i].Name, name, StringComparison.OrdinalIgnoreCase)) return i;
|
||||
throw new IndexOutOfRangeException(name);
|
||||
}
|
||||
|
||||
public override Type GetFieldType(int ordinal) => Column(ordinal).ClrType;
|
||||
public override string GetDataTypeName(int ordinal) => Column(ordinal).Type?.Name ?? "sql_variant";
|
||||
|
||||
public override object GetValue(int ordinal)
|
||||
{
|
||||
Column(ordinal);
|
||||
return Row[ordinal];
|
||||
}
|
||||
|
||||
public override int GetValues(object[] values)
|
||||
{
|
||||
var row = Row;
|
||||
var count = Math.Min(values.Length, row.Length);
|
||||
Array.Copy(row, values, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
public override bool IsDBNull(int ordinal) => GetValue(ordinal) is DBNull;
|
||||
|
||||
public override bool GetBoolean(int ordinal) => Cast<bool>(ordinal);
|
||||
public override byte GetByte(int ordinal) => Cast<byte>(ordinal);
|
||||
public override char GetChar(int ordinal) => throw new NotSupportedException("SqlDataReader 不支持 GetChar。");
|
||||
public override DateTime GetDateTime(int ordinal) => Cast<DateTime>(ordinal);
|
||||
public override decimal GetDecimal(int ordinal) => Cast<decimal>(ordinal);
|
||||
public override double GetDouble(int ordinal) => Cast<double>(ordinal);
|
||||
public override float GetFloat(int ordinal) => Cast<float>(ordinal);
|
||||
public override Guid GetGuid(int ordinal) => Cast<Guid>(ordinal);
|
||||
public override short GetInt16(int ordinal) => Cast<short>(ordinal);
|
||||
public override int GetInt32(int ordinal) => Cast<int>(ordinal);
|
||||
public override long GetInt64(int ordinal) => Cast<long>(ordinal);
|
||||
public override string GetString(int ordinal) => Cast<string>(ordinal);
|
||||
|
||||
/// <summary>和 SqlClient 一样严格:列是 varchar 时 GetInt32 报"指定的转换无效",是 NULL 时报"数据为空"</summary>
|
||||
private T Cast<T>(int ordinal)
|
||||
{
|
||||
var value = GetValue(ordinal);
|
||||
if (value is DBNull) throw new InvalidCastException("数据为空。不能对空值调用此方法或属性。");
|
||||
if (value is T typed) return typed;
|
||||
// 表达式列(如 count(*))没有声明类型,数字之间可以互相转换
|
||||
if (Column(ordinal).Type == null && value is IConvertible && typeof(T) != typeof(string))
|
||||
return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
|
||||
throw new InvalidCastException("指定的转换无效。");
|
||||
}
|
||||
|
||||
public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length)
|
||||
{
|
||||
var bytes = Cast<byte[]>(ordinal);
|
||||
if (buffer == null) return bytes.Length;
|
||||
var count = (int)Math.Min(length, bytes.Length - dataOffset);
|
||||
if (count <= 0) return 0;
|
||||
Array.Copy(bytes, dataOffset, buffer, bufferOffset, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length)
|
||||
{
|
||||
var text = Cast<string>(ordinal);
|
||||
if (buffer == null) return text.Length;
|
||||
var count = (int)Math.Min(length, text.Length - dataOffset);
|
||||
if (count <= 0) return 0;
|
||||
text.CopyTo((int)dataOffset, buffer, bufferOffset, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
public override IEnumerator GetEnumerator() => new DbEnumerator(this, closeReader: false);
|
||||
|
||||
/// <summary>列的结构信息:DbDataAdapter 填充、SqlCommandBuilder 生成增删改语句时要用(主键、自增、来自哪张表的哪一列)</summary>
|
||||
public override DataTable GetSchemaTable()
|
||||
{
|
||||
var table = new DataTable("SchemaTable");
|
||||
table.Columns.Add(SchemaTableColumn.ColumnName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int));
|
||||
table.Columns.Add(SchemaTableColumn.ColumnSize, typeof(int));
|
||||
table.Columns.Add(SchemaTableColumn.NumericPrecision, typeof(short));
|
||||
table.Columns.Add(SchemaTableColumn.NumericScale, typeof(short));
|
||||
table.Columns.Add(SchemaTableColumn.IsUnique, typeof(bool));
|
||||
table.Columns.Add(SchemaTableColumn.IsKey, typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.BaseColumnName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.BaseTableName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.DataType, typeof(Type));
|
||||
table.Columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool));
|
||||
table.Columns.Add(SchemaTableColumn.ProviderType, typeof(int));
|
||||
table.Columns.Add(SchemaTableColumn.IsAliased, typeof(bool));
|
||||
table.Columns.Add(SchemaTableColumn.IsExpression, typeof(bool));
|
||||
table.Columns.Add("IsIdentity", typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool));
|
||||
table.Columns.Add(SchemaTableColumn.IsLong, typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool));
|
||||
table.Columns.Add("DataTypeName", typeof(string));
|
||||
|
||||
var columns = Current?.Columns ?? Array.Empty<ResultColumn>();
|
||||
for (var i = 0; i < columns.Count; i++)
|
||||
{
|
||||
var c = columns[i];
|
||||
var row = table.NewRow();
|
||||
row[SchemaTableColumn.ColumnName] = c.Name;
|
||||
row[SchemaTableColumn.ColumnOrdinal] = i;
|
||||
row[SchemaTableColumn.ColumnSize] = c.Type?.MaxLength ?? (c.Type?.IsMax == true ? int.MaxValue : 0);
|
||||
row[SchemaTableColumn.NumericPrecision] = (short)(c.Type?.Precision ?? 0);
|
||||
row[SchemaTableColumn.NumericScale] = (short)(c.Type?.Scale ?? 0);
|
||||
row[SchemaTableColumn.IsUnique] = c.IsKey;
|
||||
row[SchemaTableColumn.IsKey] = c.IsKey;
|
||||
row[SchemaTableColumn.BaseColumnName] = (object?)c.BaseColumn ?? DBNull.Value;
|
||||
row[SchemaTableColumn.BaseSchemaName] = c.BaseTable != null ? "dbo" : DBNull.Value;
|
||||
row[SchemaTableColumn.BaseTableName] = (object?)c.BaseTable ?? DBNull.Value;
|
||||
row[SchemaTableColumn.DataType] = c.ClrType;
|
||||
row[SchemaTableColumn.AllowDBNull] = c.AllowNull;
|
||||
row[SchemaTableColumn.ProviderType] = 0;
|
||||
row[SchemaTableColumn.IsAliased] = c.BaseColumn != null && !string.Equals(c.BaseColumn, c.Name, StringComparison.OrdinalIgnoreCase);
|
||||
row[SchemaTableColumn.IsExpression] = c.BaseColumn == null;
|
||||
row["IsIdentity"] = c.IsIdentity;
|
||||
row[SchemaTableOptionalColumn.IsAutoIncrement] = c.IsIdentity;
|
||||
row[SchemaTableOptionalColumn.IsRowVersion] = false;
|
||||
row[SchemaTableOptionalColumn.IsHidden] = false;
|
||||
row[SchemaTableColumn.IsLong] = c.Type is { IsMax: true } or { Name: "text" or "ntext" or "image" };
|
||||
row[SchemaTableOptionalColumn.IsReadOnly] = c.IsIdentity || c.BaseColumn == null;
|
||||
row["DataTypeName"] = c.Type?.Name ?? "sql_variant";
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private ResultColumn Column(int ordinal)
|
||||
{
|
||||
var columns = Current?.Columns;
|
||||
if (columns == null || ordinal < 0 || ordinal >= columns.Count) throw new IndexOutOfRangeException("索引超出了数组界限。");
|
||||
return columns[ordinal];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Data.Common;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库报错。Number 和 Message 与中文版 SQL Server 一致(如 208"对象名 'X' 无效。"),
|
||||
/// 学生在网上搜报错信息能找到对应的说明。
|
||||
/// </summary>
|
||||
public sealed class SqlException : DbException
|
||||
{
|
||||
internal SqlException(int number, string message, int lineNumber = 1) : base(message, unchecked((int)0x80131904))
|
||||
{
|
||||
Number = number;
|
||||
LineNumber = lineNumber;
|
||||
}
|
||||
|
||||
public int Number { get; }
|
||||
public int LineNumber { get; }
|
||||
public byte Class => 16;
|
||||
public byte State => 1;
|
||||
public string Server => ".";
|
||||
public string Procedure => "";
|
||||
|
||||
/// <summary>在 SqlClient 的公开方法里重新抛出:学生看到的调用栈从 SqlCommand.ExecuteNonQuery 等开始,不含平台内部的实现</summary>
|
||||
internal SqlException Rethrow() => new(Number, Message, LineNumber);
|
||||
|
||||
public override string ToString() => $"{GetType().FullName} (0x80131904): {Message}{(StackTrace != null ? Environment.NewLine + StackTrace : "")}";
|
||||
|
||||
// ---------- 常见报错 ----------
|
||||
|
||||
internal static SqlException Syntax(string near, int line = 1) =>
|
||||
new(102, IsKeyword(near) ? $"关键字 '{near}' 附近有语法错误。" : $"'{near}' 附近有语法错误。", line);
|
||||
|
||||
internal static SqlException InvalidObject(string name) => new(208, $"对象名 '{name}' 无效。");
|
||||
internal static SqlException InvalidColumn(string name) => new(207, $"列名 '{name}' 无效。");
|
||||
internal static SqlException AmbiguousColumn(string name) => new(209, $"列名 '{name}' 不明确。");
|
||||
internal static SqlException ColumnCountMismatch() => new(213, "列名或所提供值的数目与表定义不匹配。");
|
||||
internal static SqlException IdentityInsert(string table) => new(544, $"当 IDENTITY_INSERT 设置为 OFF 时,不能为表 '{table}' 中的标识列插入显式值。");
|
||||
internal static SqlException NullNotAllowed(string column, string table) => new(515, $"不能将值 NULL 插入列 '{column}',表 '{table}';列不允许有 Null 值。INSERT 失败。");
|
||||
internal static SqlException DuplicateKey(string table) => new(2627, $"违反了 PRIMARY KEY 约束。不能在对象 'dbo.{table}' 中插入重复键。");
|
||||
internal static SqlException Truncated() => new(8152, "将截断字符串或二进制数据。");
|
||||
internal static SqlException ConvertFailed(string value, string type) => new(245, $"在将 varchar 值 '{value}' 转换成数据类型 {type} 时失败。");
|
||||
internal static SqlException DateConvertFailed() => new(241, "从字符串转换日期和/或时间时,转换失败。");
|
||||
internal static SqlException ObjectExists(string name) => new(2714, $"数据库中已存在名为 '{name}' 的对象。");
|
||||
internal static SqlException DatabaseExists(string name) => new(1801, $"数据库 '{name}' 已存在。请选择其他数据库名称。");
|
||||
internal static SqlException CannotOpenDatabase(string name) => new(4060, $"无法打开登录所请求的数据库 \"{name}\"。登录失败。");
|
||||
internal static SqlException DatabaseNotFound(string name) => new(911, $"数据库 '{name}' 不存在。请确保正确地输入了该名称。");
|
||||
internal static SqlException DatabaseInUse(string name) => new(3702, $"无法删除数据库 \"{name}\",因为该数据库当前正在使用。");
|
||||
internal static SqlException CannotDropTable(string name) => new(3701, $"无法对 表 '{name}' 执行 删除,因为它不存在,或者您没有所需的权限。");
|
||||
internal static SqlException UnknownFunction(string name) => new(195, $"'{name}' 不是可以识别的 内置函数名称。");
|
||||
internal static SqlException UndeclaredVariable(string name) => new(137, $"必须声明标量变量 \"{name}\"。");
|
||||
internal static SqlException DivideByZero() => new(8134, "遇到以零作除数错误。");
|
||||
internal static SqlException NotSupported(string what) => new(40000, $"平台暂不支持 {what}。");
|
||||
|
||||
private static bool IsKeyword(string word) => word.ToUpperInvariant() is
|
||||
"SELECT" or "FROM" or "WHERE" or "INSERT" or "INTO" or "VALUES" or "UPDATE" or "SET" or "DELETE" or "CREATE" or "TABLE"
|
||||
or "DROP" or "ALTER" or "AND" or "OR" or "NOT" or "NULL" or "ORDER" or "GROUP" or "BY" or "HAVING" or "AS" or "ON"
|
||||
or "JOIN" or "INNER" or "LEFT" or "RIGHT" or "OUTER" or "IS" or "IN" or "LIKE" or "BETWEEN" or "TOP" or "DISTINCT"
|
||||
or "PRIMARY" or "KEY" or "IDENTITY" or "DEFAULT" or "DATABASE" or "USE" or "CASE" or "WHEN" or "THEN" or "ELSE" or "END";
|
||||
}
|
||||
Reference in new issue
Block a user