diff --git a/libs/server/API/GarnetApi.cs b/libs/server/API/GarnetApi.cs
index 9d1c0cf817..6c17967e17 100644
--- a/libs/server/API/GarnetApi.cs
+++ b/libs/server/API/GarnetApi.cs
@@ -127,8 +127,8 @@ public GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input)
=> storageSession.SET_Conditional(ref key, ref input, ref context);
///
- public GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, RespCommand cmd)
- => storageSession.SET_Conditional(ref key, ref input, ref output, ref context, cmd);
+ public GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output)
+ => storageSession.SET_Conditional(ref key, ref input, ref output, ref context);
///
public GarnetStatus SET(ArgSlice key, Memory value)
diff --git a/libs/server/API/GarnetStatus.cs b/libs/server/API/GarnetStatus.cs
index 3e9c26eb02..0972da6fb6 100644
--- a/libs/server/API/GarnetStatus.cs
+++ b/libs/server/API/GarnetStatus.cs
@@ -24,9 +24,5 @@ public enum GarnetStatus : byte
/// Wrong type
///
WRONGTYPE,
- ///
- /// ETAG mismatch result for an etag based command
- ///
- ETAGMISMATCH,
}
}
\ No newline at end of file
diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs
index 82c9de2106..207111178c 100644
--- a/libs/server/API/IGarnetApi.cs
+++ b/libs/server/API/IGarnetApi.cs
@@ -41,7 +41,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
///
/// SET Conditional
///
- GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, RespCommand cmd);
+ GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output);
///
/// SET
diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs
index 8b81ce30d6..3422e88ebb 100644
--- a/libs/server/Resp/BasicCommands.cs
+++ b/libs/server/Resp/BasicCommands.cs
@@ -386,11 +386,96 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi)
private bool NetworkSETIFMATCH(ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
- Debug.Assert(parseState.Count == 3);
+ if (parseState.Count < 3 || parseState.Count > 5)
+ {
+ return AbortWithWrongNumberOfArguments(nameof(RespCommand.SETIFMATCH));
+ }
- var key = parseState.GetArgSliceByRef(0).SpanByte;
+ // SETIFMATCH Args: KEY VAL ETAG -> [ ((EX || PX) expiration)]
+ int expiry = 0;
+ ReadOnlySpan errorMessage = default;
+ var expOption = ExpirationOption.None;
+
+ var tokenIdx = 3;
+ Span nextOpt = default;
+ var optUpperCased = false;
+ while (tokenIdx < parseState.Count || optUpperCased)
+ {
+ if (!optUpperCased)
+ {
+ nextOpt = parseState.GetArgSliceByRef(tokenIdx++).Span;
+ }
- NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, ref key, getValue: true, highPrecision: false, withEtag: true, ref storageApi);
+ if (nextOpt.SequenceEqual(CmdStrings.EX))
+ {
+ // Validate expiry
+ if (!parseState.TryGetInt(tokenIdx++, out expiry))
+ {
+ errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER;
+ break;
+ }
+
+ if (expOption != ExpirationOption.None)
+ {
+ errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR;
+ break;
+ }
+
+ expOption = ExpirationOption.EX;
+ if (expiry <= 0)
+ {
+ errorMessage = CmdStrings.RESP_ERR_GENERIC_INVALIDEXP_IN_SET;
+ break;
+ }
+ }
+ else if (nextOpt.SequenceEqual(CmdStrings.PX))
+ {
+ // Validate expiry
+ if (!parseState.TryGetInt(tokenIdx++, out expiry))
+ {
+ errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER;
+ break;
+ }
+
+ if (expOption != ExpirationOption.None)
+ {
+ errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR;
+ break;
+ }
+
+ expOption = ExpirationOption.PX;
+ if (expiry <= 0)
+ {
+ errorMessage = CmdStrings.RESP_ERR_GENERIC_INVALIDEXP_IN_SET;
+ break;
+ }
+ }
+ else
+ {
+ if (!optUpperCased)
+ {
+ AsciiUtils.ToUpperInPlace(nextOpt);
+ optUpperCased = true;
+ continue;
+ }
+
+ errorMessage = CmdStrings.RESP_ERR_GENERIC_UNK_CMD;
+ break;
+ }
+
+ optUpperCased = false;
+ }
+
+ if (!errorMessage.IsEmpty)
+ {
+ while (!RespWriteUtils.WriteError(errorMessage, ref dcurr, dend))
+ SendAndReset();
+ return true;
+ }
+
+ SpanByte key = parseState.GetArgSliceByRef(0).SpanByte;
+
+ NetworkSET_Conditional(RespCommand.SETIFMATCH, expiry, ref key, getValue: true, highPrecision: expOption == ExpirationOption.PX, withEtag: true, ref storageApi);
return true;
}
@@ -797,7 +882,7 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref
// anything with getValue or withEtag may choose to write to the buffer in success scenarios
outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr));
status = storageApi.SET_Conditional(ref key,
- ref input, ref outputBuffer, cmd);
+ ref input, ref outputBuffer);
}
else
{
@@ -809,13 +894,8 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref
switch ((getValue, withEtag, cmd, status))
{
- case (_, _, RespCommand.SETIFMATCH, GarnetStatus.ETAGMISMATCH): // write back mismatch error
- while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ETAGMISMTACH, ref dcurr, dend))
- SendAndReset();
- break;
-
// since SET with etag goes down RMW a not found is okay and data is on buffer
- case (_, true, RespCommand.SET, GarnetStatus.NOTFOUND):
+ case (_, true, RespCommand.SET, GarnetStatus.NOTFOUND):
// if getvalue || etag and Status is OK then the response is always on the buffer, getvalue is never used with conditionals
// extra pattern matching on command below for invariant get value cannot be used with EXXX and EXNX
case (true, _, RespCommand.SET or RespCommand.SETIFMATCH or RespCommand.SETKEEPTTL, GarnetStatus.OK):
diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs
index af2d1a116a..0b08be5f45 100644
--- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs
+++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs
@@ -292,7 +292,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re
var cmd = input.header.cmd;
int etagIgnoredOffset = 0;
int etagIgnoredEnd = -1;
- long oldEtag = -1;
+ long oldEtag = Constants.BaseEtag;
if (recordInfo.ETag)
{
etagIgnoredOffset = Constants.EtagSize;
@@ -322,27 +322,37 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re
case RespCommand.SETIFMATCH:
long etagFromClient = input.parseState.GetLong(1);
- // No Etag is the same as having an etag of 0
- long prevEtag = recordInfo.ETag ? *(long*)value.ToPointer() : 0;
-
- if (prevEtag != etagFromClient)
+ if (oldEtag != etagFromClient)
{
- // Cancelling the operation and returning false is used to indicate ETAGMISMATCH
- rmwInfo.Action = RMWAction.CancelOperation;
- return false;
+ // write back array of the format [etag, value]
+ var valueToWrite = value.AsReadOnlySpan(etagIgnoredOffset);
+ var digitsInLenOfValue = NumUtils.NumDigitsInLong(valueToWrite.Length);
+ // *2\r\n: + + \r\n + $ + + \r\n + + \r\n
+ var numDigitsInEtag = NumUtils.NumDigitsInLong(oldEtag);
+ WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + 1 + digitsInLenOfValue + 2 + valueToWrite.Length + 2, ref valueToWrite, oldEtag, ref output, writeDirect: false);
+ return true;
}
// Need Copy update if no space for new value
var inputValue = input.parseState.GetArgSliceByRef(0);
- if (value.Length < inputValue.length + Constants.EtagSize)
+
+ // retain metadata unless metadata sent
+ int metadataSize = input.arg1 != 0 ? sizeof(long) : value.MetadataSize;
+
+ if (value.Length < inputValue.length + Constants.EtagSize + metadataSize)
return false;
+ if (input.arg1 != 0)
+ {
+ value.ExtraMetadata = input.arg1;
+ }
+
recordInfo.SetHasETag();
// Increment the ETag
- long newEtag = prevEtag + 1;
+ long newEtag = oldEtag + 1;
// Adjust value length if user shrinks it, how to get rid of spanbyte infront
- value.ShrinkSerializedLength(inputValue.Length + Constants.EtagSize);
+ value.ShrinkSerializedLength(metadataSize + inputValue.Length + Constants.EtagSize);
rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize);
rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize);
@@ -379,7 +389,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re
ArgSlice setValue = input.parseState.GetArgSliceByRef(0);
// Need CU if no space for new value
- int metadataSize = input.arg1 == 0 ? 0 : sizeof(long);
+ metadataSize = input.arg1 == 0 ? 0 : sizeof(long);
if (setValue.Length + metadataSize > value.Length - nextUpdateEtagOffset)
return false;
@@ -433,7 +443,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re
// this is the case where we have withetag option and no etag from before
nextUpdateEtagOffset = Constants.EtagSize;
nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata;
- oldEtag = Constants.BaseEtag;
}
setValue = input.parseState.GetArgSliceByRef(0);
@@ -765,20 +774,24 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB
long etagToCheckWith = input.parseState.GetLong(1);
// lack of an etag is the same as having a zero'd etag
long existingEtag;
- // No Etag is the same as having an etag of 0
+ // No Etag is the same as having the base etag
if (rmwInfo.RecordInfo.ETag)
{
existingEtag = *(long*)oldValue.ToPointer();
}
else
{
- existingEtag = 0;
+ existingEtag = Constants.BaseEtag;
}
if (existingEtag != etagToCheckWith)
{
- // cancellation and return false indicates ETag mismatch
- rmwInfo.Action = RMWAction.CancelOperation;
+ // write back array of the format [etag, value]
+ var valueToWrite = oldValue.AsReadOnlySpan(etagIgnoredOffset);
+ var digitsInLenOfValue = NumUtils.NumDigitsInLong(valueToWrite.Length);
+ // *2\r\n: + + \r\n + $ + + \r\n + \r\n
+ var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag);
+ WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + 1 + digitsInLenOfValue + 2 + valueToWrite.Length + 2, ref valueToWrite, existingEtag, ref output, writeDirect: false);
return false;
}
@@ -848,7 +861,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte
bool shouldUpdateEtag = true;
int etagIgnoredOffset = 0;
int etagIgnoredEnd = -1;
- long oldEtag = -1;
+ long oldEtag = Constants.BaseEtag;
if (recordInfo.ETag)
{
etagIgnoredEnd = oldValue.LengthWithoutMetadata;
@@ -861,24 +874,27 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte
case RespCommand.SETIFMATCH:
shouldUpdateEtag = false;
- // No Etag is the same as having an etag of 0
- if (!recordInfo.ETag)
- {
- oldEtag = 0;
- }
-
- *(long*)newValue.ToPointer() = oldEtag + 1;
-
// Copy input to value
Span dest = newValue.AsSpan(Constants.EtagSize);
ReadOnlySpan src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan;
- Debug.Assert(src.Length + Constants.EtagSize + oldValue.MetadataSize == newValue.Length);
+ // retain metadata unless metadata sent
+ int metadataSize = input.arg1 != 0 ? sizeof(long) : oldValue.MetadataSize;
+
+ Debug.Assert(src.Length + Constants.EtagSize + metadataSize == newValue.Length);
- // retain metadata
- newValue.ExtraMetadata = oldValue.ExtraMetadata;
src.CopyTo(dest);
+ newValue.ExtraMetadata = oldValue.ExtraMetadata;
+ if (input.arg1 != 0)
+ {
+ newValue.ExtraMetadata = input.arg1;
+ }
+
+ *(long*)newValue.ToPointer() = oldEtag + 1;
+
+ recordInfo.SetHasETag();
+
// Write Etag and Val back to Client
CopyRespToWithInput(ref input, ref newValue, ref output, isFromPending: false, 0, -1, hasEtagInVal: true);
break;
@@ -900,7 +916,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte
// this is the case where we have withetag option and no etag from before
nextUpdateEtagOffset = Constants.EtagSize;
nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata;
- oldEtag = 0;
recordInfo.SetHasETag();
}
@@ -914,7 +929,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte
// Copy input to value
var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan;
- int metadataSize = input.arg1 == 0 ? 0 : sizeof(long);
+ metadataSize = input.arg1 == 0 ? 0 : sizeof(long);
// new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX
Debug.Assert(newInputValue.Length + metadataSize + nextUpdateEtagOffset == newValue.Length);
@@ -947,7 +962,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte
// this is the case where we have withetag option and no etag from before
nextUpdateEtagOffset = Constants.EtagSize;
nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata;
- oldEtag = 0;
recordInfo.SetHasETag();
}
diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs
index 974817a59b..16cbab30c0 100644
--- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs
+++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs
@@ -218,8 +218,8 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b
return sizeof(int) + t.LengthWithoutMetadata;
case RespCommand.SETIFMATCH:
var newValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan;
- // always preserves the metadata and includes the etag
- return sizeof(int) + newValue.Length + Constants.EtagSize + t.MetadataSize;
+ int metadataSize = input.arg1 == 0 ? t.MetadataSize : sizeof(long);
+ return sizeof(int) + newValue.Length + Constants.EtagSize + metadataSize;
case RespCommand.EXPIRE:
case RespCommand.PEXPIRE:
case RespCommand.EXPIREAT:
@@ -250,7 +250,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b
{
var functions = functionsState.GetCustomCommandFunctions((ushort)cmd);
// compute metadata for result
- var metadataSize = input.arg1 switch
+ metadataSize = input.arg1 switch
{
-1 => 0,
0 => t.MetadataSize,
diff --git a/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs b/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs
index ac0f877754..f09d34e7c9 100644
--- a/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs
+++ b/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs
@@ -244,7 +244,7 @@ public unsafe GarnetStatus HyperLogLogMerge(ref RawStringInput input, out bool e
parseState.InitializeWithArgument(mergeSlice);
currInput.parseState = parseState;
- SET_Conditional(ref dstKey, ref currInput, ref mergeBuffer, ref currLockableContext, input.header.cmd);
+ SET_Conditional(ref dstKey, ref currInput, ref mergeBuffer, ref currLockableContext);
#endregion
}
diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs
index fe4ff370a8..0ec59bb6d9 100644
--- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs
+++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs
@@ -376,7 +376,7 @@ public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawSt
}
}
- public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, ref TContext context, RespCommand cmd)
+ public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, ref TContext context)
where TContext : ITsavoriteContext
{
var status = context.RMW(ref key, ref input, ref output);
@@ -393,11 +393,6 @@ public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawSt
incr_session_notfound();
return GarnetStatus.NOTFOUND;
}
- else if (cmd == RespCommand.SETIFMATCH && status.IsCanceled)
- {
- // The RMW operation for SETIFMATCH upon not finding the etags match between the existing record and sent etag returns Cancelled Operation
- return GarnetStatus.ETAGMISMATCH;
- }
else
{
incr_session_found();
@@ -571,7 +566,6 @@ public unsafe GarnetStatus RENAMENX(ArgSlice oldKeySlice, ArgSlice newKeySlice,
return RENAME(oldKeySlice, newKeySlice, storeType, true, out result, withEtag);
}
- // HK TODO
private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, bool isNX, out int result, bool withEtag)
{
RawStringInput input = default;
@@ -627,7 +621,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S
var expirePtrVal = (byte*)expireMemoryHandle.Pointer;
RespReadUtils.TryRead64Int(out var expireTimeMs, ref expirePtrVal, expirePtrVal + expireSpan.Length, out var _);
- input = isNX ? new RawStringInput(RespCommand.SETEXNX): new RawStringInput(RespCommand.SET);
+ input = isNX ? new RawStringInput(RespCommand.SETEXNX) : new RawStringInput(RespCommand.SET);
// If the key has an expiration, set the new key with the expiration
if (expireTimeMs > 0)
diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs
index 5a9a8ed3be..32b5138e97 100644
--- a/test/Garnet.test/RespEtagTests.cs
+++ b/test/Garnet.test/RespEtagTests.cs
@@ -54,14 +54,14 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches()
IDatabase db = redis.GetDatabase(0);
var key = "florida";
- RedisResult res = (RedisResult)db.Execute("SET", [key, "one", "WITHETAG"]);
+ RedisResult res = db.Execute("SET", [key, "one", "WITHETAG"]);
long initalEtag = long.Parse(res.ToString());
ClassicAssert.AreEqual(1, initalEtag);
- // ETAGMISMATCH test
var incorrectEtag = 1738;
- RedisResult etagMismatchMsg = db.Execute("SETIFMATCH", [key, "nextone", incorrectEtag]);
- ClassicAssert.AreEqual("ETAGMISMATCH", etagMismatchMsg.ToString());
+ RedisResult[] etagMismatchMsg = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextone", incorrectEtag]);
+ ClassicAssert.AreEqual("1", etagMismatchMsg[0].ToString());
+ ClassicAssert.AreEqual("one", etagMismatchMsg[1].ToString());
// set a bigger val
RedisResult[] setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextone", initalEtag]);
@@ -81,8 +81,9 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches()
ClassicAssert.AreEqual(value, "nextnextone");
// ETAGMISMATCH again
- etagMismatchMsg = db.Execute("SETIFMATCH", [key, "lastOne", incorrectEtag]);
- ClassicAssert.AreEqual("ETAGMISMATCH", etagMismatchMsg.ToString());
+ res = db.Execute("SETIFMATCH", [key, "lastOne", incorrectEtag]);
+ ClassicAssert.AreEqual(nextEtag.ToString(), res[0].ToString());
+ ClassicAssert.AreEqual("nextnextone", res[1].ToString());
// set a smaller val
setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "lastOne", nextEtag]);
@@ -91,6 +92,122 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches()
ClassicAssert.AreEqual(4, nextEtag);
ClassicAssert.AreEqual(value, "lastOne");
+
+ // ETAGMISMATCH on data that never had an etag
+ db.KeyDelete(key);
+ db.StringSet(key, "one");
+ res = db.Execute("SETIFMATCH", [key, "lastOne", incorrectEtag]);
+ ClassicAssert.AreEqual("0", res[0].ToString());
+ ClassicAssert.AreEqual("one", res[1].ToString());
+ }
+
+ [Test]
+ public void SetIfMatchWorksWithExpiration()
+ {
+ using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
+ IDatabase db = redis.GetDatabase(0);
+
+ var key = "florida";
+ // Scenario: Key existed before and had no expiration
+ RedisResult res = db.Execute("SET", key, "one", "WITHETAG");
+ long initalEtag = long.Parse(res.ToString());
+ ClassicAssert.AreEqual(1, initalEtag);
+
+ // expiration added
+ long updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextone", 1, "EX", 100)[0].ToString());
+ ClassicAssert.AreEqual(2, updatedEtagRes);
+
+ // confirm expiration added -> TTL should exist
+ var ttl = db.KeyTimeToLive(key);
+ ClassicAssert.IsTrue(ttl.HasValue);
+
+ updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextoneeexpretained", updatedEtagRes)[0].ToString());
+ ClassicAssert.AreEqual(3, updatedEtagRes);
+
+ // TTL should be retained
+ ttl = db.KeyTimeToLive(key);
+ ClassicAssert.IsTrue(ttl.HasValue);
+
+ db.KeyDelete(key); // cleanup
+
+ // Scenario: Key existed before and had expiration
+ res = db.Execute("SET", key, "one", "WITHETAG", "PX", 100000);
+
+ // confirm expiration added -> TTL should exist
+ ttl = db.KeyTimeToLive(key);
+ ClassicAssert.IsTrue(ttl.HasValue);
+
+ // change value and retain expiration
+ updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextone", 1)[0].ToString());
+ ClassicAssert.AreEqual(2, updatedEtagRes);
+
+ // TTL should be retained
+ ttl = db.KeyTimeToLive(key);
+ ClassicAssert.IsTrue(ttl.HasValue);
+
+ // change value and change expiration
+ updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextoneeexpretained", 2, "EX", 100)[0].ToString());
+ ClassicAssert.AreEqual(3, updatedEtagRes);
+
+ db.KeyDelete(key); // cleanup
+
+ // Scenario: SET without etag and existing expiration when sent with setifmatch will add etag and retain the expiration too
+ res = db.Execute("SET", key, "one", "EX", 100000);
+ // when no etag then count 0 as it's existing etag
+ updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextone", 0)[0].ToString());
+ ClassicAssert.AreEqual(1, updatedEtagRes);
+
+ // confirm expiration retained -> TTL should exist
+ ttl = db.KeyTimeToLive(key);
+ ClassicAssert.IsTrue(ttl.HasValue);
+
+ // confirm has etag now
+ var etag = long.Parse(db.Execute("GETWITHETAG", key)[0].ToString());
+ ClassicAssert.AreEqual(1, etag);
+
+ db.KeyDelete(key); // cleanup
+
+
+ // Scenario: SET without etag and without expiration when sent with setifmatch will add etag and retain the expiration too
+ // copy update
+ res = db.Execute("SET", key, "one");
+ // when no etag then count 0 as it's existing etag
+ updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextone", 0, "EX", 10000)[0].ToString());
+ ClassicAssert.AreEqual(1, updatedEtagRes);
+
+ // confirm expiration retained -> TTL should exist
+ ttl = db.KeyTimeToLive(key);
+ ClassicAssert.IsTrue(ttl.HasValue);
+
+ // confirm has etag now
+ etag = long.Parse(db.Execute("GETWITHETAG", key)[0].ToString());
+ ClassicAssert.AreEqual(1, etag);
+
+ // same length update
+ res = db.Execute("SET", key, "one");
+ // when no etag then count 0 as it's existing etag
+ updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "two", 0, "EX", 10000)[0].ToString());
+ ClassicAssert.AreEqual(1, updatedEtagRes);
+
+ // confirm expiration retained -> TTL should exist
+ ttl = db.KeyTimeToLive(key);
+ ClassicAssert.IsTrue(ttl.HasValue);
+
+ // confirm has etag now
+ etag = long.Parse(db.Execute("GETWITHETAG", key)[0].ToString());
+ ClassicAssert.AreEqual(1, etag);
+
+ db.KeyDelete(key); // cleanup
+
+ // Scenario: smaller length update
+ res = db.Execute("SET", key, "oneofusoneofus");
+ // when no etag then count 0 as it's existing etag
+ updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "i", 0, "EX", 10000)[0].ToString());
+ ClassicAssert.AreEqual(1, updatedEtagRes);
+
+ // confirm expiration retained -> TTL should exist
+ ttl = db.KeyTimeToLive(key);
+ ClassicAssert.IsTrue(ttl.HasValue);
}
#endregion
@@ -338,15 +455,16 @@ public void SETOnAlreadyExistingNonEtagDataOverridesIt()
[Test]
- public void SetIfMatchOnNonEtagDataReturnsEtagMismatch()
+ public void SetIfMatchOnNonEtagDataReturnsNewEtagAndValue()
{
using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
IDatabase db = redis.GetDatabase(0);
var _ = db.StringSet("h", "k");
- var res = db.Execute("SETIFMATCH", ["h", "t", "2"]);
- ClassicAssert.AreEqual("ETAGMISMATCH", res.ToString());
+ var res = (RedisResult[])db.Execute("SETIFMATCH", ["h", "t", "2"]);
+ ClassicAssert.AreEqual("0", res[0].ToString());
+ ClassicAssert.AreEqual("k", res[1].ToString());
}
[Test]