Skip to content

Commit

Permalink
Support HSTRLEN command. (#115)
Browse files Browse the repository at this point in the history
* Support `HSTRLEN` command.

* consume left tokens

* Simplify the HashStrLength implementation

* Handle the wrong number of arguments

* add additional params check test

* update document

* fix command parse error

---------

Co-authored-by: Tal Zaccai <talzacc@microsoft.com>
  • Loading branch information
argsno and TalZaccai committed Mar 27, 2024
1 parent e07686c commit 439e312
Show file tree
Hide file tree
Showing 15 changed files with 200 additions and 3 deletions.
4 changes: 4 additions & 0 deletions libs/server/API/GarnetApiObjectCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ public GarnetStatus HashLength(ArgSlice key, out int count)
public GarnetStatus HashLength(byte[] key, ArgSlice input, out ObjectOutputHeader output)
=> storageSession.HashLength(key, input, out output, ref objectContext);

/// <inheritdoc />
public GarnetStatus HashStrLength(byte[] key, ArgSlice input, out ObjectOutputHeader output)
=> storageSession.HashStrLength(key, input, out output, ref objectContext);

/// <inheritdoc />
public GarnetStatus HashExists(ArgSlice key, ArgSlice field, out bool exists)
=> storageSession.HashExists(key, field, out exists, ref objectContext);
Expand Down
7 changes: 7 additions & 0 deletions libs/server/API/GarnetWatchApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,13 @@ public GarnetStatus HashGet(byte[] key, ArgSlice input, ref GarnetObjectStoreOut
return garnetApi.HashGet(key, input, ref outputFooter);
}

/// <inheritdoc />
public GarnetStatus HashStrLength(byte[] key, ArgSlice input, out ObjectOutputHeader output)
{
garnetApi.WATCH(key, StoreType.Object);
return garnetApi.HashStrLength(key, input, out output);
}

/// <inheritdoc />
public GarnetStatus HashExists(byte[] key, ArgSlice input, out ObjectOutputHeader output)
{
Expand Down
9 changes: 9 additions & 0 deletions libs/server/API/IGarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,15 @@ public interface IGarnetReadApi
/// <returns></returns>
GarnetStatus HashLength(ArgSlice key, out int count);

/// <summary>
///Returns the string length of the value associated with field in the hash stored at key. If the key or the field do not exist, 0 is returned.
/// </summary>
/// <param name="key"></param>
/// <param name="input"></param>
/// <param name="output"></param>
/// <returns></returns>
GarnetStatus HashStrLength(byte[] key, ArgSlice input, out ObjectOutputHeader output);

/// <summary>
/// Returns the number of fields contained in the hash Key.
/// </summary>
Expand Down
6 changes: 5 additions & 1 deletion libs/server/Objects/Hash/HashObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public enum HashOperation : byte
HINCRBY,
HINCRBYFLOAT,
HRANDFIELD,
HSCAN
HSCAN,
HSTRLEN
}


Expand Down Expand Up @@ -145,6 +146,9 @@ public override unsafe bool Operate(ref SpanByte input, ref SpanByteAndMemory ou
case HashOperation.HLEN:
HashLength(_output);
break;
case HashOperation.HSTRLEN:
HashStrLength(_input, input.Length, _output);
break;
case HashOperation.HEXISTS:
HashExists(_input, input.Length, _output);
break;
Expand Down
16 changes: 16 additions & 0 deletions libs/server/Objects/Hash/HashObjectImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,22 @@ private void HashLength(byte* output)
((ObjectOutputHeader*)output)->countDone = hash.Count;
}

private void HashStrLength(byte* input, int length, byte* output)
{
var _output = (ObjectOutputHeader*)output;

byte* startptr = input + sizeof(ObjectInputHeader);
byte* ptr = startptr;

*_output = default;
if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var key, ref ptr, input + length))
return;

_output->opsDone = 1;
_output->countDone = hash.TryGetValue(key, out var _value) ? _value.Length : 0;
_output->bytesDone = (int)(ptr - startptr);
}

private void HashExists(byte* input, int length, byte* output)
{
var _input = (ObjectInputHeader*)input;
Expand Down
74 changes: 74 additions & 0 deletions libs/server/Resp/Objects/HashCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,80 @@ private unsafe bool HashLength<TGarnetApi>(int count, byte* ptr, ref TGarnetApi
return true;
}

/// <summary>
/// Returns the string length of the value associated with field in the hash stored at key. If the key or the field do not exist, 0 is returned.
/// </summary>
/// <param name="count"></param>
/// <param name="ptr"></param>
/// <param name="storageApi"></param>
/// <typeparam name="TGarnetApi"></typeparam>
/// <returns></returns>
private unsafe bool HashStrLength<TGarnetApi>(int count, byte* ptr, ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
ptr += 13;

if (count != 3)
{
hashItemsDoneCount = hashOpsCount = 0;
return AbortWithWrongNumberOfArguments("HSTRLEN", count);
}
else
{
// Get the key for Hash
if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var key, ref ptr, recvBufferPtr + bytesRead))
return false;

if (NetworkSingleKeySlotVerify(key, true))
{
var bufSpan = new ReadOnlySpan<byte>(recvBufferPtr, bytesRead);
if (!DrainCommands(bufSpan, count))
return false;
return true;
}

// Prepare input
var inputPtr = (ObjectInputHeader*)(ptr - sizeof(ObjectInputHeader));

// Save old values on buffer for possible revert
var save = *inputPtr;

// Prepare length of header in input buffer
var inputLength = (int)(recvBufferPtr + bytesRead - (byte*)inputPtr);

// Prepare header in input buffer
inputPtr->header.type = GarnetObjectType.Hash;
inputPtr->header.HashOp = HashOperation.HSTRLEN;
inputPtr->count = 1;
inputPtr->done = 0;

var status = storageApi.HashStrLength(key, new ArgSlice((byte*)inputPtr, inputLength), out ObjectOutputHeader output);

// Restore input buffer
*inputPtr = save;

switch (status)
{
case GarnetStatus.OK:
// Process output
while (!RespWriteUtils.WriteInteger(output.countDone, ref dcurr, dend))
SendAndReset();
ptr += output.bytesDone;
break;
case GarnetStatus.NOTFOUND:
while (!RespWriteUtils.WriteResponse(CmdStrings.RESP_RETURN_VAL_0, ref dcurr, dend))
SendAndReset();
ReadLeftToken(count - 2, ref ptr);
break;
}
}

// Reset session counters
hashItemsDoneCount = hashOpsCount = 0;
readHead = (int)(ptr - recvBufferPtr);
return true;
}

/// <summary>
/// Removes the specified fields from the hash stored at key.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions libs/server/Resp/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,9 @@ private RespCommand FastParseCommand(byte* ptr)
return (RespCommand.Hash, (byte)HashOperation.HRANDFIELD);
if (*(long*)ptr == 4702694004776318244L && *(ushort*)(ptr + 8) == 3406 && *(ptr + 10) == 10)
return (RespCommand.Hash, (byte)HashOperation.HSCAN);
//[$7|HSTRLEN|] = 13 bytes = 8 (long) + 2 (ushort) + 3 bytes
if (*(long*)ptr == 5932458178025174820L && *(ushort*)(ptr + 8) == 17740 && *(ptr + 12) == 10)
return (RespCommand.Hash, (byte)HashOperation.HSTRLEN);
#endregion

#region Set Operations
Expand Down
1 change: 1 addition & 0 deletions libs/server/Resp/RespCommandsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ public static RespCommandsInfo findCommand(RespCommand cmd, byte subCmd = 0)
{(byte)HashOperation.HSETNX, new RespCommandsInfo("HSETNX", RespCommand.Hash, 4, null, (byte)HashOperation.HSETNX)},
{(byte)HashOperation.HRANDFIELD, new RespCommandsInfo("HRANDFIELD", RespCommand.Hash, -2, null, (byte)HashOperation.HRANDFIELD)},
{(byte)HashOperation.HSCAN, new RespCommandsInfo("HSCAN", RespCommand.Hash, -3, null, (byte)HashOperation.HSCAN)},
{(byte)HashOperation.HSTRLEN, new RespCommandsInfo("HSTRLEN", RespCommand.Hash, 3, null, (byte)HashOperation.HSTRLEN)},
};

private static readonly Dictionary<byte, RespCommandsInfo> setCommandsInfoMap = new Dictionary<byte, RespCommandsInfo>
Expand Down
2 changes: 1 addition & 1 deletion libs/server/Resp/RespInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static HashSet<string> GetCommands()
// List
"LPOP", "LPUSH", "RPOP", "RPUSH", "LLEN", "LTRIM", "LRANGE", "LINDEX", "LINSERT", "LREM", "RPOPLPUSH", "LMOVE", "LPUSHX", "RPUSHX",
// Hash
"HSET", "HGET", "HMGET", "HMSET", "HDEL", "HLEN", "HEXISTS", "HGETALL", "HKEYS", "HVALS", "HINCRBY", "HINCRBYFLOAT", "HSETNX", "HRANDFIELD", "HSCAN",
"HSET", "HGET", "HMGET", "HMSET", "HDEL", "HLEN", "HEXISTS", "HGETALL", "HKEYS", "HVALS", "HINCRBY", "HINCRBYFLOAT", "HSETNX", "HRANDFIELD", "HSCAN", "HSTRLEN",
// Hyperloglog
"PFADD", "PFCOUNT", "PFMERGE",
// Bitmap
Expand Down
1 change: 1 addition & 0 deletions libs/server/Resp/RespServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ private bool ProcessArrayCommands<TGarnetApi>(ref TGarnetApi storageApi)
(RespCommand.Hash, (byte)HashOperation.HGETALL) => HashGet(count, ptr, HashOperation.HGETALL, ref storageApi),
(RespCommand.Hash, (byte)HashOperation.HDEL) => HashDelete(count, ptr, ref storageApi),
(RespCommand.Hash, (byte)HashOperation.HLEN) => HashLength(count, ptr, ref storageApi),
(RespCommand.Hash, (byte)HashOperation.HSTRLEN) => HashStrLength(count, ptr, ref storageApi),
(RespCommand.Hash, (byte)HashOperation.HEXISTS) => HashExists(count, ptr, ref storageApi),
(RespCommand.Hash, (byte)HashOperation.HKEYS) => HashKeys(count, ptr, HashOperation.HKEYS, ref storageApi),
(RespCommand.Hash, (byte)HashOperation.HVALS) => HashKeys(count, ptr, HashOperation.HVALS, ref storageApi),
Expand Down
13 changes: 13 additions & 0 deletions libs/server/Storage/Session/ObjectStore/HashOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,19 @@ public GarnetStatus HashLength<TObjectContext>(byte[] key, ArgSlice input, out O
where TObjectContext : ITsavoriteContext<byte[], IGarnetObject, SpanByte, GarnetObjectStoreOutput, long>
=> ReadObjectStoreOperation(key, input, out output, ref objectStoreContext);

/// <summary>
/// Returns the string length of the value associated with field in the hash stored at key. If the key or the field do not exist, 0 is returned.
/// </summary>
/// <param name="key"></param>
/// <param name="input"></param>
/// <param name="output"></param>
/// <param name="objectStoreContext"></param>
/// <typeparam name="TObjectContext"></typeparam>
/// <returns></returns>
public GarnetStatus HashStrLength<TObjectContext>(byte[] key, ArgSlice input, out ObjectOutputHeader output, ref TObjectContext objectStoreContext)
where TObjectContext : ITsavoriteContext<byte[], IGarnetObject, SpanByte, GarnetObjectStoreOutput, long>
=> ReadObjectStoreOperation(key, input, out output, ref objectStoreContext);

/// <summary>
/// Removes the specified fields from the hash key.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions libs/server/Transaction/TxnKeyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ private int HashObjectKeys(byte subCommand)
(byte)HashOperation.HINCRBYFLOAT => SingleKey(1, true, LockType.Exclusive),
(byte)HashOperation.HSETNX => SingleKey(1, true, LockType.Exclusive),
(byte)HashOperation.HRANDFIELD => SingleKey(1, true, LockType.Shared),
(byte)HashOperation.HSTRLEN => SingleKey(1, true, LockType.Shared),
_ => -1
};
}
Expand Down
52 changes: 52 additions & 0 deletions test/Garnet.test/RespHashTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ public void CanDoHExists()
Assert.AreEqual(false, result);
}

[Test]
public void CanDoHStrLen()
{
using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
var db = redis.GetDatabase(0);
db.HashSet("user.user1", new HashEntry[] { new HashEntry("Title", "Tsavorite") });
long r = db.HashStringLength("user.user1", "Title");
Assert.AreEqual(9, r, 0);
r = db.HashStringLength("user.user1", "NoExist");
Assert.AreEqual(0, r, 0);
r = db.HashStringLength("user.user2", "Title");
Assert.AreEqual(0, r, 0);
}

[Test]
public void CanDoHKeys()
Expand Down Expand Up @@ -648,6 +661,45 @@ public void CanDoHExistsLC()
Assert.AreEqual(expectedResponse, actualValue);
}

[Test]
public void CanDoHStrLenLC()
{
using var lightClientRequest = TestUtils.CreateRequest();
var response = lightClientRequest.SendCommand("HSET myhash field1 myvalue");
var expectedResponse = ":1\r\n";
var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, actualValue);

// get an existing field
response = lightClientRequest.SendCommand("HSTRLEN myhash field1");
expectedResponse = ":7\r\n";
actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, actualValue);

// get an nonexisting field
response = lightClientRequest.SendCommand("HSTRLEN myhash field0");
expectedResponse = ":0\r\n";
actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, actualValue);

//non existing hash
response = lightClientRequest.SendCommand("HSTRLEN foo field0");
expectedResponse = ":0\r\n";
actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, actualValue);

//missing paramenters
response = lightClientRequest.SendCommand("HSTRLEN foo");
expectedResponse = "-ERR wrong number of arguments for HSTRLEN command.\r\n";
actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, actualValue);

//too many paramenters
response = lightClientRequest.SendCommand("HSTRLEN foo field0 field1");
expectedResponse = "-ERR wrong number of arguments for HSTRLEN command.\r\n";
actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length);
Assert.AreEqual(expectedResponse, actualValue);
}

[Test]
public void CanDoIncrByLC()
Expand Down
2 changes: 1 addition & 1 deletion website/docs/commands/api-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Note that this list is subject to change as we continue to expand our API comman
| | [HSCAN](data-structures.md#hscan) || |
| | [HSET](data-structures.md#hset) || |
| | [HSETNX](data-structures.md#hsetnx) || |
| | HSTRLEN | | |
| | [HSTRLEN](data-structures.md#hstrlen) | | |
| | [HVALS](data-structures.md#hvals) || |
| <span id="hyperloglog">**HYPERLOGLOG**</span> | [PFADD](analytics.md#pfadd) || |
| | [PFCOUNT](analytics.md#pfcount) || |
Expand Down
12 changes: 12 additions & 0 deletions website/docs/commands/data-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ Sets field in the hash stored at **key** to value, only if field does not yet ex
---
### HSTRLEN
#### Syntax
```bash
HSTRLEN key field
```
Returns the string length of the value associated with **field** in the hash stored at **key**. If the **key** or the **field** do not exist, 0 is returned.
---
### HVALS
#### Syntax
Expand Down

0 comments on commit 439e312

Please sign in to comment.