Skip to content

Commit

Permalink
fixing issue with timestamps always being updated (#498)
Browse files Browse the repository at this point in the history
* fixing issue with timestamps always being updated

* fixing issue with byte array updates
  • Loading branch information
slorello89 authored Oct 28, 2024
1 parent 48ea85e commit 57c271a
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 3 deletions.
66 changes: 66 additions & 0 deletions src/Redis.OM/Modeling/DateTimeJsonConvertNewtonsoft.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Redis.OM.Modeling;

/// <summary>
/// Converter for Newtonsoft.
/// </summary>
internal class DateTimeJsonConvertNewtonsoft : JsonConverter
{
/// <summary>
/// Determines is the object is convertable.
/// </summary>
/// <param name="objectType">the object type.</param>
/// <returns>whether it can be converted.</returns>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
}

/// <summary>
/// writes the object to json.
/// </summary>
/// <param name="writer">the writer.</param>
/// <param name="value">the value.</param>
/// <param name="serializer">the serializer.</param>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is DateTime dateTime)
{
// Convert DateTime to Unix timestamp
long unixTimestamp = ((DateTimeOffset)dateTime).ToUnixTimeMilliseconds();
writer.WriteValue(unixTimestamp);
}
else
{
writer.WriteNull();
}
}

/// <summary>
/// reads an object back from json.
/// </summary>
/// <param name="reader">the reader.</param>
/// <param name="objectType">the object type.</param>
/// <param name="existingValue">the existing value.</param>
/// <param name="serializer">the serializer.</param>
/// <returns>The converted object.</returns>
/// <exception cref="JsonSerializationException">thrown if issue comes up deserializing.</exception>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return objectType == typeof(DateTime?) ? null : DateTime.MinValue;
}

if (reader.TokenType == JsonToken.Integer && reader.Value is long unixTimestamp)
{
// Convert Unix timestamp back to DateTime
return DateTimeOffset.FromUnixTimeMilliseconds(unixTimestamp).UtcDateTime;
}

throw new JsonSerializationException("Invalid token type for Unix timestamp conversion.");
}
}
21 changes: 18 additions & 3 deletions src/Redis.OM/Modeling/RedisCollectionStateManager.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
Expand All @@ -11,6 +12,11 @@ namespace Redis.OM.Modeling
/// </summary>
public class RedisCollectionStateManager
{
private static JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore, Converters = new List<JsonConverter> { new DateTimeJsonConvertNewtonsoft() },
};

/// <summary>
/// Initializes a new instance of the <see cref="RedisCollectionStateManager"/> class.
/// </summary>
Expand Down Expand Up @@ -76,7 +82,7 @@ internal void InsertIntoSnapshot(string key, object value)

if (DocumentAttribute.StorageType == StorageType.Json)
{
var json = JToken.FromObject(value, Newtonsoft.Json.JsonSerializer.Create(new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
var json = JToken.FromObject(value, Newtonsoft.Json.JsonSerializer.Create(new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = new List<JsonConverter> { new DateTimeJsonConvertNewtonsoft() } }));
Snapshot.Add(key, json);
}
else
Expand Down Expand Up @@ -115,7 +121,7 @@ internal bool TryDetectDifferencesSingle(string key, object value, out IList<IOb
var snapshotHash = (IDictionary<string, object>)Snapshot[key];
var deletedKeys = snapshotHash.Keys.Except(dataHash.Keys).Select(x => new KeyValuePair<string, string>(x, string.Empty));
var modifiedKeys = dataHash.Where(x =>
!snapshotHash.Keys.Contains(x.Key) || snapshotHash[x.Key] != x.Value).Select(x =>
!snapshotHash.Keys.Contains(x.Key) || !snapshotHash[x.Key].Equals(x.Value)).Select(x =>
new KeyValuePair<string, string>(x.Key, x.Value.ToString()));
differences = new List<IObjectDiff>
{
Expand Down Expand Up @@ -323,7 +329,16 @@ private static JObject FindDiff(JToken currentObject, JToken snapshotObject)

break;
default:
if (currentObject.ToString() != snapshotObject.ToString())
if (snapshotObject.Type == JTokenType.Bytes)
{
var snapShotObjectStr = Convert.ToBase64String(snapshotObject.Value<byte[]>());

if (snapShotObjectStr != currentObject.ToString())
{
diff["+"] = currentObject.ToString();
}
}
else if (currentObject.ToString() != snapshotObject.ToString())
{
diff["+"] = currentObject;
diff["-"] = snapshotObject;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using Redis.OM.Modeling;

namespace Redis.OM.Unit.Tests.RediSearchTests;

[Document(StorageType = StorageType.Json, Prefixes = new []{"obj"})]
public class ObjectWIthMultipleDateTimes
{
[RedisIdField]
[Indexed]
public string Id { get; set; }
public DateTime DateTime1 { get; set; }
public DateTime DateTime2 { get; set; }
}

[Document(Prefixes = new []{"obj"})]
public class ObjectWIthMultipleDateTimesHash
{
[RedisIdField]
[Indexed]
public string Id { get; set; }
public DateTime DateTime1 { get; set; }
public DateTime DateTime2 { get; set; }
}
15 changes: 15 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/ObjectWithByteArray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Redis.OM.Modeling;

namespace Redis.OM.Unit.Tests.RediSearchTests;

[Document(StorageType = StorageType.Json, Prefixes = new []{"obj"})]
public class ObjectWithByteArray
{
[RedisIdField]
[Indexed]
public string Id { get; set; }

public byte[] Bytes1 { get; set; }

public byte[] Bytes2 { get; set; }
}
15 changes: 15 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,5 +1296,20 @@ public async Task TestSearchByMatchPattern(string pattern, int existingRecordsCo
var res = await collection.Where(x => x.Name.MatchPattern(pattern)).ToListAsync();
Assert.Equal(existingRecordsCount, res.Count);
}

[Fact]
public async Task TestUpdateByteArray()
{
var collection = new RedisCollection<ObjectWithByteArray>(_connection);
var obj = new ObjectWithByteArray() { Bytes1 = new byte[] { 1, 2, 3 }, Bytes2 = new byte[] { 4, 5, 6 } };
var id = await collection.InsertAsync(obj);
var res = (await collection.Where(x => x.Id == obj.Id).ToListAsync()).First();
res.Bytes1 = new byte[] { 7, 8, 9 };
res.Bytes2 = new byte[] { 10, 11, 12 };
await collection.UpdateAsync(res);
var updated = (await collection.Where(x => x.Id == obj.Id).ToListAsync()).First();
Assert.Equal(new byte[] { 7, 8, 9 }, updated.Bytes1);
Assert.Equal(new byte[] { 10, 11, 12 }, updated.Bytes2);
}
}
}
88 changes: 88 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,46 @@ public class SearchTests
})
};

private readonly RedisReply _mockedReplyObjectWIthMultipleDateTimes = new[]
{
new RedisReply(1),
new RedisReply(
"obj:01FVN836BNQGYMT80V7RCVY73N"),
new RedisReply(new RedisReply[]
{
"$",
"{\"Id\":\"01FVN836BNQGYMT80V7RCVY73N\",\"DateTime1\":1729592130000,\"DateTime2\":1730475900000}"
})
};

private readonly RedisReply _mockedReplyObjectWIthMultipleByteArrays = new[]
{
new RedisReply(1),
new RedisReply(
"obj:01FVN836BNQGYMT80V7RCVY73N"),
new RedisReply(new RedisReply[]
{
"$",
"{\"Id\":\"01FVN836BNQGYMT80V7RCVY73N\",\"Bytes1\":\"AQID\",\"Bytes2\":\"BAUG\"}"
})
};

private readonly RedisReply _mockedReplyObjectWIthMultipleDateTimesHash = new[]
{
new RedisReply(1),
new RedisReply(
"obj:01FVN836BNQGYMT80V7RCVY73N"),
new RedisReply(new RedisReply[]
{
"Id",
"01FVN836BNQGYMT80V7RCVY73N",
"DateTime1",
"1729592130000",
"DateTime2",
"1730475900000"
})
};

[Fact]
public void TestBasicQuery()
{
Expand Down Expand Up @@ -1030,6 +1070,54 @@ public async Task TestUpdateJson()
await _substitute.Received().ExecuteAsync("EVALSHA", Arg.Any<string>(), "1", new RedisKey("Redis.OM.Unit.Tests.RediSearchTests.Person:01FVN836BNQGYMT80V7RCVY73N"), "SET", "$.Age", "33");
Scripts.ShaCollection.Clear();
}

[Fact]
public async Task TestUpdateJsonWithMultipleDateTimes()
{
_substitute.ExecuteAsync("FT.SEARCH", Arg.Any<object[]>()).Returns(_mockedReplyObjectWIthMultipleDateTimes);

_substitute.ExecuteAsync("EVALSHA", Arg.Any<object[]>()).Returns(Task.FromResult(new RedisReply("42")));
_substitute.ExecuteAsync("SCRIPT", Arg.Any<object[]>())
.Returns(Task.FromResult(new RedisReply("cbbf1c4fab5064f419e469cc51c563f8bf51e6fb")));
var collection = new RedisCollection<ObjectWIthMultipleDateTimes>(_substitute);
var obj = (await collection.Where(x => x.Id == "01FVN836BNQGYMT80V7RCVY73N").ToListAsync()).First();
obj.DateTime1 = obj.DateTime1.AddMilliseconds(1);
await collection.UpdateAsync(obj);
await _substitute.Received().ExecuteAsync("EVALSHA", Arg.Any<string>(), "1", new RedisKey("obj:01FVN836BNQGYMT80V7RCVY73N"), "SET", "$.DateTime1", "1729592130001");
Scripts.ShaCollection.Clear();
}


[Fact]
public async Task TestUpdateJsonWithMultipleDateTimesHash()
{
_substitute.ExecuteAsync("FT.SEARCH", Arg.Any<object[]>()).Returns(_mockedReplyObjectWIthMultipleDateTimesHash);

_substitute.ExecuteAsync("EVALSHA", Arg.Any<object[]>()).Returns(Task.FromResult(new RedisReply("42")));
_substitute.ExecuteAsync("SCRIPT", Arg.Any<object[]>())
.Returns(Task.FromResult(new RedisReply("cbbf1c4fab5064f419e469cc51c563f8bf51e6fb")));
var collection = new RedisCollection<ObjectWIthMultipleDateTimesHash>(_substitute);
var obj = (await collection.Where(x => x.Id == "01FVN836BNQGYMT80V7RCVY73N").ToListAsync()).First();
obj.DateTime1 = obj.DateTime1.AddMilliseconds(1);
await collection.UpdateAsync(obj);
await _substitute.Received().ExecuteAsync("EVALSHA", Arg.Any<string>(), "1", new RedisKey("obj:01FVN836BNQGYMT80V7RCVY73N"), "1", "DateTime1", "1729592130001");
Scripts.ShaCollection.Clear();
}

[Fact]
public async Task TestUpdateJsonWithByteArrays()
{
_substitute.ExecuteAsync("FT.SEARCH", Arg.Any<object[]>()).Returns(_mockedReplyObjectWIthMultipleByteArrays);
_substitute.ExecuteAsync("EVALSHA", Arg.Any<object[]>()).Returns(Task.FromResult(new RedisReply("42")));
_substitute.ExecuteAsync("SCRIPT", Arg.Any<object[]>())
.Returns(Task.FromResult(new RedisReply("cbbf1c4fab5064f419e469cc51c563f8bf51e6fb")));

var collection = new RedisCollection<ObjectWithByteArray>(_substitute);
var obj = (await collection.Where(x => x.Id == "01FVN836BNQGYMT80V7RCVY73N").ToListAsync()).First();
obj.Bytes1 = new byte[] { 4, 5, 6 };
await collection.UpdateAsync(obj);
await _substitute.Received().ExecuteAsync("EVALSHA", Arg.Any<string>(), "1", new RedisKey("obj:01FVN836BNQGYMT80V7RCVY73N"), "SET", "$.Bytes1","\"BAUG\"");
}

[Fact]
public async Task TestUpdateJsonUnloadedScriptAsync()
Expand Down
2 changes: 2 additions & 0 deletions test/Redis.OM.Unit.Tests/RedisSetupCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public RedisSetup()
Connection.CreateIndex(typeof(SelectTestObject));
Connection.CreateIndex(typeof(ObjectWithDateTimeOffsetJson));
Connection.CreateIndex(typeof(ObjectWithMultipleSearchableAttributes));
Connection.CreateIndex(typeof(ObjectWithByteArray));
}

private IRedisConnectionProvider _provider;
Expand Down Expand Up @@ -64,6 +65,7 @@ public void Dispose()
Connection.DropIndexAndAssociatedRecords(typeof(SelectTestObject));
Connection.DropIndexAndAssociatedRecords(typeof(ObjectWithDateTimeOffsetJson));
Connection.DropIndexAndAssociatedRecords(typeof(ObjectWithMultipleSearchableAttributes));
Connection.DropIndexAndAssociatedRecords(typeof(ObjectWithByteArray));
}
}
}

0 comments on commit 57c271a

Please sign in to comment.