diff --git a/CHANGELOG.md b/CHANGELOG.md index 951db65fda..d794567e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * Node: Fix `zrangeWithScores` (disallow `RangeByLex` as it is not supported) ([#2926](https://github.com/valkey-io/valkey-glide/pull/2926)) * Core: improve fix in #2381 ([#2929](https://github.com/valkey-io/valkey-glide/pull/2929)) +* Java: Fix `lpopCount` null handling ([#3025](https://github.com/valkey-io/valkey-glide/pull/3025)) #### Operational Enhancements diff --git a/go/api/base_client.go b/go/api/base_client.go index 294c50d619..8de1b86b49 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -31,7 +31,6 @@ type BaseClient interface { SetCommands StreamCommands SortedSetCommands - ConnectionManagementCommands HyperLogLogCommands GenericBaseCommands BitmapCommands @@ -3104,52 +3103,6 @@ func (client *baseClient) BLMove( return handleStringOrNilResponse(result) } -// Pings the server. -// -// Return value: -// -// Returns "PONG". -// -// For example: -// -// result, err := client.Ping() -// -// [valkey.io]: https://valkey.io/commands/ping/ -func (client *baseClient) Ping() (string, error) { - result, err := client.executeCommand(C.Ping, []string{}) - if err != nil { - return defaultStringResponse, err - } - - return handleStringResponse(result) -} - -// Pings the server with a custom message. -// -// Parameters: -// -// message - A message to include in the `PING` command. -// -// Return value: -// -// Returns the copy of message. -// -// For example: -// -// result, err := client.PingWithMessage("Hello") -// -// [valkey.io]: https://valkey.io/commands/ping/ -func (client *baseClient) PingWithMessage(message string) (string, error) { - args := []string{message} - - result, err := client.executeCommand(C.Ping, args) - if err != nil { - return defaultStringResponse, err - } - - return handleStringResponse(result) -} - // Del removes the specified keys from the database. A key is ignored if it does not exist. // // Note: @@ -5595,34 +5548,6 @@ func (client *baseClient) ObjectEncoding(key string) (Result[string], error) { return handleStringOrNilResponse(result) } -// Echo the provided message back. -// The command will be routed a random node. -// -// Parameters: -// -// message - The provided message. -// -// Return value: -// -// The provided message -// -// For example: -// -// result, err := client.Echo("Hello World") -// if err != nil { -// // handle error -// } -// fmt.Println(result.Value()) // Output: Hello World -// -// [valkey.io]: https://valkey.io/commands/echo/ -func (client *baseClient) Echo(message string) (Result[string], error) { - result, err := client.executeCommand(C.Echo, []string{message}) - if err != nil { - return CreateNilStringResult(), err - } - return handleStringOrNilResponse(result) -} - // Destroys the consumer group `group` for the stream stored at `key`. // // See [valkey.io] for details. diff --git a/go/api/connection_management_cluster_commands.go b/go/api/connection_management_cluster_commands.go new file mode 100644 index 0000000000..61d524d1b9 --- /dev/null +++ b/go/api/connection_management_cluster_commands.go @@ -0,0 +1,16 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + +// Supports commands and transactions for the "Connection Management" group of commands for cluster client. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#connection +type ConnectionManagementClusterCommands interface { + Ping() (string, error) + + PingWithOptions(pingOptions options.ClusterPingOptions) (string, error) +} diff --git a/go/api/connection_management_commands.go b/go/api/connection_management_commands.go index 480b85af91..ba9a77ff8b 100644 --- a/go/api/connection_management_commands.go +++ b/go/api/connection_management_commands.go @@ -2,6 +2,8 @@ package api +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + // Supports commands and transactions for the "Connection Management" group of commands for standalone client. // // See [valkey.io] for details. @@ -10,7 +12,7 @@ package api type ConnectionManagementCommands interface { Ping() (string, error) - PingWithMessage(message string) (string, error) + PingWithOptions(pingOptions options.PingOptions) (string, error) Echo(message string) (Result[string], error) } diff --git a/go/api/generic_cluster_commands.go b/go/api/generic_cluster_commands.go index 3d5186caa3..44b656f885 100644 --- a/go/api/generic_cluster_commands.go +++ b/go/api/generic_cluster_commands.go @@ -2,6 +2,8 @@ package api +import "github.com/valkey-io/valkey-glide/go/glide/api/config" + // GenericClusterCommands supports commands for the "Generic Commands" group for cluster client. // // See [valkey.io] for details. @@ -9,4 +11,6 @@ package api // [valkey.io]: https://valkey.io/commands/#generic type GenericClusterCommands interface { CustomCommand(args []string) (ClusterValue[interface{}], error) + + CustomCommandWithRoute(args []string, route config.Route) (ClusterValue[interface{}], error) } diff --git a/go/api/glide_client.go b/go/api/glide_client.go index cefda9b8d2..6fa46fefa5 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -7,6 +7,7 @@ package api import "C" import ( + "github.com/valkey-io/valkey-glide/go/glide/api/options" "github.com/valkey-io/valkey-glide/go/glide/utils" ) @@ -19,6 +20,7 @@ type GlideClientCommands interface { GenericCommands ServerManagementCommands BitmapCommands + ConnectionManagementCommands } // GlideClient implements standalone mode operations by extending baseClient functionality. @@ -225,3 +227,72 @@ func (client *GlideClient) DBSize() (int64, error) { } return handleIntResponse(result) } + +// Echo the provided message back. +// The command will be routed a random node. +// +// Parameters: +// +// message - The provided message. +// +// Return value: +// +// The provided message +// +// For example: +// +// result, err := client.Echo("Hello World") +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: Hello World +// +// [valkey.io]: https://valkey.io/commands/echo/ +func (client *GlideClient) Echo(message string) (Result[string], error) { + result, err := client.executeCommand(C.Echo, []string{message}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Pings the server. +// +// Return value: +// +// Returns "PONG". +// +// For example: +// +// result, err := client.Ping() +// fmt.Println(result) // Output: PONG +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClient) Ping() (string, error) { + return client.PingWithOptions(options.PingOptions{}) +} + +// Pings the server. +// +// Parameters: +// +// pingOptions - The PingOptions type. +// +// Return value: +// +// Returns the copy of message. +// +// For example: +// +// options := options.NewPingOptionsBuilder().SetMessage("hello") +// result, err := client.PingWithOptions(options) +// result: "hello" +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClient) PingWithOptions(pingOptions options.PingOptions) (string, error) { + result, err := client.executeCommand(C.Ping, pingOptions.ToArgs()) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} diff --git a/go/api/glide_cluster_client.go b/go/api/glide_cluster_client.go index 59f9c589f1..eb967bced1 100644 --- a/go/api/glide_cluster_client.go +++ b/go/api/glide_cluster_client.go @@ -7,6 +7,7 @@ package api import "C" import ( + "github.com/valkey-io/valkey-glide/go/glide/api/config" "github.com/valkey-io/valkey-glide/go/glide/api/options" ) @@ -18,6 +19,7 @@ type GlideClusterClientCommands interface { BaseClient GenericClusterCommands ServerManagementClusterCommands + ConnectionManagementClusterCommands } // GlideClusterClient implements cluster mode operations by extending baseClient functionality. @@ -165,6 +167,106 @@ func (client *GlideClusterClient) InfoWithOptions(options ClusterInfoOptions) (C return createClusterSingleValue[string](data), nil } +// CustomCommandWithRoute executes a single command, specified by args, without checking inputs. Every part of the command, +// including the command name and subcommands, should be added as a separate value in args. The returning value depends on +// the executed command. +// +// See [Valkey GLIDE Wiki] for details on the restrictions and limitations of the custom command API. +// +// Parameters: +// +// args - Arguments for the custom command including the command name. +// route - Specifies the routing configuration for the command. The client will route the +// command to the nodes defined by route. +// +// Return value: +// +// The returning value depends on the executed command and route. +// +// For example: +// +// route := config.SimpleNodeRoute(config.RandomRoute) +// result, err := client.CustomCommandWithRoute([]string{"ping"}, route) +// result.SingleValue().(string): "PONG" +// +// [Valkey GLIDE Wiki]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command +func (client *GlideClusterClient) CustomCommandWithRoute( + args []string, + route config.Route, +) (ClusterValue[interface{}], error) { + res, err := client.executeCommandWithRoute(C.CustomCommand, args, route) + if err != nil { + return createEmptyClusterValue[interface{}](), err + } + data, err := handleInterfaceResponse(res) + if err != nil { + return createEmptyClusterValue[interface{}](), err + } + return createClusterValue[interface{}](data), nil +} + +// Pings the server. +// The command will be routed to all primary nodes. +// +// Return value: +// +// Returns "PONG". +// +// For example: +// +// result, err := clusterClient.Ping() +// fmt.Println(result) // Output: PONG +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClusterClient) Ping() (string, error) { + result, err := client.executeCommand(C.Ping, []string{}) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} + +// Pings the server. +// The command will be routed to all primary nodes, unless `Route` is provided in `pingOptions`. +// +// Parameters: +// +// pingOptions - The PingOptions type. +// +// Return value: +// +// Returns the copy of message. +// +// For example: +// +// route := config.Route(config.RandomRoute) +// opts := options.ClusterPingOptions{ +// PingOptions: &options.PingOptions{ +// Message: "Hello", +// }, +// Route: &route, +// } +// result, err := clusterClient.PingWithOptions(opts) +// fmt.Println(result) // Output: Hello +// +// [valkey.io]: https://valkey.io/commands/ping/ +func (client *GlideClusterClient) PingWithOptions(pingOptions options.ClusterPingOptions) (string, error) { + if pingOptions.Route == nil { + response, err := client.executeCommand(C.Ping, pingOptions.ToArgs()) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(response) + } + + response, err := client.executeCommandWithRoute(C.Ping, pingOptions.ToArgs(), *pingOptions.Route) + if err != nil { + return defaultStringResponse, err + } + + return handleStringResponse(response) +} + // Returns the server time. // The command will be routed to a random node, unless Route in opts is provided. // diff --git a/go/api/options/ping_options.go b/go/api/options/ping_options.go new file mode 100644 index 0000000000..dc5c527ff9 --- /dev/null +++ b/go/api/options/ping_options.go @@ -0,0 +1,31 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/api/config" +) + +// Optional arguments for `Ping` for standalone client +type PingOptions struct { + Message string +} + +// Optional arguments for `Ping` for cluster client +type ClusterPingOptions struct { + *PingOptions + // Specifies the routing configuration for the command. + // The client will route the command to the nodes defined by *Route*. + Route *config.Route +} + +func (opts *PingOptions) ToArgs() []string { + if opts == nil { + return []string{} + } + args := []string{} + if opts.Message != "" { + args = append(args, opts.Message) + } + return args +} diff --git a/go/integTest/cluster_commands_test.go b/go/integTest/cluster_commands_test.go index 34c683a355..52bca71abb 100644 --- a/go/integTest/cluster_commands_test.go +++ b/go/integTest/cluster_commands_test.go @@ -108,6 +108,85 @@ func (suite *GlideTestSuite) TestInfoCluster() { } } +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_Info() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.AllPrimaries) + result, err := client.CustomCommandWithRoute([]string{"INFO"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsMultiValue()) + multiValue := result.MultiValue() + for _, value := range multiValue { + assert.True(suite.T(), strings.Contains(value.(string), "# Stats")) + } +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_Echo() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.RandomRoute) + result, err := client.CustomCommandWithRoute([]string{"ECHO", "GO GLIDE GO"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsSingleValue()) + assert.Equal(suite.T(), "GO GLIDE GO", result.SingleValue().(string)) +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_InvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.NewByAddressRoute("invalidHost", 9999) + result, err := client.CustomCommandWithRoute([]string{"PING"}, invalidRoute) + assert.NotNil(suite.T(), err) + assert.True(suite.T(), result.IsEmpty()) +} + +func (suite *GlideTestSuite) TestClusterCustomCommandWithRoute_AllNodes() { + client := suite.defaultClusterClient() + route := config.SimpleNodeRoute(config.AllNodes) + result, err := client.CustomCommandWithRoute([]string{"PING"}, route) + assert.Nil(suite.T(), err) + assert.True(suite.T(), result.IsSingleValue()) + assert.Equal(suite.T(), "PONG", result.SingleValue()) +} + +func (suite *GlideTestSuite) TestPingWithOptions_NoRoute() { + client := suite.defaultClusterClient() + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: nil, + } + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_WithRoute() { + client := suite.defaultClusterClient() + route := config.Route(config.AllNodes) + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: &route, + } + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_InvalidRoute() { + client := suite.defaultClusterClient() + invalidRoute := config.Route(config.NewByAddressRoute("invalidHost", 9999)) + options := options.ClusterPingOptions{ + PingOptions: &options.PingOptions{ + Message: "hello", + }, + Route: &invalidRoute, + } + result, err := client.PingWithOptions(options) + assert.NotNil(suite.T(), err) + assert.Empty(suite.T(), result) +} + func (suite *GlideTestSuite) TestTimeWithoutRoute() { client := suite.defaultClusterClient() options := options.RouteOption{Route: nil} diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index b2fd7eacd4..e6cece6883 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -603,23 +603,6 @@ func (suite *GlideTestSuite) TestGetDel_EmptyKey() { }) } -func (suite *GlideTestSuite) TestPing_NoArgument() { - suite.runWithDefaultClients(func(client api.BaseClient) { - result, err := client.Ping() - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), "PONG", result) - }) -} - -func (suite *GlideTestSuite) TestPing_WithArgument() { - suite.runWithDefaultClients(func(client api.BaseClient) { - // Passing "Hello" as the message - result, err := client.PingWithMessage("Hello") - assert.Nil(suite.T(), err) - assert.Equal(suite.T(), "Hello", result) - }) -} - func (suite *GlideTestSuite) TestHSet_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { fields := map[string]string{"field1": "value1", "field2": "value2"} @@ -6223,17 +6206,6 @@ func (suite *GlideTestSuite) TestRestoreWithOptions() { }) } -func (suite *GlideTestSuite) TestEcho() { - suite.runWithDefaultClients(func(client api.BaseClient) { - // Test 1: Check if Echo command return the message - value := "Hello world" - t := suite.T() - resultEcho, err := client.Echo(value) - assert.Nil(t, err) - assert.Equal(t, value, resultEcho.Value()) - }) -} - func (suite *GlideTestSuite) TestZRemRangeByRank() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := uuid.New().String() diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 35127a7eea..792627422d 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -432,6 +432,60 @@ func (suite *GlideTestSuite) TestDBSize() { assert.Greater(suite.T(), result, int64(0)) } +func (suite *GlideTestSuite) TestPing_NoArgument() { + client := suite.defaultClient() + + result, err := client.Ping() + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "PONG", result) +} + +func (suite *GlideTestSuite) TestEcho() { + client := suite.defaultClient() + // Test 1: Check if Echo command return the message + value := "Hello world" + t := suite.T() + resultEcho, err := client.Echo(value) + assert.Nil(t, err) + assert.Equal(t, value, resultEcho.Value()) +} + +func (suite *GlideTestSuite) TestPing_ClosedClient() { + client := suite.defaultClient() + client.Close() + + result, err := client.Ping() + + assert.NotNil(suite.T(), err) + assert.Equal(suite.T(), "", result) + assert.IsType(suite.T(), &errors.ClosingError{}, err) +} + +func (suite *GlideTestSuite) TestPingWithOptions_WithMessage() { + client := suite.defaultClient() + options := options.PingOptions{ + Message: "hello", + } + + result, err := client.PingWithOptions(options) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "hello", result) +} + +func (suite *GlideTestSuite) TestPingWithOptions_ClosedClient() { + client := suite.defaultClient() + client.Close() + + options := options.PingOptions{ + Message: "hello", + } + + result, err := client.PingWithOptions(options) + assert.NotNil(suite.T(), err) + assert.Equal(suite.T(), "", result) + assert.IsType(suite.T(), &errors.ClosingError{}, err) +} + func (suite *GlideTestSuite) TestTime_Success() { client := suite.defaultClient() results, err := client.Time() diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 6039f84e8a..45e16d0107 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1384,7 +1384,7 @@ public CompletableFuture lpopCount(@NonNull String key, long count) { return commandManager.submitNewCommand( LPop, new String[] {key, Long.toString(count)}, - response -> castArray(handleArrayResponse(response), String.class)); + response -> castArray(handleArrayOrNullResponse(response), String.class)); } @Override @@ -1392,7 +1392,7 @@ public CompletableFuture lpopCount(@NonNull GlideString key, long return commandManager.submitNewCommand( LPop, new GlideString[] {key, gs(Long.toString(count))}, - response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); } @Override diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index e58e3b5180..341ee307b3 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -1695,6 +1695,7 @@ public void lpush_lpop_lrange_existing_non_existing_key(BaseClient client) { assertArrayEquals(new String[] {"value2", "value3"}, client.lpopCount(key, 2).get()); assertArrayEquals(new String[] {}, client.lrange("non_existing_key", 0, -1).get()); assertNull(client.lpop("non_existing_key").get()); + assertNull(client.lpopCount("non_existing_key", 2).get()); } @SneakyThrows @@ -1714,6 +1715,7 @@ public void lpush_lpop_lrange_binary_existing_non_existing_key(BaseClient client new GlideString[] {gs("value2"), gs("value3")}, client.lpopCount(key, 2).get()); assertArrayEquals(new GlideString[] {}, client.lrange(gs("non_existing_key"), 0, -1).get()); assertNull(client.lpop(gs("non_existing_key")).get()); + assertNull(client.lpopCount(gs("non_existing_key"), 2).get()); } @SneakyThrows diff --git a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java index 64d1faeed3..7a59533931 100644 --- a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java +++ b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java @@ -23,8 +23,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; @Timeout(10) // seconds public class StandaloneClientTests {