Skip to content

Commit 068a19c

Browse files
committed
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.
1 parent 6c8e098 commit 068a19c

3 files changed

Lines changed: 80 additions & 1 deletion

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
using SQLite.CodeFirst.Utility;
5+
6+
namespace SQLite.CodeFirst.Test.UnitTests.Utility
7+
{
8+
[TestClass]
9+
public class HistoryRecordSelectorTest
10+
{
11+
private static IHistory Record(string context)
12+
{
13+
return new History { Context = context, Hash = "hash-" + context };
14+
}
15+
16+
[TestMethod]
17+
public void SelectForContext_ReturnsRecordForGivenContext()
18+
{
19+
var records = new List<IHistory> { Record("ContextA"), Record("ContextB") };
20+
21+
IHistory result = HistoryRecordSelector.SelectForContext(records, "ContextB");
22+
23+
Assert.IsNotNull(result);
24+
Assert.AreEqual("ContextB", result.Context);
25+
}
26+
27+
[TestMethod]
28+
public void SelectForContext_ReturnsNull_WhenNoRecordMatches()
29+
{
30+
var records = new List<IHistory> { Record("ContextA") };
31+
32+
IHistory result = HistoryRecordSelector.SelectForContext(records, "ContextB");
33+
34+
Assert.IsNull(result);
35+
}
36+
37+
[TestMethod]
38+
public void SelectForContext_ReturnsNull_WhenEmpty()
39+
{
40+
IHistory result = HistoryRecordSelector.SelectForContext(new List<IHistory>(), "ContextA");
41+
42+
Assert.IsNull(result);
43+
}
44+
45+
[TestMethod]
46+
public void SelectForContext_Throws_WhenMultipleRecordsShareContext()
47+
{
48+
// One record per context is an invariant maintained by SaveHistory. If it is ever
49+
// violated, surfacing it is better than silently picking an arbitrary record.
50+
var records = new List<IHistory> { Record("ContextA"), Record("ContextA") };
51+
52+
Assert.ThrowsExactly<InvalidOperationException>(
53+
() => HistoryRecordSelector.SelectForContext(records, "ContextA"));
54+
}
55+
}
56+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace SQLite.CodeFirst.Utility
5+
{
6+
/// <summary>
7+
/// Selects the history record that belongs to a specific context from the history table.
8+
/// A database can be shared by multiple contexts, so the lookup must be scoped by the context
9+
/// key. Selecting without that scope would return more than one record on a shared database and
10+
/// make the underlying <see cref="Enumerable.SingleOrDefault{TSource}(IEnumerable{TSource})"/> throw.
11+
/// </summary>
12+
internal static class HistoryRecordSelector
13+
{
14+
public static IHistory SelectForContext(IEnumerable<IHistory> records, string contextKey)
15+
{
16+
return records.SingleOrDefault(record => record.Context == contextKey);
17+
}
18+
}
19+
}

SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,11 @@ private IHistory GetHistoryRecord(TContext context)
175175
// in order to be supported by .NET 4.0.
176176
DbQuery dbQuery = context.Set(historyEntityType).AsNoTracking();
177177
IEnumerable<IHistory> records = Enumerable.Cast<IHistory>(dbQuery);
178-
return records.SingleOrDefault();
178+
179+
// A database can be shared by several contexts, so the record must be looked up by the
180+
// context key that SaveHistory writes. An unscoped lookup would match every context's
181+
// record and throw on a shared history table.
182+
return HistoryRecordSelector.SelectForContext(records, context.GetType().FullName);
179183
}
180184

181185
private string GetHashFromModel(DbConnection connection)

0 commit comments

Comments
 (0)