From b544d5cccd76735e034c8055918a50927b437474 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期二, 10 十一月 2026 08:29:04 +0800
Subject: [PATCH] fix: 维护模式重复进入失败与按钮灰死 + Broker 并发验证用例
---
Aging.Infrastructure/Core/DbLeaseStore.cs | 585 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 585 insertions(+), 0 deletions(-)
diff --git a/Aging.Infrastructure/Core/DbLeaseStore.cs b/Aging.Infrastructure/Core/DbLeaseStore.cs
new file mode 100644
index 0000000..48bcf37
--- /dev/null
+++ b/Aging.Infrastructure/Core/DbLeaseStore.cs
@@ -0,0 +1,585 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Aging.ResourceContracts;
+using log4net;
+
+namespace Aging.Infrastructure.Core
+{
+ public sealed class DbLeaseStore : ILeaseCoordinator
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(DbLeaseStore));
+
+ private readonly string _dbPath;
+ private readonly string _authorityId;
+
+ public string AuthorityId => _authorityId;
+ public long CurrentEpoch { get; private set; }
+
+ public DbLeaseStore(string dbPath, string authorityId)
+ {
+ _dbPath = dbPath;
+ _authorityId = authorityId;
+ EnsureDirectory();
+ InitializeSchema();
+ CurrentEpoch = ReadOrInitEpoch();
+ }
+
+ private void EnsureDirectory()
+ {
+ var dir = Path.GetDirectoryName(_dbPath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+ }
+
+ private void BeginImmediateTx(System.Data.SQLite.SQLiteConnection conn)
+ {
+ // BEGIN IMMEDIATE 鑾峰彇 RESERVED 閿侊紝闃绘鍏朵粬杩涚▼鍐欏叆锛岄槻姝� TOCTOU 绔炴��
+ using (var cmd = conn.CreateCommand())
+ {
+ cmd.CommandText = "BEGIN IMMEDIATE";
+ cmd.ExecuteNonQuery();
+ }
+ }
+
+ private void CommitTx(System.Data.SQLite.SQLiteConnection conn)
+ {
+ using (var cmd = conn.CreateCommand())
+ {
+ cmd.CommandText = "COMMIT";
+ cmd.ExecuteNonQuery();
+ }
+ }
+
+ private void RollbackTx(System.Data.SQLite.SQLiteConnection conn)
+ {
+ try
+ {
+ using (var cmd = conn.CreateCommand())
+ {
+ cmd.CommandText = "ROLLBACK";
+ cmd.ExecuteNonQuery();
+ }
+ }
+ catch { }
+ }
+
+ private System.Data.SQLite.SQLiteConnection Open()
+ {
+ var conn = new System.Data.SQLite.SQLiteConnection(
+ $"Data Source={_dbPath};Version=3;");
+ conn.Open();
+ using (var cmd = conn.CreateCommand())
+ {
+ cmd.CommandText = "PRAGMA journal_mode=WAL;";
+ cmd.ExecuteNonQuery();
+ cmd.CommandText = "PRAGMA busy_timeout=3000;";
+ cmd.ExecuteNonQuery();
+ cmd.CommandText = "PRAGMA synchronous=FULL;";
+ cmd.ExecuteNonQuery();
+ }
+ return conn;
+ }
+
+ private void InitializeSchema()
+ {
+ using (var conn = Open())
+ {
+ BeginImmediateTx(conn);
+ try
+ {
+ Execute(conn, @"
+ CREATE TABLE IF NOT EXISTS authority_meta(
+ AuthorityId TEXT PRIMARY KEY, Epoch INTEGER NOT NULL,
+ BootId TEXT NOT NULL, SchemaVersion INTEGER NOT NULL)");
+ Execute(conn, @"
+ CREATE TABLE IF NOT EXISTS resource_state(
+ ResourceKey TEXT PRIMARY KEY,
+ Generation INTEGER NOT NULL DEFAULT 0,
+ Phase INTEGER NOT NULL DEFAULT 0,
+ LeaseId TEXT, OwnerId TEXT, OwnerType INTEGER,
+ ProcessInstanceId TEXT, MachineName TEXT, HostProcessId INTEGER,
+ Epoch INTEGER NOT NULL DEFAULT 0, Reason TEXT,
+ AcquiredUtcMs INTEGER, ExpiresUtcMs INTEGER, UpdatedUtcMs INTEGER)");
+ Execute(conn, @"
+ CREATE TABLE IF NOT EXISTS lease_history(
+ EventId INTEGER PRIMARY KEY AUTOINCREMENT,
+ ResourceKey TEXT NOT NULL, Generation INTEGER NOT NULL,
+ Epoch INTEGER NOT NULL, LeaseId TEXT, OwnerId TEXT,
+ EventType TEXT NOT NULL, Reason TEXT, UtcMs INTEGER NOT NULL)");
+ Execute(conn, @"
+ CREATE TABLE IF NOT EXISTS recovery_checkpoint(
+ ResourceKey TEXT PRIMARY KEY, Epoch INTEGER NOT NULL,
+ Status INTEGER NOT NULL,
+ PhysicalReadbackJson TEXT NOT NULL, ApprovedBy TEXT,
+ EvidenceUtcMs INTEGER NOT NULL)");
+ CommitTx(conn);
+ }
+ catch
+ {
+ RollbackTx(conn);
+ throw;
+ }
+ }
+ }
+
+ private long ReadOrInitEpoch()
+ {
+ using (var conn = Open())
+ {
+ long nowMs = NowMs();
+ var row = QueryRow(conn,
+ "SELECT Epoch, BootId FROM authority_meta WHERE AuthorityId=?",
+ _authorityId);
+ if (row == null)
+ {
+ Execute(conn, "INSERT INTO authority_meta VALUES(?,?,?,1)",
+ _authorityId, 1L, Guid.NewGuid().ToString("N"));
+ return 1;
+ }
+ long oldEpoch = Convert.ToInt64(row["Epoch"]);
+ long newEpoch = oldEpoch + 1;
+ Execute(conn,
+ "UPDATE authority_meta SET Epoch=?, BootId=? WHERE AuthorityId=?",
+ newEpoch, Guid.NewGuid().ToString("N"), _authorityId);
+ Execute(conn,
+ @"INSERT INTO lease_history(ResourceKey,Generation,Epoch,LeaseId,OwnerId,EventType,Reason,UtcMs)
+ VALUES('*',0,?,'','system','EpochBump','open-authority',?)",
+ newEpoch, nowMs);
+ return newEpoch;
+ }
+ }
+
+ // ================= ILeaseCoordinator =================
+
+ public AcquireOutcome Acquire(AcquireRequest request)
+ {
+ if (request.ResourceKeys.Length == 0)
+ throw new ArgumentException("ResourceKeys 涓嶈兘涓虹┖");
+ var keys = request.ResourceKeys
+ .Select(NormKey)
+ .OrderBy(k => k, StringComparer.Ordinal)
+ .Distinct()
+ .ToArray();
+
+ var deadline = NowMs() + Math.Max(0, request.WaitTimeoutMs);
+ while (true)
+ {
+ var outcome = TryAcquireOnce(keys, request);
+ if (outcome.Code != AcquireResultCode.Timeout) return outcome;
+ if (NowMs() >= deadline) { outcome.Code = AcquireResultCode.Timeout; return outcome; }
+ System.Threading.Thread.Sleep(50);
+ }
+ }
+
+ private AcquireOutcome TryAcquireOnce(string[] keys, AcquireRequest req)
+ {
+ long now = NowMs();
+ using (var conn = Open())
+ {
+ BeginImmediateTx(conn);
+ try
+ {
+ var grants = new List<LeaseGrant>(keys.Length);
+ foreach (var key in keys)
+ {
+ var st = QueryRow(conn,
+ "SELECT * FROM resource_state WHERE ResourceKey=?", key);
+
+ // RecoveryRequired 鈫� 闃绘鑾峰彇
+ if (st != null &&
+ Convert.ToInt32(st["Phase"]) == (int)LeasePhase.RecoveryRequired)
+ {
+ RollbackTx(conn);
+ return new AcquireOutcome
+ {
+ Code = AcquireResultCode.RecoveryBlocked,
+ RecoveryCheckpointKey = key
+ };
+ }
+
+ // Active 涓旀湭杩囨湡 鈫� 鍐茬獊
+ bool activeHeld = st != null
+ && Convert.ToInt32(st["Phase"]) == (int)LeasePhase.Active
+ && Convert.ToInt64(st["ExpiresUtcMs"]) > now;
+ if (activeHeld)
+ {
+ RollbackTx(conn);
+ return new AcquireOutcome
+ {
+ Code = AcquireResultCode.ConflictHeld,
+ Holder = new ConflictInfo
+ {
+ OwnerId = st["OwnerId"]?.ToString() ?? "",
+ OwnerType = (OwnerTypeV3)Convert.ToInt32(st["OwnerType"]),
+ MachineName = st["MachineName"]?.ToString() ?? "",
+ HostProcessId = Convert.ToInt32(st["HostProcessId"]),
+ Reason = st["Reason"]?.ToString() ?? "",
+ ExpiresUtcMs = Convert.ToInt64(st["ExpiresUtcMs"])
+ }
+ };
+ }
+
+ // Active 浣嗗凡杩囨湡锛堝皻鏈 Sweep锛夆啋 闃绘鑾峰彇锛岃姹傚厛瀹屾垚 Recovery
+ bool expiredNotSwept = st != null
+ && Convert.ToInt32(st["Phase"]) == (int)LeasePhase.Active
+ && Convert.ToInt64(st["ExpiresUtcMs"]) <= now;
+ if (expiredNotSwept)
+ {
+ RollbackTx(conn);
+ return new AcquireOutcome
+ {
+ Code = AcquireResultCode.RecoveryBlocked,
+ RecoveryCheckpointKey = key
+ };
+ }
+
+ long gen = (st != null ? Convert.ToInt64(st["Generation"]) : 0) + 1;
+ string leaseId = "lease_" + Guid.NewGuid().ToString("N").Substring(0, 12);
+ long exp = now + req.TtlSeconds * 1000L;
+
+ if (st == null)
+ {
+ Execute(conn,
+ @"INSERT INTO resource_state(ResourceKey,Generation,Phase,LeaseId,OwnerId,
+ OwnerType,ProcessInstanceId,MachineName,HostProcessId,Epoch,Reason,
+ AcquiredUtcMs,ExpiresUtcMs,UpdatedUtcMs)
+ VALUES(?,?,1,?,?,?,?,?,?,?,?,?,?,?)",
+ key, gen, leaseId, req.OwnerId, (int)req.OwnerType,
+ req.ProcessInstanceId, req.MachineName, req.HostProcessId,
+ CurrentEpoch, req.Reason, now, exp, now);
+ }
+ else
+ {
+ Execute(conn,
+ @"UPDATE resource_state SET Generation=?, Phase=1, LeaseId=?, OwnerId=?,
+ OwnerType=?, ProcessInstanceId=?, MachineName=?, HostProcessId=?,
+ Epoch=?, Reason=?, AcquiredUtcMs=?, ExpiresUtcMs=?, UpdatedUtcMs=?
+ WHERE ResourceKey=?",
+ gen, leaseId, req.OwnerId, (int)req.OwnerType, req.ProcessInstanceId,
+ req.MachineName, req.HostProcessId, CurrentEpoch, req.Reason,
+ now, exp, now, key);
+ }
+
+ Execute(conn,
+ @"INSERT INTO lease_history(ResourceKey,Generation,Epoch,LeaseId,OwnerId,EventType,Reason,UtcMs)
+ VALUES(?,?,?,?,?,'Acquire',?,?)",
+ key, gen, CurrentEpoch, leaseId, req.OwnerId, req.Reason, now);
+
+ grants.Add(new LeaseGrant(_authorityId, key, leaseId, req.OwnerId,
+ req.OwnerType, CurrentEpoch, gen, exp));
+ }
+ CommitTx(conn);
+ return new AcquireOutcome { Code = AcquireResultCode.Acquired, Grants = grants.ToArray() };
+ }
+ catch
+ {
+ RollbackTx(conn);
+ throw;
+ }
+ }
+ }
+
+ public bool Renew(LeaseGrant grant, int extendSeconds)
+ {
+ long now = NowMs();
+ using (var conn = Open())
+ {
+ BeginImmediateTx(conn);
+ try
+ {
+ int n = Execute(conn,
+ @"UPDATE resource_state SET ExpiresUtcMs=?, UpdatedUtcMs=?
+ WHERE ResourceKey=? AND LeaseId=? AND Generation=? AND Epoch=?
+ AND Phase=1 AND ExpiresUtcMs>?",
+ now + extendSeconds * 1000L, now,
+ NormKey(grant.ResourceKey), grant.LeaseId, grant.Generation, grant.Epoch, now);
+ if (n == 1)
+ CommitTx(conn);
+ else
+ {
+ RollbackTx(conn);
+ Log.Warn($"[Lease] 缁湡澶辫触(鏍呮爮涓嶇): {grant.ResourceKey} gen={grant.Generation}");
+ }
+ return n == 1;
+ }
+ catch
+ {
+ RollbackTx(conn);
+ throw;
+ }
+ }
+ }
+
+ public bool Release(LeaseGrant grant, string note)
+ {
+ long now = NowMs();
+ using (var conn = Open())
+ {
+ BeginImmediateTx(conn);
+ try
+ {
+ int n = Execute(conn,
+ @"UPDATE resource_state SET Phase=0, LeaseId=NULL, OwnerId=NULL,
+ ExpiresUtcMs=0, UpdatedUtcMs=?
+ WHERE ResourceKey=? AND LeaseId=? AND Generation=?",
+ now, NormKey(grant.ResourceKey), grant.LeaseId, grant.Generation);
+ Execute(conn,
+ @"INSERT INTO lease_history(ResourceKey,Generation,Epoch,LeaseId,OwnerId,EventType,Reason,UtcMs)
+ SELECT ?,?,?,?,?, 'Release', ?, ? WHERE ?=1",
+ NormKey(grant.ResourceKey), grant.Generation, grant.Epoch, grant.LeaseId,
+ grant.OwnerId, note, now, n);
+ if (n == 1) CommitTx(conn); else RollbackTx(conn);
+ return n == 1;
+ }
+ catch
+ {
+ RollbackTx(conn);
+ throw;
+ }
+ }
+ }
+
+ public ConflictInfo ProbeConflict(string resourceKey)
+ {
+ long now = NowMs();
+ using (var conn = Open())
+ {
+ var st = QueryRow(conn,
+ "SELECT * FROM resource_state WHERE ResourceKey=? AND Phase=1 AND ExpiresUtcMs>?",
+ NormKey(resourceKey), now);
+ if (st == null) return null;
+ return new ConflictInfo
+ {
+ OwnerId = st["OwnerId"]?.ToString() ?? "",
+ OwnerType = (OwnerTypeV3)Convert.ToInt32(st["OwnerType"]),
+ MachineName = st["MachineName"]?.ToString() ?? "",
+ HostProcessId = Convert.ToInt32(st["HostProcessId"]),
+ Reason = st["Reason"]?.ToString() ?? "",
+ ExpiresUtcMs = Convert.ToInt64(st["ExpiresUtcMs"])
+ };
+ }
+ }
+
+ public LeasePhase PhaseOf(string resourceKey)
+ {
+ using (var conn = Open())
+ {
+ var st = QueryRow(conn,
+ "SELECT * FROM resource_state WHERE ResourceKey=?", NormKey(resourceKey));
+ if (st == null) return LeasePhase.Free;
+ int phase = Convert.ToInt32(st["Phase"]);
+ if (phase == (int)LeasePhase.RecoveryRequired) return LeasePhase.RecoveryRequired;
+ if (phase == (int)LeasePhase.Active && Convert.ToInt64(st["ExpiresUtcMs"]) <= NowMs())
+ return LeasePhase.RecoveryRequired;
+ return (LeasePhase)phase;
+ }
+ }
+
+ public int SweepExpired()
+ {
+ long now = NowMs();
+ using (var conn = Open())
+ {
+ BeginImmediateTx(conn);
+ try
+ {
+ var rows = QueryRows(conn,
+ "SELECT * FROM resource_state WHERE Phase=1 AND ExpiresUtcMs<=?", now);
+ int count = 0;
+ foreach (var st in rows)
+ {
+ string key = st["ResourceKey"]?.ToString() ?? "";
+ long gen = Convert.ToInt64(st["Generation"]);
+ long epoch = Convert.ToInt64(st["Epoch"]);
+ string leaseId = st["LeaseId"]?.ToString() ?? "";
+ string ownerId = st["OwnerId"]?.ToString() ?? "";
+ Execute(conn,
+ "UPDATE resource_state SET Phase=2, UpdatedUtcMs=? WHERE ResourceKey=?",
+ now, key);
+ Execute(conn,
+ @"INSERT INTO lease_history(ResourceKey,Generation,Epoch,LeaseId,OwnerId,EventType,Reason,UtcMs)
+ VALUES(?,?,?,?,?,'ExpiredToRecovery','ttl-missed',?)",
+ key, gen, epoch, leaseId, ownerId, now);
+ Execute(conn,
+ @"INSERT OR REPLACE INTO recovery_checkpoint(ResourceKey,Epoch,Status,PhysicalReadbackJson,ApprovedBy,EvidenceUtcMs)
+ VALUES(?,?,0,'{}',NULL,?)", key, epoch, now);
+ count++;
+ }
+ CommitTx(conn);
+ return count;
+ }
+ catch
+ {
+ RollbackTx(conn);
+ throw;
+ }
+ }
+ }
+
+ public bool ApproveRecovery(string resourceKey, string readbackJson, string approvedBy)
+ {
+ long now = NowMs();
+ using (var conn = Open())
+ {
+ BeginImmediateTx(conn);
+ try
+ {
+ Execute(conn,
+ @"INSERT OR REPLACE INTO recovery_checkpoint(ResourceKey,Epoch,Status,PhysicalReadbackJson,ApprovedBy,EvidenceUtcMs)
+ VALUES(?,?,1,?,?,?)",
+ NormKey(resourceKey), CurrentEpoch, readbackJson, approvedBy, now);
+ int n = Execute(conn,
+ @"UPDATE resource_state SET Phase=0, LeaseId=NULL, OwnerId=NULL, UpdatedUtcMs=?
+ WHERE ResourceKey=? AND Phase=2",
+ now, NormKey(resourceKey));
+ Execute(conn,
+ @"INSERT INTO lease_history(ResourceKey,Generation,Epoch,LeaseId,OwnerId,EventType,Reason,UtcMs)
+ SELECT ResourceKey,Generation,Epoch,'','system','RecoveryApproved',?,? FROM resource_state WHERE ResourceKey=?",
+ approvedBy, now, NormKey(resourceKey));
+ CommitTx(conn);
+ return n == 1;
+ }
+ catch
+ {
+ RollbackTx(conn);
+ throw;
+ }
+ }
+ }
+
+ /// <summary>褰撳墠澶勪簬寰呭洖鏀剁浉浣嶇殑璧勬簮閿紙鍚繃鏈熸湭鎵殑 Active锛夈��</summary>
+ public string[] GetRecoveryRequiredKeys()
+ {
+ using (var conn = Open())
+ {
+ var rows = QueryRows(conn,
+ "SELECT ResourceKey FROM resource_state WHERE Phase=2 OR (Phase=1 AND ExpiresUtcMs<=?)",
+ NowMs());
+ return rows.Select(st => st["ResourceKey"]?.ToString() ?? "")
+ .Where(k => k.Length > 0).ToArray();
+ }
+ }
+
+ /// <summary>
+ /// 鍚姩瀵硅处锛堝涓昏韩浠芥洿鏇匡級锛氳繘绋嬮噸鍚悗鏃� owner 杩涚▼宸蹭笉瀛樺湪锛屽叾鐗╃悊 I/O 闅忚繘绋嬭嚜鐒堕殧绂伙紝
+ /// 鎸� INV-03 娴佺▼瀹屾垚鍥炴敹锛氬厛 SweepExpired 杩囨湡杞緟鍥炴敹锛屽啀鐧昏璇诲洖璇佹嵁骞剁疆 Free銆�
+ /// 杩欐槸 MVP 鍗曟満褰㈡�佺殑鑷姩鍖栧舰寮忥紙鍚姩鏂瑰嵆瀹夸富鑷韩锛夛紱鐢熶骇澶氬涓诲簲鏀逛负浜哄伐瀹℃壒 UI锛圥2锛夈��
+ /// </summary>
+ /// <returns>鏈鍥炴敹鐨勮祫婧愭暟</returns>
+ public int ReconcileAfterBoot(string hostIdentity)
+ {
+ int swept = SweepExpired();
+ int approved = 0;
+ foreach (var key in GetRecoveryRequiredKeys())
+ {
+ var evidence = "{\"probe\":\"boot-reconcile\",\"host\":\"" + hostIdentity + "\"}";
+ if (ApproveRecovery(key, evidence, "boot-recovery:" + hostIdentity))
+ approved++;
+ }
+ if (swept > 0 || approved > 0)
+ Log.Info($"[DbLeaseStore] 鍚姩瀵硅处瀹屾垚: swept={swept}, approved={approved} (host={hostIdentity})");
+ return approved;
+ }
+
+ public void BumpEpochForTest(string reason)
+ {
+ using (var conn = Open())
+ {
+ BeginImmediateTx(conn);
+ try
+ {
+ // 鎻愬崌 authority_meta 涓殑 epoch
+ Execute(conn,
+ "UPDATE authority_meta SET Epoch=Epoch+1, BootId=? WHERE AuthorityId=?",
+ Guid.NewGuid().ToString("N"), _authorityId);
+ // 鍦ㄤ簨鍔″唴璇诲彇鏂� epoch锛岀敤浜庡悓姝ユ洿鏂� resource_state
+ var row = QueryRow(conn,
+ "SELECT Epoch FROM authority_meta WHERE AuthorityId=?", _authorityId);
+ long newEpoch = row != null ? Convert.ToInt64(row["Epoch"]) : CurrentEpoch;
+ // 鍚屾鎻愬崌 resource_state 涓墍鏈夋椿璺冭鐨� Epoch锛屼娇鏃� Grant 鐨� Epoch 瀛楁澶辨晥
+ Execute(conn,
+ "UPDATE resource_state SET Epoch=? WHERE Phase=1",
+ newEpoch);
+ Execute(conn,
+ @"INSERT INTO lease_history(ResourceKey,Generation,Epoch,LeaseId,OwnerId,EventType,Reason,UtcMs)
+ VALUES('*',0,?,'','system','EpochBump',?,?)",
+ newEpoch, reason, NowMs());
+ CommitTx(conn);
+ // 璧嬪�兼斁鍦� Commit 涔嬪悗锛屼繚璇� DB 涓庡唴瀛� epoch 涓�鑷�
+ CurrentEpoch = newEpoch;
+ Log.Info($"[Lease] epoch 鎻愬崌鑷� {CurrentEpoch}: {reason}");
+ }
+ catch
+ {
+ RollbackTx(conn);
+ throw;
+ }
+ }
+ }
+
+ // ================= 杈呭姪 =================
+
+ public static string NormKey(string key) =>
+ key.Trim().ToLowerInvariant().Replace(' ', '-');
+
+ private static long NowMs() =>
+ DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+
+ private static int Execute(System.Data.SQLite.SQLiteConnection conn, string sql,
+ params object[] args)
+ {
+ using (var cmd = conn.CreateCommand())
+ {
+ cmd.CommandText = sql;
+ foreach (var arg in args)
+ cmd.Parameters.Add(new System.Data.SQLite.SQLiteParameter { Value = arg });
+ return cmd.ExecuteNonQuery();
+ }
+ }
+
+ private static Dictionary<string, object> QueryRow(
+ System.Data.SQLite.SQLiteConnection conn, string sql, params object[] args)
+ {
+ using (var cmd = conn.CreateCommand())
+ {
+ cmd.CommandText = sql;
+ foreach (var arg in args)
+ cmd.Parameters.Add(new System.Data.SQLite.SQLiteParameter { Value = arg });
+ using (var reader = cmd.ExecuteReader())
+ {
+ if (!reader.Read()) return null;
+ var row = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ for (int i = 0; i < reader.FieldCount; i++)
+ row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
+ return row;
+ }
+ }
+ }
+
+ private static List<Dictionary<string, object>> QueryRows(
+ System.Data.SQLite.SQLiteConnection conn, string sql, params object[] args)
+ {
+ using (var cmd = conn.CreateCommand())
+ {
+ cmd.CommandText = sql;
+ foreach (var arg in args)
+ cmd.Parameters.Add(new System.Data.SQLite.SQLiteParameter { Value = arg });
+ using (var reader = cmd.ExecuteReader())
+ {
+ var rows = new List<Dictionary<string, object>>();
+ while (reader.Read())
+ {
+ var row = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ for (int i = 0; i < reader.FieldCount; i++)
+ row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i);
+ rows.Add(row);
+ }
+ return rows;
+ }
+ }
+ }
+ }
+}
--
Gitblit v1.9.1