From 2882c81eebb1646180e1296c6ba12dcbf641d2b5 Mon Sep 17 00:00:00 2001 From: Edric Cuartero Date: Tue, 17 Dec 2024 15:12:36 +0800 Subject: [PATCH] Go: Implement Type, Touch and Unlink commands (#2756) * Go: Implement Type, Touch and Unlink command Signed-off-by: EdricCua --- go/api/base_client.go | 26 +++ go/api/generic_commands.go | 72 ++++++ go/integTest/shared_commands_test.go | 318 ++++++++++++++++----------- 3 files changed, 285 insertions(+), 131 deletions(-) diff --git a/go/api/base_client.go b/go/api/base_client.go index 150257f15f..ba83dedd95 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -1129,3 +1129,29 @@ func (client *baseClient) PTTL(key string) (Result[int64], error) { return handleLongResponse(result) } + +func (client *baseClient) Unlink(keys []string) (Result[int64], error) { + result, err := client.executeCommand(C.Unlink, keys) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} + +func (client *baseClient) Type(key string) (Result[string], error) { + result, err := client.executeCommand(C.Type, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNullResponse(result) +} + +func (client *baseClient) Touch(keys []string) (Result[int64], error) { + result, err := client.executeCommand(C.Touch, keys) + if err != nil { + return CreateNilInt64Result(), err + } + + return handleLongResponse(result) +} diff --git a/go/api/generic_commands.go b/go/api/generic_commands.go index 090442ec68..5978ce0c25 100644 --- a/go/api/generic_commands.go +++ b/go/api/generic_commands.go @@ -313,4 +313,76 @@ type GenericBaseCommands interface { // // [valkey.io]: https://valkey.io/commands/pttl/ PTTL(key string) (Result[int64], error) + + // Unlink (delete) multiple keys from the database. A key is ignored if it does not exist. + // This command, similar to Del However, this command does not block the server + // + // Note: + // In cluster mode, if keys in keys map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // keys - One or more keys to unlink. + // + // Return value: + // Return the number of keys that were unlinked. + // + // Example: + // result, err := client.Unlink([]string{"key1", "key2", "key3"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: 3 + // + // [valkey.io]: Https://valkey.io/commands/unlink/ + Unlink(keys []string) (Result[int64], error) + + // Alters the last access time of a key(s). A key is ignored if it does not exist. + // + // Note: + // In cluster mode, if keys in keys map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // The keys to update last access time. + // + // Return value: + // The number of keys that were updated. + // + // Example: + // result, err := client.Touch([]string{"key1", "key2", "key3"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: 3 + // + // [valkey.io]: Https://valkey.io/commands/touch/ + Touch(keys []string) (Result[int64], error) + + // Type returns the string representation of the type of the value stored at key. + // The different types that can be returned are: string, list, set, zset, hash and stream. + // + // Parameters: + // key - string + // + // Return value: + // If the key exists, the type of the stored value is returned. Otherwise, a none" string is returned. + // + // Example: + // result, err := client.Type([]string{"key"}) + // if err != nil { + // // handle error + // } + // fmt.Println(result.Value()) // Output: string + // + // [valkey.io]: Https://valkey.io/commands/type/ + Type(key string) (Result[string], error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 2e78819db5..46e4c86133 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -2675,137 +2675,6 @@ func (suite *GlideTestSuite) TestLMove() { }) } -func (suite *GlideTestSuite) TestBLMove() { - if suite.serverVersion < "6.2.0" { - suite.T().Skip("This feature is added in version 6.2.0") - } - suite.runWithDefaultClients(func(client api.BaseClient) { - key1 := "{key}-1" + uuid.NewString() - key2 := "{key}-2" + uuid.NewString() - nonExistentKey := "{key}-3" + uuid.NewString() - nonListKey := "{key}-4" + uuid.NewString() - - res1, err := client.BLMove(key1, key2, api.Left, api.Right, float64(0.1)) - assert.Equal(suite.T(), api.CreateNilStringResult(), res1) - assert.Nil(suite.T(), err) - - res2, err := client.LPush(key1, []string{"four", "three", "two", "one"}) - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), int64(4), res2.Value()) - - // only source exists, only source elements gets popped, creates a list at nonExistingKey - res3, err := client.BLMove(key1, nonExistentKey, api.Right, api.Left, float64(0.1)) - assert.Equal(suite.T(), "four", res3.Value()) - assert.Nil(suite.T(), err) - - res4, err := client.LRange(key1, int64(0), int64(-1)) - assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res4, - ) - - // source and destination are the same, performing list rotation, "one" gets popped and added back - res5, err := client.BLMove(key1, key1, api.Left, api.Left, float64(0.1)) - assert.Equal(suite.T(), "one", res5.Value()) - assert.Nil(suite.T(), err) - - res6, err := client.LRange(key1, int64(0), int64(-1)) - assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res6, - ) - // normal use case, "three" gets popped and added to the left of destination - res7, err := client.LPush(key2, []string{"six", "five", "four"}) - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), int64(3), res7.Value()) - - res8, err := client.BLMove(key1, key2, api.Right, api.Left, float64(0.1)) - assert.Equal(suite.T(), "three", res8.Value()) - assert.Nil(suite.T(), err) - - res9, err := client.LRange(key1, int64(0), int64(-1)) - assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - }, - res9, - ) - res10, err := client.LRange(key2, int64(0), int64(-1)) - assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("three"), - api.CreateStringResult("four"), - api.CreateStringResult("five"), - api.CreateStringResult("six"), - }, - res10, - ) - - // source exists but is not a list type key - suite.verifyOK(client.Set(nonListKey, "value")) - - res11, err := client.BLMove(nonListKey, key1, api.Left, api.Left, float64(0.1)) - assert.Equal(suite.T(), api.CreateNilStringResult(), res11) - assert.NotNil(suite.T(), err) - assert.IsType(suite.T(), &api.RequestError{}, err) - - // destination exists but is not a list type key - suite.verifyOK(client.Set(nonListKey, "value")) - - res12, err := client.BLMove(key1, nonListKey, api.Left, api.Left, float64(0.1)) - assert.Equal(suite.T(), api.CreateNilStringResult(), res12) - assert.NotNil(suite.T(), err) - assert.IsType(suite.T(), &api.RequestError{}, err) - }) -} - -func (suite *GlideTestSuite) TestDel_MultipleKeys() { - suite.runWithDefaultClients(func(client api.BaseClient) { - key1 := "testKey1_" + uuid.New().String() - key2 := "testKey2_" + uuid.New().String() - key3 := "testKey3_" + uuid.New().String() - - suite.verifyOK(client.Set(key1, initialValue)) - suite.verifyOK(client.Set(key2, initialValue)) - suite.verifyOK(client.Set(key3, initialValue)) - - deletedCount, err := client.Del([]string{key1, key2, key3}) - - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), int64(3), deletedCount.Value()) - - result1, err1 := client.Get(key1) - result2, err2 := client.Get(key2) - result3, err3 := client.Get(key3) - - assert.Nil(suite.T(), err1) - assert.True(suite.T(), result1.IsNil()) - - assert.Nil(suite.T(), err2) - assert.True(suite.T(), result2.IsNil()) - - assert.Nil(suite.T(), err3) - assert.True(suite.T(), result3.IsNil()) - }) -} - func (suite *GlideTestSuite) TestExists() { suite.runWithDefaultClients(func(client api.BaseClient) { key := uuid.New().String() @@ -3448,3 +3317,190 @@ func (suite *GlideTestSuite) TestPTTL_WithExpiredKey() { assert.Equal(suite.T(), int64(-2), resPTTL.Value()) }) } + +func (suite *GlideTestSuite) TestBLMove() { + if suite.serverVersion < "6.2.0" { + suite.T().Skip("This feature is added in version 6.2.0") + } + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-1" + uuid.NewString() + key2 := "{key}-2" + uuid.NewString() + nonExistentKey := "{key}-3" + uuid.NewString() + nonListKey := "{key}-4" + uuid.NewString() + + res1, err := client.BLMove(key1, key2, api.Left, api.Right, float64(0.1)) + assert.Equal(suite.T(), api.CreateNilStringResult(), res1) + assert.Nil(suite.T(), err) + + res2, err := client.LPush(key1, []string{"four", "three", "two", "one"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(4), res2.Value()) + + // only source exists, only source elements gets popped, creates a list at nonExistingKey + res3, err := client.BLMove(key1, nonExistentKey, api.Right, api.Left, float64(0.1)) + assert.Equal(suite.T(), "four", res3.Value()) + assert.Nil(suite.T(), err) + + res4, err := client.LRange(key1, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + []api.Result[string]{ + api.CreateStringResult("one"), + api.CreateStringResult("two"), + api.CreateStringResult("three"), + }, + res4, + ) + + // source and destination are the same, performing list rotation, "one" gets popped and added back + res5, err := client.BLMove(key1, key1, api.Left, api.Left, float64(0.1)) + assert.Equal(suite.T(), "one", res5.Value()) + assert.Nil(suite.T(), err) + + res6, err := client.LRange(key1, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + []api.Result[string]{ + api.CreateStringResult("one"), + api.CreateStringResult("two"), + api.CreateStringResult("three"), + }, + res6, + ) + // normal use case, "three" gets popped and added to the left of destination + res7, err := client.LPush(key2, []string{"six", "five", "four"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(3), res7.Value()) + + res8, err := client.BLMove(key1, key2, api.Right, api.Left, float64(0.1)) + assert.Equal(suite.T(), "three", res8.Value()) + assert.Nil(suite.T(), err) + + res9, err := client.LRange(key1, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + []api.Result[string]{ + api.CreateStringResult("one"), + api.CreateStringResult("two"), + }, + res9, + ) + res10, err := client.LRange(key2, int64(0), int64(-1)) + assert.Nil(suite.T(), err) + assert.Equal( + suite.T(), + []api.Result[string]{ + api.CreateStringResult("three"), + api.CreateStringResult("four"), + api.CreateStringResult("five"), + api.CreateStringResult("six"), + }, + res10, + ) + + // source exists but is not a list type key + suite.verifyOK(client.Set(nonListKey, "value")) + + res11, err := client.BLMove(nonListKey, key1, api.Left, api.Left, float64(0.1)) + assert.Equal(suite.T(), api.CreateNilStringResult(), res11) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // destination exists but is not a list type key + suite.verifyOK(client.Set(nonListKey, "value")) + + res12, err := client.BLMove(key1, nonListKey, api.Left, api.Left, float64(0.1)) + assert.Equal(suite.T(), api.CreateNilStringResult(), res12) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestDel_MultipleKeys() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "testKey1_" + uuid.New().String() + key2 := "testKey2_" + uuid.New().String() + key3 := "testKey3_" + uuid.New().String() + + suite.verifyOK(client.Set(key1, initialValue)) + suite.verifyOK(client.Set(key2, initialValue)) + suite.verifyOK(client.Set(key3, initialValue)) + + deletedCount, err := client.Del([]string{key1, key2, key3}) + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(3), deletedCount.Value()) + + result1, err1 := client.Get(key1) + result2, err2 := client.Get(key2) + result3, err3 := client.Get(key3) + + assert.Nil(suite.T(), err1) + assert.True(suite.T(), result1.IsNil()) + + assert.Nil(suite.T(), err2) + assert.True(suite.T(), result2.IsNil()) + + assert.Nil(suite.T(), err3) + assert.True(suite.T(), result3.IsNil()) + }) +} + +func (suite *GlideTestSuite) TestType() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check if the value is string + keyName := "{keyName}" + uuid.NewString() + suite.verifyOK(client.Set(keyName, initialValue)) + result, err := client.Type(keyName) + assert.Nil(suite.T(), err) + assert.IsType(suite.T(), result, api.CreateStringResult("string"), "Value is string") + + // Test 2: Check if the value is list + key1 := "{keylist}-1" + uuid.NewString() + resultLPush, err := client.LPush(key1, []string{"one", "two", "three"}) + assert.Equal(suite.T(), int64(3), resultLPush.Value()) + assert.Nil(suite.T(), err) + resultType, err := client.Type(key1) + assert.Nil(suite.T(), err) + assert.IsType(suite.T(), resultType, api.CreateStringResult("list"), "Value is list") + }) +} + +func (suite *GlideTestSuite) TestTouch() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check if an touch valid key + keyName := "{keyName}" + uuid.NewString() + keyName1 := "{keyName1}" + uuid.NewString() + suite.verifyOK(client.Set(keyName, initialValue)) + suite.verifyOK(client.Set(keyName1, "anotherValue")) + result, err := client.Touch([]string{keyName, keyName1}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), result.Value(), "The touch should be 2") + + // Test 2: Check if an touch invalid key + resultInvalidKey, err := client.Touch([]string{"invalidKey", "invalidKey1"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInvalidKey.Value(), "The touch should be 0") + }) +} + +func (suite *GlideTestSuite) TestUnlink() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // Test 1: Check if an unlink valid key + keyName := "{keyName}" + uuid.NewString() + keyName1 := "{keyName1}" + uuid.NewString() + suite.verifyOK(client.Set(keyName, initialValue)) + suite.verifyOK(client.Set(keyName1, "anotherValue")) + resultValidKey, err := client.Unlink([]string{keyName, keyName1}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(2), resultValidKey.Value(), "The unlink should be 2") + + // Test 2: Check if an unlink for invalid key + resultInvalidKey, err := client.Unlink([]string{"invalidKey2", "invalidKey3"}) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInvalidKey.Value(), "The unlink should be 0") + }) +}