Skip to content

Commit 8406508

Browse files
authored
Merge pull request #2489 from JKamsker/ReadTransform
Add support for on-the-fly document upgrades
2 parents c54674e + 1517f66 commit 8406508

File tree

7 files changed

+189
-15
lines changed

7 files changed

+189
-15
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using FluentAssertions;
2+
3+
using LiteDB.Engine;
4+
5+
using System.IO;
6+
7+
using Xunit;
8+
9+
namespace LiteDB.Tests.Database;
10+
11+
public class DocumentUpgrade_Tests
12+
{
13+
[Fact]
14+
public void DocumentUpgrade_Test()
15+
{
16+
var ms = new MemoryStream();
17+
using (var db = new LiteDatabase(ms))
18+
{
19+
var col = db.GetCollection("col");
20+
21+
col.Insert(new BsonDocument { ["version"] = 1, ["_id"] = 1, ["name"] = "John" });
22+
}
23+
24+
ms.Position = 0;
25+
26+
using (var db = new LiteDatabase(ms))
27+
{
28+
var col = db.GetCollection("col");
29+
30+
col.Count().Should().Be(1);
31+
32+
var doc = col.FindById(1);
33+
34+
doc["version"].AsInt32.Should().Be(1);
35+
doc["name"].AsString.Should().Be("John");
36+
doc["age"].AsInt32.Should().Be(0);
37+
}
38+
39+
ms.Position = 0;
40+
41+
using var engine = new LiteEngine(new EngineSettings
42+
{
43+
DataStream = ms,
44+
ReadTransform = (collectionName, val) =>
45+
{
46+
if (val is not BsonDocument doc)
47+
{
48+
return val;
49+
}
50+
51+
if (doc.TryGetValue("version", out var version) && version.AsInt32 == 1)
52+
{
53+
doc["version"] = 2;
54+
doc["age"] = 30;
55+
}
56+
57+
return val;
58+
}
59+
});
60+
61+
using (var db = new LiteDatabase(engine))
62+
{
63+
var col = db.GetCollection("col");
64+
65+
col.Count().Should().Be(1);
66+
67+
var doc = col.FindById(1);
68+
69+
doc["version"].AsInt32.Should().Be(2);
70+
doc["name"].AsString.Should().Be("John");
71+
doc["age"].AsInt32.Should().Be(30);
72+
}
73+
}
74+
75+
[Fact]
76+
public void DocumentUpgrade_BsonMapper_Test()
77+
{
78+
var ms = new MemoryStream();
79+
using (var db = new LiteDatabase(ms))
80+
{
81+
var col = db.GetCollection("col");
82+
83+
col.Insert(new BsonDocument { ["version"] = 1, ["_id"] = 1, ["name"] = "John" });
84+
}
85+
86+
ms.Position = 0;
87+
88+
using (var db = new LiteDatabase(ms))
89+
{
90+
var col = db.GetCollection("col");
91+
92+
col.Count().Should().Be(1);
93+
94+
var doc = col.FindById(1);
95+
96+
doc["version"].AsInt32.Should().Be(1);
97+
doc["name"].AsString.Should().Be("John");
98+
doc["age"].AsInt32.Should().Be(0);
99+
}
100+
101+
ms.Position = 0;
102+
103+
var mapper = new BsonMapper();
104+
mapper.OnDeserialization = (sender, type, val) =>
105+
{
106+
if (val is not BsonDocument doc)
107+
{
108+
return val;
109+
}
110+
111+
if (doc.TryGetValue("version", out var version) && version.AsInt32 == 1)
112+
{
113+
doc["version"] = 2;
114+
doc["age"] = 30;
115+
}
116+
117+
return doc;
118+
};
119+
120+
using (var db = new LiteDatabase(ms, mapper))
121+
{
122+
var col = db.GetCollection("col");
123+
124+
col.Count().Should().Be(1);
125+
126+
var doc = col.FindById(1);
127+
128+
doc["version"].AsInt32.Should().Be(2);
129+
doc["name"].AsString.Should().Be("John");
130+
doc["age"].AsInt32.Should().Be(30);
131+
}
132+
}
133+
}

LiteDB/Client/Mapper/BsonMapper.Deserialize.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ namespace LiteDB
99
{
1010
public partial class BsonMapper
1111
{
12+
#region Deserialization Hooks
13+
14+
/// <summary>
15+
/// Delegate for deserialization callback.
16+
/// </summary>
17+
/// <param name="sender">The BsonMapper instance that triggered the deserialization.</param>
18+
/// <param name="target">The target type for deserialization.</param>
19+
/// <param name="value">The BsonValue to be deserialized.</param>
20+
/// <returns>The deserialized BsonValue.</returns>
21+
public delegate BsonValue DeserializationCallback(BsonMapper sender, Type target, BsonValue value);
22+
23+
/// <summary>
24+
/// Gets called before deserialization of a value
25+
/// </summary>
26+
public DeserializationCallback? OnDeserialization { get; set; }
27+
28+
#endregion Deserialization Hooks
29+
1230
#region Basic direct .NET convert types
1331

1432
// direct bson types
@@ -78,6 +96,15 @@ public T Deserialize<T>(BsonValue value)
7896
/// </summary>
7997
public object Deserialize(Type type, BsonValue value)
8098
{
99+
if (OnDeserialization is not null)
100+
{
101+
var result = OnDeserialization(this, type, value);
102+
if (result is not null)
103+
{
104+
value = result;
105+
}
106+
}
107+
81108
// null value - null returns
82109
if (value.IsNull) return null;
83110

LiteDB/Client/Structures/ConnectionString.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using LiteDB.Engine;
1+
using LiteDB.Engine;
22
using System;
33
using System.Collections.Generic;
44
using System.Globalization;
@@ -107,7 +107,7 @@ public ConnectionString(string connectionString)
107107
/// <summary>
108108
/// Create ILiteEngine instance according string connection parameters. For now, only Local/Shared are supported
109109
/// </summary>
110-
internal ILiteEngine CreateEngine()
110+
internal ILiteEngine CreateEngine(Action<EngineSettings> engineSettingsAction = null)
111111
{
112112
var settings = new EngineSettings
113113
{
@@ -120,6 +120,8 @@ internal ILiteEngine CreateEngine()
120120
AutoRebuild = this.AutoRebuild,
121121
};
122122

123+
engineSettingsAction?.Invoke(settings);
124+
123125
// create engine implementation as Connection Type
124126
if (this.Connection == ConnectionType.Direct)
125127
{

LiteDB/Document/DataReader/BsonDataReader.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using LiteDB.Engine;
1+
using LiteDB.Engine;
22
using System;
33
using System.Collections;
44
using System.Collections.Generic;
@@ -56,10 +56,10 @@ internal BsonDataReader(IEnumerable<BsonValue> values, string collection, Engine
5656
if (_source.MoveNext())
5757
{
5858
_hasValues = _isFirst = true;
59-
_current = _source.Current;
59+
_current = _state.ReadTransform(_collection, _source.Current);
6060
}
6161
}
62-
catch(Exception ex)
62+
catch (Exception ex)
6363
{
6464
_state.Handle(ex);
6565
throw;
@@ -102,10 +102,10 @@ public bool Read()
102102
try
103103
{
104104
var read = _source.MoveNext(); // can throw any error here
105-
_current = _source.Current;
105+
_current = _state.ReadTransform(_collection, _source.Current);
106106
return read;
107107
}
108-
catch(Exception ex)
108+
catch (Exception ex)
109109
{
110110
_state.Handle(ex);
111111
throw ex;
@@ -117,7 +117,7 @@ public bool Read()
117117
}
118118
}
119119
}
120-
120+
121121
public BsonValue this[string field]
122122
{
123123
get

LiteDB/Engine/EngineSettings.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.ComponentModel;
@@ -7,6 +7,7 @@
77
using System.Reflection;
88
using System.Text;
99
using System.Text.RegularExpressions;
10+
1011
using static LiteDB.Constants;
1112

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

70+
/// <summary>
71+
/// Is used to transform a <see cref="BsonValue"/> from the database on read. This can be used to upgrade data from older versions.
72+
/// </summary>
73+
public Func<string, BsonValue, BsonValue> ReadTransform { get; set; }
74+
6975
/// <summary>
7076
/// Create new IStreamFactory for datafile
7177
/// </summary>

LiteDB/Engine/EngineState.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.IO;
@@ -9,7 +9,6 @@
99

1010
using static LiteDB.Constants;
1111

12-
1312
namespace LiteDB.Engine
1413
{
1514
internal class EngineState
@@ -25,7 +24,7 @@ internal class EngineState
2524
#endif
2625

2726
public EngineState(LiteEngine engine, EngineSettings settings)
28-
{
27+
{
2928
_engine = engine;
3029
_settings = settings;
3130
}
@@ -39,7 +38,7 @@ public bool Handle(Exception ex)
3938
{
4039
LOG(ex.Message, "ERROR");
4140

42-
if (ex is IOException ||
41+
if (ex is IOException ||
4342
(ex is LiteException lex && lex.ErrorCode == LiteException.INVALID_DATAFILE_STATE))
4443
{
4544
_exception = ex;
@@ -51,5 +50,12 @@ public bool Handle(Exception ex)
5150

5251
return true;
5352
}
53+
54+
public BsonValue ReadTransform(string collection, BsonValue value)
55+
{
56+
if (_settings?.ReadTransform is null) return value;
57+
58+
return _settings.ReadTransform(collection, value);
59+
}
5460
}
55-
}
61+
}

LiteDB/LiteDB.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<SignAssembly Condition="'$(OS)'=='Windows_NT'">true</SignAssembly>
2929
<AssemblyOriginatorKeyFile Condition="'$(Configuration)' == 'Release'">LiteDB.snk</AssemblyOriginatorKeyFile>
3030
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
31-
<LangVersion>8.0</LangVersion>
31+
<LangVersion>latest</LangVersion>
3232
</PropertyGroup>
3333

3434
<!--

0 commit comments

Comments
 (0)