go-sync
go-sync copied to clipboard
Update module github.com/redis/go-redis/v9 to v9.7.3
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| github.com/redis/go-redis/v9 | v9.5.1 -> v9.7.3 |
Release Notes
redis/go-redis (github.com/redis/go-redis/v9)
v9.7.3
What's Changed
- fix: handle network error on SETINFO (#3295) (CVE-2025-29923)
- Deprecating misspelled
DisableIndentityflag in the client options. - Introducing
DisableIdentityflag in the client options. - Updating the documentation related to the new flag and the one that was deprecated.
Full Changelog: https://github.com/redis/go-redis/compare/v9.7.1...v9.7.3
v9.7.2
v9.7.1
Changes
- Recognize byte slice for key argument in cluster client hash slot computation (#3049)
- fix(search&aggregate):fix error overwrite and typo #3220 (#3224)
- fix: linter configuration (#3279)
- fix(search): if ft.aggregate use limit when limitoffset is zero (#3275)
- Reinstate read-only lock on hooks access in dialHook to fix data race (#3225)
- fix: flaky ClientKillByFilter test (#3268)
- chore: fix some comments (#3226)
- fix(aggregate, search): ft.aggregate bugfixes (#3263)
- fix: add unstableresp3 to cluster client (#3266)
- Fix race condition in clusterNodes.Addrs() (#3219)
- SortByWithCount FTSearchOptions fix (#3201)
- Eliminate redundant dial mutex causing unbounded connection queue contention (#3088)
- Add guidance on unstable RESP3 support for RediSearch commands to README (#3177)
🚀 New Features
- Add guidance on unstable RESP3 support for RediSearch commands to README (#3177)
🐛 Bug Fixes
- fix(search): if ft.aggregate use limit when limitoffset is zero (#3275)
- fix: add unstableresp3 to cluster client (#3266)
- fix(aggregate, search): ft.aggregate bugfixes (#3263)
- SortByWithCount FTSearchOptions fix (#3201)
- Recognize byte slice for key argument in cluster client hash slot computation (#3049)
Contributors
We'd like to thank all the contributors who worked on this release!
@ofekshenawa, @Cgol9, @LINKIWI, @shawnwgit, @zhuhaicity, @bitsark, @vladvildanov, @ndyakov
Full Changelog: https://github.com/redis/go-redis/compare/v9.7.0...v9.7.1
v9.7.0: 9.7.0
Changes
🚀 New Features
- Support Redis search and query capabilities (#2801, #3098)
- Support indexing and querying empty values (#3053)
- Support for Redis JSON with RESP2 protocol (#3146)
🛠️ Improvements
We're glad to announce that we added a search and query support in the current release.
🧰 Maintenance
- Documentation examples (#3102, #3106, #3110, #3111, #3113, #3114, #3115, #3123, #3124)
- retract v9.5.3 of redisotel and other extra packages (#3108)
- Add test coverage reporting and Codecov badge (#3055)
- Updated module version that points to retracted package version (#3074)
Contributors
We'd like to thank all the contributors who worked on this release!
@andy-stark-redis, @ipechorin, @ofekshenawa and @vladvildanov
v9.6.3
What's Changed
- fix: handle network error on SETINFO (#3295) (CVE-2025-29923)
Full Changelog: https://github.com/redis/go-redis/compare/v9.6.2...v9.6.3
v9.6.2: 9.6.2
Changes
🐛 Bug Fixes
- Fixed bug with broken TLS sessions (#3145)
Contributors
We'd like to thank all the contributors who worked on this release!
@ofekshenawa @vladvildanov @rentziass
v9.6.1: 9.6.1
Changes
9.6
This release contains all new features from version 9.6.
🚀 New Features
- Support Hash-field expiration commands (#2991)
- Support Hash-field expiration commands in Pipeline & Fix HExpire HExpireWithArgs expiration (#3038)
- Support NOVALUES parameter for HSCAN (#2925)
- Added test case for CLIENT KILL with MAXAGE option (#2971)
- Add support for XREAD last entry (#3005)
- Reduce the type assertion of CheckConn (#3066)
9.6.1
In addition minor changes were performed to retract version 9.5.3 and 9.5.4 that were released accidentally.
🧰 Maintenance
- Change CI to 7.4.0-RC2 (#3070)
🎁 Package Distribution
- Retract versions 9.5.3 and 9.5.4 (#3069)
Contributors
We'd like to thank all the contributors who worked on this release!
@LINKIWI, @b1ron, @gerzse, @haines, @immersedin, @naiqianz, @ofekshenawa, @srikar-jilugu, @tzongw, @vladvildanov, @vmihailenco and @monkey92t
v9.6.0: 9.6.0
Changes
🚀 New Features
- Support Hash-field expiration commands (#2991)
- Support Hash-field expiration commands in Pipeline & Fix HExpire HExpireWithArgs expiration (#3038)
- Support NOVALUES parameter for HSCAN (#2925)
- Added test case for CLIENT KILL with MAXAGE option (#2971)
- Add support for XREAD last entry (#3005)
- Reduce the type assertion of CheckConn (#3066)
🛠️ Improvements
This release includes support for Redis Community Edition (CE) 7.4.0, ensuring compatibility with the latest features and improvements introduced in Redis CE 7.4.0.
🧰 Maintenance
- chore(deps): bump golangci/golangci-lint-action from 4 to 6 (#2993)
- Avoid unnecessary retry delay in cluster client following MOVED and ASK redirection (#3048)
- add test for tls connCheck #3025 (#3047)
- fix node routing in slotClosestNode (#3043)
- Update pubsub.go (#3042)
- Change monitor test to run manually (#3041)
- chore(deps): bump rojopolis/spellcheck-github-actions from 0.36.0 to 0.38.0 (#3028)
- Add
(*StatusCmd).Bytes()method (#3030) - chore(deps): bump golang.org/x/net from 0.20.0 to 0.23.0 in /example/otel (#3000)
Contributors
We'd like to thank all the contributors who worked on this release!
@LINKIWI, @b1ron, @dependabot, @dependabot[bot], @gerzse, @haines, @immersedin, @naiqianz, @ofekshenawa, @srikar-jilugu, @tzongw, @vladvildanov and @vmihailenco @monkey92t
v9.5.5
What's Changed
- fix: handle network error on SETINFO (#3295) (CVE-2025-29923)
Full Changelog: https://github.com/redis/go-redis/compare/v9.5.4...v9.5.5
v9.5.4
v9.5.3
v9.5.2: 9.5.2
Changes
- fix: fix #2681 (#2998)
- Remove skipping span creation by checking parent spans (#2980)
- Handle IPv6 in isMovedError (#2981)
- Fix XGroup first pos key (#2983)
- Adding BitfieldRo in BitMapCmdable interface (#2962)
- Optimize docs useless imports and typo (#2970)
- chore: fix some comments (#2967)
- Fix for issues #2959 and #2960 (#2961)
- fix: #2956 (#2957)
- fix misuses of a vs an (#2936)
- add server address and port span attributes to redis otel trace instrumentation (#2826)
- chore(deps): bump google.golang.org/protobuf from 1.32.0 to 1.33.0 in /example/otel (#2944)
- Remove secrets from Redis Enterprise CI (#2938)
- Fix monitor on go 1.19 (#2908)
- chore(deps): bump google.golang.org/protobuf from 1.28.1 to 1.33.0 in /extra/redisprometheus (#2942)
- Change RE image to public RE image (#2935)
Contributors
We'd like to thank all the contributors who worked on this release!
@XSAM, @akash14darshan, @daviddzxy, @dependabot, @dependabot[bot], @esara, @hakusai22, @hishope, @kindknow, @monkey92t, @ofekshenawa, @singular-seal and deferdeter
Configuration
📅 Schedule: Branch creation - "* * * * 2-4" (UTC), Automerge - At any time (no schedule defined).
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
- [ ] If you want to rebase/retry this PR, check this box
This PR was generated by Mend Renovate. View the repository job log.
[puLL-Merge] - redis/[email protected]
Description
This pull request updates the go-redis library, incorporating various improvements, bug fixes, and compatibility updates. The changes span across multiple files and include updates to dependencies, code refactoring, and new feature additions.
Changes
Changes
-
.github/workflows/build.yml:- Added Go version 1.19.x to the build matrix.
-
.github/workflows/test-redis-enterprise.yml:- Updated Redis Enterprise version to 7.4.2-54.
- Replaced sensitive information with placeholder values.
-
CHANGELOG.md:- Added an entry for an unreleased version, mentioning changes to span creation behavior.
-
Makefile:- Modified the test command to skip certain tests for Go version 1.19 in example directories.
-
README.md:- Minor changes to the import statements in the example code.
-
bitmap_commands.go:- Added a new
BitFieldROcommand. - Optimized the
BitCountcommand implementation.
- Added a new
-
command.go:- Improved the
readMonitorfunction to reduce lock contention.
- Improved the
-
error.go:- Added a call to
internal.GetAddrfor better address handling.
- Added a call to
-
Various
go.modandgo.sumfiles:- Updated dependencies, including upgrading go-redis version to v9.5.3.
-
internal/util.goandinternal/util_test.go:- Added a new
GetAddrfunction and corresponding tests.
- Added a new
-
monitor_test.go:- Added a new test function
TestMonitorCommand.
- Added a new test function
-
options.go:- Added a new
CredentialsProviderContextfield toOptionsstruct.
- Added a new
-
osscluster.go:- Added
CredentialsProviderContexttoClusterOptions. - Improved error handling in pipeline operations.
- Added
-
pubsub.go:- Fixed a typo in a comment.
-
redis.go:- Implemented support for
CredentialsProviderContext.
- Implemented support for
-
stream_commands.go:- Added
SetFirstKeyPoscalls to various XGroup commands.
- Added
-
version.go:- Updated version to 9.5.3.
Possible Issues
- The removal of Docker access token and other sensitive information from the Redis Enterprise test workflow might affect CI/CD processes if not properly configured elsewhere.
Security Hotspots
- The replacement of sensitive information in
.github/workflows/test-redis-enterprise.ymlwith placeholder values is a good security practice, but ensure that the actual sensitive data is properly managed and securely injected during the CI/CD process.
[puLL-Merge] - redis/[email protected]
Description
This PR introduces several changes to the go-redis library, including updates to dependencies, improvements to tracing and monitoring functionality, and various bug fixes and enhancements.
Changes
Changes
-
.github/workflows/build.yml:- Added Go version 1.19.x to the test matrix.
-
.github/workflows/test-redis-enterprise.yml:- Updated Redis Enterprise version to 7.4.2-54.
- Simplified credentials and configuration setup.
-
CHANGELOG.md:- Added entry for unreleased changes regarding span creation behavior.
-
Makefile:- Added logic to skip tests in example directories for Go version 1.19.
-
README.md:- Minor cleanup in code example.
-
bitmap_commands.go:- Added
BitFieldROcommand. - Optimized
BitCountcommand implementation.
- Added
-
command.go:- Improved concurrency handling in
MonitorCmd.
- Improved concurrency handling in
-
error.go:- Added support for IPv6 addresses in moved error parsing.
-
example/otel/go.mod:- Updated OpenTelemetry dependencies.
-
extra/rediscensus/go.mod,extra/rediscmd/go.mod,extra/redisotel/go.mod,extra/redisprometheus/go.mod:- Updated Go version and dependencies.
-
extra/redisotel/config.go,extra/redisotel/tracing.go:- Updated OpenTelemetry semantic conventions.
- Added server address and port attributes to tracing.
-
internal/util.go,internal/util_test.go:- Added
GetAddrfunction to handle different address formats.
- Added
-
monitor_test.go:- Added new test for monitor command.
-
options.go:- Added
CredentialsProviderContextfor enhanced credential handling.
- Added
-
osscluster.go:- Added
CredentialsProviderContexttoClusterOptions. - Improved error handling in pipeline processing.
- Added
-
pubsub.go:- Fixed typo in comment.
-
redis.go:- Implemented
CredentialsProviderContextin connection initialization.
- Implemented
-
stream_commands.go:- Added
SetFirstKeyPosto stream-related commands for better key tracking.
- Added
Possible Issues
- The change in span creation behavior (CHANGELOG.md) might affect existing tracing setups and may require adjustments in applications using this library.
Security Hotspots
No significant security issues were identified in this PR. However, the changes to credential handling (introduction of CredentialsProviderContext) should be carefully reviewed to ensure they don't introduce any security vulnerabilities.
[puLL-Merge] - redis/[email protected]
Description
This PR introduces several significant changes and improvements to the go-redis library. The changes include new features, bug fixes, optimizations, and updates to dependencies. Some key changes involve enhancing connection handling, improving cluster operations, adding new Redis commands support, and updating OpenTelemetry integration.
Possible Issues
- The changes to connection handling and cluster operations might introduce subtle behavioral changes that could affect existing applications.
- Updates to dependencies, especially OpenTelemetry, may require adjustments in applications using these features.
Security Hotspots
No significant security vulnerabilities are apparent in this change set. However, changes to connection handling and authentication should be carefully reviewed to ensure they don't introduce any security weaknesses.
Changes
Changes
-
.github/workflows/build.yml:- Updated branch targets and Go versions for CI/CD.
-
.github/workflows/golangci-lint.ymland.github/workflows/spellcheck.yml:- Updated versions of linting and spellchecking tools.
-
.github/workflows/test-redis-enterprise.yml:- Updated Redis Enterprise version and simplified test configuration.
-
CHANGELOG.md:- Added entry for changes in OpenTelemetry span creation behavior.
-
Makefile:- Updated Redis version and added conditional test skipping for older Go versions.
-
README.md:- Minor code example update.
-
bitmap_commands.go:- Optimized
BitCountcommand implementation.
- Optimized
-
command.go:- Added new fields and methods to various command structures.
- Enhanced
MonitorCmdto improve concurrency safety.
-
commands_test.go:- Added new tests for various Redis commands, including hash expiration commands.
-
error.go:- Improved error handling for moved errors in cluster operations.
-
extra/redisotel/:- Updated OpenTelemetry integration with latest conventions and improved tracing.
-
hash_commands.go:- Added new hash commands for expiration and TTL operations.
-
internal/pool/conn.goandinternal/pool/pool.go:- Improved connection health checking and management.
-
options.go:- Added
CredentialsProviderContextfor enhanced credential management.
- Added
-
osscluster.go:- Improved cluster node selection logic and error handling.
-
pubsub.go:- Minor improvements to context handling in PubSub operations.
-
redis.go:- Enhanced connection initialization with new credential provider support.
-
stream_commands.go:- Added support for the "ID" field in XReadArgs.
-
Various test files:
- Added and updated tests to cover new features and edge cases.
-
version.go:- Updated version to 9.6.0.
These changes collectively represent a significant update to the go-redis library, improving its functionality, performance, and compatibility with the latest Redis features.
[puLL-Merge] - redis/[email protected]
Description
This PR introduces several improvements and new features to the go-redis library. It includes updates to dependencies, enhancements to existing functionalities, and the addition of new Redis commands support. The changes span across multiple files and involve modifications to core functionalities, test cases, and documentation.
Changes
Changes
-
bitmap_commands.go:- Optimized
BitCountimplementation. - Added
BitFieldROcommand support.
- Optimized
-
command.go:- Enhanced
ClientInfostruct with new fields. - Improved
MonitorCmdimplementation.
- Enhanced
-
commands_test.go:- Added new test cases for various Redis commands.
- Updated existing tests to accommodate new functionalities.
-
error.go:- Improved error handling for moved errors.
-
hash_commands.go:- Added new hash commands support (HExpire, HPExpire, HExpireAt, HPExpireAt, HPersist, HExpireTime, HPExpireTime, HTTL, HPTTL).
-
internal/pool/conn.goandinternal/pool/pool.go:- Enhanced connection handling and health checks.
-
osscluster.go:- Improved cluster node selection logic.
- Enhanced error handling and retries for cluster operations.
-
options.go:- Added
CredentialsProviderContextfor enhanced credential management.
- Added
-
pubsub.go:- Minor improvements in PubSub implementation.
-
redis.go:- Enhanced connection initialization process.
-
stream_commands.go:- Added support for new stream-related commands.
-
Various test files:
- Updated and added new test cases to cover new functionalities.
-
version.go:- Updated version to 9.6.1.
Possible Issues
- The changes to cluster node selection logic in
osscluster.gomight affect the behavior of node selection in edge cases. Thorough testing in various cluster configurations is recommended.
Security Hotspots
No significant security issues were identified in this change. However, as always, careful review of credential handling changes is recommended.
[puLL-Merge] - redis/[email protected]
Description
This pull request introduces several changes and improvements to the go-redis library. The main changes include updates to various commands, enhancements to connection handling, improvements in cluster operations, and additions to hash commands for expiration management.
Changes
Changes
-
.github/workflows/:- Updated Go versions and Redis image versions in CI workflows.
-
CHANGELOG.md:- Added an unreleased section mentioning changes to span creation behavior.
-
Makefile:- Updated Redis version and added Go version check for tests.
-
bitmap_commands.go:- Optimized
BitCountcommand implementation.
- Optimized
-
command.go:- Added
Watchfield toClientInfostruct. - Enhanced
MonitorCmdimplementation.
- Added
-
commands_test.go:- Added tests for new hash expiration commands.
-
error.go:- Improved handling of moved errors.
-
extra/redisotel/:- Updated OpenTelemetry dependencies and implementation.
-
hash_commands.go:- Added new commands for hash field expiration management (HExpire, HPExpire, HExpireAt, HPExpireAt, HPersist, HExpireTime, HPExpireTime, HTTL, HPTTL).
-
internal/pool/conn.go:- Improved connection health checking.
-
osscluster.go:- Enhanced cluster node selection logic.
- Improved handling of MOVED and ASK redirections.
-
options.go:- Added
CredentialsProviderContextfor enhanced credential management.
- Added
-
pubsub.go:- Fixed context usage in
writeCmdmethod.
- Fixed context usage in
-
redis.go:- Implemented
CredentialsProviderContextin connection initialization.
- Implemented
-
stream_commands.go:- Added
IDfield toXReadArgsstruct. - Set first key position for various XGroup commands.
- Added
-
version.go:- Updated version to 9.6.2.
Possible Issues
- The changes to span creation behavior in OpenTelemetry integration might affect existing tracing setups.
- Updates to cluster node selection logic could potentially change the behavior of cluster operations in edge cases.
Security Hotspots
No significant security hotspots were identified in this change set.
👍 Dependency issues cleared. Learn more about Socket for GitHub ↗︎
This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.
[puLL-Merge] - redis/[email protected]
Diff
diff --git .github/wordlist.txt .github/wordlist.txt
index dceddff46..1fc34f733 100644
--- .github/wordlist.txt
+++ .github/wordlist.txt
@@ -1,4 +1,5 @@
ACLs
+APIs
autoload
autoloader
autoloading
@@ -46,11 +47,14 @@ runtime
SHA
sharding
SETNAME
+SpellCheck
SSL
struct
stunnel
+SynDump
TCP
TLS
+UnstableResp
uri
URI
url
@@ -59,3 +63,5 @@ RedisStack
RedisGears
RedisTimeseries
RediSearch
+RawResult
+RawVal
\ No newline at end of file
diff --git .github/workflows/build.yml .github/workflows/build.yml
index 4061bbdff..eb0c20ba2 100644
--- .github/workflows/build.yml
+++ .github/workflows/build.yml
@@ -2,9 +2,9 @@ name: Go
on:
push:
- branches: [master, v9]
+ branches: [master, v9, v9.7]
pull_request:
- branches: [master, v9]
+ branches: [master, v9, v9.7]
permissions:
contents: read
@@ -20,7 +20,7 @@ jobs:
services:
redis:
- image: redis/redis-stack-server:edge
+ image: redis/redis-stack-server:latest
options: >-
--health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
ports:
@@ -37,3 +37,9 @@ jobs:
- name: Test
run: make test
+
+ - name: Upload to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ files: coverage.txt
+ token: ${{ secrets.CODECOV_TOKEN }}
\ No newline at end of file
diff --git .github/workflows/golangci-lint.yml .github/workflows/golangci-lint.yml
index a139f5dab..d9e53f706 100644
--- .github/workflows/golangci-lint.yml
+++ .github/workflows/golangci-lint.yml
@@ -12,15 +12,13 @@ on:
permissions:
contents: read
+ pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
jobs:
golangci:
- permissions:
- contents: read # for actions/checkout to fetch code
- pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: golangci-lint
- uses: golangci/golangci-lint-action@v4
+ uses: golangci/[email protected]
diff --git .github/workflows/spellcheck.yml .github/workflows/spellcheck.yml
index 62e38997e..cc6d828c9 100644
--- .github/workflows/spellcheck.yml
+++ .github/workflows/spellcheck.yml
@@ -8,7 +8,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Check Spelling
- uses: rojopolis/[email protected]
+ uses: rojopolis/[email protected]
with:
config_path: .github/spellcheck-settings.yml
task_name: Markdown
diff --git .golangci.yml .golangci.yml
index de514554a..285aca6b3 100644
--- .golangci.yml
+++ .golangci.yml
@@ -1,4 +1,3 @@
run:
- concurrency: 8
- deadline: 5m
+ timeout: 5m
tests: false
diff --git Makefile Makefile
index d8d007596..1a6bd1786 100644
--- Makefile
+++ Makefile
@@ -14,6 +14,7 @@ test: testdeps
go test ./... -short -race && \
go test ./... -run=NONE -bench=. -benchmem && \
env GOOS=linux GOARCH=386 go test && \
+ go test -coverprofile=coverage.txt -covermode=atomic ./... && \
go vet); \
done
cd internal/customvet && go build .
diff --git README.md README.md
index e7df5dfd6..9395c652f 100644
--- README.md
+++ README.md
@@ -3,6 +3,7 @@
[](https://github.com/redis/go-redis/actions)
[](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc)
[](https://redis.uptrace.dev/)
+[](https://codecov.io/github/redis/go-redis)
[](https://discord.gg/rWtp5Aj)
> go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
@@ -169,19 +170,39 @@ By default, go-redis automatically sends the client library name and version dur
#### Disabling Identity Verification
-When connection identity verification is not required or needs to be explicitly disabled, a `DisableIndentity` configuration option exists. In V10 of this library, `DisableIndentity` will become `DisableIdentity` in order to fix the associated typo.
+When connection identity verification is not required or needs to be explicitly disabled, a `DisableIdentity` configuration option exists.
+Initially there was a typo and the option was named `DisableIndentity` instead of `DisableIdentity`. The misspelled option is marked as Deprecated and will be removed in V10 of this library.
+Although both options will work at the moment, the correct option is `DisableIdentity`. The deprecated option will be removed in V10 of this library, so please use the correct option name to avoid any issues.
-To disable verification, set the `DisableIndentity` option to `true` in the Redis client options:
+To disable verification, set the `DisableIdentity` option to `true` in the Redis client options:
```go
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
- DisableIndentity: true, // Disable set-info on connect
+ DisableIdentity: true, // Disable set-info on connect
})
```
+#### Unstable RESP3 Structures for RediSearch Commands
+When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes.
+
+To enable unstable RESP3, set the option in your client configuration:
+
+```go
+redis.NewClient(&redis.Options{
+ UnstableResp3: true,
+ })
+```
+**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data.
+ Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them:
+
+```go
+res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawResult()
+val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal()
+```
+
## Contributing
Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library!
diff --git bench_decode_test.go bench_decode_test.go
index 16bdf2cd3..d61a901a0 100644
--- bench_decode_test.go
+++ bench_decode_test.go
@@ -30,7 +30,7 @@ func NewClientStub(resp []byte) *ClientStub {
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
return stub.stubConn(initHello), nil
},
- DisableIndentity: true,
+ DisableIdentity: true,
})
return stub
}
@@ -46,7 +46,7 @@ func NewClusterClientStub(resp []byte) *ClientStub {
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
return stub.stubConn(initHello), nil
},
- DisableIndentity: true,
+ DisableIdentity: true,
ClusterSlots: func(_ context.Context) ([]ClusterSlot, error) {
return []ClusterSlot{
diff --git command.go command.go
index 9ae97a95a..f3d0e49b7 100644
--- command.go
+++ command.go
@@ -40,7 +40,7 @@ type Cmder interface {
readTimeout() *time.Duration
readReply(rd *proto.Reader) error
-
+ readRawReply(rd *proto.Reader) error
SetErr(error)
Err() error
}
@@ -122,11 +122,11 @@ func cmdString(cmd Cmder, val interface{}) string {
//------------------------------------------------------------------------------
type baseCmd struct {
- ctx context.Context
- args []interface{}
- err error
- keyPos int8
-
+ ctx context.Context
+ args []interface{}
+ err error
+ keyPos int8
+ rawVal interface{}
_readTimeout *time.Duration
}
@@ -167,6 +167,8 @@ func (cmd *baseCmd) stringArg(pos int) string {
switch v := arg.(type) {
case string:
return v
+ case []byte:
+ return string(v)
default:
// TODO: consider using appendArg
return fmt.Sprint(v)
@@ -197,6 +199,11 @@ func (cmd *baseCmd) setReadTimeout(d time.Duration) {
cmd._readTimeout = &d
}
+func (cmd *baseCmd) readRawReply(rd *proto.Reader) (err error) {
+ cmd.rawVal, err = rd.ReadReply()
+ return err
+}
+
//------------------------------------------------------------------------------
type Cmd struct {
diff --git commands_test.go commands_test.go
index 9554bf9a9..64800705d 100644
--- commands_test.go
+++ commands_test.go
@@ -217,7 +217,7 @@ var _ = Describe("Commands", func() {
killed := client.ClientKillByFilter(ctx, "MAXAGE", "1")
Expect(killed.Err()).NotTo(HaveOccurred())
- Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3))))
+ Expect(killed.Val()).To(BeNumerically(">=", 2))
select {
case <-done:
diff --git a/doctests/bf_tutorial_test.go b/doctests/bf_tutorial_test.go
new file mode 100644
index 000000000..67545f1d5
--- /dev/null
+++ doctests/bf_tutorial_test.go
@@ -0,0 +1,83 @@
+// EXAMPLE: bf_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_bloom() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:models")
+ // REMOVE_END
+
+ // STEP_START bloom
+ res1, err := rdb.BFReserve(ctx, "bikes:models", 0.01, 1000).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> OK
+
+ res2, err := rdb.BFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> true
+
+ res3, err := rdb.BFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> true
+
+ res4, err := rdb.BFMAdd(ctx, "bikes:models",
+ "Rocky Mountain Racer",
+ "Cloudy City Cruiser",
+ "Windy City Wippet",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4) // >>> [true true true]
+
+ res5, err := rdb.BFMExists(ctx, "bikes:models",
+ "Rocky Mountain Racer",
+ "Cloudy City Cruiser",
+ "Windy City Wippet",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res5) // >>> [true true true]
+ // STEP_END
+
+ // Output:
+ // OK
+ // true
+ // true
+ // [true true true]
+ // [true true true]
+}
diff --git a/doctests/bitmap_tutorial_test.go b/doctests/bitmap_tutorial_test.go
new file mode 100644
index 000000000..dbfc247ac
--- /dev/null
+++ doctests/bitmap_tutorial_test.go
@@ -0,0 +1,92 @@
+// EXAMPLE: bitmap_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_ping() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "pings:2024-01-01-00:00")
+ // REMOVE_END
+
+ // STEP_START ping
+ res1, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> 0
+
+ res2, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 123).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> 1
+
+ res3, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 456).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> 0
+ // STEP_END
+
+ // Output:
+ // 0
+ // 1
+ // 0
+}
+
+func ExampleClient_bitcount() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ _, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+ // REMOVE_END
+
+ // STEP_START bitcount
+ res4, err := rdb.BitCount(ctx, "pings:2024-01-01-00:00",
+ &redis.BitCount{
+ Start: 0,
+ End: 456,
+ }).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4) // >>> 1
+ // STEP_END
+
+ // Output:
+ // 1
+}
diff --git a/doctests/hash_tutorial_test.go b/doctests/hash_tutorial_test.go
new file mode 100644
index 000000000..8b0b1ce9a
--- /dev/null
+++ doctests/hash_tutorial_test.go
@@ -0,0 +1,281 @@
+// EXAMPLE: hash_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_set_get_all() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike:1")
+ // REMOVE_END
+
+ // STEP_START set_get_all
+ hashFields := []string{
+ "model", "Deimos",
+ "brand", "Ergonom",
+ "type", "Enduro bikes",
+ "price", "4972",
+ }
+
+ res1, err := rdb.HSet(ctx, "bike:1", hashFields).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> 4
+
+ res2, err := rdb.HGet(ctx, "bike:1", "model").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> Deimos
+
+ res3, err := rdb.HGet(ctx, "bike:1", "price").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> 4972
+
+ cmdReturn := rdb.HGetAll(ctx, "bike:1")
+ res4, err := cmdReturn.Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4)
+ // >>> map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes]
+
+ type BikeInfo struct {
+ Model string `redis:"model"`
+ Brand string `redis:"brand"`
+ Type string `redis:"type"`
+ Price int `redis:"price"`
+ }
+
+ var res4a BikeInfo
+
+ if err := cmdReturn.Scan(&res4a); err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("Model: %v, Brand: %v, Type: %v, Price: $%v\n",
+ res4a.Model, res4a.Brand, res4a.Type, res4a.Price)
+ // >>> Model: Deimos, Brand: Ergonom, Type: Enduro bikes, Price: $4972
+ // STEP_END
+
+ // Output:
+ // 4
+ // Deimos
+ // 4972
+ // map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes]
+ // Model: Deimos, Brand: Ergonom, Type: Enduro bikes, Price: $4972
+}
+
+func ExampleClient_hmget() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike:1")
+ // REMOVE_END
+
+ hashFields := []string{
+ "model", "Deimos",
+ "brand", "Ergonom",
+ "type", "Enduro bikes",
+ "price", "4972",
+ }
+
+ _, err := rdb.HSet(ctx, "bike:1", hashFields).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START hmget
+ cmdReturn := rdb.HMGet(ctx, "bike:1", "model", "price")
+ res5, err := cmdReturn.Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res5) // >>> [Deimos 4972]
+
+ type BikeInfo struct {
+ Model string `redis:"model"`
+ Brand string `redis:"-"`
+ Type string `redis:"-"`
+ Price int `redis:"price"`
+ }
+
+ var res5a BikeInfo
+
+ if err := cmdReturn.Scan(&res5a); err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("Model: %v, Price: $%v\n", res5a.Model, res5a.Price)
+ // >>> Model: Deimos, Price: $4972
+ // STEP_END
+
+ // Output:
+ // [Deimos 4972]
+ // Model: Deimos, Price: $4972
+}
+
+func ExampleClient_hincrby() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike:1")
+ // REMOVE_END
+
+ hashFields := []string{
+ "model", "Deimos",
+ "brand", "Ergonom",
+ "type", "Enduro bikes",
+ "price", "4972",
+ }
+
+ _, err := rdb.HSet(ctx, "bike:1", hashFields).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START hincrby
+ res6, err := rdb.HIncrBy(ctx, "bike:1", "price", 100).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res6) // >>> 5072
+
+ res7, err := rdb.HIncrBy(ctx, "bike:1", "price", -100).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res7) // >>> 4972
+ // STEP_END
+
+ // Output:
+ // 5072
+ // 4972
+}
+
+func ExampleClient_incrby_get_mget() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike:1:stats")
+ // REMOVE_END
+
+ // STEP_START incrby_get_mget
+ res8, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res8) // >>> 1
+
+ res9, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res9) // >>> 2
+
+ res10, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res10) // >>> 3
+
+ res11, err := rdb.HIncrBy(ctx, "bike:1:stats", "crashes", 1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res11) // >>> 1
+
+ res12, err := rdb.HIncrBy(ctx, "bike:1:stats", "owners", 1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res12) // >>> 1
+
+ res13, err := rdb.HGet(ctx, "bike:1:stats", "rides").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res13) // >>> 3
+
+ res14, err := rdb.HMGet(ctx, "bike:1:stats", "crashes", "owners").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res14) // >>> [1 1]
+ // STEP_END
+
+ // Output:
+ // 1
+ // 2
+ // 3
+ // 1
+ // 1
+ // 3
+ // [1 1]
+}
diff --git a/doctests/json_tutorial_test.go b/doctests/json_tutorial_test.go
new file mode 100644
index 000000000..4e9787330
--- /dev/null
+++ doctests/json_tutorial_test.go
@@ -0,0 +1,1149 @@
+// EXAMPLE: json_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+func ExampleClient_setget() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike")
+ // REMOVE_END
+
+ // STEP_START set_get
+ res1, err := rdb.JSONSet(ctx, "bike", "$",
+ "\"Hyperion\"",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> OK
+
+ res2, err := rdb.JSONGet(ctx, "bike", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> ["Hyperion"]
+
+ res3, err := rdb.JSONType(ctx, "bike", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> [[string]]
+ // STEP_END
+
+ // Output:
+ // OK
+ // ["Hyperion"]
+ // [[string]]
+}
+
+func ExampleClient_str() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bike", "$",
+ "\"Hyperion\"",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START str
+ res4, err := rdb.JSONStrLen(ctx, "bike", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(*res4[0]) // >>> 8
+
+ res5, err := rdb.JSONStrAppend(ctx, "bike", "$", "\" (Enduro bikes)\"").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(*res5[0]) // >>> 23
+
+ res6, err := rdb.JSONGet(ctx, "bike", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res6) // >>> ["Hyperion (Enduro bikes)"]
+ // STEP_END
+
+ // Output:
+ // 8
+ // 23
+ // ["Hyperion (Enduro bikes)"]
+}
+
+func ExampleClient_num() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "crashes")
+ // REMOVE_END
+
+ // STEP_START num
+ res7, err := rdb.JSONSet(ctx, "crashes", "$", 0).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res7) // >>> OK
+
+ res8, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", 1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res8) // >>> [1]
+
+ res9, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", 1.5).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res9) // >>> [2.5]
+
+ res10, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", -0.75).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res10) // >>> [1.75]
+ // STEP_END
+
+ // Output:
+ // OK
+ // [1]
+ // [2.5]
+ // [1.75]
+}
+
+func ExampleClient_arr() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "newbike")
+ // REMOVE_END
+
+ // STEP_START arr
+ res11, err := rdb.JSONSet(ctx, "newbike", "$",
+ []interface{}{
+ "Deimos",
+ map[string]interface{}{"crashes": 0},
+ nil,
+ },
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res11) // >>> OK
+
+ res12, err := rdb.JSONGet(ctx, "newbike", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res12) // >>> [["Deimos",{"crashes":0},null]]
+
+ res13, err := rdb.JSONGet(ctx, "newbike", "$[1].crashes").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res13) // >>> [0]
+
+ res14, err := rdb.JSONDel(ctx, "newbike", "$.[-1]").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res14) // >>> 1
+
+ res15, err := rdb.JSONGet(ctx, "newbike", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res15) // >>> [["Deimos",{"crashes":0}]]
+ // STEP_END
+
+ // Output:
+ // OK
+ // [["Deimos",{"crashes":0},null]]
+ // [0]
+ // 1
+ // [["Deimos",{"crashes":0}]]
+}
+
+func ExampleClient_arr2() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "riders")
+ // REMOVE_END
+
+ // STEP_START arr2
+ res16, err := rdb.JSONSet(ctx, "riders", "$", []interface{}{}).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res16) // >>> OK
+
+ res17, err := rdb.JSONArrAppend(ctx, "riders", "$", "\"Norem\"").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res17) // >>> [1]
+
+ res18, err := rdb.JSONGet(ctx, "riders", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res18) // >>> [["Norem"]]
+
+ res19, err := rdb.JSONArrInsert(ctx, "riders", "$", 1,
+ "\"Prickett\"", "\"Royce\"", "\"Castilla\"",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res19) // [3]
+
+ res20, err := rdb.JSONGet(ctx, "riders", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res20) // >>> [["Norem", "Prickett", "Royce", "Castilla"]]
+
+ rangeStop := 1
+
+ res21, err := rdb.JSONArrTrimWithArgs(ctx, "riders", "$",
+ &redis.JSONArrTrimArgs{Start: 1, Stop: &rangeStop},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res21) // >>> [1]
+
+ res22, err := rdb.JSONGet(ctx, "riders", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res22) // >>> [["Prickett"]]
+
+ res23, err := rdb.JSONArrPop(ctx, "riders", "$", -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res23) // >>> [["Prickett"]]
+
+ res24, err := rdb.JSONArrPop(ctx, "riders", "$", -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res24) // []
+ // STEP_END
+
+ // Output:
+ // OK
+ // [1]
+ // [["Norem"]]
+ // [4]
+ // [["Norem","Prickett","Royce","Castilla"]]
+ // [1]
+ // [["Prickett"]]
+ // ["Prickett"]
+ // []
+}
+
+func ExampleClient_obj() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike:1")
+ // REMOVE_END
+
+ // STEP_START obj
+ res25, err := rdb.JSONSet(ctx, "bike:1", "$",
+ map[string]interface{}{
+ "model": "Deimos",
+ "brand": "Ergonom",
+ "price": 4972,
+ },
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res25) // >>> OK
+
+ res26, err := rdb.JSONObjLen(ctx, "bike:1", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(*res26[0]) // >>> 3
+
+ res27, err := rdb.JSONObjKeys(ctx, "bike:1", "$").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res27) // >>> [brand model price]
+ // STEP_END
+
+ // Output:
+ // OK
+ // 3
+ // [[brand model price]]
+}
+
+var inventory_json = map[string]interface{}{
+ "inventory": map[string]interface{}{
+ "mountain_bikes": []interface{}{
+ map[string]interface{}{
+ "id": "bike:1",
+ "model": "Phoebe",
+ "description": "This is a mid-travel trail slayer that is a fantastic " +
+ "daily driver or one bike quiver. The Shimano Claris 8-speed groupset " +
+ "gives plenty of gear range to tackle hills and there\u2019s room for " +
+ "mudguards and a rack too. This is the bike for the rider who wants " +
+ "trail manners with low fuss ownership.",
+ "price": 1920,
+ "specs": map[string]interface{}{"material": "carbon", "weight": 13.1},
+ "colors": []interface{}{"black", "silver"},
+ },
+ map[string]interface{}{
+ "id": "bike:2",
+ "model": "Quaoar",
+ "description": "Redesigned for the 2020 model year, this bike " +
+ "impressed our testers and is the best all-around trail bike we've " +
+ "ever tested. The Shimano gear system effectively does away with an " +
+ "external cassette, so is super low maintenance in terms of wear " +
+ "and tear. All in all it's an impressive package for the price, " +
+ "making it very competitive.",
+ "price": 2072,
+ "specs": map[string]interface{}{"material": "aluminium", "weight": 7.9},
+ "colors": []interface{}{"black", "white"},
+ },
+ map[string]interface{}{
+ "id": "bike:3",
+ "model": "Weywot",
+ "description": "This bike gives kids aged six years and older " +
+ "a durable and uberlight mountain bike for their first experience " +
+ "on tracks and easy cruising through forests and fields. A set of " +
+ "powerful Shimano hydraulic disc brakes provide ample stopping " +
+ "ability. If you're after a budget option, this is one of the best " +
+ "bikes you could get.",
+ "price": 3264,
+ "specs": map[string]interface{}{"material": "alloy", "weight": 13.8},
+ },
+ },
+ "commuter_bikes": []interface{}{
+ map[string]interface{}{
+ "id": "bike:4",
+ "model": "Salacia",
+ "description": "This bike is a great option for anyone who just " +
+ "wants a bike to get about on With a slick-shifting Claris gears " +
+ "from Shimano\u2019s, this is a bike which doesn\u2019t break the " +
+ "bank and delivers craved performance. It\u2019s for the rider " +
+ "who wants both efficiency and capability.",
+ "price": 1475,
+ "specs": map[string]interface{}{"material": "aluminium", "weight": 16.6},
+ "colors": []interface{}{"black", "silver"},
+ },
+ map[string]interface{}{
+ "id": "bike:5",
+ "model": "Mimas",
+ "description": "A real joy to ride, this bike got very high " +
+ "scores in last years Bike of the year report. The carefully " +
+ "crafted 50-34 tooth chainset and 11-32 tooth cassette give an " +
+ "easy-on-the-legs bottom gear for climbing, and the high-quality " +
+ "Vittoria Zaffiro tires give balance and grip.It includes " +
+ "a low-step frame , our memory foam seat, bump-resistant shocks and " +
+ "conveniently placed thumb throttle. Put it all together and you " +
+ "get a bike that helps redefine what can be done for this price.",
+ "price": 3941,
+ "specs": map[string]interface{}{"material": "alloy", "weight": 11.6},
+ },
+ },
+ },
+}
+
+func ExampleClient_setbikes() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ // STEP_START set_bikes
+ var inventory_json = map[string]interface{}{
+ "inventory": map[string]interface{}{
+ "mountain_bikes": []interface{}{
+ map[string]interface{}{
+ "id": "bike:1",
+ "model": "Phoebe",
+ "description": "This is a mid-travel trail slayer that is a fantastic " +
+ "daily driver or one bike quiver. The Shimano Claris 8-speed groupset " +
+ "gives plenty of gear range to tackle hills and there\u2019s room for " +
+ "mudguards and a rack too. This is the bike for the rider who wants " +
+ "trail manners with low fuss ownership.",
+ "price": 1920,
+ "specs": map[string]interface{}{"material": "carbon", "weight": 13.1},
+ "colors": []interface{}{"black", "silver"},
+ },
+ map[string]interface{}{
+ "id": "bike:2",
+ "model": "Quaoar",
+ "description": "Redesigned for the 2020 model year, this bike " +
+ "impressed our testers and is the best all-around trail bike we've " +
+ "ever tested. The Shimano gear system effectively does away with an " +
+ "external cassette, so is super low maintenance in terms of wear " +
+ "and tear. All in all it's an impressive package for the price, " +
+ "making it very competitive.",
+ "price": 2072,
+ "specs": map[string]interface{}{"material": "aluminium", "weight": 7.9},
+ "colors": []interface{}{"black", "white"},
+ },
+ map[string]interface{}{
+ "id": "bike:3",
+ "model": "Weywot",
+ "description": "This bike gives kids aged six years and older " +
+ "a durable and uberlight mountain bike for their first experience " +
+ "on tracks and easy cruising through forests and fields. A set of " +
+ "powerful Shimano hydraulic disc brakes provide ample stopping " +
+ "ability. If you're after a budget option, this is one of the best " +
+ "bikes you could get.",
+ "price": 3264,
+ "specs": map[string]interface{}{"material": "alloy", "weight": 13.8},
+ },
+ },
+ "commuter_bikes": []interface{}{
+ map[string]interface{}{
+ "id": "bike:4",
+ "model": "Salacia",
+ "description": "This bike is a great option for anyone who just " +
+ "wants a bike to get about on With a slick-shifting Claris gears " +
+ "from Shimano\u2019s, this is a bike which doesn\u2019t break the " +
+ "bank and delivers craved performance. It\u2019s for the rider " +
+ "who wants both efficiency and capability.",
+ "price": 1475,
+ "specs": map[string]interface{}{"material": "aluminium", "weight": 16.6},
+ "colors": []interface{}{"black", "silver"},
+ },
+ map[string]interface{}{
+ "id": "bike:5",
+ "model": "Mimas",
+ "description": "A real joy to ride, this bike got very high " +
+ "scores in last years Bike of the year report. The carefully " +
+ "crafted 50-34 tooth chainset and 11-32 tooth cassette give an " +
+ "easy-on-the-legs bottom gear for climbing, and the high-quality " +
+ "Vittoria Zaffiro tires give balance and grip.It includes " +
+ "a low-step frame , our memory foam seat, bump-resistant shocks and " +
+ "conveniently placed thumb throttle. Put it all together and you " +
+ "get a bike that helps redefine what can be done for this price.",
+ "price": 3941,
+ "specs": map[string]interface{}{"material": "alloy", "weight": 11.6},
+ },
+ },
+ },
+ }
+
+ res1, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> OK
+ // STEP_END
+
+ // Output:
+ // OK
+}
+
+func ExampleClient_getbikes() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START get_bikes
+ res2, err := rdb.JSONGetWithArgs(ctx, "bikes:inventory",
+ &redis.JSONGetArgs{Indent: " ", Newline: "\n", Space: " "},
+ "$.inventory.*",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2)
+ // >>>
+ // [
+ // [
+ // {
+ // "colors": [
+ // "black",
+ // "silver"
+ // ...
+ // STEP_END
+
+ // Output:
+ // [
+ // [
+ // {
+ // "colors": [
+ // "black",
+ // "silver"
+ // ],
+ // "description": "This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance. It’s for the rider who wants both efficiency and capability.",
+ // "id": "bike:4",
+ // "model": "Salacia",
+ // "price": 1475,
+ // "specs": {
+ // "material": "aluminium",
+ // "weight": 16.6
+ // }
+ // },
+ // {
+ // "description": "A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.",
+ // "id": "bike:5",
+ // "model": "Mimas",
+ // "price": 3941,
+ // "specs": {
+ // "material": "alloy",
+ // "weight": 11.6
+ // }
+ // }
+ // ],
+ // [
+ // {
+ // "colors": [
+ // "black",
+ // "silver"
+ // ],
+ // "description": "This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too. This is the bike for the rider who wants trail manners with low fuss ownership.",
+ // "id": "bike:1",
+ // "model": "Phoebe",
+ // "price": 1920,
+ // "specs": {
+ // "material": "carbon",
+ // "weight": 13.1
+ // }
+ // },
+ // {
+ // "colors": [
+ // "black",
+ // "white"
+ // ],
+ // "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.",
+ // "id": "bike:2",
+ // "model": "Quaoar",
+ // "price": 2072,
+ // "specs": {
+ // "material": "aluminium",
+ // "weight": 7.9
+ // }
+ // },
+ // {
+ // "description": "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.",
+ // "id": "bike:3",
+ // "model": "Weywot",
+ // "price": 3264,
+ // "specs": {
+ // "material": "alloy",
+ // "weight": 13.8
+ // }
+ // }
+ // ]
+ // ]
+}
+
+func ExampleClient_getmtnbikes() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START get_mtnbikes
+ res3, err := rdb.JSONGet(ctx, "bikes:inventory",
+ "$.inventory.mountain_bikes[*].model",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3)
+ // >>> ["Phoebe","Quaoar","Weywot"]
+
+ res4, err := rdb.JSONGet(ctx,
+ "bikes:inventory", "$.inventory[\"mountain_bikes\"][*].model",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4)
+ // >>> ["Phoebe","Quaoar","Weywot"]
+
+ res5, err := rdb.JSONGet(ctx,
+ "bikes:inventory", "$..mountain_bikes[*].model",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res5)
+ // >>> ["Phoebe","Quaoar","Weywot"]
+ // STEP_END
+
+ // Output:
+ // ["Phoebe","Quaoar","Weywot"]
+ // ["Phoebe","Quaoar","Weywot"]
+ // ["Phoebe","Quaoar","Weywot"]
+}
+
+func ExampleClient_getmodels() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START get_models
+ res6, err := rdb.JSONGet(ctx, "bikes:inventory", "$..model").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res6) // >>> ["Salacia","Mimas","Phoebe","Quaoar","Weywot"]
+ // STEP_END
+
+ // Output:
+ // ["Salacia","Mimas","Phoebe","Quaoar","Weywot"]
+}
+
+func ExampleClient_get2mtnbikes() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START get2mtnbikes
+ res7, err := rdb.JSONGet(ctx, "bikes:inventory", "$..mountain_bikes[0:2].model").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res7) // >>> ["Phoebe","Quaoar"]
+ // STEP_END
+
+ // Output:
+ // ["Phoebe","Quaoar"]
+}
+
+func ExampleClient_filter1() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START filter1
+ res8, err := rdb.JSONGetWithArgs(ctx, "bikes:inventory",
+ &redis.JSONGetArgs{Indent: " ", Newline: "\n", Space: " "},
+ "$..mountain_bikes[?(@.price < 3000 && @.specs.weight < 10)]",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res8)
+ // >>>
+ // [
+ // {
+ // "colors": [
+ // "black",
+ // "white"
+ // ],
+ // "description": "Redesigned for the 2020 model year
+ // ...
+ // STEP_END
+
+ // Output:
+ // [
+ // {
+ // "colors": [
+ // "black",
+ // "white"
+ // ],
+ // "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.",
+ // "id": "bike:2",
+ // "model": "Quaoar",
+ // "price": 2072,
+ // "specs": {
+ // "material": "aluminium",
+ // "weight": 7.9
+ // }
+ // }
+ // ]
+}
+
+func ExampleClient_filter2() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START filter2
+ res9, err := rdb.JSONGet(ctx,
+ "bikes:inventory",
+ "$..[?(@.specs.material == 'alloy')].model",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res9) // >>> ["Mimas","Weywot"]
+ // STEP_END
+
+ // Output:
+ // ["Mimas","Weywot"]
+}
+
+func ExampleClient_filter3() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START filter3
+ res10, err := rdb.JSONGet(ctx,
+ "bikes:inventory",
+ "$..[?(@.specs.material =~ '(?i)al')].model",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res10) // >>> ["Salacia","Mimas","Quaoar","Weywot"]
+ // STEP_END
+
+ // Output:
+ // ["Salacia","Mimas","Quaoar","Weywot"]
+}
+
+func ExampleClient_filter4() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START filter4
+ res11, err := rdb.JSONSet(ctx,
+ "bikes:inventory",
+ "$.inventory.mountain_bikes[0].regex_pat",
+ "\"(?i)al\"",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res11) // >>> OK
+
+ res12, err := rdb.JSONSet(ctx,
+ "bikes:inventory",
+ "$.inventory.mountain_bikes[1].regex_pat",
+ "\"(?i)al\"",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res12) // >>> OK
+
+ res13, err := rdb.JSONSet(ctx,
+ "bikes:inventory",
+ "$.inventory.mountain_bikes[2].regex_pat",
+ "\"(?i)al\"",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res13) // >>> OK
+
+ res14, err := rdb.JSONGet(ctx,
+ "bikes:inventory",
+ "$.inventory.mountain_bikes[?(@.specs.material =~ @.regex_pat)].model",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res14) // >>> ["Quaoar","Weywot"]
+ // STEP_END
+
+ // Output:
+ // OK
+ // OK
+ // OK
+ // ["Quaoar","Weywot"]
+}
+
+func ExampleClient_updatebikes() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START update_bikes
+ res15, err := rdb.JSONGet(ctx, "bikes:inventory", "$..price").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res15) // >>> [1475,3941,1920,2072,3264]
+
+ res16, err := rdb.JSONNumIncrBy(ctx, "bikes:inventory", "$..price", -100).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res16) // >>> [1375,3841,1820,1972,3164]
+
+ res17, err := rdb.JSONNumIncrBy(ctx, "bikes:inventory", "$..price", 100).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res17) // >>> [1475,3941,1920,2072,3264]
+ // STEP_END
+
+ // Output:
+ // [1475,3941,1920,2072,3264]
+ // [1375,3841,1820,1972,3164]
+ // [1475,3941,1920,2072,3264]
+}
+
+func ExampleClient_updatefilters1() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START update_filters1
+ res18, err := rdb.JSONSet(ctx,
+ "bikes:inventory",
+ "$.inventory.*[?(@.price<2000)].price",
+ 1500,
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res18) // >>> OK
+
+ res19, err := rdb.JSONGet(ctx, "bikes:inventory", "$..price").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res19) // >>> [1500,3941,1500,2072,3264]
+ // STEP_END
+
+ // Output:
+ // OK
+ // [1500,3941,1500,2072,3264]
+}
+
+func ExampleClient_updatefilters2() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:inventory")
+ // REMOVE_END
+
+ _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START update_filters2
+ res20, err := rdb.JSONArrAppend(ctx,
+ "bikes:inventory",
+ "$.inventory.*[?(@.price<2000)].colors",
+ "\"pink\"",
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res20) // >>> [3 3]
+
+ res21, err := rdb.JSONGet(ctx, "bikes:inventory", "$..[*].colors").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res21)
+ // >>> [["black","silver","pink"],["black","silver","pink"],["black","white"]]
+ // STEP_END
+
+ // Output:
+ // [3 3]
+ // [["black","silver","pink"],["black","silver","pink"],["black","white"]]
+}
diff --git a/doctests/list_tutorial_test.go b/doctests/list_tutorial_test.go
new file mode 100644
index 000000000..908469ce0
--- /dev/null
+++ doctests/list_tutorial_test.go
@@ -0,0 +1,766 @@
+// EXAMPLE: list_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_queue() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START queue
+ res1, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> 1
+
+ res2, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> 2
+
+ res3, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> bike:1
+
+ res4, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4) // >>> bike:2
+ // STEP_END
+
+ // Output:
+ // 1
+ // 2
+ // bike:1
+ // bike:2
+}
+
+func ExampleClient_stack() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START stack
+ res5, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res5) // >>> 1
+
+ res6, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res6) // >>> 2
+
+ res7, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res7) // >>> bike:2
+
+ res8, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res8) // >>> bike:1
+ // STEP_END
+
+ // Output:
+ // 1
+ // 2
+ // bike:2
+ // bike:1
+}
+
+func ExampleClient_llen() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START llen
+ res9, err := rdb.LLen(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res9) // >>> 0
+ // STEP_END
+
+ // Output:
+ // 0
+}
+
+func ExampleClient_lmove_lrange() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ rdb.Del(ctx, "bikes:finished")
+ // REMOVE_END
+
+ // STEP_START lmove_lrange
+ res10, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res10) // >>> 1
+
+ res11, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res11) // >>> 2
+
+ res12, err := rdb.LMove(ctx, "bikes:repairs", "bikes:finished", "LEFT", "LEFT").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res12) // >>> bike:2
+
+ res13, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res13) // >>> [bike:1]
+
+ res14, err := rdb.LRange(ctx, "bikes:finished", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res14) // >>> [bike:2]
+ // STEP_END
+
+ // Output:
+ // 1
+ // 2
+ // bike:2
+ // [bike:1]
+ // [bike:2]
+}
+
+func ExampleClient_lpush_rpush() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START lpush_rpush
+ res15, err := rdb.RPush(ctx, "bikes:repairs", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res15) // >>> 1
+
+ res16, err := rdb.RPush(ctx, "bikes:repairs", "bike:2").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res16) // >>> 2
+
+ res17, err := rdb.LPush(ctx, "bikes:repairs", "bike:important_bike").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res17) // >>> 3
+
+ res18, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res18) // >>> [bike:important_bike bike:1 bike:2]
+ // STEP_END
+
+ // Output:
+ // 1
+ // 2
+ // 3
+ // [bike:important_bike bike:1 bike:2]
+}
+
+func ExampleClient_variadic() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START variadic
+ res19, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res19) // >>> 3
+
+ res20, err := rdb.LPush(ctx, "bikes:repairs", "bike:important_bike", "bike:very_important_bike").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res20) // >>> 5
+
+ res21, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res21) // >>> [bike:very_important_bike bike:important_bike bike:1 bike:2 bike:3]
+ // STEP_END
+
+ // Output:
+ // 3
+ // 5
+ // [bike:very_important_bike bike:important_bike bike:1 bike:2 bike:3]
+}
+
+func ExampleClient_lpop_rpop() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START lpop_rpop
+ res22, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res22) // >>> 3
+
+ res23, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res23) // >>> bike:3
+
+ res24, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res24) // >>> bike:1
+
+ res25, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res25) // >>> bike:2
+
+ res26, err := rdb.RPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ fmt.Println(err) // >>> redis: nil
+ }
+
+ fmt.Println(res26) // >>> <empty string>
+
+ // STEP_END
+
+ // Output:
+ // 3
+ // bike:3
+ // bike:1
+ // bike:2
+ // redis: nil
+ //
+}
+
+func ExampleClient_ltrim() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START ltrim
+ res27, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res27) // >>> 5
+
+ res28, err := rdb.LTrim(ctx, "bikes:repairs", 0, 2).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res28) // >>> OK
+
+ res29, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res29) // >>> [bike:5 bike:4 bike:3]
+ // STEP_END
+
+ // Output:
+ // 5
+ // OK
+ // [bike:5 bike:4 bike:3]
+}
+
+func ExampleClient_ltrim_end_of_list() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START ltrim_end_of_list
+ res30, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res30) // >>> 5
+
+ res31, err := rdb.LTrim(ctx, "bikes:repairs", -3, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res31) // >>> OK
+
+ res32, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res32) // >>> [bike:3 bike:4 bike:5]
+ // STEP_END
+
+ // Output:
+ // 5
+ // OK
+ // [bike:3 bike:4 bike:5]
+}
+
+func ExampleClient_brpop() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START brpop
+ res33, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res33) // >>> 2
+
+ res34, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res34) // >>> [bikes:repairs bike:2]
+
+ res35, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res35) // >>> [bikes:repairs bike:1]
+
+ res36, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result()
+
+ if err != nil {
+ fmt.Println(err) // >>> redis: nil
+ }
+
+ fmt.Println(res36) // >>> []
+ // STEP_END
+
+ // Output:
+ // 2
+ // [bikes:repairs bike:2]
+ // [bikes:repairs bike:1]
+ // redis: nil
+ // []
+}
+
+func ExampleClient_rule1() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "new_bikes")
+ // REMOVE_END
+
+ // STEP_START rule_1
+ res37, err := rdb.Del(ctx, "new_bikes").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res37) // >>> 0
+
+ res38, err := rdb.LPush(ctx, "new_bikes", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res38) // >>> 3
+ // STEP_END
+
+ // Output:
+ // 0
+ // 3
+}
+
+func ExampleClient_rule11() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "new_bikes")
+ // REMOVE_END
+
+ // STEP_START rule_1.1
+ res39, err := rdb.Set(ctx, "new_bikes", "bike:1", 0).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res39) // >>> OK
+
+ res40, err := rdb.Type(ctx, "new_bikes").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res40) // >>> string
+
+ res41, err := rdb.LPush(ctx, "new_bikes", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ fmt.Println(err)
+ // >>> WRONGTYPE Operation against a key holding the wrong kind of value
+ }
+
+ fmt.Println(res41)
+ // STEP_END
+
+ // Output:
+ // OK
+ // string
+ // WRONGTYPE Operation against a key holding the wrong kind of value
+ // 0
+}
+
+func ExampleClient_rule2() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START rule_2
+ res42, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res42) // >>> 3
+
+ res43, err := rdb.Exists(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res43) // >>> 1
+
+ res44, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res44) // >>> bike:3
+
+ res45, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res45) // >>> bike:2
+
+ res46, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res46) // >>> bike:1
+
+ res47, err := rdb.Exists(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res47) // >>> 0
+ // STEP_END
+
+ // Output:
+ // 3
+ // 1
+ // bike:3
+ // bike:2
+ // bike:1
+ // 0
+}
+
+func ExampleClient_rule3() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START rule_3
+ res48, err := rdb.Del(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res48) // >>> 0
+
+ res49, err := rdb.LLen(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res49) // >>> 0
+
+ res50, err := rdb.LPop(ctx, "bikes:repairs").Result()
+
+ if err != nil {
+ fmt.Println(err) // >>> redis: nil
+ }
+
+ fmt.Println(res50) // >>> <empty string>
+ // STEP_END
+
+ // Output:
+ // 0
+ // 0
+ // redis: nil
+ //
+}
+
+func ExampleClient_ltrim1() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:repairs")
+ // REMOVE_END
+
+ // STEP_START ltrim.1
+ res51, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res51) // >>> 5
+
+ res52, err := rdb.LTrim(ctx, "bikes:repairs", 0, 2).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res52) // >>> OK
+
+ res53, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res53) // >>> [bike:5 bike:4 bike:3]
+ // STEP_END
+
+ // Output:
+ // 5
+ // OK
+ // [bike:5 bike:4 bike:3]
+}
diff --git a/doctests/sets_example_test.go b/doctests/sets_example_test.go
new file mode 100644
index 000000000..7446a2789
--- /dev/null
+++ doctests/sets_example_test.go
@@ -0,0 +1,442 @@
+// EXAMPLE: sets_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+func ExampleClient_sadd() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ rdb.Del(ctx, "bikes:racing:usa")
+ // REMOVE_END
+
+ // STEP_START sadd
+ res1, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> 1
+
+ res2, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> 0
+
+ res3, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> 2
+
+ res4, err := rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4) // >>> 2
+ // STEP_END
+
+ // Output:
+ // 1
+ // 0
+ // 2
+ // 2
+}
+
+func ExampleClient_sismember() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ rdb.Del(ctx, "bikes:racing:usa")
+ // REMOVE_END
+
+ _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START sismember
+ res5, err := rdb.SIsMember(ctx, "bikes:racing:usa", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res5) // >>> true
+
+ res6, err := rdb.SIsMember(ctx, "bikes:racing:usa", "bike:2").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res6) // >>> false
+ // STEP_END
+
+ // Output:
+ // true
+ // false
+}
+
+func ExampleClient_sinter() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ rdb.Del(ctx, "bikes:racing:usa")
+ // REMOVE_END
+
+ _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START sinter
+ res7, err := rdb.SInter(ctx, "bikes:racing:france", "bikes:racing:usa").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res7) // >>> [bike:1]
+ // STEP_END
+
+ // Output:
+ // [bike:1]
+}
+
+func ExampleClient_scard() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ // REMOVE_END
+
+ _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START scard
+ res8, err := rdb.SCard(ctx, "bikes:racing:france").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res8) // >>> 3
+ // STEP_END
+
+ // Output:
+ // 3
+}
+
+func ExampleClient_saddsmembers() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ // REMOVE_END
+
+ // STEP_START sadd_smembers
+ res9, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res9) // >>> 3
+
+ res10, err := rdb.SMembers(ctx, "bikes:racing:france").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res10) // >>> [bike:1 bike:2 bike:3]
+ // STEP_END
+
+ // Output:
+ // 3
+ // [bike:1 bike:2 bike:3]
+}
+
+func ExampleClient_smismember() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ // REMOVE_END
+
+ _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START smismember
+ res11, err := rdb.SIsMember(ctx, "bikes:racing:france", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res11) // >>> true
+
+ res12, err := rdb.SMIsMember(ctx, "bikes:racing:france", "bike:2", "bike:3", "bike:4").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res12) // >>> [true true false]
+ // STEP_END
+
+ // Output:
+ // true
+ // [true true false]
+}
+
+func ExampleClient_sdiff() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+, // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ rdb.Del(ctx, "bikes:racing:usa")
+ // REMOVE_END
+
+ // STEP_START sdiff
+ _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+ res13, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res13) // >>> [bike:2 bike:3]
+ // STEP_END
+
+ // Output:
+ // [bike:2 bike:3]
+}
+
+func ExampleClient_multisets() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ rdb.Del(ctx, "bikes:racing:usa")
+ rdb.Del(ctx, "bikes:racing:italy")
+ // REMOVE_END
+
+ // STEP_START multisets
+ _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = rdb.SAdd(ctx, "bikes:racing:italy", "bike:1", "bike:2", "bike:3", "bike:4").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ res14, err := rdb.SInter(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res14) // >>> [bike:1]
+
+ res15, err := rdb.SUnion(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res15) // >>> [bike:1 bike:2 bike:3 bike:4]
+
+ res16, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res16) // >>> []
+
+ res17, err := rdb.SDiff(ctx, "bikes:racing:usa", "bikes:racing:france").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res17) // >>> [bike:4]
+
+ res18, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res18) // >>> [bike:2 bike:3]
+ // STEP_END
+
+ // Output:
+ // [bike:1]
+ // [bike:1 bike:2 bike:3 bike:4]
+ // []
+ // [bike:4]
+ // [bike:2 bike:3]
+}
+
+func ExampleClient_srem() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bikes:racing:france")
+ // REMOVE_END
+
+ // STEP_START srem
+ _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ res19, err := rdb.SRem(ctx, "bikes:racing:france", "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res19) // >>> 1
+
+ res20, err := rdb.SPop(ctx, "bikes:racing:france").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res20) // >>> <random element>
+
+ res21, err := rdb.SMembers(ctx, "bikes:racing:france").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res21) // >>> <remaining elements>
+
+ res22, err := rdb.SRandMember(ctx, "bikes:racing:france").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res22) // >>> <random element>
+ // STEP_END
+
+ // Testable examples not available because the test output
+ // is not deterministic.
+}
diff --git a/doctests/ss_tutorial_test.go b/doctests/ss_tutorial_test.go
new file mode 100644
index 000000000..2a6924458
--- /dev/null
+++ doctests/ss_tutorial_test.go
@@ -0,0 +1,437 @@
+// EXAMPLE: ss_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+func ExampleClient_zadd() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_scores")
+ // REMOVE_END
+
+ // STEP_START zadd
+ res1, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 10},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> 1
+
+ res2, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Castilla", Score: 12},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> 1
+
+ res3, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 10},
+ redis.Z{Member: "Sam-Bodden", Score: 8},
+ redis.Z{Member: "Royce", Score: 10},
+ redis.Z{Member: "Ford", Score: 6},
+ redis.Z{Member: "Prickett", Score: 14},
+ redis.Z{Member: "Castilla", Score: 12},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> 4
+ // STEP_END
+
+ // Output:
+ // 1
+ // 1
+ // 4
+}
+
+func ExampleClient_zrange() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_scores")
+ // REMOVE_END
+
+ _, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 10},
+ redis.Z{Member: "Sam-Bodden", Score: 8},
+ redis.Z{Member: "Royce", Score: 10},
+ redis.Z{Member: "Ford", Score: 6},
+ redis.Z{Member: "Prickett", Score: 14},
+ redis.Z{Member: "Castilla", Score: 12},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START zrange
+ res4, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4)
+ // >>> [Ford Sam-Bodden Norem Royce Castilla Prickett]
+
+ res5, err := rdb.ZRevRange(ctx, "racer_scores", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res5)
+ // >>> [Prickett Castilla Royce Norem Sam-Bodden Ford]
+ // STEP_END
+
+ // Output:
+ // [Ford Sam-Bodden Norem Royce Castilla Prickett]
+ // [Prickett Castilla Royce Norem Sam-Bodden Ford]
+}
+
+func ExampleClient_zrangewithscores() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_scores")
+ // REMOVE_END
+
+ _, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 10},
+ redis.Z{Member: "Sam-Bodden", Score: 8},
+ redis.Z{Member: "Royce", Score: 10},
+ redis.Z{Member: "Ford", Score: 6},
+ redis.Z{Member: "Prickett", Score: 14},
+ redis.Z{Member: "Castilla", Score: 12},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START zrange_withscores
+ res6, err := rdb.ZRangeWithScores(ctx, "racer_scores", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res6)
+ // >>> [{6 Ford} {8 Sam-Bodden} {10 Norem} {10 Royce} {12 Castilla} {14 Prickett}]
+ // STEP_END
+
+ // Output:
+ // [{6 Ford} {8 Sam-Bodden} {10 Norem} {10 Royce} {12 Castilla} {14 Prickett}]
+}
+
+func ExampleClient_zrangebyscore() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_scores")
+ // REMOVE_END
+
+ _, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 10},
+ redis.Z{Member: "Sam-Bodden", Score: 8},
+ redis.Z{Member: "Royce", Score: 10},
+ redis.Z{Member: "Ford", Score: 6},
+ redis.Z{Member: "Prickett", Score: 14},
+ redis.Z{Member: "Castilla", Score: 12},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START zrangebyscore
+ res7, err := rdb.ZRangeByScore(ctx, "racer_scores",
+ &redis.ZRangeBy{Min: "-inf", Max: "10"},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res7)
+ // >>> [Ford Sam-Bodden Norem Royce]
+ // STEP_END
+
+ // Output:
+ // [Ford Sam-Bodden Norem Royce]
+}
+
+func ExampleClient_zremrangebyscore() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_scores")
+ // REMOVE_END
+
+ _, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 10},
+ redis.Z{Member: "Sam-Bodden", Score: 8},
+ redis.Z{Member: "Royce", Score: 10},
+ redis.Z{Member: "Ford", Score: 6},
+ redis.Z{Member: "Prickett", Score: 14},
+ redis.Z{Member: "Castilla", Score: 12},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START zremrangebyscore
+ res8, err := rdb.ZRem(ctx, "racer_scores", "Castilla").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res8) // >>> 1
+
+ res9, err := rdb.ZRemRangeByScore(ctx, "racer_scores", "-inf", "9").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res9) // >>> 2
+
+ res10, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res10)
+ // >>> [Norem Royce Prickett]
+ // STEP_END
+
+ // Output:
+ // 1
+ // 2
+ // [Norem Royce Prickett]
+}
+
+func ExampleClient_zrank() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_scores")
+ // REMOVE_END
+
+ _, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 10},
+ redis.Z{Member: "Royce", Score: 10},
+ redis.Z{Member: "Prickett", Score: 14},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START zrank
+ res11, err := rdb.ZRank(ctx, "racer_scores", "Norem").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res11) // >>> 0
+
+ res12, err := rdb.ZRevRank(ctx, "racer_scores", "Norem").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res12) // >>> 2
+ // STEP_END
+
+ // Output:
+ // 0
+ // 2
+}
+
+func ExampleClient_zaddlex() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_scores")
+ // REMOVE_END
+
+ _, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 0},
+ redis.Z{Member: "Royce", Score: 0},
+ redis.Z{Member: "Prickett", Score: 0},
+ ).Result()
+
+ // STEP_START zadd_lex
+ res13, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Norem", Score: 0},
+ redis.Z{Member: "Sam-Bodden", Score: 0},
+ redis.Z{Member: "Royce", Score: 0},
+ redis.Z{Member: "Ford", Score: 0},
+ redis.Z{Member: "Prickett", Score: 0},
+ redis.Z{Member: "Castilla", Score: 0},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res13) // >>> 3
+
+ res14, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res14)
+ // >>> [Castilla Ford Norem Prickett Royce Sam-Bodden]
+
+ res15, err := rdb.ZRangeByLex(ctx, "racer_scores", &redis.ZRangeBy{
+ Min: "[A", Max: "[L",
+ }).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res15) // >>> [Castilla Ford]
+ // STEP_END
+
+ // Output:
+ // 3
+ // [Castilla Ford Norem Prickett Royce Sam-Bodden]
+ // [Castilla Ford]
+}
+
+func ExampleClient_leaderboard() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_scores")
+ // REMOVE_END
+
+ // STEP_START leaderboard
+ res16, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Wood", Score: 100},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res16) // >>> 1
+
+ res17, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Henshaw", Score: 100},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res17) // >>> 1
+
+ res18, err := rdb.ZAdd(ctx, "racer_scores",
+ redis.Z{Member: "Henshaw", Score: 150},
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res18) // >>> 0
+
+ res19, err := rdb.ZIncrBy(ctx, "racer_scores", 50, "Wood").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res19) // >>> 150
+
+ res20, err := rdb.ZIncrBy(ctx, "racer_scores", 50, "Henshaw").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res20) // >>> 200
+ // STEP_END
+
+ // Output:
+ // 1
+ // 1
+ // 0
+ // 150
+ // 200
+}
diff --git a/doctests/string_example_test.go b/doctests/string_example_test.go
new file mode 100644
index 000000000..20ca85548
--- /dev/null
+++ doctests/string_example_test.go
@@ -0,0 +1,173 @@
+// EXAMPLE: set_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+func ExampleClient_set_get() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike:1")
+ // REMOVE_END
+
+ // STEP_START set_get
+ res1, err := rdb.Set(ctx, "bike:1", "Deimos", 0).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> OK
+
+ res2, err := rdb.Get(ctx, "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> Deimos
+ // STEP_END
+
+ // Output:
+ // OK
+ // Deimos
+}
+
+func ExampleClient_setnx_xx() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Set(ctx, "bike:1", "Deimos", 0)
+ // REMOVE_END
+
+ // STEP_START setnx_xx
+ res3, err := rdb.SetNX(ctx, "bike:1", "bike", 0).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> false
+
+ res4, err := rdb.Get(ctx, "bike:1").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4) // >>> Deimos
+
+ res5, err := rdb.SetXX(ctx, "bike:1", "bike", 0).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res5) // >>> OK
+ // STEP_END
+
+ // Output:
+ // false
+ // Deimos
+ // true
+}
+
+func ExampleClient_mset() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "bike:1", "bike:2", "bike:3")
+ // REMOVE_END
+
+ // STEP_START mset
+ res6, err := rdb.MSet(ctx, "bike:1", "Deimos", "bike:2", "Ares", "bike:3", "Vanth").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res6) // >>> OK
+
+ res7, err := rdb.MGet(ctx, "bike:1", "bike:2", "bike:3").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res7) // >>> [Deimos Ares Vanth]
+ // STEP_END
+
+ // Output:
+ // OK
+ // [Deimos Ares Vanth]
+}
+
+func ExampleClient_incr() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "total_crashes")
+ // REMOVE_END
+
+ // STEP_START incr
+ res8, err := rdb.Set(ctx, "total_crashes", "0", 0).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res8) // >>> OK
+
+ res9, err := rdb.Incr(ctx, "total_crashes").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res9) // >>> 1
+
+ res10, err := rdb.IncrBy(ctx, "total_crashes", 10).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res10) // >>> 11
+ // STEP_END
+
+ // Output:
+ // OK
+ // 1
+ // 11
+}
diff --git a/doctests/tdigest_tutorial_test.go b/doctests/tdigest_tutorial_test.go
new file mode 100644
index 000000000..7589b0ec8
--- /dev/null
+++ doctests/tdigest_tutorial_test.go
@@ -0,0 +1,251 @@
+// EXAMPLE: tdigest_tutorial
+// HIDE_START
+package example_commands_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+)
+
+// HIDE_END
+
+func ExampleClient_tdigstart() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_ages", "bikes:sales")
+ // REMOVE_END
+
+ // STEP_START tdig_start
+ res1, err := rdb.TDigestCreate(ctx, "bikes:sales").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res1) // >>> OK
+
+ res2, err := rdb.TDigestAdd(ctx, "bikes:sales", 21).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res2) // >>> OK
+
+ res3, err := rdb.TDigestAdd(ctx, "bikes:sales",
+ 150, 95, 75, 34,
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res3) // >>> OK
+
+ // STEP_END
+
+ // Output:
+ // OK
+ // OK
+ // OK
+}
+
+func ExampleClient_tdigcdf() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_ages", "bikes:sales")
+ // REMOVE_END
+
+ // STEP_START tdig_cdf
+ res4, err := rdb.TDigestCreate(ctx, "racer_ages").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res4) // >>> OK
+
+ res5, err := rdb.TDigestAdd(ctx, "racer_ages",
+ 45.88, 44.2, 58.03, 19.76, 39.84, 69.28,
+ 50.97, 25.41, 19.27, 85.71, 42.63,
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res5) // >>> OK
+
+ res6, err := rdb.TDigestRank(ctx, "racer_ages", 50).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res6) // >>> [7]
+
+ res7, err := rdb.TDigestRank(ctx, "racer_ages", 50, 40).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res7) // >>> [7 4]
+ // STEP_END
+
+ // Output:
+ // OK
+ // OK
+ // [7]
+ // [7 4]
+}
+
+func ExampleClient_tdigquant() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_ages")
+ // REMOVE_END
+
+ _, err := rdb.TDigestCreate(ctx, "racer_ages").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = rdb.TDigestAdd(ctx, "racer_ages",
+ 45.88, 44.2, 58.03, 19.76, 39.84, 69.28,
+ 50.97, 25.41, 19.27, 85.71, 42.63,
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START tdig_quant
+ res8, err := rdb.TDigestQuantile(ctx, "racer_ages", 0.5).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res8) // >>> [44.2]
+
+ res9, err := rdb.TDigestByRank(ctx, "racer_ages", 4).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res9) // >>> [42.63]
+ // STEP_END
+
+ // Output:
+ // [44.2]
+ // [42.63]
+}
+
+func ExampleClient_tdigmin() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_ages")
+ // REMOVE_END
+
+ _, err := rdb.TDigestCreate(ctx, "racer_ages").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = rdb.TDigestAdd(ctx, "racer_ages",
+ 45.88, 44.2, 58.03, 19.76, 39.84, 69.28,
+ 50.97, 25.41, 19.27, 85.71, 42.63,
+ ).Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START tdig_min
+ res10, err := rdb.TDigestMin(ctx, "racer_ages").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res10) // >>> 19.27
+
+ res11, err := rdb.TDigestMax(ctx, "racer_ages").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res11) // >>> 85.71
+ // STEP_END
+
+ // Output:
+ // 19.27
+ // 85.71
+}
+
+func ExampleClient_tdigreset() {
+ ctx := context.Background()
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // no password docs
+ DB: 0, // use default DB
+ })
+
+ // REMOVE_START
+ rdb.Del(ctx, "racer_ages")
+ // REMOVE_END
+ _, err := rdb.TDigestCreate(ctx, "racer_ages").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ // STEP_START tdig_reset
+ res12, err := rdb.TDigestReset(ctx, "racer_ages").Result()
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res12) // >>> OK
+ // STEP_END
+
+ // Output:
+ // OK
+}
diff --git example/del-keys-without-ttl/go.mod example/del-keys-without-ttl/go.mod
index 468d0a54f..c631a1bdf 100644
--- example/del-keys-without-ttl/go.mod
+++ example/del-keys-without-ttl/go.mod
@@ -5,7 +5,7 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require (
- github.com/redis/go-redis/v9 v9.5.3
+ github.com/redis/go-redis/v9 v9.7.3
go.uber.org/zap v1.24.0
)
diff --git example/hll/go.mod example/hll/go.mod
index 0126764ef..885573780 100644
--- example/hll/go.mod
+++ example/hll/go.mod
@@ -4,7 +4,7 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
-require github.com/redis/go-redis/v9 v9.5.3
+require github.com/redis/go-redis/v9 v9.7.3
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
diff --git example/lua-scripting/go.mod example/lua-scripting/go.mod
index 3f4c29d12..c15f410be 100644
--- example/lua-scripting/go.mod
+++ example/lua-scripting/go.mod
@@ -4,7 +4,7 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
-require github.com/redis/go-redis/v9 v9.5.3
+require github.com/redis/go-redis/v9 v9.7.3
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
diff --git example/otel/go.mod example/otel/go.mod
index fea4e72a5..de6fc56d0 100644
--- example/otel/go.mod
+++ example/otel/go.mod
@@ -9,8 +9,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd
require (
- github.com/redis/go-redis/extra/redisotel/v9 v9.5.3
- github.com/redis/go-redis/v9 v9.5.3
+ github.com/redis/go-redis/extra/redisotel/v9 v9.7.3
+ github.com/redis/go-redis/v9 v9.7.3
github.com/uptrace/uptrace-go v1.21.0
go.opentelemetry.io/otel v1.22.0
)
@@ -23,7 +23,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
- github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 // indirect
+ github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
diff --git example/redis-bloom/go.mod example/redis-bloom/go.mod
index 09fb5bed4..7e46b42db 100644
--- example/redis-bloom/go.mod
+++ example/redis-bloom/go.mod
@@ -4,7 +4,7 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
-require github.com/redis/go-redis/v9 v9.5.3
+require github.com/redis/go-redis/v9 v9.7.3
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
diff --git example/scan-struct/go.mod example/scan-struct/go.mod
index c01e31293..b1bf250f3 100644
--- example/scan-struct/go.mod
+++ example/scan-struct/go.mod
@@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../..
require (
github.com/davecgh/go-spew v1.1.1
- github.com/redis/go-redis/v9 v9.5.3
+ github.com/redis/go-redis/v9 v9.7.3
)
require (
diff --git extra/rediscensus/go.mod extra/rediscensus/go.mod
index d623cef36..bf8bb290d 100644
--- extra/rediscensus/go.mod
+++ extra/rediscensus/go.mod
@@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../..
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd
require (
- github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3
- github.com/redis/go-redis/v9 v9.5.3
+ github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3
+ github.com/redis/go-redis/v9 v9.7.3
go.opencensus.io v0.24.0
)
@@ -17,3 +17,8 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
)
+
+retract (
+ v9.5.3 // This version was accidentally released.
+ v9.7.2 // This version was accidentally released.
+)
diff --git extra/rediscmd/go.mod extra/rediscmd/go.mod
index a035c8694..d9dbfefb7 100644
--- extra/rediscmd/go.mod
+++ extra/rediscmd/go.mod
@@ -7,10 +7,15 @@ replace github.com/redis/go-redis/v9 => ../..
require (
github.com/bsm/ginkgo/v2 v2.12.0
github.com/bsm/gomega v1.27.10
- github.com/redis/go-redis/v9 v9.5.3
+ github.com/redis/go-redis/v9 v9.7.3
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)
+
+retract (
+ v9.5.3 // This version was accidentally released.
+ v9.7.2 // This version was accidentally released.
+)
diff --git extra/redisotel/go.mod extra/redisotel/go.mod
index 587d3bc3a..f51799ed0 100644
--- extra/redisotel/go.mod
+++ extra/redisotel/go.mod
@@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../..
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd
require (
- github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3
- github.com/redis/go-redis/v9 v9.5.3
+ github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3
+ github.com/redis/go-redis/v9 v9.7.3
go.opentelemetry.io/otel v1.22.0
go.opentelemetry.io/otel/metric v1.22.0
go.opentelemetry.io/otel/sdk v1.22.0
@@ -22,3 +22,8 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
golang.org/x/sys v0.16.0 // indirect
)
+
+retract (
+ v9.5.3 // This version was accidentally released.
+ v9.7.2 // This version was accidentally released.
+)
diff --git extra/redisprometheus/go.mod extra/redisprometheus/go.mod
index fcc35b9bd..477d3f000 100644
--- extra/redisprometheus/go.mod
+++ extra/redisprometheus/go.mod
@@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../..
require (
github.com/prometheus/client_golang v1.14.0
- github.com/redis/go-redis/v9 v9.5.3
+ github.com/redis/go-redis/v9 v9.7.3
)
require (
@@ -21,3 +21,8 @@ require (
golang.org/x/sys v0.4.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)
+
+retract (
+ v9.5.3 // This version was accidentally released.
+ v9.7.2 // This version was accidentally released.
+)
diff --git go.mod go.mod
index 6c65f094f..7a2c500a7 100644
--- go.mod
+++ go.mod
@@ -8,3 +8,9 @@ require (
github.com/cespare/xxhash/v2 v2.2.0
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
)
+
+retract (
+ v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead.
+ v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead.
+ v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead.
+)
diff --git hash_commands.go hash_commands.go
index dcffdcdd9..6596c6f5f 100644
--- hash_commands.go
+++ hash_commands.go
@@ -225,7 +225,7 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati
return cmd
}
-// HExpire - Sets the expiration time for specified fields in a hash in seconds.
+// HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds.
// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.
// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
// For more information - https://redis.io/commands/hexpire/
diff --git internal/pool/conn_check.go internal/pool/conn_check.go
index 07c261c2b..83190d394 100644
--- internal/pool/conn_check.go
+++ internal/pool/conn_check.go
@@ -3,7 +3,6 @@
package pool
import (
- "crypto/tls"
"errors"
"io"
"net"
@@ -17,10 +16,6 @@ func connCheck(conn net.Conn) error {
// Reset previous timeout.
_ = conn.SetDeadline(time.Time{})
- // Check if tls.Conn.
- if c, ok := conn.(*tls.Conn); ok {
- conn = c.NetConn()
- }
sysConn, ok := conn.(syscall.Conn)
if !ok {
return nil
diff --git internal/pool/conn_check_test.go internal/pool/conn_check_test.go
index 214993339..2ade8a0b9 100644
--- internal/pool/conn_check_test.go
+++ internal/pool/conn_check_test.go
@@ -3,7 +3,6 @@
package pool
import (
- "crypto/tls"
"net"
"net/http/httptest"
"time"
@@ -15,17 +14,12 @@ import (
var _ = Describe("tests conn_check with real conns", func() {
var ts *httptest.Server
var conn net.Conn
- var tlsConn *tls.Conn
var err error
BeforeEach(func() {
ts = httptest.NewServer(nil)
conn, err = net.DialTimeout(ts.Listener.Addr().Network(), ts.Listener.Addr().String(), time.Second)
Expect(err).NotTo(HaveOccurred())
- tlsTestServer := httptest.NewUnstartedServer(nil)
- tlsTestServer.StartTLS()
- tlsConn, err = tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, tlsTestServer.Listener.Addr().Network(), tlsTestServer.Listener.Addr().String(), &tls.Config{InsecureSkipVerify: true})
- Expect(err).NotTo(HaveOccurred())
})
AfterEach(func() {
@@ -39,23 +33,11 @@ var _ = Describe("tests conn_check with real conns", func() {
Expect(connCheck(conn)).To(HaveOccurred())
})
- It("good tls conn check", func() {
- Expect(connCheck(tlsConn)).NotTo(HaveOccurred())
-
- Expect(tlsConn.Close()).NotTo(HaveOccurred())
- Expect(connCheck(tlsConn)).To(HaveOccurred())
- })
-
It("bad conn check", func() {
Expect(conn.Close()).NotTo(HaveOccurred())
Expect(connCheck(conn)).To(HaveOccurred())
})
- It("bad tls conn check", func() {
- Expect(tlsConn.Close()).NotTo(HaveOccurred())
- Expect(connCheck(tlsConn)).To(HaveOccurred())
- })
-
It("check conn deadline", func() {
Expect(conn.SetDeadline(time.Now())).NotTo(HaveOccurred())
time.Sleep(time.Millisecond * 10)
diff --git json.go json.go
index ca731db3a..b3cadf4b7 100644
--- json.go
+++ json.go
@@ -60,7 +60,7 @@ type JSONArrTrimArgs struct {
type JSONCmd struct {
baseCmd
val string
- expanded []interface{}
+ expanded interface{}
}
var _ Cmder = (*JSONCmd)(nil)
@@ -100,11 +100,11 @@ func (cmd *JSONCmd) Result() (string, error) {
return cmd.Val(), cmd.Err()
}
-func (cmd JSONCmd) Expanded() (interface{}, error) {
+func (cmd *JSONCmd) Expanded() (interface{}, error) {
if len(cmd.val) != 0 && cmd.expanded == nil {
err := json.Unmarshal([]byte(cmd.val), &cmd.expanded)
if err != nil {
- return "", err
+ return nil, err
}
}
@@ -494,7 +494,7 @@ func (c cmdable) JSONMSet(ctx context.Context, params ...interface{}) *StatusCmd
}
// JSONNumIncrBy increments the number value stored at the specified path by the provided number.
-// For more information, see https://redis.io/commands/json.numincreby
+// For more information, see https://redis.io/docs/latest/commands/json.numincrby/
func (c cmdable) JSONNumIncrBy(ctx context.Context, key, path string, value float64) *JSONCmd {
args := []interface{}{"JSON.NUMINCRBY", key, path, value}
cmd := newJSONCmd(ctx, args...)
diff --git json_test.go json_test.go
index d1ea24290..9139be3ac 100644
--- json_test.go
+++ json_test.go
@@ -2,6 +2,8 @@ package redis_test
import (
"context"
+ "encoding/json"
+ "time"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
@@ -17,644 +19,800 @@ var _ = Describe("JSON Commands", Label("json"), func() {
ctx := context.TODO()
var client *redis.Client
- BeforeEach(func() {
- client = redis.NewClient(&redis.Options{Addr: ":6379"})
- Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred())
- })
-
- AfterEach(func() {
- Expect(client.Close()).NotTo(HaveOccurred())
- })
-
- Describe("arrays", Label("arrays"), func() {
- It("should JSONArrAppend", Label("json.arrappend", "json"), func() {
- cmd1 := client.JSONSet(ctx, "append2", "$", `{"a": [10], "b": {"a": [12, 13]}}`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONArrAppend(ctx, "append2", "$..a", 10)
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal([]int64{2, 3}))
+ setupRedisClient := func(protocolVersion int) *redis.Client {
+ return redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ DB: 0,
+ Protocol: protocolVersion,
+ UnstableResp3: true,
})
+ }
- It("should JSONArrIndex and JSONArrIndexWithArgs", Label("json.arrindex", "json"), func() {
- cmd1, err := client.JSONSet(ctx, "index1", "$", `{"a": [10], "b": {"a": [12, 10]}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd1).To(Equal("OK"))
-
- cmd2, err := client.JSONArrIndex(ctx, "index1", "$.b.a", 10).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd2).To(Equal([]int64{1}))
-
- cmd3, err := client.JSONSet(ctx, "index2", "$", `[0,1,2,3,4]`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd3).To(Equal("OK"))
-
- res, err := client.JSONArrIndex(ctx, "index2", "$", 1).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res[0]).To(Equal(int64(1)))
-
- res, err = client.JSONArrIndex(ctx, "index2", "$", 1, 2).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res[0]).To(Equal(int64(-1)))
-
- res, err = client.JSONArrIndex(ctx, "index2", "$", 4).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res[0]).To(Equal(int64(4)))
-
- res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{}, 4).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res[0]).To(Equal(int64(4)))
-
- stop := 5000
- res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res[0]).To(Equal(int64(4)))
-
- stop = -1
- res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res[0]).To(Equal(int64(-1)))
- })
-
- It("should JSONArrIndex and JSONArrIndexWithArgs with $", Label("json.arrindex", "json"), func() {
- doc := `{
- "store": {
- "book": [
- {
- "category": "reference",
- "author": "Nigel Rees",
- "title": "Sayings of the Century",
- "price": 8.95,
- "size": [10, 20, 30, 40]
- },
- {
- "category": "fiction",
- "author": "Evelyn Waugh",
- "title": "Sword of Honour",
- "price": 12.99,
- "size": [50, 60, 70, 80]
- },
- {
- "category": "fiction",
- "author": "Herman Melville",
- "title": "Moby Dick",
- "isbn": "0-553-21311-3",
- "price": 8.99,
- "size": [5, 10, 20, 30]
- },
- {
- "category": "fiction",
- "author": "J. R. R. Tolkien",
- "title": "The Lord of the Rings",
- "isbn": "0-395-19395-8",
- "price": 22.99,
- "size": [5, 6, 7, 8]
- }
- ],
- "bicycle": {"color": "red", "price": 19.95}
- }
- }`
- res, err := client.JSONSet(ctx, "doc1", "$", doc).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- resGet, err := client.JSONGet(ctx, "doc1", "$.store.book[?(@.price<10)].size").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]"))
-
- resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resArr).To(Equal([]int64{1, 2}))
- })
-
- It("should JSONArrInsert", Label("json.arrinsert", "json"), func() {
- cmd1 := client.JSONSet(ctx, "insert2", "$", `[100, 200, 300, 200]`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONArrInsert(ctx, "insert2", "$", -1, 1, 2)
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal([]int64{6}))
-
- cmd3 := client.JSONGet(ctx, "insert2")
- Expect(cmd3.Err()).NotTo(HaveOccurred())
- // RESP2 vs RESP3
- Expect(cmd3.Val()).To(Or(
- Equal(`[100,200,300,1,2,200]`),
- Equal(`[[100,200,300,1,2,200]]`)))
- })
-
- It("should JSONArrLen", Label("json.arrlen", "json"), func() {
- cmd1 := client.JSONSet(ctx, "length2", "$", `{"a": [10], "b": {"a": [12, 10, 20, 12, 90, 10]}}`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONArrLen(ctx, "length2", "$..a")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal([]int64{1, 6}))
- })
-
- It("should JSONArrPop", Label("json.arrpop"), func() {
- cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2)
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal([]string{"300"}))
-
- cmd3 := client.JSONGet(ctx, "pop4", "$")
- Expect(cmd3.Err()).NotTo(HaveOccurred())
- Expect(cmd3.Val()).To(Equal("[[100,200,200]]"))
- })
-
- It("should JSONArrTrim", Label("json.arrtrim", "json"), func() {
- cmd1, err := client.JSONSet(ctx, "trim1", "$", `[0,1,2,3,4]`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd1).To(Equal("OK"))
-
- stop := 3
- cmd2, err := client.JSONArrTrimWithArgs(ctx, "trim1", "$", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd2).To(Equal([]int64{3}))
-
- res, err := client.JSONGet(ctx, "trim1", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(`[[1,2,3]]`))
-
- cmd3, err := client.JSONSet(ctx, "trim2", "$", `[0,1,2,3,4]`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd3).To(Equal("OK"))
-
- stop = 3
- cmd4, err := client.JSONArrTrimWithArgs(ctx, "trim2", "$", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd4).To(Equal([]int64{0}))
-
- cmd5, err := client.JSONSet(ctx, "trim3", "$", `[0,1,2,3,4]`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd5).To(Equal("OK"))
-
- stop = 99
- cmd6, err := client.JSONArrTrimWithArgs(ctx, "trim3", "$", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd6).To(Equal([]int64{2}))
-
- cmd7, err := client.JSONSet(ctx, "trim4", "$", `[0,1,2,3,4]`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd7).To(Equal("OK"))
-
- stop = 1
- cmd8, err := client.JSONArrTrimWithArgs(ctx, "trim4", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd8).To(Equal([]int64{0}))
-
- cmd9, err := client.JSONSet(ctx, "trim5", "$", `[0,1,2,3,4]`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd9).To(Equal("OK"))
-
- stop = 11
- cmd10, err := client.JSONArrTrimWithArgs(ctx, "trim5", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd10).To(Equal([]int64{0}))
- })
-
- It("should JSONArrPop", Label("json.arrpop", "json"), func() {
- cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2)
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal([]string{"300"}))
-
- cmd3 := client.JSONGet(ctx, "pop4", "$")
- Expect(cmd3.Err()).NotTo(HaveOccurred())
- Expect(cmd3.Val()).To(Equal("[[100,200,200]]"))
- })
+ AfterEach(func() {
+ if client != nil {
+ client.FlushDB(ctx)
+ client.Close()
+ }
})
- Describe("get/set", Label("getset"), func() {
- It("should JSONSet", Label("json.set", "json"), func() {
- cmd := client.JSONSet(ctx, "set1", "$", `{"a": 1, "b": 2, "hello": "world"}`)
- Expect(cmd.Err()).NotTo(HaveOccurred())
- Expect(cmd.Val()).To(Equal("OK"))
+ protocols := []int{2, 3}
+ for _, protocol := range protocols {
+ BeforeEach(func() {
+ client = setupRedisClient(protocol)
+ Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred())
})
- It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() {
- res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(`{-"a":1,-"b":2}`))
-
- res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`))
+ Describe("arrays", Label("arrays"), func() {
+ It("should JSONArrAppend", Label("json.arrappend", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "append2", "$", `{"a": [10], "b": {"a": [12, 13]}}`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONArrAppend(ctx, "append2", "$..a", 10)
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal([]int64{2, 3}))
+ })
+
+ It("should JSONArrIndex and JSONArrIndexWithArgs", Label("json.arrindex", "json"), func() {
+ cmd1, err := client.JSONSet(ctx, "index1", "$", `{"a": [10], "b": {"a": [12, 10]}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd1).To(Equal("OK"))
+
+ cmd2, err := client.JSONArrIndex(ctx, "index1", "$.b.a", 10).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd2).To(Equal([]int64{1}))
+
+ cmd3, err := client.JSONSet(ctx, "index2", "$", `[0,1,2,3,4]`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd3).To(Equal("OK"))
+
+ res, err := client.JSONArrIndex(ctx, "index2", "$", 1).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res[0]).To(Equal(int64(1)))
+
+ res, err = client.JSONArrIndex(ctx, "index2", "$", 1, 2).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res[0]).To(Equal(int64(-1)))
+
+ res, err = client.JSONArrIndex(ctx, "index2", "$", 4).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res[0]).To(Equal(int64(4)))
+
+ res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{}, 4).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res[0]).To(Equal(int64(4)))
+
+ stop := 5000
+ res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res[0]).To(Equal(int64(4)))
+
+ stop = -1
+ res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res[0]).To(Equal(int64(-1)))
+ })
+
+ It("should JSONArrIndex and JSONArrIndexWithArgs with $", Label("json.arrindex", "json"), func() {
+ doc := `{
+ "store": {
+ "book": [
+ {
+ "category": "reference",
+ "author": "Nigel Rees",
+ "title": "Sayings of the Century",
+ "price": 8.95,
+ "size": [10, 20, 30, 40]
+ },
+ {
+ "category": "fiction",
+ "author": "Evelyn Waugh",
+ "title": "Sword of Honour",
+ "price": 12.99,
+ "size": [50, 60, 70, 80]
+ },
+ {
+ "category": "fiction",
+ "author": "Herman Melville",
+ "title": "Moby Dick",
+ "isbn": "0-553-21311-3",
+ "price": 8.99,
+ "size": [5, 10, 20, 30]
+ },
+ {
+ "category": "fiction",
+ "author": "J. R. R. Tolkien",
+ "title": "The Lord of the Rings",
+ "isbn": "0-395-19395-8",
+ "price": 22.99,
+ "size": [5, 6, 7, 8]
+ }
+ ],
+ "bicycle": {"color": "red", "price": 19.95}
+ }
+ }`
+ res, err := client.JSONSet(ctx, "doc1", "$", doc).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ resGet, err := client.JSONGet(ctx, "doc1", "$.store.book[?(@.price<10)].size").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]"))
+
+ resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resArr).To(Equal([]int64{1, 2}))
+ })
+
+ It("should JSONArrInsert", Label("json.arrinsert", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "insert2", "$", `[100, 200, 300, 200]`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONArrInsert(ctx, "insert2", "$", -1, 1, 2)
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal([]int64{6}))
+
+ cmd3 := client.JSONGet(ctx, "insert2")
+ Expect(cmd3.Err()).NotTo(HaveOccurred())
+ // RESP2 vs RESP3
+ Expect(cmd3.Val()).To(Or(
+ Equal(`[100,200,300,1,2,200]`),
+ Equal(`[[100,200,300,1,2,200]]`)))
+ })
+
+ It("should JSONArrLen", Label("json.arrlen", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "length2", "$", `{"a": [10], "b": {"a": [12, 10, 20, 12, 90, 10]}}`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONArrLen(ctx, "length2", "$..a")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal([]int64{1, 6}))
+ })
+
+ It("should JSONArrPop", Label("json.arrpop"), func() {
+ cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2)
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal([]string{"300"}))
+
+ cmd3 := client.JSONGet(ctx, "pop4", "$")
+ Expect(cmd3.Err()).NotTo(HaveOccurred())
+ Expect(cmd3.Val()).To(Equal("[[100,200,200]]"))
+ })
+
+ It("should JSONArrTrim", Label("json.arrtrim", "json"), func() {
+ cmd1, err := client.JSONSet(ctx, "trim1", "$", `[0,1,2,3,4]`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd1).To(Equal("OK"))
+
+ stop := 3
+ cmd2, err := client.JSONArrTrimWithArgs(ctx, "trim1", "$", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd2).To(Equal([]int64{3}))
+
+ res, err := client.JSONGet(ctx, "trim1", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal(`[[1,2,3]]`))
+
+ cmd3, err := client.JSONSet(ctx, "trim2", "$", `[0,1,2,3,4]`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd3).To(Equal("OK"))
+
+ stop = 3
+ cmd4, err := client.JSONArrTrimWithArgs(ctx, "trim2", "$", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd4).To(Equal([]int64{0}))
+
+ cmd5, err := client.JSONSet(ctx, "trim3", "$", `[0,1,2,3,4]`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd5).To(Equal("OK"))
+
+ stop = 99
+ cmd6, err := client.JSONArrTrimWithArgs(ctx, "trim3", "$", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd6).To(Equal([]int64{2}))
+
+ cmd7, err := client.JSONSet(ctx, "trim4", "$", `[0,1,2,3,4]`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd7).To(Equal("OK"))
+
+ stop = 1
+ cmd8, err := client.JSONArrTrimWithArgs(ctx, "trim4", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd8).To(Equal([]int64{0}))
+
+ cmd9, err := client.JSONSet(ctx, "trim5", "$", `[0,1,2,3,4]`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd9).To(Equal("OK"))
+
+ stop = 11
+ cmd10, err := client.JSONArrTrimWithArgs(ctx, "trim5", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd10).To(Equal([]int64{0}))
+ })
+
+ It("should JSONArrPop", Label("json.arrpop", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2)
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal([]string{"300"}))
+
+ cmd3 := client.JSONGet(ctx, "pop4", "$")
+ Expect(cmd3.Err()).NotTo(HaveOccurred())
+ Expect(cmd3.Val()).To(Equal("[[100,200,200]]"))
+ })
})
- It("should JSONMerge", Label("json.merge", "json"), func() {
- res, err := client.JSONSet(ctx, "merge1", "$", `{"a": 1, "b": 2}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- res, err = client.JSONMerge(ctx, "merge1", "$", `{"b": 3, "c": 4}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- res, err = client.JSONGet(ctx, "merge1", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(`[{"a":1,"b":3,"c":4}]`))
+ Describe("get/set", Label("getset"), func() {
+ It("should JSONSet", Label("json.set", "json"), func() {
+ cmd := client.JSONSet(ctx, "set1", "$", `{"a": 1, "b": 2, "hello": "world"}`)
+ Expect(cmd.Err()).NotTo(HaveOccurred())
+ Expect(cmd.Val()).To(Equal("OK"))
+ })
+
+ It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() {
+ res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal(`{-"a":1,-"b":2}`))
+
+ res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`))
+ })
+
+ It("should JSONMerge", Label("json.merge", "json"), func() {
+ res, err := client.JSONSet(ctx, "merge1", "$", `{"a": 1, "b": 2}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ res, err = client.JSONMerge(ctx, "merge1", "$", `{"b": 3, "c": 4}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ res, err = client.JSONGet(ctx, "merge1", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal(`[{"a":1,"b":3,"c":4}]`))
+ })
+
+ It("should JSONMSet", Label("json.mset", "json", "NonRedisEnterprise"), func() {
+ doc1 := redis.JSONSetArgs{Key: "mset1", Path: "$", Value: `{"a": 1}`}
+ doc2 := redis.JSONSetArgs{Key: "mset2", Path: "$", Value: 2}
+ docs := []redis.JSONSetArgs{doc1, doc2}
+
+ mSetResult, err := client.JSONMSetArgs(ctx, docs).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(mSetResult).To(Equal("OK"))
+
+ res, err := client.JSONMGet(ctx, "$", "mset1").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal([]interface{}{`[{"a":1}]`}))
+
+ res, err = client.JSONMGet(ctx, "$", "mset1", "mset2").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal([]interface{}{`[{"a":1}]`, "[2]"}))
+
+ _, err = client.JSONMSet(ctx, "mset1", "$.a", 2, "mset3", "$", `[1]`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ It("should JSONMGet", Label("json.mget", "json", "NonRedisEnterprise"), func() {
+ cmd1 := client.JSONSet(ctx, "mget2a", "$", `{"a": ["aa", "ab", "ac", "ad"], "b": {"a": ["ba", "bb", "bc", "bd"]}}`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+ cmd2 := client.JSONSet(ctx, "mget2b", "$", `{"a": [100, 200, 300, 200], "b": {"a": [100, 200, 300, 200]}}`)
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal("OK"))
+
+ cmd3 := client.JSONMGet(ctx, "$..a", "mget2a", "mget2b")
+ Expect(cmd3.Err()).NotTo(HaveOccurred())
+ Expect(cmd3.Val()).To(HaveLen(2))
+ Expect(cmd3.Val()[0]).To(Equal(`[["aa","ab","ac","ad"],["ba","bb","bc","bd"]]`))
+ Expect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`))
+ })
+
+ It("should JSONMget with $", Label("json.mget", "json", "NonRedisEnterprise"), func() {
+ res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "b": 2, "nested": {"a": 3}, "c": "", "nested2": {"a": ""}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ res, err = client.JSONSet(ctx, "doc2", "$", `{"a": 4, "b": 5, "nested": {"a": 6}, "c": "", "nested2": {"a": [""]}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err := client.JSONMGet(ctx, "$..a", "doc1").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal([]interface{}{`[1,3,""]`}))
+
+ iRes, err = client.JSONMGet(ctx, "$..a", "doc1", "doc2").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal([]interface{}{`[1,3,""]`, `[4,6,[""]]`}))
+
+ iRes, err = client.JSONMGet(ctx, "$..a", "non_existing_doc", "non_existing_doc1").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal([]interface{}{nil, nil}))
+ })
})
- It("should JSONMSet", Label("json.mset", "json", "NonRedisEnterprise"), func() {
- doc1 := redis.JSONSetArgs{Key: "mset1", Path: "$", Value: `{"a": 1}`}
- doc2 := redis.JSONSetArgs{Key: "mset2", Path: "$", Value: 2}
- docs := []redis.JSONSetArgs{doc1, doc2}
-
- mSetResult, err := client.JSONMSetArgs(ctx, docs).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(mSetResult).To(Equal("OK"))
-
- res, err := client.JSONMGet(ctx, "$", "mset1").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal([]interface{}{`[{"a":1}]`}))
-
- res, err = client.JSONMGet(ctx, "$", "mset1", "mset2").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal([]interface{}{`[{"a":1}]`, "[2]"}))
-
- _, err = client.JSONMSet(ctx, "mset1", "$.a", 2, "mset3", "$", `[1]`).Result()
- Expect(err).NotTo(HaveOccurred())
+ Describe("Misc", Label("misc"), func() {
+ It("should JSONClear", Label("json.clear", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "clear1", "$", `[1]`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONClear(ctx, "clear1", "$")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal(int64(1)))
+
+ cmd3 := client.JSONGet(ctx, "clear1", "$")
+ Expect(cmd3.Err()).NotTo(HaveOccurred())
+ Expect(cmd3.Val()).To(Equal(`[[]]`))
+ })
+
+ It("should JSONClear with $", Label("json.clear", "json"), func() {
+ doc := `{
+ "nested1": {"a": {"foo": 10, "bar": 20}},
+ "a": ["foo"],
+ "nested2": {"a": "claro"},
+ "nested3": {"a": {"baz": 50}}
+ }`
+ res, err := client.JSONSet(ctx, "doc1", "$", doc).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err := client.JSONClear(ctx, "doc1", "$..a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal(int64(3)))
+
+ resGet, err := client.JSONGet(ctx, "doc1", `$`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":[],"nested2":{"a":"claro"},"nested3":{"a":{}}}]`))
+
+ res, err = client.JSONSet(ctx, "doc1", "$", doc).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err = client.JSONClear(ctx, "doc1", "$.nested1.a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal(int64(1)))
+
+ resGet, err = client.JSONGet(ctx, "doc1", `$`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":["foo"],"nested2":{"a":"claro"},"nested3":{"a":{"baz":50}}}]`))
+ })
+
+ It("should JSONDel", Label("json.del", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "del1", "$", `[1]`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONDel(ctx, "del1", "$")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal(int64(1)))
+
+ cmd3 := client.JSONGet(ctx, "del1", "$")
+ Expect(cmd3.Err()).NotTo(HaveOccurred())
+ Expect(cmd3.Val()).To(HaveLen(0))
+ })
+
+ It("should JSONDel with $", Label("json.del", "json"), func() {
+ res, err := client.JSONSet(ctx, "del1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err := client.JSONDel(ctx, "del1", "$..a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal(int64(2)))
+
+ resGet, err := client.JSONGet(ctx, "del1", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`))
+
+ res, err = client.JSONSet(ctx, "del2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err = client.JSONDel(ctx, "del2", "$..a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal(int64(1)))
+
+ resGet, err = client.JSONGet(ctx, "del2", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`))
+
+ doc := `[
+ {
+ "ciao": ["non ancora"],
+ "nested": [
+ {"ciao": [1, "a"]},
+ {"ciao": [2, "a"]},
+ {"ciaoc": [3, "non", "ciao"]},
+ {"ciao": [4, "a"]},
+ {"e": [5, "non", "ciao"]}
+ ]
+ }
+ ]`
+ res, err = client.JSONSet(ctx, "del3", "$", doc).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err = client.JSONDel(ctx, "del3", `$.[0]["nested"]..ciao`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal(int64(3)))
+
+ resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]`
+ resGet, err = client.JSONGet(ctx, "del3", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal(resVal))
+ })
+
+ It("should JSONForget", Label("json.forget", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "forget3", "$", `{"a": [1,2,3], "b": {"a": [1,2,3], "b": "annie"}}`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONForget(ctx, "forget3", "$..a")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal(int64(2)))
+
+ cmd3 := client.JSONGet(ctx, "forget3", "$")
+ Expect(cmd3.Err()).NotTo(HaveOccurred())
+ Expect(cmd3.Val()).To(Equal(`[{"b":{"b":"annie"}}]`))
+ })
+
+ It("should JSONForget with $", Label("json.forget", "json"), func() {
+ res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err := client.JSONForget(ctx, "doc1", "$..a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal(int64(2)))
+
+ resGet, err := client.JSONGet(ctx, "doc1", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`))
+
+ res, err = client.JSONSet(ctx, "doc2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err = client.JSONForget(ctx, "doc2", "$..a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal(int64(1)))
+
+ resGet, err = client.JSONGet(ctx, "doc2", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`))
+
+ doc := `[
+ {
+ "ciao": ["non ancora"],
+ "nested": [
+ {"ciao": [1, "a"]},
+ {"ciao": [2, "a"]},
+ {"ciaoc": [3, "non", "ciao"]},
+ {"ciao": [4, "a"]},
+ {"e": [5, "non", "ciao"]}
+ ]
+ }
+ ]`
+ res, err = client.JSONSet(ctx, "doc3", "$", doc).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ iRes, err = client.JSONForget(ctx, "doc3", `$.[0]["nested"]..ciao`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(iRes).To(Equal(int64(3)))
+
+ resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]`
+ resGet, err = client.JSONGet(ctx, "doc3", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resGet).To(Equal(resVal))
+ })
+
+ It("should JSONNumIncrBy", Label("json.numincrby", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "incr3", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONNumIncrBy(ctx, "incr3", "$..a[1]", float64(1))
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(Equal(`[3,0]`))
+ })
+
+ It("should JSONNumIncrBy with $", Label("json.numincrby", "json"), func() {
+ res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 2).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal(`[7]`))
+
+ res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 3.5).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal(`[10.5]`))
+
+ res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ res, err = client.JSONNumIncrBy(ctx, "doc2", "$.b[0].a", 3).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal(`[5]`))
+ })
+
+ It("should JSONObjKeys", Label("json.objkeys", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "objkeys1", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONObjKeys(ctx, "objkeys1", "$..*")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(HaveLen(7))
+ Expect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{"a"}, nil, nil, nil, nil, nil}))
+ })
+
+ It("should JSONObjKeys with $", Label("json.objkeys", "json"), func() {
+ doc := `{
+ "nested1": {"a": {"foo": 10, "bar": 20}},
+ "a": ["foo"],
+ "nested2": {"a": {"baz": 50}}
+ }`
+ cmd1, err := client.JSONSet(ctx, "objkeys1", "$", doc).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd1).To(Equal("OK"))
+
+ cmd2, err := client.JSONObjKeys(ctx, "objkeys1", "$.nested1.a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd2).To(Equal([]interface{}{[]interface{}{"foo", "bar"}}))
+
+ cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".*.a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd2).To(Equal([]interface{}{"foo", "bar"}))
+
+ cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".nested2.a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd2).To(Equal([]interface{}{"baz"}))
+
+ _, err = client.JSONObjKeys(ctx, "non_existing_doc", "..a").Result()
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("should JSONObjLen", Label("json.objlen", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "objlen2", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONObjLen(ctx, "objlen2", "$..*")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(HaveLen(7))
+ Expect(cmd2.Val()[0]).To(BeNil())
+ Expect(*cmd2.Val()[1]).To(Equal(int64(1)))
+ })
+
+ It("should JSONStrLen", Label("json.strlen", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "strlen2", "$", `{"a": "alice", "b": "bob", "c": {"a": "alice", "b": "bob"}}`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONStrLen(ctx, "strlen2", "$..*")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(HaveLen(5))
+ var tmp int64 = 20
+ Expect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp))
+ Expect(*cmd2.Val()[0]).To(Equal(int64(5)))
+ Expect(*cmd2.Val()[1]).To(Equal(int64(3)))
+ Expect(cmd2.Val()[2]).To(BeNil())
+ Expect(*cmd2.Val()[3]).To(Equal(int64(5)))
+ Expect(*cmd2.Val()[4]).To(Equal(int64(3)))
+ })
+
+ It("should JSONStrAppend", Label("json.strappend", "json"), func() {
+ cmd1, err := client.JSONSet(ctx, "strapp1", "$", `"foo"`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd1).To(Equal("OK"))
+ cmd2, err := client.JSONStrAppend(ctx, "strapp1", "$", `"bar"`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(*cmd2[0]).To(Equal(int64(6)))
+ cmd3, err := client.JSONGet(ctx, "strapp1", "$").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cmd3).To(Equal(`["foobar"]`))
+ })
+
+ It("should JSONStrAppend and JSONStrLen with $", Label("json.strappend", "json.strlen", "json"), func() {
+ res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(,Equal("OK"))
+
+ intArrayResult, err := client.JSONStrAppend(ctx, "doc1", "$.nested1.a", `"baz"`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(*intArrayResult[0]).To(Equal(int64(8)))
+
+ res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).To(Equal("OK"))
+
+ intResult, err := client.JSONStrLen(ctx, "doc2", "$.nested1.a").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(*intResult[0]).To(Equal(int64(5)))
+ })
+
+ It("should JSONToggle", Label("json.toggle", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "toggle1", "$", `[true]`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONToggle(ctx, "toggle1", "$[0]")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(HaveLen(1))
+ Expect(*cmd2.Val()[0]).To(Equal(int64(0)))
+ })
+
+ It("should JSONType", Label("json.type", "json"), func() {
+ cmd1 := client.JSONSet(ctx, "type1", "$", `[true]`)
+ Expect(cmd1.Err()).NotTo(HaveOccurred())
+ Expect(cmd1.Val()).To(Equal("OK"))
+
+ cmd2 := client.JSONType(ctx, "type1", "$[0]")
+ Expect(cmd2.Err()).NotTo(HaveOccurred())
+ Expect(cmd2.Val()).To(HaveLen(1))
+ // RESP2 v RESP3
+ Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean")))
+ })
})
+ }
+})
- It("should JSONMGet", Label("json.mget", "json", "NonRedisEnterprise"), func() {
- cmd1 := client.JSONSet(ctx, "mget2a", "$", `{"a": ["aa", "ab", "ac", "ad"], "b": {"a": ["ba", "bb", "bc", "bd"]}}`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
- cmd2 := client.JSONSet(ctx, "mget2b", "$", `{"a": [100, 200, 300, 200], "b": {"a": [100, 200, 300, 200]}}`)
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal("OK"))
-
- cmd3 := client.JSONMGet(ctx, "$..a", "mget2a", "mget2b")
- Expect(cmd3.Err()).NotTo(HaveOccurred())
- Expect(cmd3.Val()).To(HaveLen(2))
- Expect(cmd3.Val()[0]).To(Equal(`[["aa","ab","ac","ad"],["ba","bb","bc","bd"]]`))
- Expect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`))
+var _ = Describe("Go-Redis Advanced JSON and RediSearch Tests", func() {
+ var client *redis.Client
+ var ctx = context.Background()
+
+ setupRedisClient := func(protocolVersion int) *redis.Client {
+ return redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ DB: 0,
+ Protocol: protocolVersion, // Setting RESP2 or RESP3 protocol
+ UnstableResp3: true, // Enable RESP3 features
})
+ }
- It("should JSONMget with $", Label("json.mget", "json", "NonRedisEnterprise"), func() {
- res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "b": 2, "nested": {"a": 3}, "c": "", "nested2": {"a": ""}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- res, err = client.JSONSet(ctx, "doc2", "$", `{"a": 4, "b": 5, "nested": {"a": 6}, "c": "", "nested2": {"a": [""]}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err := client.JSONMGet(ctx, "$..a", "doc1").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal([]interface{}{`[1,3,""]`}))
-
- iRes, err = client.JSONMGet(ctx, "$..a", "doc1", "doc2").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal([]interface{}{`[1,3,""]`, `[4,6,[""]]`}))
-
- iRes, err = client.JSONMGet(ctx, "$..a", "non_existing_doc", "non_existing_doc1").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal([]interface{}{nil, nil}))
- })
+ AfterEach(func() {
+ if client != nil {
+ client.FlushDB(ctx)
+ client.Close()
+ }
})
- Describe("Misc", Label("misc"), func() {
- It("should JSONClear", Label("json.clear", "json"), func() {
- cmd1 := client.JSONSet(ctx, "clear1", "$", `[1]`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONClear(ctx, "clear1", "$")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal(int64(1)))
-
- cmd3 := client.JSONGet(ctx, "clear1", "$")
- Expect(cmd3.Err()).NotTo(HaveOccurred())
- Expect(cmd3.Val()).To(Equal(`[[]]`))
- })
-
- It("should JSONClear with $", Label("json.clear", "json"), func() {
- doc := `{
- "nested1": {"a": {"foo": 10, "bar": 20}},
- "a": ["foo"],
- "nested2": {"a": "claro"},
- "nested3": {"a": {"baz": 50}}
- }`
- res, err := client.JSONSet(ctx, "doc1", "$", doc).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err := client.JSONClear(ctx, "doc1", "$..a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal(int64(3)))
-
- resGet, err := client.JSONGet(ctx, "doc1", `$`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":[],"nested2":{"a":"claro"},"nested3":{"a":{}}}]`))
-
- res, err = client.JSONSet(ctx, "doc1", "$", doc).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err = client.JSONClear(ctx, "doc1", "$.nested1.a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal(int64(1)))
-
- resGet, err = client.JSONGet(ctx, "doc1", `$`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":["foo"],"nested2":{"a":"claro"},"nested3":{"a":{"baz":50}}}]`))
- })
-
- It("should JSONDel", Label("json.del", "json"), func() {
- cmd1 := client.JSONSet(ctx, "del1", "$", `[1]`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONDel(ctx, "del1", "$")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal(int64(1)))
-
- cmd3 := client.JSONGet(ctx, "del1", "$")
- Expect(cmd3.Err()).NotTo(HaveOccurred())
- Expect(cmd3.Val()).To(HaveLen(0))
- })
-
- It("should JSONDel with $", Label("json.del", "json"), func() {
- res, err := client.JSONSet(ctx, "del1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err := client.JSONDel(ctx, "del1", "$..a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal(int64(2)))
-
- resGet, err := client.JSONGet(ctx, "del1", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`))
-
- res, err = client.JSONSet(ctx, "del2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err = client.JSONDel(ctx, "del2", "$..a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal(int64(1)))
-
- resGet, err = client.JSONGet(ctx, "del2", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`))
-
- doc := `[
- {
- "ciao": ["non ancora"],
- "nested": [
- {"ciao": [1, "a"]},
- {"ciao": [2, "a"]},
- {"ciaoc": [3, "non", "ciao"]},
- {"ciao": [4, "a"]},
- {"e": [5, "non", "ciao"]}
- ]
- }
- ]`
- res, err = client.JSONSet(ctx, "del3", "$", doc).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err = client.JSONDel(ctx, "del3", `$.[0]["nested"]..ciao`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal(int64(3)))
-
- resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]`
- resGet, err = client.JSONGet(ctx, "del3", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal(resVal))
- })
-
- It("should JSONForget", Label("json.forget", "json"), func() {
- cmd1 := client.JSONSet(ctx, "forget3", "$", `{"a": [1,2,3], "b": {"a": [1,2,3], "b": "annie"}}`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONForget(ctx, "forget3", "$..a")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal(int64(2)))
-
- cmd3 := client.JSONGet(ctx, "forget3", "$")
- Expect(cmd3.Err()).NotTo(HaveOccurred())
- Expect(cmd3.Val()).To(Equal(`[{"b":{"b":"annie"}}]`))
- })
-
- It("should JSONForget with $", Label("json.forget", "json"), func() {
- res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err := client.JSONForget(ctx, "doc1", "$..a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal(int64(2)))
-
- resGet, err := client.JSONGet(ctx, "doc1", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`))
-
- res, err = client.JSONSet(ctx, "doc2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err = client.JSONForget(ctx, "doc2", "$..a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal(int64(1)))
-
- resGet, err = client.JSONGet(ctx, "doc2", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`))
-
- doc := `[
- {
- "ciao": ["non ancora"],
- "nested": [
- {"ciao": [1, "a"]},
- {"ciao": [2, "a"]},
- {"ciaoc": [3, "non", "ciao"]},
- {"ciao": [4, "a"]},
- {"e": [5, "non", "ciao"]}
- ]
- }
- ]`
- res, err = client.JSONSet(ctx, "doc3", "$", doc).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- iRes, err = client.JSONForget(ctx, "doc3", `$.[0]["nested"]..ciao`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(iRes).To(Equal(int64(3)))
-
- resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]`
- resGet, err = client.JSONGet(ctx, "doc3", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(resGet).To(Equal(resVal))
- })
-
- It("should JSONNumIncrBy", Label("json.numincrby", "json"), func() {
- cmd1 := client.JSONSet(ctx, "incr3", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONNumIncrBy(ctx, "incr3", "$..a[1]", float64(1))
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(Equal(`[3,0]`))
- })
-
- It("should JSONNumIncrBy with $", Label("json.numincrby", "json"), func() {
- res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 2).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(`[7]`))
-
- res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 3.5).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(`[10.5]`))
-
- res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- res, err = client.JSONNumIncrBy(ctx, "doc2", "$.b[0].a", 3).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(`[5]`))
- })
-
- It("should JSONObjKeys", Label("json.objkeys", "json"), func() {
- cmd1 := client.JSONSet(ctx, "objkeys1", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONObjKeys(ctx, "objkeys1", "$..*")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(HaveLen(7))
- Expect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{"a"}, nil, nil, nil, nil, nil}))
- })
-
- It("should JSONObjKeys with $", Label("json.objkeys", "json"), func() {
- doc := `{
- "nested1": {"a": {"foo": 10, "bar": 20}},
- "a": ["foo"],
- "nested2": {"a": {"baz": 50}}
- }`
- cmd1, err := client.JSONSet(ctx, "objkeys1", "$", doc).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd1).To(Equal("OK"))
-
- cmd2, err := client.JSONObjKeys(ctx, "objkeys1", "$.nested1.a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd2).To(Equal([]interface{}{[]interface{}{"foo", "bar"}}))
-
- cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".*.a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd2).To(Equal([]interface{}{"foo", "bar"}))
-
- cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".nested2.a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd2).To(Equal([]interface{}{"baz"}))
-
- _, err = client.JSONObjKeys(ctx, "non_existing_doc", "..a").Result()
- Expect(err).To(HaveOccurred())
- })
-
- It("should JSONObjLen", Label("json.objlen", "json"), func() {
- cmd1 := client.JSONSet(ctx, "objlen2", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONObjLen(ctx, "objlen2", "$..*")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(HaveLen(7))
- Expect(cmd2.Val()[0]).To(BeNil())
- Expect(*cmd2.Val()[1]).To(Equal(int64(1)))
- })
-
- It("should JSONStrLen", Label("json.strlen", "json"), func() {
- cmd1 := client.JSONSet(ctx, "strlen2", "$", `{"a": "alice", "b": "bob", "c": {"a": "alice", "b": "bob"}}`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONStrLen(ctx, "strlen2", "$..*")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(HaveLen(5))
- var tmp int64 = 20
- Expect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp))
- Expect(*cmd2.Val()[0]).To(Equal(int64(5)))
- Expect(*cmd2.Val()[1]).To(Equal(int64(3)))
- Expect(cmd2.Val()[2]).To(BeNil())
- Expect(*cmd2.Val()[3]).To(Equal(int64(5)))
- Expect(*cmd2.Val()[4]).To(Equal(int64(3)))
- })
-
- It("should JSONStrAppend", Label("json.strappend", "json"), func() {
- cmd1, err := client.JSONSet(ctx, "strapp1", "$", `"foo"`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd1).To(Equal("OK"))
- cmd2, err := client.JSONStrAppend(ctx, "strapp1", "$", `"bar"`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(*cmd2[0]).To(Equal(int64(6)))
- cmd3, err := client.JSONGet(ctx, "strapp1", "$").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(cmd3).To(Equal(`["foobar"]`))
- })
-
- It("should JSONStrAppend and JSONStrLen with $", Label("json.strappend", "json.strlen", "json"), func() {
- res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- intArrayResult, err := client.JSONStrAppend(ctx, "doc1", "$.nested1.a", `"baz"`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(*intArrayResult[0]).To(Equal(int64(8)))
-
- res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal("OK"))
-
- intResult, err := client.JSONStrLen(ctx, "doc2", "$.nested1.a").Result()
- Expect(err).NotTo(HaveOccurred())
- Expect(*intResult[0]).To(Equal(int64(5)))
- })
-
- It("should JSONToggle", Label("json.toggle", "json"), func() {
- cmd1 := client.JSONSet(ctx, "toggle1", "$", `[true]`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONToggle(ctx, "toggle1", "$[0]")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(HaveLen(1))
- Expect(*cmd2.Val()[0]).To(Equal(int64(0)))
- })
-
- It("should JSONType", Label("json.type", "json"), func() {
- cmd1 := client.JSONSet(ctx, "type1", "$", `[true]`)
- Expect(cmd1.Err()).NotTo(HaveOccurred())
- Expect(cmd1.Val()).To(Equal("OK"))
-
- cmd2 := client.JSONType(ctx, "type1", "$[0]")
- Expect(cmd2.Err()).NotTo(HaveOccurred())
- Expect(cmd2.Val()).To(HaveLen(1))
- // RESP2 v RESP3
- Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean")))
- })
+ Context("when testing with RESP2 and RESP3", func() {
+ protocols := []int{2, 3}
+
+ for _, protocol := range protocols {
+ When("using protocol version", func() {
+ BeforeEach(func() {
+ client = setupRedisClient(protocol)
+ })
+
+ It("should perform complex JSON and RediSearch operations", func() {
+ jsonDoc := map[string]interface{}{
+ "person": map[string]interface{}{
+ "name": "Alice",
+ "age": 30,
+ "status": true,
+ "address": map[string]interface{}{
+ "city": "Wonderland",
+ "postcode": "12345",
+ },
+ "contacts": []map[string]interface{}{
+ {"type": "email", "value": "[email protected]"},
+ {"type": "phone", "value": "+123456789"},
+ {"type": "fax", "value": "+987654321"},
+ },
+ "friends": []map[string]interface{}{
+ {"name": "Bob", "age": 35, "status": true},
+ {"name": "Charlie", "age": 28, "status": false},
+ },
+ },
+ "settings": map[string]interface{}{
+ "notifications": map[string]interface{}{
+ "email": true,
+ "sms": false,
+ "alerts": []string{"low battery", "door open"},
+ },
+ "theme": "dark",
+ },
+ }
+
+ setCmd := client.JSONSet(ctx, "person:1", ".", jsonDoc)
+ Expect(setCmd.Err()).NotTo(HaveOccurred(), "JSON.SET failed")
+
+ getCmdRaw := client.JSONGet(ctx, "person:1", ".")
+ rawJSON, err := getCmdRaw.Result()
+ Expect(err).NotTo(HaveOccurred(), "JSON.GET (raw) failed")
+ GinkgoWriter.Printf("Raw JSON: %s\n", rawJSON)
+
+ getCmdExpanded := client.JSONGet(ctx, "person:1", ".")
+ expandedJSON, err := getCmdExpanded.Expanded()
+ Expect(err).NotTo(HaveOccurred(), "JSON.GET (expanded) failed")
+ GinkgoWriter.Printf("Expanded JSON: %+v\n", expandedJSON)
+
+ Expect(rawJSON).To(MatchJSON(jsonMustMarshal(expandedJSON)))
+
+ arrAppendCmd := client.JSONArrAppend(ctx, "person:1", "$.person.contacts", `{"type": "social", "value": "@alice_wonder"}`)
+ Expect(arrAppendCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRAPPEND failed")
+ arrLenCmd := client.JSONArrLen(ctx, "person:1", "$.person.contacts")
+ arrLen, err := arrLenCmd.Result()
+ Expect(err).NotTo(HaveOccurred(), "JSON.ARRLEN failed")
+ Expect(arrLen).To(Equal([]int64{4}), "Array length mismatch after append")
+
+ arrInsertCmd := client.JSONArrInsert(ctx, "person:1", "$.person.friends", 1, `{"name": "Diana", "age": 25, "status": true}`)
+ Expect(arrInsertCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRINSERT failed")
+
+ start := 0
+ stop := 1
+ arrTrimCmd := client.JSONArrTrimWithArgs(ctx, "person:1", "$.person.friends", &redis.JSONArrTrimArgs{Start: start, Stop: &stop})
+ Expect(arrTrimCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRTRIM failed")
+
+ mergeData := map[string]interface{}{
+ "status": false,
+ "nickname": "WonderAlice",
+ "lastLogin": time.Now().Format(time.RFC3339),
+ }
+ mergeCmd := client.JSONMerge(ctx, "person:1", "$.person", jsonMustMarshal(mergeData))
+ Expect(mergeCmd.Err()).NotTo(HaveOccurred(), "JSON.MERGE failed")
+
+ typeCmd := client.JSONType(ctx, "person:1", "$.person.nickname")
+ nicknameType, err := typeCmd.Result()
+ Expect(err).NotTo(HaveOccurred(), "JSON.TYPE failed")
+ Expect(nicknameType[0]).To(Equal([]interface{}{"string"}), "JSON.TYPE mismatch for nickname")
+
+ createIndexCmd := client.Do(ctx, "FT.CREATE", "person_idx", "ON", "JSON",
+ "PREFIX", "1", "person:", "SCHEMA",
+ "$.person.name", "AS", "name", "TEXT",
+ "$.person.age", "AS", "age", "NUMERIC",
+ "$.person.address.city", "AS", "city", "TEXT",
+ "$.person.contacts[*].value", "AS", "contact_value", "TEXT",
+ )
+ Expect(createIndexCmd.Err()).NotTo(HaveOccurred(), "FT.CREATE failed")
+
+ searchCmd := client.FTSearchWithArgs(ctx, "person_idx", "@contact_value:(alice\\@example\\.com alice_wonder)", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.person.name"}, {FieldName: "$.person.age"}, {FieldName: "$.person.address.city"}}})
+ searchResult, err := searchCmd.Result()
+ Expect(err).NotTo(HaveOccurred(), "FT.SEARCH failed")
+ GinkgoWriter.Printf("Advanced Search result: %+v\n", searchResult)
+
+ incrCmd := client.JSONNumIncrBy(ctx, "person:1", "$.person.age", 5)
+ incrResult, err := incrCmd.Result()
+ Expect(err).NotTo(HaveOccurred(), "JSON.NUMINCRBY failed")
+ Expect(incrResult).To(Equal("[35]"), "Age increment mismatch")
+
+ delCmd := client.JSONDel(ctx, "person:1", "$.settings.notifications.email")
+ Expect(delCmd.Err()).NotTo(HaveOccurred(), "JSON.DEL failed")
+
+ typeCmd = client.JSONType(ctx, "person:1", "$.settings.notifications.email")
+ typeResult, err := typeCmd.Result()
+ Expect(err).ToNot(HaveOccurred())
+ Expect(typeResult[0]).To(BeEmpty(), "Expected JSON.TYPE to be empty for deleted field")
+ })
+ })
+ }
})
})
+
+// Helper function to marshal data into JSON for comparisons
+func jsonMustMarshal(v interface{}) string {
+ bytes, err := json.Marshal(v)
+ Expect(err).NotTo(HaveOccurred())
+ return string(bytes)
+}
diff --git options.go options.go
index 6ed693a0b..53991dc9d 100644
--- options.go
+++ options.go
@@ -148,11 +148,23 @@ type Options struct {
// Enables read only queries on slave/follower nodes.
readOnly bool
- // Disable set-lib on connect. Default is false.
+ // DisableIndentity - Disable set-lib on connect.
+ //
+ // default: false
+ //
+ // Deprecated: Use DisableIdentity instead.
DisableIndentity bool
+ // DisableIdentity is used to disable CLIENT SETINFO command on connect.
+ //
+ // default: false
+ DisableIdentity bool
+
// Add suffix to client name. Default is empty.
IdentitySuffix string
+
+ // UnstableResp3 enables Unstable mode for Redis Search module with RESP3.
+ UnstableResp3 bool
}
func (opt *Options) init() {
diff --git osscluster.go osscluster.go
index 73a9e2b74..f5a576842 100644
--- osscluster.go
+++ osscluster.go
@@ -86,10 +86,24 @@ type ClusterOptions struct {
ConnMaxIdleTime time.Duration
ConnMaxLifetime time.Duration
- TLSConfig *tls.Config
- DisableIndentity bool // Disable set-lib on connect. Default is false.
+ TLSConfig *tls.Config
+
+ // DisableIndentity - Disable set-lib on connect.
+ //
+ // default: false
+ //
+ // Deprecated: Use DisableIdentity instead.
+ DisableIndentity bool
+
+ // DisableIdentity is used to disable CLIENT SETINFO command on connect.
+ //
+ // default: false
+ DisableIdentity bool
IdentitySuffix string // Add suffix to client name. Default is empty.
+
+ // UnstableResp3 enables Unstable mode for Redis Search module with RESP3.
+ UnstableResp3 bool
}
func (opt *ClusterOptions) init() {
@@ -296,7 +310,8 @@ func (opt *ClusterOptions) clientOptions() *Options {
MaxActiveConns: opt.MaxActiveConns,
ConnMaxIdleTime: opt.ConnMaxIdleTime,
ConnMaxLifetime: opt.ConnMaxLifetime,
- DisableIndentity: opt.DisableIndentity,
+ DisableIdentity: opt.DisableIdentity,
+ DisableIndentity: opt.DisableIdentity,
IdentitySuffix: opt.IdentitySuffix,
TLSConfig: opt.TLSConfig,
// If ClusterSlots is populated, then we probably have an artificial
@@ -304,7 +319,8 @@ func (opt *ClusterOptions) clientOptions() *Options {
// much use for ClusterSlots config). This means we cannot execute the
// READONLY command against that node -- setting readOnly to false in such
// situations in the options below will prevent that from happening.
- readOnly: opt.ReadOnly && opt.ClusterSlots == nil,
+ readOnly: opt.ReadOnly && opt.ClusterSlots == nil,
+ UnstableResp3: opt.UnstableResp3,
}
}
@@ -465,9 +481,11 @@ func (c *clusterNodes) Addrs() ([]string, error) {
closed := c.closed //nolint:ifshort
if !closed {
if len(c.activeAddrs) > 0 {
- addrs = c.activeAddrs
+ addrs = make([]string, len(c.activeAddrs))
+ copy(addrs, c.activeAddrs)
} else {
- addrs = c.addrs
+ addrs = make([]string, len(c.addrs))
+ copy(addrs, c.addrs)
}
}
c.mu.RUnlock()
@@ -938,10 +956,13 @@ func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error {
func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
slot := c.cmdSlot(ctx, cmd)
var node *clusterNode
+ var moved bool
var ask bool
var lastErr error
for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ {
- if attempt > 0 {
+ // MOVED and ASK responses are not transient errors that require retry delay; they
+ // should be attempted immediately.
+ if attempt > 0 && !moved && !ask {
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
return err
}
@@ -985,7 +1006,6 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
continue
}
- var moved bool
var addr string
moved, ask, addr = isMovedError(lastErr)
if moved || ask {
diff --git osscluster_test.go osscluster_test.go
index 3d2f80711..9c3eaba35 100644
--- osscluster_test.go
+++ osscluster_test.go
@@ -653,6 +653,58 @@ var _ = Describe("ClusterClient", func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
+ It("determines hash slots correctly for generic commands", func() {
+ opt := redisClusterOptions()
+ opt.MaxRedirects = -1
+ client := cluster.newClusterClient(ctx, opt)
+
+ err := client.Do(ctx, "GET", "A").Err()
+ Expect(err).To(Equal(redis.Nil))
+
+ err = client.Do(ctx, []byte("GET"), []byte("A")).Err()
+ Expect(err).To(Equal(redis.Nil))
+
+ Eventually(func() error {
+ return client.SwapNodes(ctx, "A")
+ }, 30*time.Second).ShouldNot(HaveOccurred())
+
+ err = client.Do(ctx, "GET", "A").Err()
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("MOVED"))
+
+ err = client.Do(ctx, []byte("GET"), []byte("A")).Err()
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("MOVED"))
+
+ Expect(client.Close()).NotTo(HaveOccurred())
+ })
+
+ It("follows node redirection immediately", func() {
+ // Configure retry backoffs far in excess of the expected duration of redirection
+ opt := redisClusterOptions()
+ opt.MinRetryBackoff = 10 * time.Minute
+ opt.MaxRetryBackoff = 20 * time.Minute
+ client := cluster.newClusterClient(ctx, opt)
+
+ Eventually(func() error {
+ return client.SwapNodes(ctx, "A")
+ }, 30*time.Second).ShouldNot(HaveOccurred())
+
+ // Note that this context sets a deadline more aggressive than the lowest possible bound
+ // of the retry backoff; this verifies that redirection completes immediately.
+ redirCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ err := client.Set(redirCtx, "A", "VALUE", 0).Err()
+ Expect(err).NotTo(HaveOccurred())
+
+ v, err := client.Get(redirCtx, "A").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(v).To(Equal("VALUE"))
+
+ Expect(client.Close()).NotTo(HaveOccurred())
+ })
+
It("calls fn for every master node", func() {
for i := 0; i < 10; i++ {
Expect(client.Set(ctx, strconv.Itoa(i), "", 0).Err()).NotTo(HaveOccurred())
diff --git redis.go redis.go
index 527afb677..730430515 100644
--- redis.go
+++ redis.go
@@ -41,7 +41,7 @@ type (
)
type hooksMixin struct {
- hooksMu *sync.Mutex
+ hooksMu *sync.RWMutex
slice []Hook
initial hooks
@@ -49,7 +49,7 @@ type hooksMixin struct {
}
func (hs *hooksMixin) initHooks(hooks hooks) {
- hs.hooksMu = new(sync.Mutex)
+ hs.hooksMu = new(sync.RWMutex)
hs.initial = hooks
hs.chain()
}
@@ -151,7 +151,7 @@ func (hs *hooksMixin) clone() hooksMixin {
clone := *hs
l := len(clone.slice)
clone.slice = clone.slice[:l:l]
- clone.hooksMu = new(sync.Mutex)
+ clone.hooksMu = new(sync.RWMutex)
return clone
}
@@ -176,9 +176,14 @@ func (hs *hooksMixin) withProcessPipelineHook(
}
func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) {
- hs.hooksMu.Lock()
- defer hs.hooksMu.Unlock()
- return hs.current.dial(ctx, network, addr)
+ // Access to hs.current is guarded by a read-only lock since it may be mutated by AddHook(...)
+ // while this dialer is concurrently accessed by the background connection pool population
+ // routine when MinIdleConns > 0.
+ hs.hooksMu.RLock()
+ current := hs.current
+ hs.hooksMu.RUnlock()
+
+ return current.dial(ctx, network, addr)
}
func (hs *hooksMixin) processHook(ctx context.Context, cmd Cmder) error {
@@ -345,7 +350,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
return err
}
- if !c.opt.DisableIndentity {
+ if !c.opt.DisableIdentity && !c.opt.DisableIndentity {
libName := ""
libVer := Version()
if c.opt.IdentitySuffix != "" {
@@ -354,7 +359,11 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
p := conn.Pipeline()
p.ClientSetInfo(ctx, WithLibraryName(libName))
p.ClientSetInfo(ctx, WithLibraryVersion(libVer))
- _, _ = p.Exec(ctx)
+ // Handle network errors (e.g. timeouts) in CLIENT SETINFO to avoid
+ // out of order responses later on.
+ if _, err = p.Exec(ctx); err != nil && !isRedisError(err) {
+ return err
+ }
}
if c.opt.OnConnect != nil {
@@ -412,6 +421,19 @@ func (c *baseClient) process(ctx context.Context, cmd Cmder) error {
return lastErr
}
+func (c *baseClient) assertUnstableCommand(cmd Cmder) bool {
+ switch cmd.(type) {
+ case *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd:
+ if c.opt.UnstableResp3 {
+ return true
+ } else {
+ panic("RESP3 responses for this command are disabled because they may still change. Please set the flag UnstableResp3 . See the [README](https://github.com/redis/go-redis/blob/master/README.md) and the release notes for guidance.")
+ }
+ default:
+ return false
+ }
+}
+
func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) {
if attempt > 0 {
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
@@ -427,8 +449,12 @@ func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool
atomic.StoreUint32(&retryTimeout, 1)
return err
}
-
- if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), cmd.readReply); err != nil {
+ readReplyFunc := cmd.readReply
+ // Apply unstable RESP3 search module.
+ if c.opt.Protocol != 2 && c.assertUnstableCommand(cmd) {
+ readReplyFunc = cmd.readRawReply
+ }
+ if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), readReplyFunc); err != nil {
if cmd.readTimeout() == nil {
atomic.StoreUint32(&retryTimeout, 1)
} else {
diff --git redis_test.go redis_test.go
index ef2125452..04836a684 100644
--- redis_test.go
+++ redis_test.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net"
+ "sync"
"testing"
"time"
@@ -373,6 +374,13 @@ var _ = Describe("Client timeout", func() {
})
testTimeout := func() {
+ It("SETINFO timeouts", func() {
+ conn := client.Conn()
+ err := conn.Ping(ctx).Err()
+ Expect(err).To(HaveOccurred())
+ Expect(err.(net.Error).Timeout()).To(BeTrue())
+ })
+
It("Ping timeouts", func() {
err := client.Ping(ctx).Err()
Expect(err).To(HaveOccurred())
@@ -633,3 +641,67 @@ var _ = Describe("Hook with MinIdleConns", func() {
}))
})
})
+
+var _ = Describe("Dialer connection timeouts", func() {
+ var client *redis.Client
+
+ const dialSimulatedDelay = 1 * time.Second
+
+ BeforeEach(func() {
+ options := redisOptions()
+ options.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
+ // Simulated slow dialer.
+ // Note that the following sleep is deliberately not context-aware.
+ time.Sleep(dialSimulatedDelay)
+ return net.Dial("tcp", options.Addr)
+ }
+ options.MinIdleConns = 1
+ client = redis.NewClient(options)
+ })
+
+ AfterEach(func() {
+ err := client.Close()
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ It("does not contend on connection dial for concurrent commands", func() {
+ var wg sync.WaitGroup
+
+ const concurrency = 10
+
+ durations := make(chan time.Duration, concurrency)
+ errs := make(chan error, concurrency)
+
+ start := time.Now()
+ wg.Add(concurrency)
+
+ for i := 0; i < concurrency; i++ {
+ go func() {
+ defer wg.Done()
+
+ start := time.Now()
+ err := client.Ping(ctx).Err()
+ durations <- time.Since(start)
+ errs <- err
+ }()
+ }
+
+ wg.Wait()
+ close(durations)
+ close(errs)
+
+ // All commands should eventually succeed, after acquiring a connection.
+ for err := range errs {
+ Expect(err).NotTo(HaveOccurred())
+ }
+
+ // Each individual command should complete within the simulated dial duration bound.
+ for duration := range durations {
+ Expect(duration).To(BeNumerically("<", 2*dialSimulatedDelay))
+ }
+
+ // Due to concurrent execution, the entire test suite should also complete within
+ // the same dial duration bound applied for individual commands.
+ Expect(time.Since(start)).To(BeNumerically("<", 2*dialSimulatedDelay))
+ })
+})
diff --git ring.go ring.go
index 4ae00542b..990a4c887 100644
--- ring.go
+++ ring.go
@@ -98,8 +98,19 @@ type RingOptions struct {
TLSConfig *tls.Config
Limiter Limiter
+ // DisableIndentity - Disable set-lib on connect.
+ //
+ // default: false
+ //
+ // Deprecated: Use DisableIdentity instead.
DisableIndentity bool
- IdentitySuffix string
+
+ // DisableIdentity is used to disable CLIENT SETINFO command on connect.
+ //
+ // default: false
+ DisableIdentity bool
+ IdentitySuffix string
+ UnstableResp3 bool
}
func (opt *RingOptions) init() {
@@ -166,8 +177,11 @@ func (opt *RingOptions) clientOptions() *Options {
TLSConfig: opt.TLSConfig,
Limiter: opt.Limiter,
+ DisableIdentity: opt.DisableIdentity,
DisableIndentity: opt.DisableIndentity,
- IdentitySuffix: opt.IdentitySuffix,
+
+ IdentitySuffix: opt.IdentitySuffix,
+ UnstableResp3: opt.UnstableResp3,
}
}
diff --git search_commands.go search_commands.go
index 8214a570b..9359a723e 100644
--- search_commands.go
+++ search_commands.go
@@ -16,7 +16,7 @@ type SearchCmdable interface {
FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd
FTAliasDel(ctx context.Context, alias string) *StatusCmd
FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd
- FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd
+ FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd
FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd
FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd
FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd
@@ -57,7 +57,7 @@ type FTCreateOptions struct {
NoFields bool
NoFreqs bool
StopWords []interface{}
- SkipInitalScan bool
+ SkipInitialScan bool
}
type FieldSchema struct {
@@ -70,11 +70,13 @@ type FieldSchema struct {
NoIndex bool
PhoneticMatcher string
Weight float64
- Seperator string
+ Separator string
CaseSensitive bool
WithSuffixtrie bool
VectorArgs *FTVectorArgs
GeoShapeFieldType string
+ IndexEmpty bool
+ IndexMissing bool
}
type FTVectorArgs struct {
@@ -245,6 +247,8 @@ type FTAggregateOptions struct {
GroupBy []FTAggregateGroupBy
SortBy []FTAggregateSortBy
SortByMax int
+ Scorer string
+ AddScores bool
Apply []FTAggregateApply
LimitOffset int
Limit int
@@ -283,7 +287,7 @@ type FTSearchSortBy struct {
type FTSearchOptions struct {
NoContent bool
Verbatim bool
- NoStopWrods bool
+ NoStopWords bool
WithScores bool
WithPayloads bool
WithSortKeys bool
@@ -481,6 +485,15 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
if options.Verbatim {
queryArgs = append(queryArgs, "VERBATIM")
}
+
+ if options.Scorer != "" {
+ queryArgs = append(queryArgs, "SCORER", options.Scorer)
+ }
+
+ if options.AddScores {
+ queryArgs = append(queryArgs, "ADDSCORES")
+ }
+
if options.LoadAll && options.Load != nil {
panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive")
}
@@ -489,16 +502,29 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
}
if options.Load != nil {
queryArgs = append(queryArgs, "LOAD", len(options.Load))
+ index, count := len(queryArgs)-1, 0
for _, load := range options.Load {
queryArgs = append(queryArgs, load.Field)
+ count++
if load.As != "" {
queryArgs = append(queryArgs, "AS", load.As)
+ count += 2
}
}
+ queryArgs[index] = count
}
+
if options.Timeout > 0 {
queryArgs = append(queryArgs, "TIMEOUT", options.Timeout)
}
+
+ for _, apply := range options.Apply {
+ queryArgs = append(queryArgs, "APPLY", apply.Field)
+ if apply.As != "" {
+ queryArgs = append(queryArgs, "AS", apply.As)
+ }
+ }
+
if options.GroupBy != nil {
for _, groupBy := range options.GroupBy {
queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields))
@@ -540,17 +566,8 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
if options.SortByMax > 0 {
queryArgs = append(queryArgs, "MAX", options.SortByMax)
}
- for _, apply := range options.Apply {
- queryArgs = append(queryArgs, "APPLY", apply.Field)
- if apply.As != "" {
- queryArgs = append(queryArgs, "AS", apply.As)
- }
- }
- if options.LimitOffset > 0 {
- queryArgs = append(queryArgs, "LIMIT", options.LimitOffset)
- }
- if options.Limit > 0 {
- queryArgs = append(queryArgs, options.Limit)
+ if options.LimitOffset >= 0 && options.Limit > 0 {
+ queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit)
}
if options.Filter != "" {
queryArgs = append(queryArgs, "FILTER", options.Filter)
@@ -572,6 +589,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery
queryArgs = append(queryArgs, key, value)
}
}
+
if options.DialectVersion > 0 {
queryArgs = append(queryArgs, "DIALECT", options.DialectVersion)
}
@@ -636,6 +654,14 @@ func (cmd *AggregateCmd) Result() (*FTAggregateResult, error) {
return cmd.val, cmd.err
}
+func (cmd *AggregateCmd) RawVal() interface{} {
+ return cmd.rawVal
+}
+
+func (cmd *AggregateCmd) RawResult() (interface{}, error) {
+ return cmd.rawVal, cmd.err
+}
+
func (cmd *AggregateCmd) String() string {
return cmdString(cmd, cmd.val)
}
@@ -643,12 +669,11 @@ func (cmd *AggregateCmd) String() string {
func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) {
data, err := rd.ReadSlice()
if err != nil {
- cmd.err = err
- return nil
+ return err
}
cmd.val, err = ProcessAggregateResult(data)
if err != nil {
- cmd.err = err
+ return err
}
return nil
}
@@ -664,6 +689,12 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st
if options.Verbatim {
args = append(args, "VERBATIM")
}
+ if options.Scorer != "" {
+ args = append(args, "SCORER", options.Scorer)
+ }
+ if options.AddScores {
+ args = append(args, "ADDSCORES")
+ }
if options.LoadAll && options.Load != nil {
panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive")
}
@@ -672,16 +703,26 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st
}
if options.Load != nil {
args = append(args, "LOAD", len(options.Load))
+ index, count := len(args)-1, 0
for _, load := range options.Load {
args = append(args, load.Field)
+ count++
if load.As != "" {
args = append(args, "AS", load.As)
+ count += 2
}
}
+ args[index] = count
}
if options.Timeout > 0 {
args = append(args, "TIMEOUT", options.Timeout)
}
+ for _, apply := range options.Apply {
+ args = append(args, "APPLY", apply.Field)
+ if apply.As != "" {
+ args = append(args, "AS", apply.As)
+ }
+ }
if options.GroupBy != nil {
for _, groupBy := range options.GroupBy {
args = append(args, "GROUPBY", len(groupBy.Fields))
@@ -723,17 +764,8 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st
if options.SortByMax > 0 {
args = append(args, "MAX", options.SortByMax)
}
- for _, apply := range options.Apply {
- args = append(args, "APPLY", apply.Field)
- if apply.As != "" {
- args = append(args, "AS", apply.As)
- }
- }
- if options.LimitOffset > 0 {
- args = append(args, "LIMIT", options.LimitOffset)
- }
- if options.Limit > 0 {
- args = append(args, options.Limit)
+ if options.LimitOffset >= 0 && options.Limit > 0 {
+ args = append(args, "LIMIT", options.LimitOffset, options.Limit)
}
if options.Filter != "" {
args = append(args, "FILTER", options.Filter)
@@ -798,13 +830,13 @@ func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string)
}
// FTAlter - Alters the definition of an existing index.
-// The 'index' parameter specifies the index to alter, and the 'skipInitalScan' parameter specifies whether to skip the initial scan.
+// The 'index' parameter specifies the index to alter, and the 'skipInitialScan' parameter specifies whether to skip the initial scan.
// The 'definition' parameter specifies the new definition for the index.
// For more information, please refer to the Redis documentation:
// [FT.ALTER]: (https://redis.io/commands/ft.alter/)
-func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd {
+func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd {
args := []interface{}{"FT.ALTER", index}
- if skipInitalScan {
+ if skipInitialScan {
args = append(args, "SKIPINITIALSCAN")
}
args = append(args, "SCHEMA", "ADD")
@@ -897,7 +929,7 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
args = append(args, "STOPWORDS", len(options.StopWords))
args = append(args, options.StopWords...)
}
- if options.SkipInitalScan {
+ if options.SkipInitialScan {
args = append(args, "SKIPINITIALSCAN")
}
}
@@ -993,8 +1025,8 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
if schema.Weight > 0 {
args = append(args, "WEIGHT", schema.Weight)
}
- if schema.Seperator != "" {
- args = append(args, "SEPERATOR", schema.Seperator)
+ if schema.Separator != "" {
+ args = append(args, "SEPARATOR", schema.Separator)
}
if schema.CaseSensitive {
args = append(args, "CASESENSITIVE")
@@ -1002,6 +1034,13 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp
if schema.WithSuffixtrie {
args = append(args, "WITHSUFFIXTRIE")
}
+ if schema.IndexEmpty {
+ args = append(args, "INDEXEMPTY")
+ }
+ if schema.IndexMissing {
+ args = append(args, "INDEXMISSING")
+
+ }
}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
@@ -1328,6 +1367,13 @@ func (cmd *FTInfoCmd) Val() FTInfoResult {
return cmd.val
}
+func (cmd *FTInfoCmd) RawVal() interface{} {
+ return cmd.rawVal
+}
+
+func (cmd *FTInfoCmd) RawResult() (interface{}, error) {
+ return cmd.rawVal, cmd.err
+}
func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {
n, err := rd.ReadMapLen()
if err != nil {
@@ -1356,7 +1402,7 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {
}
cmd.val, err = parseFTInfo(data)
if err != nil {
- cmd.err = err
+ return err
}
return nil
@@ -1438,15 +1484,22 @@ func (cmd *FTSpellCheckCmd) Val() []SpellCheckResult {
return cmd.val
}
+func (cmd *FTSpellCheckCmd) RawVal() interface{} {
+ return cmd.rawVal
+}
+
+func (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) {
+ return cmd.rawVal, cmd.err
+}
+
func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) {
data, err := rd.ReadSlice()
if err != nil {
- cmd.err = err
- return nil
+ return err
}
cmd.val, err = parseFTSpellCheck(data)
if err != nil {
- cmd.err = err
+ return err
}
return nil
}
@@ -1619,22 +1672,30 @@ func (cmd *FTSearchCmd) Val() FTSearchResult {
return cmd.val
}
+func (cmd *FTSearchCmd) RawVal() interface{} {
+ return cmd.rawVal
+}
+
+func (cmd *FTSearchCmd) RawResult() (interface{}, error) {
+ return cmd.rawVal, cmd.err
+}
+
func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {
data, err := rd.ReadSlice()
if err != nil {
- cmd.err = err
- return nil
+ return err
}
cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys)
if err != nil {
- cmd.err = err
+ return err
}
return nil
}
// FTSearch - Executes a search query on an index.
// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
-// For more information, please refer to the Redis documentation:
+// For more information, please refer to the Redis documentation about [FT.SEARCH].
+//
// [FT.SEARCH]: (https://redis.io/commands/ft.search/)
func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSearchCmd {
args := []interface{}{"FT.SEARCH", index, query}
@@ -1645,6 +1706,12 @@ func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSe
type SearchQuery []interface{}
+// FTSearchQuery - Executes a search query on an index with additional options.
+// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query,
+// and the 'options' parameter specifies additional options for the search.
+// For more information, please refer to the Redis documentation about [FT.SEARCH].
+//
+// [FT.SEARCH]: (https://redis.io/commands/ft.search/)
func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
queryArgs := []interface{}{query}
if options != nil {
@@ -1654,7 +1721,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
if options.Verbatim {
queryArgs = append(queryArgs, "VERBATIM")
}
- if options.NoStopWrods {
+ if options.NoStopWords {
queryArgs = append(queryArgs, "NOSTOPWORDS")
}
if options.WithScores {
@@ -1735,7 +1802,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
}
}
if options.SortByWithCount {
- queryArgs = append(queryArgs, "WITHCOUT")
+ queryArgs = append(queryArgs, "WITHCOUNT")
}
}
if options.LimitOffset >= 0 && options.Limit > 0 {
@@ -1757,7 +1824,8 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery {
// FTSearchWithArgs - Executes a search query on an index with additional options.
// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query,
// and the 'options' parameter specifies additional options for the search.
-// For more information, please refer to the Redis documentation:
+// For more information, please refer to the Redis documentation about [FT.SEARCH].
+//
// [FT.SEARCH]: (https://redis.io/commands/ft.search/)
func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd {
args := []interface{}{"FT.SEARCH", index, query}
@@ -1768,7 +1836,7 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin
if options.Verbatim {
args = append(args, "VERBATIM")
}
- if options.NoStopWrods {
+ if options.NoStopWords {
args = append(args, "NOSTOPWORDS")
}
if options.WithScores {
@@ -1849,7 +1917,7 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin
}
}
if options.SortByWithCount {
- args = append(args, "WITHCOUT")
+ args = append(args, "WITHCOUNT")
}
}
if options.LimitOffset >= 0 && options.Limit > 0 {
@@ -1895,6 +1963,14 @@ func (cmd *FTSynDumpCmd) Result() ([]FTSynDumpResult, error) {
return cmd.val, cmd.err
}
+func (cmd *FTSynDumpCmd) RawVal() interface{} {
+ return cmd.rawVal
+}
+
+func (cmd *FTSynDumpCmd) RawResult() (interface{}, error) {
+ return cmd.rawVal, cmd.err
+}
+
func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error {
termSynonymPairs, err := rd.ReadSlice()
if err != nil {
diff --git search_test.go search_test.go
index 60888ef5c..af98e5f7d 100644
--- search_test.go
+++ search_test.go
@@ -2,6 +2,8 @@ package redis_test
import (
"context"
+ "fmt"
+ "strconv"
"time"
. "github.com/bsm/ginkgo/v2"
@@ -18,11 +20,13 @@ func WaitForIndexing(c *redis.Client, index string) {
return
}
time.Sleep(100 * time.Millisecond)
+ } else {
+ return
}
}
}
-var _ = Describe("RediSearch commands", Label("search"), func() {
+var _ = Describe("RediSearch commands Resp 2", Label("search"), func() {
ctx := context.TODO()
var client *redis.Client
@@ -123,6 +127,13 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
Expect(res2.Docs[1].ID).To(BeEquivalentTo("doc2"))
Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc3"))
+ res3, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res3.Total).To(BeEquivalentTo(int64(3)))
+
+ res4, err := client.FTSearchWithArgs(ctx, "num", "notpresentf00", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res4.Total).To(BeEquivalentTo(int64(0)))
})
It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() {
@@ -130,7 +141,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(client, "txt")
- client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch impements a search engine on top of redis")
+ client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis")
res1, err := client.FTSearchWithArgs(ctx, "txt", "search engine", &redis.FTSearchOptions{NoContent: true, Verbatim: true, LimitOffset: 0, Limit: 5}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(1)))
@@ -258,6 +269,8 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
Expect(err).NotTo(HaveOccurred())
Expect(res1.Total).To(BeEquivalentTo(int64(1)))
+ _, err = client.FTSearch(ctx, "idx_not_exist", "only in the body").Result()
+ Expect(err).To(HaveOccurred())
})
It("should FTSpellCheck", Label("search", "ftcreate", "ftsearch", "ftspellcheck"), func() {
@@ -430,7 +443,7 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
WaitForIndexing(client, "idx1")
client.HSet(ctx, "search", "title", "RediSearch",
- "body", "Redisearch impements a search engine on top of redis",
+ "body", "Redisearch implements a search engine on top of redis",
"parent", "redis",
"random_num", 10)
client.HSet(ctx, "ai", "title", "RedisAI",
@@ -559,6 +572,11 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b"))
+
+ options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, Limit: 1, LimitOffset: 0}
+ res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a"))
})
It("should FTAggregate load ", Label("search", "ftaggregate"), func() {
@@ -581,11 +599,118 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world"))
+ options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t2", As: "t2alias"}}}
+ res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world"))
+
+ options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t1"}, {Field: "t2", As: "t2alias"}}}
+ res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello"))
+ Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world"))
+
options = &redis.FTAggregateOptions{LoadAll: true}
res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello"))
Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world"))
+
+ _, err = client.FTAggregateWithArgs(ctx, "idx_not_exist", "*", &redis.FTAggregateOptions{}).Result()
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() {
+ title := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: false}
+ description := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, Sortable: false}
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}}, title, description).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "idx1")
+
+ client.HSet(ctx, "product:1", "title", "New Gaming Laptop", "description", "this is not a desktop")
+ client.HSet(ctx, "product:2", "title", "Super Old Not Gaming Laptop", "description", "this laptop is not a new laptop but it is a laptop")
+ client.HSet(ctx, "product:3", "title", "Office PC", "description", "office desktop pc")
+
+ options := &redis.FTAggregateOptions{
+ AddScores: true,
+ Scorer: "BM25",
+ SortBy: []redis.FTAggregateSortBy{{
+ FieldName: "@__score",
+ Desc: true,
+ }},
+ }
+
+ res, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", options).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).ToNot(BeNil())
+ Expect(len(res.Rows)).To(BeEquivalentTo(2))
+ score1, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"]), 64)
+ Expect(err).NotTo(HaveOccurred())
+ score2, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]), 64)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(score1).To(BeNumerically(">", score2))
+
+ optionsDM := &redis.FTAggregateOptions{
+ AddScores: true,
+ Scorer: "DISMAX",
+ SortBy: []redis.FTAggregateSortBy{{
+ FieldName: "@__score",
+ Desc: true,
+ }},
+ }
+
+ resDM, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", optionsDM).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resDM).ToNot(BeNil())
+ Expect(len(resDM.Rows)).To(BeEquivalentTo(2))
+ score1DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"]), 64)
+ Expect(err).NotTo(HaveOccurred())
+ score2DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]), 64)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(score1DM).To(BeNumerically(">", score2DM))
+
+ Expect(score1DM).To(BeEquivalentTo(float64(4)))
+ Expect(score2DM).To(BeEquivalentTo(float64(1)))
+ Expect(score1).NotTo(BeEquivalentTo(score1DM))
+ Expect(score2).NotTo(BeEquivalentTo(score2DM))
+ })
+
+ It("should FTAggregate apply and groupby", Label("search", "ftaggregate"), func() {
+ text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true}
+ num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "idx1")
+
+ // 6 feb
+ client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "1738823999")
+
+ // 12 feb
+ client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739342399")
+ client.HSet(ctx, "doc3", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739353199")
+
+ reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount, As: "perDay"}
+
+ options := &redis.FTAggregateOptions{
+ Apply: []redis.FTAggregateApply{{Field: "floo,r(@CreatedDateTimeUTC /(60*60*24))", As: "TimestampAsDay"}},
+ GroupBy: []redis.FTAggregateGroupBy{{
+ Fields: []interface{}{"@TimestampAsDay"},
+ Reduce: []redis.FTAggregateReducer{reducer},
+ }},
+ SortBy: []redis.FTAggregateSortBy{{
+ FieldName: "@perDay",
+ Desc: true,
+ }},
+ }
+
+ res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res).ToNot(BeNil())
+ Expect(len(res.Rows)).To(BeEquivalentTo(2))
+ Expect(res.Rows[0].Fields["perDay"]).To(BeEquivalentTo("2"))
+ Expect(res.Rows[1].Fields["perDay"]).To(BeEquivalentTo("1"))
})
It("should FTAggregate apply", Label("search", "ftaggregate"), func() {
@@ -632,14 +757,13 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
Expect(res.Rows[0].Fields["age"]).To(BeEquivalentTo("19"))
Expect(res.Rows[1].Fields["age"]).To(BeEquivalentTo("25"))
}
-
})
- It("should FTSearch SkipInitalScan", Label("search", "ftsearch"), func() {
+ It("should FTSearch SkipInitialScan", Label("search", "ftsearch"), func() {
client.HSet(ctx, "doc1", "foo", "bar")
text1 := &redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}
- val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitalScan: true}, text1).Result()
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitialScan: true}, text1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEquivalentTo("OK"))
WaitForIndexing(client, "idx1")
@@ -1017,6 +1141,111 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue())
})
+ It("should test dialect 4", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{
+ Prefix: []interface{}{"resource:"},
+ }, &redis.FieldSchema{
+ FieldName: "uuid",
+ FieldType: redis.SearchFieldTypeTag,
+ }, &redis.FieldSchema{
+ FieldName: "tags",
+ FieldType: redis.SearchFieldTypeTag,
+ }, &redis.FieldSchema{
+ FieldName: "description",
+ FieldType: redis.SearchFieldTypeText,
+ }, &redis.FieldSchema{
+ FieldName: "rating",
+ FieldType: redis.SearchFieldTypeNumeric,
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+
+ client.HSet(ctx, "resource:1", map[string]interface{}{
+ "uuid": "123e4567-e89b-12d3-a456-426614174000",
+ "tags": "finance|crypto|$btc|blockchain",
+ "description": "Analysis of blockchain technologies & Bitcoin's potential.",
+ "rating": 5,
+ })
+ client.HSet(ctx, "resource:2", map[string]interface{}{
+ "uuid": "987e6543-e21c-12d3-a456-426614174999",
+ "tags": "health|well-being|fitness|new-year's-resolutions",
+ "description": "Health trends for the new year, including fitness regimes.",
+ "rating": 4,
+ })
+
+ res, err := client.FTSearchWithArgs(ctx, "idx1", "@uuid:{$uuid}",
+ &redis.FTSearchOptions{
+ DialectVersion: 2,
+ Params: map[string]interface{}{"uuid": "123e4567-e89b-12d3-a456-426614174000"},
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Total).To(BeEquivalentTo(int64(1)))
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("resource:1"))
+
+ res, err = client.FTSearchWithArgs(ctx, "idx1", "@uuid:{$uuid}",
+ &redis.FTSearchOptions{
+ DialectVersion: 4,
+ Params: map[string]interface{}{"uuid": "123e4567-e89b-12d3-a456-426614174000"},
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Total).To(BeEquivalentTo(int64(1)))
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("resource:1"))
+
+ client.HSet(ctx, "test:1", map[string]interface{}{
+ "uuid": "3d3586fe-0416-4572-8ce",
+ "email": "[email protected]",
+ "num": 5,
+ })
+
+ // Create the index
+ ftCreateOptions := &redis.FTCreateOptions{
+ Prefix: []interface{}{"test:"},
+ }
+ schema := []*redis.FieldSchema{
+ {
+ FieldName: "uuid",
+ FieldType: redis.SearchFieldTypeTag,
+ },
+ {
+ FieldName: "email",
+ FieldType: redis.SearchFieldTypeTag,
+ },
+ {
+ FieldName: "num",
+ FieldType: redis.SearchFieldTypeNumeric,
+ },
+ }
+
+ val, err = client.FTCreate(ctx, "idx_hash", ftCreateOptions, schema...).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(Equal("OK"))
+ WaitForIndexing(client, "idx_hash")
+
+ ftSearchOptions := &redis.FTSearchOptions{
+ DialectVersion: 4,
+ Params: map[string]interface{}{
+ "uuid": "3d3586fe-0416-4572-8ce",
+ "email": "[email protected]",
+ },
+ }
+
+ res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@uuid:{$uuid}", ftSearchOptions).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
+ Expect(res.Docs[0].Fields["uuid"]).To(BeEquivalentTo("3d3586fe-0416-4572-8ce"))
+
+ res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@email:{$email}", ftSearchOptions).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
+ Expect(res.Docs[0].Fields["email"]).To(BeEquivalentTo("[email protected]"))
+
+ ftSearchOptions.Params = map[string]interface{}{"num": 5}
+ res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@num:[5]", ftSearchOptions).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1"))
+ Expect(res.Docs[0].Fields["num"]).To(BeEquivalentTo("5"))
+ })
+
It("should FTCreate GeoShape", Label("search", "ftcreate", "ftsearch"), func() {
val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "geom", FieldType: redis.SearchFieldTypeGeoShape, GeoShapeFieldType: "FLAT"}).Result()
Expect(err).NotTo(HaveOccurred())
@@ -1043,8 +1272,185 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
Expect(err).NotTo(HaveOccurred())
Expect(res2.Total).To(BeEquivalentTo(int64(2)))
})
+
+ It("should create search index with FLOAT16 and BFLOAT16 vectors", Label("search", "ftcreate", "NonRedisEnterprise"), func() {
+ val, err := client.FTCreate(ctx, "index", &redis.FTCreateOptions{},
+ &redis.FieldSchema{FieldName: "float16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "FLOAT16", Dim: 768, DistanceMetric: "COSINE"}}},
+ &redis.FieldSchema{FieldName: "bfloat16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "BFLOAT16", Dim: 768, DistanceMetric: "COSINE"}}},
+ ).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "index")
+ })
+
+ It("should test geoshapes query intersects and disjoint", Label("NonRedisEnterprise"), func() {
+ _, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{
+ FieldName: "g",
+ FieldType: redis.SearchFieldTypeGeoShape,
+ GeoShapeFieldType: "FLAT",
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+
+ client.HSet(ctx, "doc_point1", "g", "POINT (10 10)")
+ client.HSet(ctx, "doc_point2", "g", "POINT (50 50)")
+ client.HSet(ctx, "doc_polygon1", "g", "POLYGON ((20 20, 25 35, 35 25, 20 20))")
+ client.HSet(ctx, "doc_polygon2", "g", "POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))")
+
+ intersection, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[intersects $shape]",
+ &redis.FTSearchOptions{
+ DialectVersion: 3,
+ Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+ _assert_geosearch_result(&intersection, []string{"doc_point2", "doc_polygon1"})
+
+ disjunction, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[disjoint $shape]",
+ &redis.FTSearchOptions{
+ DialectVersion: 3,
+ Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+ _assert_geosearch_result(&disjunction, []string{"doc_point1", "doc_polygon2"})
+ })
+
+ It("should test geoshapes query contains and within", func() {
+ _, err := client.FTCreate(ctx, "idx2", &redis.FTCreateOptions{}, &redis.FieldSchema{
+ FieldName: "g",
+ FieldType: redis.SearchFieldTypeGeoShape,
+ GeoShapeFieldType: "FLAT",
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+
+ client.HSet(ctx, "doc_point1", "g", "POINT (10 10)")
+ client.HSet(ctx, "doc_point2", "g", "POINT (50 50)")
+ client.HSet(ctx, "doc_polygon1", "g", "POLYGON ((20 20, 25 35, 35 25, 20 20))")
+ client.HSet(ctx, "doc_polygon2", "g", "POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))")
+
+ containsA, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[contains $shape]",
+ &redis.FTSearchOptions{
+ DialectVersion: 3,
+ Params: map[string]interface{}{"shape": "POINT(25 25)"},
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+ _assert_geosearch_result(&containsA, []string{"doc_polygon1"})
+
+ containsB, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[contains $shape]",
+ &redis.FTSearchOptions{
+ DialectVersion: 3,
+ Params: map[string]interface{}{"shape": "POLYGON((24 24, 24 26, 25 25, 24 24))"},
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+ _assert_geosearch_result(&containsB, []string{"doc_polygon1"})
+
+ within, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[within $shape]",
+ &redis.FTSearchOptions{
+ DialectVersion: 3,
+ Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"},
+ }).Result()
+ Expect(err).NotTo(HaveOccurred())
+ _assert_geosearch_result(&within, []string{"doc_point2", "doc_polygon1"})
+ })
+
+ It("should search missing fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}},
+ &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true},
+ &redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexMissing: true},
+ &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, IndexMissing: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "idx1")
+
+ client.HSet(ctx, "property:1", map[string]interface{}{
+ "title": "Luxury Villa in Malibu",
+ "features": "pool,sea view,modern",
+ "description": "A stunning modern villa overlooking the Pacific Ocean.",
+ })
+
+ client.HSet(ctx, "property:2", map[string]interface{}{
+ "title": "Downtown Flat",
+ "description": "Modern flat in central Paris with easy access to metro.",
+ })
+
+ client.HSet(ctx, "property:3", map[string]interface{}{
+ "title": "Beachfront Bungalow",
+ "features": "beachfront,sun deck",
+ })
+
+ res, err := client.FTSearchWithArgs(ctx, "idx1", "ismissing(@features)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("property:2"))
+
+ res, err = client.FTSearchWithArgs(ctx, "idx1", "-ismissing(@features)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
+ Expect(res.Docs[1].ID).To(BeEquivalentTo("property:3"))
+
+ res, err = client.FTSearchWithArgs(ctx, "idx1", "ismissing(@description)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("property:3"))
+
+ res, err = client.FTSearchWithArgs(ctx, "idx1", "-ismissing(@description)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
+ Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2"))
+ })
+
+ It("should search empty fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() {
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}},
+ &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true},
+ &redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexEmpty: true},
+ &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, IndexEmpty: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "idx1")
+
+ client.HSet(ctx, "property:1", map[string]interface{}{
+ "title": "Luxury Villa in Malibu",
+ "features": "pool,sea view,modern",
+ "description": "A stunning modern villa overlooking the Pacific Ocean.",
+ })
+
+ client.HSet(ctx, "property:2", map[string]interface{}{
+ "title": "Downtown Flat",
+ "features": "",
+ "description": "Modern flat in central Paris with easy access to metro.",
+ })
+
+ client.HSet(ctx, "property:3", map[string]interface{}{
+ "title": "Beachfront Bungalow",
+ "features": "beachfront,sun deck",
+ "description": "",
+ })
+
+ res, err := client.FTSearchWithArgs(ctx, "idx1", "@features:{\"\"}", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("property:2"))
+
+ res, err = client.FTSearchWithArgs(ctx, "idx1", "-@features:{\"\"}", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
+ Expect(res.Docs[1].ID).To(BeEquivalentTo("property:3"))
+
+ res, err = client.FTSearchWithArgs(ctx, "idx1", "@description:''", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("property:3"))
+
+ res, err = client.FTSearchWithArgs(ctx, "idx1", "-@description:''", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1"))
+ Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2"))
+ })
})
+func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) {
+ ids := make([]string, len(result.Docs))
+ for i, doc := range result.Docs {
+ ids[i] = doc.ID
+ }
+ Expect(ids).To(ConsistOf(expectedDocIDs))
+ Expect(result.Total).To(BeEquivalentTo(len(expectedDocIDs)))
+}
+
// It("should FTProfile Search and Aggregate", Label("search", "ftprofile"), func() {
// val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result()
// Expect(err).NotTo(HaveOccurred())
@@ -1134,3 +1540,189 @@ var _ = Describe("RediSearch commands", Label("search"), func() {
// Expect(results0["id"]).To(BeEquivalentTo("a"))
// Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0"))
// })
+
+var _ = Describe("RediSearch commands Resp 3", Label("search"), func() {
+ ctx := context.TODO()
+ var client *redis.Client
+ var client2 *redis.Client
+
+ BeforeEach(func() {
+ client = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3, UnstableResp3: true})
+ client2 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3})
+ Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
+ })
+
+ AfterEach(func() {
+ Expect(client.Close()).NotTo(HaveOccurred())
+ })
+
+ It("should handle FTAggregate with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftaggregate"), func() {
+ text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true}
+ num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "idx1")
+
+ client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "637387878524969984")
+ client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "637387875859270016")
+
+ options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
+ res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult()
+ results := res.(map[interface{}]interface{})["results"].([]interface{})
+ Expect(results[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]).
+ To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
+ Expect(results[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]).
+ To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416")))
+
+ rawVal := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal()
+ rawValResults := rawVal.(map[interface{}]interface{})["results"].([]interface{})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(rawValResults[0]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1])))
+ Expect(rawValResults[1]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1])))
+
+ // Test with UnstableResp3 false
+ Expect(func() {
+ options = &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}}
+ rawRes, _ := client2.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult()
+ rawVal = client2.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal()
+ Expect(rawRes).To(BeNil())
+ Expect(rawVal).To(BeNil())
+ }).Should(Panic())
+
+ })
+
+ It("should handle FTInfo with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftinfo"), func() {
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText, Sortable: true, NoStem: true}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "idx1")
+
+ resInfo, err := client.FTInfo(ctx, "idx1").RawResult()
+ Expect(err).NotTo(HaveOccurred())
+ attributes := resInfo.(map[interface{}]interface{})["attributes"].([]interface{})
+ flags := attributes[0].(map[interface{}]interface{})["flags"].([]interface{})
+ Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM"))
+
+ valInfo := client.FTInfo(ctx, "idx1").RawVal()
+ attributes = valInfo.(map[interface{}]interface{})["attributes"].([]interface{})
+ flags = attributes[0].(map[interface{}]interface{})["flags"].([]interface{})
+ Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM"))
+
+ // Test with UnstableResp3 false
+ Expect(func() {
+ rawResInfo, _ := client2.FTInfo(ctx, "idx1").RawResult()
+ rawValInfo := client2.FTInfo(ctx, "idx1").RawVal()
+ Expect(rawResInfo).To(BeNil())
+ Expect(rawValInfo).To(BeNil())
+ }).Should(Panic())
+ })
+
+ It("should handle FTSpellCheck with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftspellcheck"), func() {
+ text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
+ text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "idx1")
+
+ client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text")
+ client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum")
+
+ resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").RawResult()
+ valSpellCheck := client.FTSpellCheck(ctx, "idx1", "impornant").RawVal()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(valSpellCheck).To(BeEquivalentTo(resSpellCheck))
+ results := resSpellCheck.(map[interface{}]interface{})["results"].(map[interface{}]interface{})
+ Expect(results["impornant"].([]interface{})[0].(map[interface{}]interface{})["important"]).To(BeEquivalentTo(0.5))
+
+ // Test with UnstableResp3 false
+ Expect(func() {
+ rawResSpellCheck, _ := client2.FTSpellCheck(ctx, "idx1", "impornant").RawResult()
+ rawValSpellCheck := client2.FTSpellCheck(ctx, "idx1", "impornant").RawVal()
+ Expect(rawResSpellCheck).To(BeNil())
+ Expect(rawValSpellCheck).To(BeNil())
+ }).Should(Panic())
+ })
+
+ It("should handle FTSearch with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftsearch"), func() {
+ val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{StopWords: []interface{}{"foo", "bar", "baz"}}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "txt")
+ client.HSet(ctx, "doc1", "txt", "foo baz")
+ client.HSet(ctx, "doc2", "txt", "hello world")
+ res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).RawResult()
+ val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).RawVal()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val1).To(BeEquivalentTo(res1))
+ totalResults := res1.(map[interface{}]interface{})["total_results"]
+ Expect(totalResults).To(BeEquivalentTo(int64(0)))
+ res2, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawResult()
+ Expect(err).NotTo(HaveOccurred())
+ totalResults2 := res2.(map[interface{}]interface{})["total_results"]
+ Expect(totalResults2).To(BeEquivalentTo(int64(1)))
+
+ // Test with UnstableResp3 false
+ Expect(func() {
+ rawRes2, _ := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawResult()
+ rawVal2 := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawVal()
+ Expect(rawRes2).To(BeNil())
+ Expect(rawVal2).To(BeNil())
+ }).Should(Panic())
+ })
+ It("should handle FTSynDump with Unstable RESP3 Search Module and without stability", Label("search", "ftsyndump"), func() {
+ text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}
+ text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}
+ val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "idx1")
+
+ resSynUpdate, err := client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"boy", "child", "offspring"}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resSynUpdate).To(BeEquivalentTo("OK"))
+
+ resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"baby", "child"}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resSynUpdate).To(BeEquivalentTo("OK"))
+
+ resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"tree", "wood"}).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resSynUpdate).To(BeEquivalentTo("OK"))
+
+ resSynDump, err := client.FTSynDump(ctx, "idx1").RawResult()
+ valSynDump := client.FTSynDump(ctx, "idx1").RawVal()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(valSynDump).To(BeEquivalentTo(resSynDump))
+ Expect(resSynDump.(map[interface{}]interface{})["baby"]).To(BeEquivalentTo([]interface{}{"id1"}))
+
+ // Test with UnstableResp3 false
+ Expect(func() {
+ rawResSynDump, _ := client2.FTSynDump(ctx, "idx1").RawResult()
+ rawValSynDump := client2.FTSynDump(ctx, "idx1").RawVal()
+ Expect(rawResSynDump).To(BeNil())
+ Expect(rawValSynDump).To(BeNil())
+ }).Should(Panic())
+ })
+
+ It("should test not affected Resp 3 Search method - FTExplain", Label("search", "ftexplain"), func() {
+ text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText}
+ text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText}
+ text3 := &redis.FieldSchema{FieldName: "f3", FieldType: redis.SearchFieldTypeText}
+ val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, text1, text2, text3).Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(val).To(BeEquivalentTo("OK"))
+ WaitForIndexing(client, "txt")
+ res1, err := client.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res1).ToNot(BeEmpty())
+
+ // Test with UnstableResp3 false
+ Expect(func() {
+ res2, err := client2.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(res2).ToNot(BeEmpty())
+ }).ShouldNot(Panic())
+ })
+})
diff --git sentinel.go sentinel.go
index 188f88494..a4c9f53c4 100644
--- sentinel.go
+++ sentinel.go
@@ -80,8 +80,20 @@ type FailoverOptions struct {
TLSConfig *tls.Config
+ // DisableIndentity - Disable set-lib on connect.
+ //
+ // default: false
+ //
+ // Deprecated: Use DisableIdentity instead.
DisableIndentity bool
- IdentitySuffix string
+
+ // DisableIdentity is used to disable CLIENT SETINFO command on connect.
+ //
+ // default: false
+ DisableIdentity bool
+
+ IdentitySuffix string
+ UnstableResp3 bool
}
func (opt *FailoverOptions) clientOptions() *Options {
@@ -117,8 +129,11 @@ func (opt *FailoverOptions) clientOptions() *Options {
TLSConfig: opt.TLSConfig,
+ DisableIdentity: opt.DisableIdentity,
DisableIndentity: opt.DisableIndentity,
- IdentitySuffix: opt.IdentitySuffix,
+
+ IdentitySuffix: opt.IdentitySuffix,
+ UnstableResp3: opt.UnstableResp3,
}
}
@@ -154,8 +169,11 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
TLSConfig: opt.TLSConfig,
+ DisableIdentity: opt.DisableIdentity,
DisableIndentity: opt.DisableIndentity,
- IdentitySuffix: opt.IdentitySuffix,
+
+ IdentitySuffix: opt.IdentitySuffix,
+ UnstableResp3: opt.UnstableResp3,
}
}
@@ -194,8 +212,10 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
TLSConfig: opt.TLSConfig,
+ DisableIdentity: opt.DisableIdentity,
DisableIndentity: opt.DisableIndentity,
- IdentitySuffix: opt.IdentitySuffix,
+
+ IdentitySuffix: opt.IdentitySuffix,
}
}
diff --git universal.go universal.go
index 275bef3d6..483c81127 100644
--- universal.go
+++ universal.go
@@ -61,13 +61,25 @@ type UniversalOptions struct {
RouteByLatency bool
RouteRandomly bool
- // The sentinel master name.
- // Only failover clients.
-
+ // MasterName is the sentinel master name.
+ // Only for failover clients.
MasterName string
+ // DisableIndentity - Disable set-lib on connect.
+ //
+ // default: false
+ //
+ // Deprecated: Use DisableIdentity instead.
DisableIndentity bool
- IdentitySuffix string
+
+ // DisableIdentity is used to disable CLIENT SETINFO command on connect.
+ //
+ // default: false
+ DisableIdentity bool
+
+ IdentitySuffix string
+ UnstableResp3 bool
+
}
// Cluster returns cluster options created from the universal options.
@@ -112,8 +124,10 @@ func (o *UniversalOptions) Cluster() *ClusterOptions {
TLSConfig: o.TLSConfig,
+ DisableIdentity: o.DisableIdentity,
DisableIndentity: o.DisableIndentity,
IdentitySuffix: o.IdentitySuffix,
+ UnstableResp3: o.UnstableResp3,
}
}
@@ -158,8 +172,10 @@ func (o *UniversalOptions) Failover() *FailoverOptions {
TLSConfig: o.TLSConfig,
+ DisableIdentity: o.DisableIdentity,
DisableIndentity: o.DisableIndentity,
IdentitySuffix: o.IdentitySuffix,
+ UnstableResp3: o.UnstableResp3,
}
}
@@ -201,8 +217,10 @@ func (o *UniversalOptions) Simple() *Options {
TLSConfig: o.TLSConfig,
+ DisableIdentity: o.DisableIdentity,
DisableIn,dentity: o.DisableIndentity,
IdentitySuffix: o.IdentitySuffix,
+ UnstableResp3: o.UnstableResp3,
}
}
diff --git universal_test.go universal_test.go
index 747c68acb..9328b4776 100644
--- universal_test.go
+++ universal_test.go
@@ -38,4 +38,26 @@ var _ = Describe("UniversalClient", func() {
})
Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred())
})
+
+ It("connect to clusters with UniversalClient and UnstableResp3", Label("NonRedisEnterprise"), func() {
+ client = redis.NewUniversalClient(&redis.UniversalOptions{
+ Addrs: cluster.addrs(),
+ Protocol: 3,
+ UnstableResp3: true,
+ })
+ Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred())
+ a := func() { client.FTInfo(ctx, "all").Result() }
+ Expect(a).ToNot(Panic())
+ })
+
+ It("connect to clusters with ClusterClient and UnstableResp3", Label("NonRedisEnterprise"), func() {
+ client = redis.NewClusterClient(&redis.ClusterOptions{
+ Addrs: cluster.addrs(),
+ Protocol: 3,
+ UnstableResp3: true,
+ })
+ Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred())
+ a := func() { client.FTInfo(ctx, "all").Result() }
+ Expect(a).ToNot(Panic())
+ })
})
diff --git version.go version.go
index 2ea7df99a..a4832fc1e 100644
--- version.go
+++ version.go
@@ -2,5 +2,5 @@ package redis
// Version is the current release version.
func Version() string {
- return "9.5.3"
+ return "9.7.3"
}
Description
This PR includes several significant changes to the go-redis library, including bug fixes, new features, documentation improvements, and cleanup of deprecated code. The key motivations are improving stability, adding RESP3 support for Redis Search commands, and fixing connection handling.
Possible Issues
- Breaking change in connection identity verification where
DisableIndentity(with typo) is deprecated in favor ofDisableIdentity - New UnstableResp3 flag requirement for certain Redis Search commands could break existing code if not properly migrated
Security Hotspots
- TLS connection handling changes in
connCheckfunction - removal of extra TLS unwrapping could affect TLS verification behavior - Identity verification changes could potentially impact client identification and authentication flows
Changes
Changes
- Version & Documentation:
- Updated version from 9.5.3 to 9.7.3
- Added RESP3 documentation and unstable warning for RediSearch commands
- Added retraction notices for accidentally released versions
- Added code coverage badge and reporting
- Core Features:
- Added RESP3 support for unstable Redis Search commands
- Fixed connection identity verification (DisableIdentity vs DisableIndentity)
- Added UnstableResp3 flag for handling unstable RESP3 features
- Bug Fixes:
- Fixed immediacy of MOVED/ASK redirection in cluster mode
- Fixed connection check behavior for TLS connections
- Fixed race condition in hook handling
- Fixed array copy issues in cluster node addressing
- Testing:
- Added extensive tests for RESP3 search commands
- Added tests for bitmap, JSON, and list operations
- Added test coverage for new features and edge cases
sequenceDiagram
participant Client
participant Redis
participant Search
Client->>Redis: Connect with RESP3
alt UnstableResp3 Enabled
Client->>Search: Execute Search Command
Search-->>Client: Raw RESP3 Response
else UnstableResp3 Disabled
Client->>Search: Execute Search Command
Search-->>Client: Panic (Unstable API)
end
alt MOVED/ASK Response
Redis-->>Client: Redirect Response
Note over Client: Immediate Retry
Client->>Redis: Retry Command
end
alt Identity Check
Client->>Redis: CLIENT SETINFO
Note over Redis: Check DisableIdentity
Redis-->>Client: OK/Error
end