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

- 每个学生一套数据库,长期保存(data/databases/<用户编号>/库名.db);学生程序运行时挂进沙箱可读写
- Sim.Data:SqlConnection / SqlCommand / SqlDataReader / SqlDataAdapter / SqlCommandBuilder / SqlParameter;
  连接字符串只按库名打开学生自己的库;ScriptDom 解析 T-SQL 后按白名单转写成 SQLite 语句,
  STRICT 表 + 元数据表还原 SQL Server 的列类型,报错编号和中文信息与 SQL Server 一致;
  INSERT 不写列名时跳过自增列,日期统一格式(书上 DateTime.ToString() 拼接的日期能正确比较)
- 常驻 SQL 服务(Sim.Runner sql-server):数据库页面的查询、对象资源管理器,评分前按实验 database.sql 建库
- 数据库页面(代替 SSMS):对象资源管理器、查询窗口、编辑前 200 行、表设计器、恢复实验初始数据
- DataGridView(绑定 DataView/DataTable/DataSet,单元格修改写回 DataTable)、DateTimePicker;设计器工具箱加入
- 评分:每个检查项一份新建的实验库;新增点 / 修改表格单元格、选日期、表格里有 / 没有某行的检查
- 示例:项目 6 任务 2、3;时间格式改成和中文 Windows 一致(9:05:03);异常堆栈不暴露服务器路径
- 测试:数据库端到端、接口(每人只看到自己的库、恢复实验数据)、评分出错情况、T-SQL 生成

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-23 09:18:56 +08:00
1 parent 6b088240da
commit 670abe5921
72 files changed
+7253 -51

No files matched your search

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