Skip to content

Commit

Permalink
mixed embedded object paths (#324)
Browse files Browse the repository at this point in the history
  • Loading branch information
slorello89 authored Mar 14, 2023
1 parent 4b54057 commit 8d05d91
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 18 deletions.
56 changes: 39 additions & 17 deletions src/Redis.OM/Modeling/RedisSchemaField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ namespace Redis.OM.Modeling
/// </summary>
internal static class RedisSchemaField
{
internal static bool IsComplexType(Type type)
{
return !TypeDeterminationUtilities.IsNumeric(type)
&& type != typeof(string)
&& type != typeof(GeoLoc)
&& type != typeof(Ulid)
&& type != typeof(bool)
&& type != typeof(Guid)
&& !type.IsEnum
&& !IsTypeIndexableArray(type);
}

/// <summary>
/// gets the schema field args serialized for json.
/// </summary>
Expand All @@ -35,24 +47,17 @@ internal static string[] SerializeArgsJson(this PropertyInfo info, int remaining
var ret = new List<string>();
foreach (var attr in attributes)
{
int cascadeDepth = remainingDepth == -1 ? attr.CascadeDepth : remainingDepth;
if (attr.JsonPath != null)
{
ret.AddRange(SerializeIndexFromJsonPaths(info, attr));
ret.AddRange(SerializeIndexFromJsonPaths(info, attr, pathPrefix, aliasPrefix, cascadeDepth));
}
else
{
var innerType = Nullable.GetUnderlyingType(info.PropertyType) ?? info.PropertyType;

if (!TypeDeterminationUtilities.IsNumeric(innerType)
&& innerType != typeof(string)
&& innerType != typeof(GeoLoc)
&& innerType != typeof(Ulid)
&& innerType != typeof(bool)
&& innerType != typeof(Guid)
&& !innerType.IsEnum
&& !IsTypeIndexableArray(innerType))
if (IsComplexType(innerType))
{
int cascadeDepth = remainingDepth == -1 ? attr.CascadeDepth : remainingDepth;
if (cascadeDepth > 0)
{
foreach (var property in info.PropertyType.GetProperties())
Expand Down Expand Up @@ -96,7 +101,7 @@ internal static string[] SerializeArgs(this PropertyInfo info)

private static bool IsTypeIndexableArray(Type t) => t == typeof(string[]) || t == typeof(bool[]) || t == typeof(List<string>) || t == typeof(List<bool>);

private static IEnumerable<string> SerializeIndexFromJsonPaths(PropertyInfo parentInfo, SearchFieldAttribute attribute, string prefix = "$.")
private static IEnumerable<string> SerializeIndexFromJsonPaths(PropertyInfo parentInfo, SearchFieldAttribute attribute, string prefix = "$.", string aliasPrefix = "", int remainingDepth = -1)
{
var isCollection = false;
var indexArgs = new List<string>();
Expand Down Expand Up @@ -128,12 +133,29 @@ private static IEnumerable<string> SerializeIndexFromJsonPaths(PropertyInfo pare
type = childProperty.PropertyType;
}

var arrayStr = isCollection ? "[*]" : string.Empty;
indexArgs.Add($"{prefix}{parentInfo.Name}{arrayStr}{path.Substring(1)}");
indexArgs.Add("AS");
indexArgs.Add($"{parentInfo.Name}_{string.Join("_", propertyNames)}");
var underlyingType = Nullable.GetUnderlyingType(type);
indexArgs.AddRange(CommonSerialization(attribute, underlyingType ?? type, propertyInfo));
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
if (!IsComplexType(underlyingType))
{
var arrayStr = isCollection ? "[*]" : string.Empty;
indexArgs.Add($"{prefix}{parentInfo.Name}{arrayStr}{path.Substring(1)}");
indexArgs.Add("AS");
indexArgs.Add($"{aliasPrefix}{parentInfo.Name}_{string.Join("_", propertyNames)}");
indexArgs.AddRange(CommonSerialization(attribute, underlyingType, propertyInfo));
}
else
{
int cascadeDepth = remainingDepth == -1 ? attribute.CascadeDepth : remainingDepth;
if (cascadeDepth > 0)
{
foreach (var property in propertyInfo.PropertyType.GetProperties())
{
var pathPrefix = $"{prefix}{parentInfo.Name}{path.Substring(1)}.";
var alias = $"{aliasPrefix}{parentInfo.Name}_{string.Join("_", propertyNames)}_";
indexArgs.AddRange(property.SerializeArgsJson(cascadeDepth - 1, pathPrefix, alias));
}
}
}

return indexArgs;
}

Expand Down
6 changes: 5 additions & 1 deletion src/Redis.OM/Searching/RedisCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,11 @@ public T First(Expression<Func<T, bool>> expression)
query.Limit = new SearchLimit { Number = 1, Offset = 0 };
var res = _connection.Search<T>(query);
var result = res.Documents.FirstOrDefault();
SaveToStateManager(result.Key, result.Value);
if (result.Key != null)
{
SaveToStateManager(result.Key, result.Value);
}

return result.Value;
}

Expand Down
30 changes: 30 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,36 @@ public void TestIntSelects()
collection.Delete(obj);
}

[Fact]
public void TestComplexObjectsWithMixedNesting()
{
var obj = new ComplexObjectWithCascadeAndJsonPath
{
InnerCascade = new InnerObject()
{
InnerInnerCascade = new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 },
InnerInnerCollection = new[] { new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 } },
InnerInnerJson = new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 }
},
InnerJson = new InnerObject()
{
InnerInnerCascade = new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 },
InnerInnerCollection = new[] { new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 } },
InnerInnerJson = new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 }
}
};

var collection = new RedisCollection<ComplexObjectWithCascadeAndJsonPath>(_connection);

collection.Insert(obj);

Assert.NotNull(collection.FirstOrDefault(x => x.InnerCascade.InnerInnerCascade.Tag == "World"));
Assert.NotNull(collection.FirstOrDefault(x=> x.InnerCascade.InnerInnerCascade.Num == 42));
Assert.NotNull(collection.FirstOrDefault(x=> x.InnerCascade.InnerInnerCollection.Any(x=>x.Tag == "World")));
Assert.NotNull(collection.FirstOrDefault(x=>x.InnerJson.InnerInnerCascade.Tag == "World"));
Assert.NotNull(collection.FirstOrDefault(x=>x.InnerJson.InnerInnerCascade.Arr.Contains("hello")));
}

[Fact]
public void TestUpdateWithQuotes()
{
Expand Down
66 changes: 66 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2888,5 +2888,71 @@ public void TestNullableEnumQueries()
"100"
));
}

[Fact]
public void TestMixedNestingIndexCreation()
{
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
.Returns("OK");

_mock.Object.CreateIndex(typeof(ComplexObjectWithCascadeAndJsonPath));

_mock.Verify(x => x.Execute(
"FT.CREATE",
$"{nameof(ComplexObjectWithCascadeAndJsonPath).ToLower()}-idx",
"ON",
"Json",
"PREFIX",
"1",
$"Redis.OM.Unit.Tests.{nameof(ComplexObjectWithCascadeAndJsonPath)}:",
"SCHEMA", "$.InnerCascade.InnerInnerJson.Tag", "AS", "InnerCascade_InnerInnerJson_Tag", "TAG", "SEPARATOR", "|",
"$.InnerCascade.InnerInnerCascade.Tag", "AS", "InnerCascade_InnerInnerCascade_Tag", "TAG", "SEPARATOR", "|",
"$.InnerCascade.InnerInnerCascade.Num", "AS", "InnerCascade_InnerInnerCascade_Num", "NUMERIC",
"$.InnerCascade.InnerInnerCascade.Arr[*]", "AS", "InnerCascade_InnerInnerCascade_Arr", "TAG", "SEPARATOR", "|",
"$.InnerCascade.InnerInnerCollection[*].Tag", "AS", "InnerCascade_InnerInnerCollection_Tag", "TAG", "SEPARATOR", "|",
"$.InnerJson.InnerInnerCascade.Tag", "AS", "InnerJson_InnerInnerCascade_Tag", "TAG", "SEPARATOR", "|",
"$.InnerJson.InnerInnerCascade.Num", "AS", "InnerJson_InnerInnerCascade_Num", "NUMERIC",
"$.InnerJson.InnerInnerCascade.Arr[*]", "AS", "InnerJson_InnerInnerCascade_Arr", "TAG", "SEPARATOR", "|"));
}

[Fact]
public void TestMixedNestingQuerying()
{
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
.Returns(_mockReply);

var collection = new RedisCollection<ComplexObjectWithCascadeAndJsonPath>(_mock.Object);

collection.FirstOrDefault(x => x.InnerCascade.InnerInnerCascade.Tag == "hello");

_mock.Verify(x => x.Execute(
"FT.SEARCH",
"complexobjectwithcascadeandjsonpath-idx",
"(@InnerCascade_InnerInnerCascade_Tag:{hello})",
"LIMIT",
"0",
"1"
));

collection.FirstOrDefault(x => x.InnerCascade.InnerInnerCascade.Num == 5);
_mock.Verify(x => x.Execute(
"FT.SEARCH",
"complexobjectwithcascadeandjsonpath-idx",
"(@InnerCascade_InnerInnerCascade_Num:[5 5])",
"LIMIT",
"0",
"1"
));

collection.FirstOrDefault(x => x.InnerCascade.InnerInnerCollection.Any(x=>x.Tag == "hello"));
_mock.Verify(x => x.Execute(
"FT.SEARCH",
"complexobjectwithcascadeandjsonpath-idx",
"(@InnerCascade_InnerInnerCollection_Tag:{hello})",
"LIMIT",
"0",
"1"
));
}
}
}
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 @@ -26,6 +26,7 @@ public RedisSetup()
Connection.CreateIndex(typeof(ObjectWithDateTime));
Connection.CreateIndex(typeof(ObjectWithDateTimeHash));
Connection.CreateIndex(typeof(PersonWithNestedArrayOfObject));
Connection.CreateIndex(typeof(ComplexObjectWithCascadeAndJsonPath));
Connection.CreateIndex(typeof(BasicJsonObjectTestSave));
}

Expand Down Expand Up @@ -61,6 +62,7 @@ public void Dispose()
Connection.DropIndexAndAssociatedRecords(typeof(ObjectWithDateTime));
Connection.DropIndexAndAssociatedRecords(typeof(ObjectWithDateTimeHash));
Connection.DropIndexAndAssociatedRecords(typeof(PersonWithNestedArrayOfObject));
Connection.DropIndexAndAssociatedRecords(typeof(ComplexObjectWithCascadeAndJsonPath));
Connection.DropIndexAndAssociatedRecords(typeof(BasicJsonObjectTestSave));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Redis.OM.Modeling;

namespace Redis.OM.Unit.Tests;

[Document(StorageType = StorageType.Json)]
public class ComplexObjectWithCascadeAndJsonPath
{
[Indexed(CascadeDepth = 2)] public InnerObject InnerCascade { get; set; }
[Indexed(JsonPath = "$.InnerInnerCascade", CascadeDepth = 2)] public InnerObject InnerJson { get; set; }
}

public class InnerObject
{
[Indexed(JsonPath = "$.Tag")] public InnerInnerObject InnerInnerJson { get; set; }
[Indexed(CascadeDepth = 1)] public InnerInnerObject InnerInnerCascade { get; set; }
[Indexed(JsonPath = "$.Tag")] public InnerInnerObject[] InnerInnerCollection { get; set; }
}

public class InnerInnerObject
{
[Indexed] public string Tag { get; set; }
[Indexed] public int Num { get; set; }
[Indexed] public string[] Arr { get; set; }
}

0 comments on commit 8d05d91

Please sign in to comment.