Skip to content

Commit

Permalink
Merge pull request #2489 from JKamsker/ReadTransform
Browse files Browse the repository at this point in the history
Add support for on-the-fly document upgrades
  • Loading branch information
mbdavid authored Jul 3, 2024
2 parents c54674e + 1517f66 commit 8406508
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 15 deletions.
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);
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);
}
}
}
}
2 changes: 1 addition & 1 deletion LiteDB/LiteDB.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<SignAssembly Condition="'$(OS)'=='Windows_NT'">true</SignAssembly>
<AssemblyOriginatorKeyFile Condition="'$(Configuration)' == 'Release'">LiteDB.snk</AssemblyOriginatorKeyFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>8.0</LangVersion>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<!--
Expand Down

0 comments on commit 8406508

Please sign in to comment.