项目 6 数据存储:仿冒 SqlClient(T-SQL 转写成 SQLite)、数据库页面、DataGridView、DateTimePicker
- 每个学生一套数据库,长期保存(data/databases/<用户编号>/库名.db);学生程序运行时挂进沙箱可读写 - Sim.Data:SqlConnection / SqlCommand / SqlDataReader / SqlDataAdapter / SqlCommandBuilder / SqlParameter; 连接字符串只按库名打开学生自己的库;ScriptDom 解析 T-SQL 后按白名单转写成 SQLite 语句, STRICT 表 + 元数据表还原 SQL Server 的列类型,报错编号和中文信息与 SQL Server 一致; INSERT 不写列名时跳过自增列,日期统一格式(书上 DateTime.ToString() 拼接的日期能正确比较) - 常驻 SQL 服务(Sim.Runner sql-server):数据库页面的查询、对象资源管理器,评分前按实验 database.sql 建库 - 数据库页面(代替 SSMS):对象资源管理器、查询窗口、编辑前 200 行、表设计器、恢复实验初始数据 - DataGridView(绑定 DataView/DataTable/DataSet,单元格修改写回 DataTable)、DateTimePicker;设计器工具箱加入 - 评分:每个检查项一份新建的实验库;新增点 / 修改表格单元格、选日期、表格里有 / 没有某行的检查 - 示例:项目 6 任务 2、3;时间格式改成和中文 Windows 一致(9:05:03);异常堆栈不暴露服务器路径 - 测试:数据库端到端、接口(每人只看到自己的库、恢复实验数据)、评分出错情况、T-SQL 生成 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b088240da
commit
670abe5921
72 files changed
+7253
-51
No files matched your search
@@ -0,0 +1,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);
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,302 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using Sim.Data.Engine;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
/// <summary>执行 T-SQL 语句。一条命令里可以有多条语句(用分号或换行分开)</summary>
|
||||
public sealed class SqlCommand : DbCommand, ICloneable
|
||||
{
|
||||
private SqlConnection? _connection;
|
||||
private string _commandText = "";
|
||||
private CommandType _commandType = CommandType.Text;
|
||||
|
||||
public SqlCommand() { }
|
||||
|
||||
public SqlCommand(string cmdText) => CommandText = cmdText;
|
||||
|
||||
public SqlCommand(string cmdText, SqlConnection connection) : this(cmdText) => Connection = connection;
|
||||
|
||||
public SqlCommand(string cmdText, SqlConnection connection, SqlTransaction transaction) : this(cmdText, connection) => Transaction = transaction;
|
||||
|
||||
[Diagnostics.CodeAnalysis.AllowNull]
|
||||
public override string CommandText
|
||||
{
|
||||
get => _commandText;
|
||||
set => _commandText = value ?? "";
|
||||
}
|
||||
|
||||
public override int CommandTimeout { get; set; } = 30;
|
||||
|
||||
public override CommandType CommandType
|
||||
{
|
||||
get => _commandType;
|
||||
set => _commandType = value;
|
||||
}
|
||||
|
||||
public new SqlConnection? Connection
|
||||
{
|
||||
get => _connection;
|
||||
set => _connection = value;
|
||||
}
|
||||
|
||||
protected override DbConnection? DbConnection
|
||||
{
|
||||
get => _connection;
|
||||
set => _connection = (SqlConnection?)value;
|
||||
}
|
||||
|
||||
public new SqlParameterCollection Parameters { get; } = new();
|
||||
protected override DbParameterCollection DbParameterCollection => Parameters;
|
||||
|
||||
public new SqlTransaction? Transaction { get; set; }
|
||||
|
||||
protected override DbTransaction? DbTransaction
|
||||
{
|
||||
get => Transaction;
|
||||
set => Transaction = (SqlTransaction?)value;
|
||||
}
|
||||
|
||||
public override bool DesignTimeVisible { get; set; } = true;
|
||||
public override UpdateRowSource UpdatedRowSource { get; set; } = UpdateRowSource.Both;
|
||||
|
||||
public override int ExecuteNonQuery() => Run(nameof(ExecuteNonQuery)).RecordsAffected;
|
||||
|
||||
public override object? ExecuteScalar()
|
||||
{
|
||||
var result = Run(nameof(ExecuteScalar));
|
||||
return result.Results.Count > 0 && result.Results[0].Rows.Count > 0 ? result.Results[0].Rows[0][0] : null;
|
||||
}
|
||||
|
||||
public new SqlDataReader ExecuteReader() => ExecuteReader(CommandBehavior.Default);
|
||||
|
||||
public new SqlDataReader ExecuteReader(CommandBehavior behavior) =>
|
||||
new(Run(nameof(ExecuteReader)), behavior.HasFlag(CommandBehavior.CloseConnection) ? _connection : null);
|
||||
|
||||
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => ExecuteReader(behavior);
|
||||
|
||||
public override void Cancel() { }
|
||||
public override void Prepare() { }
|
||||
|
||||
public new SqlParameter CreateParameter() => new();
|
||||
protected override DbParameter CreateDbParameter() => CreateParameter();
|
||||
|
||||
public SqlCommand Clone()
|
||||
{
|
||||
var clone = new SqlCommand(_commandText, _connection!) { CommandType = _commandType, CommandTimeout = CommandTimeout, Transaction = Transaction };
|
||||
foreach (SqlParameter parameter in Parameters) clone.Parameters.Add(parameter.Clone());
|
||||
return clone;
|
||||
}
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
private BatchResult Run(string operation)
|
||||
{
|
||||
if (_connection == null) throw new InvalidOperationException($"{operation}: Connection 属性尚未初始化。");
|
||||
var session = _connection.Session(operation);
|
||||
if (string.IsNullOrWhiteSpace(_commandText)) throw new InvalidOperationException($"{operation}: CommandText 属性尚未初始化");
|
||||
if (_commandType == CommandType.StoredProcedure) throw SqlException.NotSupported("存储过程");
|
||||
try
|
||||
{
|
||||
return session.Execute(_commandText, name =>
|
||||
{
|
||||
var parameter = Parameters.Find(name);
|
||||
return parameter == null ? (false, null) : (true, parameter.Value);
|
||||
});
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
throw ex.Rethrow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SqlParameter : DbParameter, ICloneable
|
||||
{
|
||||
private SqlDbType _sqlDbType = SqlDbType.NVarChar;
|
||||
private bool _typeSet;
|
||||
private string _parameterName = "";
|
||||
private string _sourceColumn = "";
|
||||
|
||||
public SqlParameter() { }
|
||||
|
||||
public SqlParameter(string parameterName, object? value)
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public SqlParameter(string parameterName, SqlDbType dbType)
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
SqlDbType = dbType;
|
||||
}
|
||||
|
||||
public SqlParameter(string parameterName, SqlDbType dbType, int size) : this(parameterName, dbType) => Size = size;
|
||||
|
||||
public SqlParameter(string parameterName, SqlDbType dbType, int size, string sourceColumn) : this(parameterName, dbType, size) => SourceColumn = sourceColumn;
|
||||
|
||||
[Diagnostics.CodeAnalysis.AllowNull]
|
||||
public override string ParameterName
|
||||
{
|
||||
get => _parameterName;
|
||||
set => _parameterName = value ?? "";
|
||||
}
|
||||
|
||||
public override object? Value { get; set; }
|
||||
|
||||
public SqlDbType SqlDbType
|
||||
{
|
||||
get => _sqlDbType;
|
||||
set
|
||||
{
|
||||
_sqlDbType = value;
|
||||
_typeSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override DbType DbType
|
||||
{
|
||||
get => _sqlDbType switch
|
||||
{
|
||||
SqlDbType.Int => DbType.Int32,
|
||||
SqlDbType.BigInt => DbType.Int64,
|
||||
SqlDbType.SmallInt => DbType.Int16,
|
||||
SqlDbType.TinyInt => DbType.Byte,
|
||||
SqlDbType.Bit => DbType.Boolean,
|
||||
SqlDbType.Float => DbType.Double,
|
||||
SqlDbType.Real => DbType.Single,
|
||||
SqlDbType.Decimal or SqlDbType.Money => DbType.Decimal,
|
||||
SqlDbType.DateTime or SqlDbType.SmallDateTime or SqlDbType.Date => DbType.DateTime,
|
||||
SqlDbType.VarBinary or SqlDbType.Image or SqlDbType.Binary => DbType.Binary,
|
||||
SqlDbType.UniqueIdentifier => DbType.Guid,
|
||||
SqlDbType.VarChar or SqlDbType.Char or SqlDbType.Text => DbType.AnsiString,
|
||||
_ => DbType.String,
|
||||
};
|
||||
set => SqlDbType = value switch
|
||||
{
|
||||
DbType.Int32 => SqlDbType.Int,
|
||||
DbType.Int64 => SqlDbType.BigInt,
|
||||
DbType.Int16 => SqlDbType.SmallInt,
|
||||
DbType.Byte => SqlDbType.TinyInt,
|
||||
DbType.Boolean => SqlDbType.Bit,
|
||||
DbType.Double => SqlDbType.Float,
|
||||
DbType.Single => SqlDbType.Real,
|
||||
DbType.Decimal or DbType.Currency => SqlDbType.Decimal,
|
||||
DbType.DateTime or DbType.Date => SqlDbType.DateTime,
|
||||
DbType.Binary => SqlDbType.VarBinary,
|
||||
DbType.Guid => SqlDbType.UniqueIdentifier,
|
||||
DbType.AnsiString => SqlDbType.VarChar,
|
||||
_ => SqlDbType.NVarChar,
|
||||
};
|
||||
}
|
||||
|
||||
public override ParameterDirection Direction { get; set; } = ParameterDirection.Input;
|
||||
public override bool IsNullable { get; set; }
|
||||
public override int Size { get; set; }
|
||||
public new byte Precision { get; set; }
|
||||
public new byte Scale { get; set; }
|
||||
|
||||
[Diagnostics.CodeAnalysis.AllowNull]
|
||||
public override string SourceColumn
|
||||
{
|
||||
get => _sourceColumn;
|
||||
set => _sourceColumn = value ?? "";
|
||||
}
|
||||
|
||||
public override bool SourceColumnNullMapping { get; set; }
|
||||
public override DataRowVersion SourceVersion { get; set; } = DataRowVersion.Current;
|
||||
|
||||
public override void ResetDbType()
|
||||
{
|
||||
_sqlDbType = SqlDbType.NVarChar;
|
||||
_typeSet = false;
|
||||
}
|
||||
|
||||
internal bool TypeSet => _typeSet;
|
||||
|
||||
public SqlParameter Clone() => new(_parameterName, Value)
|
||||
{
|
||||
_sqlDbType = _sqlDbType,
|
||||
_typeSet = _typeSet,
|
||||
Direction = Direction,
|
||||
Size = Size,
|
||||
SourceColumn = SourceColumn,
|
||||
SourceVersion = SourceVersion,
|
||||
SourceColumnNullMapping = SourceColumnNullMapping,
|
||||
IsNullable = IsNullable,
|
||||
};
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
public override string ToString() => _parameterName;
|
||||
}
|
||||
|
||||
public sealed class SqlParameterCollection : DbParameterCollection
|
||||
{
|
||||
private readonly List<SqlParameter> _items = new();
|
||||
|
||||
public override int Count => _items.Count;
|
||||
public override object SyncRoot => this;
|
||||
|
||||
public new SqlParameter this[int index]
|
||||
{
|
||||
get => _items[index];
|
||||
set => _items[index] = value;
|
||||
}
|
||||
|
||||
public new SqlParameter this[string parameterName]
|
||||
{
|
||||
get => Find(parameterName) ?? throw new IndexOutOfRangeException($"此 SqlParameterCollection 中未包含带有 ParameterName“{parameterName}”的 SqlParameter。");
|
||||
set => _items[IndexOf(parameterName)] = value;
|
||||
}
|
||||
|
||||
/// <summary>按名字找参数,"@id" 和 "id" 视为同一个(和 SqlClient 一样不区分大小写)</summary>
|
||||
internal SqlParameter? Find(string name)
|
||||
{
|
||||
var bare = name.TrimStart('@');
|
||||
return _items.FirstOrDefault(p => string.Equals(p.ParameterName.TrimStart('@'), bare, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public SqlParameter Add(SqlParameter value)
|
||||
{
|
||||
_items.Add(value ?? throw new ArgumentNullException(nameof(value)));
|
||||
return value;
|
||||
}
|
||||
|
||||
public override int Add(object value)
|
||||
{
|
||||
Add(value as SqlParameter ?? throw new InvalidCastException("SqlParameterCollection 只接受非空的 SqlParameter 类型对象。"));
|
||||
return _items.Count - 1;
|
||||
}
|
||||
|
||||
public SqlParameter Add(string parameterName, SqlDbType sqlDbType) => Add(new SqlParameter(parameterName, sqlDbType));
|
||||
public SqlParameter Add(string parameterName, SqlDbType sqlDbType, int size) => Add(new SqlParameter(parameterName, sqlDbType, size));
|
||||
public SqlParameter Add(string parameterName, SqlDbType sqlDbType, int size, string sourceColumn) => Add(new SqlParameter(parameterName, sqlDbType, size, sourceColumn));
|
||||
public SqlParameter AddWithValue(string parameterName, object? value) => Add(new SqlParameter(parameterName, value));
|
||||
|
||||
public override void AddRange(Array values)
|
||||
{
|
||||
foreach (var value in values) Add(value);
|
||||
}
|
||||
|
||||
public void AddRange(SqlParameter[] values) => AddRange((Array)values);
|
||||
|
||||
public override void Clear() => _items.Clear();
|
||||
public override bool Contains(object value) => value is SqlParameter p && _items.Contains(p);
|
||||
public override bool Contains(string value) => Find(value) != null;
|
||||
public override void CopyTo(Array array, int index) => ((ICollection)_items).CopyTo(array, index);
|
||||
public override IEnumerator GetEnumerator() => _items.GetEnumerator();
|
||||
public override int IndexOf(object value) => value is SqlParameter p ? _items.IndexOf(p) : -1;
|
||||
public override int IndexOf(string parameterName) => Find(parameterName) is { } p ? _items.IndexOf(p) : -1;
|
||||
public override void Insert(int index, object value) => _items.Insert(index, (SqlParameter)value);
|
||||
public override void Remove(object value) => _items.Remove((SqlParameter)value);
|
||||
public override void RemoveAt(int index) => _items.RemoveAt(index);
|
||||
public override void RemoveAt(string parameterName) => _items.RemoveAt(IndexOf(parameterName));
|
||||
protected override DbParameter GetParameter(int index) => _items[index];
|
||||
protected override DbParameter GetParameter(string parameterName) => this[parameterName];
|
||||
protected override void SetParameter(int index, DbParameter value) => _items[index] = (SqlParameter)value;
|
||||
protected override void SetParameter(string parameterName, DbParameter value) => this[parameterName] = (SqlParameter)value;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using Sim.Data.Engine;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库连接。连接字符串里的服务器名、账号、密码都不检查(书上的长服务器名、"." 都能连上),
|
||||
/// 只按 Initial Catalog(或 Database)打开这个学生自己的同名数据库;不写时连到 master。
|
||||
/// </summary>
|
||||
public sealed class SqlConnection : DbConnection, ICloneable
|
||||
{
|
||||
private static readonly HashSet<string> KnownKeywords = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"data source", "server", "address", "addr", "network address", "initial catalog", "database", "user id", "uid", "user",
|
||||
"password", "pwd", "integrated security", "trusted_connection", "persist security info", "persistsecurityinfo",
|
||||
"connect timeout", "connection timeout", "timeout", "pooling", "max pool size", "min pool size", "multipleactiveresultsets",
|
||||
"encrypt", "trustservercertificate", "application name", "app", "workstation id", "wsid", "packet size",
|
||||
"asynchronous processing", "async", "user instance", "attachdbfilename", "connection lifetime", "load balance timeout", "current language", "language",
|
||||
};
|
||||
|
||||
private string _connectionString = "";
|
||||
private ConnectionState _state = ConnectionState.Closed;
|
||||
private Session? _session;
|
||||
|
||||
public SqlConnection() { }
|
||||
|
||||
public SqlConnection(string connectionString) => ConnectionString = connectionString;
|
||||
|
||||
[Diagnostics.CodeAnalysis.AllowNull]
|
||||
public override string ConnectionString
|
||||
{
|
||||
get => _connectionString;
|
||||
set
|
||||
{
|
||||
if (_state != ConnectionState.Closed) throw new InvalidOperationException("不允许更改“ConnectionString”属性。连接的当前状态为打开。");
|
||||
_connectionString = value ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
public override string Database => _session?.Database ?? Setting("initial catalog", "database") ?? DatabaseStore.Master;
|
||||
public override string DataSource => Setting("data source", "server", "address", "addr", "network address") ?? "";
|
||||
public override string ServerVersion => "10.50.1600";
|
||||
public override ConnectionState State => _state;
|
||||
public override int ConnectionTimeout => 15;
|
||||
public string WorkstationId => Environment.MachineName;
|
||||
|
||||
/// <summary>连接打开时的数据库会话(平台内部使用)</summary>
|
||||
internal Session Session(string operation) =>
|
||||
_state == ConnectionState.Open && _session != null
|
||||
? _session
|
||||
: throw new InvalidOperationException($"{operation} 要求已打开且可用的连接。连接的当前状态为已关闭。");
|
||||
|
||||
public override void Open()
|
||||
{
|
||||
if (_state == ConnectionState.Open) throw new InvalidOperationException("连接未关闭。连接的当前状态为打开。");
|
||||
CheckKeywords();
|
||||
var store = DatabaseStore.FromEnvironment()
|
||||
?? throw new SqlException(-1, "在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误:这次运行没有准备数据库。");
|
||||
try
|
||||
{
|
||||
_session = new Session(store, Database);
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
throw ex.Rethrow();
|
||||
}
|
||||
SetState(ConnectionState.Open);
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (_session == null) return;
|
||||
_session.Dispose();
|
||||
_session = null;
|
||||
SetState(ConnectionState.Closed);
|
||||
}
|
||||
|
||||
public override void ChangeDatabase(string databaseName) => Session("ChangeDatabase").ChangeDatabase(databaseName);
|
||||
|
||||
public new SqlCommand CreateCommand() => new() { Connection = this };
|
||||
|
||||
protected override DbCommand CreateDbCommand() => CreateCommand();
|
||||
|
||||
public new SqlTransaction BeginTransaction() => BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
public new SqlTransaction BeginTransaction(IsolationLevel iso)
|
||||
{
|
||||
Session("BeginTransaction").BeginTransaction();
|
||||
return new SqlTransaction(this, iso);
|
||||
}
|
||||
|
||||
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => BeginTransaction(isolationLevel);
|
||||
|
||||
public object Clone() => new SqlConnection(_connectionString);
|
||||
|
||||
public static void ClearAllPools() { }
|
||||
public static void ClearPool(SqlConnection connection) { }
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing) Close();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void SetState(ConnectionState state)
|
||||
{
|
||||
var original = _state;
|
||||
_state = state;
|
||||
OnStateChange(new StateChangeEventArgs(original, state));
|
||||
}
|
||||
|
||||
private string? Setting(params string[] keys)
|
||||
{
|
||||
var builder = Parse();
|
||||
foreach (var key in keys)
|
||||
if (builder.TryGetValue(key, out var value) && value?.ToString() is { Length: > 0 } text) return text.Trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
private DbConnectionStringBuilder Parse()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new DbConnectionStringBuilder { ConnectionString = _connectionString };
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw new ArgumentException("初始化字符串的格式不符合从索引 0 处开始的规范。");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckKeywords()
|
||||
{
|
||||
foreach (string key in Parse().Keys)
|
||||
if (!KnownKeywords.Contains(key)) throw new ArgumentException($"不支持关键字:“{key}”。");
|
||||
if (Setting("attachdbfilename") != null) throw new SqlException(-1, "平台不支持 AttachDbFilename:请用 Initial Catalog 指定数据库名。");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SqlTransaction : DbTransaction
|
||||
{
|
||||
private readonly SqlConnection _connection;
|
||||
private bool _done;
|
||||
|
||||
internal SqlTransaction(SqlConnection connection, IsolationLevel isolationLevel)
|
||||
{
|
||||
_connection = connection;
|
||||
IsolationLevel = isolationLevel;
|
||||
}
|
||||
|
||||
public new SqlConnection Connection => _connection;
|
||||
protected override DbConnection DbConnection => _connection;
|
||||
public override IsolationLevel IsolationLevel { get; }
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (_done) throw new InvalidOperationException("此 SqlTransaction 已完成;它再也无法使用。");
|
||||
_done = true;
|
||||
_connection.Session("Commit").Commit();
|
||||
}
|
||||
|
||||
public override void Rollback()
|
||||
{
|
||||
if (_done) throw new InvalidOperationException("此 SqlTransaction 已完成;它再也无法使用。");
|
||||
_done = true;
|
||||
_connection.Session("Rollback").Rollback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Data.Common;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
public delegate void SqlRowUpdatingEventHandler(object sender, SqlRowUpdatingEventArgs e);
|
||||
public delegate void SqlRowUpdatedEventHandler(object sender, SqlRowUpdatedEventArgs e);
|
||||
|
||||
public sealed class SqlRowUpdatingEventArgs : RowUpdatingEventArgs
|
||||
{
|
||||
public SqlRowUpdatingEventArgs(DataRow row, IDbCommand? command, StatementType statementType, DataTableMapping tableMapping)
|
||||
: base(row, command, statementType, tableMapping) { }
|
||||
|
||||
public new SqlCommand? Command
|
||||
{
|
||||
get => (SqlCommand?)base.Command;
|
||||
set => base.Command = value;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SqlRowUpdatedEventArgs : RowUpdatedEventArgs
|
||||
{
|
||||
public SqlRowUpdatedEventArgs(DataRow row, IDbCommand? command, StatementType statementType, DataTableMapping tableMapping)
|
||||
: base(row, command, statementType, tableMapping) { }
|
||||
|
||||
public new SqlCommand? Command => (SqlCommand?)base.Command;
|
||||
}
|
||||
|
||||
/// <summary>在数据库和 DataSet 之间搬运数据:Fill 读出来,Update 把 DataTable 里改过的行写回去</summary>
|
||||
public sealed class SqlDataAdapter : DbDataAdapter, ICloneable
|
||||
{
|
||||
public SqlDataAdapter() { }
|
||||
|
||||
public SqlDataAdapter(SqlCommand selectCommand) => SelectCommand = selectCommand;
|
||||
|
||||
public SqlDataAdapter(string selectCommandText, SqlConnection selectConnection) => SelectCommand = new SqlCommand(selectCommandText, selectConnection);
|
||||
|
||||
public SqlDataAdapter(string selectCommandText, string selectConnectionString) =>
|
||||
SelectCommand = new SqlCommand(selectCommandText, new SqlConnection(selectConnectionString));
|
||||
|
||||
public new SqlCommand? SelectCommand
|
||||
{
|
||||
get => (SqlCommand?)base.SelectCommand;
|
||||
set => base.SelectCommand = value;
|
||||
}
|
||||
|
||||
public new SqlCommand? InsertCommand
|
||||
{
|
||||
get => (SqlCommand?)base.InsertCommand;
|
||||
set => base.InsertCommand = value;
|
||||
}
|
||||
|
||||
public new SqlCommand? UpdateCommand
|
||||
{
|
||||
get => (SqlCommand?)base.UpdateCommand;
|
||||
set => base.UpdateCommand = value;
|
||||
}
|
||||
|
||||
public new SqlCommand? DeleteCommand
|
||||
{
|
||||
get => (SqlCommand?)base.DeleteCommand;
|
||||
set => base.DeleteCommand = value;
|
||||
}
|
||||
|
||||
public event SqlRowUpdatingEventHandler? RowUpdating;
|
||||
public event SqlRowUpdatedEventHandler? RowUpdated;
|
||||
|
||||
protected override RowUpdatingEventArgs CreateRowUpdatingEvent(DataRow dataRow, IDbCommand? command, StatementType statementType, DataTableMapping tableMapping) =>
|
||||
new SqlRowUpdatingEventArgs(dataRow, command, statementType, tableMapping);
|
||||
|
||||
protected override RowUpdatedEventArgs CreateRowUpdatedEvent(DataRow dataRow, IDbCommand? command, StatementType statementType, DataTableMapping tableMapping) =>
|
||||
new SqlRowUpdatedEventArgs(dataRow, command, statementType, tableMapping);
|
||||
|
||||
protected override void OnRowUpdating(RowUpdatingEventArgs value) => RowUpdating?.Invoke(this, (SqlRowUpdatingEventArgs)value);
|
||||
|
||||
protected override void OnRowUpdated(RowUpdatedEventArgs value) => RowUpdated?.Invoke(this, (SqlRowUpdatedEventArgs)value);
|
||||
|
||||
object ICloneable.Clone() => new SqlDataAdapter { SelectCommand = SelectCommand, InsertCommand = InsertCommand, UpdateCommand = UpdateCommand, DeleteCommand = DeleteCommand };
|
||||
}
|
||||
|
||||
/// <summary>按 SqlDataAdapter 的查询语句自动生成 INSERT / UPDATE / DELETE(要求查询包含主键列)</summary>
|
||||
public sealed class SqlCommandBuilder : DbCommandBuilder
|
||||
{
|
||||
public SqlCommandBuilder()
|
||||
{
|
||||
QuotePrefix = "[";
|
||||
QuoteSuffix = "]";
|
||||
}
|
||||
|
||||
public SqlCommandBuilder(SqlDataAdapter adapter) : this() => DataAdapter = adapter;
|
||||
|
||||
public new SqlDataAdapter? DataAdapter
|
||||
{
|
||||
get => (SqlDataAdapter?)base.DataAdapter;
|
||||
set => base.DataAdapter = value;
|
||||
}
|
||||
|
||||
public new SqlCommand GetInsertCommand() => (SqlCommand)base.GetInsertCommand();
|
||||
public new SqlCommand GetInsertCommand(bool useColumnsForParameterNames) => (SqlCommand)base.GetInsertCommand(useColumnsForParameterNames);
|
||||
public new SqlCommand GetUpdateCommand() => (SqlCommand)base.GetUpdateCommand();
|
||||
public new SqlCommand GetUpdateCommand(bool useColumnsForParameterNames) => (SqlCommand)base.GetUpdateCommand(useColumnsForParameterNames);
|
||||
public new SqlCommand GetDeleteCommand() => (SqlCommand)base.GetDeleteCommand();
|
||||
public new SqlCommand GetDeleteCommand(bool useColumnsForParameterNames) => (SqlCommand)base.GetDeleteCommand(useColumnsForParameterNames);
|
||||
|
||||
protected override void ApplyParameterInfo(DbParameter parameter, DataRow row, StatementType statementType, bool whereClause) { }
|
||||
|
||||
protected override string GetParameterName(int parameterOrdinal) => "@p" + parameterOrdinal.ToString(Globalization.CultureInfo.InvariantCulture);
|
||||
protected override string GetParameterName(string parameterName) => "@" + parameterName;
|
||||
protected override string GetParameterPlaceholder(int parameterOrdinal) => GetParameterName(parameterOrdinal);
|
||||
|
||||
public override string QuoteIdentifier(string unquotedIdentifier) => "[" + unquotedIdentifier.Replace("]", "]]") + "]";
|
||||
|
||||
protected override void SetRowUpdatingHandler(DbDataAdapter adapter)
|
||||
{
|
||||
if (adapter is not SqlDataAdapter sql) throw new InvalidOperationException("SqlCommandBuilder 只能用于 SqlDataAdapter。");
|
||||
if (adapter == base.DataAdapter) sql.RowUpdating += Handler;
|
||||
else sql.RowUpdating -= Handler;
|
||||
}
|
||||
|
||||
private void Handler(object sender, SqlRowUpdatingEventArgs e) => RowUpdatingHandler(e);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using System.Collections;
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using Sim.Data.Engine;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
/// <summary>按行读取查询结果(结果已在内存里,读的时候不再访问数据库)</summary>
|
||||
public sealed class SqlDataReader : DbDataReader
|
||||
{
|
||||
private readonly BatchResult _batch;
|
||||
private readonly SqlConnection? _closeConnection;
|
||||
private int _resultIndex;
|
||||
private int _row = -1;
|
||||
private bool _closed;
|
||||
|
||||
internal SqlDataReader(BatchResult batch, SqlConnection? closeConnection)
|
||||
{
|
||||
_batch = batch;
|
||||
_closeConnection = closeConnection;
|
||||
}
|
||||
|
||||
private QueryResult? Current => _resultIndex < _batch.Results.Count ? _batch.Results[_resultIndex] : null;
|
||||
|
||||
private object[] Row
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_closed) throw new InvalidOperationException("阅读器关闭时尝试调用 Read 无效。");
|
||||
var current = Current;
|
||||
if (current == null || _row < 0 || _row >= current.Rows.Count) throw new InvalidOperationException("不存在数据时进行无效的读取尝试。");
|
||||
return current.Rows[_row];
|
||||
}
|
||||
}
|
||||
|
||||
public override int FieldCount => Current?.Columns.Count ?? 0;
|
||||
public override bool HasRows => Current?.Rows.Count > 0;
|
||||
public override bool IsClosed => _closed;
|
||||
public override int RecordsAffected => _batch.RecordsAffected;
|
||||
public override int Depth => 0;
|
||||
|
||||
public override object this[int ordinal] => GetValue(ordinal);
|
||||
public override object this[string name] => GetValue(GetOrdinal(name));
|
||||
|
||||
public override bool Read()
|
||||
{
|
||||
if (_closed) throw new InvalidOperationException("阅读器关闭时尝试调用 Read 无效。");
|
||||
var current = Current;
|
||||
if (current == null) return false;
|
||||
_row++;
|
||||
return _row < current.Rows.Count;
|
||||
}
|
||||
|
||||
public override bool NextResult()
|
||||
{
|
||||
_resultIndex++;
|
||||
_row = -1;
|
||||
return Current != null;
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (_closed) return;
|
||||
_closed = true;
|
||||
_closeConnection?.Close();
|
||||
}
|
||||
|
||||
public override string GetName(int ordinal) => Column(ordinal).Name;
|
||||
|
||||
public override int GetOrdinal(string name)
|
||||
{
|
||||
var columns = Current?.Columns ?? throw new IndexOutOfRangeException(name);
|
||||
for (var i = 0; i < columns.Count; i++)
|
||||
if (string.Equals(columns[i].Name, name, StringComparison.OrdinalIgnoreCase)) return i;
|
||||
throw new IndexOutOfRangeException(name);
|
||||
}
|
||||
|
||||
public override Type GetFieldType(int ordinal) => Column(ordinal).ClrType;
|
||||
public override string GetDataTypeName(int ordinal) => Column(ordinal).Type?.Name ?? "sql_variant";
|
||||
|
||||
public override object GetValue(int ordinal)
|
||||
{
|
||||
Column(ordinal);
|
||||
return Row[ordinal];
|
||||
}
|
||||
|
||||
public override int GetValues(object[] values)
|
||||
{
|
||||
var row = Row;
|
||||
var count = Math.Min(values.Length, row.Length);
|
||||
Array.Copy(row, values, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
public override bool IsDBNull(int ordinal) => GetValue(ordinal) is DBNull;
|
||||
|
||||
public override bool GetBoolean(int ordinal) => Cast<bool>(ordinal);
|
||||
public override byte GetByte(int ordinal) => Cast<byte>(ordinal);
|
||||
public override char GetChar(int ordinal) => throw new NotSupportedException("SqlDataReader 不支持 GetChar。");
|
||||
public override DateTime GetDateTime(int ordinal) => Cast<DateTime>(ordinal);
|
||||
public override decimal GetDecimal(int ordinal) => Cast<decimal>(ordinal);
|
||||
public override double GetDouble(int ordinal) => Cast<double>(ordinal);
|
||||
public override float GetFloat(int ordinal) => Cast<float>(ordinal);
|
||||
public override Guid GetGuid(int ordinal) => Cast<Guid>(ordinal);
|
||||
public override short GetInt16(int ordinal) => Cast<short>(ordinal);
|
||||
public override int GetInt32(int ordinal) => Cast<int>(ordinal);
|
||||
public override long GetInt64(int ordinal) => Cast<long>(ordinal);
|
||||
public override string GetString(int ordinal) => Cast<string>(ordinal);
|
||||
|
||||
/// <summary>和 SqlClient 一样严格:列是 varchar 时 GetInt32 报"指定的转换无效",是 NULL 时报"数据为空"</summary>
|
||||
private T Cast<T>(int ordinal)
|
||||
{
|
||||
var value = GetValue(ordinal);
|
||||
if (value is DBNull) throw new InvalidCastException("数据为空。不能对空值调用此方法或属性。");
|
||||
if (value is T typed) return typed;
|
||||
// 表达式列(如 count(*))没有声明类型,数字之间可以互相转换
|
||||
if (Column(ordinal).Type == null && value is IConvertible && typeof(T) != typeof(string))
|
||||
return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
|
||||
throw new InvalidCastException("指定的转换无效。");
|
||||
}
|
||||
|
||||
public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length)
|
||||
{
|
||||
var bytes = Cast<byte[]>(ordinal);
|
||||
if (buffer == null) return bytes.Length;
|
||||
var count = (int)Math.Min(length, bytes.Length - dataOffset);
|
||||
if (count <= 0) return 0;
|
||||
Array.Copy(bytes, dataOffset, buffer, bufferOffset, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length)
|
||||
{
|
||||
var text = Cast<string>(ordinal);
|
||||
if (buffer == null) return text.Length;
|
||||
var count = (int)Math.Min(length, text.Length - dataOffset);
|
||||
if (count <= 0) return 0;
|
||||
text.CopyTo((int)dataOffset, buffer, bufferOffset, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
public override IEnumerator GetEnumerator() => new DbEnumerator(this, closeReader: false);
|
||||
|
||||
/// <summary>列的结构信息:DbDataAdapter 填充、SqlCommandBuilder 生成增删改语句时要用(主键、自增、来自哪张表的哪一列)</summary>
|
||||
public override DataTable GetSchemaTable()
|
||||
{
|
||||
var table = new DataTable("SchemaTable");
|
||||
table.Columns.Add(SchemaTableColumn.ColumnName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int));
|
||||
table.Columns.Add(SchemaTableColumn.ColumnSize, typeof(int));
|
||||
table.Columns.Add(SchemaTableColumn.NumericPrecision, typeof(short));
|
||||
table.Columns.Add(SchemaTableColumn.NumericScale, typeof(short));
|
||||
table.Columns.Add(SchemaTableColumn.IsUnique, typeof(bool));
|
||||
table.Columns.Add(SchemaTableColumn.IsKey, typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.BaseColumnName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.BaseTableName, typeof(string));
|
||||
table.Columns.Add(SchemaTableColumn.DataType, typeof(Type));
|
||||
table.Columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool));
|
||||
table.Columns.Add(SchemaTableColumn.ProviderType, typeof(int));
|
||||
table.Columns.Add(SchemaTableColumn.IsAliased, typeof(bool));
|
||||
table.Columns.Add(SchemaTableColumn.IsExpression, typeof(bool));
|
||||
table.Columns.Add("IsIdentity", typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool));
|
||||
table.Columns.Add(SchemaTableColumn.IsLong, typeof(bool));
|
||||
table.Columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool));
|
||||
table.Columns.Add("DataTypeName", typeof(string));
|
||||
|
||||
var columns = Current?.Columns ?? Array.Empty<ResultColumn>();
|
||||
for (var i = 0; i < columns.Count; i++)
|
||||
{
|
||||
var c = columns[i];
|
||||
var row = table.NewRow();
|
||||
row[SchemaTableColumn.ColumnName] = c.Name;
|
||||
row[SchemaTableColumn.ColumnOrdinal] = i;
|
||||
row[SchemaTableColumn.ColumnSize] = c.Type?.MaxLength ?? (c.Type?.IsMax == true ? int.MaxValue : 0);
|
||||
row[SchemaTableColumn.NumericPrecision] = (short)(c.Type?.Precision ?? 0);
|
||||
row[SchemaTableColumn.NumericScale] = (short)(c.Type?.Scale ?? 0);
|
||||
row[SchemaTableColumn.IsUnique] = c.IsKey;
|
||||
row[SchemaTableColumn.IsKey] = c.IsKey;
|
||||
row[SchemaTableColumn.BaseColumnName] = (object?)c.BaseColumn ?? DBNull.Value;
|
||||
row[SchemaTableColumn.BaseSchemaName] = c.BaseTable != null ? "dbo" : DBNull.Value;
|
||||
row[SchemaTableColumn.BaseTableName] = (object?)c.BaseTable ?? DBNull.Value;
|
||||
row[SchemaTableColumn.DataType] = c.ClrType;
|
||||
row[SchemaTableColumn.AllowDBNull] = c.AllowNull;
|
||||
row[SchemaTableColumn.ProviderType] = 0;
|
||||
row[SchemaTableColumn.IsAliased] = c.BaseColumn != null && !string.Equals(c.BaseColumn, c.Name, StringComparison.OrdinalIgnoreCase);
|
||||
row[SchemaTableColumn.IsExpression] = c.BaseColumn == null;
|
||||
row["IsIdentity"] = c.IsIdentity;
|
||||
row[SchemaTableOptionalColumn.IsAutoIncrement] = c.IsIdentity;
|
||||
row[SchemaTableOptionalColumn.IsRowVersion] = false;
|
||||
row[SchemaTableOptionalColumn.IsHidden] = false;
|
||||
row[SchemaTableColumn.IsLong] = c.Type is { IsMax: true } or { Name: "text" or "ntext" or "image" };
|
||||
row[SchemaTableOptionalColumn.IsReadOnly] = c.IsIdentity || c.BaseColumn == null;
|
||||
row["DataTypeName"] = c.Type?.Name ?? "sql_variant";
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private ResultColumn Column(int ordinal)
|
||||
{
|
||||
var columns = Current?.Columns;
|
||||
if (columns == null || ordinal < 0 || ordinal >= columns.Count) throw new IndexOutOfRangeException("索引超出了数组界限。");
|
||||
return columns[ordinal];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Data.Common;
|
||||
|
||||
namespace System.Data.SqlClient;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库报错。Number 和 Message 与中文版 SQL Server 一致(如 208"对象名 'X' 无效。"),
|
||||
/// 学生在网上搜报错信息能找到对应的说明。
|
||||
/// </summary>
|
||||
public sealed class SqlException : DbException
|
||||
{
|
||||
internal SqlException(int number, string message, int lineNumber = 1) : base(message, unchecked((int)0x80131904))
|
||||
{
|
||||
Number = number;
|
||||
LineNumber = lineNumber;
|
||||
}
|
||||
|
||||
public int Number { get; }
|
||||
public int LineNumber { get; }
|
||||
public byte Class => 16;
|
||||
public byte State => 1;
|
||||
public string Server => ".";
|
||||
public string Procedure => "";
|
||||
|
||||
/// <summary>在 SqlClient 的公开方法里重新抛出:学生看到的调用栈从 SqlCommand.ExecuteNonQuery 等开始,不含平台内部的实现</summary>
|
||||
internal SqlException Rethrow() => new(Number, Message, LineNumber);
|
||||
|
||||
public override string ToString() => $"{GetType().FullName} (0x80131904): {Message}{(StackTrace != null ? Environment.NewLine + StackTrace : "")}";
|
||||
|
||||
// ---------- 常见报错 ----------
|
||||
|
||||
internal static SqlException Syntax(string near, int line = 1) =>
|
||||
new(102, IsKeyword(near) ? $"关键字 '{near}' 附近有语法错误。" : $"'{near}' 附近有语法错误。", line);
|
||||
|
||||
internal static SqlException InvalidObject(string name) => new(208, $"对象名 '{name}' 无效。");
|
||||
internal static SqlException InvalidColumn(string name) => new(207, $"列名 '{name}' 无效。");
|
||||
internal static SqlException AmbiguousColumn(string name) => new(209, $"列名 '{name}' 不明确。");
|
||||
internal static SqlException ColumnCountMismatch() => new(213, "列名或所提供值的数目与表定义不匹配。");
|
||||
internal static SqlException IdentityInsert(string table) => new(544, $"当 IDENTITY_INSERT 设置为 OFF 时,不能为表 '{table}' 中的标识列插入显式值。");
|
||||
internal static SqlException NullNotAllowed(string column, string table) => new(515, $"不能将值 NULL 插入列 '{column}',表 '{table}';列不允许有 Null 值。INSERT 失败。");
|
||||
internal static SqlException DuplicateKey(string table) => new(2627, $"违反了 PRIMARY KEY 约束。不能在对象 'dbo.{table}' 中插入重复键。");
|
||||
internal static SqlException Truncated() => new(8152, "将截断字符串或二进制数据。");
|
||||
internal static SqlException ConvertFailed(string value, string type) => new(245, $"在将 varchar 值 '{value}' 转换成数据类型 {type} 时失败。");
|
||||
internal static SqlException DateConvertFailed() => new(241, "从字符串转换日期和/或时间时,转换失败。");
|
||||
internal static SqlException ObjectExists(string name) => new(2714, $"数据库中已存在名为 '{name}' 的对象。");
|
||||
internal static SqlException DatabaseExists(string name) => new(1801, $"数据库 '{name}' 已存在。请选择其他数据库名称。");
|
||||
internal static SqlException CannotOpenDatabase(string name) => new(4060, $"无法打开登录所请求的数据库 \"{name}\"。登录失败。");
|
||||
internal static SqlException DatabaseNotFound(string name) => new(911, $"数据库 '{name}' 不存在。请确保正确地输入了该名称。");
|
||||
internal static SqlException DatabaseInUse(string name) => new(3702, $"无法删除数据库 \"{name}\",因为该数据库当前正在使用。");
|
||||
internal static SqlException CannotDropTable(string name) => new(3701, $"无法对 表 '{name}' 执行 删除,因为它不存在,或者您没有所需的权限。");
|
||||
internal static SqlException UnknownFunction(string name) => new(195, $"'{name}' 不是可以识别的 内置函数名称。");
|
||||
internal static SqlException UndeclaredVariable(string name) => new(137, $"必须声明标量变量 \"{name}\"。");
|
||||
internal static SqlException DivideByZero() => new(8134, "遇到以零作除数错误。");
|
||||
internal static SqlException NotSupported(string what) => new(40000, $"平台暂不支持 {what}。");
|
||||
|
||||
private static bool IsKeyword(string word) => word.ToUpperInvariant() is
|
||||
"SELECT" or "FROM" or "WHERE" or "INSERT" or "INTO" or "VALUES" or "UPDATE" or "SET" or "DELETE" or "CREATE" or "TABLE"
|
||||
or "DROP" or "ALTER" or "AND" or "OR" or "NOT" or "NULL" or "ORDER" or "GROUP" or "BY" or "HAVING" or "AS" or "ON"
|
||||
or "JOIN" or "INNER" or "LEFT" or "RIGHT" or "OUTER" or "IS" or "IN" or "LIKE" or "BETWEEN" or "TOP" or "DISTINCT"
|
||||
or "PRIMARY" or "KEY" or "IDENTITY" or "DEFAULT" or "DATABASE" or "USE" or "CASE" or "WHEN" or "THEN" or "ELSE" or "END";
|
||||
}
|
||||
Reference in new issue
Block a user