项目 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

+168
View File
@@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using Microsoft.Data.Sqlite;
namespace Sim.Data.Engine;
/// <summary>一列的定义(来自元数据表,保留建表时的 T-SQL 写法)</summary>
internal sealed record ColumnInfo(string Name, TSqlType Type, bool Nullable, bool Identity, bool PrimaryKey, string? Default);
internal sealed record TableInfo(string Name, IReadOnlyList<ColumnInfo> Columns)
{
public ColumnInfo? Find(string column) => Columns.FirstOrDefault(c => string.Equals(c.Name, column, StringComparison.OrdinalIgnoreCase));
public ColumnInfo? Identity => Columns.FirstOrDefault(c => c.Identity);
}
/// <summary>
/// 一个学生的全部数据库:一个目录,每个库一个 SQLite 文件(库名.db)。
/// master 是默认库(连接字符串里不写 Initial Catalog 时连到它),和 SQL Server 一样也能在里面建表。
/// </summary>
internal sealed class DatabaseStore
{
public const string Master = "master";
/// <summary>元数据表:每列的 T-SQL 类型、是否自增等。学生的 SQL 里不能引用它</summary>
public const string MetaTable = "__sim_columns";
private const int MaxDatabases = 20;
/// <summary>每个库最多约 50 MB(4 KB 一页)</summary>
private const int MaxPages = 12800;
public DatabaseStore(string directory) => Directory = directory;
public string Directory { get; }
/// <summary>学生程序运行时:服务器通过环境变量 SIM_DB_DIR 指定(沙箱里挂载学生自己的数据库目录)</summary>
public static DatabaseStore? FromEnvironment() =>
Environment.GetEnvironmentVariable("SIM_DB_DIR") is { Length: > 0 } dir ? new DatabaseStore(dir) : null;
public IEnumerable<string> List() =>
System.IO.Directory.Exists(Directory)
? System.IO.Directory.EnumerateFiles(Directory, "*.db").Select(f => Path.GetFileNameWithoutExtension(f)!).Where(n => n != Master).Order(StringComparer.Ordinal)
: Enumerable.Empty<string>();
/// <summary>库名大小写不敏感(和 SQL Server 一样),返回磁盘上实际的名字</summary>
public string? Resolve(string name)
{
if (string.Equals(name, Master, StringComparison.OrdinalIgnoreCase)) return Master;
return List().FirstOrDefault(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase));
}
public SqliteConnection Open(string name)
{
var resolved = Resolve(name);
if (resolved == null) throw SqlException.CannotOpenDatabase(name);
if (resolved == Master && !File.Exists(PathOf(Master))) Create(Master, allowMaster: true);
// 不用连接池:最后一个连接关闭时 WAL 里的数据写回主文件,复制、恢复数据库文件时不会丢数据
var connection = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = PathOf(resolved), Mode = SqliteOpenMode.ReadWrite, Pooling = false }.ToString());
connection.Open();
Functions.Register(connection);
using var pragma = connection.CreateCommand();
pragma.CommandText = $"PRAGMA max_page_count = {MaxPages}; PRAGMA case_sensitive_like = OFF;";
pragma.ExecuteNonQuery();
return connection;
}
public void Create(string name, bool allowMaster = false)
{
CheckName(name);
if (!allowMaster && Resolve(name) != null) throw SqlException.DatabaseExists(name);
if (List().Count() >= MaxDatabases) throw new SqlException(1105, $"最多只能建 {MaxDatabases} 个数据库,先删除不用的数据库。");
System.IO.Directory.CreateDirectory(Directory);
using var connection = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = PathOf(name), Mode = SqliteOpenMode.ReadWriteCreate, Pooling = false }.ToString());
connection.Open();
using var command = connection.CreateCommand();
command.CommandText =
"PRAGMA journal_mode = WAL;" +
$"CREATE TABLE IF NOT EXISTS {MetaTable} (table_name TEXT NOT NULL COLLATE NOCASE, column_name TEXT NOT NULL COLLATE NOCASE, " +
"ordinal INTEGER NOT NULL, type_name TEXT NOT NULL, nullable INTEGER NOT NULL, is_identity INTEGER NOT NULL, " +
"is_primary_key INTEGER NOT NULL, default_text TEXT, PRIMARY KEY (table_name, column_name));";
command.ExecuteNonQuery();
}
public void Drop(string name)
{
var resolved = Resolve(name);
if (resolved == null || resolved == Master) throw new SqlException(3701, $"无法对 数据库 '{name}' 执行 删除,因为它不存在,或者您没有所需的权限。");
foreach (var suffix in new[] { "", "-wal", "-shm" }) File.Delete(PathOf(resolved) + suffix);
}
private string PathOf(string name) => Path.Combine(Directory, name + ".db");
/// <summary>库名就是文件名:不能有路径分隔符等字符</summary>
private static void CheckName(string name)
{
if (name.Length is 0 or > 64 || name.StartsWith('.') || name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || name.IndexOfAny(['\\', ':', '*', '?', '"', '<', '>', '|']) >= 0)
throw new SqlException(102, $"数据库名 '{name}' 无效:不能为空、不能超过 64 个字,也不能包含 \\ / : * ? \" < > | 等字符。");
}
// ---------- 表结构(元数据表) ----------
public static TableInfo? GetTable(SqliteConnection connection, string name)
{
if (IsHidden(name)) return null;
using var command = connection.CreateCommand();
command.CommandText = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = $name COLLATE NOCASE";
command.Parameters.AddWithValue("$name", name);
if (command.ExecuteScalar() is not string actual) return null;
command.CommandText = $"SELECT column_name, type_name, nullable, is_identity, is_primary_key, default_text FROM {MetaTable} WHERE table_name = $name ORDER BY ordinal";
var columns = new List<ColumnInfo>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
columns.Add(new ColumnInfo(reader.GetString(0), TSqlType.Parse(reader.GetString(1)), reader.GetInt64(2) != 0,
reader.GetInt64(3) != 0, reader.GetInt64(4) != 0, reader.IsDBNull(5) ? null : reader.GetString(5)));
}
}
return new TableInfo(actual, columns);
}
public static IReadOnlyList<string> ListTables(SqliteConnection connection)
{
using var command = connection.CreateCommand();
command.CommandText = "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name";
var tables = new List<string>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var name = reader.GetString(0);
if (!IsHidden(name)) tables.Add(name);
}
return tables;
}
public static void SaveColumns(SqliteConnection connection, string table, IEnumerable<ColumnInfo> columns, int firstOrdinal = 0)
{
using var command = connection.CreateCommand();
command.CommandText = $"INSERT OR REPLACE INTO {MetaTable} VALUES ($t, $c, $o, $type, $null, $id, $pk, $def)";
var ordinal = firstOrdinal;
foreach (var column in columns)
{
command.Parameters.Clear();
command.Parameters.AddWithValue("$t", table);
command.Parameters.AddWithValue("$c", column.Name);
command.Parameters.AddWithValue("$o", ordinal++);
command.Parameters.AddWithValue("$type", column.Type.ToString());
command.Parameters.AddWithValue("$null", column.Nullable ? 1 : 0);
command.Parameters.AddWithValue("$id", column.Identity ? 1 : 0);
command.Parameters.AddWithValue("$pk", column.PrimaryKey ? 1 : 0);
command.Parameters.AddWithValue("$def", (object?)column.Default ?? DBNull.Value);
command.ExecuteNonQuery();
}
}
public static void DeleteColumns(SqliteConnection connection, string table)
{
using var command = connection.CreateCommand();
command.CommandText = $"DELETE FROM {MetaTable} WHERE table_name = $t";
command.Parameters.AddWithValue("$t", table);
command.ExecuteNonQuery();
}
/// <summary>平台内部的表和 SQLite 系统表,学生看不到也不能引用</summary>
public static bool IsHidden(string name) =>
name.StartsWith("__sim", StringComparison.OrdinalIgnoreCase) || name.StartsWith("sqlite_", StringComparison.OrdinalIgnoreCase);
}
+254
View File
@@ -0,0 +1,254 @@
using System;
using System.Data.SqlClient;
using System.Globalization;
using Microsoft.Data.Sqlite;
namespace Sim.Data.Engine;
/// <summary>
/// 注册到 SQLite 连接上的 T-SQL 函数和运算(转写后的 SQL 调用它们,学生的 SQL 不能直接调用 tsql_ 开头的名字)。
/// 函数里抛出的 SqlException 先记在 <see cref="Pending"/>,执行语句处取出原样抛给学生(SQLite 只能传回文字信息)。
/// </summary>
internal static class Functions
{
[ThreadStatic] private static SqlException? _pending;
public static SqlException? TakePending()
{
var pending = _pending;
_pending = null;
return pending;
}
private static Exception Fail(SqlException error)
{
_pending = error;
return error;
}
public static void Register(SqliteConnection c)
{
c.CreateFunction<object?, object?, object?>("tsql_add", Add, isDeterministic: true);
c.CreateFunction<object?, object?, object?>("tsql_div", Divide, isDeterministic: true);
c.CreateFunction<object?, object?, object?>("tsql_mod", Modulo, isDeterministic: true);
c.CreateFunction("tsql_getdate", () => Values.FormatDateTime(TruncateToMilliseconds(DateTime.Now)));
c.CreateFunction<string, object?, object?>("tsql_datepart", DatePart, isDeterministic: true);
c.CreateFunction<string, object?, object?, object?>("tsql_dateadd", DateAdd, isDeterministic: true);
c.CreateFunction<string, object?, object?, object?>("tsql_datediff", DateDiff, isDeterministic: true);
c.CreateFunction<string, object?, object?, object?, object?>("tsql_convert", Convert, isDeterministic: true);
c.CreateFunction<object?, object?>("tsql_len", s => s is null ? null : (long)Text(s).TrimEnd(' ').Length, isDeterministic: true);
c.CreateFunction<object?, object?, object?>("tsql_left", (s, n) => s is null || n is null ? null : Left(Text(s), ToLong(n)), isDeterministic: true);
c.CreateFunction<object?, object?, object?>("tsql_right", (s, n) => s is null || n is null ? null : Right(Text(s), ToLong(n)), isDeterministic: true);
c.CreateFunction<object?, object?, object?>("tsql_charindex", (f, s) => f is null || s is null ? null : (long)Text(s).IndexOf(Text(f), StringComparison.OrdinalIgnoreCase) + 1, isDeterministic: true);
c.CreateFunction<object?, object?>("tsql_floor", x => x is null ? null : Math.Floor(ToDouble(x)), isDeterministic: true);
c.CreateFunction<object?, object?>("tsql_ceiling", x => x is null ? null : Math.Ceiling(ToDouble(x)), isDeterministic: true);
c.CreateFunction<object?, object?, object?>("tsql_power", (x, y) => x is null || y is null ? null : Math.Pow(ToDouble(x), ToDouble(y)), isDeterministic: true);
c.CreateFunction<object?, object?>("tsql_sqrt", x => x is null ? null : Math.Sqrt(ToDouble(x)), isDeterministic: true);
c.CreateFunction("tsql_newid", () => Guid.NewGuid().ToString().ToUpperInvariant());
c.CreateFunction("tsql_rand", () => Random.Shared.NextDouble());
}
private static DateTime TruncateToMilliseconds(DateTime value) => new(value.Ticks - value.Ticks % TimeSpan.TicksPerMillisecond, value.Kind);
// ---------- 运算 ----------
/// <summary>T-SQL 的 +:两边都是字符串时连接,有数字时按数字相加(字符串转不成数字时报错)</summary>
private static object? Add(object? a, object? b)
{
if (a is null || b is null) return null;
if (a is string sa && b is string sb) return sa + sb;
if (a is byte[] || b is byte[]) throw Fail(SqlException.NotSupported("二进制数据的 + 运算"));
var x = Number(a);
var y = Number(b);
return x is long lx && y is long ly ? lx + ly : ToDouble(x) + ToDouble(y);
}
private static object? Divide(object? a, object? b)
{
if (a is null || b is null) return null;
var x = Number(a);
var y = Number(b);
if (ToDouble(y) == 0) throw Fail(SqlException.DivideByZero());
return x is long lx && y is long ly ? lx / ly : ToDouble(x) / ToDouble(y);
}
private static object? Modulo(object? a, object? b)
{
if (a is null || b is null) return null;
var x = Number(a);
var y = Number(b);
if (ToDouble(y) == 0) throw Fail(SqlException.DivideByZero());
return x is long lx && y is long ly ? lx % ly : ToDouble(x) % ToDouble(y);
}
/// <summary>数字原样返回;字符串按 T-SQL 隐式转换成 int(带小数点时转成 float),转不了时报 245</summary>
private static object Number(object value) => value switch
{
long or double => value,
string s when long.TryParse(s.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var l) => l,
string s when s.Contains('.') && double.TryParse(s.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => d,
_ => throw Fail(Values.ConvertError(value, "int")),
};
private static double ToDouble(object value) => value switch
{
long l => l,
double d => d,
_ => ToDouble(Number(value)),
};
private static long ToLong(object value) => value is long l ? l : (long)ToDouble(value);
private static string Text(object value) => value switch
{
string s => s,
double d => d.ToString(CultureInfo.InvariantCulture),
_ => System.Convert.ToString(value, CultureInfo.InvariantCulture) ?? "",
};
private static string Left(string s, long n) => n <= 0 ? "" : s[..(int)Math.Min(n, s.Length)];
private static string Right(string s, long n) => n <= 0 ? "" : s[^(int)Math.Min(n, s.Length)..];
// ---------- 日期 ----------
private static DateTime ToDateTime(object value)
{
if (value is string s && Values.TryParseDateTime(s, out var d)) return d;
throw Fail(SqlException.DateConvertFailed());
}
private static object? DatePart(string part, object? value)
{
if (value is null) return null;
var d = ToDateTime(value);
return part.ToLowerInvariant() switch
{
"year" or "yy" or "yyyy" => d.Year,
"quarter" or "qq" or "q" => (d.Month - 1) / 3 + 1,
"month" or "mm" or "m" => d.Month,
"dayofyear" or "dy" or "y" => d.DayOfYear,
"day" or "dd" or "d" => d.Day,
"week" or "wk" or "ww" => CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(d, CalendarWeekRule.FirstDay, DayOfWeek.Sunday),
"weekday" or "dw" or "w" => (int)d.DayOfWeek + 1,
"hour" or "hh" => d.Hour,
"minute" or "mi" or "n" => d.Minute,
"second" or "ss" or "s" => d.Second,
"millisecond" or "ms" => d.Millisecond,
_ => throw Fail(new SqlException(155, $"'{part}' 不是可以识别的 datepart 选项。")),
};
}
private static object? DateAdd(string part, object? amount, object? value)
{
if (amount is null || value is null) return null;
var d = ToDateTime(value);
var n = ToLong(amount);
var result = part.ToLowerInvariant() switch
{
"year" or "yy" or "yyyy" => d.AddYears((int)n),
"quarter" or "qq" or "q" => d.AddMonths((int)n * 3),
"month" or "mm" or "m" => d.AddMonths((int)n),
"day" or "dd" or "d" or "dayofyear" or "dy" or "y" or "weekday" or "dw" or "w" => d.AddDays(n),
"week" or "wk" or "ww" => d.AddDays(n * 7),
"hour" or "hh" => d.AddHours(n),
"minute" or "mi" or "n" => d.AddMinutes(n),
"second" or "ss" or "s" => d.AddSeconds(n),
"millisecond" or "ms" => d.AddMilliseconds(n),
_ => throw Fail(new SqlException(155, $"'{part}' 不是可以识别的 dateadd 选项。")),
};
return Values.FormatDateTime(result);
}
/// <summary>和 SQL Server 一样数跨过的边界:DATEDIFF(day, '2024-01-01 23:00', '2024-01-02 01:00') = 1</summary>
private static object? DateDiff(string part, object? start, object? end)
{
if (start is null || end is null) return null;
var a = ToDateTime(start);
var b = ToDateTime(end);
return part.ToLowerInvariant() switch
{
"year" or "yy" or "yyyy" => (long)(b.Year - a.Year),
"quarter" or "qq" or "q" => (long)((b.Year - a.Year) * 4 + (b.Month - 1) / 3 - (a.Month - 1) / 3),
"month" or "mm" or "m" => (long)((b.Year - a.Year) * 12 + b.Month - a.Month),
"day" or "dd" or "d" or "dayofyear" or "dy" or "y" => (long)(b.Date - a.Date).TotalDays,
"week" or "wk" or "ww" => (long)((b.Date.AddDays(-(int)b.DayOfWeek) - a.Date.AddDays(-(int)a.DayOfWeek)).TotalDays / 7),
"hour" or "hh" => (long)(Trim(b, TimeSpan.TicksPerHour) - Trim(a, TimeSpan.TicksPerHour)).TotalHours,
"minute" or "mi" or "n" => (long)(Trim(b, TimeSpan.TicksPerMinute) - Trim(a, TimeSpan.TicksPerMinute)).TotalMinutes,
"second" or "ss" or "s" => (long)(Trim(b, TimeSpan.TicksPerSecond) - Trim(a, TimeSpan.TicksPerSecond)).TotalSeconds,
"millisecond" or "ms" => (long)(b - a).TotalMilliseconds,
_ => throw Fail(new SqlException(155, $"'{part}' 不是可以识别的 datediff 选项。")),
};
}
private static DateTime Trim(DateTime d, long unit) => new(d.Ticks - d.Ticks % unit);
// ---------- CONVERT / CAST,以及插入、修改时按列类型转换 ----------
/// <summary>tsql_convert('varchar', 10, 值, 120):转换成 T-SQL 类型;日期按 style 格式化</summary>
private static object? Convert(string type, object? length, object? value, object? style)
{
if (value is null) return null;
var t = TSqlType.Parse(length is null ? type : $"{type}({(ToLong(length) < 0 ? "MAX" : ToLong(length).ToString(CultureInfo.InvariantCulture))})");
if (t.IsString)
{
var text = style is not null && value is string s && Values.TryParseDateTime(s, out var date)
? FormatStyle(date, (int)ToLong(style))
: Text(value);
return t.MaxLength is { } max && text.Length > max ? text[..max] : text;
}
if (t.IsInteger)
{
return value switch
{
long => value,
double d => (long)Math.Truncate(d),
string s when long.TryParse(s.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var l) => l,
_ => throw Fail(Values.ConvertError(value, t.Name)),
};
}
if (t.IsBit)
{
return value switch
{
long l => l != 0 ? 1L : 0L,
double d => d != 0 ? 1L : 0L,
string s when s.Trim().Equals("true", StringComparison.OrdinalIgnoreCase) => 1L,
string s when s.Trim().Equals("false", StringComparison.OrdinalIgnoreCase) => 0L,
string s when long.TryParse(s.Trim(), out var l) => l != 0 ? 1L : 0L,
_ => throw Fail(Values.ConvertError(value, "bit")),
};
}
if (t.IsFloat)
{
return value switch
{
long l => (double)l,
double => value,
string s when double.TryParse(s.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => d,
_ => throw Fail(Values.ConvertError(value, t.Name)),
};
}
if (t.IsDateTime || t.IsDate)
{
if (value is not string s || !Values.TryParseDateTime(s, out var d)) throw Fail(SqlException.DateConvertFailed());
return t.IsDate ? Values.FormatDate(d) : Values.FormatDateTime(d);
}
return value;
}
/// <summary>CONVERT 的日期样式(教材用 120:yyyy-mm-dd hh:mi:ss)</summary>
private static string FormatStyle(DateTime d, int style) => style switch
{
20 or 120 => d.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
21 or 25 or 121 => d.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture),
23 => d.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
101 => d.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture),
102 => d.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture),
103 => d.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture),
108 or 8 => d.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
111 => d.ToString("yyyy/MM/dd", CultureInfo.InvariantCulture),
112 => d.ToString("yyyyMMdd", CultureInfo.InvariantCulture),
126 => d.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture),
_ => d.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
};
}
+243
View File
@@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite;
using Microsoft.SqlServer.TransactSql.ScriptDom;
namespace Sim.Data.Engine;
/// <summary>结果集的一列:名字、T-SQL 类型(表达式列为 null),以及来自哪张表的哪一列(SqlCommandBuilder 生成语句时要用)</summary>
internal sealed record ResultColumn(string Name, TSqlType? Type, string? BaseTable, string? BaseColumn, bool IsKey, bool IsIdentity, bool AllowNull)
{
public Type ClrType => Type?.ClrType is { } t && t != typeof(object) ? t : typeof(object);
}
/// <summary>一个结果集(读完放在内存里)</summary>
internal sealed record QueryResult(IReadOnlyList<ResultColumn> Columns, List<object[]> Rows);
/// <summary>一批语句的执行结果:各个结果集和受影响的行数(只有增删改时才算,否则为 -1)</summary>
internal sealed record BatchResult(List<QueryResult> Results, int RecordsAffected);
/// <summary>
/// 一个打开的数据库连接:当前库(USE 可以切换)、事务,执行一批 T-SQL。
/// SqlConnection 和数据库页面的查询窗口都用它。
/// </summary>
internal sealed class Session : IDisposable
{
private SqliteTransaction? _transaction;
private long _deadline = long.MaxValue;
public Session(DatabaseStore store, string database, TimeSpan? timeLimit = null)
{
Store = store;
TimeLimit = timeLimit;
Connection = Open(database);
Database = store.Resolve(database)!;
}
/// <summary>每批语句最长执行时间(数据库页面的查询窗口用;学生程序里由沙箱的 CPU 时间限制兜底)</summary>
public TimeSpan? TimeLimit { get; }
private SqliteConnection Open(string database)
{
var connection = Store.Open(database);
if (TimeLimit != null)
{
// 每执行一千条虚拟机指令检查一次,超时就中断(SQLite 报 interrupted)
SQLitePCL.raw.sqlite3_progress_handler(connection.Handle, 1000, _ => Environment.TickCount64 > _deadline ? 1 : 0, null);
}
return connection;
}
public DatabaseStore Store { get; }
public SqliteConnection Connection { get; private set; }
public string Database { get; private set; }
public bool InTransaction => _transaction != null;
public TableInfo? Lookup(string table) => DatabaseStore.GetTable(Connection, table);
public void ChangeDatabase(string name)
{
if (Store.Resolve(name) == null) throw SqlException.DatabaseNotFound(name);
if (_transaction != null) throw new SqlException(226, "在多语句事务内不允许使用 USE 语句。");
var next = Open(name);
Connection.Dispose();
Connection = next;
Database = Store.Resolve(name)!;
}
public void BeginTransaction()
{
if (_transaction != null) throw new InvalidOperationException("SqlConnection 不支持并行事务。");
_transaction = Connection.BeginTransaction();
}
public void Commit()
{
_transaction?.Commit();
_transaction = null;
}
public void Rollback()
{
_transaction?.Rollback();
_transaction = null;
}
/// <summary>执行一批语句;parameter 按名字(带不带 @ 都行)取参数值</summary>
public BatchResult Execute(string sql, Func<string, (bool Found, object? Value)> parameter)
{
var statements = Translator.Parse(sql);
if (TimeLimit is { } limit) _deadline = Environment.TickCount64 + (long)limit.TotalMilliseconds;
var results = new List<QueryResult>();
var affected = -1;
foreach (var parsed in statements)
{
var statement = Translate(parsed);
switch (statement)
{
case CreateDatabase create:
Store.Create(create.Name);
break;
case DropDatabases drop:
foreach (var name in drop.Names)
{
if (string.Equals(Store.Resolve(name), Database, StringComparison.OrdinalIgnoreCase)) throw SqlException.DatabaseInUse(name);
Store.Drop(name);
}
break;
case UseDatabase use:
ChangeDatabase(use.Name);
break;
case SqliteStatement run:
var (result, changes) = Run(run, parameter);
if (result != null) results.Add(result);
else if (changes >= 0) affected = Math.Max(affected, 0) + changes;
break;
}
}
return new BatchResult(results, affected);
}
private Statement Translate(TSqlStatement statement)
{
try
{
return Translator.Translate(statement, Lookup);
}
catch (SqliteException ex)
{
throw Map(ex);
}
}
private (QueryResult? Result, int Changes) Run(SqliteStatement statement, Func<string, (bool Found, object? Value)> parameter)
{
using var command = Connection.CreateCommand();
command.Transaction = _transaction;
command.CommandText = statement.Sql;
foreach (var name in statement.Parameters)
{
var (found, value) = parameter(name);
if (!found) throw SqlException.UndeclaredVariable(name);
command.Parameters.AddWithValue(name, Values.ToStorage(value) ?? DBNull.Value);
}
// 建表等语句和元数据一起提交;已经在学生的事务里时就用那个事务
var own = _transaction == null && statement.AfterExecute != null ? Connection.BeginTransaction() : null;
if (own != null) command.Transaction = own;
try
{
QueryResult? result = null;
int changes;
if (statement.IsQuery)
{
result = Read(command);
changes = -1;
}
else
{
changes = command.ExecuteNonQuery();
statement.AfterExecute?.Invoke(Connection);
if (statement.AfterExecute != null) changes = -1;
}
own?.Commit();
return (result, changes);
}
catch (SqliteException ex)
{
own?.Rollback();
throw Map(ex);
}
finally
{
own?.Dispose();
}
}
private QueryResult Read(SqliteCommand command)
{
using var reader = command.ExecuteReader();
var schema = reader.GetSchemaTable();
var tables = new Dictionary<string, TableInfo?>(StringComparer.OrdinalIgnoreCase);
var columns = new List<ResultColumn>();
for (var i = 0; i < reader.FieldCount; i++)
{
var row = schema.Rows[i];
var baseTable = row["BaseTableName"] as string;
var baseColumn = row["BaseColumnName"] as string;
ColumnInfo? info = null;
if (!string.IsNullOrEmpty(baseTable) && !string.IsNullOrEmpty(baseColumn))
{
if (!tables.TryGetValue(baseTable, out var table)) tables[baseTable] = table = Lookup(baseTable);
info = table?.Find(baseColumn);
}
columns.Add(new ResultColumn(reader.GetName(i), info?.Type, info != null ? baseTable : null, info?.Name,
info?.PrimaryKey ?? false, info?.Identity ?? false, info?.Nullable ?? true));
}
var rows = new List<object[]>();
while (reader.Read())
{
var values = new object[reader.FieldCount];
reader.GetValues(values);
for (var i = 0; i < values.Length; i++) values[i] = Values.FromStorage(values[i], columns[i].Type);
rows.Add(values);
}
return new QueryResult(columns, rows);
}
/// <summary>SQLite 的报错转成 SQL Server 的错误号和中文信息</summary>
private SqlException Map(SqliteException ex)
{
if (Functions.TakePending() is { } pending) return pending;
var message = ex.Message;
Match m;
if ((m = Regex.Match(message, @"no such table: (?:main\.)?(.+?)'?$")).Success) return SqlException.InvalidObject(m.Groups[1].Value);
// SQLite 对写错的列名会附带"是不是想写字符串"的提示,只取列名本身
if ((m = Regex.Match(message, @"no such column: ""?([^""\s']+)")).Success) return SqlException.InvalidColumn(m.Groups[1].Value);
if ((m = Regex.Match(message, @"ambiguous column name: (.+?)'?$")).Success) return SqlException.AmbiguousColumn(m.Groups[1].Value);
if ((m = Regex.Match(message, @"UNIQUE constraint failed: (\w+)\.")).Success) return SqlException.DuplicateKey(m.Groups[1].Value);
if ((m = Regex.Match(message, @"NOT NULL constraint failed: (.+?)\.(.+?)'?$")).Success)
return SqlException.NullNotAllowed(m.Groups[2].Value, $"{Database}.dbo.{m.Groups[1].Value}");
if (message.Contains("CHECK constraint failed: trunc:")) return SqlException.Truncated();
if ((m = Regex.Match(message, @"CHECK constraint failed: bit:(.+?)'?$")).Success) return new SqlException(245, $"列 '{m.Groups[1].Value}' 是 bit 类型,只能保存 0、1 或 NULL。");
if ((m = Regex.Match(message, @"cannot store (\w+) value in (\w+) column (.+?)\.(.+?)'?$")).Success)
{
var type = Lookup(m.Groups[3].Value)?.Find(m.Groups[4].Value)?.Type.Name ?? m.Groups[2].Value.ToLowerInvariant();
return new SqlException(245, $"将数据插入列 '{m.Groups[4].Value}' 时,无法把值转换成数据类型 {type}。");
}
if (message.Contains("database or disk is full")) return new SqlException(1105, "数据库已满:每个数据库最多约 50 MB。");
if (message.Contains("misuse of aggregate")) return new SqlException(147, "聚合不应出现在 WHERE 子句中,除非该聚合位于 HAVING 子句或选择列表所包含的子查询中。");
if (message.Contains("interrupted")) return new SqlException(-2, $"执行超时:查询超过 {TimeLimit?.TotalSeconds ?? 0} 秒,已停止。检查是否缺少 WHERE 条件或连接条件。");
if (message.Contains("database is locked")) return new SqlException(1222, "已超过了锁请求超时时段:数据库正被其他程序修改,请稍后再试。");
return new SqlException(50000, "执行出错:" + message.Replace("SQLite Error ", "").Trim());
}
public void Dispose()
{
_transaction?.Dispose();
Connection.Dispose();
}
}
+83
View File
@@ -0,0 +1,83 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Sim.Data.Engine;
/// <summary>
/// 一列的 T-SQL 数据类型,如 varchar(50)、int、datetime。
/// SQLite 表用 STRICT 模式保存(类型见 <see cref="StorageType"/>),原始的 T-SQL 类型记在元数据表里,
/// 读取时按它把值还原成 SQL Server 返回的 .NET 类型。
/// </summary>
internal sealed record TSqlType(string Name, int? Length = null, bool IsMax = false, int? Precision = null, int? Scale = null)
{
public static readonly TSqlType Unknown = new("sql_variant");
/// <summary>如 varchar(50)、varchar(MAX)、decimal(10, 2)</summary>
public override string ToString() => Name switch
{
_ when IsMax => $"{Name}(MAX)",
_ when Length != null => $"{Name}({Length})",
"decimal" or "numeric" when Precision != null => Scale != null ? $"{Name}({Precision}, {Scale})" : $"{Name}({Precision})",
_ => Name,
};
public static TSqlType Parse(string text)
{
var m = Regex.Match(text.Trim(), @"^(\w+)\s*(?:\(\s*(MAX|\d+)\s*(?:,\s*(\d+)\s*)?\))?$", RegexOptions.IgnoreCase);
if (!m.Success) return Unknown;
var name = m.Groups[1].Value.ToLowerInvariant();
if (!m.Groups[2].Success) return new TSqlType(name);
if (m.Groups[2].Value.Equals("MAX", StringComparison.OrdinalIgnoreCase)) return new TSqlType(name, IsMax: true);
var first = int.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture);
if (name is "decimal" or "numeric")
return new TSqlType(name, Precision: first, Scale: m.Groups[3].Success ? int.Parse(m.Groups[3].Value, CultureInfo.InvariantCulture) : null);
return new TSqlType(name, Length: first);
}
public bool IsString => Name is "char" or "varchar" or "nchar" or "nvarchar" or "text" or "ntext";
public bool IsDateTime => Name is "datetime" or "datetime2" or "smalldatetime";
public bool IsDate => Name == "date";
public bool IsInteger => Name is "int" or "bigint" or "smallint" or "tinyint";
public bool IsBit => Name == "bit";
public bool IsFloat => Name is "float" or "real" or "decimal" or "numeric" or "money" or "smallmoney";
public bool IsBinary => Name is "binary" or "varbinary" or "image";
/// <summary>SQLite STRICT 表的列类型</summary>
public string StorageType => this switch
{
{ IsInteger: true } or { IsBit: true } => "INTEGER",
{ IsFloat: true } => "REAL",
{ IsBinary: true } => "BLOB",
{ IsString: true } or { IsDateTime: true } or { IsDate: true } or { Name: "time" or "uniqueidentifier" } => "TEXT",
_ => "ANY",
};
/// <summary>字符串列的长度上限(超出时和 SQL Server 一样报"将截断字符串")</summary>
public int? MaxLength => IsString && !IsMax && Name is not ("text" or "ntext") ? Length ?? 1 : null;
/// <summary>SQL Server 返回这一列时的 .NET 类型(DataTable 的列类型、DataGridView 的显示方式由它决定)</summary>
public Type ClrType => Name switch
{
"int" => typeof(int),
"bigint" => typeof(long),
"smallint" => typeof(short),
"tinyint" => typeof(byte),
"bit" => typeof(bool),
"float" => typeof(double),
"real" => typeof(float),
"decimal" or "numeric" or "money" or "smallmoney" => typeof(decimal),
"date" or "datetime" or "datetime2" or "smalldatetime" => typeof(DateTime),
"time" => typeof(TimeSpan),
"uniqueidentifier" => typeof(Guid),
"binary" or "varbinary" or "image" => typeof(byte[]),
"char" or "varchar" or "nchar" or "nvarchar" or "text" or "ntext" => typeof(string),
_ => typeof(object),
};
/// <summary>平台支持的 T-SQL 类型(建表时检查)</summary>
public static bool IsSupported(string name) => name is
"int" or "bigint" or "smallint" or "tinyint" or "bit" or "float" or "real" or "decimal" or "numeric" or "money" or "smallmoney"
or "date" or "datetime" or "datetime2" or "smalldatetime" or "time" or "uniqueidentifier"
or "binary" or "varbinary" or "image" or "char" or "varchar" or "nchar" or "nvarchar" or "text" or "ntext";
}
+916
View File
@@ -0,0 +1,916 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.TransactSql.ScriptDom;
namespace Sim.Data.Engine;
/// <summary>翻译好的一条语句</summary>
internal abstract record Statement;
/// <summary>在 SQLite 上执行的语句;IsQuery 表示有结果集;AfterExecute 在同一事务里更新元数据</summary>
internal sealed record SqliteStatement(string Sql, bool IsQuery, IReadOnlyList<string> Parameters, Action<Microsoft.Data.Sqlite.SqliteConnection>? AfterExecute = null) : Statement;
internal sealed record CreateDatabase(string Name) : Statement;
internal sealed record DropDatabases(IReadOnlyList<string> Names) : Statement;
internal sealed record UseDatabase(string Name) : Statement;
/// <summary>SET NOCOUNT ON 之类不影响结果的语句</summary>
internal sealed record IgnoredStatement : Statement;
/// <summary>
/// 把 T-SQL(SQL Server 2008 语法)转写成 SQLite 语句。只转写认识的语法节点,其余一律报"平台暂不支持",
/// 生成的 SQL 里只会出现这里写出的关键字和 tsql_ 函数,学生的 SQL 无法借此执行 SQLite 特有的命令。
/// 一批语句先整体解析(语法错误和 SQL Server 一样在执行前报出),执行时再逐条转写(后面的语句能用到前面刚建的表)。
/// </summary>
internal sealed class Translator
{
private readonly Func<string, TableInfo?> _lookup;
private readonly StringBuilder _sql = new();
private readonly List<string> _parameters = new();
private Translator(Func<string, TableInfo?> lookup) => _lookup = lookup;
/// <summary>解析整批语句;有语法错误时抛出 SqlException 102</summary>
public static IReadOnlyList<TSqlStatement> Parse(string sql)
{
var parser = new TSql100Parser(initialQuotedIdentifiers: true);
var fragment = parser.Parse(new StringReader(sql), out var errors);
if (errors.Count > 0) throw SyntaxError(sql, errors[0]);
return ((TSqlScript)fragment).Batches.SelectMany(b => b.Statements).ToList();
}
/// <summary>转写一条语句;lookup 按表名取表结构(INSERT 跳过自增列、按列类型转换值时要用)</summary>
public static Statement Translate(TSqlStatement statement, Func<string, TableInfo?> lookup) => new Translator(lookup).Statement(statement);
private static SqlException SyntaxError(string sql, ParseError error)
{
// ParseError 的信息是英文,这里取出错位置的词,按 SQL Server 中文版的格式报
var rest = error.Offset < sql.Length ? sql[error.Offset..] : "";
var token = new string(rest.TakeWhile(c => !char.IsWhiteSpace(c) && c is not (',' or ')' or '(' or ';')).ToArray());
if (token.Length == 0 && rest.Length > 0) token = rest[..1];
if (token.Length == 0) return new SqlException(102, "语句不完整,结尾附近有语法错误。", error.Line);
if (token.StartsWith('\'')) return new SqlException(105, $"字符串 '{token.Trim('\'')}' 后的引号不完整。", error.Line);
return SqlException.Syntax(token, error.Line);
}
// ---------- 语句 ----------
private Statement Statement(TSqlStatement statement)
{
switch (statement)
{
case SelectStatement select:
if (select.Into != null) throw SqlException.NotSupported("SELECT ... INTO");
if (select.WithCtesAndXmlNamespaces != null) throw SqlException.NotSupported("WITH(公用表表达式)");
Query(select.QueryExpression);
return Done(isQuery: true);
case InsertStatement insert:
Insert(insert.InsertSpecification);
return Done(isQuery: false);
case UpdateStatement update:
Update(update.UpdateSpecification);
return Done(isQuery: false);
case DeleteStatement delete:
Delete(delete.DeleteSpecification);
return Done(isQuery: false);
case CreateTableStatement create:
return CreateTable(create);
case DropTableStatement drop:
return DropTable(drop);
case AlterTableAddTableElementStatement alter:
return AddColumns(alter);
case TruncateTableStatement truncate:
{
var table = RequireTable(truncate.TableName);
_sql.Append("DELETE FROM ").Append(Values.QuoteName(table.Name));
return Done(isQuery: false);
}
case CreateDatabaseStatement create:
return new CreateDatabase(create.DatabaseName.Value);
case DropDatabaseStatement drop:
return new DropDatabases(drop.Databases.Select(d => d.Value).ToList());
case UseStatement use:
return new UseDatabase(use.DatabaseName.Value);
case PredicateSetStatement:
return new IgnoredStatement();
default:
throw SqlException.NotSupported(StatementName(statement));
}
}
private SqliteStatement Done(bool isQuery, Action<Microsoft.Data.Sqlite.SqliteConnection>? after = null) =>
new(_sql.ToString(), isQuery, _parameters.Distinct(StringComparer.OrdinalIgnoreCase).ToList(), after);
private static string StatementName(TSqlStatement statement) => statement.GetType().Name.Replace("Statement", "") switch
{
"CreateProcedure" or "AlterProcedure" => "存储过程",
"CreateView" or "AlterView" => "视图",
"CreateTrigger" => "触发器",
"CreateIndex" => "CREATE INDEX",
"DeclareVariable" => "DECLARE 变量",
"If" => "IF 语句",
"While" => "WHILE 循环",
"BeginEndBlock" => "BEGIN ... END",
"Execute" => "EXEC",
var name => name + " 语句",
};
// ---------- 查询 ----------
private void Query(QueryExpression query)
{
switch (query)
{
case QuerySpecification spec:
QuerySpec(spec);
break;
case BinaryQueryExpression binary:
Query(binary.FirstQueryExpression);
_sql.Append(binary.BinaryQueryExpressionType switch
{
BinaryQueryExpressionType.Union => binary.All ? " UNION ALL " : " UNION ",
BinaryQueryExpressionType.Except => " EXCEPT ",
_ => " INTERSECT ",
});
Query(binary.SecondQueryExpression);
OrderBy(binary.OrderByClause);
break;
case QueryParenthesisExpression paren:
_sql.Append("SELECT * FROM (");
Query(paren.QueryExpression);
_sql.Append(')');
OrderBy(paren.OrderByClause);
break;
default:
throw SqlException.NotSupported(query.GetType().Name);
}
}
private void QuerySpec(QuerySpecification spec)
{
_sql.Append("SELECT ");
if (spec.UniqueRowFilter == UniqueRowFilter.Distinct) _sql.Append("DISTINCT ");
for (var i = 0; i < spec.SelectElements.Count; i++)
{
if (i > 0) _sql.Append(", ");
SelectElement(spec.SelectElements[i]);
}
if (spec.FromClause != null)
{
_sql.Append(" FROM ");
for (var i = 0; i < spec.FromClause.TableReferences.Count; i++)
{
if (i > 0) _sql.Append(", ");
TableReference(spec.FromClause.TableReferences[i]);
}
}
Where(spec.WhereClause);
if (spec.GroupByClause != null)
{
_sql.Append(" GROUP BY ");
for (var i = 0; i < spec.GroupByClause.GroupingSpecifications.Count; i++)
{
if (i > 0) _sql.Append(", ");
if (spec.GroupByClause.GroupingSpecifications[i] is not ExpressionGroupingSpecification group) throw SqlException.NotSupported("这种 GROUP BY 写法");
Expression(group.Expression);
}
}
if (spec.HavingClause != null)
{
_sql.Append(" HAVING ");
Boolean(spec.HavingClause.SearchCondition);
}
OrderBy(spec.OrderByClause);
Top(spec.TopRowFilter);
}
private void SelectElement(SelectElement element)
{
switch (element)
{
case SelectStarExpression star:
if (star.Qualifier != null) _sql.Append(Values.QuoteName(star.Qualifier.Identifiers[^1].Value)).Append('.');
_sql.Append('*');
break;
case SelectScalarExpression scalar:
Expression(scalar.Expression);
if (scalar.ColumnName != null)
{
// as '编号' 和 as 编号 都按列名处理
_sql.Append(" AS ").Append(Values.QuoteName(scalar.ColumnName.Value));
}
else if (scalar.Expression is not ColumnReferenceExpression)
{
// 和 SQL Server 一样,没有起别名的表达式列没有列名(DataTable 里叫 Column1)
_sql.Append(" AS \"\"");
}
break;
default:
throw SqlException.NotSupported("SELECT @变量 = ...");
}
}
private void TableReference(TableReference reference)
{
switch (reference)
{
case NamedTableReference named:
var table = RequireTable(named.SchemaObject);
_sql.Append(Values.QuoteName(table.Name));
if (named.Alias != null) _sql.Append(" AS ").Append(Values.QuoteName(named.Alias.Value));
break;
case QualifiedJoin join:
TableReference(join.FirstTableReference);
_sql.Append(join.QualifiedJoinType switch
{
QualifiedJoinType.Inner => " INNER JOIN ",
QualifiedJoinType.LeftOuter => " LEFT JOIN ",
QualifiedJoinType.RightOuter => " RIGHT JOIN ",
_ => " FULL JOIN ",
});
TableReference(join.SecondTableReference);
_sql.Append(" ON ");
Boolean(join.SearchCondition);
break;
case UnqualifiedJoin cross when cross.UnqualifiedJoinType == UnqualifiedJoinType.CrossJoin:
TableReference(cross.FirstTableReference);
_sql.Append(" CROSS JOIN ");
TableReference(cross.SecondTableReference);
break;
case QueryDerivedTable derived:
_sql.Append('(');
Query(derived.QueryExpression);
_sql.Append(')');
if (derived.Alias != null) _sql.Append(" AS ").Append(Values.QuoteName(derived.Alias.Value));
break;
default:
throw SqlException.NotSupported("这种 FROM 写法");
}
}
private void Where(WhereClause? where)
{
if (where == null) return;
if (where.Cursor != null) throw SqlException.NotSupported("游标");
_sql.Append(" WHERE ");
Boolean(where.SearchCondition);
}
private void OrderBy(OrderByClause? orderBy)
{
if (orderBy == null) return;
_sql.Append(" ORDER BY ");
for (var i = 0; i < orderBy.OrderByElements.Count; i++)
{
if (i > 0) _sql.Append(", ");
Expression(orderBy.OrderByElements[i].Expression);
if (orderBy.OrderByElements[i].SortOrder == SortOrder.Descending) _sql.Append(" DESC");
}
}
private void Top(TopRowFilter? top)
{
if (top == null) return;
if (top.Percent) throw SqlException.NotSupported("TOP ... PERCENT");
if (top.WithTies) throw SqlException.NotSupported("TOP ... WITH TIES");
_sql.Append(" LIMIT ");
Expression(top.Expression);
}
// ---------- 增删改 ----------
private void Insert(InsertSpecification spec)
{
if (spec.TopRowFilter != null) throw SqlException.NotSupported("INSERT TOP");
if (spec.Target is not NamedTableReference target) throw SqlException.NotSupported("这种 INSERT 写法");
var table = RequireTable(target.SchemaObject);
// 列:写了列名就按列名;没写就是除自增列以外的全部列(和 SQL Server 一样)
List<ColumnInfo> columns;
if (spec.Columns.Count > 0)
{
columns = spec.Columns.Select(c => RequireColumn(table, c.MultiPartIdentifier.Identifiers[^1].Value)).ToList();
if (columns.FirstOrDefault(c => c.Identity) != null) throw SqlException.IdentityInsert(table.Name);
}
else
{
columns = table.Columns.Where(c => !c.Identity).ToList();
}
_sql.Append("INSERT INTO ").Append(Values.QuoteName(table.Name));
switch (spec.InsertSource)
{
case ValuesInsertSource { IsDefaultValues: true }:
_sql.Append(" DEFAULT VALUES");
break;
case ValuesInsertSource values:
_sql.Append(" (").Append(string.Join(", ", columns.Select(c => Values.QuoteName(c.Name)))).Append(") VALUES ");
for (var r = 0; r < values.RowValues.Count; r++)
{
var row = values.RowValues[r].ColumnValues;
if (row.Count != columns.Count) throw SqlException.ColumnCountMismatch();
if (r > 0) _sql.Append(", ");
_sql.Append('(');
for (var i = 0; i < row.Count; i++)
{
if (i > 0) _sql.Append(", ");
ValueFor(columns[i], row[i]);
}
_sql.Append(')');
}
break;
case SelectInsertSource select:
_sql.Append(" (").Append(string.Join(", ", columns.Select(c => Values.QuoteName(c.Name)))).Append(") ");
Query(select.Select);
break;
default:
throw SqlException.NotSupported("INSERT ... EXEC");
}
}
private void Update(UpdateSpecification spec)
{
if (spec.FromClause != null) throw SqlException.NotSupported("UPDATE ... FROM");
if (spec.Target is not NamedTableReference target) throw SqlException.NotSupported("这种 UPDATE 写法");
var table = RequireTable(target.SchemaObject);
_sql.Append("UPDATE ").Append(Values.QuoteName(table.Name)).Append(" SET ");
for (var i = 0; i < spec.SetClauses.Count; i++)
{
if (spec.SetClauses[i] is not AssignmentSetClause { Column: not null } set || set.AssignmentKind != AssignmentKind.Equals)
throw SqlException.NotSupported("这种 SET 写法");
var column = RequireColumn(table, set.Column.MultiPartIdentifier.Identifiers[^1].Value);
if (column.Identity) throw new SqlException(8102, $"无法更新标识列 '{column.Name}'。");
if (i > 0) _sql.Append(", ");
_sql.Append(Values.QuoteName(column.Name)).Append(" = ");
ValueFor(column, set.NewValue);
}
LimitedWhere(table, spec.WhereClause, spec.TopRowFilter);
}
private void Delete(DeleteSpecification spec)
{
if (spec.FromClause != null) throw SqlException.NotSupported("DELETE ... FROM ... JOIN");
if (spec.Target is not NamedTableReference target) throw SqlException.NotSupported("这种 DELETE 写法");
var table = RequireTable(target.SchemaObject);
_sql.Append("DELETE FROM ").Append(Values.QuoteName(table.Name));
LimitedWhere(table, spec.WhereClause, spec.TopRowFilter);
}
/// <summary>UPDATE TOP (n) / DELETE TOP (n):SQLite 不支持,改成按 rowid 限定前 n 行</summary>
private void LimitedWhere(TableInfo table, WhereClause? where, TopRowFilter? top)
{
if (top == null)
{
Where(where);
return;
}
_sql.Append(" WHERE rowid IN (SELECT rowid FROM ").Append(Values.QuoteName(table.Name));
Where(where);
Top(top);
_sql.Append(')');
}
/// <summary>插入、修改的值按目标列的类型转换(日期统一格式、字符串转数字失败时报和 SQL Server 一样的错)</summary>
private void ValueFor(ColumnInfo column, ScalarExpression value)
{
if (value is NullLiteral || value is DefaultLiteral)
{
_sql.Append(value is NullLiteral ? "NULL" : throw SqlException.NotSupported("VALUES 里的 DEFAULT"));
return;
}
var type = column.Type;
if (type.IsString || type.IsBinary || type.StorageType == "ANY")
{
Expression(value);
return;
}
_sql.Append("tsql_convert(").Append(Values.Quote(type.Name)).Append(", NULL, ");
Expression(value);
_sql.Append(", NULL)");
}
// ---------- 建表、删表、加列 ----------
private Statement CreateTable(CreateTableStatement create)
{
var name = TableName(create.SchemaObjectName);
if (DatabaseStore.IsHidden(name)) throw new SqlException(102, $"表名 '{name}' 是平台保留的名字,请换一个。");
if (_lookup(name) != null) throw SqlException.ObjectExists(name);
var definition = create.Definition;
var primaryKey = definition.TableConstraints.OfType<UniqueConstraintDefinition>().FirstOrDefault(c => c.IsPrimaryKey);
var keyColumns = primaryKey?.Columns.Select(c => c.Column.MultiPartIdentifier.Identifiers[^1].Value).ToList() ?? new List<string>();
var columns = definition.ColumnDefinitions.Select(c => Column(c, keyColumns)).ToList();
if (columns.Count(c => c.Identity) > 1) throw new SqlException(2744, $"为表 '{name}' 指定了多个标识列。只允许为每个表指定一个标识列。");
foreach (var key in keyColumns)
if (columns.All(c => !string.Equals(c.Name, key, StringComparison.OrdinalIgnoreCase))) throw SqlException.InvalidColumn(key);
_sql.Append("CREATE TABLE ").Append(Values.QuoteName(name)).Append(" (");
_sql.Append(string.Join(", ", columns.Select(c => ColumnSql(c, single: keyColumns.Count <= 1 && c.PrimaryKey))));
if (keyColumns.Count > 1) _sql.Append(", PRIMARY KEY (").Append(string.Join(", ", keyColumns.Select(Values.QuoteName))).Append(')');
_sql.Append(") STRICT");
return Done(isQuery: false, after: c => DatabaseStore.SaveColumns(c, name, columns));
}
private Statement AddColumns(AlterTableAddTableElementStatement alter)
{
var table = RequireTable(alter.SchemaObjectName);
if (alter.Definition.TableConstraints.Count > 0) throw SqlException.NotSupported("ALTER TABLE 添加约束");
var columns = alter.Definition.ColumnDefinitions.Select(c => Column(c, new List<string>())).ToList();
foreach (var column in columns)
{
if (table.Find(column.Name) != null) throw new SqlException(2705, $"各表中的列名必须唯一。在表 '{table.Name}' 中多次指定了列名 '{column.Name}'。");
if (column.Identity || column.PrimaryKey) throw SqlException.NotSupported("ALTER TABLE 添加主键或标识列");
if (_sql.Length > 0) _sql.Append("; ");
_sql.Append("ALTER TABLE ").Append(Values.QuoteName(table.Name)).Append(" ADD COLUMN ").Append(ColumnSql(column, single: false));
}
return Done(isQuery: false, after: c => DatabaseStore.SaveColumns(c, table.Name, columns, table.Columns.Count));
}
private Statement DropTable(DropTableStatement drop)
{
var tables = drop.Objects.Select(o => _lookup(TableName(o)) ?? throw SqlException.CannotDropTable(TableName(o))).ToList();
_sql.Append(string.Join("; ", tables.Select(t => "DROP TABLE " + Values.QuoteName(t.Name))));
return Done(isQuery: false, after: c =>
{
foreach (var table in tables) DatabaseStore.DeleteColumns(c, table.Name);
});
}
private ColumnInfo Column(ColumnDefinition definition, List<string> keyColumns)
{
var name = definition.ColumnIdentifier.Value;
if (definition.DataType is not SqlDataTypeReference dataType) throw new SqlException(2715, $"第 1 个列、参数或变量: 找不到数据类型 {definition.DataType?.Name?.BaseIdentifier?.Value}。");
var type = TypeOf(dataType);
var identity = definition.IdentityOptions != null;
if (identity && !type.IsInteger) throw new SqlException(2749, $"标识列 '{name}' 的数据类型必须是 int、bigint、smallint 或 tinyint。");
var primaryKey = keyColumns.Any(k => string.Equals(k, name, StringComparison.OrdinalIgnoreCase))
|| definition.Constraints.OfType<UniqueConstraintDefinition>().Any(c => c.IsPrimaryKey);
var nullable = !identity && !primaryKey && definition.Constraints.OfType<NullableConstraintDefinition>().All(c => c.Nullable);
if (definition.Constraints.Any(c => c is not (NullableConstraintDefinition or UniqueConstraintDefinition { IsPrimaryKey: true })))
throw SqlException.NotSupported("PRIMARY KEY、NULL / NOT NULL 以外的列约束");
string? defaultText = null;
if (definition.DefaultConstraint != null) defaultText = DefaultSql(definition.DefaultConstraint.Expression);
if (identity && definition.IdentityOptions!.IdentitySeed is IntegerLiteral seed && seed.Value != "1")
throw SqlException.NotSupported("从 1 以外的数开始的 IDENTITY");
return new ColumnInfo(name, type, nullable, identity, primaryKey, defaultText);
}
private static TSqlType TypeOf(SqlDataTypeReference dataType)
{
var name = dataType.SqlDataTypeOption.ToString().ToLowerInvariant();
if (!TSqlType.IsSupported(name)) throw SqlException.NotSupported($"数据类型 {name}");
var parameters = dataType.Parameters;
if (parameters.Count == 0) return new TSqlType(name);
if (parameters[0] is MaxLiteral) return new TSqlType(name, IsMax: true);
var first = int.Parse(parameters[0].Value, CultureInfo.InvariantCulture);
if (name is "decimal" or "numeric")
return new TSqlType(name, Precision: first, Scale: parameters.Count > 1 ? int.Parse(parameters[1].Value, CultureInfo.InvariantCulture) : null);
return new TSqlType(name, Length: first);
}
/// <summary>SQLite 的列定义:STRICT 类型 + 约束;字符串长度和 bit 取值用 CHECK 实现(约束名用来认出是哪种错误)</summary>
private static string ColumnSql(ColumnInfo column, bool single)
{
var sql = new StringBuilder(Values.QuoteName(column.Name));
if (column.Identity && single)
{
sql.Append(" INTEGER PRIMARY KEY AUTOINCREMENT");
return sql.ToString();
}
if (column.Identity) throw SqlException.NotSupported("标识列不是唯一主键的表");
sql.Append(' ').Append(column.Type.StorageType);
if (single && column.PrimaryKey) sql.Append(" PRIMARY KEY");
if (!column.Nullable) sql.Append(" NOT NULL");
if (column.Type.StorageType == "TEXT") sql.Append(" COLLATE NOCASE");
if (column.Default != null) sql.Append(" DEFAULT ").Append(column.Default);
if (column.Type.MaxLength is { } max)
sql.Append(" CONSTRAINT ").Append(Values.QuoteName("trunc:" + column.Name)).Append(" CHECK (length(").Append(Values.QuoteName(column.Name)).Append(") <= ").Append(max).Append(')');
if (column.Type.IsBit)
sql.Append(" CONSTRAINT ").Append(Values.QuoteName("bit:" + column.Name)).Append(" CHECK (").Append(Values.QuoteName(column.Name)).Append(" IN (0, 1))");
return sql.ToString();
}
/// <summary>DEFAULT 只支持常量和 getdate()</summary>
private static string DefaultSql(ScalarExpression expression) => Unparen(expression) switch
{
IntegerLiteral i => i.Value,
NumericLiteral n => n.Value,
RealLiteral r => r.Value,
StringLiteral s => Values.Quote(s.Value),
NullLiteral => "NULL",
UnaryExpression { UnaryExpressionType: UnaryExpressionType.Negative, Expression: IntegerLiteral or NumericLiteral } u => "-" + ((Literal)u.Expression).Value,
FunctionCall f when f.FunctionName.Value.Equals("getdate", StringComparison.OrdinalIgnoreCase) => "(strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'))",
_ => throw SqlException.NotSupported("这种 DEFAULT 默认值"),
};
private static ScalarExpression Unparen(ScalarExpression expression) =>
expression is ParenthesisExpression p ? Unparen(p.Expression) : expression;
// ---------- 表名、列名 ----------
/// <summary>dbo.表名、库名.dbo.表名 都取最后一段(跨库访问不支持)</summary>
private static string TableName(SchemaObjectName name)
{
if (name.ServerIdentifier != null) throw SqlException.NotSupported("访问其他服务器上的表");
if (name.SchemaIdentifier != null && !name.SchemaIdentifier.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase))
throw SqlException.NotSupported($"架构 {name.SchemaIdentifier.Value}(只有 dbo)");
return name.BaseIdentifier.Value;
}
private TableInfo RequireTable(SchemaObjectName name)
{
if (name.DatabaseIdentifier != null) throw SqlException.NotSupported("在一条语句里访问其他数据库的表,请先用 USE 切换数据库");
var table = TableName(name);
return _lookup(table) ?? throw SqlException.InvalidObject(table);
}
private static ColumnInfo RequireColumn(TableInfo table, string column) => table.Find(column) ?? throw SqlException.InvalidColumn(column);
// ---------- 表达式 ----------
private void Expression(ScalarExpression expression)
{
switch (expression)
{
case ColumnReferenceExpression column:
ColumnReference(column);
break;
case IntegerLiteral or NumericLiteral or RealLiteral or MoneyLiteral:
_sql.Append(((Literal)expression).Value.TrimStart('$', '¥'));
break;
case StringLiteral s:
_sql.Append(Values.Quote(s.Value));
break;
case NullLiteral:
_sql.Append("NULL");
break;
case BinaryLiteral b:
_sql.Append("X'").Append(b.Value[2..]).Append('\'');
break;
case VariableReference variable:
_parameters.Add(variable.Name);
_sql.Append(variable.Name);
break;
case GlobalVariableExpression global when global.Name.Equals("@@IDENTITY", StringComparison.OrdinalIgnoreCase):
_sql.Append("last_insert_rowid()");
break;
case ParenthesisExpression paren:
_sql.Append('(');
Expression(paren.Expression);
_sql.Append(')');
break;
case UnaryExpression unary:
_sql.Append(unary.UnaryExpressionType switch
{
UnaryExpressionType.Negative => "-",
UnaryExpressionType.Positive => "+",
_ => "~",
});
Expression(unary.Expression);
break;
case BinaryExpression binary:
Binary(binary);
break;
case FunctionCall call:
Function(call);
break;
case LeftFunctionCall left:
Call("tsql_left", left.Parameters);
break;
case RightFunctionCall right:
Call("tsql_right", right.Parameters);
break;
case CastCall cast:
Conversion(cast.DataType, cast.Parameter, null);
break;
case ConvertCall convert:
Conversion(convert.DataType, convert.Parameter, convert.Style);
break;
case CoalesceExpression coalesce:
Call("coalesce", coalesce.Expressions);
break;
case NullIfExpression nullIf:
Call("nullif", new[] { nullIf.FirstExpression, nullIf.SecondExpression });
break;
case SearchedCaseExpression searched:
_sql.Append("CASE");
foreach (var when in searched.WhenClauses)
{
_sql.Append(" WHEN ");
Boolean(when.WhenExpression);
_sql.Append(" THEN ");
Expression(when.ThenExpression);
}
Else(searched.ElseExpression);
break;
case SimpleCaseExpression simple:
_sql.Append("CASE ");
Expression(simple.InputExpression);
foreach (var when in simple.WhenClauses)
{
_sql.Append(" WHEN ");
Expression(when.WhenExpression);
_sql.Append(" THEN ");
Expression(when.ThenExpression);
}
Else(simple.ElseExpression);
break;
case ScalarSubquery subquery:
_sql.Append('(');
Query(subquery.QueryExpression);
_sql.Append(')');
break;
default:
throw SqlException.NotSupported(expression.GetType().Name.Replace("Expression", "") + " 表达式");
}
}
private void Else(ScalarExpression? otherwise)
{
if (otherwise != null)
{
_sql.Append(" ELSE ");
Expression(otherwise);
}
_sql.Append(" END");
}
private void ColumnReference(ColumnReferenceExpression column)
{
if (column.ColumnType == ColumnType.Wildcard)
{
_sql.Append('*');
return;
}
if (column.ColumnType != ColumnType.Regular) throw SqlException.NotSupported(column.ColumnType.ToString());
var parts = column.MultiPartIdentifier.Identifiers;
// 表名.列名 保留表名(或别名);库名.dbo.表名.列名 只取最后两段
var take = Math.Min(parts.Count, 2);
_sql.Append(string.Join(".", parts.Skip(parts.Count - take).Select(p => Values.QuoteName(p.Value))));
}
private void Binary(BinaryExpression binary)
{
switch (binary.BinaryExpressionType)
{
// + 要在运行时看两边的类型:字符串连接还是数字相加
case BinaryExpressionType.Add:
Call("tsql_add", new[] { binary.FirstExpression, binary.SecondExpression });
return;
case BinaryExpressionType.Divide:
Call("tsql_div", new[] { binary.FirstExpression, binary.SecondExpression });
return;
case BinaryExpressionType.Modulo:
Call("tsql_mod", new[] { binary.FirstExpression, binary.SecondExpression });
return;
}
_sql.Append('(');
Expression(binary.FirstExpression);
_sql.Append(binary.BinaryExpressionType switch
{
BinaryExpressionType.Subtract => " - ",
BinaryExpressionType.Multiply => " * ",
BinaryExpressionType.BitwiseAnd => " & ",
BinaryExpressionType.BitwiseOr => " | ",
_ => throw SqlException.NotSupported("^ 运算"),
});
Expression(binary.SecondExpression);
_sql.Append(')');
}
private void Call(string name, IList<ScalarExpression> arguments)
{
_sql.Append(name).Append('(');
for (var i = 0; i < arguments.Count; i++)
{
if (i > 0) _sql.Append(", ");
Expression(arguments[i]);
}
_sql.Append(')');
}
private void Conversion(DataTypeReference dataType, ScalarExpression value, ScalarExpression? style)
{
if (dataType is not SqlDataTypeReference sqlType) throw SqlException.NotSupported($"转换成 {dataType.Name.BaseIdentifier.Value}");
var type = TypeOf(sqlType);
_sql.Append("tsql_convert(").Append(Values.Quote(type.Name)).Append(", ")
.Append(type.IsMax ? "-1" : type.Length?.ToString(CultureInfo.InvariantCulture) ?? (type.IsString ? "30" : "NULL")).Append(", ");
Expression(value);
_sql.Append(", ");
if (style != null) Expression(style);
else _sql.Append("NULL");
_sql.Append(')');
}
private void Function(FunctionCall call)
{
if (call.CallTarget != null) throw SqlException.NotSupported("这种函数调用");
if (call.OverClause != null) throw SqlException.NotSupported("OVER(开窗函数)");
var name = call.FunctionName.Value.ToLowerInvariant();
var args = call.Parameters;
var distinct = call.UniqueRowFilter == UniqueRowFilter.Distinct ? "DISTINCT " : "";
void Expect(int min, int max)
{
if (args.Count < min || args.Count > max)
throw new SqlException(174, $"{call.FunctionName.Value} 函数要求有 {(min == max ? min.ToString(CultureInfo.InvariantCulture) : $"{min} 到 {max}")} 个参数。");
}
switch (name)
{
case "count" or "sum" or "avg" or "min" or "max":
Expect(1, 1);
_sql.Append(name).Append('(').Append(distinct);
if (args[0] is ColumnReferenceExpression { ColumnType: ColumnType.Wildcard }) _sql.Append('*');
else Expression(args[0]);
_sql.Append(')');
return;
case "getdate" or "current_timestamp" or "sysdatetime":
Expect(0, 0);
_sql.Append("tsql_getdate()");
return;
case "year" or "month" or "day":
Expect(1, 1);
_sql.Append("tsql_datepart('").Append(name).Append("', ");
Expression(args[0]);
_sql.Append(')');
return;
case "datepart" or "datename":
Expect(2, 2);
_sql.Append("tsql_datepart(").Append(Values.Quote(DatePartName(args[0]))).Append(", ");
Expression(args[1]);
_sql.Append(')');
return;
case "dateadd" or "datediff":
Expect(3, 3);
_sql.Append("tsql_").Append(name).Append('(').Append(Values.Quote(DatePartName(args[0]))).Append(", ");
Expression(args[1]);
_sql.Append(", ");
Expression(args[2]);
_sql.Append(')');
return;
case "scope_identity" or "ident_current":
_sql.Append("last_insert_rowid()");
return;
case "len":
Expect(1, 1);
Call("tsql_len", args);
return;
case "datalength":
Expect(1, 1);
Call("length", args);
return;
case "substring":
Expect(3, 3);
Call("substr", args);
return;
case "charindex":
Expect(2, 2);
Call("tsql_charindex", args);
return;
case "upper" or "lower" or "ltrim" or "rtrim" or "replace" or "abs" or "round" or "sign":
Expect(1, 3);
Call(name, name == "round" ? args.Take(2).ToList() : args);
return;
case "isnull":
Expect(2, 2);
Call("ifnull", args);
return;
case "floor" or "ceiling" or "sqrt":
Expect(1, 1);
Call("tsql_" + name, args);
return;
case "power":
Expect(2, 2);
Call("tsql_power", args);
return;
case "newid":
Expect(0, 0);
_sql.Append("tsql_newid()");
return;
case "rand":
_sql.Append("tsql_rand()");
return;
default:
throw SqlException.UnknownFunction(call.FunctionName.Value);
}
}
/// <summary>DATEADD(day, ...) 的第一个参数是关键字,解析成列名</summary>
private static string DatePartName(ScalarExpression expression) => expression switch
{
ColumnReferenceExpression c => c.MultiPartIdentifier.Identifiers[^1].Value,
StringLiteral s => s.Value,
_ => throw SqlException.NotSupported("这种 datepart 写法"),
};
// ---------- 条件 ----------
private void Boolean(BooleanExpression expression)
{
switch (expression)
{
case BooleanComparisonExpression comparison:
_sql.Append('(');
Comparand(comparison.FirstExpression);
_sql.Append(comparison.ComparisonType switch
{
BooleanComparisonType.Equals => " = ",
BooleanComparisonType.GreaterThan => " > ",
BooleanComparisonType.LessThan => " < ",
BooleanComparisonType.GreaterThanOrEqualTo or BooleanComparisonType.NotLessThan => " >= ",
BooleanComparisonType.LessThanOrEqualTo or BooleanComparisonType.NotGreaterThan => " <= ",
BooleanComparisonType.NotEqualToBrackets or BooleanComparisonType.NotEqualToExclamation => " <> ",
_ => throw SqlException.NotSupported(comparison.ComparisonType.ToString()),
});
Comparand(comparison.SecondExpression);
_sql.Append(')');
break;
case BooleanBinaryExpression binary:
_sql.Append('(');
Boolean(binary.FirstExpression);
_sql.Append(binary.BinaryExpressionType == BooleanBinaryExpressionType.And ? " AND " : " OR ");
Boolean(binary.SecondExpression);
_sql.Append(')');
break;
case BooleanNotExpression not:
_sql.Append("NOT (");
Boolean(not.Expression);
_sql.Append(')');
break;
case BooleanParenthesisExpression paren:
_sql.Append('(');
Boolean(paren.Expression);
_sql.Append(')');
break;
case BooleanIsNullExpression isNull:
_sql.Append('(');
Expression(isNull.Expression);
_sql.Append(isNull.IsNot ? " IS NOT NULL)" : " IS NULL)");
break;
case LikePredicate like:
_sql.Append('(');
Expression(like.FirstExpression);
_sql.Append(like.NotDefined ? " NOT LIKE " : " LIKE ");
Expression(like.SecondExpression);
if (like.EscapeExpression != null)
{
_sql.Append(" ESCAPE ");
Expression(like.EscapeExpression);
}
_sql.Append(')');
break;
case InPredicate inPredicate:
_sql.Append('(');
Comparand(inPredicate.Expression);
_sql.Append(inPredicate.NotDefined ? " NOT IN (" : " IN (");
if (inPredicate.Subquery != null)
{
Query(inPredicate.Subquery.QueryExpression);
}
else
{
for (var i = 0; i < inPredicate.Values.Count; i++)
{
if (i > 0) _sql.Append(", ");
Comparand(inPredicate.Values[i]);
}
}
_sql.Append("))");
break;
case BooleanTernaryExpression between:
_sql.Append('(');
Comparand(between.FirstExpression);
_sql.Append(between.TernaryExpressionType == BooleanTernaryExpressionType.NotBetween ? " NOT BETWEEN " : " BETWEEN ");
Comparand(between.SecondExpression);
_sql.Append(" AND ");
Comparand(between.ThirdExpression);
_sql.Append(')');
break;
case ExistsPredicate exists:
_sql.Append("EXISTS (");
Query(exists.Subquery.QueryExpression);
_sql.Append(')');
break;
default:
throw SqlException.NotSupported(expression.GetType().Name.Replace("Expression", "") + " 条件");
}
}
/// <summary>比较里的日期字符串转成标准格式,日期列按文本比较才是按时间先后(BETWEEN '2024/1/16 9:05:03' AND ...)</summary>
private void Comparand(ScalarExpression expression)
{
if (expression is StringLiteral s && Values.LooksLikeDate(s.Value))
{
_sql.Append(Values.Quote(Values.NormalizeDateLiteral(s.Value)));
return;
}
Expression(expression);
}
}
+102
View File
@@ -0,0 +1,102 @@
using System;
using System.Data.SqlClient;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Sim.Data.Engine;
/// <summary>值在 .NET、SQLite、T-SQL 之间的转换。日期一律以 "yyyy-MM-dd HH:mm:ss" 文本保存,按文本比较即按时间先后。</summary>
internal static class Values
{
private static readonly string[] DateFormats =
{
"yyyy-M-d", "yyyy-M-d H:m", "yyyy-M-d H:m:s", "yyyy-M-d H:m:s.FFFFFFF", "yyyy-M-dTH:m:s", "yyyy-M-dTH:m:s.FFFFFFF",
"yyyy/M/d", "yyyy/M/d H:m", "yyyy/M/d H:m:s", "yyyy/M/d H:m:s.FFFFFFF",
"yyyyMMdd", "yyyyMMdd H:m:s",
"yyyy.M.d", "yyyy.M.d H:m:s",
};
/// <summary>形如日期的字符串字面量(比较时转成标准格式,例如 BETWEEN '2024/1/16 9:05:03' AND ...)</summary>
private static readonly Regex DateLike = new(@"^\d{4}[-/.]\d{1,2}[-/.]\d{1,2}([ T]\d{1,2}:\d{1,2}(:\d{1,2}(\.\d+)?)?)?$");
public static bool LooksLikeDate(string text) => DateLike.IsMatch(text.Trim());
/// <summary>按 SQL Server 接受的写法解析日期(包括中文 Windows 上 DateTime.ToString() 的 2024/1/16 9:05:03)</summary>
public static bool TryParseDateTime(string text, out DateTime value) =>
DateTime.TryParseExact(text.Trim(), DateFormats, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out value);
public static string FormatDateTime(DateTime value) =>
value.Millisecond == 0 && value.Ticks % TimeSpan.TicksPerMillisecond == 0
? value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
: value.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
public static string FormatDate(DateTime value) => value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
/// <summary>字符串里的日期改成标准格式;只有日期没有时间的保持只有日期(与 datetime 比较时相当于当天 0 点)</summary>
public static string NormalizeDateLiteral(string text)
{
if (!TryParseDateTime(text, out var value)) return text;
return text.Contains(':') ? FormatDateTime(value) : FormatDate(value);
}
/// <summary>C# 传进来的参数值 → SQLite 能保存的值</summary>
public static object? ToStorage(object? value) => value switch
{
null or DBNull => null,
DateTime d => FormatDateTime(d),
DateTimeOffset d => FormatDateTime(d.LocalDateTime),
TimeSpan t => t.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture),
bool b => b ? 1L : 0L,
Guid g => g.ToString().ToUpperInvariant(),
decimal m => (double)m,
Enum e => Convert.ToInt64(e, CultureInfo.InvariantCulture),
_ => value,
};
/// <summary>SQLite 读出的值 → SQL Server 对这个类型返回的 .NET 值</summary>
public static object FromStorage(object value, TSqlType? type)
{
if (value is DBNull) return value;
if (type == null || type.ClrType == typeof(object)) return FromExpression(value);
try
{
return type.ClrType switch
{
var t when t == typeof(int) => Convert.ToInt32(value, CultureInfo.InvariantCulture),
var t when t == typeof(long) => Convert.ToInt64(value, CultureInfo.InvariantCulture),
var t when t == typeof(short) => Convert.ToInt16(value, CultureInfo.InvariantCulture),
var t when t == typeof(byte) => Convert.ToByte(value, CultureInfo.InvariantCulture),
var t when t == typeof(bool) => Convert.ToInt64(value, CultureInfo.InvariantCulture) != 0,
var t when t == typeof(double) => Convert.ToDouble(value, CultureInfo.InvariantCulture),
var t when t == typeof(float) => Convert.ToSingle(value, CultureInfo.InvariantCulture),
var t when t == typeof(decimal) => Convert.ToDecimal(value, CultureInfo.InvariantCulture),
var t when t == typeof(DateTime) => value is string s && TryParseDateTime(s, out var d) ? d : value,
var t when t == typeof(TimeSpan) => value is string s && TimeSpan.TryParse(s, CultureInfo.InvariantCulture, out var ts) ? ts : value,
var t when t == typeof(Guid) => value is string s && Guid.TryParse(s, out var g) ? g : value,
var t when t == typeof(string) => Convert.ToString(value, CultureInfo.InvariantCulture)!,
_ => value,
};
}
catch (Exception ex) when (ex is FormatException or OverflowException or InvalidCastException)
{
return value;
}
}
/// <summary>表达式列(如 count(*)、getdate())没有声明类型:整数按 int 返回,日期样子的文本按 DateTime 返回</summary>
public static object FromExpression(object value) => value switch
{
long l when l is >= int.MinValue and <= int.MaxValue => (int)l,
string s when DateLike.IsMatch(s) && s.Contains(':') && TryParseDateTime(s, out var d) => d,
_ => value,
};
/// <summary>T-SQL 字符串字面量</summary>
public static string Quote(string text) => "'" + text.Replace("'", "''") + "'";
/// <summary>SQLite 标识符</summary>
public static string QuoteName(string name) => "\"" + name.Replace("\"", "\"\"") + "\"";
public static SqlException ConvertError(object? value, string type) =>
SqlException.ConvertFailed(Convert.ToString(value, CultureInfo.InvariantCulture) ?? "", type);
}
+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- 仿真的 System.Data.SqlClient:教材用 SQL Server,平台底层用 SQLite(每个学生一套数据库文件),
T-SQL 语句用 ScriptDom 解析后转写成 SQLite 语句。学生代码编译时引用本程序集。 -->
<PropertyGroup>
<AssemblyName>Sim.Data</AssemblyName>
<RootNamespace>System.Data.SqlClient</RootNamespace>
<NoWarn>$(NoWarn);CS1591;CS8618</NoWarn>
</PropertyGroup>
<ItemGroup>
<!-- 运行器的 SQL 服务(数据库页面、准备实验数据)直接使用执行层 -->
<InternalsVisibleTo Include="Sim.Runner" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.13" />
<PackageReference Include="Microsoft.SqlServer.TransactSql.ScriptDom" Version="180.140.0" />
</ItemGroup>
</Project>
+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";
}