diff --git a/src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs b/src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs index 4dd7e85fced..f2fb6f699ab 100644 --- a/src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs +++ b/src/NHibernate.Test/Async/Extralazy/ExtraLazyFixture.cs @@ -8,10 +8,13 @@ //------------------------------------------------------------------------------ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using NHibernate.Cfg; using NUnit.Framework; +using NUnit.Framework.Constraints; namespace NHibernate.Test.Extralazy { @@ -34,13 +37,2212 @@ protected override string CacheConcurrencyStrategy get { return null; } } + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Cfg.Environment.GenerateStatistics, "true"); + } + protected override void OnTearDown() { using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - s.Delete("from System.Object"); - t.Commit(); + s.Delete("from System.Object"); + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListAddAsync(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Gavin's companies count after get"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after companies count"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after count"); + + // Test adding companies with ICollection interface + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Test adding companies with IList interface + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + // Returned value is not valid with lazy list, no check for it. + ((IList) gavin.Companies).Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Gavin's companies count after adding through IList"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through IList"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding through IList"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding through IList"); + + // Check existence of added companies + Sfi.Statistics.Clear(); + // Have to skip unloaded (non-queued indeed) elements to avoid triggering existence queries on them. + foreach (var item in addedItems.Skip(5)) + { + Assert.That(gavin.Companies.Contains(item), Is.True, "Company '{0}' existence", item.Name); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of non-flushed"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after checking existence of non-flushed"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after checking existence of non-flushed"); + + // Check existence of not loaded companies + Assert.That(gavin.Companies.Contains(addedItems[0]), Is.True, "First company existence"); + Assert.That(gavin.Companies.Contains(addedItems[1]), Is.True, "Second company existence"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of unloaded"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking existence of unloaded"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after checking existence of unloaded"); + + // Check existence of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False, "First non-existent test"); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False, "Second non-existent test"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking non-existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking non-existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after checking non-existence"); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [Explicit] + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public async Task ListAddDuplicatedAsync(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count before flush"); + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id)); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after get"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statement count after count"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after get"); + + // Re-add items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + gavin.Companies.Add(addedItems[i]); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statement count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after re-adding"); + + if (flush) + { + await (s.FlushAsync()); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after second flush"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after second flush"); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumeration"); + Assert.That(gavin.Companies.Count, Is.EqualTo(flush ? 5 : 10), "Companies count after enumeration"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after loading Gavin again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after loading Gavin again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListInsertAsync(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after get"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after get"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after get"); + + // Test inserting companies at the start + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(0, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after insert"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after insert"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after insert"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after insert"); + + // Test inserting companies at the end + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after tail insert"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after tail insert"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after tail insert"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after tail insert"); + + // Try insert invalid indexes + Assert.Throws( + () => gavin.Companies.Insert(-1, new Company("c-1", -1, gavin)), "inserting at -1"); + Assert.Throws( + () => gavin.Companies.Insert(20, new Company("c20", 20, gavin)), "inserting too far"); + + // Check existence of added companies + Sfi.Statistics.Clear(); + // Have to skip unloaded (non-queued indeed) elements to avoid triggering existence queries on them. + foreach (var item in addedItems.Skip(5)) + { + Assert.That(gavin.Companies.Contains(item), Is.True, "Company '{0}' existence", item.Name); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after existence check"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after existence check"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after existence check"); + + // Check existence of not loaded companies + Assert.That(gavin.Companies.Contains(addedItems[0]), Is.True, "First company existence"); + Assert.That(gavin.Companies.Contains(addedItems[1]), Is.True, "Second company existence"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after unloaded existence check"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after unloaded existence check"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after unloaded existence check"); + + // Check existence of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False, "First non-existence test"); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False, "Second non-existence test"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after non-existence check"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after non-existence check"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after non-existence check"); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumeration"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after enumeration"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [Explicit] + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public async Task ListInsertDuplicatedAsync(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count before flush"); + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id)); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after get"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after count"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after count"); + + // Re-add items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + gavin.Companies.Insert(4 - i, addedItems[i]); + } + + Assert.That(gavin.Companies[0].ListIndex, Is.EqualTo(4), "Company at 0"); + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after re-insert"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-insert"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after re-insert"); + + if (flush) + { + await (s.FlushAsync()); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after flush"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after flush"); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumeration"); + Assert.That(gavin.Companies.Count, Is.EqualTo(flush ? 5 : 10), "Companies count after enumeration"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListRemoveAtAsync(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = new List {0, 1, 2, 6, 8, 9}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id)); + } + + // Add transient companies + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Remove transient companies + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(5); + gavin.Companies.RemoveAt(6); + + Assert.That(gavin.Companies.Count, Is.EqualTo(8), "Gavin's companies count after removing 2 transient companies"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing transient companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing transient companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing transient companies"); + + // Remove persisted companies + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(3); + gavin.Companies.RemoveAt(3); + + Assert.That(gavin.Companies.Count, Is.EqualTo(6), "Gavin's companies count after removing 2 persisted companies"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing persisted companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after removing persisted companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing persisted companies"); + + // Try remove invalid indexes + Assert.Throws(() => gavin.Companies.RemoveAt(-1), "Removing at -1"); + Assert.Throws(() => gavin.Companies.RemoveAt(8), "Removing too far"); + + // Check existence of companies + Sfi.Statistics.Clear(); + var removedIndexes = new HashSet {3, 4, 5, 7}; + for (var i = 0; i < addedItems.Count; i++) + { + Assert.That( + gavin.Companies.Contains(addedItems[i]), + removedIndexes.Contains(i) ? Is.False : (IResolveConstraint) Is.True, + $"Element at index {i} was {(removedIndexes.Contains(i) ? "not " : "")}removed"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Gavin's companies count after checking existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3), "Flushes count after checking existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization after checking existence"); + + // Check existence of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False, "Checking existence of non-existence"); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False, "Checking existence of non-existence"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Gavin's companies count after checking non-existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Flushes count after checking non-existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization after checking non-existence"); + + gavin.UpdateCompaniesIndexes(); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(6), "Companies count after enumerating"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Companies.Count, Is.EqualTo(6), "Companies count after loading again Gavin"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListGetSetAsync(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = new List {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id)); + } + + // Add transient companies + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Compare all items + Sfi.Statistics.Clear(); + for (var i = 0; i < 10; i++) + { + Assert.That(gavin.Companies[i], Is.EqualTo(addedItems[i]), "Comparing added company at index {0}", i); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding comparing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding comparing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after comparing"); + + // Try get invalid indexes + Assert.Throws(() => + { + var item = gavin.Companies[10]; + }, "Get too far"); + Assert.Throws(() => + { + var item = gavin.Companies[-1]; + }, "Get at -1"); + + // Try set invalid indexes + Assert.Throws(() => gavin.Companies[10] = addedItems[0], "Set too far"); + Assert.Throws(() => gavin.Companies[-1] = addedItems[0], "Set at -1"); + + // Swap transient and persisted indexes + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var hiIndex = 9 - i; + var tmp = gavin.Companies[i]; + gavin.Companies[i] = gavin.Companies[hiIndex]; + gavin.Companies[hiIndex] = tmp; + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after swapping"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after swapping"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(10), "Statements count after adding swapping"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after swapping"); + + // Check indexes + Sfi.Statistics.Clear(); + for (var i = 0; i < 10; i++) + { + Assert.That(gavin.Companies[i].ListIndex, Is.EqualTo(finalIndexOrder[i]), "Comparing company ListIndex at index {0}", i); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after comparing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after comparing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after comparing"); + + gavin.UpdateCompaniesIndexes(); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after enumerating"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after loading again Gavin"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListFlushAsync(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = Enumerable.Range(0, 13).ToList(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id)); + } + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Add transient companies with Insert + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(5), "Statements count after inserting"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Gavin's companies count after inserting 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after inserting"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after inserting"); + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 15; i < 20; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(20), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Remove last 5 transient companies + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 15; i < 20; i++) + { + Assert.That(gavin.Companies.Remove(addedItems[i]), Is.True, "Removing transient company at index {0}", i); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(10), "Statements count after removing"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Gavin's companies count after removing 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing"); + + // Remove last 5 transient companies + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 10; i < 15; i++) + { + gavin.Companies.RemoveAt(10); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "DELETE \n FROM"), Is.EqualTo(5), "Statements count after second removing"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after second removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after second removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(7), "Statements count after second removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after second removing"); + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems[i] = item; + // NOTE: the returned index is currently invalid due to extra-lazy avoiding to query the count or initializing the collection + ((IList) gavin.Companies).Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Gavin's companies count after adding through IList"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through IList"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding through IList"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding through IList"); + + // Remove last transient company + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + Assert.That(gavin.Companies.Remove(addedItems[14]), Is.EqualTo(true), "Removing last transient company"); + var log = sqlLog.GetWholeLog(); + Assert.That(FindAllOccurrences(log, "DELETE \n FROM"), Is.EqualTo(5), "Delete statements count after removing last transient company"); + Assert.That(FindAllOccurrences(log, "INSERT \n INTO"), Is.EqualTo(5), "Insert statements count after removing last transient company"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(14), "Gavin's companies count after adding removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing"); + + // Test index getter + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies[0], Is.EqualTo(addedItems[0]), "Comparing first item with index getter"); + + Assert.That(gavin.Companies.Count, Is.EqualTo(14), "Gavin's companies count after adding comparing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after comparing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3), "Statements count after comparing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after comparing"); + + // Remove last transient company + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Remove(addedItems[13]), Is.EqualTo(true), "Removing last transient company"); + + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Gavin's companies count after adding repeated removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after repeated removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after repeated removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after repeated removing"); + + // Test index setter + Sfi.Statistics.Clear(); + gavin.Companies[0] = addedItems[0]; + + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Gavin's companies count after setting first item"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after setting first item"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3), "Statements count after setting first item"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after setting first item"); + + // Test manual flush after remove + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(12); + using (var sqlLog = new SqlLogSpy()) + { + await (s.FlushAsync()); + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "DELETE \n FROM"), Is.EqualTo(1), "Delete statements count after removing at 12 index"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(12), "Gavin's companies count after removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing"); + + // Test manual flush after insert + Sfi.Statistics.Clear(); + gavin.Companies.Add(new Company("c12", 12, gavin)); + using (var sqlLog = new SqlLogSpy()) + { + await (s.FlushAsync()); + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(1), "Insert statements count after flushing"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Gavin's companies count after flushing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after flushing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after flushing"); + + for (var i = 0; i < gavin.Companies.Count; i++) + { + Assert.That(gavin.Companies[i].ListIndex, Is.EqualTo(i), "Comparing company ListIndex at index {0}", i); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Companies count after enumerating"); + Assert.That(gavin.Companies.Select(o => o.ListIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Companies count after loading again Gavin"); + Assert.That(gavin.Companies.Select(o => o.ListIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListClearAsync(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new CreditCard($"c{i}", i, gavin); + addedItems.Add(item); + gavin.CreditCards.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id)); + } + + var collection = gavin.CreditCards; + + // Add transient credit cards + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new CreditCard($"c{i}", i, gavin); + addedItems.Add(item); + collection.Insert(i, item); + } + + Assert.That(collection.Count, Is.EqualTo(10), "Gavin's credit cards count after inserting 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after inserting"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after inserting"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after inserting"); + + Sfi.Statistics.Clear(); + collection.Clear(); + + Assert.That(collection.Count, Is.EqualTo(0), "Gavin's credit cards count after clearing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after clearing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after clearing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after clearing"); + + // Re-add two not loaded and two transient credit cards + collection.Add(addedItems[0]); + collection.Add(addedItems[1]); + collection.Add(addedItems[5]); + collection.Add(addedItems[6]); + + Assert.That(collection.Count, Is.EqualTo(4), "Gavin's credit cards count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after re-adding"); + + // Remove one not loaded and one transient credit cards + Assert.That(collection.Remove(addedItems[1]), Is.True, "Removing not loaded credit card"); + Assert.That(collection.Remove(addedItems[6]), Is.True, "Removing transient credit card"); + + Assert.That(collection.Count, Is.EqualTo(2), "Gavin's credit cards count after removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after removing"); + + // Remove not existing items + Assert.That(collection.Remove(addedItems[1]), Is.False, "Removing not-existing credit card"); + Assert.That(collection.Remove(addedItems[6]), Is.False, "Removing not-existing credit card"); + + Assert.That(collection.Count, Is.EqualTo(2), "Gavin's credit cards count after not-existing removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after not-existing removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after not-existing removing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after not-existing removing"); + + if (initialize) + { + using (var e = collection.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.True, "Credit cards initialization status after enumerating"); + Assert.That(collection.Count, Is.EqualTo(2), "Credit cards count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + var collection = gavin.CreditCards; + // As the cascade option is set to all, the clear operation will only work on + // transient credit cards + Assert.That(collection.Count, Is.EqualTo(6), "Credit cards count after loading again Gavin"); + for (var i = 0; i < 10; i++) + { + Assert.That(collection.Contains(addedItems[i]), i < 6 ? Is.True : (IResolveConstraint) Is.False, "Checking existence for item at {0}", i); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task ListIndexOperationsAsync(bool initialize) + { + User gavin; + var finalIndexOrder = new List {6, 0, 4}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + gavin.Companies.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + // Current tracker state: + // Indexes: 0,1,2,3,4 + // Queue: / + // RemoveDbIndexes: / + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Gavin's companies count"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status"); + + Sfi.Statistics.Clear(); + gavin.Companies.Insert(1, new Company("c5", 5, gavin)); + // Current tracker state: + // Indexes: 0,5,1,2,3,4 + // Queue: {1, 5} + // RemoveDbIndexes: / + + gavin.Companies.Insert(0, new Company("c6", 6, gavin)); + // Current tracker state: + // Indexes: 6,0,5,1,2,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: / + + gavin.Companies.RemoveAt(4); + // Current tracker state: + // Indexes: 6,0,5,1,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 2 + + gavin.Companies.RemoveAt(3); + // Current tracker state: + // Indexes: 6,0,5,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 1,2 + + gavin.Companies.RemoveAt(3); + // Current tracker state: + // Indexes: 6,0,5,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 1,2,3 + + gavin.Companies.RemoveAt(2); + // Current tracker state: + // Indexes: 6,0,4 + // Queue: {0, 6} + // RemoveDbIndexes: 1,2,3 + + Assert.That(gavin.Companies.Count, Is.EqualTo(3), "Gavin's companies count after remove/insert operations"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after remove/insert operations"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3), "Statements count after remove/insert operations"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after remove/insert operations"); + + gavin.UpdateCompaniesIndexes(); + + for (var i = 0; i < gavin.Companies.Count; i++) + { + Assert.That(gavin.Companies[i].OriginalIndex, Is.EqualTo(finalIndexOrder[i]), "Comparing company index at {0}", i); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(3), "Companies count after enumerating"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Companies.Count, Is.EqualTo(3), "Companies count after loading again Gavin"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task SetAddAsync(bool initialize) + { + User gavin; + Document hia; + Document hia2; + var addedDocuments = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + hia = new Document("HiA", "blah blah blah", gavin); + hia2 = new Document("HiA2", "blah blah blah blah", gavin); + gavin.Documents.Add(hia); + gavin.Documents.Add(hia2); + await (s.PersistAsync(gavin)); + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(2), "Gavin's documents count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding"); + + // Test adding documents with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document{i}", $"content{i}", gavin); + addedDocuments.Add(document); + Assert.That(gavin.Documents.Add(document), Is.True, "Adding document through ISet"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(7), "Gavin's documents count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding"); + + // Test adding documents with ICollection interface + Sfi.Statistics.Clear(); + var documents = (ICollection) gavin.Documents; + for (var i = 0; i < 5; i++) + { + var document = new Document($"document2{i}", $"content{i}", gavin); + addedDocuments.Add(document); + documents.Add(document); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Gavin's documents count after adding through ICollection<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through ICollection<>"); + // In this case we cannot determine whether the entities are transient or not so + // we are forced to check the database + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding through ICollection<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding through ICollection<>"); + + // Test re-adding documents with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Add(addedDocuments[i]), Is.False, "Re-add document through ISet<>"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Gavin's documents count after re-adding"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after re-adding"); + + // Test re-adding documents with ICollection interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + documents.Add(addedDocuments[i]); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Gavin's documents count after re-adding through ICollection<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding through ICollection<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding through ICollection<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after re-adding through ICollection<>"); + + // Check existence of added documents + Sfi.Statistics.Clear(); + foreach (var document in addedDocuments) + { + Assert.That(gavin.Documents.Contains(document), Is.True, "Checking existence of an existing document"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after checking existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after checking existence"); + + // Check existence of not loaded documents + Assert.That(gavin.Documents.Contains(hia), Is.True, "Checking existence of not loaded document"); + Assert.That(gavin.Documents.Contains(hia2), Is.True, "Checking existence of not loaded document"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after checking existence"); + + // Check existence of not existing documents + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Contains(new Document("test1", "content", gavin)), Is.False, "Checking existence of not-existing document"); + Assert.That(gavin.Documents.Contains(new Document("test2", "content", gavin)), Is.False, "Checking existence of not-existing document"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking non-existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking non-existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after checking non-existence"); + + // Test adding not loaded documents + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Add(hia), Is.False, "Adding not loaded element"); + documents.Add(hia); + + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Gavin's documents count after adding not loaded element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding not loaded element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after adding not loaded element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding not loaded element"); + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True, "Documents initialization status after enumerating"); + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Documents count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Documents count after loading again Gavin"); + Assert.That(gavin.Documents.Contains(hia2), Is.True, "Checking not loaded element"); + Assert.That(gavin.Documents.Contains(hia), Is.True, "Checking not loaded element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public async Task SetAddDuplicatedAsync(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new Document($"d{i}", $"c{i}", gavin); + addedItems.Add(item); + gavin.Documents.Add(item); + gavin.Documents.Add(item); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after adding 5"); + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Title)); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after reload"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after reload"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after reload"); + + // Re-add items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Add(addedItems[i]), Is.False, "Re-adding element"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after re-adding"); + + if (flush) + { + await (s.FlushAsync()); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after flushing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after flushing"); + } + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True, "Documents initialization status after enumerating"); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Documents count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Documents count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task SetAddTransientAsync(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = await (s.GetAsync("gavin")); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Permissions.Count, Is.EqualTo(5), "Gavin's permissions count after adding 5"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after adding"); + + // Test adding permissions with ICollection interface + Sfi.Statistics.Clear(); + var items = (ICollection) gavin.Permissions; + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p2{i}", gavin); + addedItems.Add(item); + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10), "Gavin's permissions count after adding through ICollection<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through ICollection<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding through ICollection<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after adding through ICollection<>"); + + // Test re-adding permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(5)) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10), "Gavin's permissions count after re-adding"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding"); + + // Test adding not loaded permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Take(5)) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10), "Gavin's permissions count after re-adding not loaded elements"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding not loaded elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after re-adding not loaded elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding not loaded elements"); + + // Test adding loaded permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in s.Query()) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10), "Gavin's permissions count after re-adding loaded elements"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding loaded elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(6), "Statements count after re-adding loaded elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding loaded elements"); + + // Test adding permissions with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p3{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Gavin's permissions count after adding through ISet<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through ISet<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding through ISet<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after adding through ISet<>"); + + // Test re-adding permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(10)) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Gavin's permissions count after re-adding through ISet<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding through ISet<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding through ISet<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding through ISet<>"); + + // Test adding not loaded permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Take(5)) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Gavin's permissions count after re-adding not loaded permissions through ISet<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding not loaded permissions through ISet<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after re-adding not loaded permissions through ISet<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding not loaded permissions through ISet<>"); + + // Test adding loaded permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in s.Query()) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Gavin's permissions count after re-adding loaded permissions through ISet<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding loaded permissions through ISet<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(6), "Statements count after re-adding loaded permissions through ISet<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding loaded permissions through ISet<>"); + + if (initialize) + { + using (var e = gavin.Permissions.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.True, "Permissions initialization status after enumerating"); + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Permissions count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Permissions count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task SetRemoveAsync(bool initialize) + { + User gavin; + var addedDocuments = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document{i}", $"content{i}", gavin); + addedDocuments.Add(document); + gavin.Documents.Add(document); + } + + await (s.PersistAsync(gavin)); + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedDocuments[i] = await (s.GetAsync(addedDocuments[i].Title)); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after refresh"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after refresh"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after refresh"); + + // Add new documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document2{i}", $"content{i}", gavin); + addedDocuments.Add(document); + ((ICollection)gavin.Documents).Add(document); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(10), "Gavin's documents count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding"); + + // Test removing existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.True, "Removing existing document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing"); + + // Test removing removed existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.False, "Checking existence of a removed document"); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.False, "Removing removed document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after removing removed documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing removed documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing removed documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing removed documents"); + + // Test removing not existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"test{i}", "content", gavin); + Assert.That(gavin.Documents.Remove(document), Is.False, "Removing not existing document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after removing not existing documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing not existing documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after removing not existing documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing not existing documents"); + + // Test removing newly added documents + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.True, "Checking existence of an added document"); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.True, "Removing added document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Gavin's documents count after removing added documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing added documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing added documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing added documents"); + + // Test removing removed newly added documents + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.False, "Checking existence of a removed document"); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.False, "Removing removed document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Gavin's documents count after removing removed documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing removed documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing removed documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing removed documents"); + + // Test removing not existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"test{i}", "content", gavin); + Assert.That(gavin.Documents.Remove(document), Is.False, "Removing not existing document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Gavin's documents count after removing not existing documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing not existing documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after removing not existing documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing not existing documents"); + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True, "Documents initialization status after enumerating"); + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Documents count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Documents count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task SetClearAsync(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = await (s.GetAsync("gavin")); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = await (s.GetAsync(addedItems[i].Id)); + } + + var collection = gavin.Permissions; + + Sfi.Statistics.Clear(); + Assert.That(collection.Count, Is.EqualTo(5), "Gavin's permissions count after refresh"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after refresh"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after refresh"); + + // Add transient permissions + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p2{i}", gavin); + addedItems.Add(item); + collection.Add(item); + } + + Assert.That(collection.Count, Is.EqualTo(10), "Gavin's permissions count after adding 5"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after adding"); + + Sfi.Statistics.Clear(); + collection.Clear(); + + Assert.That(collection.Count, Is.EqualTo(0), "Gavin's permissions count after flushing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after flushing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after flushing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after flushing"); + + // Re-add two not loaded and two transient permissions + Assert.That(collection.Add(addedItems[0]), Is.True, "Re-adding not loaded element"); + Assert.That(collection.Add(addedItems[1]), Is.True, "Re-adding not loaded element"); + Assert.That(collection.Add(addedItems[5]), Is.True, "Re-adding transient element"); + Assert.That(collection.Add(addedItems[6]), Is.True, "Re-adding transient element"); + + Assert.That(collection.Count, Is.EqualTo(4), "Gavin's permissions count after re-adding"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after re-adding"); + + // Remove one not loaded and one transient permissions + Assert.That(collection.Remove(addedItems[1]), Is.True, "Removing not loaded element"); + Assert.That(collection.Remove(addedItems[6]), Is.True, "Removing transient element"); + + Assert.That(collection.Count, Is.EqualTo(2), "Gavin's permissions count after removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after removing"); + + // Remove not existing items + Assert.That(collection.Remove(addedItems[1]), Is.False, "Removing removed element"); + Assert.That(collection.Remove(addedItems[6]), Is.False, "Removing removed element"); + + Assert.That(collection.Count, Is.EqualTo(2), "Gavin's permissions count after removing removed elements"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing removed elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing removed elements"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after removing removed elements"); + + if (initialize) + { + using (var e = collection.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.True, "Permissions initialization status after enumerating"); + Assert.That(collection.Count, Is.EqualTo(2), "Permissions count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + var collection = gavin.Permissions; + // As the cascade option is set to all, the clear operation will only work on + // transient permissions + Assert.That(collection.Count, Is.EqualTo(6), "Permissions count after loading again Gavin"); + for (var i = 0; i < 10; i++) + { + Assert.That(collection.Contains(addedItems[i]), i < 6 ? Is.True : (IResolveConstraint) Is.False, + "Checking existence of added element at {0}", i); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task MapAddAsync(bool initialize) + { + User gavin; + UserSetting setting; + var addedSettings = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + addedSettings.Add(setting); + gavin.Settings.Add(setting.Name, setting); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5), "Gavin's user settings count after load"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after load"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after load"); + + // Test adding settings with Add method + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + addedSettings.Add(setting); + gavin.Settings.Add(setting.Name, setting); + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after adding"); + + // Test adding settings with [] + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s3{i}", $"data{i}", gavin); + addedSettings.Add(setting); + + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(15), "Gavin's user settings count after adding 5 through indexer"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through indexer"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding through indexer"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after adding through indexer"); + + // Check existence of added settings + Sfi.Statistics.Clear(); + foreach (var item in addedSettings.Skip(5)) + { + Assert.That(gavin.Settings.ContainsKey(item.Name), Is.True, "Checking existence of added element"); + Assert.That(gavin.Settings.Contains(new KeyValuePair(item.Name, item)), Is.True, "Checking existence of added element using KeyValuePair<,>"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of added elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after checking existence of added elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after checking existence of added elements"); + + // Check existence of not loaded settings + foreach (var item in addedSettings.Take(5)) + { + Assert.That(gavin.Settings.ContainsKey(item.Name), Is.True, "Checking key existence of not loaded elements"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of not loaded elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after checking existence of not loaded elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after checking existence of not loaded elements"); + + // Check existence of not existing settings + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.ContainsKey("test"), Is.False, "Checking existence of not existing element"); + Assert.That(gavin.Settings.ContainsKey("test2"), Is.False, "Checking existence of not existing element"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of not existing elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking existence of not existing elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after checking existence of not existing elements"); + + // Try to add an existing setting + Assert.Throws(() => gavin.Settings.Add("s0", new UserSetting("s0", "data", gavin)), "Adding an existing key"); + Assert.Throws(() => gavin.Settings.Add("s20", new UserSetting("s20", "data", gavin)), "Adding an existing key"); + Assert.Throws(() => gavin.Settings.Add("s30", new UserSetting("s30", "data", gavin)), "Adding an existing key"); + + // Get values of not loaded keys + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.TryGetValue("s0", out setting), Is.True, "Getting value of not loaded key"); + Assert.That(setting.Id, Is.EqualTo(addedSettings[0].Id), "Comparing retrieved element id"); + Assert.That(gavin.Settings["s0"].Id, Is.EqualTo(addedSettings[0].Id), "Comparing retrieved element id by indexer"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after reading elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after reading elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after reading elements"); + + // Get values of newly added keys + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.TryGetValue("s20", out setting), Is.True, "Getting value of a newly added key"); + Assert.That(setting, Is.EqualTo(addedSettings[5]), "Comparing retrieved element"); + Assert.That(gavin.Settings["s20"], Is.EqualTo(addedSettings[5]), "Comparing retrieved element by indexer"); + Assert.That(gavin.Settings.TryGetValue("s30", out setting), Is.True, "Getting value of a newly added key"); + Assert.That(setting, Is.EqualTo(addedSettings[10]), "Comparing retrieved element"); + Assert.That(gavin.Settings["s30"], Is.EqualTo(addedSettings[10]), "Getting value of a newly added key"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after reading elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after reading elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after reading elements"); + + // Try to get a non existing setting + Assert.That(gavin.Settings.TryGetValue("test", out setting), Is.False, "Try to get a not existing key"); + Assert.That(gavin.Settings.TryGetValue("test2", out setting), Is.False, "Try to get a not existing key"); + Assert.Throws(() => + { + setting = gavin.Settings["test"]; + }, "Getting a not existing key"); + Assert.Throws(() => + { + setting = gavin.Settings["test2"]; + }, "Getting a not existing key"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after reading not existing elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(4), "Statements count after reading not existing elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after reading not existing elements"); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True, "User settings initialization status after enumerating"); + Assert.That(gavin.Settings.Count, Is.EqualTo(15), "User settings count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Settings.Count, Is.EqualTo(15), "User settings count after loading again Gavin"); + Assert.That(gavin.Settings.ContainsKey(addedSettings[0].Name), Is.True, "Checking key existence"); + Assert.That(gavin.Settings.ContainsKey(addedSettings[5].Name), Is.True, "Checking key existence"); + Assert.That(gavin.Settings.ContainsKey(addedSettings[10].Name), Is.True, "Checking key existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task MapSetAsync(bool initialize) + { + User gavin; + UserSetting setting; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + gavin.Settings.Add(setting.Name, setting); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5), "Gavin's user settings count after load"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after load"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after load"); + + // Set a key that does not exist in db and is not in the queue + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after adding"); + + // Set a key that does not exist in db and it is in the queue + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after re-adding existing keys"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding existing keys"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding existing keys"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after re-adding existing keys"); + + // Set a key that exists in db and is not in the queue + Sfi.Statistics.Clear(); + gavin.Settings["s0"] = new UserSetting("s0", "s0", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after re-adding a not loaded key"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding a not loaded key"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after re-adding a not loaded key"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after re-adding a not loaded key"); + + // Set a key that exists in db and it is in the queue + Sfi.Statistics.Clear(); + gavin.Settings["s0"] = new UserSetting("s0", "s0", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after re-adding a loaded key"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding a loaded key"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding a loaded key"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after re-adding a loaded key"); + + // Set a key that exists in db and it is in the removal queue + Assert.That(gavin.Settings.Remove("s1"), Is.True, "Removing an existing key"); + Sfi.Statistics.Clear(); + gavin.Settings["s1"] = new UserSetting("s1", "s1", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after removing an existing key"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing an existing key"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing an existing key"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing an existing key"); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True, "User settings initialization status after enumerating"); + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "User settings count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "User settings count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after loading again"); + + await (t.CommitAsync()); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task MapRemoveAsync(bool initialize) + { + User gavin; + UserSetting setting; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + await (s.PersistAsync(gavin)); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + gavin.Settings.Add(setting.Name, setting); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5), "Gavin's user settings count after loading"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after loading"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after loading"); + + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after adding"); + + // Remove a key that exists in db and is not in the queue and removal queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s0"), Is.True, "Removing an existing element"); + + Assert.That(gavin.Settings.Count, Is.EqualTo(9), "Gavin's user settings count after removing a not loaded element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing a not loaded element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after removing a not loaded element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing a not loaded element"); + + // Remove a key that exists in db and it is in the queue + var item = gavin.Settings["s1"]; + Assert.That(gavin.Settings.Remove("s1"), Is.True, "Removing an existing element"); + gavin.Settings.Add(item.Name, item); + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s1"), Is.True, "Removing a re-added element"); + + Assert.That(gavin.Settings.Count, Is.EqualTo(8), "Gavin's user settings count after removing a re-added element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing a re-added element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing a re-added element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing a re-added element"); + + // Remove a key that does not exist in db and is not in the queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("test"), Is.False, "Removing not existing element"); + + Assert.That(gavin.Settings.Count, Is.EqualTo(8), "Gavin's user settings count after removing not existing element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing not existing element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after removing not existing element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing not existing element"); + + // Remove a key that does not exist in db and it is in the queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s20"), Is.True); + + Assert.That(gavin.Settings.Count, Is.EqualTo(7), "Gavin's user settings count after removing an existing element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing an existing element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing an existing element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing an existing element"); + + // Remove a key that exists in db and it is in the removal queue + Assert.That(gavin.Settings.Remove("s2"), Is.True, "Removing not loaded element"); + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s2"), Is.False, "Removing removed element"); + + Assert.That(gavin.Settings.Count, Is.EqualTo(6), "Gavin's user settings count after removing a not loaded element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing a not loaded element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing a not loaded element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing a not loaded element"); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True, "User settings initialization status after enumerating"); + Assert.That(gavin.Settings.Count, Is.EqualTo(6), "User settings count after enumerating"); + } + + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = await (s.GetAsync("gavin")); + Assert.That(gavin.Settings.Count, Is.EqualTo(6), "User settings count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after loading again"); + + await (t.CommitAsync()); } } @@ -395,5 +2597,20 @@ public async Task AddToUninitializedSetWithLaterLazyLoadAsync() await (t.CommitAsync()); } } + + private int FindAllOccurrences(string source, string substring) + { + if (source == null) + { + return 0; + } + int n = 0, count = 0; + while ((n = source.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1) + { + n += substring.Length; + ++count; + } + return count; + } } } diff --git a/src/NHibernate.Test/Extralazy/Company.cs b/src/NHibernate.Test/Extralazy/Company.cs new file mode 100644 index 00000000000..728a5f85e8d --- /dev/null +++ b/src/NHibernate.Test/Extralazy/Company.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.Extralazy +{ + public class Company + { + protected Company() { } + + public Company(string name, int index, User owner) + { + Name = name; + Owner = owner; + OriginalIndex = ListIndex = index; + } + + public virtual int Id { get; set; } + + public virtual int ListIndex { get; set; } + + public virtual int OriginalIndex { get; set; } + + public virtual string Name { get; set; } + + public virtual User Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/Extralazy/CreditCard.cs b/src/NHibernate.Test/Extralazy/CreditCard.cs new file mode 100644 index 00000000000..ffeb98a831b --- /dev/null +++ b/src/NHibernate.Test/Extralazy/CreditCard.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.Extralazy +{ + public class CreditCard + { + protected CreditCard() { } + + public CreditCard(string name, int index, User owner) + { + Name = name; + Owner = owner; + OriginalIndex = ListIndex = index; + } + + public virtual int Id { get; set; } + + public virtual int ListIndex { get; set; } + + public virtual int OriginalIndex { get; set; } + + public virtual string Name { get; set; } + + public virtual User Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs b/src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs index 248abdf63e8..bc2e9e6019c 100644 --- a/src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs +++ b/src/NHibernate.Test/Extralazy/ExtraLazyFixture.cs @@ -1,7 +1,10 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using NHibernate.Cfg; using NUnit.Framework; +using NUnit.Framework.Constraints; namespace NHibernate.Test.Extralazy { @@ -23,12 +26,2211 @@ protected override string CacheConcurrencyStrategy get { return null; } } + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Cfg.Environment.GenerateStatistics, "true"); + } + protected override void OnTearDown() { using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - s.Delete("from System.Object"); + s.Delete("from System.Object"); + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListAdd(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Gavin's companies count after get"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after companies count"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after count"); + + // Test adding companies with ICollection interface + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Test adding companies with IList interface + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + // Returned value is not valid with lazy list, no check for it. + ((IList) gavin.Companies).Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Gavin's companies count after adding through IList"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through IList"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding through IList"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding through IList"); + + // Check existence of added companies + Sfi.Statistics.Clear(); + // Have to skip unloaded (non-queued indeed) elements to avoid triggering existence queries on them. + foreach (var item in addedItems.Skip(5)) + { + Assert.That(gavin.Companies.Contains(item), Is.True, "Company '{0}' existence", item.Name); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of non-flushed"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after checking existence of non-flushed"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after checking existence of non-flushed"); + + // Check existence of not loaded companies + Assert.That(gavin.Companies.Contains(addedItems[0]), Is.True, "First company existence"); + Assert.That(gavin.Companies.Contains(addedItems[1]), Is.True, "Second company existence"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of unloaded"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking existence of unloaded"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after checking existence of unloaded"); + + // Check existence of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False, "First non-existent test"); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False, "Second non-existent test"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking non-existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking non-existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after checking non-existence"); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after loading again"); + + t.Commit(); + } + } + + [Explicit] + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void ListAddDuplicated(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count before flush"); + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after get"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statement count after count"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after get"); + + // Re-add items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + gavin.Companies.Add(addedItems[i]); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statement count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after re-adding"); + + if (flush) + { + s.Flush(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after second flush"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after second flush"); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumeration"); + Assert.That(gavin.Companies.Count, Is.EqualTo(flush ? 5 : 10), "Companies count after enumeration"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after loading Gavin again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after loading Gavin again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListInsert(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after get"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after get"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after get"); + + // Test inserting companies at the start + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(0, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after insert"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after insert"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after insert"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after insert"); + + // Test inserting companies at the end + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after tail insert"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after tail insert"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after tail insert"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after tail insert"); + + // Try insert invalid indexes + Assert.Throws( + () => gavin.Companies.Insert(-1, new Company("c-1", -1, gavin)), "inserting at -1"); + Assert.Throws( + () => gavin.Companies.Insert(20, new Company("c20", 20, gavin)), "inserting too far"); + + // Check existence of added companies + Sfi.Statistics.Clear(); + // Have to skip unloaded (non-queued indeed) elements to avoid triggering existence queries on them. + foreach (var item in addedItems.Skip(5)) + { + Assert.That(gavin.Companies.Contains(item), Is.True, "Company '{0}' existence", item.Name); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after existence check"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after existence check"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after existence check"); + + // Check existence of not loaded companies + Assert.That(gavin.Companies.Contains(addedItems[0]), Is.True, "First company existence"); + Assert.That(gavin.Companies.Contains(addedItems[1]), Is.True, "Second company existence"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after unloaded existence check"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after unloaded existence check"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after unloaded existence check"); + + // Check existence of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False, "First non-existence test"); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False, "Second non-existence test"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after non-existence check"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after non-existence check"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after non-existence check"); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumeration"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after enumeration"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Companies count after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after loading again"); + + t.Commit(); + } + } + + [Explicit] + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void ListInsertDuplicated(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count before flush"); + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after get"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after count"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after count"); + + // Re-add items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + gavin.Companies.Insert(4 - i, addedItems[i]); + } + + Assert.That(gavin.Companies[0].ListIndex, Is.EqualTo(4), "Company at 0"); + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after re-insert"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-insert"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after re-insert"); + + if (flush) + { + s.Flush(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after flush"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after flush"); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumeration"); + Assert.That(gavin.Companies.Count, Is.EqualTo(flush ? 5 : 10), "Companies count after enumeration"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Companies count after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListRemoveAt(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = new List {0, 1, 2, 6, 8, 9}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + // Add transient companies + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Remove transient companies + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(5); + gavin.Companies.RemoveAt(6); + + Assert.That(gavin.Companies.Count, Is.EqualTo(8), "Gavin's companies count after removing 2 transient companies"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing transient companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing transient companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing transient companies"); + + // Remove persisted companies + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(3); + gavin.Companies.RemoveAt(3); + + Assert.That(gavin.Companies.Count, Is.EqualTo(6), "Gavin's companies count after removing 2 persisted companies"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing persisted companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after removing persisted companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing persisted companies"); + + // Try remove invalid indexes + Assert.Throws(() => gavin.Companies.RemoveAt(-1), "Removing at -1"); + Assert.Throws(() => gavin.Companies.RemoveAt(8), "Removing too far"); + + // Check existence of companies + Sfi.Statistics.Clear(); + var removedIndexes = new HashSet {3, 4, 5, 7}; + for (var i = 0; i < addedItems.Count; i++) + { + Assert.That( + gavin.Companies.Contains(addedItems[i]), + removedIndexes.Contains(i) ? Is.False : (IResolveConstraint) Is.True, + $"Element at index {i} was {(removedIndexes.Contains(i) ? "not " : "")}removed"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Gavin's companies count after checking existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3), "Flushes count after checking existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization after checking existence"); + + // Check existence of not existing companies + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Contains(new Company("test1", 15, gavin)), Is.False, "Checking existence of non-existence"); + Assert.That(gavin.Companies.Contains(new Company("test2", 16, gavin)), Is.False, "Checking existence of non-existence"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Gavin's companies count after checking non-existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Flushes count after checking non-existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization after checking non-existence"); + + gavin.UpdateCompaniesIndexes(); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(6), "Companies count after enumerating"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(6), "Companies count after loading again Gavin"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListGetSet(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = new List {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + // Add transient companies + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding companies"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding companies"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Compare all items + Sfi.Statistics.Clear(); + for (var i = 0; i < 10; i++) + { + Assert.That(gavin.Companies[i], Is.EqualTo(addedItems[i]), "Comparing added company at index {0}", i); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding comparing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding comparing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after comparing"); + + // Try get invalid indexes + Assert.Throws(() => + { + var item = gavin.Companies[10]; + }, "Get too far"); + Assert.Throws(() => + { + var item = gavin.Companies[-1]; + }, "Get at -1"); + + // Try set invalid indexes + Assert.Throws(() => gavin.Companies[10] = addedItems[0], "Set too far"); + Assert.Throws(() => gavin.Companies[-1] = addedItems[0], "Set at -1"); + + // Swap transient and persisted indexes + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var hiIndex = 9 - i; + var tmp = gavin.Companies[i]; + gavin.Companies[i] = gavin.Companies[hiIndex]; + gavin.Companies[hiIndex] = tmp; + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after swapping"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after swapping"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(10), "Statements count after adding swapping"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after swapping"); + + // Check indexes + Sfi.Statistics.Clear(); + for (var i = 0; i < 10; i++) + { + Assert.That(gavin.Companies[i].ListIndex, Is.EqualTo(finalIndexOrder[i]), "Comparing company ListIndex at index {0}", i); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after comparing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after comparing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after comparing"); + + gavin.UpdateCompaniesIndexes(); + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after enumerating"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Companies count after loading again Gavin"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListFlush(bool initialize) + { + User gavin; + var addedItems = new List(); + var finalIndexOrder = Enumerable.Range(0, 13).ToList(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Add transient companies with Insert + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Insert(i, item); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(5), "Statements count after inserting"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Gavin's companies count after inserting 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after inserting"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after inserting"); + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 15; i < 20; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems.Add(item); + gavin.Companies.Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(20), "Gavin's companies count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding"); + + // Remove last 5 transient companies + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 15; i < 20; i++) + { + Assert.That(gavin.Companies.Remove(addedItems[i]), Is.True, "Removing transient company at index {0}", i); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(10), "Statements count after removing"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Gavin's companies count after removing 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing"); + + // Remove last 5 transient companies + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + for (var i = 10; i < 15; i++) + { + gavin.Companies.RemoveAt(10); + } + + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "DELETE \n FROM"), Is.EqualTo(5), "Statements count after second removing"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(10), "Gavin's companies count after second removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after second removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(7), "Statements count after second removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after second removing"); + + // Add transient companies with Add + Sfi.Statistics.Clear(); + for (var i = 10; i < 15; i++) + { + var item = new Company($"c{i}", i, gavin); + addedItems[i] = item; + // NOTE: the returned index is currently invalid due to extra-lazy avoiding to query the count or initializing the collection + ((IList) gavin.Companies).Add(item); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(15), "Gavin's companies count after adding through IList"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through IList"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding through IList"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after adding through IList"); + + // Remove last transient company + Sfi.Statistics.Clear(); + using (var sqlLog = new SqlLogSpy()) + { + Assert.That(gavin.Companies.Remove(addedItems[14]), Is.EqualTo(true), "Removing last transient company"); + var log = sqlLog.GetWholeLog(); + Assert.That(FindAllOccurrences(log, "DELETE \n FROM"), Is.EqualTo(5), "Delete statements count after removing last transient company"); + Assert.That(FindAllOccurrences(log, "INSERT \n INTO"), Is.EqualTo(5), "Insert statements count after removing last transient company"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(14), "Gavin's companies count after adding removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing"); + + // Test index getter + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies[0], Is.EqualTo(addedItems[0]), "Comparing first item with index getter"); + + Assert.That(gavin.Companies.Count, Is.EqualTo(14), "Gavin's companies count after adding comparing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after comparing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3), "Statements count after comparing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after comparing"); + + // Remove last transient company + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Remove(addedItems[13]), Is.EqualTo(true), "Removing last transient company"); + + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Gavin's companies count after adding repeated removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after repeated removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after repeated removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after repeated removing"); + + // Test index setter + Sfi.Statistics.Clear(); + gavin.Companies[0] = addedItems[0]; + + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Gavin's companies count after setting first item"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after setting first item"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3), "Statements count after setting first item"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after setting first item"); + + // Test manual flush after remove + Sfi.Statistics.Clear(); + gavin.Companies.RemoveAt(12); + using (var sqlLog = new SqlLogSpy()) + { + s.Flush(); + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "DELETE \n FROM"), Is.EqualTo(1), "Delete statements count after removing at 12 index"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(12), "Gavin's companies count after removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after removing"); + + // Test manual flush after insert + Sfi.Statistics.Clear(); + gavin.Companies.Add(new Company("c12", 12, gavin)); + using (var sqlLog = new SqlLogSpy()) + { + s.Flush(); + Assert.That(FindAllOccurrences(sqlLog.GetWholeLog(), "INSERT \n INTO"), Is.EqualTo(1), "Insert statements count after flushing"); + } + + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Gavin's companies count after flushing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(1), "Flushes count after flushing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after flushing"); + + for (var i = 0; i < gavin.Companies.Count; i++) + { + Assert.That(gavin.Companies[i].ListIndex, Is.EqualTo(i), "Comparing company ListIndex at index {0}", i); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Companies count after enumerating"); + Assert.That(gavin.Companies.Select(o => o.ListIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(13), "Companies count after loading again Gavin"); + Assert.That(gavin.Companies.Select(o => o.ListIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListClear(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new CreditCard($"c{i}", i, gavin); + addedItems.Add(item); + gavin.CreditCards.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + var collection = gavin.CreditCards; + + // Add transient credit cards + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + var item = new CreditCard($"c{i}", i, gavin); + addedItems.Add(item); + collection.Insert(i, item); + } + + Assert.That(collection.Count, Is.EqualTo(10), "Gavin's credit cards count after inserting 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after inserting"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after inserting"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after inserting"); + + Sfi.Statistics.Clear(); + collection.Clear(); + + Assert.That(collection.Count, Is.EqualTo(0), "Gavin's credit cards count after clearing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after clearing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after clearing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after clearing"); + + // Re-add two not loaded and two transient credit cards + collection.Add(addedItems[0]); + collection.Add(addedItems[1]); + collection.Add(addedItems[5]); + collection.Add(addedItems[6]); + + Assert.That(collection.Count, Is.EqualTo(4), "Gavin's credit cards count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after re-adding"); + + // Remove one not loaded and one transient credit cards + Assert.That(collection.Remove(addedItems[1]), Is.True, "Removing not loaded credit card"); + Assert.That(collection.Remove(addedItems[6]), Is.True, "Removing transient credit card"); + + Assert.That(collection.Count, Is.EqualTo(2), "Gavin's credit cards count after removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after removing"); + + // Remove not existing items + Assert.That(collection.Remove(addedItems[1]), Is.False, "Removing not-existing credit card"); + Assert.That(collection.Remove(addedItems[6]), Is.False, "Removing not-existing credit card"); + + Assert.That(collection.Count, Is.EqualTo(2), "Gavin's credit cards count after not-existing removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after not-existing removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after not-existing removing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after not-existing removing"); + + if (initialize) + { + using (var e = collection.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.True, "Credit cards initialization status after enumerating"); + Assert.That(collection.Count, Is.EqualTo(2), "Credit cards count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + var collection = gavin.CreditCards; + // As the cascade option is set to all, the clear operation will only work on + // transient credit cards + Assert.That(collection.Count, Is.EqualTo(6), "Credit cards count after loading again Gavin"); + for (var i = 0; i < 10; i++) + { + Assert.That(collection.Contains(addedItems[i]), i < 6 ? Is.True : (IResolveConstraint) Is.False, "Checking existence for item at {0}", i); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Credit cards initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ListIndexOperations(bool initialize) + { + User gavin; + var finalIndexOrder = new List {6, 0, 4}; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Company($"c{i}", i, gavin); + gavin.Companies.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Current tracker state: + // Indexes: 0,1,2,3,4 + // Queue: / + // RemoveDbIndexes: / + + Sfi.Statistics.Clear(); + Assert.That(gavin.Companies.Count, Is.EqualTo(5), "Gavin's companies count"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status"); + + Sfi.Statistics.Clear(); + gavin.Companies.Insert(1, new Company("c5", 5, gavin)); + // Current tracker state: + // Indexes: 0,5,1,2,3,4 + // Queue: {1, 5} + // RemoveDbIndexes: / + + gavin.Companies.Insert(0, new Company("c6", 6, gavin)); + // Current tracker state: + // Indexes: 6,0,5,1,2,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: / + + gavin.Companies.RemoveAt(4); + // Current tracker state: + // Indexes: 6,0,5,1,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 2 + + gavin.Companies.RemoveAt(3); + // Current tracker state: + // Indexes: 6,0,5,3,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 1,2 + + gavin.Companies.RemoveAt(3); + // Current tracker state: + // Indexes: 6,0,5,4 + // Queue: {0, 6}, {2, 5} + // RemoveDbIndexes: 1,2,3 + + gavin.Companies.RemoveAt(2); + // Current tracker state: + // Indexes: 6,0,4 + // Queue: {0, 6} + // RemoveDbIndexes: 1,2,3 + + Assert.That(gavin.Companies.Count, Is.EqualTo(3), "Gavin's companies count after remove/insert operations"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after remove/insert operations"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(3), "Statements count after remove/insert operations"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.False, "Companies initialization status after remove/insert operations"); + + gavin.UpdateCompaniesIndexes(); + + for (var i = 0; i < gavin.Companies.Count; i++) + { + Assert.That(gavin.Companies[i].OriginalIndex, Is.EqualTo(finalIndexOrder[i]), "Comparing company index at {0}", i); + } + + if (initialize) + { + using (var e = gavin.Companies.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after enumerating"); + Assert.That(gavin.Companies.Count, Is.EqualTo(3), "Companies count after enumerating"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Companies.Count, Is.EqualTo(3), "Companies count after loading again Gavin"); + Assert.That(gavin.Companies.Select(o => o.OriginalIndex), Is.EquivalentTo(finalIndexOrder), "Companies indexes after loading again"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Companies), Is.True, "Companies initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void SetAdd(bool initialize) + { + User gavin; + Document hia; + Document hia2; + var addedDocuments = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + hia = new Document("HiA", "blah blah blah", gavin); + hia2 = new Document("HiA2", "blah blah blah blah", gavin); + gavin.Documents.Add(hia); + gavin.Documents.Add(hia2); + s.Persist(gavin); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(2), "Gavin's documents count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding"); + + // Test adding documents with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document{i}", $"content{i}", gavin); + addedDocuments.Add(document); + Assert.That(gavin.Documents.Add(document), Is.True, "Adding document through ISet"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(7), "Gavin's documents count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding"); + + // Test adding documents with ICollection interface + Sfi.Statistics.Clear(); + var documents = (ICollection) gavin.Documents; + for (var i = 0; i < 5; i++) + { + var document = new Document($"document2{i}", $"content{i}", gavin); + addedDocuments.Add(document); + documents.Add(document); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Gavin's documents count after adding through ICollection<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through ICollection<>"); + // In this case we cannot determine whether the entities are transient or not so + // we are forced to check the database + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding through ICollection<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding through ICollection<>"); + + // Test re-adding documents with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Add(addedDocuments[i]), Is.False, "Re-add document through ISet<>"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Gavin's documents count after re-adding"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after re-adding"); + + // Test re-adding documents with ICollection interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + documents.Add(addedDocuments[i]); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Gavin's documents count after re-adding through ICollection<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding through ICollection<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding through ICollection<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after re-adding through ICollection<>"); + + // Check existence of added documents + Sfi.Statistics.Clear(); + foreach (var document in addedDocuments) + { + Assert.That(gavin.Documents.Contains(document), Is.True, "Checking existence of an existing document"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after checking existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after checking existence"); + + // Check existence of not loaded documents + Assert.That(gavin.Documents.Contains(hia), Is.True, "Checking existence of not loaded document"); + Assert.That(gavin.Documents.Contains(hia2), Is.True, "Checking existence of not loaded document"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after checking existence"); + + // Check existence of not existing documents + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Contains(new Document("test1", "content", gavin)), Is.False, "Checking existence of not-existing document"); + Assert.That(gavin.Documents.Contains(new Document("test2", "content", gavin)), Is.False, "Checking existence of not-existing document"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking non-existence"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking non-existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after checking non-existence"); + + // Test adding not loaded documents + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Add(hia), Is.False, "Adding not loaded element"); + documents.Add(hia); + + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Gavin's documents count after adding not loaded element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding not loaded element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after adding not loaded element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding not loaded element"); + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True, "Documents initialization status after enumerating"); + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Documents count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Documents.Count, Is.EqualTo(12), "Documents count after loading again Gavin"); + Assert.That(gavin.Documents.Contains(hia2), Is.True, "Checking not loaded element"); + Assert.That(gavin.Documents.Contains(hia), Is.True, "Checking not loaded element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void SetAddDuplicated(bool initialize, bool flush) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new Document($"d{i}", $"c{i}", gavin); + addedItems.Add(item); + gavin.Documents.Add(item); + gavin.Documents.Add(item); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after adding 5"); + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Title); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after reload"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after reload"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after reload"); + + // Re-add items + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Add(addedItems[i]), Is.False, "Re-adding element"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after re-adding"); + + if (flush) + { + s.Flush(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after flushing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after flushing"); + } + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True, "Documents initialization status after enumerating"); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Documents count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Documents count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void SetAddTransient(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Permissions.Count, Is.EqualTo(5), "Gavin's permissions count after adding 5"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after adding"); + + // Test adding permissions with ICollection interface + Sfi.Statistics.Clear(); + var items = (ICollection) gavin.Permissions; + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p2{i}", gavin); + addedItems.Add(item); + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10), "Gavin's permissions count after adding through ICollection<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through ICollection<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding through ICollection<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after adding through ICollection<>"); + + // Test re-adding permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(5)) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10), "Gavin's permissions count after re-adding"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding"); + + // Test adding not loaded permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Take(5)) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10), "Gavin's permissions count after re-adding not loaded elements"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding not loaded elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after re-adding not loaded elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding not loaded elements"); + + // Test adding loaded permissions with ICollection interface + Sfi.Statistics.Clear(); + foreach (var item in s.Query()) + { + items.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(10), "Gavin's permissions count after re-adding loaded elements"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding loaded elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(6), "Statements count after re-adding loaded elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding loaded elements"); + + // Test adding permissions with ISet interface + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p3{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Gavin's permissions count after adding through ISet<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through ISet<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding through ISet<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after adding through ISet<>"); + + // Test re-adding permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Skip(10)) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Gavin's permissions count after re-adding through ISet<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding through ISet<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding through ISet<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding through ISet<>"); + + // Test adding not loaded permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in addedItems.Take(5)) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Gavin's permissions count after re-adding not loaded permissions through ISet<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding not loaded permissions through ISet<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after re-adding not loaded permissions through ISet<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding not loaded permissions through ISet<>"); + + // Test adding loaded permissions with ISet interface + Sfi.Statistics.Clear(); + foreach (var item in s.Query()) + { + gavin.Permissions.Add(item); + } + + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Gavin's permissions count after re-adding loaded permissions through ISet<>"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding loaded permissions through ISet<>"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(6), "Statements count after re-adding loaded permissions through ISet<>"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after re-adding loaded permissions through ISet<>"); + + if (initialize) + { + using (var e = gavin.Permissions.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.True, "Permissions initialization status after enumerating"); + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Permissions count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Permissions.Count, Is.EqualTo(15), "Permissions count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Permissions), Is.False, "Permissions initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void SetRemove(bool initialize) + { + User gavin; + var addedDocuments = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document{i}", $"content{i}", gavin); + addedDocuments.Add(document); + gavin.Documents.Add(document); + } + + s.Persist(gavin); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedDocuments[i] = s.Get(addedDocuments[i].Title); + } + + Sfi.Statistics.Clear(); + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after refresh"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after refresh"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after refresh"); + + // Add new documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"document2{i}", $"content{i}", gavin); + addedDocuments.Add(document); + ((ICollection)gavin.Documents).Add(document); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(10), "Gavin's documents count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after adding"); + + // Test removing existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.True, "Removing existing document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after removing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after removing"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing"); + + // Test removing removed existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.False, "Checking existence of a removed document"); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.False, "Removing removed document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after removing removed documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing removed documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing removed documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing removed documents"); + + // Test removing not existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"test{i}", "content", gavin); + Assert.That(gavin.Documents.Remove(document), Is.False, "Removing not existing document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(5), "Gavin's documents count after removing not existing documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing not existing documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after removing not existing documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing not existing documents"); + + // Test removing newly added documents + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.True, "Checking existence of an added document"); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.True, "Removing added document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Gavin's documents count after removing added documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing added documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing added documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing added documents"); + + // Test removing removed newly added documents + Sfi.Statistics.Clear(); + for (var i = 5; i < 10; i++) + { + Assert.That(gavin.Documents.Contains(addedDocuments[i]), Is.False, "Checking existence of a removed document"); + Assert.That(gavin.Documents.Remove(addedDocuments[i]), Is.False, "Removing removed document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Gavin's documents count after removing removed documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing removed documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing removed documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing removed documents"); + + // Test removing not existing documents + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var document = new Document($"test{i}", "content", gavin); + Assert.That(gavin.Documents.Remove(document), Is.False, "Removing not existing document"); + } + + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Gavin's documents count after removing not existing documents"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing not existing documents"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after removing not existing documents"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after removing not existing documents"); + + if (initialize) + { + using (var e = gavin.Documents.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.True, "Documents initialization status after enumerating"); + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Documents count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Documents.Count, Is.EqualTo(0), "Documents count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Documents), Is.False, "Documents initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void SetClear(bool initialize) + { + User gavin; + var addedItems = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p{i}", gavin); + addedItems.Add(item); + gavin.Permissions.Add(item); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.FlushMode = FlushMode.Commit; + + gavin = s.Get("gavin"); + // Refresh added items + for (var i = 0; i < 5; i++) + { + addedItems[i] = s.Get(addedItems[i].Id); + } + + var collection = gavin.Permissions; + + Sfi.Statistics.Clear(); + Assert.That(collection.Count, Is.EqualTo(5), "Gavin's permissions count after refresh"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after refresh"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after refresh"); + + // Add transient permissions + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + var item = new UserPermission($"p2{i}", gavin); + addedItems.Add(item); + collection.Add(item); + } + + Assert.That(collection.Count, Is.EqualTo(10), "Gavin's permissions count after adding 5"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after adding"); + + Sfi.Statistics.Clear(); + collection.Clear(); + + Assert.That(collection.Count, Is.EqualTo(0), "Gavin's permissions count after flushing"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after flushing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after flushing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after flushing"); + + // Re-add two not loaded and two transient permissions + Assert.That(collection.Add(addedItems[0]), Is.True, "Re-adding not loaded element"); + Assert.That(collection.Add(addedItems[1]), Is.True, "Re-adding not loaded element"); + Assert.That(collection.Add(addedItems[5]), Is.True, "Re-adding transient element"); + Assert.That(collection.Add(addedItems[6]), Is.True, "Re-adding transient element"); + + Assert.That(collection.Count, Is.EqualTo(4), "Gavin's permissions count after re-adding"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after re-adding"); + + // Remove one not loaded and one transient permissions + Assert.That(collection.Remove(addedItems[1]), Is.True, "Removing not loaded element"); + Assert.That(collection.Remove(addedItems[6]), Is.True, "Removing transient element"); + + Assert.That(collection.Count, Is.EqualTo(2), "Gavin's permissions count after removing"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after removing"); + + // Remove not existing items + Assert.That(collection.Remove(addedItems[1]), Is.False, "Removing removed element"); + Assert.That(collection.Remove(addedItems[6]), Is.False, "Removing removed element"); + + Assert.That(collection.Count, Is.EqualTo(2), "Gavin's permissions count after removing removed elements"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing removed elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing removed elements"); + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after removing removed elements"); + + if (initialize) + { + using (var e = collection.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.True, "Permissions initialization status after enumerating"); + Assert.That(collection.Count, Is.EqualTo(2), "Permissions count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + var collection = gavin.Permissions; + // As the cascade option is set to all, the clear operation will only work on + // transient permissions + Assert.That(collection.Count, Is.EqualTo(6), "Permissions count after loading again Gavin"); + for (var i = 0; i < 10; i++) + { + Assert.That(collection.Contains(addedItems[i]), i < 6 ? Is.True : (IResolveConstraint) Is.False, + "Checking existence of added element at {0}", i); + } + + Assert.That(NHibernateUtil.IsInitialized(collection), Is.False, "Permissions initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void MapAdd(bool initialize) + { + User gavin; + UserSetting setting; + var addedSettings = new List(); + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + addedSettings.Add(setting); + gavin.Settings.Add(setting.Name, setting); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5), "Gavin's user settings count after load"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after load"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after load"); + + // Test adding settings with Add method + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + addedSettings.Add(setting); + gavin.Settings.Add(setting.Name, setting); + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after adding"); + + // Test adding settings with [] + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s3{i}", $"data{i}", gavin); + addedSettings.Add(setting); + + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(15), "Gavin's user settings count after adding 5 through indexer"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding through indexer"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding through indexer"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after adding through indexer"); + + // Check existence of added settings + Sfi.Statistics.Clear(); + foreach (var item in addedSettings.Skip(5)) + { + Assert.That(gavin.Settings.ContainsKey(item.Name), Is.True, "Checking existence of added element"); + Assert.That(gavin.Settings.Contains(new KeyValuePair(item.Name, item)), Is.True, "Checking existence of added element using KeyValuePair<,>"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of added elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after checking existence of added elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after checking existence of added elements"); + + // Check existence of not loaded settings + foreach (var item in addedSettings.Take(5)) + { + Assert.That(gavin.Settings.ContainsKey(item.Name), Is.True, "Checking key existence of not loaded elements"); + } + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of not loaded elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after checking existence of not loaded elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after checking existence of not loaded elements"); + + // Check existence of not existing settings + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.ContainsKey("test"), Is.False, "Checking existence of not existing element"); + Assert.That(gavin.Settings.ContainsKey("test2"), Is.False, "Checking existence of not existing element"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after checking existence of not existing elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after checking existence of not existing elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after checking existence of not existing elements"); + + // Try to add an existing setting + Assert.Throws(() => gavin.Settings.Add("s0", new UserSetting("s0", "data", gavin)), "Adding an existing key"); + Assert.Throws(() => gavin.Settings.Add("s20", new UserSetting("s20", "data", gavin)), "Adding an existing key"); + Assert.Throws(() => gavin.Settings.Add("s30", new UserSetting("s30", "data", gavin)), "Adding an existing key"); + + // Get values of not loaded keys + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.TryGetValue("s0", out setting), Is.True, "Getting value of not loaded key"); + Assert.That(setting.Id, Is.EqualTo(addedSettings[0].Id), "Comparing retrieved element id"); + Assert.That(gavin.Settings["s0"].Id, Is.EqualTo(addedSettings[0].Id), "Comparing retrieved element id by indexer"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after reading elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2), "Statements count after reading elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after reading elements"); + + // Get values of newly added keys + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.TryGetValue("s20", out setting), Is.True, "Getting value of a newly added key"); + Assert.That(setting, Is.EqualTo(addedSettings[5]), "Comparing retrieved element"); + Assert.That(gavin.Settings["s20"], Is.EqualTo(addedSettings[5]), "Comparing retrieved element by indexer"); + Assert.That(gavin.Settings.TryGetValue("s30", out setting), Is.True, "Getting value of a newly added key"); + Assert.That(setting, Is.EqualTo(addedSettings[10]), "Comparing retrieved element"); + Assert.That(gavin.Settings["s30"], Is.EqualTo(addedSettings[10]), "Getting value of a newly added key"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after reading elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after reading elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after reading elements"); + + // Try to get a non existing setting + Assert.That(gavin.Settings.TryGetValue("test", out setting), Is.False, "Try to get a not existing key"); + Assert.That(gavin.Settings.TryGetValue("test2", out setting), Is.False, "Try to get a not existing key"); + Assert.Throws(() => + { + setting = gavin.Settings["test"]; + }, "Getting a not existing key"); + Assert.Throws(() => + { + setting = gavin.Settings["test2"]; + }, "Getting a not existing key"); + + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after reading not existing elements"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(4), "Statements count after reading not existing elements"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after reading not existing elements"); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True, "User settings initialization status after enumerating"); + Assert.That(gavin.Settings.Count, Is.EqualTo(15), "User settings count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Settings.Count, Is.EqualTo(15), "User settings count after loading again Gavin"); + Assert.That(gavin.Settings.ContainsKey(addedSettings[0].Name), Is.True, "Checking key existence"); + Assert.That(gavin.Settings.ContainsKey(addedSettings[5].Name), Is.True, "Checking key existence"); + Assert.That(gavin.Settings.ContainsKey(addedSettings[10].Name), Is.True, "Checking key existence"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void MapSet(bool initialize) + { + User gavin; + UserSetting setting; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + gavin.Settings.Add(setting.Name, setting); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5), "Gavin's user settings count after load"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after load"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after load"); + + // Set a key that does not exist in db and is not in the queue + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after adding"); + + // Set a key that does not exist in db and it is in the queue + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after re-adding existing keys"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding existing keys"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding existing keys"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after re-adding existing keys"); + + // Set a key that exists in db and is not in the queue + Sfi.Statistics.Clear(); + gavin.Settings["s0"] = new UserSetting("s0", "s0", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after re-adding a not loaded key"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding a not loaded key"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after re-adding a not loaded key"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after re-adding a not loaded key"); + + // Set a key that exists in db and it is in the queue + Sfi.Statistics.Clear(); + gavin.Settings["s0"] = new UserSetting("s0", "s0", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after re-adding a loaded key"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after re-adding a loaded key"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after re-adding a loaded key"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after re-adding a loaded key"); + + // Set a key that exists in db and it is in the removal queue + Assert.That(gavin.Settings.Remove("s1"), Is.True, "Removing an existing key"); + Sfi.Statistics.Clear(); + gavin.Settings["s1"] = new UserSetting("s1", "s1", gavin); + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after removing an existing key"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing an existing key"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing an existing key"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing an existing key"); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True, "User settings initialization status after enumerating"); + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "User settings count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "User settings count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after loading again"); + + t.Commit(); + } + } + + [TestCase(false)] + [TestCase(true)] + public void MapRemove(bool initialize) + { + User gavin; + UserSetting setting; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = new User("gavin", "secret"); + s.Persist(gavin); + + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s{i}", $"data{i}", gavin); + gavin.Settings.Add(setting.Name, setting); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Count, Is.EqualTo(5), "Gavin's user settings count after loading"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after loading"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after loading"); + + Sfi.Statistics.Clear(); + for (var i = 0; i < 5; i++) + { + setting = new UserSetting($"s2{i}", $"data{i}", gavin); + gavin.Settings[setting.Name] = setting; + } + + Assert.That(gavin.Settings.Count, Is.EqualTo(10), "Gavin's user settings count after adding 5"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after adding"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(5), "Statements count after adding"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after adding"); + + // Remove a key that exists in db and is not in the queue and removal queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s0"), Is.True, "Removing an existing element"); + + Assert.That(gavin.Settings.Count, Is.EqualTo(9), "Gavin's user settings count after removing a not loaded element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing a not loaded element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after removing a not loaded element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing a not loaded element"); + + // Remove a key that exists in db and it is in the queue + var item = gavin.Settings["s1"]; + Assert.That(gavin.Settings.Remove("s1"), Is.True, "Removing an existing element"); + gavin.Settings.Add(item.Name, item); + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s1"), Is.True, "Removing a re-added element"); + + Assert.That(gavin.Settings.Count, Is.EqualTo(8), "Gavin's user settings count after removing a re-added element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing a re-added element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing a re-added element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing a re-added element"); + + // Remove a key that does not exist in db and is not in the queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("test"), Is.False, "Removing not existing element"); + + Assert.That(gavin.Settings.Count, Is.EqualTo(8), "Gavin's user settings count after removing not existing element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing not existing element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1), "Statements count after removing not existing element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing not existing element"); + + // Remove a key that does not exist in db and it is in the queue + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s20"), Is.True); + + Assert.That(gavin.Settings.Count, Is.EqualTo(7), "Gavin's user settings count after removing an existing element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing an existing element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing an existing element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing an existing element"); + + // Remove a key that exists in db and it is in the removal queue + Assert.That(gavin.Settings.Remove("s2"), Is.True, "Removing not loaded element"); + Sfi.Statistics.Clear(); + Assert.That(gavin.Settings.Remove("s2"), Is.False, "Removing removed element"); + + Assert.That(gavin.Settings.Count, Is.EqualTo(6), "Gavin's user settings count after removing a not loaded element"); + Assert.That(Sfi.Statistics.FlushCount, Is.EqualTo(0), "Flushes count after removing a not loaded element"); + Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0), "Statements count after removing a not loaded element"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after removing a not loaded element"); + + if (initialize) + { + using (var e = gavin.Settings.GetEnumerator()) + { + e.MoveNext(); + } + + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.True, "User settings initialization status after enumerating"); + Assert.That(gavin.Settings.Count, Is.EqualTo(6), "User settings count after enumerating"); + } + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + gavin = s.Get("gavin"); + Assert.That(gavin.Settings.Count, Is.EqualTo(6), "User settings count after loading again Gavin"); + Assert.That(NHibernateUtil.IsInitialized(gavin.Settings), Is.False, "User settings initialization status after loading again"); + t.Commit(); } } @@ -384,5 +2586,20 @@ public void AddToUninitializedSetWithLaterLazyLoad() t.Commit(); } } + + private int FindAllOccurrences(string source, string substring) + { + if (source == null) + { + return 0; + } + int n = 0, count = 0; + while ((n = source.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1) + { + n += substring.Length; + ++count; + } + return count; + } } } diff --git a/src/NHibernate.Test/Extralazy/User.cs b/src/NHibernate.Test/Extralazy/User.cs index 979fed54be4..e25b92adea5 100644 --- a/src/NHibernate.Test/Extralazy/User.cs +++ b/src/NHibernate.Test/Extralazy/User.cs @@ -45,5 +45,21 @@ public virtual ISet Photos get { return photos; } set { photos = value; } } + + public virtual IDictionary Settings { get; set; } = new Dictionary(); + + public virtual ISet Permissions { get; set; } = new HashSet(); + + public virtual IList Companies { get; set; } = new List(); + + public virtual IList CreditCards { get; set; } = new List(); + + public virtual void UpdateCompaniesIndexes() + { + for (var i = 0; i < Companies.Count; i++) + { + Companies[i].ListIndex = i; + } + } } } diff --git a/src/NHibernate.Test/Extralazy/UserGroup.hbm.xml b/src/NHibernate.Test/Extralazy/UserGroup.hbm.xml index 19d201f2bd4..9641c5800c6 100644 --- a/src/NHibernate.Test/Extralazy/UserGroup.hbm.xml +++ b/src/NHibernate.Test/Extralazy/UserGroup.hbm.xml @@ -21,6 +21,11 @@ + + + + + @@ -29,11 +34,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/Extralazy/UserPermission.cs b/src/NHibernate.Test/Extralazy/UserPermission.cs new file mode 100644 index 00000000000..3c4820c0c72 --- /dev/null +++ b/src/NHibernate.Test/Extralazy/UserPermission.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.Extralazy +{ + public class UserPermission + { + protected UserPermission() { } + + public UserPermission(string name, User owner) + { + Name = name; + Owner = owner; + } + + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + + public virtual User Owner { get; set; } + } +} diff --git a/src/NHibernate.Test/Extralazy/UserSetting.cs b/src/NHibernate.Test/Extralazy/UserSetting.cs new file mode 100644 index 00000000000..da86331a3f3 --- /dev/null +++ b/src/NHibernate.Test/Extralazy/UserSetting.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Test.Extralazy +{ + public class UserSetting + { + protected UserSetting() { } + public UserSetting(string name, string data, User owner) + { + Name = name; + Data = data; + Owner = owner; + } + + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + + public virtual string Data { get; set; } + + public virtual User Owner { get; set; } + } +} diff --git a/src/NHibernate/Async/Collection/AbstractPersistentCollection.cs b/src/NHibernate/Async/Collection/AbstractPersistentCollection.cs index d3c2d48dc7e..aba0ae46271 100644 --- a/src/NHibernate/Async/Collection/AbstractPersistentCollection.cs +++ b/src/NHibernate/Async/Collection/AbstractPersistentCollection.cs @@ -16,10 +16,12 @@ using System.Threading; using System.Threading.Tasks; using NHibernate.Collection.Generic; +using NHibernate.Collection.Trackers; using NHibernate.Engine; using NHibernate.Impl; using NHibernate.Loader; using NHibernate.Persister.Collection; +using NHibernate.Proxy; using NHibernate.Type; using NHibernate.Util; @@ -28,6 +30,62 @@ namespace NHibernate.Collection public abstract partial class AbstractPersistentCollection : IPersistentCollection, ILazyInitializedCollection { + protected virtual async Task ReadKeyExistenceAsync(TKey elementKey, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractMapQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + await (session.FlushAsync(cancellationToken)).ConfigureAwait(false); + } + + return persister.IndexExists(entry.LoadedKey, elementKey, session); + } + + if (queueOperationTracker.ContainsKey(elementKey)) + { + return true; + } + + if (queueOperationTracker.Cleared) + { + return false; + } + + if (queueOperationTracker.IsElementKeyQueuedForDelete(elementKey)) + { + return false; + } + + // As keys are unordered we don't have to calculate the current order of the key + return persister.IndexExists(entry.LoadedKey, elementKey, session); + } + Read(); + } + return null; + } + + internal async Task IsTransientAsync(object element, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var queryableCollection = (IQueryableCollection) Session.Factory.GetCollectionPersister(Role); + return + queryableCollection != null && + queryableCollection.ElementType.IsEntityType && + !element.IsProxy() && + !Session.PersistenceContext.IsEntryFor(element) && + await (ForeignKeys.IsTransientFastAsync(queryableCollection.ElementPersister.EntityName, element, Session, cancellationToken)).ConfigureAwait(false) == true; + } + /// /// Initialize the collection, if possible, wrapping any exceptions /// in a runtime exception diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs index 9707d1dbefa..3b229ac54b8 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs index 6b6e8126ffb..1417c98e361 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs index ccba8292fe4..e574577a111 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs index 65d992d6c28..91f2f86f626 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs @@ -16,6 +16,7 @@ using System.Linq; using System.Linq.Expressions; using NHibernate.Collection.Generic.SetHelpers; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; diff --git a/src/NHibernate/Collection/AbstractPersistentCollection.cs b/src/NHibernate/Collection/AbstractPersistentCollection.cs index edb8bbb46d3..13e0957914a 100644 --- a/src/NHibernate/Collection/AbstractPersistentCollection.cs +++ b/src/NHibernate/Collection/AbstractPersistentCollection.cs @@ -6,10 +6,12 @@ using System.Threading; using System.Threading.Tasks; using NHibernate.Collection.Generic; +using NHibernate.Collection.Trackers; using NHibernate.Engine; using NHibernate.Impl; using NHibernate.Loader; using NHibernate.Persister.Collection; +using NHibernate.Proxy; using NHibernate.Type; using NHibernate.Util; @@ -22,9 +24,15 @@ namespace NHibernate.Collection [Serializable] public abstract partial class AbstractPersistentCollection : IPersistentCollection, ILazyInitializedCollection { + // Since v5.3 + [Obsolete("This field has no more usages in NHibernate and will be removed in a future version.")] protected internal static readonly object Unknown = new object(); //place holder + // Since v5.3 + [Obsolete("This field has no more usages in NHibernate and will be removed in a future version.")] protected internal static readonly object NotFound = new object(); //place holder + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected interface IDelayedOperation { object AddedInstance { get; } @@ -54,6 +62,7 @@ public int GetHashCode(object obj) } } + // 6.0 TODO: Remove private class AdditionEnumerable : IEnumerable { private readonly AbstractPersistentCollection enclosingInstance; @@ -110,9 +119,12 @@ public void Reset() [NonSerialized] private ISessionImplementor session; private bool initialized; - [NonSerialized] private List operationQueue; +#pragma warning disable 618 + [NonSerialized] private List operationQueue; // 6.0 TODO: Remove +#pragma warning restore 618 [NonSerialized] private bool directlyAccessible; [NonSerialized] private bool initializing; + [NonSerialized] private AbstractQueueOperationTracker _queueOperationTracker; private object owner; private int cachedSize = -1; @@ -301,26 +313,41 @@ protected virtual bool ReadSize() { return true; } - else + + ThrowLazyInitializationExceptionIfNotConnected(); + var entry = session.PersistenceContext.GetCollectionEntry(this); + var persister = entry.LoadedPersister; + if (persister.IsExtraLazy) { - ThrowLazyInitializationExceptionIfNotConnected(); - CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); - ICollectionPersister persister = entry.LoadedPersister; - if (persister.IsExtraLazy) + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) { if (HasQueuedOperations) { session.Flush(); } + cachedSize = persister.GetSize(entry.LoadedKey, session); return true; } + + if (!queueOperationTracker.Cleared && !queueOperationTracker.DatabaseCollectionSize.HasValue) + { + queueOperationTracker.DatabaseCollectionSize = persister.GetSize(entry.LoadedKey, session); + } + + cachedSize = queueOperationTracker.Cleared + ? queueOperationTracker.GetQueueSize() + : queueOperationTracker.GetCollectionSize(); + return true; } + Read(); } - Read(); return false; } + // Since v5.3 + [Obsolete("This method has no more usages in NHibernate and will be removed in a future version.")] protected virtual bool? ReadIndexExistence(object index) { if (!initialized) @@ -341,6 +368,8 @@ protected virtual bool ReadSize() return null; } + // Since v5.3 + [Obsolete("This method has no more usages in NHibernate and will be removed in a future version.")] protected virtual bool? ReadElementExistence(object element) { if (!initialized) @@ -361,6 +390,8 @@ protected virtual bool ReadSize() return null; } + // Since v5.3 + [Obsolete("This method has no more usages in NHibernate and will be removed in a future version.")] protected virtual object ReadElementByIndex(object index) { if (!initialized) @@ -382,6 +413,228 @@ protected virtual object ReadElementByIndex(object index) return Unknown; } + protected virtual bool? ReadKeyExistence(TKey elementKey) + { + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractMapQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + session.Flush(); + } + + return persister.IndexExists(entry.LoadedKey, elementKey, session); + } + + if (queueOperationTracker.ContainsKey(elementKey)) + { + return true; + } + + if (queueOperationTracker.Cleared) + { + return false; + } + + if (queueOperationTracker.IsElementKeyQueuedForDelete(elementKey)) + { + return false; + } + + // As keys are unordered we don't have to calculate the current order of the key + return persister.IndexExists(entry.LoadedKey, elementKey, session); + } + Read(); + } + return null; + } + + protected virtual bool? ReadElementExistence(T element, out bool? existsInDb) + { + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractCollectionQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + session.Flush(); + } + + existsInDb = persister.ElementExists(entry.LoadedKey, element, session); + return existsInDb; + } + + if (queueOperationTracker.ContainsElement(element)) + { + existsInDb = null; + return true; + } + + if (queueOperationTracker.Cleared) + { + existsInDb = null; + return false; + } + + if (queueOperationTracker.IsElementQueuedForDelete(element)) + { + existsInDb = null; + return false; + } + + existsInDb = persister.ElementExists(entry.LoadedKey, element, session); + return existsInDb; + } + Read(); + } + + existsInDb = null; + return null; + } + + protected virtual bool? TryReadElementByKey(TKey elementKey, out TValue element, out bool? existsInDb) + { + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractMapQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + session.Flush(); + } + } + else + { + if (queueOperationTracker.TryGetElementByKey(elementKey, out element)) + { + existsInDb = null; + return true; + } + + if (queueOperationTracker.Cleared) + { + existsInDb = null; + return false; + } + + if (queueOperationTracker.IsElementKeyQueuedForDelete(elementKey)) + { + existsInDb = null; + return false; + } + } + + var elementByKey = persister.GetElementByIndex(entry.LoadedKey, elementKey, session, owner); + if (persister.NotFoundObject == elementByKey) + { + element = default(TValue); + existsInDb = false; + return false; + } + + element = (TValue) elementByKey; + existsInDb = true; + return true; + } + Read(); + } + + element = default(TValue); + existsInDb = null; + return null; + } + + protected virtual bool? TryReadElementAtIndex(int index, out T element) + { + if (!initialized) + { + ThrowLazyInitializationExceptionIfNotConnected(); + CollectionEntry entry = session.PersistenceContext.GetCollectionEntry(this); + ICollectionPersister persister = entry.LoadedPersister; + if (persister.IsExtraLazy) + { + var queueOperationTracker = (AbstractCollectionQueueOperationTracker) GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + if (HasQueuedOperations) + { + session.Flush(); + } + } + else if (HasQueuedOperations) + { + if (queueOperationTracker.RequiresFlushing(nameof(queueOperationTracker.TryGetElementAtIndex))) + { + session.Flush(); + } + else + { + if (!queueOperationTracker.DatabaseCollectionSize.HasValue && + queueOperationTracker.RequiresDatabaseCollectionSize(nameof(queueOperationTracker.TryGetElementAtIndex)) && + !ReadSize()) + { + element = default(T); + return null; // The collection was loaded + } + + if (queueOperationTracker.TryGetElementAtIndex(index, out element)) + { + return true; + } + + if (queueOperationTracker.Cleared) + { + return false; + } + + // We have to calculate the database index based on the changes in the queue + var dbIndex = queueOperationTracker.GetDatabaseElementIndex(index); + if (!dbIndex.HasValue) + { + element = default(T); + return false; + } + + index = dbIndex.Value; + } + } + + var elementByIndex = persister.GetElementByIndex(entry.LoadedKey, index, session, owner); + if (persister.NotFoundObject == elementByIndex) + { + element = default(T); + return false; + } + + element = (T) elementByIndex; + return true; + } + Read(); + } + + element = default(T); + return null; + } + /// /// Called by any writer method of the collection interface /// @@ -391,9 +644,30 @@ protected virtual void Write() Dirty(); } + internal virtual AbstractQueueOperationTracker CreateQueueOperationTracker() => null; + + internal AbstractQueueOperationTracker QueueOperationTracker + { + get => _queueOperationTracker; + set => _queueOperationTracker = value; + } + + internal AbstractQueueOperationTracker GetOrCreateQueueOperationTracker() + { + if (_queueOperationTracker != null) + { + return _queueOperationTracker; + } + + _queueOperationTracker = CreateQueueOperationTracker(); + return _queueOperationTracker; + } + /// /// Queue an addition, delete etc. if the persistent collection supports it /// + // Since v5.3 + [Obsolete("This method has no more usages in NHibernate and will be removed in a future version.")] protected virtual void QueueOperation(IDelayedOperation element) { if (operationQueue == null) @@ -404,6 +678,114 @@ protected virtual void QueueOperation(IDelayedOperation element) dirty = true; //needed so that we remove this collection from the second-level cache } + protected bool QueueAddElement(T element) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.AddElement)); + return queueOperationTracker.AddElement(element); + } + + protected void QueueRemoveExistingElement(T element, bool? existsInDb) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.RemoveExistingElement), out var wasFlushed); + queueOperationTracker.RemoveExistingElement(element, wasFlushed ? true : existsInDb); + } + + protected void QueueRemoveElementAtIndex(int index, T element) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.RemoveElementAtIndex)); + queueOperationTracker.RemoveElementAtIndex(index, element); + } + + protected void QueueAddElementAtIndex(int index, T element) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.AddElementAtIndex)); + queueOperationTracker.AddElementAtIndex(index, element); + } + + protected void QueueSetElementAtIndex(int index, T element, T oldElement) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractCollectionQueueOperationTracker.SetElementAtIndex)); + queueOperationTracker.SetElementAtIndex(index, element, oldElement); + } + + protected void QueueClearCollection() + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractQueueOperationTracker.ClearCollection), out _); + queueOperationTracker.ClearCollection(); + } + + protected void QueueAddElementByKey(TKey elementKey, TValue element) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractMapQueueOperationTracker.AddElementByKey)); + queueOperationTracker.AddElementByKey(elementKey, element); + } + + protected void QueueSetElementByKey(TKey elementKey, TValue element, TValue oldElement, bool? existsInDb) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractMapQueueOperationTracker.SetElementByKey)); + queueOperationTracker.SetElementByKey(elementKey, element, oldElement, existsInDb); + } + + protected bool QueueRemoveElementByKey(TKey elementKey, TValue oldElement, bool? existsInDb) + { + var queueOperationTracker = TryFlushAndGetQueueOperationTracker(nameof(AbstractMapQueueOperationTracker.RemoveElementByKey)); + return queueOperationTracker.RemoveElementByKey(elementKey, oldElement, existsInDb); + } + + private AbstractMapQueueOperationTracker TryFlushAndGetQueueOperationTracker(string operationName) + { + return (AbstractMapQueueOperationTracker) TryFlushAndGetQueueOperationTracker(operationName, out _); + } + + private AbstractCollectionQueueOperationTracker TryFlushAndGetQueueOperationTracker(string operationName) + { + return (AbstractCollectionQueueOperationTracker) TryFlushAndGetQueueOperationTracker(operationName, out _); + } + + private AbstractCollectionQueueOperationTracker TryFlushAndGetQueueOperationTracker(string operationName, out bool wasFlushed) + { + return (AbstractCollectionQueueOperationTracker) TryFlushAndGetQueueOperationTracker(operationName, out wasFlushed); + } + + private AbstractQueueOperationTracker TryFlushAndGetQueueOperationTracker(string operationName, out bool wasFlushed) + { + var queueOperationTracker = GetOrCreateQueueOperationTracker(); + if (queueOperationTracker == null) + { + throw new InvalidOperationException($"{nameof(CreateQueueOperationTracker)} must return a not null value."); + } + + if (queueOperationTracker.RequiresFlushing(operationName)) + { + session.Flush(); + wasFlushed = true; + } + else + { + wasFlushed = false; + } + + queueOperationTracker.BeforeOperation(operationName); + if (!queueOperationTracker.DatabaseCollectionSize.HasValue && queueOperationTracker.RequiresDatabaseCollectionSize(operationName) && !ReadSize()) + { + throw new InvalidOperationException($"The collection role {Role} must be mapped as extra lazy."); + } + + dirty = true; // Needed so that we remove this collection from the second-level cache + return queueOperationTracker; + } + + internal bool IsTransient(object element) + { + var queryableCollection = (IQueryableCollection) Session.Factory.GetCollectionPersister(Role); + return + queryableCollection != null && + queryableCollection.ElementType.IsEntityType && + !element.IsProxy() && + !Session.PersistenceContext.IsEntryFor(element) && + ForeignKeys.IsTransientFast(queryableCollection.ElementPersister.EntityName, element, Session) == true; + } + // Obsolete since v5.2 /// /// After reading all existing elements from the database, @@ -451,6 +833,7 @@ public void SetSnapshot(object key, string role, object snapshot) public virtual void PostAction() { operationQueue = null; + _queueOperationTracker?.AfterFlushing(); cachedSize = -1; ClearDirty(); } @@ -481,7 +864,7 @@ public virtual bool AfterInitialize(ICollectionPersister persister) { SetInitialized(); cachedSize = -1; - return operationQueue == null; + return operationQueue == null && _queueOperationTracker == null; } /// @@ -661,10 +1044,7 @@ public bool WasInitialized } /// Does this instance have any "queued" additions? - public bool HasQueuedOperations - { - get { return operationQueue != null && operationQueue.Count > 0; } - } + public bool HasQueuedOperations => (operationQueue != null && operationQueue.Count > 0) || _queueOperationTracker?.HasChanges() == true; /// public IEnumerable QueuedAdditionIterator @@ -673,6 +1053,13 @@ public IEnumerable QueuedAdditionIterator { if (HasQueuedOperations) { + // Use the queue operation tracker when available to get the added elements as AdditionEnumerable + // does not work correctly when the item is added and then removed + if (_queueOperationTracker != null) + { + return _queueOperationTracker.GetAddedElements(); + } + return new AdditionEnumerable(this); } else @@ -686,20 +1073,35 @@ public ICollection GetQueuedOrphans(string entityName) { if (HasQueuedOperations) { - List additions = new List(operationQueue.Count); - List removals = new List(operationQueue.Count); - for (int i = 0; i < operationQueue.Count; i++) + List additions; + List removals; + + // Use the queue operation tracker when available to get the orphans as the default logic + // does not work correctly when readding a transient entity. Removals list should + // contain only entities that are already in the database. + if (_queueOperationTracker != null) { - IDelayedOperation op = operationQueue[i]; - if (op.AddedInstance != null) - { - additions.Add(op.AddedInstance); - } - if (op.Orphan != null) + removals = new List(_queueOperationTracker.GetOrphans().Cast()); + additions = new List(_queueOperationTracker.GetAddedElements().Cast()); + } + else // 6.0 TODO: Remove whole else block + { + additions = new List(operationQueue.Count); + removals = new List(operationQueue.Count); + for (int i = 0; i < operationQueue.Count; i++) { - removals.Add(op.Orphan); + var op = operationQueue[i]; + if (op.AddedInstance != null) + { + additions.Add(op.AddedInstance); + } + if (op.Orphan != null) + { + removals.Add(op.Orphan); + } } } + return GetOrphans(removals, additions, entityName, session); } diff --git a/src/NHibernate/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Collection/Generic/PersistentGenericBag.cs index 6db7b695ecf..98c916717c3 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericBag.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericBag.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -48,7 +49,7 @@ public partial class PersistentGenericBag : AbstractPersistentCollection, ILi * ! */ private IList _gbag; - private bool _isOneToMany; + private bool _isOneToMany; // 6.0 TODO: Remove public PersistentGenericBag() { @@ -67,6 +68,19 @@ public PersistentGenericBag(ISessionImplementor session, IEnumerable coll) IsDirectlyAccessible = true; } + internal override AbstractQueueOperationTracker CreateQueueOperationTracker() + { + var entry = Session.PersistenceContext.GetCollectionEntry(this); + return new BagQueueOperationTracker(entry.LoadedPersister); + } + + public override void ApplyQueuedOperations() + { + var queueOperation = (BagQueueOperationTracker) QueueOperationTracker; + queueOperation?.ApplyChanges(_gbag); + QueueOperationTracker = null; + } + protected IList InternalBag { get { return _gbag; } @@ -119,15 +133,16 @@ int IList.IndexOf(object value) int IList.Add(object value) { - Add((T) value); + if (!IsOperationQueueEnabled || !ReadSize()) + { + Write(); + return ((IList) _gbag).Add((T) value); + } + + var val = (T) value; + QueueAddElement(val); - //TODO: take a look at this - I don't like it because it changes the - // meaning of Add - instead of returning the index it was added at - // returns a "fake" index - not consistent with IList interface... - var count = !IsOperationQueueEnabled - ? _gbag.Count - : 0; - return count - 1; + return CachedSize; } void IList.Insert(int index, object value) @@ -181,7 +196,7 @@ public void Add(T item) } else { - QueueOperation(new SimpleAddDelayedOperation(this, item)); + QueueAddElement(item); } } @@ -189,7 +204,7 @@ public void Clear() { if (ClearQueueEnabled) { - QueueOperation(new ClearDelayedOperation(this)); + QueueClearCollection(); } else { @@ -204,7 +219,7 @@ public void Clear() public bool Contains(T item) { - return ReadElementExistence(item) ?? _gbag.Contains(item); + return ReadElementExistence(item, out _) ?? _gbag.Contains(item); } public void CopyTo(T[] array, int arrayIndex) @@ -501,78 +516,5 @@ private static int CountOccurrences(object element, IEnumerable list, IType elem return result; } - - private sealed class ClearDelayedOperation : IDelayedOperation - { - private readonly PersistentGenericBag _enclosingInstance; - - public ClearDelayedOperation(PersistentGenericBag enclosingInstance) - { - _enclosingInstance = enclosingInstance; - } - - public object AddedInstance - { - get { return null; } - } - - public object Orphan - { - get { throw new NotSupportedException("queued clear cannot be used with orphan delete"); } - } - - public void Operate() - { - _enclosingInstance._gbag.Clear(); - } - } - - private sealed class SimpleAddDelayedOperation : IDelayedOperation - { - private readonly PersistentGenericBag _enclosingInstance; - private readonly T _value; - - public SimpleAddDelayedOperation(PersistentGenericBag enclosingInstance, T value) - { - _enclosingInstance = enclosingInstance; - _value = value; - } - - public object AddedInstance - { - get { return _value; } - } - - public object Orphan - { - get { return null; } - } - - public void Operate() - { - // NH Different behavior for NH-739. A "bag" mapped as a bidirectional one-to-many of an entity with an - // id generator causing it to be inserted on flush must not replay the addition after initialization, - // if the entity was previously saved. In that case, the entity save has inserted it in database with - // its association to the bag, without causing a full flush. So for the bag, the operation is still - // pending, but in database it is already done. On initialization, the bag thus already receives the - // entity in its loaded list, and it should not be added again. - // Since a one-to-many bag is actually a set, we can simply check if the entity is already in the loaded - // state, and discard it if yes. (It also relies on the bag not having pending removes, which is the - // case, since it only handles delayed additions and clears.) - // Since this condition happens with transient instances added in the bag then saved, ReferenceEquals - // is enough to match them. - // This solution is a workaround, the root cause is not fixed. The root cause is the insertion on save - // done without caring for pending operations of one-to-many collections. This root cause could be fixed - // by triggering a full flush there before the insert (currently it just flushes pending inserts), or - // maybe by flushing just the dirty one-to-many non-initialized collections (but this can be tricky). - // (It is also likely one-to-many lists have a similar issue, but nothing is done yet for them. And - // their case is more complex due to having to care for the indexes and to handle many more delayed - // operation kinds.) - if (_enclosingInstance._isOneToMany && _enclosingInstance._gbag.Any(l => ReferenceEquals(l, _value))) - return; - - _enclosingInstance._gbag.Add(_value); - } - } } } diff --git a/src/NHibernate/Collection/Generic/PersistentGenericList.cs b/src/NHibernate/Collection/Generic/PersistentGenericList.cs index bf8082395f1..626b0f2fc12 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericList.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericList.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -53,6 +54,12 @@ public PersistentGenericList(ISessionImplementor session, IList list) : base( IsDirectlyAccessible = true; } + internal override AbstractQueueOperationTracker CreateQueueOperationTracker() + { + var entry = Session.PersistenceContext.GetCollectionEntry(this); + return new ListQueueOperationTracker(entry.LoadedPersister); + } + public override object GetSnapshot(ICollectionPersister persister) { var clonedList = new List(WrappedList.Count); @@ -99,6 +106,13 @@ public override void BeforeInitialize(ICollectionPersister persister, int antici WrappedList = (IList) persister.CollectionType.Instantiate(anticipatedSize); } + public override void ApplyQueuedOperations() + { + var queueOperation = (ListQueueOperationTracker) QueueOperationTracker; + queueOperation?.ApplyChanges(WrappedList); + QueueOperationTracker = null; + } + public override bool IsWrapper(object collection) { return ReferenceEquals(WrappedList, collection); @@ -251,7 +265,9 @@ int IList.Add(object value) return ((IList)WrappedList).Add(value); } - QueueOperation(new SimpleAddDelayedOperation(this, (T) value)); + var val = (T) value; + QueueAddElement(val); + //TODO: take a look at this - I don't like it because it changes the // meaning of Add - instead of returning the index it was added at // returns a "fake" index - not consistent with IList interface... @@ -267,7 +283,7 @@ public void Clear() { if (ClearQueueEnabled) { - QueueOperation(new ClearDelayedOperation(this)); + QueueClearCollection(); } else { @@ -299,17 +315,20 @@ public void RemoveAt(int index) { if (index < 0) { - throw new IndexOutOfRangeException("negative index"); + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); } - object old = PutQueueEnabled ? ReadElementByIndex(index) : Unknown; - if (old == Unknown) + + var found = TryReadElementAtIndex(index, out var element); + if (!found.HasValue) { Write(); WrappedList.RemoveAt(index); } else { - QueueOperation(new RemoveDelayedOperation(this, index, old == NotFound ? null : old)); + QueueRemoveElementAtIndex(index, element); } } @@ -343,7 +362,7 @@ public void Insert(int index, T item) { if (index < 0) { - throw new IndexOutOfRangeException("negative index"); + throw new ArgumentOutOfRangeException(nameof(index), "Index must be within the bounds of the List."); } if (!IsOperationQueueEnabled) { @@ -352,7 +371,7 @@ public void Insert(int index, T item) } else { - QueueOperation(new AddDelayedOperation(this, index, item)); + QueueAddElementAtIndex(index, item); } } @@ -362,39 +381,53 @@ public T this[int index] { if (index < 0) { - throw new IndexOutOfRangeException("negative index"); + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); } - object result = ReadElementByIndex(index); - if (result == Unknown) + + var found = TryReadElementAtIndex(index, out var element); + if (!found.HasValue) { return WrappedList[index]; } - if (result == NotFound) + if (!found.Value) { // check if the index is valid if (index >= Count) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); } return default(T); } - return (T) result; + return element; } set { if (index < 0) { - throw new IndexOutOfRangeException("negative index"); + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); } - object old = PutQueueEnabled ? ReadElementByIndex(index) : Unknown; - if (old == Unknown) + + var old = default(T); + var found = PutQueueEnabled ? TryReadElementAtIndex(index, out old) : null; + if (!found.HasValue) { Write(); WrappedList[index] = value; } else { - QueueOperation(new SetDelayedOperation(this, index, value, old == NotFound ? null : old)); + if (EqualityComparer.Default.Equals(value, old)) + { + return; + } + + QueueSetElementAtIndex(index, value, old); } } } @@ -445,13 +478,13 @@ public void Add(T item) } else { - QueueOperation(new SimpleAddDelayedOperation(this, item)); + QueueAddElement(item); } } public bool Contains(T item) { - return ReadElementExistence(item) ?? WrappedList.Contains(item); + return ReadElementExistence(item, out _) ?? WrappedList.Contains(item); } public void CopyTo(T[] array, int arrayIndex) @@ -466,7 +499,8 @@ bool ICollection.IsReadOnly { public bool Remove(T item) { - bool? exists = PutQueueEnabled ? ReadElementExistence(item) : null; + bool? existsInDb = null; + bool? exists = PutQueueEnabled ? ReadElementExistence(item, out existsInDb) : null; if (!exists.HasValue) { Initialize(true); @@ -479,9 +513,11 @@ public bool Remove(T item) } else if (exists.Value) { - QueueOperation(new SimpleRemoveDelayedOperation(this, item)); + QueueRemoveExistingElement(item, existsInDb); + return true; } + return false; } @@ -524,6 +560,8 @@ IEnumerator IEnumerable.GetEnumerator() #region DelayedOperations + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class ClearDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -549,6 +587,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SimpleAddDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -576,6 +616,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class AddDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -605,6 +647,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SetDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -636,6 +680,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class RemoveDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; @@ -665,6 +711,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SimpleRemoveDelayedOperation : IDelayedOperation { private readonly PersistentGenericList _enclosingInstance; diff --git a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs index 6133897c80d..c3a1e6c8cb4 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -56,6 +57,12 @@ public PersistentGenericMap(ISessionImplementor session, IDictionary(entry.LoadedPersister); + } + public override object GetSnapshot(ICollectionPersister persister) { Dictionary clonedMap = new Dictionary(WrappedMap.Count); @@ -108,6 +115,13 @@ public override void BeforeInitialize(ICollectionPersister persister, int antici WrappedMap = (IDictionary)persister.CollectionType.Instantiate(anticipatedSize); } + public override void ApplyQueuedOperations() + { + var queueOperation = (AbstractMapQueueOperationTracker) QueueOperationTracker; + queueOperation?.ApplyChanges(WrappedMap); + QueueOperationTracker = null; + } + public override bool Empty { get { return (WrappedMap.Count == 0); } @@ -242,8 +256,7 @@ public override bool EntryExists(object entry, int i) public bool ContainsKey(TKey key) { - bool? exists = ReadIndexExistence(key); - return !exists.HasValue ? WrappedMap.ContainsKey(key) : exists.Value; + return ReadKeyExistence(key) ?? WrappedMap.ContainsKey(key); } public void Add(TKey key, TValue value) @@ -254,13 +267,20 @@ public void Add(TKey key, TValue value) } if (PutQueueEnabled) { - object old = ReadElementByIndex(key); - if (old != Unknown) + var found = TryReadElementByKey(key, out _, out _); + if (found.HasValue) { - QueueOperation(new PutDelayedOperation(this, key, value, old == NotFound ? null : old)); + if (found.Value) + { + throw new ArgumentException("An item with the same key has already been added."); // Mimic dictionary behavior + } + + QueueAddElementByKey(key, value); + return; } } + Initialize(true); WrappedMap.Add(key, value); Dirty(); @@ -268,8 +288,10 @@ public void Add(TKey key, TValue value) public bool Remove(TKey key) { - object old = PutQueueEnabled ? ReadElementByIndex(key) : Unknown; - if (old == Unknown) // queue is not enabled for 'puts', or element not found + var oldValue = default(TValue); + var existsInDb = default(bool?); + var found = PutQueueEnabled ? TryReadElementByKey(key, out oldValue, out existsInDb) : null; + if (!found.HasValue) // queue is not enabled for 'puts' or collection was initialized { Initialize(true); bool contained = WrappedMap.Remove(key); @@ -280,66 +302,71 @@ public bool Remove(TKey key) return contained; } - QueueOperation(new RemoveDelayedOperation(this, key, old == NotFound ? null : old)); - return true; + return QueueRemoveElementByKey(key, oldValue, existsInDb); } public bool TryGetValue(TKey key, out TValue value) { - object result = ReadElementByIndex(key); - if (result == Unknown) + var found = TryReadElementByKey(key, out value, out _); + if (!found.HasValue) // collection was initialized { return WrappedMap.TryGetValue(key, out value); } - if(result == NotFound) + + if (found.Value) { - value = default(TValue); - return false; + return true; } - value = (TValue)result; - return true; + + value = default(TValue); + return false; } public TValue this[TKey key] { get { - object result = ReadElementByIndex(key); - if (result == Unknown) + var found = TryReadElementByKey(key, out var value, out _); + if (!found.HasValue) // collection was initialized { return WrappedMap[key]; } - if (result == NotFound) + + if (!found.Value) { throw new KeyNotFoundException(); } - return (TValue) result; + + return value; } set { // NH Note: the assignment in NET work like the put method in JAVA (mean assign or add) if (PutQueueEnabled) { - object old = ReadElementByIndex(key); - if (old != Unknown) + var found = TryReadElementByKey(key, out var oldValue, out var existsInDb); + if (found.HasValue) { - QueueOperation(new PutDelayedOperation(this, key, value, old == NotFound ? null : old)); + QueueSetElementByKey(key, value, oldValue, existsInDb); + return; } } + Initialize(true); - TValue tempObject; - WrappedMap.TryGetValue(key, out tempObject); - WrappedMap[key] = value; - TValue old2 = tempObject; - // would be better to use the element-type to determine - // whether the old and the new are equal here; the problem being - // we do not necessarily have access to the element type in all - // cases - if (!ReferenceEquals(value, old2)) + if (!WrappedMap.TryGetValue(key, out var old)) { + WrappedMap.Add(key, value); Dirty(); } + else + { + WrappedMap[key] = value; + if (!EqualityComparer.Default.Equals(value, old)) + { + Dirty(); + } + } } } @@ -383,7 +410,7 @@ public void Clear() { if (ClearQueueEnabled) { - QueueOperation(new ClearDelayedOperation(this)); + QueueClearCollection(); } else { @@ -398,7 +425,7 @@ public void Clear() public bool Contains(KeyValuePair item) { - bool? exists = ReadIndexExistence(item.Key); + bool? exists = ReadKeyExistence(item.Key); if (!exists.HasValue) { return WrappedMap.Contains(item); @@ -493,6 +520,8 @@ IEnumerator IEnumerable.GetEnumerator() #region DelayedOperations + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class ClearDelayedOperation : IDelayedOperation { private readonly PersistentGenericMap _enclosingInstance; @@ -518,6 +547,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class PutDelayedOperation : IDelayedOperation { private readonly PersistentGenericMap _enclosingInstance; @@ -549,6 +580,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class RemoveDelayedOperation : IDelayedOperation { private readonly PersistentGenericMap _enclosingInstance; diff --git a/src/NHibernate/Collection/Generic/PersistentGenericSet.cs b/src/NHibernate/Collection/Generic/PersistentGenericSet.cs index bec3cc7505f..c4df138fe5a 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericSet.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericSet.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Linq.Expressions; using NHibernate.Collection.Generic.SetHelpers; +using NHibernate.Collection.Trackers; using NHibernate.DebugHelpers; using NHibernate.Engine; using NHibernate.Linq; @@ -73,6 +74,12 @@ public PersistentGenericSet(ISessionImplementor session, ISet original) IsDirectlyAccessible = true; } + internal override AbstractQueueOperationTracker CreateQueueOperationTracker() + { + var entry = Session.PersistenceContext.GetCollectionEntry(this); + return new SetQueueOperationTracker(entry.LoadedPersister); + } + public override bool RowUpdatePossible { get { return false; } @@ -129,6 +136,13 @@ public override void BeforeInitialize(ICollectionPersister persister, int antici WrappedSet = (ISet)persister.CollectionType.Instantiate(anticipatedSize); } + public override void ApplyQueuedOperations() + { + var queueOperation = (SetQueueOperationTracker) QueueOperationTracker; + queueOperation?.ApplyChanges(WrappedSet); + QueueOperationTracker = null; + } + /// /// Initializes this PersistentSet from the cached values. /// @@ -294,30 +308,34 @@ public override bool IsWrapper(object collection) public bool Contains(T item) { - bool? exists = ReadElementExistence(item); - return exists == null ? WrappedSet.Contains(item) : exists.Value; + return ReadElementExistence(item, out _) ?? WrappedSet.Contains(item); } public bool Add(T o) { - bool? exists = IsOperationQueueEnabled ? ReadElementExistence(o) : null; - if (!exists.HasValue) + // Skip checking the element existence in the database if we know that the element + // is transient and the operation queue is enabled + if (WasInitialized || !IsOperationQueueEnabled || !IsTransient(o)) { - Initialize(true); - if (WrappedSet.Add(o)) + var exists = IsOperationQueueEnabled ? ReadElementExistence(o, out _) : null; + if (!exists.HasValue) { - Dirty(); - return true; + Initialize(true); + if (WrappedSet.Add(o)) + { + Dirty(); + return true; + } + return false; } - return false; - } - if (exists.Value) - { - return false; + if (exists.Value) + { + return false; + } } - QueueOperation(new SimpleAddDelayedOperation(this, o)); - return true; + + return QueueAddElement(o); } public void UnionWith(IEnumerable other) @@ -420,7 +438,8 @@ public bool SetEquals(IEnumerable other) public bool Remove(T o) { - bool? exists = PutQueueEnabled ? ReadElementExistence(o) : null; + var existsInDb = default(bool?); + var exists = PutQueueEnabled ? ReadElementExistence(o, out existsInDb) : null; if (!exists.HasValue) { Initialize(true); @@ -434,9 +453,11 @@ public bool Remove(T o) if (exists.Value) { - QueueOperation(new SimpleRemoveDelayedOperation(this, o)); + QueueRemoveExistingElement(o, existsInDb); + return true; } + return false; } @@ -444,7 +465,7 @@ public void Clear() { if (ClearQueueEnabled) { - QueueOperation(new ClearDelayedOperation(this)); + QueueClearCollection(); } else { @@ -531,6 +552,8 @@ public IEnumerator GetEnumerator() #region DelayedOperations + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class ClearDelayedOperation : IDelayedOperation { private readonly PersistentGenericSet _enclosingInstance; @@ -556,6 +579,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SimpleAddDelayedOperation : IDelayedOperation { private readonly PersistentGenericSet _enclosingInstance; @@ -583,6 +608,8 @@ public void Operate() } } + // Since v5.3 + [Obsolete("This class has no more usages in NHibernate and will be removed in a future version.")] protected sealed class SimpleRemoveDelayedOperation : IDelayedOperation { private readonly PersistentGenericSet _enclosingInstance; diff --git a/src/NHibernate/Collection/Trackers/AbstractCollectionQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/AbstractCollectionQueueOperationTracker.cs new file mode 100644 index 00000000000..f4ad28b8bf0 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/AbstractCollectionQueueOperationTracker.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; + +namespace NHibernate.Collection.Trackers +{ + /// + internal abstract class AbstractCollectionQueueOperationTracker : AbstractQueueOperationTracker + { + /// + /// A method that is called when an element is added to the collection. + /// + /// The element to add. + /// True whether the element was successfully added to the queue, false otherwise + public abstract bool AddElement(T element); + + /// + /// A method that is called when an existing element is removed from the collection. + /// + /// The element to remove. + /// Whether the element exists in the database. + public abstract void RemoveExistingElement(T element, bool? existsInDb); + + /// + /// Checks whether the element exists in the queue. + /// + /// The element to check. + /// True whether the element exists in the queue, false otherwise. + public abstract bool ContainsElement(T element); + + /// + /// Checks whether the element is queued for removal. + /// + /// The element to check. + /// True whether the element is queued for removal, false otherwise. + public abstract bool IsElementQueuedForDelete(T element); + + #region Indexed operations + + /// + /// A method that is called when an element is removed by its index from the collection. + /// + /// The index of the element. + /// The element to remove. + public abstract void RemoveElementAtIndex(int index, T element); + + /// + /// A method that is called when an element is added at a specific index of the collection. + /// + /// The index to put the element. + /// The element to add. + public abstract void AddElementAtIndex(int index, T element); + + /// + /// A method that is called when an element is set at a specific index of the collection. + /// + /// The index to set the new element. + /// The element to set. + /// The element that currently occupies the . + public abstract void SetElementAtIndex(int index, T element, T oldElement); + + /// + /// Tries to retrieve the element by a specific index of the collection. + /// + /// The index to put the element. + /// The output variable for the element. + /// True whether the element was found, false otherwise. + public abstract bool TryGetElementAtIndex(int index, out T element); + + /// + /// Gets the element index where it currently lies in the database by taking into the consideration the queued operations. + /// + /// The effective index that will be when all operations would be flushed. + /// The element index in the database or -1 if the index represents a transient element. + public abstract int? GetDatabaseElementIndex(int index); + + #endregion + } + + internal abstract class AbstractCollectionQueueOperationTracker : AbstractCollectionQueueOperationTracker + where TCollection : ICollection + { + /// + /// Applies all the queued changes to the loaded collection. + /// + /// The loaded collection. + public abstract void ApplyChanges(TCollection loadedCollection); + } +} diff --git a/src/NHibernate/Collection/Trackers/AbstractMapQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/AbstractMapQueueOperationTracker.cs new file mode 100644 index 00000000000..70e9701bfaa --- /dev/null +++ b/src/NHibernate/Collection/Trackers/AbstractMapQueueOperationTracker.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; + +namespace NHibernate.Collection.Trackers +{ + /// + internal abstract class AbstractMapQueueOperationTracker : AbstractQueueOperationTracker + { + /// + /// Tries to retrieve a queued element by its key. + /// + /// The element key. + /// The output variable for the element. + /// True whether the element was found, false otherwise. + public abstract bool TryGetElementByKey(TKey elementKey, out TValue element); + + /// + /// Checks whether the key exist in the queue. + /// + /// The key to check. + /// True whether it exists, false otherwise. + public abstract bool ContainsKey(TKey key); + + /// + /// A method that is called when the map method is called. + /// + /// The key to add. + /// The element to add + public abstract void AddElementByKey(TKey elementKey, TValue element); + + /// + /// A method that is called when the map is set. + /// + /// The key to set. + /// The element to set. + /// The element that currently occupies the . + /// Whether the element exists in the database. + public abstract void SetElementByKey(TKey elementKey, TValue element, TValue oldElement, bool? existsInDb); + + /// + /// A method that is called when the map is called. + /// + /// The key to remove. + /// The element that currently occupies the . + /// Whether the element exists in the database. + /// True whether the key was successfully removed from the queue. + public abstract bool RemoveElementByKey(TKey elementKey, TValue oldElement, bool? existsInDb); + + /// + /// Checks whether the element key is queued for removal. + /// + /// The element key to check. + /// True whether the element key is queued for removal, false otherwise. + public abstract bool IsElementKeyQueuedForDelete(TKey elementKey); + + /// + /// Applies all the queued changes to the loaded map. + /// + /// The loaded map. + public abstract void ApplyChanges(IDictionary loadedMap); + } +} diff --git a/src/NHibernate/Collection/Trackers/AbstractQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/AbstractQueueOperationTracker.cs new file mode 100644 index 00000000000..cb503503422 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/AbstractQueueOperationTracker.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Collection.Trackers +{ + /// + /// A tracker that is able to track changes that are done to an uninitialized collection. + /// + internal abstract class AbstractQueueOperationTracker + { + internal static HashSet IndexOperations = new HashSet + { + nameof(AbstractCollectionQueueOperationTracker.RemoveElementAtIndex), + nameof(AbstractCollectionQueueOperationTracker.AddElementAtIndex), + nameof(AbstractCollectionQueueOperationTracker.SetElementAtIndex), + nameof(AbstractCollectionQueueOperationTracker.TryGetElementAtIndex) + }; + + /// + /// The number of elements that the collection have in the database. + /// + public virtual int? DatabaseCollectionSize { get; protected internal set; } + + /// + /// Whether the Clear operation was performed on the uninitialized collection. + /// + public virtual bool Cleared { get; protected set; } + + /// + /// Returns the current size of the queue that can be negative when there are more removed than added elements. + /// + /// The queue size. + public abstract int GetQueueSize(); + + /// + /// Returns the current size of the collection by taking into the consideration the queued operations. + /// + /// The current collection size. + public int GetCollectionSize() + { + if (Cleared) + { + return GetQueueSize(); + } + + if (!DatabaseCollectionSize.HasValue) + { + throw new InvalidOperationException($"{nameof(DatabaseCollectionSize)} is not set"); + } + + return DatabaseCollectionSize.Value + GetQueueSize(); + } + + /// + /// Checks whether the database collection size is required for the given operation. + /// + /// The operation name to check. + /// True whether the database collection size is required, false otherwise. + public virtual bool RequiresDatabaseCollectionSize(string operationName) => false; + + /// + /// Checks whether flushing is required for the given operation. + /// + /// The operation name to check. + /// True whether flushing is required, false otherwise. + public virtual bool RequiresFlushing(string operationName) => false; + + /// + /// A method that will be called once the flushing is done. + /// + public abstract void AfterFlushing(); + + /// + /// A method that will be called before an operation. + /// + /// The operation that will be executed. + public virtual void BeforeOperation(string operationName) { } + + /// + /// A method that will be called when a Clear operation is performed on the collection. + /// + public virtual void ClearCollection() + { + Cleared = true; + } + + /// + /// Returns an of elements that were added into the collection. + /// + /// An of added elements. + public abstract IEnumerable GetAddedElements(); + + /// + /// Returns an of orphan elements of the collection. + /// + /// An of orphan elements. + public abstract IEnumerable GetOrphans(); + + /// + /// Checks whether a write operation was performed. + /// + /// True whether a write operation was performed, false otherwise. + public abstract bool HasChanges(); + } +} diff --git a/src/NHibernate/Collection/Trackers/BagQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/BagQueueOperationTracker.cs new file mode 100644 index 00000000000..5b9db6ac08c --- /dev/null +++ b/src/NHibernate/Collection/Trackers/BagQueueOperationTracker.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class BagQueueOperationTracker : CollectionQueueOperationTracker> + { + public BagQueueOperationTracker(ICollectionPersister collectionPersister) : base(collectionPersister) + { + } + + public override void AfterFlushing() + { + // We have to reset the current database collection size in case an element + // was added multiple times + DatabaseCollectionSize = null; + base.AfterFlushing(); + } + + /// + public override void ApplyChanges(IList loadedCollection) + { + if (Cleared) + { + loadedCollection.Clear(); + } + else if (RemovalQueue != null) + { + foreach (var toRemove in RemovalQueue) + { + loadedCollection.Remove(toRemove); + } + } + + if (Queue != null) + { + foreach (var toAdd in Queue) + { + // NH Different behavior for NH-739. A "bag" mapped as a bidirectional one-to-many of an entity with an + // id generator causing it to be inserted on flush must not replay the addition after initialization, + // if the entity was previously saved. In that case, the entity save has inserted it in database with + // its association to the bag, without causing a full flush. So for the bag, the operation is still + // pending, but in database it is already done. On initialization, the bag thus already receives the + // entity in its loaded list, and it should not be added again. + // Since a one-to-many bag is actually a set, we can simply check if the entity is already in the loaded + // state, and discard it if yes. (It also relies on the bag not having pending removes, which is the + // case, since it only handles delayed additions and clears.) + // Since this condition happens with transient instances added in the bag then saved, ReferenceEquals + // is enough to match them. + // This solution is a workaround, the root cause is not fixed. The root cause is the insertion on save + // done without caring for pending operations of one-to-many collections. This root cause could be fixed + // by triggering a full flush there before the insert (currently it just flushes pending inserts), or + // maybe by flushing just the dirty one-to-many non-initialized collections (but this can be tricky). + // (It is also likely one-to-many lists have a similar issue, but nothing is done yet for them. And + // their case is more complex due to having to care for the indexes and to handle many more delayed + // operation kinds.) + if (CollectionPersister.IsOneToMany && loadedCollection.Any(l => ReferenceEquals(l, toAdd))) + { + continue; + } + + loadedCollection.Add(toAdd); + } + } + } + } +} diff --git a/src/NHibernate/Collection/Trackers/ClearedListQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/ClearedListQueueOperationTracker.cs new file mode 100644 index 00000000000..d6549eb5a28 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/ClearedListQueueOperationTracker.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class ClearedListQueueOperationTracker : AbstractCollectionQueueOperationTracker> + { + protected IList Collection; + private readonly ICollectionPersister _collectionPersister; + + public ClearedListQueueOperationTracker(ICollectionPersister collectionPersister) + { + _collectionPersister = collectionPersister; + } + + /// + public override bool AddElement(T element) + { + GetOrCreateQueue().Add(element); + return true; + } + + /// + public override void RemoveExistingElement(T element, bool? existsInDb) + { + Collection?.Remove(element); + } + + /// + public override bool Cleared + { + get => true; + protected set => throw new NotSupportedException(); + } + + public override void AfterFlushing() + { + throw new NotSupportedException(); + } + + /// + public override void ClearCollection() + { + Collection?.Clear(); + } + + /// + public override bool ContainsElement(T element) + { + return Collection?.Contains(element) ?? false; + } + + /// + public override int GetQueueSize() + { + return Collection?.Count ?? 0; + } + + /// + public override bool IsElementQueuedForDelete(T element) + { + return false; + } + + /// + public override bool HasChanges() + { + return true; + } + + /// + public override void ApplyChanges(IList loadedCollection) + { + loadedCollection.Clear(); + if (Collection != null) + { + foreach (var toAdd in Collection) + { + loadedCollection.Add(toAdd); + } + } + } + + /// + public override int? GetDatabaseElementIndex(int index) + { + return null; + } + + /// + public override bool TryGetElementAtIndex(int index, out T element) + { + if (Collection == null || index < 0 || index >= Collection.Count) + { + element = default(T); + return false; + } + + element = Collection[index]; + return true; + } + + /// + public override void RemoveElementAtIndex(int index, T element) + { + Collection?.RemoveAt(index); + } + + /// + public override void AddElementAtIndex(int index, T element) + { + GetOrCreateQueue().Insert(index, element); + } + + /// + public override void SetElementAtIndex(int index, T element, T oldElement) + { + GetOrCreateQueue()[index] = element; + } + + /// + public override IEnumerable GetAddedElements() + { + return Collection ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return Enumerable.Empty(); + } + + private IList GetOrCreateQueue() + { + return Collection ?? (Collection = (IList) _collectionPersister.CollectionType.Instantiate(-1)); + } + } +} diff --git a/src/NHibernate/Collection/Trackers/CollectionQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/CollectionQueueOperationTracker.cs new file mode 100644 index 00000000000..a342e01abc4 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/CollectionQueueOperationTracker.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + internal abstract class CollectionQueueOperationTracker : AbstractCollectionQueueOperationTracker + where TCollection : ICollection + { + protected TCollection Queue; + protected ISet RemovalQueue; + protected int QueueSize; + protected ISet Orphans; + protected readonly ICollectionPersister CollectionPersister; + + protected CollectionQueueOperationTracker(ICollectionPersister collectionPersister) + { + CollectionPersister = collectionPersister; + } + + /// + public override bool AddElement(T element) + { + InitializeQueue(); + RemovalQueue?.Remove(element); // We have to remove the element from the removal list when the element is readded + return Add(element); + } + + protected virtual bool Add(T element) + { + Queue.Add(element); + if (!Cleared) + { + QueueSize++; + } + + return true; + } + + /// + public override void AfterFlushing() + { + Queue?.Clear(); + QueueSize = 0; + Orphans?.Clear(); + RemovalQueue?.Clear(); + } + + /// + public override bool HasChanges() + { + return Cleared || RemovalQueue?.Count > 0 || Queue?.Count > 0; + } + + /// + public override void ApplyChanges(TCollection loadedCollection) + { + if (Cleared) + { + loadedCollection.Clear(); + } + else if (RemovalQueue != null) + { + foreach (var toRemove in RemovalQueue) + { + loadedCollection.Remove(toRemove); + } + } + + if (Queue != null) + { + foreach (var toAdd in Queue) + { + loadedCollection.Add(toAdd); + } + } + } + + /// + public override bool RequiresFlushing(string operationName) + { + return IndexOperations.Contains(operationName); + } + + /// + public override void RemoveExistingElement(T element, bool? existsInDb) + { + if (!Cleared) + { + // As this method is called only when the element exists in the queue or in database, we can safely reduce the queue size + QueueSize--; + } + + if (existsInDb == true) + { + GetOrCreateOrphansSet().Add(element); + } + + GetOrCreateRemovalQueue().Add(element); + Queue?.Remove(element); + } + + /// + public override void ClearCollection() + { + AfterFlushing(); + Cleared = true; + } + + /// + public override bool ContainsElement(T element) + { + return Queue?.Contains(element) ?? false; + } + + /// + public override int GetQueueSize() + { + return Cleared ? (Queue?.Count ?? 0) : QueueSize; + } + + /// + public override bool IsElementQueuedForDelete(T element) + { + return RemovalQueue?.Contains(element) ?? false; + } + + /// + public override IEnumerable GetAddedElements() + { + return (IEnumerable) Queue ?? Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return (IEnumerable) Orphans ?? Enumerable.Empty(); + } + + /// + public override void RemoveElementAtIndex(int index, T element) + { + throw new NotSupportedException(); + } + + /// + public override void AddElementAtIndex(int index, T element) + { + throw new NotSupportedException(); + } + + /// + public override void SetElementAtIndex(int index, T element, T oldElement) + { + throw new NotSupportedException(); + } + + /// + public override bool TryGetElementAtIndex(int index, out T element) + { + throw new NotSupportedException(); + } + + /// + public override int? GetDatabaseElementIndex(int index) + { + throw new NotSupportedException(); + } + + protected ISet GetOrCreateRemovalQueue() + { + return RemovalQueue ?? (RemovalQueue = new HashSet()); + } + + protected ISet GetOrCreateOrphansSet() + { + return Orphans ?? (Orphans = new HashSet()); + } + + private void InitializeQueue() + { + if (Queue == null) + { + Queue = (TCollection) CollectionPersister.CollectionType.Instantiate(-1); + } + } + } +} diff --git a/src/NHibernate/Collection/Trackers/IndexedListQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/IndexedListQueueOperationTracker.cs new file mode 100644 index 00000000000..32c21f1a303 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/IndexedListQueueOperationTracker.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class IndexedListQueueOperationTracker : AbstractCollectionQueueOperationTracker> + { + private static readonly KeyValuePairComparer Comparer = new KeyValuePairComparer(); + private class KeyValuePairComparer : IComparer> + { + public int Compare(KeyValuePair x, KeyValuePair y) + { + if (x.Key > y.Key) + { + return 1; + } + + if (x.Key < y.Key) + { + return -1; + } + + return 0; + } + } + + private List> _queue; // Sorted queue by index + private int _queueSize; + private List> _removedDbIndexes; // Sorted removed db indexes + private ISet _removalQueue; + + /// + public override bool RequiresFlushing(string operationName) + { + // For remove operation we don't know the index of the removal item + return nameof(RemoveExistingElement) == operationName; + } + + /// + public override bool RequiresDatabaseCollectionSize(string operationName) + { + return IndexOperations.Contains(operationName) || nameof(AddElement) == operationName; + } + + /// + public override bool ContainsElement(T element) + { + if (_queue == null) + { + return false; + } + + foreach (var pair in _queue) + { + if (EqualityComparer.Default.Equals(pair.Value, element)) + { + return true; + } + } + + return false; + } + + /// + public override bool IsElementQueuedForDelete(T element) + { + return _removalQueue?.Contains(element) ?? false; + } + + /// + public override bool HasChanges() + { + return _removedDbIndexes?.Count > 0 || _queue?.Count > 0; + } + + /// + public override void ApplyChanges(IList loadedCollection) + { + if (_removedDbIndexes != null) + { + for (var i = _removedDbIndexes.Count - 1; i >= 0; i--) + { + loadedCollection.RemoveAt(_removedDbIndexes[i].Key); + } + } + + if (_queue != null) + { + for (var i = 0; i < _queue.Count; i++) + { + var pair = _queue[i]; + loadedCollection.Insert(pair.Key, pair.Value); + } + } + } + + /// + public override bool AddElement(T element) + { + AddElementAtIndex(GetCollectionSize(), element); + return true; + } + + /// + public override void RemoveExistingElement(T element, bool? existsInDb) + { + throw new NotSupportedException(); + } + + /// + public override void ClearCollection() + { + throw new NotSupportedException(); + } + + /// + public override void AfterFlushing() + { + throw new NotSupportedException(); + } + + /// + public override IEnumerable GetAddedElements() + { + return _queue?.Select(o => o.Value) ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return _removedDbIndexes?.Select(o => o.Value) ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override bool Cleared + { + get => false; + protected set => throw new NotSupportedException(); + } + + /// + public override int GetQueueSize() + { + return _queueSize; + } + + /// + public override int? GetDatabaseElementIndex(int index) + { + if (!DatabaseCollectionSize.HasValue) + { + throw new InvalidOperationException($"{nameof(DatabaseCollectionSize)} is not set"); + } + + var dbIndex = index; + + if (_queue != null) + { + foreach (var pair in _queue) + { + if (pair.Key == index) + { + return null; // The given index represents a queued value + } + + if (pair.Key > index) + { + break; + } + + dbIndex--; + } + } + + if (_removedDbIndexes != null) + { + var i = GetQueueIndex(ref _removedDbIndexes, new KeyValuePair(dbIndex, default(T)), true); + if (i < 0) + { + dbIndex += (Math.Abs(i) - 1); + } + else + { + i++; + dbIndex += i; + + // Increase until we find a non removed db index + for (; i < _removedDbIndexes.Count; i++) + { + if (_removedDbIndexes[i].Key != dbIndex) + { + break; + } + + dbIndex++; + } + } + } + + return dbIndex >= DatabaseCollectionSize + ? (int?) null + : dbIndex; + } + + /// + public override bool TryGetElementAtIndex(int index, out T element) + { + var pair = new KeyValuePair(index, default(T)); + var keyIndex = GetQueueIndex(ref _queue, pair, true); + if (keyIndex < 0) + { + element = default(T); + return false; + } + + element = _queue[keyIndex].Value; + return true; + } + + /// + public override void RemoveElementAtIndex(int index, T element) + { + if (index >= GetCollectionSize()) + { + // Mimic list behavior + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); + } + + var pair = new KeyValuePair(index, default(T)); + var i = GetQueueIndex(ref _queue, pair, true); + if (i < 0) + { + i = Math.Abs(i) - 1; + var dbIndex = GetDatabaseElementIndex(index); + if (!dbIndex.HasValue) + { + throw new InvalidOperationException($"Invalid {nameof(index)} parameter."); + } + + var removedPair = new KeyValuePair(dbIndex.Value, element); + var j = GetQueueIndex(ref _removedDbIndexes, removedPair, true); + if (j < 0) + { + _removedDbIndexes.Insert(Math.Abs(j) - 1, removedPair); + } + } + else + { + _queue.RemoveAt(i); + } + + _queueSize--; + GetOrCreateRemovalQueue().Add(element); + + // We have to decrement all higher indexes by 1 + for (; i < _queue.Count; i++) + { + var currentPair = _queue[i]; + _queue[i] = new KeyValuePair(currentPair.Key - 1, currentPair.Value); + } + } + + /// + public override void AddElementAtIndex(int index, T element) + { + if (index > GetCollectionSize()) + { + // Mimic list behavior + throw new ArgumentOutOfRangeException(nameof(index), "Index must be within the bounds of the List."); + } + + var pair = new KeyValuePair(index, element); + var i = GetQueueIndex(ref _queue, pair, false); + _queue.Insert(i, pair); + i++; + _queueSize++; + _removalQueue?.Remove(element); + + // We have to increment all higher indexes by 1 + for (; i < _queue.Count; i++) + { + var currentPair = _queue[i]; + _queue[i] = new KeyValuePair(currentPair.Key + 1, currentPair.Value); + } + } + + /// + public override void SetElementAtIndex(int index, T element, T oldElement) + { + if (index >= GetCollectionSize()) + { + // Mimic list behavior + throw new ArgumentOutOfRangeException( + nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); + } + + var pair = new KeyValuePair(index, element); + + // NOTE: If we would need to have only items that are removed in the removal queue we would need + // to check the _queue if the item already exists as the item can be re-added to a different index. + // As this scenario should rarely happen and we also know that ContainsElement will be called before + // IsElementQueuedForDelete, we skip the check here to have a better performance. + GetOrCreateRemovalQueue().Add(oldElement); + _removalQueue.Remove(element); + + var i = GetQueueIndex(ref _queue, pair, true); + if (i < 0) + { + var dbIndex = GetDatabaseElementIndex(index); + if (!dbIndex.HasValue) + { + throw new InvalidOperationException($"Invalid {nameof(index)} parameter."); + } + + var removedPair = new KeyValuePair(dbIndex.Value, oldElement); + var j = GetQueueIndex(ref _removedDbIndexes, removedPair, true); + if (j < 0) + { + _removedDbIndexes.Insert(Math.Abs(j) - 1, removedPair); + } + + _queue.Insert(Math.Abs(i) - 1, pair); + } + else + { + _queue[i] = pair; + } + } + + private static int GetQueueIndex(ref List> queue, KeyValuePair pair, bool rawResult) + { + return GetQueueIndex(ref queue, pair, rawResult, Comparer); + } + + private static int GetQueueIndex(ref List queue, TType item, bool rawResult, IComparer comparer) + { + if (queue == null) + { + queue = new List(); + } + + var i = queue.BinarySearch(item, comparer); + if (i < 0) + { + return rawResult ? i : Math.Abs(i) - 1; + } + + return i; + } + + private ISet GetOrCreateRemovalQueue() + { + return _removalQueue ?? (_removalQueue = new HashSet()); + } + } +} diff --git a/src/NHibernate/Collection/Trackers/ListQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/ListQueueOperationTracker.cs new file mode 100644 index 00000000000..caa09999712 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/ListQueueOperationTracker.cs @@ -0,0 +1,184 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class ListQueueOperationTracker : AbstractCollectionQueueOperationTracker> + { + private readonly ICollectionPersister _collectionPersister; + private AbstractCollectionQueueOperationTracker> _tracker; + + public ListQueueOperationTracker(ICollectionPersister collectionPersister) + { + _collectionPersister = collectionPersister; + } + + /// + public override int? DatabaseCollectionSize + { + get => _tracker?.DatabaseCollectionSize ?? base.DatabaseCollectionSize; + protected internal set + { + base.DatabaseCollectionSize = value; + if (_tracker != null) + { + _tracker.DatabaseCollectionSize = value; + } + } + } + + /// + public override bool RequiresDatabaseCollectionSize(string operationName) + { + return _tracker?.RequiresDatabaseCollectionSize(operationName) ?? IndexOperations.Contains(operationName); + } + + /// + public override int GetQueueSize() + { + return _tracker?.GetQueueSize() ?? 0; + } + + /// + public override void ClearCollection() + { + _tracker = new ClearedListQueueOperationTracker(_collectionPersister); + Cleared = true; + } + + /// + public override IEnumerable GetAddedElements() + { + return _tracker?.GetAddedElements() ?? Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return _tracker?.GetOrphans() ?? Enumerable.Empty(); + } + + /// + public override bool RequiresFlushing(string operationName) + { + return _tracker?.RequiresFlushing(operationName) == true; + } + + /// + public override void AfterFlushing() + { + // We have to reset the current database collection size in case an element + // was added multiple times + DatabaseCollectionSize = null; + _tracker = null; + } + + public override void BeforeOperation(string operationName) + { + if (_tracker != null) + { + return; + } + + _tracker = IndexOperations.Contains(operationName) + ? (AbstractCollectionQueueOperationTracker>) new IndexedListQueueOperationTracker + { + DatabaseCollectionSize = DatabaseCollectionSize + } + : new NonIndexedListQueueOperationTracker(_collectionPersister) + { + DatabaseCollectionSize = DatabaseCollectionSize + }; + } + + /// + public override bool AddElement(T element) + { + return GetOrCreateStrategy().AddElement(element); + } + + /// + public override void RemoveExistingElement(T element, bool? existsInDb) + { + GetOrCreateStrategy().RemoveExistingElement(element, existsInDb); + } + + /// + public override bool ContainsElement(T element) + { + return _tracker?.ContainsElement(element) == true; + } + + /// + public override bool IsElementQueuedForDelete(T element) + { + return _tracker?.IsElementQueuedForDelete(element) == true; + } + + /// + public override void RemoveElementAtIndex(int index, T element) + { + GetOrCreateIndexedStrategy().RemoveElementAtIndex(index, element); + } + + /// + public override void AddElementAtIndex(int index, T element) + { + GetOrCreateIndexedStrategy().AddElementAtIndex(index, element); + } + + /// + public override void SetElementAtIndex(int index, T element, T oldElement) + { + GetOrCreateIndexedStrategy().SetElementAtIndex(index, element, oldElement); + } + + /// + public override bool TryGetElementAtIndex(int index, out T element) + { + if (_tracker != null) + { + return _tracker.TryGetElementAtIndex(index, out element); + } + + element = default(T); + return false; + } + + /// + public override int? GetDatabaseElementIndex(int index) + { + return _tracker?.GetDatabaseElementIndex(index) ?? index; + } + + /// + public override void ApplyChanges(IList loadedCollection) + { + _tracker?.ApplyChanges(loadedCollection); + } + + private AbstractCollectionQueueOperationTracker> GetOrCreateStrategy() + { + return _tracker ?? (_tracker = new NonIndexedListQueueOperationTracker(_collectionPersister) + { + DatabaseCollectionSize = DatabaseCollectionSize + }); + } + + private AbstractCollectionQueueOperationTracker> GetOrCreateIndexedStrategy() + { + return _tracker ?? (_tracker = new IndexedListQueueOperationTracker + { + DatabaseCollectionSize = DatabaseCollectionSize + }); + } + + public override bool HasChanges() + { + return _tracker?.HasChanges() ?? false; + } + } +} diff --git a/src/NHibernate/Collection/Trackers/MapQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/MapQueueOperationTracker.cs new file mode 100644 index 00000000000..effe2c5fa86 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/MapQueueOperationTracker.cs @@ -0,0 +1,200 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class MapQueueOperationTracker : AbstractMapQueueOperationTracker + { + private readonly ICollectionPersister _collectionPersister; + private IDictionary _queue; + private IDictionary _orphanMap; + private ISet _removalQueue; + private int _queueSize; + + public MapQueueOperationTracker(ICollectionPersister collectionPersister) + { + _collectionPersister = collectionPersister; + } + + public override void AfterFlushing() + { + _queue?.Clear(); + _removalQueue?.Clear(); + _queueSize = 0; + _orphanMap = null; + } + + /// + public override void ClearCollection() + { + AfterFlushing(); + Cleared = true; + } + + /// + public override IEnumerable GetAddedElements() + { + return _queue?.Values ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override IEnumerable GetOrphans() + { + return _orphanMap?.Values ?? (IEnumerable) Enumerable.Empty(); + } + + /// + public override void AddElementByKey(TKey elementKey, TValue element) + { + _removalQueue?.Remove(elementKey); // We have to remove the key from the removal list when the element is re-added + GetOrCreateQueue().Add(elementKey, element); + if (!Cleared) + { + _queueSize++; + } + } + + /// + public override bool ContainsKey(TKey key) + { + return _queue?.ContainsKey(key) == true; + } + + /// + public override int GetQueueSize() + { + return Cleared ? (_queue?.Count ?? 0) : _queueSize; + } + + /// + public override bool IsElementKeyQueuedForDelete(TKey elementKey) + { + return _removalQueue?.Contains(elementKey) ?? false; + } + + /// + public override bool RemoveElementByKey(TKey elementKey, TValue oldElement, bool? existsInDb) + { + if (Cleared) + { + return _queue?.Remove(elementKey) ?? false; + } + + // We can have the following scenarios: + // 1. remove a key that exists in db and is not in the queue and removal queue (decrease queue size) + // 2. remove a key that exists in db and is in the queue (decrease queue size) + // 3. remove a key that does not exist in db and is not in the queue (don't decrease queue size) + // 4. remove a key that does not exist in db and is in the queue (decrease queue size) + // 5. remove a key that exists in db and is in the removal queue (don't decrease queue size) + + // If the key is not present in the database and in the queue, do nothing + if (existsInDb == false && _queue?.ContainsKey(elementKey) != true) + { + return false; + } + + if (existsInDb == true) + { + GetOrCreateOrphanMap()[elementKey] = oldElement; + } + + // We don't want to have non database keys in the removal queue + if (_queue?.Remove(elementKey) == true | + (_orphanMap?.ContainsKey(elementKey) == true && GetOrCreateRemovalQueue().Add(elementKey))) + { + _queueSize--; + return true; + } + + return false; + } + + /// + public override void SetElementByKey(TKey elementKey, TValue element, TValue oldElement, bool? existsInDb) + { + if (Cleared) + { + GetOrCreateQueue()[elementKey] = element; + return; + } + + // We can have the following scenarios: + // 1. set a key that exists in db and is not in the queue (don't increase queue size) + // 2. set a key that exists in db and is in the queue (don't increase queue size) + // 3. set a key that does not exist in db and is not in the queue (increase queue size) + // 4. set a key that does not exist in db and is in the queue (don't increase queue size) + // 5. set a key that exists in db and is in the removal queue (increase queue size) + if ((existsInDb == false && _queue?.ContainsKey(elementKey) != true) || _removalQueue?.Remove(elementKey) == true) + { + _queueSize++; + } + + if (existsInDb == true) + { + GetOrCreateOrphanMap()[elementKey] = oldElement; + } + + GetOrCreateQueue()[elementKey] = element; + } + + /// + public override bool TryGetElementByKey(TKey elementKey, out TValue element) + { + if (_queue == null) + { + element = default(TValue); + return false; + } + + return _queue.TryGetValue(elementKey, out element); + } + + /// + public override bool HasChanges() + { + return Cleared || _removalQueue?.Count > 0 || _queue?.Count > 0; + } + + /// + public override void ApplyChanges(IDictionary loadedMap) + { + if (Cleared) + { + loadedMap.Clear(); + } + else if (_removalQueue != null) + { + foreach (var toRemove in _removalQueue) + { + loadedMap.Remove(toRemove); + } + } + + if (_queue != null) + { + foreach (var toAdd in _queue) + { + loadedMap[toAdd.Key] = toAdd.Value; + } + } + } + + private IDictionary GetOrCreateQueue() + { + return _queue ?? (_queue = (IDictionary) _collectionPersister.CollectionType.Instantiate(-1)); + } + + private IDictionary GetOrCreateOrphanMap() + { + return _orphanMap ?? (_orphanMap = (IDictionary) _collectionPersister.CollectionType.Instantiate(-1)); + } + + private ISet GetOrCreateRemovalQueue() + { + return _removalQueue ?? (_removalQueue = new HashSet()); + } + } +} diff --git a/src/NHibernate/Collection/Trackers/NonIndexedListQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/NonIndexedListQueueOperationTracker.cs new file mode 100644 index 00000000000..f8a61fe5234 --- /dev/null +++ b/src/NHibernate/Collection/Trackers/NonIndexedListQueueOperationTracker.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + internal class NonIndexedListQueueOperationTracker : CollectionQueueOperationTracker> + { + public NonIndexedListQueueOperationTracker(ICollectionPersister collectionPersister) : base(collectionPersister) + { + } + } +} diff --git a/src/NHibernate/Collection/Trackers/SetQueueOperationTracker.cs b/src/NHibernate/Collection/Trackers/SetQueueOperationTracker.cs new file mode 100644 index 00000000000..0753068520a --- /dev/null +++ b/src/NHibernate/Collection/Trackers/SetQueueOperationTracker.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using NHibernate.Persister.Collection; + +namespace NHibernate.Collection.Trackers +{ + /// + /// A tracker that is able to track changes that are done to an uninitialized set. + /// + internal class SetQueueOperationTracker : CollectionQueueOperationTracker> + { + public SetQueueOperationTracker(ICollectionPersister collectionPersister) : base(collectionPersister) + { + } + + protected override bool Add(T element) + { + if (!Queue.Add(element)) + { + return false; + } + + if (!Cleared) + { + QueueSize++; + } + + return true; + } + } +}