diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 86228029..4b6d98dd 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -1,3 +1,9 @@ +NRedisStack.Search.Aggregation.Row.Enumerator +NRedisStack.Search.Aggregation.Row.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +NRedisStack.Search.Aggregation.Row.Enumerator.Enumerator() -> void +NRedisStack.Search.Aggregation.Row.Enumerator.MoveNext() -> bool +NRedisStack.Search.Aggregation.Row.FieldCount() -> int +NRedisStack.Search.Aggregation.Row.GetEnumerator() -> NRedisStack.Search.Aggregation.Row.Enumerator NRedisStack.Search.Parameters static NRedisStack.Search.Parameters.From(T obj) -> System.Collections.Generic.IReadOnlyDictionary! [NRS001]abstract NRedisStack.Search.VectorData.AsRedisValue() -> StackExchange.Redis.RedisValue diff --git a/src/NRedisStack/Search/Row.cs b/src/NRedisStack/Search/Row.cs index 3bbf451d..8374e45b 100644 --- a/src/NRedisStack/Search/Row.cs +++ b/src/NRedisStack/Search/Row.cs @@ -1,13 +1,15 @@ -using StackExchange.Redis; +using System.Collections; +using StackExchange.Redis; namespace NRedisStack.Search.Aggregation; -public readonly struct Row +public readonly struct Row : IEnumerable> { private readonly Dictionary _fields; internal Row(Dictionary fields) { + // note: only RedisValue fields are expected and exposed, due to how AggregationResult is constructed _fields = fields; } @@ -18,4 +20,65 @@ internal Row(Dictionary fields) public string? GetString(string key) => _fields.TryGetValue(key, out var result) ? result.ToString() : default; public long GetLong(string key) => _fields.TryGetValue(key, out var result) ? (long)(RedisValue)result : default; public double GetDouble(string key) => _fields.TryGetValue(key, out var result) ? (double)(RedisValue)result : default; + + /// + /// Gets the number of fields in this row. + /// + public int FieldCount() + { + // only include RedisValue fields, since nested aggregates are not supported via this API + var count = 0; + foreach (var field in _fields) + { + if (field.Value is RedisValue) + { + count++; + } + } + return count; + } + + /// + /// Access the fields as a sequence of key/value pairs. + /// + public Enumerator GetEnumerator() => new(in this); + + public struct Enumerator : IEnumerator> + { + private Dictionary.Enumerator _enumerator; + internal Enumerator(in Row row) => _enumerator = row._fields?.GetEnumerator() ?? default; + + /// + public bool MoveNext() + { + while (_enumerator.MoveNext()) + { + if (_enumerator.Current.Value is RedisValue) + { + return true; + } + } + + return false; + } + + /// + public KeyValuePair Current + { + get + { + var pair = _enumerator.Current; + return pair.Value is RedisValue value ? new(pair.Key, value) : default; + } + } + + void IEnumerator.Reset() => throw new NotSupportedException(); + + object IEnumerator.Current => Current; + + void IDisposable.Dispose() => _enumerator.Dispose(); + } + + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Search/AggregationResultUnitTests.cs b/tests/NRedisStack.Tests/Search/AggregationResultUnitTests.cs new file mode 100644 index 00000000..1fecd008 --- /dev/null +++ b/tests/NRedisStack.Tests/Search/AggregationResultUnitTests.cs @@ -0,0 +1,57 @@ +using NRedisStack.Search.Aggregation; +using StackExchange.Redis; +using Xunit; + +namespace NRedisStack.Tests.Search; + +public class AggregationResultUnitTests +{ + [Fact] + public void CanIterateRow() + { + Row row = new(new() + { + // the fields must all be RedisValue internally, else ignored + { "name", (RedisValue)"John" }, + { "age", (RedisValue)30 }, + { "ignored", new RedisValue[] { "a", "b", "c" } }, + { "city", (RedisValue)"New York" }, + }); + + // test via Row + Assert.Equal(3, row.FieldCount()); + Assert.Equal("John", row.GetString("name")); + Assert.Equal(30, row.GetLong("age")); + Assert.Equal("New York", row.GetString("city")); + + // test via foreach + int count = 0; + foreach (var field in row) + { + switch (field.Key) + { + case "name": + Assert.Equal("John", (string?)field.Value); + break; + case "age": + Assert.Equal(30, (int)field.Value); + break; + case "city": + Assert.Equal("New York", (string?)field.Value); + break; + default: + Assert.Fail($"Unexpected field: {field.Key}"); + break; + } + count++; + } + Assert.Equal(3, count); + + // test via Select + var isolated = row.Select(x => (x.Key, x.Value)).ToArray(); + Assert.Equal(3, isolated.Length); + Assert.Contains(("name", (RedisValue)"John"), isolated); + Assert.Contains(("age", (RedisValue)30), isolated); + Assert.Contains(("city", (RedisValue)"New York"), isolated); + } +} \ No newline at end of file