项目 6 数据存储:仿冒 SqlClient(T-SQL 转写成 SQLite)、数据库页面、DataGridView、DateTimePicker
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- 每个学生一套数据库,长期保存(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:
xuyueandClaude Opus 5.5 committed 2026-10-23 09:18:56 +08:00
1 parent 6b088240da
commit 670abe5921
72 files changed
+7253 -51

No files matched your search

+302
View File
@@ -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;
}
+169
View File
@@ -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);
}
+211
View File
@@ -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";
}