Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for on-the-fly document upgrades #2489

Merged
merged 5 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions LiteDB.Tests/Database/DocumentUpgrade_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using FluentAssertions;

using LiteDB.Engine;

using System.IO;

using Xunit;

namespace LiteDB.Tests.Database;

public class DocumentUpgrade_Tests
{
[Fact]
public void DocumentUpgrade_Test()
{
var ms = new MemoryStream();
using (var db = new LiteDatabase(ms))
{
var col = db.GetCollection("col");

col.Insert(new BsonDocument { ["version"] = 1, ["_id"] = 1, ["name"] = "John" });
}

ms.Position = 0;

using (var db = new LiteDatabase(ms))
{
var col = db.GetCollection("col");

col.Count().Should().Be(1);

var doc = col.FindById(1);

doc["version"].AsInt32.Should().Be(1);
doc["name"].AsString.Should().Be("John");
doc["age"].AsInt32.Should().Be(0);
}

ms.Position = 0;

using var engine = new LiteEngine(new EngineSettings
{
DataStream = ms,
ReadTransform = (collectionName, val) =>
{
if (val is not BsonDocument doc)
{
return val;
}

if (doc.TryGetValue("version", out var version) && version.AsInt32 == 1)
{
doc["version"] = 2;
doc["age"] = 30;
}

return val;
}
});

using (var db = new LiteDatabase(engine))
{
var col = db.GetCollection("col");

col.Count().Should().Be(1);

var doc = col.FindById(1);

doc["version"].AsInt32.Should().Be(2);
doc["name"].AsString.Should().Be("John");
doc["age"].AsInt32.Should().Be(30);
}
}

[Fact]
public void DocumentUpgrade_BsonMapper_Test()
{
var ms = new MemoryStream();
using (var db = new LiteDatabase(ms))
{
var col = db.GetCollection("col");

col.Insert(new BsonDocument { ["version"] = 1, ["_id"] = 1, ["name"] = "John" });
}

ms.Position = 0;

using (var db = new LiteDatabase(ms))
{
var col = db.GetCollection("col");

col.Count().Should().Be(1);

var doc = col.FindById(1);

doc["version"].AsInt32.Should().Be(1);
doc["name"].AsString.Should().Be("John");
doc["age"].AsInt32.Should().Be(0);
}

ms.Position = 0;

var mapper = new BsonMapper();
mapper.OnDeserialization = (sender, type, val) =>
{
if (val is not BsonDocument doc)
{
return val;
}

if (doc.TryGetValue("version", out var version) && version.AsInt32 == 1)
{
doc["version"] = 2;
doc["age"] = 30;
}

return doc;
};

using (var db = new LiteDatabase(ms, mapper))
{
var col = db.GetCollection("col");

col.Count().Should().Be(1);

var doc = col.FindById(1);

doc["version"].AsInt32.Should().Be(2);
doc["name"].AsString.Should().Be("John");
doc["age"].AsInt32.Should().Be(30);
}
}
}
27 changes: 27 additions & 0 deletions LiteDB/Client/Mapper/BsonMapper.Deserialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ namespace LiteDB
{
public partial class BsonMapper
{
#region Deserialization Hooks

/// <summary>
/// Delegate for deserialization callback.
/// </summary>
/// <param name="sender">The BsonMapper instance that triggered the deserialization.</param>
/// <param name="target">The target type for deserialization.</param>
/// <param name="value">The BsonValue to be deserialized.</param>
/// <returns>The deserialized BsonValue.</returns>
public delegate BsonValue DeserializationCallback(BsonMapper sender, Type target, BsonValue value);

/// <summary>
/// Gets called before deserialization of a value
/// </summary>
public DeserializationCallback? OnDeserialization { get; set; }

#endregion Deserialization Hooks

#region Basic direct .NET convert types

// direct bson types
Expand Down Expand Up @@ -78,6 +96,15 @@ public T Deserialize<T>(BsonValue value)
/// </summary>
public object Deserialize(Type type, BsonValue value)
{
if (OnDeserialization is not null)
{
var result = OnDeserialization(this, type, value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep consicise with all code base, let's use "this." for instance methods, properties and public fields (only private _ name has no "this.")

if (result is not null)
{
value = result;
}
}

// null value - null returns
if (value.IsNull) return null;

Expand Down
6 changes: 4 additions & 2 deletions LiteDB/Client/Structures/ConnectionString.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using LiteDB.Engine;
using LiteDB.Engine;
using System;
using System.Collections.Generic;
using System.Globalization;
Expand Down Expand Up @@ -107,7 +107,7 @@ public ConnectionString(string connectionString)
/// <summary>
/// Create ILiteEngine instance according string connection parameters. For now, only Local/Shared are supported
/// </summary>
internal ILiteEngine CreateEngine()
internal ILiteEngine CreateEngine(Action<EngineSettings> engineSettingsAction = null)
{
var settings = new EngineSettings
{
Expand All @@ -120,6 +120,8 @@ internal ILiteEngine CreateEngine()
AutoRebuild = this.AutoRebuild,
};

engineSettingsAction?.Invoke(settings);

// create engine implementation as Connection Type
if (this.Connection == ConnectionType.Direct)
{
Expand Down
12 changes: 6 additions & 6 deletions LiteDB/Document/DataReader/BsonDataReader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using LiteDB.Engine;
using LiteDB.Engine;
using System;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -56,10 +56,10 @@ internal BsonDataReader(IEnumerable<BsonValue> values, string collection, Engine
if (_source.MoveNext())
{
_hasValues = _isFirst = true;
_current = _source.Current;
_current = _state.ReadTransform(_collection, _source.Current);
}
}
catch(Exception ex)
catch (Exception ex)
{
_state.Handle(ex);
throw;
Expand Down Expand Up @@ -102,10 +102,10 @@ public bool Read()
try
{
var read = _source.MoveNext(); // can throw any error here
_current = _source.Current;
_current = _state.ReadTransform(_collection, _source.Current);
return read;
}
catch(Exception ex)
catch (Exception ex)
{
_state.Handle(ex);
throw ex;
Expand All @@ -117,7 +117,7 @@ public bool Read()
}
}
}

public BsonValue this[string field]
{
get
Expand Down
8 changes: 7 additions & 1 deletion LiteDB/Engine/EngineSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
Expand All @@ -7,6 +7,7 @@
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

using static LiteDB.Constants;

namespace LiteDB.Engine
Expand Down Expand Up @@ -66,6 +67,11 @@ public class EngineSettings
/// </summary>
public bool Upgrade { get; set; } = false;

/// <summary>
/// Is used to transform a <see cref="BsonValue"/> from the database on read. This can be used to upgrade data from older versions.
/// </summary>
public Func<string, BsonValue, BsonValue> ReadTransform { get; set; }

/// <summary>
/// Create new IStreamFactory for datafile
/// </summary>
Expand Down
16 changes: 11 additions & 5 deletions LiteDB/Engine/EngineState.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
Expand All @@ -9,7 +9,6 @@

using static LiteDB.Constants;


namespace LiteDB.Engine
{
internal class EngineState
Expand All @@ -25,7 +24,7 @@ internal class EngineState
#endif

public EngineState(LiteEngine engine, EngineSettings settings)
{
{
_engine = engine;
_settings = settings;
}
Expand All @@ -39,7 +38,7 @@ public bool Handle(Exception ex)
{
LOG(ex.Message, "ERROR");

if (ex is IOException ||
if (ex is IOException ||
(ex is LiteException lex && lex.ErrorCode == LiteException.INVALID_DATAFILE_STATE))
{
_exception = ex;
Expand All @@ -51,5 +50,12 @@ public bool Handle(Exception ex)

return true;
}

public BsonValue ReadTransform(string collection, BsonValue value)
{
if (_settings?.ReadTransform is null) return value;

return _settings.ReadTransform(collection, value);
}
}
}
}
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>9.0</LangVersion>
JKamsker marked this conversation as resolved.
Show resolved Hide resolved
</PropertyGroup>

<!--
Expand Down