Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into fix-Lower_Upper_Inv…
Browse files Browse the repository at this point in the history
…ariant_BsonExpressions
  • Loading branch information
JKamsker committed Jun 4, 2024
2 parents b1e8640 + 6be2e24 commit dd53bb9
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 35 deletions.
44 changes: 44 additions & 0 deletions LiteDB.Tests/Internals/Extensions_Test.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using LiteDB.Utils.Extensions;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xunit;

namespace LiteDB.Tests.Internals;

public class Extensions_Test
{
// Asserts that chained IEnumerable<T>.OnDispose(()=> { }) calls the action on dispose, even when chained
[Fact]
public void EnumerableExtensions_OnDispose()
{
var disposed = false;
var disposed1 = false;
var enumerable = new[] { 1, 2, 3 }.OnDispose(() => disposed = true).OnDispose(() => disposed1 = true);

foreach (var item in enumerable)
{
// do nothing
}

Assert.True(disposed);
Assert.True(disposed1);
}

// tests IDisposable StartDisposable(this Stopwatch stopwatch)
[Fact]
public async Task StopWatchExtensions_StartDisposable()
{
var stopwatch = new System.Diagnostics.Stopwatch();
using (stopwatch.StartDisposable())
{
await Task.Delay(100);
}

Assert.True(stopwatch.ElapsedMilliseconds > 0);
}
}
97 changes: 97 additions & 0 deletions LiteDB.Tests/Issues/Issue2471_Test.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using FluentAssertions;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Xunit;

namespace LiteDB.Tests.Issues;

public class Issue2471_Test
{
[Fact]
public void TestFragmentDB_FindByIDException()
{
using var db = new LiteDatabase(":memory:");
var collection = db.GetCollection<object>("fragtest");

var fragment = new object { };
var id = collection.Insert(fragment);

id.Should().BeGreaterThan(0);

var frag2 = collection.FindById(id);
frag2.Should().NotBeNull();

Action act = () => db.Checkpoint();

act.Should().NotThrow();
}

[Fact]
public void MultipleReadCleansUpTransaction()
{
using var database = new LiteDatabase(":memory:");

var collection = database.GetCollection("test");
collection.Insert(new BsonDocument { ["_id"] = 1 });

for (int i = 0; i < 500; i++)
{
collection.FindById(1);
}
}

#region Model

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int[] Phones { get; set; }
public List<Address> Addresses { get; set; }
}

public class Address
{
public string Street { get; set; }
}

#endregion Model

// Copied from IndexMultiKeyIndex, but this time we ensure that the lock is released by calling db.Checkpoint()
[Fact]
public void Ensure_Query_GetPlan_Releases_Lock()
{
using var db = new LiteDatabase(new MemoryStream());
var col = db.GetCollection<User>();

col.Insert(new User { Name = "John Doe", Phones = new int[] { 1, 3, 5 }, Addresses = new List<Address> { new Address { Street = "Av.1" }, new Address { Street = "Av.3" } } });
col.Insert(new User { Name = "Joana Mark", Phones = new int[] { 1, 4 }, Addresses = new List<Address> { new Address { Street = "Av.3" } } });

// create indexes
col.EnsureIndex(x => x.Phones);
col.EnsureIndex(x => x.Addresses.Select(z => z.Street));

// testing indexes expressions
var indexes = db.GetCollection("$indexes").FindAll().ToArray();

indexes[1]["expression"].AsString.Should().Be("$.Phones[*]");
indexes[2]["expression"].AsString.Should().Be("MAP($.Addresses[*]=>@.Street)");

// doing Phone query
var queryPhone = col.Query()
.Where(x => x.Phones.Contains(3));

var planPhone = queryPhone.GetPlan();

Action act = () => db.Checkpoint();

act.Should().NotThrow();
}
}
57 changes: 22 additions & 35 deletions LiteDB/Engine/Query/QueryExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using LiteDB.Utils.Extensions;

using System;
using System.Collections.Generic;
using System.Linq;

using static LiteDB.Constants;

namespace LiteDB.Engine
Expand All @@ -22,14 +25,14 @@ internal class QueryExecutor
private readonly IEnumerable<BsonDocument> _source;

public QueryExecutor(
LiteEngine engine,
LiteEngine engine,
EngineState state,
TransactionMonitor monitor,
SortDisk sortDisk,
TransactionMonitor monitor,
SortDisk sortDisk,
DiskService disk,
EnginePragmas pragmas,
string collection,
Query query,
EnginePragmas pragmas,
string collection,
Query query,
IEnumerable<BsonDocument> source)
{
_engine = engine;
Expand Down Expand Up @@ -71,8 +74,17 @@ internal BsonDataReader ExecuteQuery(bool executionPlan)

transaction.OpenCursors.Add(_cursor);

var enumerable = RunQuery();

enumerable = enumerable.OnDispose(() => transaction.OpenCursors.Remove(_cursor));

if (isNew)
{
enumerable = enumerable.OnDispose(() => _monitor.ReleaseTransaction(transaction));
}

// return new BsonDataReader with IEnumerable source
return new BsonDataReader(RunQuery(), _collection, _state);
return new BsonDataReader(enumerable, _collection, _state);

IEnumerable<BsonDocument> RunQuery()
{
Expand All @@ -87,13 +99,6 @@ IEnumerable<BsonDocument> RunQuery()
yield return _query.Select.ExecuteScalar(_pragmas.Collation).AsDocument;
}

transaction.OpenCursors.Remove(_cursor);

if (isNew)
{
_monitor.ReleaseTransaction(transaction);
}

yield break;
}

Expand All @@ -108,14 +113,6 @@ IEnumerable<BsonDocument> RunQuery()
if (executionPlan)
{
yield return queryPlan.GetExecutionPlan();

transaction.OpenCursors.Remove(_cursor);

if (isNew)
{
_monitor.ReleaseTransaction(transaction);
}

yield break;
}

Expand All @@ -125,8 +122,8 @@ IEnumerable<BsonDocument> RunQuery()
// get current query pipe: normal or groupby pipe
var pipe = queryPlan.GetPipe(transaction, snapshot, _sortDisk, _pragmas, _disk.MAX_ITEMS_COUNT);

// start cursor elapsed timer
_cursor.Elapsed.Start();
// start cursor elapsed timer which stops on dispose
using var _ = _cursor.Elapsed.StartDisposable();

using (var enumerator = pipe.Pipe(nodes, queryPlan).GetEnumerator())
{
Expand Down Expand Up @@ -164,16 +161,6 @@ IEnumerable<BsonDocument> RunQuery()
}
}
}

// stop cursor elapsed
_cursor.Elapsed.Stop();

transaction.OpenCursors.Remove(_cursor);

if (isNew)
{
_monitor.ReleaseTransaction(transaction);
}
};
}

Expand Down
25 changes: 25 additions & 0 deletions LiteDB/Engine/Services/SnapShot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ internal class Snapshot : IDisposable
// local page cache - contains only pages about this collection (but do not contains CollectionPage - use this.CollectionPage)
private readonly Dictionary<uint, BasePage> _localPages = new Dictionary<uint, BasePage>();

private bool _disposed;

// expose
public LockMode Mode => _mode;
public string CollectionName => _collectionName;
Expand Down Expand Up @@ -89,6 +91,8 @@ public Snapshot(
/// </summary>
public IEnumerable<BasePage> GetWritablePages(bool dirty, bool includeCollectionPage)
{
ENSURE(!_disposed, "the snapshot is disposed");

// if snapshot is read only, just exit
if (_mode == LockMode.Read) yield break;

Expand All @@ -110,6 +114,8 @@ public IEnumerable<BasePage> GetWritablePages(bool dirty, bool includeCollection
/// </summary>
public void Clear()
{
ENSURE(!_disposed, "the snapshot is disposed");

// release pages only if snapshot are read only
if (_mode == LockMode.Read)
{
Expand All @@ -128,9 +134,16 @@ public void Clear()
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}

// release all data/index pages
this.Clear();

_disposed = true;

// release collection page (in read mode)
if (_mode == LockMode.Read && _collectionPage != null)
{
Expand Down Expand Up @@ -160,6 +173,7 @@ public T GetPage<T>(uint pageID)
public T GetPage<T>(uint pageID, out FileOrigin origin, out long position, out int walVersion)
where T : BasePage
{
ENSURE(!_disposed, "the snapshot is disposed");
ENSURE(pageID <= _header.LastPageID, "request page must be less or equals lastest page in data file");

// check for header page (return header single instance)
Expand Down Expand Up @@ -259,6 +273,8 @@ private T ReadPage<T>(uint pageID, out FileOrigin origin, out long position, out
/// </summary>
public DataPage GetFreeDataPage(int bytesLength)
{
ENSURE(!_disposed, "the snapshot is disposed");

var length = bytesLength + BasePage.SLOT_SIZE; // add +4 bytes for footer slot

// get minimum slot to check for free page. Returns -1 if need NewPage
Expand Down Expand Up @@ -292,6 +308,8 @@ public DataPage GetFreeDataPage(int bytesLength)
/// </summary>
public IndexPage GetFreeIndexPage(int bytesLength, ref uint freeIndexPageList)
{
ENSURE(!_disposed, "the snapshot is disposed");

IndexPage page;

// if there is not page in list pages, create new page
Expand All @@ -318,6 +336,7 @@ public IndexPage GetFreeIndexPage(int bytesLength, ref uint freeIndexPageList)
public T NewPage<T>()
where T : BasePage
{
ENSURE(!_disposed, "the snapshot is disposed");
ENSURE(_collectionPage == null, typeof(T) == typeof(CollectionPage), "if no collection page defined yet, must be first request");
ENSURE(typeof(T) == typeof(CollectionPage), _collectionPage == null, "there is no new collection page if page already exists");

Expand Down Expand Up @@ -392,6 +411,8 @@ public T NewPage<T>()
/// </summary>
public void AddOrRemoveFreeDataList(DataPage page)
{
ENSURE(!_disposed, "the snapshot is disposed");

var newSlot = DataPage.FreeIndexSlot(page.FreeBytes);
var initialSlot = page.PageListSlot;

Expand Down Expand Up @@ -423,6 +444,8 @@ public void AddOrRemoveFreeDataList(DataPage page)
/// </summary>
public void AddOrRemoveFreeIndexList(IndexPage page, ref uint startPageID)
{
ENSURE(!_disposed, "the snapshot is disposed");

var newSlot = IndexPage.FreeIndexSlot(page.FreeBytes);
var isOnList = page.PageListSlot == 0;
var mustKeep = newSlot == 0;
Expand Down Expand Up @@ -567,6 +590,8 @@ private void DeletePage<T>(T page)
/// </summary>
public void DropCollection(Action safePoint)
{
ENSURE(!_disposed, "the snapshot is disposed");

var indexer = new IndexService(this, _header.Pragmas.Collation, _disk.MAX_ITEMS_COUNT);

// CollectionPage will be last deleted page (there is no NextPageID from CollectionPage)
Expand Down
1 change: 1 addition & 0 deletions LiteDB/LiteDB.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<SignAssembly Condition="'$(OS)'=='Windows_NT'">true</SignAssembly>
<AssemblyOriginatorKeyFile Condition="'$(Configuration)' == 'Release'">LiteDB.snk</AssemblyOriginatorKeyFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>8.0</LangVersion>
</PropertyGroup>

<!--
Expand Down
24 changes: 24 additions & 0 deletions LiteDB/Utils/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;

namespace LiteDB.Utils.Extensions
{
internal static class EnumerableExtensions
{
// calls method on dispose
public static IEnumerable<T> OnDispose<T>(this IEnumerable<T> source, Action onDispose)
{
try
{
foreach (var item in source)
{
yield return item;
}
}
finally
{
onDispose();
}
}
}
}
Loading

0 comments on commit dd53bb9

Please sign in to comment.