From bacd35dfed75e3b45a8213ecf1f882ad62aecb94 Mon Sep 17 00:00:00 2001 From: prvyk <52283326+prvyk@users.noreply.github.com> Date: Sat, 8 Feb 2025 07:05:07 +0200 Subject: [PATCH] Add ZRANGEBYLEX command synonym. (#985) * Add ZRANGEBYLEX command synonym. * Add to json in correct sort order * Add testcases. --------- Co-authored-by: prvyk Co-authored-by: Tal Zaccai --- libs/resources/RespCommandsDocs.json | 51 +++++++++++++++++++ libs/resources/RespCommandsInfo.json | 25 +++++++++ .../Objects/SortedSet/SortedSetObject.cs | 2 + .../Objects/SortedSet/SortedSetObjectImpl.cs | 3 ++ libs/server/Resp/Objects/SortedSetCommands.cs | 1 + libs/server/Resp/Parser/RespCommand.cs | 5 ++ libs/server/Resp/RespServerSession.cs | 1 + libs/server/Transaction/TxnKeyManager.cs | 1 + .../CommandInfoUpdater/SupportedCommand.cs | 1 + test/Garnet.test/Resp/ACL/RespCommandTests.cs | 21 ++++++++ test/Garnet.test/RespSortedSetTests.cs | 29 +++++++++++ 11 files changed, 140 insertions(+) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index 3dc5031b4b..9a6bcbd9ed 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -7583,6 +7583,57 @@ } ] }, + { + "Command": "ZRANGEBYLEX", + "Name": "ZRANGEBYLEX", + "Summary": "Returns the number of members in a sorted set within a lexicographical range.", + "Group": "SortedSet", + "Complexity": "O(log(N)\u002BM) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", + "DocFlags": "Deprecated", + "ReplacedBy": "\u0060ZRANGE\u0060 with the \u0060BYLEX\u0060 argument", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "DisplayText": "key", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MIN", + "DisplayText": "min", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MAX", + "DisplayText": "max", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "LIMIT", + "Type": "Block", + "Token": "LIMIT", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "OFFSET", + "DisplayText": "offset", + "Type": "Integer" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "COUNT", + "DisplayText": "count", + "Type": "Integer" + } + ] + } + ] + }, { "Command": "ZRANGEBYSCORE", "Name": "ZRANGEBYSCORE", diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index 2f884589ee..5214c21013 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -5179,6 +5179,31 @@ } ] }, + { + "Command": "ZRANGEBYLEX", + "Name": "ZRANGEBYLEX", + "Arity": -4, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, SortedSet, Slow", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO, Access" + } + ] + }, { "Command": "ZRANGEBYSCORE", "Name": "ZRANGEBYSCORE", diff --git a/libs/server/Objects/SortedSet/SortedSetObject.cs b/libs/server/Objects/SortedSet/SortedSetObject.cs index cbfa9dd1a6..c2e746473f 100644 --- a/libs/server/Objects/SortedSet/SortedSetObject.cs +++ b/libs/server/Objects/SortedSet/SortedSetObject.cs @@ -27,6 +27,7 @@ public enum SortedSetOperation : byte ZINCRBY, ZRANK, ZRANGE, + ZRANGEBYLEX, ZRANGEBYSCORE, ZRANGESTORE, GEOADD, @@ -262,6 +263,7 @@ public override unsafe bool Operate(ref ObjectInput input, ref GarnetObjectStore case SortedSetOperation.ZRANGESTORE: SortedSetRange(ref input, ref output.SpanByteAndMemory); break; + case SortedSetOperation.ZRANGEBYLEX: case SortedSetOperation.ZRANGEBYSCORE: SortedSetRange(ref input, ref output.SpanByteAndMemory); break; diff --git a/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs b/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs index f22b7c68c7..837a62d45b 100644 --- a/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs +++ b/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs @@ -439,6 +439,9 @@ private void SortedSetRange(ref ObjectInput input, ref SpanByteAndMemory output) ZRangeOptions options = new(); switch (input.header.SortedSetOp) { + case SortedSetOperation.ZRANGEBYLEX: + options.ByLex = true; + break; case SortedSetOperation.ZRANGESTORE: options.WithScores = true; break; diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs index e04c353fa4..44e3d9854d 100644 --- a/libs/server/Resp/Objects/SortedSetCommands.cs +++ b/libs/server/Resp/Objects/SortedSetCommands.cs @@ -164,6 +164,7 @@ private unsafe bool SortedSetRange(RespCommand command, ref TGarnetA { RespCommand.ZRANGE => SortedSetOperation.ZRANGE, RespCommand.ZREVRANGE => SortedSetOperation.ZREVRANGE, + RespCommand.ZRANGEBYLEX => SortedSetOperation.ZRANGEBYLEX, RespCommand.ZRANGEBYSCORE => SortedSetOperation.ZRANGEBYSCORE, RespCommand.ZREVRANGEBYLEX => SortedSetOperation.ZREVRANGEBYLEX, RespCommand.ZREVRANGEBYSCORE => SortedSetOperation.ZREVRANGEBYSCORE, diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 09e312518f..2d79da87ed 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -91,6 +91,7 @@ public enum RespCommand : ushort ZMSCORE, ZRANDMEMBER, ZRANGE, + ZRANGEBYLEX, ZRANGEBYSCORE, ZRANK, ZREVRANGE, @@ -1578,6 +1579,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.ZRANGESTORE; } + else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nZRANG"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("EBYLEX\r\n"u8)) + { + return RespCommand.ZRANGEBYLEX; + } else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nZINTE"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("RSTORE\r\n"u8)) { return RespCommand.ZINTERSTORE; diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 84bdf4a349..eb8fc28ef6 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -649,6 +649,7 @@ private bool ProcessArrayCommands(RespCommand cmd, ref TGarnetApi st RespCommand.ZINCRBY => SortedSetIncrement(ref storageApi), RespCommand.ZRANK => SortedSetRank(cmd, ref storageApi), RespCommand.ZRANGE => SortedSetRange(cmd, ref storageApi), + RespCommand.ZRANGEBYLEX => SortedSetRange(cmd, ref storageApi), RespCommand.ZRANGESTORE => SortedSetRangeStore(ref storageApi), RespCommand.ZRANGEBYSCORE => SortedSetRange(cmd, ref storageApi), RespCommand.ZREVRANK => SortedSetRank(cmd, ref storageApi), diff --git a/libs/server/Transaction/TxnKeyManager.cs b/libs/server/Transaction/TxnKeyManager.cs index 2e525eee68..4df5679563 100644 --- a/libs/server/Transaction/TxnKeyManager.cs +++ b/libs/server/Transaction/TxnKeyManager.cs @@ -90,6 +90,7 @@ internal int GetKeys(RespCommand command, int inputCount, out ReadOnlySpan RespCommand.ZINCRBY => SortedSetObjectKeys(SortedSetOperation.ZINCRBY, inputCount), RespCommand.ZRANK => SortedSetObjectKeys(SortedSetOperation.ZRANK, inputCount), RespCommand.ZRANGE => SortedSetObjectKeys(SortedSetOperation.ZRANGE, inputCount), + RespCommand.ZRANGEBYLEX => SortedSetObjectKeys(SortedSetOperation.ZRANGEBYLEX, inputCount), RespCommand.ZRANGEBYSCORE => SortedSetObjectKeys(SortedSetOperation.ZRANGEBYSCORE, inputCount), RespCommand.ZREVRANK => SortedSetObjectKeys(SortedSetOperation.ZREVRANK, inputCount), RespCommand.ZREMRANGEBYLEX => SortedSetObjectKeys(SortedSetOperation.ZREMRANGEBYLEX, inputCount), diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index 3530c1b5a4..9e4f6f5d01 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -313,6 +313,7 @@ public class SupportedCommand new("ZPOPMIN", RespCommand.ZPOPMIN), new("ZRANDMEMBER", RespCommand.ZRANDMEMBER), new("ZRANGE", RespCommand.ZRANGE), + new("ZRANGEBYLEX", RespCommand.ZRANGEBYLEX), new("ZRANGEBYSCORE", RespCommand.ZRANGEBYSCORE), new("ZRANGESTORE", RespCommand.ZRANGESTORE), new("ZRANK", RespCommand.ZRANK), diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 1b54c6c6a0..29510cd586 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -6403,6 +6403,27 @@ static async Task DoZRangeStoreAsync(GarnetClient client) } } + [Test] + public async Task ZRangeByLexACLsAsync() + { + await CheckCommandsAsync( + "ZRANGEBYLEX", + [DoZRangeByLexAsync, DoZRangeByLexLimitAsync] + ); + + static async Task DoZRangeByLexAsync(GarnetClient client) + { + string[] val = await client.ExecuteForStringArrayResultAsync("ZRANGEBYLEX", ["key", "10", "20"]); + ClassicAssert.AreEqual(0, val.Length); + } + + static async Task DoZRangeByLexLimitAsync(GarnetClient client) + { + string[] val = await client.ExecuteForStringArrayResultAsync("ZRANGEBYLEX", ["key", "10", "20", "LIMIT", "2", "3"]); + ClassicAssert.AreEqual(0, val.Length); + } + } + [Test] public async Task ZRangeByScoreACLsAsync() { diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs index 250e1fbed4..48819d416e 100644 --- a/test/Garnet.test/RespSortedSetTests.cs +++ b/test/Garnet.test/RespSortedSetTests.cs @@ -189,6 +189,9 @@ public void AddWithOptions() var added = db.SortedSetAdd(key, entries); ClassicAssert.AreEqual(entries.Length, added); + var lex = db.SortedSetRangeByValue(key, default, "c"); + CollectionAssert.AreEqual(new RedisValue[] { "a", "b", "c" }, lex); + // XX - Only update elements that already exist. Don't add new elements. var testEntries = new[] { @@ -200,6 +203,8 @@ public void AddWithOptions() added = db.SortedSetAdd(key, testEntries, SortedSetWhen.Exists); ClassicAssert.AreEqual(0, added); + lex = db.SortedSetRangeByValue(key, default, "c"); + CollectionAssert.AreEqual(new RedisValue[] { "a", "c", "b" }, lex); var scores = db.SortedSetScores(key, [new RedisValue("a"), new RedisValue("b")]); CollectionAssert.AreEqual(new double[] { 3, 4 }, scores); var count = db.SortedSetLength(key); @@ -216,6 +221,8 @@ public void AddWithOptions() added = db.SortedSetAdd(key, testEntries, SortedSetWhen.NotExists); ClassicAssert.AreEqual(2, added); + lex = db.SortedSetRangeByValue(key, default, "c"); + CollectionAssert.AreEqual(new RedisValue[] { "a", "c", "b" }, lex); scores = db.SortedSetScores(key, [new RedisValue("a"), new RedisValue("b"), new RedisValue("k"), new RedisValue("l")]); CollectionAssert.AreEqual(new double[] { 3, 4, 11, 12 }, scores); count = db.SortedSetLength(key); @@ -231,6 +238,8 @@ public void AddWithOptions() added = db.SortedSetAdd(key, testEntries, SortedSetWhen.LessThan); ClassicAssert.AreEqual(1, added); + lex = db.SortedSetRangeByValue(key, default, "c"); + CollectionAssert.AreEqual(new RedisValue[] { "a", "b", "c" }, lex); scores = db.SortedSetScores(key, [new RedisValue("a"), new RedisValue("b"), new RedisValue("m")]); CollectionAssert.AreEqual(new double[] { 3, 3, 13 }, scores); count = db.SortedSetLength(key); @@ -246,6 +255,8 @@ public void AddWithOptions() added = db.SortedSetAdd(key, testEntries, SortedSetWhen.GreaterThan); ClassicAssert.AreEqual(1, added); + lex = db.SortedSetRangeByValue(key, default, "c"); + CollectionAssert.AreEqual(new RedisValue[] { "b", "c", "a" }, lex); scores = db.SortedSetScores(key, [new RedisValue("a"), new RedisValue("b"), new RedisValue("n")]); CollectionAssert.AreEqual(new double[] { 4, 3, 14 }, scores); count = db.SortedSetLength(key); @@ -2044,6 +2055,12 @@ public void CanDoZRangeByLex() expectedResponse = "*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); + + // ZRANGEBYLEX Synonym + response = lightClientRequest.SendCommand("ZRANGEBYLEX board - [c", 4); + //expectedResponse = "*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); } [Test] @@ -2082,6 +2099,12 @@ public void CanDoZRangeByLexReverse() expectedResponse = "*3\r\n$1\r\nc\r\n$1\r\nb\r\n$1\r\na\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); + + // ZREVRANGEBYLEX Synonym + response = lightClientRequest.SendCommand("ZREVRANGEBYLEX board [c - REV", 4); + //expectedResponse = "*3\r\n$1\r\nc\r\n$1\r\nb\r\n$1\r\na\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); } [Test] @@ -2098,6 +2121,12 @@ public void CanDoZRangeByLexWithLimit() expectedResponse = "*3\r\n$7\r\nNewYork\r\n$5\r\nParis\r\n$5\r\nSeoul\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); + + // ZRANGEBYLEX Synonym + response = lightClientRequest.SendCommand("ZRANGEBYLEX mycity - + LIMIT 2 3", 4); + //expectedResponse = "*3\r\n$7\r\nNewYork\r\n$5\r\nParis\r\n$5\r\nSeoul\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); }