diff --git a/libs/server/API/GarnetApiObjectCommands.cs b/libs/server/API/GarnetApiObjectCommands.cs index 208cb8af26..a63d01b176 100644 --- a/libs/server/API/GarnetApiObjectCommands.cs +++ b/libs/server/API/GarnetApiObjectCommands.cs @@ -175,6 +175,10 @@ public GarnetStatus ListLeftPush(ArgSlice key, ArgSlice element, out int count, public GarnetStatus ListLeftPush(byte[] key, ref ObjectInput input, out ObjectOutputHeader output) => storageSession.ListPush(key, ref input, out output, ref objectContext); + /// + public GarnetStatus ListPosition(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter) + => storageSession.ListPosition(key, ref input, ref outputFooter, ref objectContext); + /// public GarnetStatus ListLeftPop(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter) => storageSession.ListPop(key, ref input, ref outputFooter, ref objectContext); diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 7a84d8603d..8e3a4432b7 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -589,6 +589,16 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi #region ListPush Methods + /// + /// The command returns the index of matching elements inside a Redis list. + /// By default, when no options are given, it will scan the list from head to tail, looking for the first match of "element". + /// + /// + /// + /// + /// + GarnetStatus ListPosition(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter); + /// /// ListLeftPush ArgSlice version with ObjectOutputHeader output /// diff --git a/libs/server/Objects/List/ListObject.cs b/libs/server/Objects/List/ListObject.cs index bf6db1f3aa..24d717c4ad 100644 --- a/libs/server/Objects/List/ListObject.cs +++ b/libs/server/Objects/List/ListObject.cs @@ -34,6 +34,7 @@ public enum ListOperation : byte LSET, BRPOP, BLPOP, + LPOS, } /// @@ -179,6 +180,9 @@ public override unsafe bool Operate(ref ObjectInput input, ref SpanByteAndMemory case ListOperation.LSET: ListSet(ref input, ref output); break; + case ListOperation.LPOS: + ListPosition(ref input, ref output); + break; default: throw new GarnetException($"Unsupported operation {input.header.ListOp} in ListObject.Operate"); diff --git a/libs/server/Objects/List/ListObjectImpl.cs b/libs/server/Objects/List/ListObjectImpl.cs index fd0cbf2c20..9d3945846a 100644 --- a/libs/server/Objects/List/ListObjectImpl.cs +++ b/libs/server/Objects/List/ListObjectImpl.cs @@ -418,5 +418,206 @@ private void ListSet(ref ObjectInput input, ref SpanByteAndMemory output) output.Length = (int)(output_currptr - output_startptr); } } + + private void ListPosition(ref ObjectInput input, ref SpanByteAndMemory output) + { + var element = input.parseState.GetArgSliceByRef(input.parseStateStartIdx).ReadOnlySpan; + input.parseStateStartIdx++; + + var isMemory = false; + MemoryHandle ptrHandle = default; + var output_startptr = output.SpanByte.ToPointer(); + var output_currptr = output_startptr; + var output_end = output_currptr + output.Length; + var count = 0; + ObjectOutputHeader outputHeader = default; + + try + { + if (!ReadListPositionInput(ref input, out var rank, out count, out var maxlen, out var error)) + { + while (!RespWriteUtils.WriteError(error, ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + return; + } + + if (count < 0) + { + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + return; + } + + if (maxlen < 0) + { + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + return; + } + + int[] foundItemsArray = null; + count = count == 0 ? list.Count : count; + // TODO: (Nirmal) When count is greater then 32, directly write it in output and use buffer copy to move the data forward and add the lenght of the array at the start + Span foundItems = count <= 32 ? stackalloc int[32] : foundItemsArray = ArrayPool.Shared.Rent(count); + try + { + var noOfFoundItem = 0; + if (rank > 0) + { + var currentNode = list.First; + var currentIndex = 0; + var maxlenIndex = maxlen == 0 ? list.Count : maxlen; + do + { + var nextNode = currentNode.Next; + if (currentNode.Value.AsSpan().SequenceEqual(element)) + { + if (rank == 1) + { + foundItems[noOfFoundItem] = currentIndex; + noOfFoundItem++; + + if (noOfFoundItem == count) + { + break; + } + } + else + { + rank--; + } + } + currentNode = nextNode; + currentIndex++; + } + while (currentNode != null && currentIndex < maxlenIndex); + } + else if (rank < 0) + { + var currentNode = list.Last; + var currentIndex = list.Count -1; + var maxlenIndex = maxlen == 0 ? 0 : list.Count - maxlen; + do + { + var nextNode = currentNode.Previous; + if (currentNode.Value.AsSpan().SequenceEqual(element)) + { + if (rank == -1) + { + foundItems[noOfFoundItem] = currentIndex; + noOfFoundItem++; + + if (noOfFoundItem == count) + { + break; + } + } + else + { + rank++; + } + } + currentNode = nextNode; + currentIndex--; + } + while (currentNode != null && currentIndex >= maxlenIndex); + } + else + { + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + return; + } + + if (noOfFoundItem == 0) + { + while (!RespWriteUtils.WriteNull(ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + } + else if (noOfFoundItem == 1) + { + while (!RespWriteUtils.WriteInteger(foundItems[0], ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + } + else + { + while (!RespWriteUtils.WriteArrayLength(noOfFoundItem, ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + + foreach (var item in foundItems.Slice(0, noOfFoundItem)) + { + while (!RespWriteUtils.WriteInteger(item, ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + } + } + + outputHeader.result1 = noOfFoundItem; + } + finally + { + if (foundItemsArray is not null) + { + ArrayPool.Shared.Return(foundItemsArray); + } + } + } + finally + { + while (!RespWriteUtils.WriteDirect(ref outputHeader, ref output_currptr, output_end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end); + + if (isMemory) + ptrHandle.Dispose(); + output.Length = (int)(output_currptr - output_startptr); + } + } + + private static unsafe bool ReadListPositionInput(ref ObjectInput input, out int rank, out int count, out int maxlen, out ReadOnlySpan error) + { + var currTokenIdx = input.parseStateStartIdx; + + rank = 1; // By default, LPOS takes first match element + count = 1; // By default, LPOS return 1 element + maxlen = 0; // By default, iterate to all the item + + error = default; + + while (currTokenIdx < input.parseState.Count) + { + var sbParam = input.parseState.GetArgSliceByRef(currTokenIdx++).ReadOnlySpan; + + if (sbParam.SequenceEqual(CmdStrings.RANK) || sbParam.SequenceEqual(CmdStrings.rank)) + { + if (!input.parseState.TryGetInt(currTokenIdx++, out rank)) + { + error = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; + return false; + } + } + else if (sbParam.SequenceEqual(CmdStrings.COUNT) || sbParam.SequenceEqual(CmdStrings.count)) + { + if (!input.parseState.TryGetInt(currTokenIdx++, out count)) + { + error = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; + return false; + } + } + else if (sbParam.SequenceEqual(CmdStrings.MAXLEN) || sbParam.SequenceEqual(CmdStrings.maxlen)) + { + if (!input.parseState.TryGetInt(currTokenIdx++, out maxlen)) + { + error = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; + return false; + } + } + else + { + error = CmdStrings.RESP_SYNTAX_ERROR; + return false; + } + } + + return true; + } } } \ No newline at end of file diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index 48c86c6ed0..d4c91171d5 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -94,6 +94,10 @@ static partial class CmdStrings public static ReadOnlySpan XX => "XX"u8; public static ReadOnlySpan UNSAFETRUNCATELOG => "UNSAFETRUNCATELOG"u8; public static ReadOnlySpan SAMPLES => "SAMPLES"u8; + public static ReadOnlySpan RANK => "RANK"u8; + public static ReadOnlySpan rank => "rank"u8; + public static ReadOnlySpan MAXLEN => "MAXLEN"u8; + public static ReadOnlySpan maxlen => "maxlen"u8; /// /// Response strings diff --git a/libs/server/Resp/Objects/ListCommands.cs b/libs/server/Resp/Objects/ListCommands.cs index c6de13b0bf..159b00feec 100644 --- a/libs/server/Resp/Objects/ListCommands.cs +++ b/libs/server/Resp/Objects/ListCommands.cs @@ -156,6 +156,65 @@ private unsafe bool ListPop(RespCommand command, ref TGarnetApi stor return true; } + /// + /// The command returns the index of matching elements inside a Redis list. + /// By default, when no options are given, it will scan the list from head to tail, looking for the first match of "element". + /// + /// + /// + /// + private unsafe bool ListPosition(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + if (parseState.Count < 2) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.LPOS)); + } + + // Get the key for List + var sbKey = parseState.GetArgSliceByRef(0).SpanByte; + var element = parseState.GetArgSliceByRef(1).SpanByte; + var keyBytes = sbKey.ToByteArray(); + + if (NetworkSingleKeySlotVerify(keyBytes, false)) + { + return true; + } + + // Prepare input + var input = new ObjectInput + { + header = new RespInputHeader + { + type = GarnetObjectType.List, + ListOp = ListOperation.LPOS, + }, + parseState = parseState, + parseStateStartIdx = 1, + }; + + // Prepare GarnetObjectStore output + var outputFooter = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)) }; + + var statusOp = storageApi.ListPosition(keyBytes, ref input, ref outputFooter); + + switch (statusOp) + { + case GarnetStatus.OK: + ProcessOutputWithHeader(outputFooter.spanByteAndMemory); + break; + case GarnetStatus.NOTFOUND: + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + } + + return true; + } /// /// LMPOP numkeys key [key ...] LEFT | RIGHT [COUNT count] diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 83595b1cb0..f20a02f6cc 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -45,6 +45,7 @@ public enum RespCommand : byte KEYS, LINDEX, LLEN, + LPOS, LRANGE, MEMORY_USAGE, MGET, @@ -767,6 +768,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.LSET; } + else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("\r\nLPOS\r\n"u8)) + { + return RespCommand.LPOS; + } break; case 'M': diff --git a/libs/server/Resp/RespCommandsInfo.json b/libs/server/Resp/RespCommandsInfo.json index 65204e8205..74fe858e06 100644 --- a/libs/server/Resp/RespCommandsInfo.json +++ b/libs/server/Resp/RespCommandsInfo.json @@ -2780,6 +2780,35 @@ ], "SubCommands": null }, + { + "Command": "LPOS", + "Name": "LPOS", + "IsInternal": false, + "Arity": -3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "List, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, { "Command": "LPUSH", "Name": "LPUSH", diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index c0ffdce91c..6fb6708dfb 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -627,6 +627,7 @@ private bool ProcessArrayCommands(RespCommand cmd, ref TGarnetApi st RespCommand.LPUSH => ListPush(cmd, ref storageApi), RespCommand.LPUSHX => ListPush(cmd, ref storageApi), RespCommand.LPOP => ListPop(cmd, ref storageApi), + RespCommand.LPOS => ListPosition(ref storageApi), RespCommand.RPUSH => ListPush(cmd, ref storageApi), RespCommand.RPUSHX => ListPush(cmd, ref storageApi), RespCommand.RPOP => ListPop(cmd, ref storageApi), diff --git a/libs/server/Storage/Session/ObjectStore/ListOps.cs b/libs/server/Storage/Session/ObjectStore/ListOps.cs index 792516ff42..c1244ae60e 100644 --- a/libs/server/Storage/Session/ObjectStore/ListOps.cs +++ b/libs/server/Storage/Session/ObjectStore/ListOps.cs @@ -397,6 +397,22 @@ public GarnetStatus ListPush(byte[] key, ref ObjectInput input, return status; } + /// + /// The command returns the index of matching elements inside a Redis list. + /// By default, when no options are given, it will scan the list from head to tail, looking for the first match of "element". + /// + /// + /// + /// + /// + /// + /// + public GarnetStatus ListPosition(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter, ref TObjectContext objectStoreContext) + where TObjectContext : ITsavoriteContext + { + return ReadObjectStoreOperationWithOutput(key, ref input, ref objectStoreContext, ref outputFooter); + } + /// /// Trim an existing list so it only contains the specified range of elements. /// diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index db39c684fd..ba9cf28bf6 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -158,6 +158,7 @@ public class SupportedCommand new("LMOVE", RespCommand.LMOVE), new("LMPOP", RespCommand.LMPOP), new("LPOP", RespCommand.LPOP), + new("LPOS", RespCommand.LPOS), new("LPUSH", RespCommand.LPUSH), new("LPUSHX", RespCommand.LPUSHX), new("LRANGE", RespCommand.LRANGE), diff --git a/test/Garnet.test/RespListTests.cs b/test/Garnet.test/RespListTests.cs index 6f99ba2eb1..241e26e839 100644 --- a/test/Garnet.test/RespListTests.cs +++ b/test/Garnet.test/RespListTests.cs @@ -1440,5 +1440,121 @@ public void CheckListOperationsOnWrongTypeObjectSE() // LMPOP RIGHT RespTestsUtils.CheckCommandOnWrongTypeObjectSE(() => db.ListRightPop(keys, 3)); } + + #region LPOS + + [Test] + [TestCase("a,c,b,c,d", "a", 0)] + [TestCase("a,c,b,c,adc", "adc", 4)] + [TestCase("a,c,b,c,d", "c", 1)] + [TestCase("av,123,bs,c,d", "e", null)] + public void LPOSWithoutOptions(string items, string find, int? expectedIndex) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "KeyA"; + string[] arguments = [key, ..items.Split(",")]; + + db.Execute("RPUSH", arguments); + + var actualIndex = (int?)db.Execute("LPOS", key, find); + + ClassicAssert.AreEqual(expectedIndex, actualIndex); + } + + [Test] + public void LPOSWithInvalidKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "KeyA"; + + var result = (int?)db.Execute("LPOS", key, "e"); + + ClassicAssert.IsNull(result); + } + + [Test] + [TestCase("a,c,b,c,d", "c", "1", "rank,1")] + [TestCase("a,c,b,c,d", "c", "3", "RANK,2")] + [TestCase("a,c,b,c,d", "c", "null", "rank,3")] + [TestCase("a,c,b,c,d", "c", "3", "RANK,-1")] + [TestCase("a,c,b,c,d", "c", "1", "rank,-2")] + [TestCase("a,c,b,c,d", "c", "null", "RANK,-3")] + [TestCase("a,c,b,c,d", "a", "null", "rank,2")] + [TestCase("a,c,b,c,d", "b", "2", "count,2")] + [TestCase("a,c,b,c,d", "c", "1", "count,1")] + [TestCase("a,c,b,c,d", "c", "1,3", "COUNT,2")] + [TestCase("a,c,b,c,d", "c", "1,3", "count,3")] + [TestCase("a,c,b,c,d", "c", "1,3", "count,0")] + [TestCase("a,c,b,c,d", "c", "1", "maxlen,0")] + [TestCase("a,c,b,c,d", "c", "null", "MAXLEN,1")] + [TestCase("a,c,b,c,d", "c", "1", "maxlen,2")] + [TestCase("a,c,b,c,d", "c", "null", "rank,-1,maxlen,1")] + [TestCase("a,c,b,c,d", "c", "3", "rank,-1,maxlen,2")] + [TestCase("a,c,b,c,d", "c", "null", "rank,-2,maxlen,2")] + [TestCase("a,c,b,c,d", "c", "null", "rank,1,maxlen,1")] + [TestCase("a,c,b,c,d", "c", "1", "rank,1,maxlen,2")] + [TestCase("a,c,b,c,d", "c", "null", "rank,2,maxlen,2")] + [TestCase("a,c,b,c,d", "c", "3,1", "rank,-1,maxlen,0,count,0")] + [TestCase("a,c,b,c,d", "c", "3", "rank,-1,maxlen,0,count,1")] + [TestCase("a,c,b,c,d", "c", "1,3", "rank,1,maxlen,0,count,0")] + [TestCase("a,c,b,c,d", "c", "1", "rank,1,maxlen,0,count,1")] + public void LPOSWithOptions(string items, string find, string expectedIndexs, string options) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "KeyA"; + string[] pushArguments = [key, .. items.Split(",")]; + string[] lopsArguments = [key, find, .. options.Split(",")]; + var expectedIndexInts = expectedIndexs.Split(",").Select(ToNullableInt).ToList(); + + db.Execute("RPUSH", pushArguments); + + if (expectedIndexInts.Count == 1) + { + var actualIndex = (int?)db.Execute("LPOS", lopsArguments); + + ClassicAssert.AreEqual(expectedIndexInts[0], actualIndex); + } + else + { + var actualIndex = (int[])db.Execute("LPOS", lopsArguments); + + ClassicAssert.AreEqual(expectedIndexInts.Count, actualIndex.Length); + foreach (var index in expectedIndexInts.Zip(actualIndex)) + { + ClassicAssert.AreEqual(index.First, index.Second); + } + } + } + + [Test] + [TestCase("a,c,b,c,d", "c", "1", "rank,0")] + [TestCase("a,c,b,c,d", "c", "3", "count,-1")] + [TestCase("a,c,b,c,d", "c", "null", "MAXLEN,-5")] + [TestCase("a,c,b,c,d", "c", "null", "rand,2")] + [TestCase("a,c,b,c,d", "c", "null", "rank,1,count,-1")] + public void LPOSWithInvalidOptions(string items, string find, string expectedIndexs, string options) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "KeyA"; + string[] pushArguments = [key, .. items.Split(",")]; + string[] lopsArguments = [key, find, .. options.Split(",")]; + var expectedIndexInts = expectedIndexs.Split(",").Select(ToNullableInt).ToList(); + db.Execute("RPUSH", pushArguments); + + Assert.Throws(() => db.Execute("LPOS", lopsArguments)); + } + + private int? ToNullableInt(string s) + { + int i; + if (int.TryParse(s, out i)) return i; + return null; + } + + #endregion } } \ No newline at end of file diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index af95155613..4a3caab5a1 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -160,7 +160,7 @@ Note that this list is subject to change as we continue to expand our API comman | | [LMOVE](data-structures.md#lmove) | ➕ | | | | LMPOP | ➖ | | | | [LPOP](data-structures.md#lpop) | ➕ | | -| | LPOS | ➖ | | +| | [LPOS](data-structures.md#lpos) | ➕ | | | | [LPUSH](data-structures.md#lpush) | ➕ | | | | [LPUSHX](data-structures.md#lpushx) | ➕ | | | | [LRANGE](data-structures.md#lrange) | ➕ | | diff --git a/website/docs/commands/data-structures.md b/website/docs/commands/data-structures.md index 922bd70a2c..9a9e6912d9 100644 --- a/website/docs/commands/data-structures.md +++ b/website/docs/commands/data-structures.md @@ -319,6 +319,26 @@ By default, the command pops a single element from the beginning of the list. Wh --- +### LPOP + +#### Syntax + +```bash + LPOP key [count] +``` + +The command returns the index of matching elements inside a Redis list. By default, when no options are given, it will scan the list from head to tail, looking for the first match of "element". If the element is found, its index (the zero-based position in the list) is returned. Otherwise, if no match is found, nil is returned. + +#### Resp Reply + +Any of the following: + +* Null reply: if there is no matching element. +* Integer reply: an integer representing the matching element. +* Array reply: If the COUNT option is given, an array of integers representing the matching elements (or an empty array if there are no matches). + +--- + ### LPUSH #### Syntax