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; /// 结果集的一列:名字、T-SQL 类型(表达式列为 null),以及来自哪张表的哪一列(SqlCommandBuilder 生成语句时要用) 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); } /// 一个结果集(读完放在内存里) internal sealed record QueryResult(IReadOnlyList Columns, List Rows); /// 一批语句的执行结果:各个结果集和受影响的行数(只有增删改时才算,否则为 -1) internal sealed record BatchResult(List Results, int RecordsAffected); /// /// 一个打开的数据库连接:当前库(USE 可以切换)、事务,执行一批 T-SQL。 /// SqlConnection 和数据库页面的查询窗口都用它。 /// 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)!; } /// 每批语句最长执行时间(数据库页面的查询窗口用;学生程序里由沙箱的 CPU 时间限制兜底) 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; } /// 执行一批语句;parameter 按名字(带不带 @ 都行)取参数值 public BatchResult Execute(string sql, Func parameter) { var statements = Translator.Parse(sql); if (TimeLimit is { } limit) _deadline = Environment.TickCount64 + (long)limit.TotalMilliseconds; var results = new List(); 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 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(StringComparer.OrdinalIgnoreCase); var columns = new List(); 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(); 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); } /// SQLite 的报错转成 SQL Server 的错误号和中文信息 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(); } }