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

fixing issue with timestamps always being updated #498

Merged
merged 2 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@ -1270,5 +1270,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));
}
}
}
Loading