From 667ce494ec4f63f196b63fa980bee0557803a433 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:27:47 +0100 Subject: [PATCH 01/22] DOC-4080 Examples for ZADD and ZRANGE (#332) * DOC-4080 zadd example * DOC-4080 added zrange examples * DOC-4080 dotnet format changes --- tests/Doc/CmdsSortedSetExamples.cs | 484 +++++++++++++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 tests/Doc/CmdsSortedSetExamples.cs diff --git a/tests/Doc/CmdsSortedSetExamples.cs b/tests/Doc/CmdsSortedSetExamples.cs new file mode 100644 index 00000000..6ae81201 --- /dev/null +++ b/tests/Doc/CmdsSortedSetExamples.cs @@ -0,0 +1,484 @@ +// EXAMPLE: cmds_sorted_set +// HIDE_START + +using NRedisStack.Tests; +using StackExchange.Redis; + +// HIDE_END + +// REMOVE_START +namespace Doc; +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class CmdsSortedSet +{ + + [SkipIfRedis(Is.OSSCluster)] + public void run() + { + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + //REMOVE_START + // Clear any keys here before using them in tests. + + //REMOVE_END + // HIDE_END + + + // STEP_START bzmpop + + // STEP_END + + // Tests for 'bzmpop' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START bzpopmax + + // STEP_END + + // Tests for 'bzpopmax' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START bzpopmin + + // STEP_END + + // Tests for 'bzpopmin' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zadd + bool zAddResult1 = db.SortedSetAdd("myzset", "one", 1); + Console.WriteLine(zAddResult1); // >>> True + + bool zAddResult2 = db.SortedSetAdd("myzset", "uno", 1); + Console.WriteLine(zAddResult2); // >>> True + + long zAddResult3 = db.SortedSetAdd( + "myzset", + new SortedSetEntry[] { + new SortedSetEntry("two", 2), + new SortedSetEntry("three", 3) + } + ); + Console.WriteLine(zAddResult3); // >>> 2 + + SortedSetEntry[] zAddResult4 = db.SortedSetRangeByRankWithScores("myzset", 0, -1); + Console.WriteLine($"{string.Join(", ", zAddResult4.Select(b => $"{b.Element}: {b.Score}"))}"); + // >>> one: 1, uno: 1, two: 2, three: 3 + // STEP_END + + // Tests for 'zadd' step. + // REMOVE_START + Assert.True(zAddResult1); + Assert.True(zAddResult2); + Assert.Equal(2, zAddResult3); + Assert.Equal( + "one: 1, uno: 1, two: 2, three: 3", + string.Join(", ", zAddResult4.Select(b => $"{b.Element}: {b.Score}")) + ); + db.KeyDelete("myzset"); + // REMOVE_END + + + // STEP_START zcard + + // STEP_END + + // Tests for 'zcard' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zcount + + // STEP_END + + // Tests for 'zcount' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zdiff + + // STEP_END + + // Tests for 'zdiff' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zdiffstore + + // STEP_END + + // Tests for 'zdiffstore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zincrby + + // STEP_END + + // Tests for 'zincrby' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zinter + + // STEP_END + + // Tests for 'zinter' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zintercard + + // STEP_END + + // Tests for 'zintercard' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zinterstore + + // STEP_END + + // Tests for 'zinterstore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zlexcount + + // STEP_END + + // Tests for 'zlexcount' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zmpop + + // STEP_END + + // Tests for 'zmpop' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zmscore + + // STEP_END + + // Tests for 'zmscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zpopmax + + // STEP_END + + // Tests for 'zpopmax' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zpopmin + + // STEP_END + + // Tests for 'zpopmin' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrandmember + + // STEP_END + + // Tests for 'zrandmember' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrange1 + long zRangeResult1 = db.SortedSetAdd( + "myzset", + new SortedSetEntry[] { + new SortedSetEntry("one", 1), + new SortedSetEntry("two", 2), + new SortedSetEntry("three", 3) + } + ); + Console.WriteLine(zRangeResult1); // >>> 3 + + RedisValue[] zRangeResult2 = db.SortedSetRangeByRank("myzset", 0, -1); + Console.WriteLine(string.Join(", ", zRangeResult2)); + // >>> one, two, three + + RedisValue[] zRangeResult3 = db.SortedSetRangeByRank("myzset", 2, 3); + Console.WriteLine(string.Join(", ", zRangeResult3)); + // >>> three + + RedisValue[] zRangeResult4 = db.SortedSetRangeByRank("myzset", -2, -1); + Console.WriteLine(string.Join(", ", zRangeResult4)); + // >>> two, three + // STEP_END + + // Tests for 'zrange1' step. + // REMOVE_START + Assert.Equal(3, zRangeResult1); + Assert.Equal("one, two, three", string.Join(", ", zRangeResult2)); + Assert.Equal("three", string.Join(", ", zRangeResult3)); + Assert.Equal("two, three", string.Join(", ", zRangeResult4)); + db.KeyDelete("myzset"); + // REMOVE_END + + + // STEP_START zrange2 + long zRangeResult5 = db.SortedSetAdd( + "myzset", + new SortedSetEntry[] { + new SortedSetEntry("one", 1), + new SortedSetEntry("two", 2), + new SortedSetEntry("three", 3) + } + ); + + SortedSetEntry[] zRangeResult6 = db.SortedSetRangeByRankWithScores("myzset", 0, 1); + Console.WriteLine($"{string.Join(", ", zRangeResult6.Select(b => $"{b.Element}: {b.Score}"))}"); + // >>> one: 1, two: 2 + // STEP_END + + // Tests for 'zrange2' step. + // REMOVE_START + Assert.Equal(3, zRangeResult5); + Assert.Equal("one: 1, two: 2", string.Join(", ", zRangeResult6.Select(b => $"{b.Element}: {b.Score}"))); + db.KeyDelete("myzset"); + // REMOVE_END + + + // STEP_START zrange3 + long zRangeResult7 = db.SortedSetAdd( + "myzset", + new SortedSetEntry[] { + new SortedSetEntry("one", 1), + new SortedSetEntry("two", 2), + new SortedSetEntry("three", 3) + } + ); + + RedisValue[] zRangeResult8 = db.SortedSetRangeByScore( + "myzset", + 1, + double.PositiveInfinity, + Exclude.Start, + skip: 1, take: 1 + ); + Console.WriteLine(string.Join(", ", zRangeResult8)); + // >>> three + // STEP_END + + // Tests for 'zrange3' step. + // REMOVE_START + Assert.Equal(3, zRangeResult7); + Assert.Equal("three", string.Join(", ", zRangeResult8)); + db.KeyDelete("myzset"); + // REMOVE_END + + + // STEP_START zrangebylex + + // STEP_END + + // Tests for 'zrangebylex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrangebyscore + + // STEP_END + + // Tests for 'zrangebyscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrangestore + + // STEP_END + + // Tests for 'zrangestore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrank + + // STEP_END + + // Tests for 'zrank' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrem + + // STEP_END + + // Tests for 'zrem' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zremrangebylex + + // STEP_END + + // Tests for 'zremrangebylex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zremrangebyrank + + // STEP_END + + // Tests for 'zremrangebyrank' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zremrangebyscore + + // STEP_END + + // Tests for 'zremrangebyscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrevrange + + // STEP_END + + // Tests for 'zrevrange' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrevrangebylex + + // STEP_END + + // Tests for 'zrevrangebylex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrevrangebyscore + + // STEP_END + + // Tests for 'zrevrangebyscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zrevrank + + // STEP_END + + // Tests for 'zrevrank' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zscan + + // STEP_END + + // Tests for 'zscan' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zscore + + // STEP_END + + // Tests for 'zscore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zunion + + // STEP_END + + // Tests for 'zunion' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START zunionstore + + // STEP_END + + // Tests for 'zunionstore' step. + // REMOVE_START + + // REMOVE_END + + + // HIDE_START + } +} +// HIDE_END + From 44a27a0f2ff5327796d4b4b4c66175794e4a899d Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:18:17 +0100 Subject: [PATCH 02/22] DOC-4117 examples for DEL, EXPIRE, and TTL commands (#333) * DOC-4117 added EXPIRE examples * DOC-4117 added TTL example * DOC-4117 dotnet format changes * DOC-4117 try to fix expire tests failing for Stack v6.2.6 * DOC-4117 improved SkipIfRedis attribute * DOC-4117 dotnet format changes --- tests/Doc/CmdsGenericExample.cs | 438 ++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 tests/Doc/CmdsGenericExample.cs diff --git a/tests/Doc/CmdsGenericExample.cs b/tests/Doc/CmdsGenericExample.cs new file mode 100644 index 00000000..950805bd --- /dev/null +++ b/tests/Doc/CmdsGenericExample.cs @@ -0,0 +1,438 @@ +// EXAMPLE: cmds_generic +// HIDE_START + +using NRedisStack.Tests; +using StackExchange.Redis; + +// HIDE_END + +// REMOVE_START +namespace Doc; +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class CmdsGenericExample +{ + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void run() + { + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + //REMOVE_START + // Clear any keys here before using them in tests. + + //REMOVE_END + // HIDE_END + + + // STEP_START copy + + // STEP_END + + // Tests for 'copy' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START del + bool delResult1 = db.StringSet("key1", "Hello"); + Console.WriteLine(delResult1); // >>> true + + bool delResult2 = db.StringSet("key2", "World"); + Console.WriteLine(delResult2); // >>> true + + long delResult3 = db.KeyDelete(new RedisKey[] { "key1", "key2", "key3" }); + Console.WriteLine(delResult3); // >>> 2 + // STEP_END + + // Tests for 'del' step. + // REMOVE_START + Assert.True(delResult1); + Assert.True(delResult2); + Assert.Equal(2, delResult3); + // REMOVE_END + + + // STEP_START dump + + // STEP_END + + // Tests for 'dump' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START exists + + // STEP_END + + // Tests for 'exists' step. + // REMOVE_START + + // REMOVE_END + + // REMOVE_START + + // REMOVE_END + // STEP_START expire + bool expireResult1 = db.StringSet("mykey", "Hello"); + Console.WriteLine(expireResult1); // >>> true + + bool expireResult2 = db.KeyExpire("mykey", new TimeSpan(0, 0, 10)); + Console.WriteLine(expireResult2); // >>> true + + TimeSpan expireResult3 = db.KeyTimeToLive("mykey") ?? TimeSpan.Zero; + Console.WriteLine(Math.Round(expireResult3.TotalSeconds)); // >>> 10 + + bool expireResult4 = db.StringSet("mykey", "Hello World"); + Console.WriteLine(expireResult4); // >>> true + + TimeSpan expireResult5 = db.KeyTimeToLive("mykey") ?? TimeSpan.Zero; + Console.WriteLine(Math.Round(expireResult5.TotalSeconds).ToString()); // >>> 0 + + bool expireResult6 = db.KeyExpire("mykey", new TimeSpan(0, 0, 10), ExpireWhen.HasExpiry); + Console.WriteLine(expireResult6); // >>> false + + TimeSpan expireResult7 = db.KeyTimeToLive("mykey") ?? TimeSpan.Zero; + Console.WriteLine(Math.Round(expireResult7.TotalSeconds)); // >>> 0 + + bool expireResult8 = db.KeyExpire("mykey", new TimeSpan(0, 0, 10), ExpireWhen.HasNoExpiry); + Console.WriteLine(expireResult8); // >>> true + + TimeSpan expireResult9 = db.KeyTimeToLive("mykey") ?? TimeSpan.Zero; + Console.WriteLine(Math.Round(expireResult9.TotalSeconds)); // >>> 10 + // STEP_END + + // Tests for 'expire' step. + // REMOVE_START + Assert.True(expireResult1); + Assert.True(expireResult2); + Assert.Equal(10, Math.Round(expireResult3.TotalSeconds)); + Assert.True(expireResult4); + Assert.Equal(0, Math.Round(expireResult5.TotalSeconds)); + Assert.False(expireResult6); + Assert.Equal(0, Math.Round(expireResult7.TotalSeconds)); + Assert.True(expireResult8); + Assert.Equal(10, Math.Round(expireResult9.TotalSeconds)); + db.KeyDelete("mykey"); + + // REMOVE_END + + + // STEP_START expireat + + // STEP_END + + // Tests for 'expireat' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START expiretime + + // STEP_END + + // Tests for 'expiretime' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START keys + + // STEP_END + + // Tests for 'keys' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START migrate + + // STEP_END + + // Tests for 'migrate' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START move + + // STEP_END + + // Tests for 'move' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START object_encoding + + // STEP_END + + // Tests for 'object_encoding' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START object_freq + + // STEP_END + + // Tests for 'object_freq' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START object_idletime + + // STEP_END + + // Tests for 'object_idletime' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START object_refcount + + // STEP_END + + // Tests for 'object_refcount' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START persist + + // STEP_END + + // Tests for 'persist' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START pexpire + + // STEP_END + + // Tests for 'pexpire' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START pexpireat + + // STEP_END + + // Tests for 'pexpireat' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START pexpiretime + + // STEP_END + + // Tests for 'pexpiretime' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START pttl + + // STEP_END + + // Tests for 'pttl' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START randomkey + + // STEP_END + + // Tests for 'randomkey' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START rename + + // STEP_END + + // Tests for 'rename' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START renamenx + + // STEP_END + + // Tests for 'renamenx' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START restore + + // STEP_END + + // Tests for 'restore' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START scan1 + + // STEP_END + + // Tests for 'scan1' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START scan2 + + // STEP_END + + // Tests for 'scan2' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START scan3 + + // STEP_END + + // Tests for 'scan3' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START scan4 + + // STEP_END + + // Tests for 'scan4' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START sort + + // STEP_END + + // Tests for 'sort' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START sort_ro + + // STEP_END + + // Tests for 'sort_ro' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START touch + + // STEP_END + + // Tests for 'touch' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START ttl + bool ttlResult1 = db.StringSet("mykey", "Hello"); + Console.WriteLine(ttlResult1); // >>> true + + bool ttlResult2 = db.KeyExpire("mykey", new TimeSpan(0, 0, 10)); + Console.WriteLine(ttlResult2); + + TimeSpan ttlResult3 = db.KeyTimeToLive("mykey") ?? TimeSpan.Zero; + string ttlRes = Math.Round(ttlResult3.TotalSeconds).ToString(); + Console.WriteLine(Math.Round(ttlResult3.TotalSeconds)); // >>> 10 + // STEP_END + + // Tests for 'ttl' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START type + + // STEP_END + + // Tests for 'type' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START unlink + + // STEP_END + + // Tests for 'unlink' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START wait + + // STEP_END + + // Tests for 'wait' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START waitaof + + // STEP_END + + // Tests for 'waitaof' step. + // REMOVE_START + + // REMOVE_END + + + // HIDE_START + } +} +// HIDE_END + From 3f90d205b297fc9c91d037ce2ad18f0849d7eb85 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:26:44 +0100 Subject: [PATCH 03/22] DOC-4101 added INCR example (#334) * DOC-4101 added INCR example * DOC-4101 delete key after test --- tests/Doc/CmdsStringExample.cs | 324 +++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 tests/Doc/CmdsStringExample.cs diff --git a/tests/Doc/CmdsStringExample.cs b/tests/Doc/CmdsStringExample.cs new file mode 100644 index 00000000..462cacd1 --- /dev/null +++ b/tests/Doc/CmdsStringExample.cs @@ -0,0 +1,324 @@ +// EXAMPLE: cmds_string +// HIDE_START + +using NRedisStack.Tests; +using StackExchange.Redis; + +// HIDE_END + +// REMOVE_START +namespace Doc; +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class CmdsStringExample +{ + + [SkipIfRedis(Is.OSSCluster)] + public void run() + { + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + //REMOVE_START + // Clear any keys here before using them in tests. + + //REMOVE_END + // HIDE_END + + + // STEP_START append1 + + // STEP_END + + // Tests for 'append1' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START append2 + + // STEP_END + + // Tests for 'append2' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START decr + + // STEP_END + + // Tests for 'decr' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START decrby + + // STEP_END + + // Tests for 'decrby' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START get + + // STEP_END + + // Tests for 'get' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START getdel + + // STEP_END + + // Tests for 'getdel' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START getex + + // STEP_END + + // Tests for 'getex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START getrange + + // STEP_END + + // Tests for 'getrange' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START getset + + // STEP_END + + // Tests for 'getset' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START incr + bool incrResult1 = db.StringSet("mykey", "10"); + Console.WriteLine(incrResult1); // >>> true + + long incrResult2 = db.StringIncrement("mykey"); + Console.WriteLine(incrResult2); // >>> 11 + + RedisValue incrResult3 = db.StringGet("mykey"); + Console.WriteLine(incrResult3); // >>> 11 + // STEP_END + + // Tests for 'incr' step. + // REMOVE_START + Assert.True(incrResult1); + Assert.Equal(11, incrResult2); + Assert.Equal("11", incrResult3); + db.KeyDelete("mykey"); + // REMOVE_END + + + // STEP_START incrby + + // STEP_END + + // Tests for 'incrby' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START incrbyfloat + + // STEP_END + + // Tests for 'incrbyfloat' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START lcs1 + + // STEP_END + + // Tests for 'lcs1' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START lcs2 + + // STEP_END + + // Tests for 'lcs2' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START lcs3 + + // STEP_END + + // Tests for 'lcs3' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START lcs4 + + // STEP_END + + // Tests for 'lcs4' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START lcs5 + + // STEP_END + + // Tests for 'lcs5' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START mget + + // STEP_END + + // Tests for 'mget' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START mset + + // STEP_END + + // Tests for 'mset' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START msetnx + + // STEP_END + + // Tests for 'msetnx' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START psetex + + // STEP_END + + // Tests for 'psetex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START set + + // STEP_END + + // Tests for 'set' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START setex + + // STEP_END + + // Tests for 'setex' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START setnx + + // STEP_END + + // Tests for 'setnx' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START setrange1 + + // STEP_END + + // Tests for 'setrange1' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START setrange2 + + // STEP_END + + // Tests for 'setrange2' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START strlen + + // STEP_END + + // Tests for 'strlen' step. + // REMOVE_START + + // REMOVE_END + + + // STEP_START substr + + // STEP_END + + // Tests for 'substr' step. + // REMOVE_START + + // REMOVE_END + + + // HIDE_START + } +} +// HIDE_END + From b21c054b5bd315554b971062c17ffd6eab40f460 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:37:15 +0300 Subject: [PATCH 04/22] Update to SE.Redis 2.8.16 (#335) * Updata to SE.Redis 2.8.12 * upgrade to 2.8.16 --- src/NRedisStack/NRedisStack.csproj | 2 +- tests/Doc/Doc.csproj | 2 +- tests/NRedisStack.Tests/NRedisStack.Tests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NRedisStack/NRedisStack.csproj b/src/NRedisStack/NRedisStack.csproj index f3626ca6..26ddd3b9 100644 --- a/src/NRedisStack/NRedisStack.csproj +++ b/src/NRedisStack/NRedisStack.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/Doc/Doc.csproj b/tests/Doc/Doc.csproj index dec76f61..fefcd82a 100644 --- a/tests/Doc/Doc.csproj +++ b/tests/Doc/Doc.csproj @@ -20,7 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj index daba5299..7aceede5 100644 --- a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj +++ b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj @@ -27,7 +27,7 @@ - + From 7aedb1c63bae19485e122849514db3398308ee80 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:11:47 +0300 Subject: [PATCH 05/22] Update version to 0.13.0 (#336) --- src/NRedisStack/NRedisStack.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NRedisStack/NRedisStack.csproj b/src/NRedisStack/NRedisStack.csproj index 26ddd3b9..999af7f1 100644 --- a/src/NRedisStack/NRedisStack.csproj +++ b/src/NRedisStack/NRedisStack.csproj @@ -10,9 +10,9 @@ Redis OSS .Net Client for Redis Stack README.md - 0.12.0 - 0.12.0 - 0.12.0 + 0.13.0 + 0.13.0 + 0.13.0 From e7565683dccac6159e227fb0c9cb44845be1b522 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:24:14 +0100 Subject: [PATCH 06/22] DOC-4251 full text search examples (#339) --- tests/Doc/QueryFtExample.cs | 274 ++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 tests/Doc/QueryFtExample.cs diff --git a/tests/Doc/QueryFtExample.cs b/tests/Doc/QueryFtExample.cs new file mode 100644 index 00000000..bc7f544b --- /dev/null +++ b/tests/Doc/QueryFtExample.cs @@ -0,0 +1,274 @@ +// EXAMPLE: query_ft +// HIDE_START + +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Literals.Enums; +using NRedisStack.Tests; +using StackExchange.Redis; + +// HIDE_END + +// REMOVE_START +namespace Doc; +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class QueryFtExample +{ + + [SkipIfRedis(Is.OSSCluster)] + public void run() + { + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + //REMOVE_START + // Clear any keys here before using them in tests. + try { db.FT().DropIndex("idx:bicycle", true); } catch { } + //REMOVE_END + + Schema bikeSchema = new Schema() + .AddTextField(new FieldName("$.brand", "brand")) + .AddTextField(new FieldName("$.model", "model")) + .AddTextField(new FieldName("$.description", "description")); + + FTCreateParams bikeParams = new FTCreateParams() + .AddPrefix("bicycle:") + .On(IndexDataType.JSON); + + db.FT().Create("idx:bicycle", bikeParams, bikeSchema); + + var bicycles = new object[] + { + new + { + brand = "Velorim", + model = "Jigger", + price = 270, + description = "Small and powerful, the Jigger is the best ride " + + "for the smallest of tikes! This is the tiniest " + + "kids’ pedal bike on the market available without" + + " a coaster brake, the Jigger is the vehicle of " + + "choice for the rare tenacious little rider " + + "raring to go.", + condition = "used" + }, + new + { + brand = "Bicyk", + model = "Hillcraft", + price = 1200, + description = "Kids want to ride with as little weight as possible." + + " Especially on an incline! They may be at the age " + + "when a 27.5 inch wheel bike is just too clumsy coming " + + "off a 24 inch bike. The Hillcraft 26 is just the solution" + + " they need!", + condition = "used", + }, + new + { + brand = "Nord", + model = "Chook air 5", + price = 815, + description = "The Chook Air 5 gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first" + + " experience on tracks and easy cruising through forests" + + " and fields. The lower top tube makes it easy to mount" + + " and dismount in any situation, giving your kids greater" + + " safety on the trails.", + condition = "used", + }, + new + { + brand = "Eva", + model = "Eva 291", + price = 3400, + description = "The sister company to Nord, Eva launched in 2005 as the" + + " first and only women-dedicated bicycle brand. Designed" + + " by women for women, allEva bikes are optimized for the" + + " feminine physique using analytics from a body metrics" + + " database. If you like 29ers, try the Eva 291. It’s a " + + "brand new bike for 2022.. This full-suspension, " + + "cross-country ride has been designed for velocity. The" + + " 291 has 100mm of front and rear travel, a superlight " + + "aluminum frame and fast-rolling 29-inch wheels. Yippee!", + condition = "used", + }, + new + { + brand = "Noka Bikes", + model = "Kahuna", + price = 3200, + description = "Whether you want to try your hand at XC racing or are " + + "looking for a lively trail bike that's just as inspiring" + + " on the climbs as it is over rougher ground, the Wilder" + + " is one heck of a bike built specifically for short women." + + " Both the frames and components have been tweaked to " + + "include a women’s saddle, different bars and unique " + + "colourway.", + condition = "used", + }, + new + { + brand = "Breakout", + model = "XBN 2.1 Alloy", + price = 810, + description = "The XBN 2.1 Alloy is our entry-level road bike – but that’s" + + " not to say that it’s a basic machine. With an internal " + + "weld aluminium frame, a full carbon fork, and the slick-shifting" + + " Claris gears from Shimano’s, this is a bike which doesn’t" + + " break the bank and delivers craved performance.", + condition = "new", + }, + new + { + brand = "ScramBikes", + model = "WattBike", + price = 2300, + description = "The WattBike is the best e-bike for people who still feel young" + + " at heart. It has a Bafang 1000W mid-drive system and a 48V" + + " 17.5AH Samsung Lithium-Ion battery, allowing you to ride for" + + " more than 60 miles on one charge. It’s great for tackling hilly" + + " terrain or if you just fancy a more leisurely ride. With three" + + " working modes, you can choose between E-bike, assisted bicycle," + + " and normal bike modes.", + condition = "new", + }, + new + { + brand = "Peaknetic", + model = "Secto", + price = 430, + description = "If you struggle with stiff fingers or a kinked neck or back after" + + " a few minutes on the road, this lightweight, aluminum bike" + + " alleviates those issues and allows you to enjoy the ride. From" + + " the ergonomic grips to the lumbar-supporting seat position, the" + + " Roll Low-Entry offers incredible comfort. The rear-inclined seat" + + " tube facilitates stability by allowing you to put a foot on the" + + " ground to balance at a stop, and the low step-over frame makes it" + + " accessible for all ability and mobility levels. The saddle is" + + " very soft, with a wide back to support your hip joints and a" + + " cutout in the center to redistribute that pressure. Rim brakes" + + " deliver satisfactory braking control, and the wide tires provide" + + " a smooth, stable ride on paved roads and gravel. Rack and fender" + + " mounts facilitate setting up the Roll Low-Entry as your preferred" + + " commuter, and the BMX-like handlebar offers space for mounting a" + + " flashlight, bell, or phone holder.", + condition = "new", + }, + new + { + brand = "nHill", + model = "Summit", + price = 1200, + description = "This budget mountain bike from nHill performs well both on bike" + + " paths and on the trail. The fork with 100mm of travel absorbs" + + " rough terrain. Fat Kenda Booster tires give you grip in corners" + + " and on wet trails. The Shimano Tourney drivetrain offered enough" + + " gears for finding a comfortable pace to ride uphill, and the" + + " Tektro hydraulic disc brakes break smoothly. Whether you want an" + + " affordable bike that you can take to work, but also take trail in" + + " mountains on the weekends or you’re just after a stable," + + " comfortable ride for the bike path, the Summit gives a good value" + + " for money.", + condition = "new", + }, + new + { + model = "ThrillCycle", + brand = "BikeShind", + price = 815, + description = "An artsy, retro-inspired bicycle that’s as functional as it is" + + " pretty: The ThrillCycle steel frame offers a smooth ride. A" + + " 9-speed drivetrain has enough gears for coasting in the city, but" + + " we wouldn’t suggest taking it to the mountains. Fenders protect" + + " you from mud, and a rear basket lets you transport groceries," + + " flowers and books. The ThrillCycle comes with a limited lifetime" + + " warranty, so this little guy will last you long past graduation.", + condition = "refurbished", + }, + }; + + for (var i = 0; i < bicycles.Length; i++) + { + db.JSON().Set($"bicycle:{i}", "$", bicycles[i]); + } + // HIDE_END + + + // STEP_START ft1 + SearchResult res1 = db.FT().Search( + "idx:bicycle", + new Query("@description: kids") + ); + Console.WriteLine(res1); // >>> 2 + // STEP_END + + // Tests for 'ft1' step. + // REMOVE_START + Assert.Equal(2, res1.TotalResults); + // REMOVE_END + + + // STEP_START ft2 + SearchResult res2 = db.FT().Search( + "idx:bicycle", + new Query("@model: ka*") + ); + Console.WriteLine(res2.TotalResults); // >>> 1 + // STEP_END + + // Tests for 'ft2' step. + // REMOVE_START + Assert.Equal(1, res2.TotalResults); + // REMOVE_END + + + // STEP_START ft3 + SearchResult res3 = db.FT().Search( + "idx:bicycle", + new Query("@brand: *bikes") + ); + Console.WriteLine(res3.TotalResults); // >>> 2 + // STEP_END + + // Tests for 'ft3' step. + // REMOVE_START + Assert.Equal(2, res3.TotalResults); + // REMOVE_END + + + // STEP_START ft4 + SearchResult res4 = db.FT().Search( + "idx:bicycle", + new Query("%optamized%") + ); + Console.WriteLine(res4.TotalResults); // >>> 1 + // STEP_END + + // Tests for 'ft4' step. + // REMOVE_START + Assert.Equal(1, res4.TotalResults); + // REMOVE_END + + + // STEP_START ft5 + SearchResult res5 = db.FT().Search( + "idx:bicycle", + new Query("%%optamised%%") + ); + Console.WriteLine(res5.TotalResults); // >>> 1 + // STEP_END + + // Tests for 'ft5' step. + // REMOVE_START + Assert.Equal(1, res5.TotalResults); + // REMOVE_END + + + // HIDE_START + } +} +// HIDE_END + From cb6d6850304bce0c315696ed42162e9da8bf3413 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:21:23 +0100 Subject: [PATCH 07/22] DOC-4243 added range query examples (#338) * DOC-4243 added range query examples * DOC-4243 implemented PR feedback * DOC-4243 reinstated filter example using infinity --- tests/Doc/QueryRangeExample.cs | 273 +++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 tests/Doc/QueryRangeExample.cs diff --git a/tests/Doc/QueryRangeExample.cs b/tests/Doc/QueryRangeExample.cs new file mode 100644 index 00000000..44755930 --- /dev/null +++ b/tests/Doc/QueryRangeExample.cs @@ -0,0 +1,273 @@ +// EXAMPLE: query_range +// HIDE_START + +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Literals.Enums; +using NRedisStack.Tests; +using StackExchange.Redis; + +// HIDE_END + +// REMOVE_START +namespace Doc; +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class QueryRangeExample +{ + + [SkipIfRedis(Is.OSSCluster)] + public void run() + { + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + //REMOVE_START + // Clear any keys here before using them in tests. + try { db.FT().DropIndex("idx:bicycle", dd: true); } catch { } + //REMOVE_END + + Schema bikeSchema = new Schema() + .AddTextField(new FieldName("$.description", "description")) + .AddNumericField(new FieldName("$.price", "price")) + .AddTagField(new FieldName("$.condition", "condition")); + + FTCreateParams bikeParams = new FTCreateParams() + .AddPrefix("bicycle:") + .On(IndexDataType.JSON); + + db.FT().Create("idx:bicycle", bikeParams, bikeSchema); + + var bicycles = new object[] + { + new + { + brand = "Velorim", + model = "Jigger", + price = 270, + description = "Small and powerful, the Jigger is the best ride " + + "for the smallest of tikes! This is the tiniest " + + "kids’ pedal bike on the market available without" + + " a coaster brake, the Jigger is the vehicle of " + + "choice for the rare tenacious little rider " + + "raring to go.", + condition = "used" + }, + new + { + brand = "Bicyk", + model = "Hillcraft", + price = 1200, + description = "Kids want to ride with as little weight as possible." + + " Especially on an incline! They may be at the age " + + "when a 27.5 inch wheel bike is just too clumsy coming " + + "off a 24 inch bike. The Hillcraft 26 is just the solution" + + " they need!", + condition = "used", + }, + new + { + brand = "Nord", + model = "Chook air 5", + price = 815, + description = "The Chook Air 5 gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first" + + " experience on tracks and easy cruising through forests" + + " and fields. The lower top tube makes it easy to mount" + + " and dismount in any situation, giving your kids greater" + + " safety on the trails.", + condition = "used", + }, + new + { + brand = "Eva", + model = "Eva 291", + price = 3400, + description = "The sister company to Nord, Eva launched in 2005 as the" + + " first and only women-dedicated bicycle brand. Designed" + + " by women for women, allEva bikes are optimized for the" + + " feminine physique using analytics from a body metrics" + + " database. If you like 29ers, try the Eva 291. It’s a " + + "brand new bike for 2022.. This full-suspension, " + + "cross-country ride has been designed for velocity. The" + + " 291 has 100mm of front and rear travel, a superlight " + + "aluminum frame and fast-rolling 29-inch wheels. Yippee!", + condition = "used", + }, + new + { + brand = "Noka Bikes", + model = "Kahuna", + price = 3200, + description = "Whether you want to try your hand at XC racing or are " + + "looking for a lively trail bike that's just as inspiring" + + " on the climbs as it is over rougher ground, the Wilder" + + " is one heck of a bike built specifically for short women." + + " Both the frames and components have been tweaked to " + + "include a women’s saddle, different bars and unique " + + "colourway.", + condition = "used", + }, + new + { + brand = "Breakout", + model = "XBN 2.1 Alloy", + price = 810, + description = "The XBN 2.1 Alloy is our entry-level road bike – but that’s" + + " not to say that it’s a basic machine. With an internal " + + "weld aluminium frame, a full carbon fork, and the slick-shifting" + + " Claris gears from Shimano’s, this is a bike which doesn’t" + + " break the bank and delivers craved performance.", + condition = "new", + }, + new + { + brand = "ScramBikes", + model = "WattBike", + price = 2300, + description = "The WattBike is the best e-bike for people who still feel young" + + " at heart. It has a Bafang 1000W mid-drive system and a 48V" + + " 17.5AH Samsung Lithium-Ion battery, allowing you to ride for" + + " more than 60 miles on one charge. It’s great for tackling hilly" + + " terrain or if you just fancy a more leisurely ride. With three" + + " working modes, you can choose between E-bike, assisted bicycle," + + " and normal bike modes.", + condition = "new", + }, + new + { + brand = "Peaknetic", + model = "Secto", + price = 430, + description = "If you struggle with stiff fingers or a kinked neck or back after" + + " a few minutes on the road, this lightweight, aluminum bike" + + " alleviates those issues and allows you to enjoy the ride. From" + + " the ergonomic grips to the lumbar-supporting seat position, the" + + " Roll Low-Entry offers incredible comfort. The rear-inclined seat" + + " tube facilitates stability by allowing you to put a foot on the" + + " ground to balance at a stop, and the low step-over frame makes it" + + " accessible for all ability and mobility levels. The saddle is" + + " very soft, with a wide back to support your hip joints and a" + + " cutout in the center to redistribute that pressure. Rim brakes" + + " deliver satisfactory braking control, and the wide tires provide" + + " a smooth, stable ride on paved roads and gravel. Rack and fender" + + " mounts facilitate setting up the Roll Low-Entry as your preferred" + + " commuter, and the BMX-like handlebar offers space for mounting a" + + " flashlight, bell, or phone holder.", + condition = "new", + }, + new + { + brand = "nHill", + model = "Summit", + price = 1200, + description = "This budget mountain bike from nHill performs well both on bike" + + " paths and on the trail. The fork with 100mm of travel absorbs" + + " rough terrain. Fat Kenda Booster tires give you grip in corners" + + " and on wet trails. The Shimano Tourney drivetrain offered enough" + + " gears for finding a comfortable pace to ride uphill, and the" + + " Tektro hydraulic disc brakes break smoothly. Whether you want an" + + " affordable bike that you can take to work, but also take trail in" + + " mountains on the weekends or you’re just after a stable," + + " comfortable ride for the bike path, the Summit gives a good value" + + " for money.", + condition = "new", + }, + new + { + model = "ThrillCycle", + brand = "BikeShind", + price = 815, + description = "An artsy, retro-inspired bicycle that’s as functional as it is" + + " pretty: The ThrillCycle steel frame offers a smooth ride. A" + + " 9-speed drivetrain has enough gears for coasting in the city, but" + + " we wouldn’t suggest taking it to the mountains. Fenders protect" + + " you from mud, and a rear basket lets you transport groceries," + + " flowers and books. The ThrillCycle comes with a limited lifetime" + + " warranty, so this little guy will last you long past graduation.", + condition = "refurbished", + }, + }; + + for (var i = 0; i < bicycles.Length; i++) + { + db.JSON().Set($"bicycle:{i}", "$", bicycles[i]); + } + // HIDE_END + + + // STEP_START range1 + SearchResult res1 = db.FT().Search( + "idx:bicycle", + new Query("@price:[500 1000]") + ); + Console.WriteLine(res1.TotalResults); // >>> 3 + // STEP_END + + // Tests for 'range1' step. + // REMOVE_START + Assert.Equal(3, res1.TotalResults); + // REMOVE_END + + + // STEP_START range2 + SearchResult res2 = db.FT().Search( + "idx:bicycle", + new Query().AddFilter( + new Query.NumericFilter("price", 500, 1000) + ) + ); + Console.WriteLine(res2.TotalResults); // >>> 3 + // STEP_END + + // Tests for 'range2' step. + // REMOVE_START + Assert.Equal(3, res2.TotalResults); + // REMOVE_END + + + // STEP_START range3 + SearchResult res3 = db.FT().Search( + "idx:bicycle", + new Query("*").AddFilter(new Query.NumericFilter( + "price", 1000, true, Double.PositiveInfinity, false + ) + ) + ); + Console.WriteLine(res3.TotalResults); // >>> 5 + // STEP_END + + // Tests for 'range3' step. + // REMOVE_START + Assert.Equal(5, res3.TotalResults); + // REMOVE_END + + + // STEP_START range4 + SearchResult res4 = db.FT().Search( + "idx:bicycle", + new Query("@price:[-inf 2000]") + .SetSortBy("price") + .Limit(0, 5) + ); + Console.WriteLine(res4.TotalResults); // >>> 7 + Console.WriteLine($"Prices: {string.Join(", ", res4.Documents.Select(d => d["price"]))}"); + // >>> Prices: 270, 430, 810, 815, 815 + // STEP_END + + // Tests for 'range4' step. + // REMOVE_START + Assert.Equal(7, res4.TotalResults); + Assert.Equal( + "Prices: 270, 430, 810, 815, 815", + $"Prices: {string.Join(", ", res4.Documents.Select(d => d["price"]))}" + ); + // REMOVE_END + + + // HIDE_START + } +} +// HIDE_END + From 7f93841015cf6ebc4e39c13d251cbd707383a7ef Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 27 Sep 2024 23:09:43 +0100 Subject: [PATCH 08/22] DOC-4295 added aggregate query examples (#341) * DOC-4295 added aggregate query examples * DOC-4295 added try-catch around dropIndex call * DOC-4295 fixed non-deterministic tests * DOC-4295 replaced dodgy agg4 example for examination * DOC-4295 reinstated missing example message --- tests/Doc/QueryAggExample.cs | 327 +++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 tests/Doc/QueryAggExample.cs diff --git a/tests/Doc/QueryAggExample.cs b/tests/Doc/QueryAggExample.cs new file mode 100644 index 00000000..ec5f696d --- /dev/null +++ b/tests/Doc/QueryAggExample.cs @@ -0,0 +1,327 @@ +// EXAMPLE: query_agg +// HIDE_START + +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Aggregation; +using NRedisStack.Search.Literals.Enums; +using NRedisStack.Tests; +using StackExchange.Redis; + +// HIDE_END + +// REMOVE_START +namespace Doc; +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class QueryAggExample +{ + + [SkipIfRedis(Is.OSSCluster)] + public void run() + { + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + //REMOVE_START + // Clear any keys here before using them in tests. + try { db.FT().DropIndex("idx:bicycle"); } catch { } + //REMOVE_END + + Schema bikeSchema = new Schema() + .AddTagField(new FieldName("$.condition", "condition")) + .AddNumericField(new FieldName("$.price", "price")); + + FTCreateParams bikeParams = new FTCreateParams() + .AddPrefix("bicycle:") + .On(IndexDataType.JSON); + + db.FT().Create("idx:bicycle", bikeParams, bikeSchema); + + var bicycles = new object[] { + new + { + pickup_zone = "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, -74.0610 40.6678, -74.0610 40.7578))", + store_location = "-74.0060,40.7128", + brand = "Velorim", + model = "Jigger", + price = 270, + description = "Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.", + condition = "new" + }, + new + { + pickup_zone = "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, -118.2887 33.9872, -118.2887 34.0972))", + store_location = "-118.2437,34.0522", + brand = "Bicyk", + model = "Hillcraft", + price = 1200, + description = "Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need!", + condition = "used" + }, + new + { + pickup_zone = "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, -87.6848 41.8231, -87.6848 41.9331))", + store_location = "-87.6298,41.8781", + brand = "Nord", + model = "Chook air 5", + price = 815, + description = "The Chook Air 5 gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails.", + condition = "used" + }, + new + { + pickup_zone = "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, -80.2433 25.6967, -80.2433 25.8067))", + store_location = "-80.1918,25.7617", + brand = "Eva", + model = "Eva 291", + price = 3400, + description = "The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!", + condition = "used" + }, + new + { + pickup_zone = "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, -122.4644 37.7099, -122.4644 37.8199))", + store_location = "-122.4194,37.7749", + brand = "Noka Bikes", + model = "Kahuna", + price = 3200, + description = "Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.", + condition = "used" + }, + new + { + pickup_zone = "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, -0.1778 51.4024, -0.1778 51.5524))", + store_location = "-0.1278,51.5074", + brand = "Breakout", + model = "XBN 2.1 Alloy", + price = 810, + description = "The XBN 2.1 Alloy is our entry-level road bike – but that’s not to say that it’s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.", + condition = "new" + }, + new + { + pickup_zone = "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, 2.1767 48.5516, 2.1767 48.9016))", + store_location = "2.3522,48.8566", + brand = "ScramBikes", + model = "WattBike", + price = 2300, + description = "The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It’s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.", + condition = "new" + }, + new + { + pickup_zone = "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, 13.3260 52.2700, 13.3260 52.5700))", + store_location = "13.4050,52.5200", + brand = "Peaknetic", + model = "Secto", + price = 430, + description = "If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + condition = "new" + }, + new + { + pickup_zone = "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, 1.9450 41.1987, 1.9450 41.4301))", + store_location = "2.1734, 41.3851", + brand = "nHill", + model = "Summit", + price = 1200, + description = "This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you’re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.", + condition = "new" + }, + new + { + pickup_zone = "POLYGON((12.4464 42.1028, 12.5464 42.1028, 12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + store_location = "12.4964,41.9028", + model = "ThrillCycle", + brand = "BikeShind", + price = 815, + description = "An artsy, retro-inspired bicycle that’s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.", + condition = "refurbished" + } + }; + + for (var i = 0; i < bicycles.Length; i++) + { + db.JSON().Set($"bicycle:{i}", "$", bicycles[i]); + } + // HIDE_END + + + // STEP_START agg1 + AggregationResult res1 = db.FT().Aggregate( + "idx:bicycle", + new AggregationRequest("@condition:{new}") + .Load(new FieldName("__key"), new FieldName("price")) + .Apply("@price - (@price * 0.1)", "discounted") + ); + Console.WriteLine(res1.TotalResults); // >>> 5 + + for (int i = 0; i < res1.TotalResults; i++) + { + Row res1Row = res1.GetRow(i); + + Console.WriteLine( + $"Key: {res1Row["__key"]}, Price: {res1Row["price"]}, Discounted: {res1Row["discounted"]}" + ); + } + // >>> Key: bicycle:0, Price: 270, Discounted: 243 + // >>> Key: bicycle:5, Price: 810, Discounted: 729 + // >>> Key: bicycle:6, Price: 2300, Discounted: 2070 + // >>> Key: bicycle:7, Price: 430, Discounted: 387 + // >>> Key: bicycle:8, Price: 1200, Discounted: 1080 + // STEP_END + + // Tests for 'agg1' step. + // REMOVE_START + Assert.Equal(5, res1.TotalResults); + + for (int i = 0; i < 5; i++) + { + Row test1Row = res1.GetRow(i); + + switch (test1Row["__key"]) + { + case "bicycle:0": + Assert.Equal( + "Key: bicycle:0, Price: 270, Discounted: 243", + $"Key: {test1Row["__key"]}, Price: {test1Row["price"]}, Discounted: {test1Row["discounted"]}" + ); + break; + + case "bicycle:5": + Assert.Equal( + "Key: bicycle:5, Price: 810, Discounted: 729", + $"Key: {test1Row["__key"]}, Price: {test1Row["price"]}, Discounted: {test1Row["discounted"]}" + ); + break; + + case "bicycle:6": + Assert.Equal( + "Key: bicycle:6, Price: 2300, Discounted: 2070", + $"Key: {test1Row["__key"]}, Price: {test1Row["price"]}, Discounted: {test1Row["discounted"]}" + ); + break; + + case "bicycle:7": + Assert.Equal( + "Key: bicycle:7, Price: 430, Discounted: 387", + $"Key: {test1Row["__key"]}, Price: {test1Row["price"]}, Discounted: {test1Row["discounted"]}" + ); + break; + + case "bicycle:8": + Assert.Equal( + "Key: bicycle:8, Price: 1200, Discounted: 1080", + $"Key: {test1Row["__key"]}, Price: {test1Row["price"]}, Discounted: {test1Row["discounted"]}" + ); + break; + } + } + + // REMOVE_END + + + // STEP_START agg2 + AggregationResult res2 = db.FT().Aggregate( + "idx:bicycle", + new AggregationRequest("*") + .Load(new FieldName("price")) + .Apply("@price<1000", "price_category") + .GroupBy( + "@condition", + Reducers.Sum("@price_category").As("num_affordable") + ) + ); + Console.WriteLine(res2.TotalResults); // >>> 3 + + for (int i = 0; i < res2.TotalResults; i++) + { + Row res2Row = res2.GetRow(i); + + Console.WriteLine( + $"Condition: {res2Row["condition"]}, Num affordable: {res2Row["num_affordable"]}" + ); + } + // >>> Condition: refurbished, Num affordable: 1 + // >>> Condition: used, Num affordable: 1 + // >>> Condition: new, Num affordable: 3 + // STEP_END + + // Tests for 'agg2' step. + // REMOVE_START + Assert.Equal(3, res2.TotalResults); + + for (int i = 0; i < 3; i++) + { + Row test2Row = res2.GetRow(i); + switch (test2Row["condition"]) + { + case "refurbished": + Assert.Equal( + "Condition: refurbished, Num affordable: 1", + $"Condition: {test2Row["condition"]}, Num affordable: {test2Row["num_affordable"]}" + ); + break; + + case "used": + Assert.Equal( + "Condition: used, Num affordable: 1", + $"Condition: {test2Row["condition"]}, Num affordable: {test2Row["num_affordable"]}" + ); + break; + + case "new": + Assert.Equal( + "Condition: new, Num affordable: 3", + $"Condition: {test2Row["condition"]}, Num affordable: {test2Row["num_affordable"]}" + ); + break; + } + } + // REMOVE_END + + + // STEP_START agg3 + AggregationResult res3 = db.FT().Aggregate( + "idx:bicycle", + new AggregationRequest("*") + .Apply("'bicycle'", "type") + .GroupBy("@type", Reducers.Count().As("num_total")) + ); + Console.WriteLine(res3.TotalResults); // >>> 1 + + Row res3Row = res3.GetRow(0); + Console.WriteLine($"Type: {res3Row["type"]}, Num total: {res3Row["num_total"]}"); + // >>> Type: bicycle, Num total: 10 + // STEP_END + + // Tests for 'agg3' step. + // REMOVE_START + Assert.Equal(1, res3.TotalResults); + + Assert.Equal( + "Type: bicycle, Num total: 10", + $"Type: {res3Row["type"]}, Num total: {res3Row["num_total"]}" + ); + // REMOVE_END + + + // STEP_START agg4 + + // Not supported in NRedisStack. + + // STEP_END + + // Tests for 'agg4' step. + // REMOVE_START + + // REMOVE_END + + + // HIDE_START + } +} +// HIDE_END + From 0a8bdfae97c5a18ca178e00bc387f3981d7c5790 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:16:31 +0100 Subject: [PATCH 09/22] DOC-4201 added exact match query examples (#337) * DOC-4201 added exact match query examples * DOC-4201 dotnet format changes * DOC-4201 removed unused 'using' statements * DOC-4201 fixed email tag example --------- Co-authored-by: atakavci <58048133+atakavci@users.noreply.github.com> --- tests/Doc/QueryEmExample.cs | 284 ++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 tests/Doc/QueryEmExample.cs diff --git a/tests/Doc/QueryEmExample.cs b/tests/Doc/QueryEmExample.cs new file mode 100644 index 00000000..590a212d --- /dev/null +++ b/tests/Doc/QueryEmExample.cs @@ -0,0 +1,284 @@ +// EXAMPLE: query_em +// HIDE_START + +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Literals.Enums; +using NRedisStack.Tests; +using StackExchange.Redis; + +// HIDE_END + +// REMOVE_START +namespace Doc; +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class QueryEmExample +{ + + [SkipIfRedis(Is.OSSCluster)] + public void run() + { + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + //REMOVE_START + // Clear any keys here before using them in tests. + try { db.FT().DropIndex("idx:bicycle"); } catch { } + try { db.FT().DropIndex("idx:email"); } catch { } + //REMOVE_END + + FTCreateParams idxParams = new FTCreateParams() + .AddPrefix("bicycle:") + .On(IndexDataType.JSON); + + Schema bikeSchema = new Schema() + .AddTextField(new FieldName("$.description", "description")) + .AddNumericField(new FieldName("$.price", "price")) + .AddTagField(new FieldName("$.condition", "condition")); + + db.FT().Create("idx:bicycle", idxParams, bikeSchema); + + + var bicycles = new object[] + { + new + { + brand = "Velorim", + model = "Jigger", + price = 270, + description = "Small and powerful, the Jigger is the best ride " + + "for the smallest of tikes! This is the tiniest " + + "kids’ pedal bike on the market available without" + + " a coaster brake, the Jigger is the vehicle of " + + "choice for the rare tenacious little rider " + + "raring to go.", + condition = "used" + }, + new + { + brand = "Bicyk", + model = "Hillcraft", + price = 1200, + description = "Kids want to ride with as little weight as possible." + + " Especially on an incline! They may be at the age " + + "when a 27.5 inch wheel bike is just too clumsy coming " + + "off a 24 inch bike. The Hillcraft 26 is just the solution" + + " they need!", + condition = "used", + }, + new + { + brand = "Nord", + model = "Chook air 5", + price = 815, + description = "The Chook Air 5 gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first" + + " experience on tracks and easy cruising through forests" + + " and fields. The lower top tube makes it easy to mount" + + " and dismount in any situation, giving your kids greater" + + " safety on the trails.", + condition = "used", + }, + new + { + brand = "Eva", + model = "Eva 291", + price = 3400, + description = "The sister company to Nord, Eva launched in 2005 as the" + + " first and only women-dedicated bicycle brand. Designed" + + " by women for women, allEva bikes are optimized for the" + + " feminine physique using analytics from a body metrics" + + " database. If you like 29ers, try the Eva 291. It’s a " + + "brand new bike for 2022.. This full-suspension, " + + "cross-country ride has been designed for velocity. The" + + " 291 has 100mm of front and rear travel, a superlight " + + "aluminum frame and fast-rolling 29-inch wheels. Yippee!", + condition = "used", + }, + new + { + brand = "Noka Bikes", + model = "Kahuna", + price = 3200, + description = "Whether you want to try your hand at XC racing or are " + + "looking for a lively trail bike that's just as inspiring" + + " on the climbs as it is over rougher ground, the Wilder" + + " is one heck of a bike built specifically for short women." + + " Both the frames and components have been tweaked to " + + "include a women’s saddle, different bars and unique " + + "colourway.", + condition = "used", + }, + new + { + brand = "Breakout", + model = "XBN 2.1 Alloy", + price = 810, + description = "The XBN 2.1 Alloy is our entry-level road bike – but that’s" + + " not to say that it’s a basic machine. With an internal " + + "weld aluminium frame, a full carbon fork, and the slick-shifting" + + " Claris gears from Shimano’s, this is a bike which doesn’t" + + " break the bank and delivers craved performance.", + condition = "new", + }, + new + { + brand = "ScramBikes", + model = "WattBike", + price = 2300, + description = "The WattBike is the best e-bike for people who still feel young" + + " at heart. It has a Bafang 1000W mid-drive system and a 48V" + + " 17.5AH Samsung Lithium-Ion battery, allowing you to ride for" + + " more than 60 miles on one charge. It’s great for tackling hilly" + + " terrain or if you just fancy a more leisurely ride. With three" + + " working modes, you can choose between E-bike, assisted bicycle," + + " and normal bike modes.", + condition = "new", + }, + new + { + brand = "Peaknetic", + model = "Secto", + price = 430, + description = "If you struggle with stiff fingers or a kinked neck or back after" + + " a few minutes on the road, this lightweight, aluminum bike" + + " alleviates those issues and allows you to enjoy the ride. From" + + " the ergonomic grips to the lumbar-supporting seat position, the" + + " Roll Low-Entry offers incredible comfort. The rear-inclined seat" + + " tube facilitates stability by allowing you to put a foot on the" + + " ground to balance at a stop, and the low step-over frame makes it" + + " accessible for all ability and mobility levels. The saddle is" + + " very soft, with a wide back to support your hip joints and a" + + " cutout in the center to redistribute that pressure. Rim brakes" + + " deliver satisfactory braking control, and the wide tires provide" + + " a smooth, stable ride on paved roads and gravel. Rack and fender" + + " mounts facilitate setting up the Roll Low-Entry as your preferred" + + " commuter, and the BMX-like handlebar offers space for mounting a" + + " flashlight, bell, or phone holder.", + condition = "new", + }, + new + { + brand = "nHill", + model = "Summit", + price = 1200, + description = "This budget mountain bike from nHill performs well both on bike" + + " paths and on the trail. The fork with 100mm of travel absorbs" + + " rough terrain. Fat Kenda Booster tires give you grip in corners" + + " and on wet trails. The Shimano Tourney drivetrain offered enough" + + " gears for finding a comfortable pace to ride uphill, and the" + + " Tektro hydraulic disc brakes break smoothly. Whether you want an" + + " affordable bike that you can take to work, but also take trail in" + + " mountains on the weekends or you’re just after a stable," + + " comfortable ride for the bike path, the Summit gives a good value" + + " for money.", + condition = "new", + }, + new + { + model = "ThrillCycle", + brand = "BikeShind", + price = 815, + description = "An artsy, retro-inspired bicycle that’s as functional as it is" + + " pretty: The ThrillCycle steel frame offers a smooth ride. A" + + " 9-speed drivetrain has enough gears for coasting in the city, but" + + " we wouldn’t suggest taking it to the mountains. Fenders protect" + + " you from mud, and a rear basket lets you transport groceries," + + " flowers and books. The ThrillCycle comes with a limited lifetime" + + " warranty, so this little guy will last you long past graduation.", + condition = "refurbished", + }, + }; + + for (var i = 0; i < bicycles.Length; i++) + { + db.JSON().Set($"bicycle:{i}", "$", bicycles[i]); + } + // HIDE_END + + + // STEP_START em1 + SearchResult res1 = db.FT().Search( + "idx:bicycle", + new Query("@price:[270 270]") + ); + Console.WriteLine(res1.TotalResults); // >>> 1 + + SearchResult res2 = db.FT().Search( + "idx:bicycle", + new Query().AddFilter( + new Query.NumericFilter("price", 270, 270) + ) + ); + Console.WriteLine(res2.TotalResults); // >>> 1 + // STEP_END + + // Tests for 'em1' step. + // REMOVE_START + Assert.Equal(1, res1.TotalResults); + Assert.Equal(1, res2.TotalResults); + // REMOVE_END + + + // STEP_START em2 + SearchResult res3 = db.FT().Search( + "idx:bicycle", + new Query("@condition:{new}") + ); + Console.WriteLine(res3.TotalResults); // >>> 4 + // STEP_END + + // Tests for 'em2' step. + // REMOVE_START + Assert.Equal(4, res3.TotalResults); + // REMOVE_END + + + // STEP_START em3 + Schema emailSchema = new Schema() + .AddTagField(new FieldName("$.email", "email")); + + FTCreateParams emailParams = new FTCreateParams() + .AddPrefix("key:") + .On(IndexDataType.JSON); + + db.FT().Create("idx:email", emailParams, emailSchema); + + db.JSON().Set("key:1", "$", "{\"email\": \"test@redis.com\"}"); + + + SearchResult res4 = db.FT().Search( + "idx:email", + new Query("@email:{test\\@redis\\.com}").Dialect(2) + ); + Console.WriteLine(res4.TotalResults); // >>> 1 + // STEP_END + + // Tests for 'em3' step. + // REMOVE_START + db.FT().DropIndex("idx:email"); + Assert.Equal(1, res4.TotalResults); + // REMOVE_END + + + // STEP_START em4 + SearchResult res5 = db.FT().Search( + "idx:bicycle", + new Query("@description:\"rough terrain\"") + ); + Console.WriteLine(res5.TotalResults); // >>> 1 + // STEP_END + + // Tests for 'em4' step. + // REMOVE_START + Assert.Equal(1, res5.TotalResults); + // REMOVE_END + + + // HIDE_START + } +} +// HIDE_END + From 0171221a5911841084a6ba1655cd72e19cadea43 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:31:08 +0300 Subject: [PATCH 10/22] Deprecate Triggers and Functions (#343) deprecate T&F --- src/NRedisStack/Gears/GearsCommandBuilder.cs | 5 + src/NRedisStack/Gears/GearsCommands.cs | 7 +- src/NRedisStack/Gears/GearsCommandsAsync.cs | 5 + tests/NRedisStack.Tests/Gears/GearsTests.cs | 210 ------------------- 4 files changed, 16 insertions(+), 211 deletions(-) delete mode 100644 tests/NRedisStack.Tests/Gears/GearsTests.cs diff --git a/src/NRedisStack/Gears/GearsCommandBuilder.cs b/src/NRedisStack/Gears/GearsCommandBuilder.cs index 5af745ba..8dec9026 100644 --- a/src/NRedisStack/Gears/GearsCommandBuilder.cs +++ b/src/NRedisStack/Gears/GearsCommandBuilder.cs @@ -3,8 +3,10 @@ namespace NRedisStack { + [Obsolete] public static class GearsCommandBuilder { + [Obsolete] public static SerializedCommand TFunctionLoad(string libraryCode, bool replace = false, string? config = null) { var args = new List() { GearsArgs.LOAD }; @@ -23,11 +25,13 @@ public static SerializedCommand TFunctionLoad(string libraryCode, bool replace = return new SerializedCommand(RG.TFUNCTION, args); } + [Obsolete] public static SerializedCommand TFunctionDelete(string libraryName) { return new SerializedCommand(RG.TFUNCTION, GearsArgs.DELETE, libraryName); } + [Obsolete] public static SerializedCommand TFunctionList(bool withCode = false, int verbose = 0, string? libraryName = null) { var args = new List() { GearsArgs.LIST }; @@ -55,6 +59,7 @@ public static SerializedCommand TFunctionList(bool withCode = false, int verbose return new SerializedCommand(RG.TFUNCTION, args); } + [Obsolete] public static SerializedCommand TFCall(string libraryName, string functionName, string[]? keys = null, string[]? args = null, bool async = false) { string command = async ? RG.TFCALLASYNC : RG.TFCALL; diff --git a/src/NRedisStack/Gears/GearsCommands.cs b/src/NRedisStack/Gears/GearsCommands.cs index b82a1eb2..4787fa86 100644 --- a/src/NRedisStack/Gears/GearsCommands.cs +++ b/src/NRedisStack/Gears/GearsCommands.cs @@ -1,7 +1,7 @@ using StackExchange.Redis; namespace NRedisStack { - + [Obsolete] public static class GearsCommands //: GearsCommandsAsync, IGearsCommands { @@ -17,6 +17,7 @@ public static class GearsCommands //: GearsCommandsAsync, IGearsCommands /// an optional argument, instructs RedisGears to replace the function if its already exists. /// if everything was done correctly, Error otherwise. /// //TODO: check this link when it's available + [Obsolete] public static bool TFunctionLoad(this IDatabase db, string libraryCode, bool replace = false, string? config = null) { return db.Execute(GearsCommandBuilder.TFunctionLoad(libraryCode, replace, config)).OKtoBoolean(); @@ -28,6 +29,7 @@ public static bool TFunctionLoad(this IDatabase db, string libraryCode, bool rep /// the name of the library to delete. /// if the library was deleted successfully, Error otherwise. /// //TODO: check this link when it's available + [Obsolete] public static bool TFunctionDelete(this IDatabase db, string libraryName) { return db.Execute(GearsCommandBuilder.TFunctionDelete(libraryName)).OKtoBoolean(); @@ -42,6 +44,7 @@ public static bool TFunctionDelete(this IDatabase db, string libraryName) /// multiple times to show multiple libraries in a single command) /// Information about the requested libraries. /// //TODO: check this link when it's available + [Obsolete] public static Dictionary[] TFunctionList(this IDatabase db, bool withCode = false, int verbose = 0, string? libraryName = null) { return db.Execute(GearsCommandBuilder.TFunctionList(withCode, verbose, libraryName)).ToDictionarys(); @@ -56,6 +59,7 @@ public static Dictionary[] TFunctionList(this IDatabase db, /// Additional argument to pass to the function. /// The return value from the sync & async function on error in case of failure. /// + [Obsolete] public static RedisResult TFCall_(this IDatabase db, string libraryName, string functionName, string[]? keys = null, string[]? args = null) { return db.Execute(GearsCommandBuilder.TFCall(libraryName, functionName, keys, args, async: false)); @@ -70,6 +74,7 @@ public static RedisResult TFCall_(this IDatabase db, string libraryName, string /// Additional argument to pass to the function. /// The return value from the sync & async function on error in case of failure. /// + [Obsolete] public static RedisResult TFCallAsync_(this IDatabase db, string libraryName, string functionName, string[]? keys = null, string[]? args = null) { return db.Execute(GearsCommandBuilder.TFCall(libraryName, functionName, keys, args, async: true)); diff --git a/src/NRedisStack/Gears/GearsCommandsAsync.cs b/src/NRedisStack/Gears/GearsCommandsAsync.cs index 076d51bc..3963dfca 100644 --- a/src/NRedisStack/Gears/GearsCommandsAsync.cs +++ b/src/NRedisStack/Gears/GearsCommandsAsync.cs @@ -16,6 +16,7 @@ public static class GearsCommandsAsync //: IGearsCommandsAsync /// an optional argument, instructs RedisGears to replace the function if its already exists. /// if everything was done correctly, Error otherwise. /// //TODO: add link to the command when it's available + [Obsolete] public static async Task TFunctionLoadAsync(this IDatabase db, string libraryCode, string? config = null, bool replace = false) { return (await db.ExecuteAsync(GearsCommandBuilder.TFunctionLoad(libraryCode, replace, config))).OKtoBoolean(); @@ -27,6 +28,7 @@ public static async Task TFunctionLoadAsync(this IDatabase db, string libr /// the name of the library to delete. /// if the library was deleted successfully, Error otherwise. /// //TODO: add link to the command when it's available + [Obsolete] public static async Task TFunctionDeleteAsync(this IDatabase db, string libraryName) { return (await db.ExecuteAsync(GearsCommandBuilder.TFunctionDelete(libraryName))).OKtoBoolean(); @@ -41,6 +43,7 @@ public static async Task TFunctionDeleteAsync(this IDatabase db, string li /// multiple times to show multiple libraries in a single command) /// Information about the requested libraries. /// //TODO: add link to the command when it's available + [Obsolete] public static async Task[]> TFunctionListAsync(this IDatabase db, bool withCode = false, int verbose = 0, string? libraryName = null) { return (await db.ExecuteAsync(GearsCommandBuilder.TFunctionList(withCode, verbose, libraryName))).ToDictionarys(); @@ -55,6 +58,7 @@ public static async Task[]> TFunctionListAsync(t /// Additional argument to pass to the function. /// The return value from the sync & async function on error in case of failure. /// + [Obsolete] public async static Task TFCall_Async(this IDatabase db, string libraryName, string functionName, string[]? keys = null, string[]? args = null) { return await db.ExecuteAsync(GearsCommandBuilder.TFCall(libraryName, functionName, keys, args, async: false)); @@ -69,6 +73,7 @@ public async static Task TFCall_Async(this IDatabase db, string lib /// Additional argument to pass to the function. /// The return value from the sync & async function on error in case of failure. /// + [Obsolete] public async static Task TFCallAsync_Async(this IDatabase db, string libraryName, string functionName, string[]? keys = null, string[]? args = null) { return await db.ExecuteAsync(GearsCommandBuilder.TFCall(libraryName, functionName, keys, args, async: true)); diff --git a/tests/NRedisStack.Tests/Gears/GearsTests.cs b/tests/NRedisStack.Tests/Gears/GearsTests.cs deleted file mode 100644 index 02a7c474..00000000 --- a/tests/NRedisStack.Tests/Gears/GearsTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -using Xunit; -using StackExchange.Redis; - -namespace NRedisStack.Tests.Gears; - -public class GearsTests : AbstractNRedisStackTest, IDisposable -{ - // private readonly string key = "GEARS_TESTS"; - public GearsTests(RedisFixture redisFixture) : base(redisFixture) { } - - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.1.242")] - public void TestTFunctionLoadDelete() - { - IDatabase db = redisFixture.Redis.GetDatabase(); - if (!redisFixture.isEnterprise) - db.ExecuteBroadcast("REDISGEARS_2.REFRESHCLUSTER"); - db.Execute("FLUSHALL"); - Assert.True(db.TFunctionLoad(GenerateLibCode("lib"))); - Assert.True(db.TFunctionDelete("lib")); - } - - - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.1.242")] - public async Task TestTFunctionLoadDeleteAsync() - { - IDatabase db = redisFixture.Redis.GetDatabase(); - if (!redisFixture.isEnterprise) - db.ExecuteBroadcast("REDISGEARS_2.REFRESHCLUSTER"); - db.Execute("FLUSHALL"); - TryDeleteLib(db, "lib", "lib1", "lib2", "lib3"); - - Assert.True(await db.TFunctionLoadAsync(GenerateLibCode("lib"))); - Assert.True(await db.TFunctionDeleteAsync("lib")); - } - - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.1.242")] - public void TestTFunctionList() - { - IDatabase db = redisFixture.Redis.GetDatabase(); - if (!redisFixture.isEnterprise) - db.ExecuteBroadcast("REDISGEARS_2.REFRESHCLUSTER"); - db.Execute("FLUSHALL"); - TryDeleteLib(db, "lib", "lib1", "lib2", "lib3"); - - Assert.True(db.TFunctionLoad(GenerateLibCode("lib1"))); - Assert.True(db.TFunctionLoad(GenerateLibCode("lib2"))); - Assert.True(db.TFunctionLoad(GenerateLibCode("lib3"))); - - // test error throwing: - Assert.Throws(() => db.TFunctionList(verbose: 8)); - var functions = db.TFunctionList(verbose: 1); - Assert.Equal(3, functions.Length); - - HashSet expectedNames = new HashSet { "lib1", "lib2", "lib3" }; - HashSet actualNames = new HashSet{ - functions[0]["name"].ToString()!, - functions[1]["name"].ToString()!, - functions[2]["name"].ToString()! - }; - - Assert.Equal(expectedNames, actualNames); - - - Assert.True(db.TFunctionDelete("lib1")); - Assert.True(db.TFunctionDelete("lib2")); - Assert.True(db.TFunctionDelete("lib3")); - } - - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.1.242")] - public async Task TestTFunctionListAsync() - { - IDatabase db = redisFixture.Redis.GetDatabase(); - if (!redisFixture.isEnterprise) - db.ExecuteBroadcast("REDISGEARS_2.REFRESHCLUSTER"); - db.Execute("FLUSHALL"); - TryDeleteLib(db, "lib", "lib1", "lib2", "lib3"); - - Assert.True(await db.TFunctionLoadAsync(GenerateLibCode("lib1"))); - Assert.True(await db.TFunctionLoadAsync(GenerateLibCode("lib2"))); - Assert.True(await db.TFunctionLoadAsync(GenerateLibCode("lib3"))); - - var functions = await db.TFunctionListAsync(verbose: 1); - Assert.Equal(3, functions.Length); - - HashSet expectedNames = new HashSet { "lib1", "lib2", "lib3" }; - HashSet actualNames = new HashSet{ - functions[0]["name"].ToString()!, - functions[1]["name"].ToString()!, - functions[2]["name"].ToString()! - }; - - Assert.Equal(expectedNames, actualNames); - - - Assert.True(await db.TFunctionDeleteAsync("lib1")); - Assert.True(await db.TFunctionDeleteAsync("lib2")); - Assert.True(await db.TFunctionDeleteAsync("lib3")); - } - - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.1.242")] - public void TestTFCall() - { - IDatabase db = redisFixture.Redis.GetDatabase(); - if (!redisFixture.isEnterprise) - db.ExecuteBroadcast("REDISGEARS_2.REFRESHCLUSTER"); - db.Execute("FLUSHALL"); - TryDeleteLib(db, "lib", "lib1", "lib2", "lib3"); - - Assert.True(db.TFunctionLoad(GenerateLibCode("lib"))); - Assert.Equal("bar", db.TFCall_("lib", "foo").ToString()); - Assert.Equal("bar", db.TFCallAsync_("lib", "foo").ToString()); - - Assert.True(db.TFunctionDelete("lib")); - } - - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.1.242")] - public async Task TestTFCallAsync() - { - IDatabase db = redisFixture.Redis.GetDatabase(); - if (!redisFixture.isEnterprise) - db.ExecuteBroadcast("REDISGEARS_2.REFRESHCLUSTER"); - db.Execute("FLUSHALL"); - TryDeleteLib(db, "lib", "lib1", "lib2", "lib3"); - - Assert.True(await db.TFunctionLoadAsync(GenerateLibCode("lib"))); - Assert.Equal("bar", (await db.TFCall_Async("lib", "foo")).ToString()); - Assert.Equal("bar", (await db.TFCallAsync_Async("lib", "foo")).ToString()); - - Assert.True(await db.TFunctionDeleteAsync("lib")); - } - - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.1.242")] - public void TestGearsCommandBuilder() - { - // TFunctionLoad: - var buildCommand = GearsCommandBuilder - .TFunctionLoad(GenerateLibCode("lib"), - true, "config"); - var expected = new List - { - "LOAD", - "REPLACE", - "CONFIG", - "config", - GenerateLibCode("lib") - }; - Assert.Equal("TFUNCTION", buildCommand.Command); - Assert.Equal(expected, buildCommand.Args); - - // TFunctionDelete: - buildCommand = GearsCommandBuilder.TFunctionDelete("lib"); - expected = new List - { - "DELETE", - "lib" - }; - Assert.Equal("TFUNCTION", buildCommand.Command); - Assert.Equal(expected, buildCommand.Args); - - // TFunctionList: - buildCommand = GearsCommandBuilder.TFunctionList(true, 2, "lib"); - expected = new List - { - "LIST", - "WITHCODE", - "vv", - "LIBRARY", - "lib", - }; - Assert.Equal("TFUNCTION", buildCommand.Command); - Assert.Equal(expected, buildCommand.Args); - - // TFCall: - var buildSync = GearsCommandBuilder.TFCall("libName", "funcName", new string[] { "key1", "key2" }, new string[] { "arg1", "arg2" }, false); - var buildAsync = GearsCommandBuilder.TFCall("libName", "funcName", new string[] { "key1", "key2" }, new string[] { "arg1", "arg2" }, true); - - expected = new List - { - "libName.funcName", - 2, - "key1", - "key2", - "arg1", - "arg2" - }; - - Assert.Equal("TFCALL", buildSync.Command); - Assert.Equal(expected, buildSync.Args); - - Assert.Equal("TFCALLASYNC", buildAsync.Command); - Assert.Equal(expected, buildAsync.Args); - } - - private static void TryDeleteLib(IDatabase db, params string[] libNames) - { - foreach (var libName in libNames) - { - try - { - db.ExecuteBroadcast(GearsCommandBuilder.TFunctionDelete(libName)); - } - catch (RedisServerException) { } // ignore - } - } - - private static string GenerateLibCode(string libName) - { - return $"#!js api_version=1.0 name={libName}\n redis.registerFunction('foo', ()=>{{return 'bar'}})"; - } -} From 1f08fad70a6418250b9fda3de4a6423a972a0bd9 Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Mon, 7 Oct 2024 08:55:57 +0200 Subject: [PATCH 11/22] Load Redis test endpoints from config file or env vars (#294) * Load Redis test endpoints from config file or env vars * Fix formatting * add IsTargetConnectionExist to help skipping some tests --------- Co-authored-by: atakavci <58048133+atakavci@users.noreply.github.com> Co-authored-by: atakavci --- .../Core Commands/CoreTests.cs | 4 +- tests/NRedisStack.Tests/RedisFixture.cs | 115 +++++++++++++----- 2 files changed, 87 insertions(+), 32 deletions(-) diff --git a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs index 45856478..cf330786 100644 --- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -231,7 +231,7 @@ public void TestBZMPopMultiplexerTimeout() var configurationOptions = new ConfigurationOptions(); configurationOptions.SyncTimeout = 1000; - using var redis = redisFixture.CustomRedis(configurationOptions, out _); + using var redis = redisFixture.GetConnectionById(configurationOptions, "standalone"); var db = redis.GetDatabase(null); db.Execute("FLUSHALL"); @@ -246,7 +246,7 @@ public async Task TestBZMPopMultiplexerTimeoutAsync() var configurationOptions = new ConfigurationOptions(); configurationOptions.SyncTimeout = 1000; - await using var redis = redisFixture.CustomRedis(configurationOptions, out _); + await using var redis = redisFixture.GetConnectionById(configurationOptions, "standalone"); var db = redis.GetDatabase(null); db.Execute("FLUSHALL"); diff --git a/tests/NRedisStack.Tests/RedisFixture.cs b/tests/NRedisStack.Tests/RedisFixture.cs index 81aca9ab..0a145e85 100644 --- a/tests/NRedisStack.Tests/RedisFixture.cs +++ b/tests/NRedisStack.Tests/RedisFixture.cs @@ -1,13 +1,54 @@ using StackExchange.Redis; +using System.Text.Json; namespace NRedisStack.Tests; +public class EndpointConfig +{ + public List? endpoints { get; set; } + + public bool tls { get; set; } + + public string? password { get; set; } + + public int? bdb_id { get; set; } + + public object? raw_endpoints { get; set; } + + public ConnectionMultiplexer CreateConnection(ConfigurationOptions configurationOptions) + { + configurationOptions.EndPoints.Clear(); + + foreach (var endpoint in endpoints!) + { + configurationOptions.EndPoints.Add(endpoint); + } + + if (password != null) + { + configurationOptions.Password = password; + } + + // TODO(imalinovskiy): Add support for TLS + // TODO(imalinovskiy): Add support for Discovery/Sentinel API + + return ConnectionMultiplexer.Connect(configurationOptions); + } +} + + public class RedisFixture : IDisposable { // Set the environment variable to specify your own alternate host and port: private readonly string redisStandalone = Environment.GetEnvironmentVariable("REDIS") ?? "localhost:6379"; private readonly string? redisCluster = Environment.GetEnvironmentVariable("REDIS_CLUSTER"); private readonly string? numRedisClusterNodesEnv = Environment.GetEnvironmentVariable("NUM_REDIS_CLUSTER_NODES"); + + private readonly string defaultEndpointId = Environment.GetEnvironmentVariable("REDIS_DEFAULT_ENDPOINT_ID") ?? "standalone"; + private readonly string? redisEndpointsPath = Environment.GetEnvironmentVariable("REDIS_ENDPOINTS_CONFIG_PATH"); + private Dictionary redisEndpoints = new(); + + public bool isEnterprise = Environment.GetEnvironmentVariable("IS_ENTERPRISE") == "true"; public bool isOSSCluster; @@ -18,7 +59,42 @@ public RedisFixture() AsyncTimeout = 10000, SyncTimeout = 10000 }; - Redis = Connect(clusterConfig, out isOSSCluster); + + if (redisEndpointsPath != null && File.Exists(redisEndpointsPath)) + { + string json = File.ReadAllText(redisEndpointsPath); + var parsedEndpoints = JsonSerializer.Deserialize>(json); + + redisEndpoints = parsedEndpoints ?? throw new Exception("Failed to parse the Redis endpoints configuration."); + } + else + { + redisEndpoints.Add("standalone", + new EndpointConfig { endpoints = new List { redisStandalone } }); + + if (redisCluster != null) + { + string[] parts = redisCluster!.Split(':'); + string host = parts[0]; + int startPort = int.Parse(parts[1]); + + var endpoints = new List(); + int numRedisClusterNodes = int.Parse(numRedisClusterNodesEnv!); + for (int i = 0; i < numRedisClusterNodes; i++) + { + endpoints.Add($"{host}:{startPort + i}"); + } + + redisEndpoints.Add("cluster", + new EndpointConfig { endpoints = endpoints }); + + // Set the default endpoint to the cluster to keep the tests consistent + defaultEndpointId = "cluster"; + isOSSCluster = true; + } + } + + Redis = GetConnectionById(clusterConfig, defaultEndpointId); } public void Dispose() @@ -28,39 +104,18 @@ public void Dispose() public ConnectionMultiplexer Redis { get; } - public ConnectionMultiplexer CustomRedis(ConfigurationOptions configurationOptions, out bool isOssCluster) + public ConnectionMultiplexer GetConnectionById(ConfigurationOptions configurationOptions, string id) { - return Connect(configurationOptions, out isOssCluster); - } - - private ConnectionMultiplexer Connect(ConfigurationOptions configurationOptions, out bool isOssCluster) - { - // Redis Cluster - if (redisCluster != null && numRedisClusterNodesEnv != null) + if (!redisEndpoints.ContainsKey(id)) { - // Split to host and port - string[] parts = redisCluster!.Split(':'); - string host = parts[0]; - int startPort = int.Parse(parts[1]); - - var endpoints = new EndPointCollection(); // TODO: check if needed - - configurationOptions.EndPoints.Clear(); - int numRedisClusterNodes = int.Parse(numRedisClusterNodesEnv!); - for (int i = 0; i < numRedisClusterNodes; i++) - { - configurationOptions.EndPoints.Add(host, startPort + i); - } - - isOssCluster = true; - return ConnectionMultiplexer.Connect(configurationOptions); + throw new Exception($"The connection with id '{id}' is not configured."); } - // Redis Standalone - configurationOptions.EndPoints.Clear(); - configurationOptions.EndPoints.Add($"{redisStandalone}"); + return redisEndpoints[id].CreateConnection(configurationOptions); + } - isOssCluster = false; - return ConnectionMultiplexer.Connect(configurationOptions); + public bool IsTargetConnectionExist(string id) + { + return redisEndpoints.ContainsKey(id); } } \ No newline at end of file From 73e7a6daefed30a2e958f692a5c44dd7d73f7f22 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:02:46 +0100 Subject: [PATCH 12/22] DOC-4345 added testable version of home page JSON example (#345) * DOC-4345 added testable version of home page JSON example * DOC-4345 added try...catch around dropIndex call * DOC-4345 sorted results before assert checks * DOC-4345 removed test library import --- tests/Doc/HomeJsonExample.cs | 177 +++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/Doc/HomeJsonExample.cs diff --git a/tests/Doc/HomeJsonExample.cs b/tests/Doc/HomeJsonExample.cs new file mode 100644 index 00000000..b404469d --- /dev/null +++ b/tests/Doc/HomeJsonExample.cs @@ -0,0 +1,177 @@ +// EXAMPLE: cs_home_json + +// STEP_START import +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Aggregation; +using NRedisStack.Search.Literals.Enums; +using StackExchange.Redis; +// STEP_END + +// REMOVE_START +using NRedisStack.Tests; + +namespace Doc; +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class HomeJsonExample +{ + + [SkipIfRedis(Is.OSSCluster)] + public void run() + { + // STEP_START connect + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + // STEP_END + + //REMOVE_START + // Clear any keys here before using them in tests. + db.KeyDelete(new RedisKey[] { "user:1", "user:2", "user:3" }); + try { db.FT().DropIndex("idx:users"); } catch { } + //REMOVE_END + // HIDE_END + + // STEP_START create_data + var user1 = new + { + name = "Paul John", + email = "paul.john@example.com", + age = 42, + city = "London" + }; + + var user2 = new + { + name = "Eden Zamir", + email = "eden.zamir@example.com", + age = 29, + city = "Tel Aviv" + }; + + var user3 = new + { + name = "Paul Zamir", + email = "paul.zamir@example.com", + age = 35, + city = "Tel Aviv" + }; + // STEP_END + + // STEP_START make_index + var schema = new Schema() + .AddTextField(new FieldName("$.name", "name")) + .AddTagField(new FieldName("$.city", "city")) + .AddNumericField(new FieldName("$.age", "age")); + + bool indexCreated = db.FT().Create( + "idx:users", + new FTCreateParams() + .On(IndexDataType.JSON) + .Prefix("user:"), + schema + ); + // STEP_END + + // Tests for 'make_index' step. + // REMOVE_START + Assert.True(indexCreated); + // REMOVE_END + + + // STEP_START add_data + bool user1Set = db.JSON().Set("user:1", "$", user1); + bool user2Set = db.JSON().Set("user:2", "$", user2); + bool user3Set = db.JSON().Set("user:3", "$", user3); + // STEP_END + + // Tests for 'add_data' step. + // REMOVE_START + Assert.True(user1Set); + Assert.True(user2Set); + Assert.True(user3Set); + // REMOVE_END + + + // STEP_START query1 + SearchResult findPaulResult = db.FT().Search( + "idx:users", + new Query("Paul @age:[30 40]") + ); + Console.WriteLine(string.Join( + ", ", + findPaulResult.Documents.Select(x => x["json"]) + )); + // >>> {"name":"Paul Zamir","email":"paul.zamir@example.com", ... + // STEP_END + + // Tests for 'query1' step. + // REMOVE_START + Assert.Equal( + "{\"name\":\"Paul Zamir\",\"email\":\"paul.zamir@example.com\",\"age\":35,\"city\":\"Tel Aviv\"}", + string.Join(", ", findPaulResult.Documents.Select(x => x["json"])) + ); + // REMOVE_END + + + // STEP_START query2 + var citiesResult = db.FT().Search( + "idx:users", + new Query("Paul") + .ReturnFields(new FieldName("$.city", "city")) + ); + Console.WriteLine(string.Join( + ", ", + citiesResult.Documents.Select(x => x["city"]).OrderBy(x => x) + )); + // >>> London, Tel Aviv + // STEP_END + + // Tests for 'query2' step. + // REMOVE_START + Assert.Equal( + "London, Tel Aviv", + string.Join(", ", citiesResult.Documents.Select(x => x["city"]).OrderBy(x => x)) + ); + // REMOVE_END + + + // STEP_START query3 + AggregationRequest aggRequest = new AggregationRequest("*") + .GroupBy("@city", Reducers.Count().As("count")); + + AggregationResult aggResult = db.FT().Aggregate("idx:users", aggRequest); + IReadOnlyList> resultsList = + aggResult.GetResults(); + + for (var i = 0; i < resultsList.Count; i++) + { + Dictionary item = resultsList.ElementAt(i); + Console.WriteLine($"{item["city"]} - {item["count"]}"); + } + // >>> London - 1 + // >>> Tel Aviv - 2 + // STEP_END + + // Tests for 'query3' step. + // REMOVE_START + Assert.Equal(2, resultsList.Count); + + var sortedResults = resultsList.OrderBy(x => x["city"]); + Dictionary testItem = sortedResults.ElementAt(0); + Assert.Equal("London", testItem["city"]); + Assert.Equal(1, testItem["count"]); + + testItem = sortedResults.ElementAt(1); + Assert.Equal("Tel Aviv", testItem["city"]); + Assert.Equal(2, testItem["count"]); + // REMOVE_END + + + // HIDE_START + } +} +// HIDE_END + From dd11910e54564daeec643233a97714c122947bc0 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:42:57 +0300 Subject: [PATCH 13/22] Fix for 'toList' reducer results empty (#346) * issue #342- fix for 'toList' reducer results empty * fix collection initializer --- src/NRedisStack/Search/AggregationResult.cs | 53 ++++++++++++++++--- src/NRedisStack/Search/Row.cs | 11 ++-- tests/NRedisStack.Tests/Search/SearchTests.cs | 23 +++++--- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/NRedisStack/Search/AggregationResult.cs b/src/NRedisStack/Search/AggregationResult.cs index f3410c29..22f861a6 100644 --- a/src/NRedisStack/Search/AggregationResult.cs +++ b/src/NRedisStack/Search/AggregationResult.cs @@ -6,7 +6,9 @@ namespace NRedisStack.Search; public sealed class AggregationResult { public long TotalResults { get; } - private readonly Dictionary[] _results; + private readonly Dictionary[] _results; + private Dictionary[] _resultsAsRedisValues; + public long CursorId { get; } @@ -18,18 +20,23 @@ internal AggregationResult(RedisResult result, long cursorId = -1) // // the first element is always the number of results // TotalResults = (long)arr[0]; - _results = new Dictionary[arr.Length - 1]; + _results = new Dictionary[arr.Length - 1]; for (int i = 1; i < arr.Length; i++) { var raw = (RedisResult[])arr[i]!; - var cur = new Dictionary(); + var cur = new Dictionary(); for (int j = 0; j < raw.Length;) { var key = (string)raw[j++]!; var val = raw[j++]; if (val.Type == ResultType.MultiBulk) - continue; // TODO: handle multi-bulk (maybe change to object?) - cur.Add(key, (RedisValue)val); + { + cur.Add(key, ConvertMultiBulkToObject((RedisResult[])val!)); + } + else + { + cur.Add(key, (RedisValue)val); + } } _results[i - 1] = cur; @@ -52,17 +59,47 @@ private object ConvertMultiBulkToObject(IEnumerable multiBulkArray) { return multiBulkArray.Select(item => item.Type == ResultType.MultiBulk ? ConvertMultiBulkToObject((RedisResult[])item!) - : item) + : (RedisValue)item) .ToList(); } - public IReadOnlyList> GetResults() => _results; + /// + /// Gets the results as a read-only list of dictionaries with string keys and RedisValue values. + /// + /// + /// This method is deprecated and will be removed in future versions. + /// Please use instead. + /// + [Obsolete("This method is deprecated and will be removed in future versions. Please use 'GetRow' instead.")] + public IReadOnlyList> GetResults() + { + return getResultsAsRedisValues(); + } + /// + /// Gets the aggregation result at the specified index. + /// + /// The zero-based index of the aggregation result to retrieve. + /// + /// A dictionary containing the aggregation result as Redis values if the index is within bounds; + /// otherwise, null. + /// + [Obsolete("This method is deprecated and will be removed in future versions. Please use 'GetRow' instead.")] public Dictionary? this[int index] - => index >= _results.Length ? null : _results[index]; + => index >= getResultsAsRedisValues().Length ? null : getResultsAsRedisValues()[index]; public Row GetRow(int index) { return index >= _results.Length ? default : new Row(_results[index]); } + + private Dictionary[] getResultsAsRedisValues() + { + if (_resultsAsRedisValues == null) + _resultsAsRedisValues = _results.Select(dict => dict.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value is RedisValue value ? value : RedisValue.Null + )).ToArray(); + return _resultsAsRedisValues; + } } \ No newline at end of file diff --git a/src/NRedisStack/Search/Row.cs b/src/NRedisStack/Search/Row.cs index 0caac94e..07153f85 100644 --- a/src/NRedisStack/Search/Row.cs +++ b/src/NRedisStack/Search/Row.cs @@ -4,18 +4,19 @@ namespace NRedisStack.Search.Aggregation { public readonly struct Row { - private readonly Dictionary _fields; + private readonly Dictionary _fields; - internal Row(Dictionary fields) + internal Row(Dictionary fields) { _fields = fields; } public bool ContainsKey(string key) => _fields.ContainsKey(key); - public RedisValue this[string key] => _fields.TryGetValue(key, out var result) ? result : RedisValue.Null; + public RedisValue this[string key] => _fields.TryGetValue(key, out var result) ? (result is RedisValue ? (RedisValue)result : RedisValue.Null) : RedisValue.Null; + public object Get(string key) => _fields.TryGetValue(key, out var result) ? result : RedisValue.Null; 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)result : default; - public double GetDouble(string key) => _fields.TryGetValue(key, out var result) ? (double)result : 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; } } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Search/SearchTests.cs b/tests/NRedisStack.Tests/Search/SearchTests.cs index b39bcb09..2ed10466 100644 --- a/tests/NRedisStack.Tests/Search/SearchTests.cs +++ b/tests/NRedisStack.Tests/Search/SearchTests.cs @@ -1256,7 +1256,9 @@ public void TestAggregationGroupBy() res = rawRes.GetRow(0); Assert.Equal("redis", res["parent"]); // TODO: complete this assert after handling multi bulk reply - //Assert.Equal((RedisValue[])res["__generated_aliastolisttitle"], { "RediSearch", "RedisAI", "RedisJson"}); + var expected = new List { "RediSearch", "RedisAI", "RedisJson" }; + var actual = (List)res.Get("__generated_aliastolisttitle"); + Assert.True(!expected.Except(actual).Any() && expected.Count == actual.Count); req = new AggregationRequest("redis").GroupBy( "@parent", Reducers.FirstValue("@title").As("first")); @@ -1269,11 +1271,20 @@ public void TestAggregationGroupBy() res = ft.Aggregate("idx", req).GetRow(0); Assert.Equal("redis", res["parent"]); // TODO: complete this assert after handling multi bulk reply - // Assert.Equal(res[2], "random"); - // Assert.Equal(len(res[3]), 2); - // Assert.Equal(res[3][0] in ["RediSearch", "RedisAI", "RedisJson"]); - // req = new AggregationRequest("redis").GroupBy("@parent", redu - + actual = (List)res.Get("random"); + Assert.Equal(2, actual.Count); + List possibleValues = new List() { "RediSearch", "RedisAI", "RedisJson" }; + Assert.Contains(actual[0].ToString(), possibleValues); + Assert.Contains(actual[1].ToString(), possibleValues); + + req = new AggregationRequest("redis") + .Load(new FieldName("__key")) + .GroupBy("@parent", Reducers.ToList("__key").As("docs")); + + res = db.FT().Aggregate("idx", req).GetRow(0); + actual = (List)res.Get("docs"); + expected = new List { "ai", "search", "json" }; + Assert.True(!expected.Except(actual).Any() && expected.Count == actual.Count); } From aed3c93fd8ff612940247c05e6e5cb676afdc0c0 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:28:58 +0300 Subject: [PATCH 14/22] Support reducers in RedisTimeSeries 1.8 GA (#348) Add new reducers in RedisTimeSeries 1.8 GA --- .../TimeSeries/Extensions/ReduceExtensions.cs | 7 + .../TimeSeries/Literals/Enums/Reduce.cs | 35 ++++ .../TimeSeries/TestAPI/TestMRange.cs | 177 +++++++++++++++++- 3 files changed, 218 insertions(+), 1 deletion(-) diff --git a/src/NRedisStack/TimeSeries/Extensions/ReduceExtensions.cs b/src/NRedisStack/TimeSeries/Extensions/ReduceExtensions.cs index c5db06b4..204d64a3 100644 --- a/src/NRedisStack/TimeSeries/Extensions/ReduceExtensions.cs +++ b/src/NRedisStack/TimeSeries/Extensions/ReduceExtensions.cs @@ -9,6 +9,13 @@ internal static class ReduceExtensions TsReduce.Sum => "SUM", TsReduce.Min => "MIN", TsReduce.Max => "MAX", + TsReduce.Avg => "AVG", + TsReduce.Range => "RANGE", + TsReduce.Count => "COUNT", + TsReduce.StdP => "STD.P", + TsReduce.StdS => "STD.S", + TsReduce.VarP => "VAR.P", + TsReduce.VarS => "VAR.S", _ => throw new ArgumentOutOfRangeException(nameof(reduce), "Invalid Reduce type"), }; } diff --git a/src/NRedisStack/TimeSeries/Literals/Enums/Reduce.cs b/src/NRedisStack/TimeSeries/Literals/Enums/Reduce.cs index ffe020ee..da3f94ac 100644 --- a/src/NRedisStack/TimeSeries/Literals/Enums/Reduce.cs +++ b/src/NRedisStack/TimeSeries/Literals/Enums/Reduce.cs @@ -19,5 +19,40 @@ public enum TsReduce /// A maximum sample of all samples in the group /// Max, + + /// + /// Arithmetic mean of all non-NaN values (since RedisTimeSeries v1.8) + /// + Avg, + + /// + /// Difference between maximum non-NaN value and minimum non-NaN value (since RedisTimeSeries v1.8) + /// + Range, + + /// + /// Number of non-NaN values (since RedisTimeSeries v1.8) + /// + Count, + + /// + /// Population standard deviation of all non-NaN values (since RedisTimeSeries v1.8) + /// + StdP, + + /// + /// Sample standard deviation of all non-NaN values (since RedisTimeSeries v1.8) + /// + StdS, + + /// + /// Population variance of all non-NaN values (since RedisTimeSeries v1.8) + /// + VarP, + + /// + /// Sample variance of all non-NaN values (since RedisTimeSeries v1.8) + /// + VarS } } diff --git a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs index 1c12a4be..cd95109a 100644 --- a/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs +++ b/tests/NRedisStack.Tests/TimeSeries/TestAPI/TestMRange.cs @@ -257,7 +257,7 @@ public void TestMRangeGroupby() } [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] - public void TestMRangeReduce() + public void TestMRangeReduceSum() { IDatabase db = redisFixture.Redis.GetDatabase(); db.Execute("FLUSHALL"); @@ -281,6 +281,181 @@ public void TestMRangeReduce() } } + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] + public void TestMRangeReduceAvg() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ts = db.TS(); + foreach (var key in _keys) + { + var label = new TimeSeriesLabel("key", "MRangeReduce"); + ts.Create(key, labels: new List { label }); + } + + var tuples = CreateData(ts, 50); + var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Avg)); + Assert.Equal(1, results.Count); + Assert.Equal("key=MRangeReduce", results[0].key); + Assert.Equal(new TimeSeriesLabel("key", "MRangeReduce"), results[0].labels[0]); + Assert.Equal(new TimeSeriesLabel("__reducer__", "avg"), results[0].labels[1]); + Assert.Equal(new TimeSeriesLabel("__source__", string.Join(",", _keys)), results[0].labels[2]); + for (int i = 0; i < results[0].values.Count; i++) + { + Assert.Equal(tuples[i].Val, results[0].values[i].Val); + } + } + + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] + public void TestMRangeReduceRange() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ts = db.TS(); + foreach (var key in _keys) + { + var label = new TimeSeriesLabel("key", "MRangeReduce"); + ts.Create(key, labels: new List { label }); + } + + var tuples = CreateData(ts, 50); + var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Range)); + Assert.Equal(1, results.Count); + Assert.Equal("key=MRangeReduce", results[0].key); + Assert.Equal(new TimeSeriesLabel("key", "MRangeReduce"), results[0].labels[0]); + Assert.Equal(new TimeSeriesLabel("__reducer__", "range"), results[0].labels[1]); + Assert.Equal(new TimeSeriesLabel("__source__", string.Join(",", _keys)), results[0].labels[2]); + for (int i = 0; i < results[0].values.Count; i++) + { + Assert.Equal(0, results[0].values[i].Val); + } + } + + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] + public void TestMRangeReduceCount() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ts = db.TS(); + foreach (var key in _keys) + { + var label = new TimeSeriesLabel("key", "MRangeReduce"); + ts.Create(key, labels: new List { label }); + } + + var tuples = CreateData(ts, 50); + var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.Count)); + Assert.Equal(1, results.Count); + Assert.Equal("key=MRangeReduce", results[0].key); + Assert.Equal(new TimeSeriesLabel("key", "MRangeReduce"), results[0].labels[0]); + Assert.Equal(new TimeSeriesLabel("__reducer__", "count"), results[0].labels[1]); + Assert.Equal(new TimeSeriesLabel("__source__", string.Join(",", _keys)), results[0].labels[2]); + for (int i = 0; i < results[0].values.Count; i++) + { + Assert.Equal(2, results[0].values[i].Val); + } + } + + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] + public void TestMRangeReduceStdP() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ts = db.TS(); + foreach (var key in _keys) + { + var label = new TimeSeriesLabel("key", "MRangeReduce"); + ts.Create(key, labels: new List { label }); + } + + var tuples = CreateData(ts, 50); + var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.StdP)); + Assert.Equal(1, results.Count); + Assert.Equal("key=MRangeReduce", results[0].key); + Assert.Equal(new TimeSeriesLabel("key", "MRangeReduce"), results[0].labels[0]); + Assert.Equal(new TimeSeriesLabel("__reducer__", "std.p"), results[0].labels[1]); + Assert.Equal(new TimeSeriesLabel("__source__", string.Join(",", _keys)), results[0].labels[2]); + for (int i = 0; i < results[0].values.Count; i++) + { + Assert.Equal(0, results[0].values[i].Val); + } + } + + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] + public void TestMRangeReduceStdS() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ts = db.TS(); + foreach (var key in _keys) + { + var label = new TimeSeriesLabel("key", "MRangeReduce"); + ts.Create(key, labels: new List { label }); + } + + var tuples = CreateData(ts, 50); + var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.StdS)); + Assert.Equal(1, results.Count); + Assert.Equal("key=MRangeReduce", results[0].key); + Assert.Equal(new TimeSeriesLabel("key", "MRangeReduce"), results[0].labels[0]); + Assert.Equal(new TimeSeriesLabel("__reducer__", "std.s"), results[0].labels[1]); + Assert.Equal(new TimeSeriesLabel("__source__", string.Join(",", _keys)), results[0].labels[2]); + for (int i = 0; i < results[0].values.Count; i++) + { + Assert.Equal(0, results[0].values[i].Val); + } + } + + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] + public void TestMRangeReduceVarP() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ts = db.TS(); + foreach (var key in _keys) + { + var label = new TimeSeriesLabel("key", "MRangeReduce"); + ts.Create(key, labels: new List { label }); + } + + var tuples = CreateData(ts, 50); + var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.VarP)); + Assert.Equal(1, results.Count); + Assert.Equal("key=MRangeReduce", results[0].key); + Assert.Equal(new TimeSeriesLabel("key", "MRangeReduce"), results[0].labels[0]); + Assert.Equal(new TimeSeriesLabel("__reducer__", "var.p"), results[0].labels[1]); + Assert.Equal(new TimeSeriesLabel("__source__", string.Join(",", _keys)), results[0].labels[2]); + for (int i = 0; i < results[0].values.Count; i++) + { + Assert.Equal(0, results[0].values[i].Val); + } + } + + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] + public void TestMRangeReduceVarS() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ts = db.TS(); + foreach (var key in _keys) + { + var label = new TimeSeriesLabel("key", "MRangeReduce"); + ts.Create(key, labels: new List { label }); + } + + var tuples = CreateData(ts, 50); + var results = ts.MRange("-", "+", new List { "key=MRangeReduce" }, withLabels: true, groupbyTuple: ("key", TsReduce.VarS)); + Assert.Equal(1, results.Count); + Assert.Equal("key=MRangeReduce", results[0].key); + Assert.Equal(new TimeSeriesLabel("key", "MRangeReduce"), results[0].labels[0]); + Assert.Equal(new TimeSeriesLabel("__reducer__", "var.s"), results[0].labels[1]); + Assert.Equal(new TimeSeriesLabel("__source__", string.Join(",", _keys)), results[0].labels[2]); + for (int i = 0; i < results[0].values.Count; i++) + { + Assert.Equal(0, results[0].values[i].Val); + } + } + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] public void TestMRangeFilterBy() { From 15fd7d21a14b195a3588b38fcc3eef439bfca4e8 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:09:18 +0300 Subject: [PATCH 15/22] Bump lib version (#349) bump lib version --- src/NRedisStack/NRedisStack.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NRedisStack/NRedisStack.csproj b/src/NRedisStack/NRedisStack.csproj index 999af7f1..4f8f264a 100644 --- a/src/NRedisStack/NRedisStack.csproj +++ b/src/NRedisStack/NRedisStack.csproj @@ -10,9 +10,9 @@ Redis OSS .Net Client for Redis Stack README.md - 0.13.0 - 0.13.0 - 0.13.0 + 0.13.1 + 0.13.1 + 0.13.1 From bcfacacc311e69934da4519366b23bc97ae460ca Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:35:55 +0300 Subject: [PATCH 16/22] Azure EntraID (Token Based Authentication) integration tests (#344) * TBA integration test * add TargetEnvironmentAttribute and FaultInjectorClient * fix formatting * add namespace for httpclient * add dummy test for client testing * add AttributeUsage to TargetEnvironment * some clean up and test execution optimization * fix formatting --- .../NRedisStack.Tests.csproj | 1 + tests/NRedisStack.Tests/RedisFixture.cs | 16 ++- .../NRedisStack.Tests/SkipIfRedisAttribute.cs | 4 +- .../TargetEnvironmentAttribute.cs | 36 ++++++ .../AuthenticationTests.cs | 53 ++++++++ .../FaultInjectorClient.cs | 113 ++++++++++++++++++ 6 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 tests/NRedisStack.Tests/TargetEnvironmentAttribute.cs create mode 100644 tests/NRedisStack.Tests/TokenBasedAuthentication/AuthenticationTests.cs create mode 100644 tests/NRedisStack.Tests/TokenBasedAuthentication/FaultInjectorClient.cs diff --git a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj index 7aceede5..cb522fcc 100644 --- a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj +++ b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj @@ -31,6 +31,7 @@ + diff --git a/tests/NRedisStack.Tests/RedisFixture.cs b/tests/NRedisStack.Tests/RedisFixture.cs index 0a145e85..9dfdf2eb 100644 --- a/tests/NRedisStack.Tests/RedisFixture.cs +++ b/tests/NRedisStack.Tests/RedisFixture.cs @@ -52,9 +52,12 @@ public class RedisFixture : IDisposable public bool isEnterprise = Environment.GetEnvironmentVariable("IS_ENTERPRISE") == "true"; public bool isOSSCluster; + private ConnectionMultiplexer redis; + private ConfigurationOptions defaultConfig; + public RedisFixture() { - ConfigurationOptions clusterConfig = new ConfigurationOptions + defaultConfig = new ConfigurationOptions { AsyncTimeout = 10000, SyncTimeout = 10000 @@ -93,8 +96,6 @@ public RedisFixture() isOSSCluster = true; } } - - Redis = GetConnectionById(clusterConfig, defaultEndpointId); } public void Dispose() @@ -102,7 +103,14 @@ public void Dispose() Redis.Close(); } - public ConnectionMultiplexer Redis { get; } + public ConnectionMultiplexer Redis + { + get + { + redis = redis ?? GetConnectionById(defaultConfig, defaultEndpointId); + return redis; + } + } public ConnectionMultiplexer GetConnectionById(ConfigurationOptions configurationOptions, string id) { diff --git a/tests/NRedisStack.Tests/SkipIfRedisAttribute.cs b/tests/NRedisStack.Tests/SkipIfRedisAttribute.cs index b62dc17a..eae76c4d 100644 --- a/tests/NRedisStack.Tests/SkipIfRedisAttribute.cs +++ b/tests/NRedisStack.Tests/SkipIfRedisAttribute.cs @@ -21,6 +21,8 @@ public class SkipIfRedisAttribute : FactAttribute private readonly Comparison _comparison; private readonly List _environments = new List(); + private static Version serverVersion = null; + public SkipIfRedisAttribute( Is environment, Comparison comparison = Comparison.LessThan, @@ -95,7 +97,7 @@ public override string? Skip } // Version check (if Is.Standalone/Is.OSSCluster is set then ) - var serverVersion = redisFixture.Redis.GetServer(redisFixture.Redis.GetEndPoints()[0]).Version; + serverVersion = serverVersion ?? redisFixture.Redis.GetServer(redisFixture.Redis.GetEndPoints()[0]).Version; var targetVersion = new Version(_targetVersion); int comparisonResult = serverVersion.CompareTo(targetVersion); diff --git a/tests/NRedisStack.Tests/TargetEnvironmentAttribute.cs b/tests/NRedisStack.Tests/TargetEnvironmentAttribute.cs new file mode 100644 index 00000000..4497aef0 --- /dev/null +++ b/tests/NRedisStack.Tests/TargetEnvironmentAttribute.cs @@ -0,0 +1,36 @@ +using Xunit; + +namespace NRedisStack.Tests; +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class TargetEnvironmentAttribute : SkipIfRedisAttribute +{ + private string targetEnv; + public TargetEnvironmentAttribute(string targetEnv) : base(Comparison.LessThan, "0.0.0") + { + this.targetEnv = targetEnv; + } + + public TargetEnvironmentAttribute(string targetEnv, Is environment, Comparison comparison = Comparison.LessThan, + string targetVersion = "0.0.0") : base(environment, comparison, targetVersion) + { + this.targetEnv = targetEnv; + } + + public TargetEnvironmentAttribute(string targetEnv, Is environment1, Is environment2, Comparison comparison = Comparison.LessThan, + string targetVersion = "0.0.0") : base(environment1, environment2, comparison, targetVersion) + { + this.targetEnv = targetEnv; + } + + public override string? Skip + { + get + { + if (!new RedisFixture().IsTargetConnectionExist(targetEnv)) + { + return "Test skipped, because: target environment not found."; + } + return base.Skip; + } + } +} \ No newline at end of file diff --git a/tests/NRedisStack.Tests/TokenBasedAuthentication/AuthenticationTests.cs b/tests/NRedisStack.Tests/TokenBasedAuthentication/AuthenticationTests.cs new file mode 100644 index 00000000..db8842c2 --- /dev/null +++ b/tests/NRedisStack.Tests/TokenBasedAuthentication/AuthenticationTests.cs @@ -0,0 +1,53 @@ +using Xunit; +using StackExchange.Redis; +using Azure.Identity; +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; + +namespace NRedisStack.Tests.TokenBasedAuthentication +{ + public class AuthenticationTests : AbstractNRedisStackTest + { + static readonly string key = "myKey"; + static readonly string value = "myValue"; + static readonly string index = "myIndex"; + static readonly string field = "myField"; + static readonly string alias = "myAlias"; + public AuthenticationTests(RedisFixture redisFixture) : base(redisFixture) { } + + [TargetEnvironment("standalone-entraid-acl")] + public void TestTokenBasedAuthentication() + { + + var configurationOptions = new ConfigurationOptions().ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential()).Result!; + configurationOptions.Ssl = false; + configurationOptions.AbortOnConnectFail = true; // Fail fast for the purposes of this sample. In production code, this should remain false to retry connections on startup + + ConnectionMultiplexer? connectionMultiplexer = redisFixture.GetConnectionById(configurationOptions, "standalone-entraid-acl"); + + IDatabase db = connectionMultiplexer.GetDatabase(); + + db.KeyDelete(key); + try + { + db.FT().DropIndex(index); + } + catch { } + + db.StringSet(key, value); + string result = db.StringGet(key); + Assert.Equal(value, result); + + var ft = db.FT(); + Schema sc = new Schema().AddTextField(field); + Assert.True(ft.Create(index, FTCreateParams.CreateParams(), sc)); + + db.HashSet(index, new HashEntry[] { new HashEntry(field, value) }); + + Assert.True(ft.AliasAdd(alias, index)); + SearchResult res1 = ft.Search(alias, new Query("*").ReturnFields(field)); + Assert.Equal(1, res1.TotalResults); + Assert.Equal(value, res1.Documents[0][field]); + } + } +} \ No newline at end of file diff --git a/tests/NRedisStack.Tests/TokenBasedAuthentication/FaultInjectorClient.cs b/tests/NRedisStack.Tests/TokenBasedAuthentication/FaultInjectorClient.cs new file mode 100644 index 00000000..cf319cf1 --- /dev/null +++ b/tests/NRedisStack.Tests/TokenBasedAuthentication/FaultInjectorClient.cs @@ -0,0 +1,113 @@ +using System.Text; +using System.Text.Json; +using System.Net.Http; + +public class FaultInjectorClient +{ + private static readonly string BASE_URL; + + static FaultInjectorClient() + { + BASE_URL = Environment.GetEnvironmentVariable("FAULT_INJECTION_API_URL") ?? "http://127.0.0.1:20324"; + } + + public class TriggerActionResponse + { + public string ActionId { get; } + private DateTime? LastRequestTime { get; set; } + private DateTime? CompletedAt { get; set; } + private DateTime? FirstRequestAt { get; set; } + + public TriggerActionResponse(string actionId) + { + ActionId = actionId; + } + + public async Task IsCompletedAsync(TimeSpan checkInterval, TimeSpan delayAfter, TimeSpan timeout) + { + if (CompletedAt.HasValue) + { + return DateTime.UtcNow - CompletedAt.Value >= delayAfter; + } + + if (FirstRequestAt.HasValue && DateTime.UtcNow - FirstRequestAt.Value >= timeout) + { + throw new TimeoutException("Timeout"); + } + + if (!LastRequestTime.HasValue || DateTime.UtcNow - LastRequestTime.Value >= checkInterval) + { + LastRequestTime = DateTime.UtcNow; + + if (!FirstRequestAt.HasValue) + { + FirstRequestAt = LastRequestTime; + } + + using var httpClient = GetHttpClient(); + var request = new HttpRequestMessage(HttpMethod.Get, $"{BASE_URL}/action/{ActionId}"); + + try + { + var response = await httpClient.SendAsync(request); + var result = await response.Content.ReadAsStringAsync(); + + + if (result.Contains("success")) + { + CompletedAt = DateTime.UtcNow; + return DateTime.UtcNow - CompletedAt.Value >= delayAfter; + } + } + catch (HttpRequestException e) + { + throw new Exception("Fault injection proxy error", e); + } + } + return false; + } + } + + private static HttpClient GetHttpClient() + { + var httpClient = new HttpClient + { + Timeout = TimeSpan.FromMilliseconds(5000) + }; + return httpClient; + } + + public async Task TriggerActionAsync(string actionType, Dictionary parameters) + { + var payload = new Dictionary + { + { "type", actionType }, + { "parameters", parameters } + }; + + var jsonString = JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + using var httpClient = GetHttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, $"{BASE_URL}/action") + { + Content = new StringContent(jsonString, Encoding.UTF8, "application/json") + }; + + try + { + var response = await httpClient.SendAsync(request); + var result = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(result, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + } + catch (HttpRequestException e) + { + throw; + } + } +} From 8246616da8e434e0d38a3a8a7512dbc6cc0d2719 Mon Sep 17 00:00:00 2001 From: Evgenii Date: Wed, 25 Dec 2024 16:29:58 +0700 Subject: [PATCH 17/22] Make FieldName Properties Public (#353) Made FieldName properties public --- src/NRedisStack/Search/FieldName.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/NRedisStack/Search/FieldName.cs b/src/NRedisStack/Search/FieldName.cs index b07d23f4..eee3ca7e 100644 --- a/src/NRedisStack/Search/FieldName.cs +++ b/src/NRedisStack/Search/FieldName.cs @@ -2,27 +2,27 @@ namespace NRedisStack.Search { public class FieldName { - private readonly string fieldName; - private string? alias; + public string Name { get; } + public string? Alias { get; private set; } public FieldName(string name) : this(name, null) { } public FieldName(string name, string? attribute) { - this.fieldName = name; - this.alias = attribute; + this.Name = name; + this.Alias = attribute; } public int AddCommandArguments(List args) { - args.Add(fieldName); - if (alias == null) + args.Add(Name); + if (Alias is null) { return 1; } args.Add("AS"); - args.Add(alias); + args.Add(Alias); return 3; } @@ -33,7 +33,7 @@ public static FieldName Of(string name) public FieldName As(string attribute) { - this.alias = attribute; + this.Alias = attribute; return this; } } From 1eeb25701718423e62b1ffd24a1f5e5c091c529d Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:38:14 +0300 Subject: [PATCH 18/22] Handle INDEXEMPTY and INDEXMISSING in FT.INFO response (#356) * issue #351 - handle INDEXEMPTY and INDEXMISSING in FT.INFO response properly * should work with enterprise and cluster but not with olders without INDEXMISSING support * test run against 7.3 and later --- .../Search/DataTypes/InfoResult.cs | 39 ++++++++++++------- tests/NRedisStack.Tests/Search/SearchTests.cs | 34 ++++++++++++++++ 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/NRedisStack/Search/DataTypes/InfoResult.cs b/src/NRedisStack/Search/DataTypes/InfoResult.cs index b676ca24..f931df92 100644 --- a/src/NRedisStack/Search/DataTypes/InfoResult.cs +++ b/src/NRedisStack/Search/DataTypes/InfoResult.cs @@ -1,14 +1,20 @@ -using StackExchange.Redis; +using System.Reflection.Emit; +using StackExchange.Redis; namespace NRedisStack.Search.DataTypes; public class InfoResult { private readonly Dictionary _all = new(); - private static readonly string[] booleanAttributes = { "SORTABLE", "UNF", "NOSTEM", "NOINDEX", "CASESENSITIVE", "WITHSUFFIXTRIE" }; + private Dictionary[] _attributes; + private Dictionary _indexOption; + private Dictionary _gcStats; + private Dictionary _cursorStats; + + private static readonly string[] booleanAttributes = { "SORTABLE", "UNF", "NOSTEM", "NOINDEX", "CASESENSITIVE", "WITHSUFFIXTRIE", "INDEXEMPTY", "INDEXMISSING" }; public string IndexName => GetString("index_name")!; - public Dictionary IndexOption => GetRedisResultDictionary("index_options")!; - public Dictionary[] Attributes => GetRedisResultDictionaryArray("attributes")!; + public Dictionary IndexOption => _indexOption = _indexOption ?? GetRedisResultDictionary("index_options")!; + public Dictionary[] Attributes => _attributes = _attributes ?? GetAttributesAsDictionaryArray()!; public long NumDocs => GetLong("num_docs"); public string MaxDocId => GetString("max_doc_id")!; public long NumTerms => GetLong("num_terms"); @@ -48,9 +54,9 @@ public class InfoResult public long NumberOfUses => GetLong("number_of_uses"); - public Dictionary GcStats => GetRedisResultDictionary("gc_stats")!; + public Dictionary GcStats => _gcStats = _gcStats ?? GetRedisResultDictionary("gc_stats")!; - public Dictionary CursorStats => GetRedisResultDictionary("cursor_stats")!; + public Dictionary CursorStats => _cursorStats = _cursorStats ?? GetRedisResultDictionary("cursor_stats")!; public InfoResult(RedisResult result) { @@ -94,24 +100,29 @@ private double GetDouble(string key) return result; } - private Dictionary[]? GetRedisResultDictionaryArray(string key) + private Dictionary[]? GetAttributesAsDictionaryArray() { - if (!_all.TryGetValue(key, out var value)) return default; + if (!_all.TryGetValue("attributes", out var value)) return default; var values = (RedisResult[])value!; var result = new Dictionary[values.Length]; for (int i = 0; i < values.Length; i++) { - var fv = (RedisResult[])values[i]!; var dict = new Dictionary(); - for (int j = 0; j < fv.Length; j += 2) + + IEnumerable enumerable = (RedisResult[])values[i]!; + IEnumerator results = enumerable.GetEnumerator(); + while (results.MoveNext()) { - if (booleanAttributes.Contains((string)fv[j]!)) + string attribute = (string)results.Current; + // if its boolean attributes add itself to the dictionary and continue + if (booleanAttributes.Contains(attribute)) { - dict.Add((string)fv[j]!, fv[j--]); + dict.Add(attribute, results.Current); } else - { - dict.Add((string)fv[j]!, fv[j + 1]); + {//if its not a boolean attribute, add the next item as value to the dictionary + results.MoveNext(); ; + dict.Add(attribute, results.Current); } } result[i] = dict; diff --git a/tests/NRedisStack.Tests/Search/SearchTests.cs b/tests/NRedisStack.Tests/Search/SearchTests.cs index 2ed10466..a5acfb68 100644 --- a/tests/NRedisStack.Tests/Search/SearchTests.cs +++ b/tests/NRedisStack.Tests/Search/SearchTests.cs @@ -887,6 +887,40 @@ public void AlterAddSortable() Assert.Equal(4, info.CursorStats.Count); } + [SkipIfRedis(Comparison.LessThan, "7.3.0")] + public void InfoWithIndexEmptyAndIndexMissing() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ft = db.FT(2); + var vectorAttrs = new Dictionary() + { + ["TYPE"] = "FLOAT32", + ["DIM"] = "2", + ["DISTANCE_METRIC"] = "L2", + }; + + Schema sc = new Schema() + .AddTextField("text1", 1.0, emptyIndex: true, missingIndex: true) + .AddTagField("tag1", emptyIndex: true, missingIndex: true) + .AddNumericField("numeric1", missingIndex: true) + .AddGeoField("geo1", missingIndex: true) + .AddGeoShapeField("geoshape1", Schema.GeoShapeField.CoordinateSystem.FLAT, missingIndex: true) + .AddVectorField("vector1", Schema.VectorField.VectorAlgo.FLAT, vectorAttrs, missingIndex: true); + Assert.True(ft.Create(index, FTCreateParams.CreateParams(), sc)); + + var info = ft.Info(index); + var attributes = info.Attributes; + foreach (var attribute in attributes) + { + Assert.True(attribute.ContainsKey("INDEXMISSING")); + if (attribute["attribute"].ToString() == "text1" || attribute["attribute"].ToString() == "tag1") + { + Assert.True(attribute.ContainsKey("INDEXEMPTY")); + } + } + } + [SkipIfRedis(Is.OSSCluster, Is.Enterprise)] public async Task AlterAddSortableAsync() { From e152ef232bb911400845ea73fd3a69a9e4bc8444 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:46:16 +0300 Subject: [PATCH 19/22] Set DIALECT 2 as default configurable dialect version (#355) - provide dialect 2 as default configurable dialect version --- src/NRedisStack/ModulePrefixes.cs | 2 +- src/NRedisStack/Search/AggregationRequest.cs | 8 ++- src/NRedisStack/Search/FTSpellCheckParams.cs | 23 +++------ src/NRedisStack/Search/IDialectAwareParam.cs | 15 ++++++ src/NRedisStack/Search/Query.cs | 9 +++- src/NRedisStack/Search/SearchCommands.cs | 38 +++----------- src/NRedisStack/Search/SearchCommandsAsync.cs | 50 +++++++++++-------- 7 files changed, 74 insertions(+), 71 deletions(-) create mode 100644 src/NRedisStack/Search/IDialectAwareParam.cs diff --git a/src/NRedisStack/ModulePrefixes.cs b/src/NRedisStack/ModulePrefixes.cs index 70dce707..8e46890c 100644 --- a/src/NRedisStack/ModulePrefixes.cs +++ b/src/NRedisStack/ModulePrefixes.cs @@ -17,7 +17,7 @@ public static class ModulePrefixes public static TdigestCommands TDIGEST(this IDatabase db) => new TdigestCommands(db); - public static SearchCommands FT(this IDatabase db, int? searchDialect = null) => new SearchCommands(db, searchDialect); + public static SearchCommands FT(this IDatabase db, int? searchDialect = 2) => new SearchCommands(db, searchDialect); public static JsonCommands JSON(this IDatabase db) => new JsonCommands(db); diff --git a/src/NRedisStack/Search/AggregationRequest.cs b/src/NRedisStack/Search/AggregationRequest.cs index 68d4a3c4..faab2bbd 100644 --- a/src/NRedisStack/Search/AggregationRequest.cs +++ b/src/NRedisStack/Search/AggregationRequest.cs @@ -2,7 +2,7 @@ using NRedisStack.Search.Literals; namespace NRedisStack.Search; -public class AggregationRequest +public class AggregationRequest : IDialectAwareParam { private List args = new List(); // Check if Readonly private bool isWithCursor = false; @@ -184,4 +184,10 @@ public bool IsWithCursor() { return isWithCursor; } + + int? IDialectAwareParam.Dialect + { + get { return dialect; } + set { dialect = value; } + } } diff --git a/src/NRedisStack/Search/FTSpellCheckParams.cs b/src/NRedisStack/Search/FTSpellCheckParams.cs index 74d3af86..cfc9e551 100644 --- a/src/NRedisStack/Search/FTSpellCheckParams.cs +++ b/src/NRedisStack/Search/FTSpellCheckParams.cs @@ -1,7 +1,7 @@ using NRedisStack.Search.Literals; namespace NRedisStack.Search { - public class FTSpellCheckParams + public class FTSpellCheckParams : IDialectAwareParam { List args = new List(); private List> terms = new List>(); @@ -59,38 +59,29 @@ public List GetArgs() } public void SerializeRedisArgs() - { - Distance(); - Terms(); - Dialect(); - } - - private void Dialect() { if (dialect != null) { args.Add(SearchArgs.DIALECT); args.Add(dialect); } - } - - private void Terms() - { foreach (var term in terms) { args.Add(SearchArgs.TERMS); args.Add(term.Value); args.Add(term.Key); } - } - - private void Distance() - { if (distance != null) { args.Add(SearchArgs.DISTANCE); args.Add(distance); } } + + int? IDialectAwareParam.Dialect + { + get { return dialect; } + set { dialect = value; } + } } } diff --git a/src/NRedisStack/Search/IDialectAwareParam.cs b/src/NRedisStack/Search/IDialectAwareParam.cs new file mode 100644 index 00000000..42ccfd3f --- /dev/null +++ b/src/NRedisStack/Search/IDialectAwareParam.cs @@ -0,0 +1,15 @@ +namespace NRedisStack.Search +{ + /// + /// Interface for dialect-aware parameters. + /// To provide a single interface to manage default dialect version under which to execute the query. + /// + internal interface IDialectAwareParam + { + /// + /// Selects the dialect version under which to execute the query. + /// + internal int? Dialect { get; set; } + } + +} \ No newline at end of file diff --git a/src/NRedisStack/Search/Query.cs b/src/NRedisStack/Search/Query.cs index 3f77d67e..524a191f 100644 --- a/src/NRedisStack/Search/Query.cs +++ b/src/NRedisStack/Search/Query.cs @@ -7,7 +7,7 @@ namespace NRedisStack.Search /// /// Query represents query parameters and filters to load results from the engine /// - public sealed class Query + public sealed class Query : IDialectAwareParam { /// /// Filter represents a filtering rules in a query @@ -691,5 +691,12 @@ public Query SetExpander(String field) _expander = field; return this; } + + int? IDialectAwareParam.Dialect + { + get { return dialect; } + set { dialect = value; } + } + } } \ No newline at end of file diff --git a/src/NRedisStack/Search/SearchCommands.cs b/src/NRedisStack/Search/SearchCommands.cs index 9bb74dde..6d9a64e6 100644 --- a/src/NRedisStack/Search/SearchCommands.cs +++ b/src/NRedisStack/Search/SearchCommands.cs @@ -6,21 +6,10 @@ namespace NRedisStack { public class SearchCommands : SearchCommandsAsync, ISearchCommands { - IDatabase _db; - public SearchCommands(IDatabase db, int? defaultDialect) : base(db) + private IDatabase _db; + public SearchCommands(IDatabase db, int? defaultDialect = 2) : base(db, defaultDialect) { _db = db; - SetDefaultDialect(defaultDialect); - this.defaultDialect = defaultDialect; - } - - public void SetDefaultDialect(int? defaultDialect) - { - if (defaultDialect == 0) - { - throw new System.ArgumentOutOfRangeException("DIALECT=0 cannot be set."); - } - this.defaultDialect = defaultDialect; } /// @@ -32,11 +21,7 @@ public RedisResult[] _List() /// public AggregationResult Aggregate(string index, AggregationRequest query) { - if (query.dialect == null && defaultDialect != null) - { - query.Dialect((int)defaultDialect); - } - + setDefaultDialectIfUnset(query); var result = _db.Execute(SearchCommandBuilder.Aggregate(index, query)); return result.ToAggregationResult(query); } @@ -130,20 +115,14 @@ public bool DropIndex(string indexName, bool dd = false) /// public string Explain(string indexName, string query, int? dialect = null) { - if (dialect == null && defaultDialect != null) - { - dialect = defaultDialect; - } + dialect = checkAndGetDefaultDialect(dialect); return _db.Execute(SearchCommandBuilder.Explain(indexName, query, dialect)).ToString()!; } /// public RedisResult[] ExplainCli(string indexName, string query, int? dialect = null) { - if (dialect == null && defaultDialect != null) - { - dialect = defaultDialect; - } + dialect = checkAndGetDefaultDialect(dialect); return _db.Execute(SearchCommandBuilder.ExplainCli(indexName, query, dialect)).ToArray(); } @@ -160,22 +139,21 @@ public Tuple> ProfileSearch(string /// public Tuple> ProfileAggregate(string indexName, AggregationRequest query, bool limited = false) { + setDefaultDialectIfUnset(query); return _db.Execute(SearchCommandBuilder.ProfileAggregate(indexName, query, limited)) .ToProfileAggregateResult(query); } /// public SearchResult Search(string indexName, Query q) { - if (q.dialect == null && defaultDialect != null) - { - q.Dialect((int)defaultDialect); - } + setDefaultDialectIfUnset(q); return _db.Execute(SearchCommandBuilder.Search(indexName, q)).ToSearchResult(q); } /// public Dictionary> SpellCheck(string indexName, string query, FTSpellCheckParams? spellCheckParams = null) { + setDefaultDialectIfUnset(spellCheckParams); return _db.Execute(SearchCommandBuilder.SpellCheck(indexName, query, spellCheckParams)).ToFtSpellCheckResult(); } diff --git a/src/NRedisStack/Search/SearchCommandsAsync.cs b/src/NRedisStack/Search/SearchCommandsAsync.cs index 6935e948..0da1c1f0 100644 --- a/src/NRedisStack/Search/SearchCommandsAsync.cs +++ b/src/NRedisStack/Search/SearchCommandsAsync.cs @@ -6,12 +6,33 @@ namespace NRedisStack { public class SearchCommandsAsync : ISearchCommandsAsync { - IDatabaseAsync _db; + private IDatabaseAsync _db; protected int? defaultDialect; - public SearchCommandsAsync(IDatabaseAsync db) + public SearchCommandsAsync(IDatabaseAsync db, int? defaultDialect = 2) { _db = db; + SetDefaultDialect(defaultDialect); + } + + internal void setDefaultDialectIfUnset(IDialectAwareParam param) + { + if (param != null && param.Dialect == null && defaultDialect != null) + { + param.Dialect = defaultDialect; + } + } + + internal int? checkAndGetDefaultDialect(int? dialect) => + (dialect == null && defaultDialect != null) ? defaultDialect : dialect; + + public void SetDefaultDialect(int? defaultDialect) + { + if (defaultDialect == 0) + { + throw new System.ArgumentOutOfRangeException("DIALECT=0 cannot be set."); + } + this.defaultDialect = defaultDialect; } /// @@ -23,11 +44,7 @@ public async Task _ListAsync() /// public async Task AggregateAsync(string index, AggregationRequest query) { - if (query.dialect == null && defaultDialect != null) - { - query.Dialect((int)defaultDialect); - } - + setDefaultDialectIfUnset(query); var result = await _db.ExecuteAsync(SearchCommandBuilder.Aggregate(index, query)); if (query.IsWithCursor()) { @@ -129,22 +146,14 @@ public async Task DropIndexAsync(string indexName, bool dd = false) /// public async Task ExplainAsync(string indexName, string query, int? dialect = null) { - if (dialect == null && defaultDialect != null) - { - dialect = defaultDialect; - } - + dialect = checkAndGetDefaultDialect(dialect); return (await _db.ExecuteAsync(SearchCommandBuilder.Explain(indexName, query, dialect))).ToString()!; } /// public async Task ExplainCliAsync(string indexName, string query, int? dialect = null) { - if (dialect == null && defaultDialect != null) - { - dialect = defaultDialect; - } - + dialect = checkAndGetDefaultDialect(dialect); return (await _db.ExecuteAsync(SearchCommandBuilder.ExplainCli(indexName, query, dialect))).ToArray(); } @@ -168,17 +177,14 @@ public async Task>> Pro /// public async Task SearchAsync(string indexName, Query q) { - if (q.dialect == null && defaultDialect != null) - { - q.Dialect((int)defaultDialect); - } - + setDefaultDialectIfUnset(q); return (await _db.ExecuteAsync(SearchCommandBuilder.Search(indexName, q))).ToSearchResult(q); } /// public async Task>> SpellCheckAsync(string indexName, string query, FTSpellCheckParams? spellCheckParams = null) { + setDefaultDialectIfUnset(spellCheckParams); return (await _db.ExecuteAsync(SearchCommandBuilder.SpellCheck(indexName, query, spellCheckParams))).ToFtSpellCheckResult(); } From 5ca1999547b231d64a786eaae4a738b287ab0e84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:57:57 +0300 Subject: [PATCH 20/22] Bump BouncyCastle.Cryptography from 2.2.0 to 2.3.1 in /tests/NRedisStack.Tests in the nuget group across 1 directory (#298) Bump BouncyCastle.Cryptography Bumps the nuget group with 1 update in the /tests/NRedisStack.Tests directory: [BouncyCastle.Cryptography](https://github.com/bcgit/bc-csharp). Updates `BouncyCastle.Cryptography` from 2.2.0 to 2.3.1 - [Commits](https://github.com/bcgit/bc-csharp/compare/release-2.2.0...release-2.3.1) --- updated-dependencies: - dependency-name: BouncyCastle.Cryptography dependency-type: direct:production dependency-group: nuget ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: atakavci <58048133+atakavci@users.noreply.github.com> --- tests/NRedisStack.Tests/NRedisStack.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj index cb522fcc..595a01a6 100644 --- a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj +++ b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj @@ -30,7 +30,7 @@ - + From b1ae31ac10f103f4b5f9d1d884bd3eb09eddb337 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:39:02 +0300 Subject: [PATCH 21/22] Bump Microsoft.NET.Test.Sdk from 16.11.0 to 17.10.0 (#299) * Bump Microsoft.NET.Test.Sdk from 16.11.0 to 17.10.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.11.0 to 17.10.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v16.11.0...v17.10.0) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * bump Microsoft.NET.Test.Sdk -> 17.10.0 with Doc.csproj --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: atakavci <58048133+atakavci@users.noreply.github.com> --- tests/Doc/Doc.csproj | 4 ++-- tests/NRedisStack.Tests/NRedisStack.Tests.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Doc/Doc.csproj b/tests/Doc/Doc.csproj index fefcd82a..3e21eee6 100644 --- a/tests/Doc/Doc.csproj +++ b/tests/Doc/Doc.csproj @@ -14,7 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj index 595a01a6..7521af79 100644 --- a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj +++ b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj @@ -20,11 +20,11 @@ all + runtime; build; native; contentfiles; analyzers; buildtransitive all - From dfc75d24d103d566fc17deda692e63110e9f4e91 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:44:48 +0300 Subject: [PATCH 22/22] Fix failing docuement.load in query results with an expired doc (#357) * fix failing docuement load in query results with an expired doc * IsCompletedSuccessfully" is *not* available in all test envs * set test case for non cluster env --- src/NRedisStack/Search/Document.cs | 4 ++ tests/NRedisStack.Tests/Search/SearchTests.cs | 50 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/NRedisStack/Search/Document.cs b/src/NRedisStack/Search/Document.cs index 3223054f..c8f9dec5 100644 --- a/src/NRedisStack/Search/Document.cs +++ b/src/NRedisStack/Search/Document.cs @@ -31,6 +31,10 @@ public static Document Load(string id, double score, byte[]? payload, RedisValue { Document ret = new Document(id, score, payload); if (fields == null) return ret; + if (fields.Length == 1 && fields[0].IsNull) + { + return ret; + } for (int i = 0; i < fields.Length; i += 2) { string fieldName = fields[i]!; diff --git a/tests/NRedisStack.Tests/Search/SearchTests.cs b/tests/NRedisStack.Tests/Search/SearchTests.cs index a5acfb68..c19c7457 100644 --- a/tests/NRedisStack.Tests/Search/SearchTests.cs +++ b/tests/NRedisStack.Tests/Search/SearchTests.cs @@ -11,7 +11,6 @@ namespace NRedisStack.Tests.Search; - public class SearchTests : AbstractNRedisStackTest, IDisposable { // private readonly string key = "SEARCH_TESTS"; @@ -3313,4 +3312,53 @@ public void TestNumericLogicalOperatorsInDialect4() Assert.Equal(1, ft.Search(index, new Query("@version:[123 123] | @id:[456 7890]")).TotalResults); Assert.Equal(1, ft.Search(index, new Query("@version==123 @id==456").Dialect(4)).TotalResults); } + + [Fact] + public void TestDocumentLoad_Issue352() + { + Document d = Document.Load("1", 0.5, null, new RedisValue[] { RedisValue.Null }); + Assert.Empty(d.GetProperties().ToList()); + } + + [SkipIfRedis(Is.OSSCluster)] + public void TestDocumentLoadWithDB_Issue352() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var ft = db.FT(); + + Schema sc = new Schema().AddTextField("first", 1.0).AddTextField("last", 1.0).AddNumericField("age"); + Assert.True(ft.Create(index, FTCreateParams.CreateParams(), sc)); + + Document droppedDocument = null; + int numberOfAttempts = 0; + do + { + db.HashSet("student:1111", new HashEntry[] { new("first", "Joe"), new("last", "Dod"), new("age", 18) }); + + Assert.True(db.KeyExpire("student:1111", TimeSpan.FromMilliseconds(500))); + + Boolean cancelled = false; + Task searchTask = Task.Run(() => + { + for (int i = 0; i < 100000; i++) + { + SearchResult result = ft.Search(index, new Query()); + List docs = result.Documents; + if (docs.Count == 0 || cancelled) + { + break; + } + else if (docs[0].GetProperties().ToList().Count == 0) + { + droppedDocument = docs[0]; + } + } + }); + Task.WhenAny(searchTask, Task.Delay(1000)).GetAwaiter().GetResult(); + Assert.True(searchTask.IsCompleted); + Assert.Null(searchTask.Exception); + cancelled = true; + } while (droppedDocument == null && numberOfAttempts++ < 3); + } }