From c2140406bb49ca7298414ae62319aac5043a19f4 Mon Sep 17 00:00:00 2001 From: Marc Sallin Date: Tue, 16 Jun 2026 02:49:32 +0200 Subject: [PATCH] fix: scope history record lookup by context key SqliteDropCreateDatabaseWhenModelChanges stores the context name on each history record and documents that a database may be shared by several contexts, but GetHistoryRecord selected with SingleOrDefault and no Context filter. On a shared history table that matches multiple records: IsSameModel swallows the resulting exception and reports a model change, while SaveHistory throws. Look the record up by context.GetType().FullName, matching what SaveHistory writes. --- .../Utility/HistoryRecordSelectorTest.cs | 56 +++++++++++++++++++ .../Internal/Utility/HistoryRecordSelector.cs | 19 +++++++ ...qliteDropCreateDatabaseWhenModelChanges.cs | 6 +- 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 SQLite.CodeFirst.Test/UnitTests/Utility/HistoryRecordSelectorTest.cs create mode 100644 SQLite.CodeFirst/Internal/Utility/HistoryRecordSelector.cs diff --git a/SQLite.CodeFirst.Test/UnitTests/Utility/HistoryRecordSelectorTest.cs b/SQLite.CodeFirst.Test/UnitTests/Utility/HistoryRecordSelectorTest.cs new file mode 100644 index 0000000..b53a240 --- /dev/null +++ b/SQLite.CodeFirst.Test/UnitTests/Utility/HistoryRecordSelectorTest.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SQLite.CodeFirst.Utility; + +namespace SQLite.CodeFirst.Test.UnitTests.Utility +{ + [TestClass] + public class HistoryRecordSelectorTest + { + private static IHistory Record(string context) + { + return new History { Context = context, Hash = "hash-" + context }; + } + + [TestMethod] + public void SelectForContext_ReturnsRecordForGivenContext() + { + var records = new List { Record("ContextA"), Record("ContextB") }; + + IHistory result = HistoryRecordSelector.SelectForContext(records, "ContextB"); + + Assert.IsNotNull(result); + Assert.AreEqual("ContextB", result.Context); + } + + [TestMethod] + public void SelectForContext_ReturnsNull_WhenNoRecordMatches() + { + var records = new List { Record("ContextA") }; + + IHistory result = HistoryRecordSelector.SelectForContext(records, "ContextB"); + + Assert.IsNull(result); + } + + [TestMethod] + public void SelectForContext_ReturnsNull_WhenEmpty() + { + IHistory result = HistoryRecordSelector.SelectForContext(new List(), "ContextA"); + + Assert.IsNull(result); + } + + [TestMethod] + public void SelectForContext_Throws_WhenMultipleRecordsShareContext() + { + // One record per context is an invariant maintained by SaveHistory. If it is ever + // violated, surfacing it is better than silently picking an arbitrary record. + var records = new List { Record("ContextA"), Record("ContextA") }; + + Assert.ThrowsExactly( + () => HistoryRecordSelector.SelectForContext(records, "ContextA")); + } + } +} diff --git a/SQLite.CodeFirst/Internal/Utility/HistoryRecordSelector.cs b/SQLite.CodeFirst/Internal/Utility/HistoryRecordSelector.cs new file mode 100644 index 0000000..9621370 --- /dev/null +++ b/SQLite.CodeFirst/Internal/Utility/HistoryRecordSelector.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SQLite.CodeFirst.Utility +{ + /// + /// Selects the history record that belongs to a specific context from the history table. + /// A database can be shared by multiple contexts, so the lookup must be scoped by the context + /// key. Selecting without that scope would return more than one record on a shared database and + /// make the underlying throw. + /// + internal static class HistoryRecordSelector + { + public static IHistory SelectForContext(IEnumerable records, string contextKey) + { + return records.SingleOrDefault(record => record.Context == contextKey); + } + } +} diff --git a/SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs b/SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs index 7e6d834..dbe050c 100644 --- a/SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs +++ b/SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs @@ -175,7 +175,11 @@ private IHistory GetHistoryRecord(TContext context) // in order to be supported by .NET 4.0. DbQuery dbQuery = context.Set(historyEntityType).AsNoTracking(); IEnumerable records = Enumerable.Cast(dbQuery); - return records.SingleOrDefault(); + + // A database can be shared by several contexts, so the record must be looked up by the + // context key that SaveHistory writes. An unscoped lookup would match every context's + // record and throw on a shared history table. + return HistoryRecordSelector.SelectForContext(records, context.GetType().FullName); } private string GetHashFromModel(DbConnection connection)