diff --git a/README.md b/README.md index 91bc18dc..b3e84d45 100644 --- a/README.md +++ b/README.md @@ -499,6 +499,7 @@ We'd love your contributions! If you want to contribute please read our [Contrib * [@CormacLennon](https://github.com/CormacLennon) * [@ahmedisam99](https://github.com/ahmedisam99) * [@kirollosonsi](https://github.com/kirollosonsi) +* [@tgmoore](https://github.com/tgmoore) [Logo]: images/logo.svg diff --git a/src/Redis.OM/Aggregation/RedisAggregationSet.cs b/src/Redis.OM/Aggregation/RedisAggregationSet.cs index 5f58ed88..a82315aa 100644 --- a/src/Redis.OM/Aggregation/RedisAggregationSet.cs +++ b/src/Redis.OM/Aggregation/RedisAggregationSet.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Redis.OM.Common; using Redis.OM.Contracts; using Redis.OM.Modeling; using Redis.OM.Searching; @@ -115,6 +116,27 @@ public async ValueTask>> ToListAsync() public async ValueTask[]> ToArrayAsync() => (await ToListAsync()).ToArray(); + /// + /// A string representation of the aggregation command and parameters, a serialization of the Expression with all parameters explicitly quoted. + /// Warning: this string may not be suitable for direct execution and is intended only for use in debugging. + /// + /// A string representing the Expression serialized to an aggregation command and parameters. + public string ToQueryString() + { + var serializedArgs = ExpressionTranslator.BuildAggregationFromExpression(Expression, typeof(T)).Serialize().ToList(); + + if (_useCursor) + { + serializedArgs.Add("WITHCURSOR"); + serializedArgs.Add("COUNT"); + serializedArgs.Add(_chunkSize.ToString()); + } + + var quotedArgs = serializedArgs.Select(arg => $"\"{arg}\""); + + return $"\"FT.AGGREGATE\" {string.Join(" ", quotedArgs)}"; + } + private void Initialize(RedisQueryProvider provider, Expression? expression, bool useCursor) { Provider = provider ?? throw new ArgumentNullException(nameof(provider)); diff --git a/src/Redis.OM/Searching/IRedisCollection.cs b/src/Redis.OM/Searching/IRedisCollection.cs index d72388f1..d016009c 100644 --- a/src/Redis.OM/Searching/IRedisCollection.cs +++ b/src/Redis.OM/Searching/IRedisCollection.cs @@ -339,5 +339,12 @@ public interface IRedisCollection : IOrderedQueryable, IAsyncEnumerable /// The Ids to look up. /// A dictionary correlating the ids provided to the objects in Redis. Task> FindByIdsAsync(IEnumerable ids); + + /// + /// A string representation of the Redisearch command and parameters, a serialization of the Expression with all parameters explicitly quoted. + /// Warning: this string may not be suitable for direct execution and is intended only for use in debugging. + /// + /// A string representing the Expression serialized to a Redisearch command and parameters. + string ToQueryString(); } } \ No newline at end of file diff --git a/src/Redis.OM/Searching/RedisCollection.cs b/src/Redis.OM/Searching/RedisCollection.cs index f9469872..ac3b62cb 100644 --- a/src/Redis.OM/Searching/RedisCollection.cs +++ b/src/Redis.OM/Searching/RedisCollection.cs @@ -731,6 +731,17 @@ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToke return new RedisCollectionEnumerator(Expression, provider.Connection, ChunkSize, StateManager, BooleanExpression, SaveState, RootType, typeof(T)); } + /// + public string ToQueryString() + { + var query = ExpressionTranslator.BuildQueryFromExpression(Expression, typeof(T), BooleanExpression, RootType); + query.Limit ??= new SearchLimit { Number = ChunkSize, Offset = 0 }; + + var quotedArgs = query.SerializeQuery().Select(arg => $"\"{arg}\""); + + return $"\"FT.SEARCH\" {string.Join(" ", quotedArgs)}"; + } + private static MethodInfo GetMethodInfo(Func f, T1 unused) { return f.Method; diff --git a/test/Redis.OM.Unit.Tests/RediSearchTests/AggregationSetTests.cs b/test/Redis.OM.Unit.Tests/RediSearchTests/AggregationSetTests.cs index 4f1ef418..227b0716 100644 --- a/test/Redis.OM.Unit.Tests/RediSearchTests/AggregationSetTests.cs +++ b/test/Redis.OM.Unit.Tests/RediSearchTests/AggregationSetTests.cs @@ -659,5 +659,23 @@ public async Task TestNoCursorDelete() await _substitute.Received().ExecuteAsync("FT.AGGREGATE", "person-idx", "*", "FILTER", "@TagField == 'foo' && @Address_State == 'FL'"); await _substitute.DidNotReceive().ExecuteAsync("FT.CURSOR", Arg.Any()); } + + [Fact] + public void TestToQueryString() + { + var collection = new RedisAggregationSet(_substitute, true, chunkSize: 10000); + var command = "\"FT.AGGREGATE\" \"person-idx\" \"@Salary:[(30.55 inf]\" \"LOAD\" \"*\" \"APPLY\" \"@Address_HouseNumber + 4\" \"AS\" \"house_num_modified\" \"SORTBY\" \"2\" \"@Age\" \"DESC\" \"LIMIT\" \"0\" \"10\" \"WITHCURSOR\" \"COUNT\" \"10000\""; + + var queryString = collection + .LoadAll() + .Apply(x => x.RecordShell.Address.HouseNumber + 4, "house_num_modified") + .Where(a => a.RecordShell!.Salary > 30.55M) + .OrderByDescending(p=>p.RecordShell.Age) + .Skip(0) + .Take(10) + .ToQueryString(); + + Assert.Equal(command, queryString); + } } } diff --git a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs index 7200e923..92625fd1 100644 --- a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs +++ b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs @@ -3889,5 +3889,17 @@ await _substitute.Received().ExecuteAsync("FT.SEARCH", "0", "100"); } + + [Fact] + public void TestToQueryString() + { + _substitute.ClearSubstitute(); + var command = "\"FT.SEARCH\" \"person-idx\" \"(((@Name:Ste) | (@Height:[70 inf])) (@Age:[-inf (33]))\" \"LIMIT\" \"100\" \"10\" \"SORTBY\" \"Age\" \"ASC\""; + + var collection = new RedisCollection(_substitute); + var queryString = collection.Where(x => x.Name.Contains("Ste") || x.Height >= 70).Where(x => x.Age < 33).OrderBy(x => x.Age).Skip(100).Take(10).ToQueryString(); + + Assert.Equal(command, queryString); + } } } \ No newline at end of file