1
0
mirror of https://github.com/redis/go-redis.git synced 2025-04-26 21:08:50 +03:00

Merge branch 'master' into os-add-csc-redis-commands

This commit is contained in:
ofekshenawa 2025-03-13 10:54:58 +02:00 committed by GitHub
commit da5a7e3def
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
129 changed files with 19469 additions and 3601 deletions
.github
.gitignore.golangci.ymlCHANGELOG.mdCONTRIBUTING.mdMakefileREADME.mdacl_commands.goacl_commands_test.gobench_test.gobitmap_commands.gobitmap_commands_test.gocluster_commands.gocommand.gocommands.gocommands_test.godocker-compose.yml
dockers
doctests
error.go
example
del-keys-without-ttl
hll
hset-struct
lua-scripting
otel
redis-bloom
scan-struct
example_test.go
extra
gears_commands_test.gogeneric_commands.gogo.modgo.sumhash_commands.go
internal
iterator_test.go

53
.github/actions/run-tests/action.yml vendored Normal file

@ -0,0 +1,53 @@
name: 'Run go-redis tests'
description: 'Runs go-redis tests against different Redis versions and configurations'
inputs:
go-version:
description: 'Go version to use for running tests'
default: '1.23'
redis-version:
description: 'Redis version to test against'
required: true
runs:
using: "composite"
steps:
- name: Set up ${{ inputs.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ inputs.go-version }}
- name: Setup Test environment
env:
REDIS_VERSION: ${{ inputs.redis-version }}
CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ inputs.redis-version }}"
run: |
set -e
redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+')
# Mapping of redis version to redis testing containers
declare -A redis_version_mapping=(
["8.0-M03"]="8.0-M04-pre"
["7.4.2"]="rs-7.4.0-v2"
["7.2.7"]="rs-7.2.0-v14"
)
if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then
echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV
echo "REDIS_IMAGE=redis:${{ inputs.redis-version }}" >> $GITHUB_ENV
echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV
else
echo "Version not found in the mapping."
exit 1
fi
sleep 10 # wait for redis to start
shell: bash
- name: Set up Docker Compose environment with redis ${{ inputs.redis-version }}
run: |
make docker.start
shell: bash
- name: Run tests
env:
RCE_DOCKER: "true"
RE_CLUSTER: "false"
run: |
make test.ci
shell: bash

12
.github/wordlist.txt vendored

@ -1,7 +1,9 @@
ACLs
APIs
autoload
autoloader
autoloading
analytics
Autoloading
backend
backends
@ -13,6 +15,7 @@ customizable
Customizable
dataset
de
DisableIdentity
ElastiCache
extensibility
FPM
@ -43,15 +46,22 @@ RocksDB
runtime
SHA
sharding
SETNAME
SpellCheck
SSL
struct
stunnel
SynDump
TCP
TLS
UnstableResp
uri
URI
url
variadic
RedisStack
RedisGears
RedisTimeseries
RedisTimeseries
RediSearch
RawResult
RawVal

@ -2,29 +2,27 @@ 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
jobs:
build:
name: build
benchmark:
name: benchmark
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: [1.20.x, 1.21.x]
services:
redis:
image: redis/redis-stack-server:edge
options: >-
--health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 6379:6379
redis-version:
- "8.0-M03" # 8.0 milestone 4
- "7.4.2" # should use redis stack 7.4
go-version:
- "1.23.x"
- "1.24.x"
steps:
- name: Set up ${{ matrix.go-version }}
@ -35,5 +33,65 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Test
run: make test
- name: Setup Test environment
env:
REDIS_VERSION: ${{ matrix.redis-version }}
CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ matrix.redis-version }}"
run: |
set -e
redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+')
# Mapping of redis version to redis testing containers
declare -A redis_version_mapping=(
["8.0-M03"]="8.0-M04-pre"
["7.4.2"]="rs-7.4.0-v2"
)
if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then
echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV
echo "REDIS_IMAGE=redis:${{ matrix.redis-version }}" >> $GITHUB_ENV
echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV
else
echo "Version not found in the mapping."
exit 1
fi
shell: bash
- name: Set up Docker Compose environment with redis ${{ matrix.redis-version }}
run: make docker.start
shell: bash
- name: Benchmark Tests
env:
RCE_DOCKER: "true"
RE_CLUSTER: "false"
run: make bench
shell: bash
test-redis-ce:
name: test-redis-ce
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
redis-version:
- "8.0-M03" # 8.0 milestone 4
- "7.4.2" # should use redis stack 7.4
- "7.2.7" # should redis stack 7.2
go-version:
- "1.23.x"
- "1.24.x"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests
uses: ./.github/actions/run-tests
with:
go-version: ${{matrix.go-version}}
redis-version: ${{ matrix.redis-version }}
- name: Upload to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}

68
.github/workflows/codeql-analysis.yml vendored Normal file

@ -0,0 +1,68 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

@ -25,7 +25,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: [ "1.18", "1.19", "1.20", "1.21" ]
go-version: ["1.24"]
steps:
- name: Set up ${{ matrix.go-version }}
@ -38,4 +38,4 @@ jobs:
- name: Test doc examples
working-directory: ./doctests
run: go test
run: go test -v

@ -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/golangci-lint-action@v6.5.0

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@v6
with:
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
config-name: release-drafter-config.yml

@ -8,7 +8,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Check Spelling
uses: rojopolis/spellcheck-github-actions@0.36.0
uses: rojopolis/spellcheck-github-actions@0.47.0
with:
config_path: .github/spellcheck-settings.yml
task_name: Markdown

@ -15,8 +15,8 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: [1.21.x]
re-build: ["7.2.4-108"]
go-version: [1.24.x]
re-build: ["7.4.2-54"]
steps:
- name: Checkout code
@ -36,19 +36,18 @@ jobs:
- name: Build cluster
working-directory: redis-ee
env:
IMAGE: "redislabs/redis-internal:${{ matrix.re-build }}"
RE_USERNAME: ${{ secrets.RE_USERNAME }}
RE_PASS: ${{ secrets.RE_PASS }}
RE_CLUSTER_NAME: ${{ secrets.RE_CLUSTER_NAME }}
IMAGE: "redislabs/redis:${{ matrix.re-build }}"
RE_USERNAME: test@test.com
RE_PASS: 12345
RE_CLUSTER_NAME: re-test
RE_USE_OSS_CLUSTER: false
RE_DB_PORT: ${{ secrets.RE_DB_PORT }}
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
RE_DB_PORT: 6379
run: ./build.sh
- name: Test
env:
RE_CLUSTER: "1"
RE_CLUSTER: true
REDIS_VERSION: "7.4"
run: |
go test \
--ginkgo.skip-file="ring_test.go" \

6
.gitignore vendored

@ -3,4 +3,8 @@ testdata/*
.idea/
.DS_Store
*.tar.gz
*.dic
*.dic
redis8tests.sh
coverage.txt
**/coverage.txt
.vscode

@ -1,4 +1,3 @@
run:
concurrency: 8
deadline: 5m
timeout: 5m
tests: false

@ -1,3 +1,12 @@
## Unreleased
### Changed
* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980))
Users can use the OpenTelemetry sampler to control the sampling behavior.
For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep
a similar behavior (drop orphan spans) of `go-redis` as before.
## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29)

@ -32,20 +32,33 @@ Here's how to get started with your code contribution:
1. Create your own fork of go-redis
2. Do the changes in your fork
3. If you need a development environment, run `make test`. Note: this clones and builds the latest release of [redis](https://redis.io). You also need a redis-stack-server docker, in order to run the capabilities tests. This can be started by running:
```docker run -p 6379:6379 -it redis/redis-stack-server:edge```
4. While developing, make sure the tests pass by running `make tests`
3. If you need a development environment, run `make docker.start`.
> Note: this clones and builds the docker containers specified in `docker-compose.yml`, to understand more about
> the infrastructure that will be started you can check the `docker-compose.yml`. You also have the possiblity
> to specify the redis image that will be pulled with the env variable `CLIENT_LIBS_TEST_IMAGE`.
> By default the docker image that will be pulled and started is `redislabs/client-libs-test:rs-7.4.0-v2`.
> If you want to test with newer Redis version, using a newer version of `redislabs/client-libs-test` should work out of the box.
4. While developing, make sure the tests pass by running `make test` (if you have the docker containers running, `make test.ci` may be sufficient).
> Note: `make test` will try to start all containers, run the tests with `make test.ci` and then stop all containers.
5. If you like the change and think the project could use it, send a
pull request
To see what else is part of the automation, run `invoke -l`
## Testing
Call `make test` to run all tests, including linters.
### Setting up Docker
To run the tests, you need to have Docker installed and running. If you are using a host OS that does not support
docker host networks out of the box (e.g. Windows, OSX), you need to set up a docker desktop and enable docker host networks.
### Running tests
Call `make test` to run all tests.
Continuous Integration uses these same wrappers to run all of these
tests against multiple versions of python. Feel free to test your
tests against multiple versions of redis. Feel free to test your
changes against all the go versions supported, as declared by the
[build.yml](./.github/workflows/build.yml) file.

@ -1,36 +1,35 @@
GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
test: testdeps
docker.start:
docker compose --profile all up -d --quiet-pull
docker.stop:
docker compose --profile all down
test:
$(MAKE) docker.start
$(MAKE) test.ci
$(MAKE) docker.stop
test.ci:
set -e; for dir in $(GO_MOD_DIRS); do \
echo "go test in $${dir}"; \
(cd "$${dir}" && \
go mod tidy -compat=1.18 && \
go test && \
go test ./... -short -race && \
go test ./... -run=NONE -bench=. -benchmem && \
env GOOS=linux GOARCH=386 go test && \
go vet); \
go vet && \
go test -coverprofile=coverage.txt -covermode=atomic ./... -race); \
done
cd internal/customvet && go build .
go vet -vettool ./internal/customvet/customvet
testdeps: testdata/redis/src/redis-server
bench: testdeps
bench:
go test ./... -test.run=NONE -test.bench=. -test.benchmem
.PHONY: all test testdeps bench fmt
.PHONY: all test bench fmt
build:
go build .
testdata/redis:
mkdir -p $@
wget -qO- https://download.redis.io/releases/redis-7.2.1.tar.gz | tar xvz --strip-components=1 -C $@
testdata/redis/src/redis-server: testdata/redis
cd $< && make all
fmt:
gofumpt -w ./
goimports -w -local github.com/redis/go-redis ./

@ -3,15 +3,24 @@
[![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc)
[![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.uptrace.dev/)
[![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis)
[![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj)
> go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can
> use it to monitor applications and set up automatic alerts to receive notifications via email,
> Slack, Telegram, and others.
>
> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which
> demonstrates how you can use Uptrace to monitor go-redis.
> go-redis is the official Redis client library for the Go programming language. It offers a straightforward interface for interacting with Redis servers.
## Supported versions
In `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support:
- [Redis 7.2](https://raw.githubusercontent.com/redis/redis/7.2/00-RELEASENOTES) - using Redis Stack 7.2 for modules support
- [Redis 7.4](https://raw.githubusercontent.com/redis/redis/7.4/00-RELEASENOTES) - using Redis Stack 7.4 for modules support
- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 where modules are included
Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three
versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0),
[1.24](https://go.dev/doc/devel/release#go1.24.0)). We observe that some modules related test may not pass with
Redis Stack 7.2 and some commands are changed with Redis CE 8.0.
Please do refer to the documentation and the tests if you experience any issues. We do plan to update the go version
in the `go.mod` to `go 1.24` in one of the next releases.
## How do I Redis?
@ -51,8 +60,8 @@ key value NoSQL database that uses RocksDB as storage engine and is compatible w
## Features
- Redis 3 commands except QUIT, MONITOR, and SYNC.
- Automatic connection pooling with
- Redis commands except QUIT and SYNC.
- Automatic connection pooling.
- [Pub/Sub](https://redis.uptrace.dev/guide/go-redis-pubsub.html).
- [Pipelines and transactions](https://redis.uptrace.dev/guide/go-redis-pipelines.html).
- [Scripting](https://redis.uptrace.dev/guide/lua-scripting.html).
@ -143,9 +152,6 @@ to this specification.
```go
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
@ -161,6 +167,48 @@ func ExampleClient() *redis.Client {
```
### Advanced Configuration
go-redis supports extending the client identification phase to allow projects to send their own custom client identification.
#### Default Client Identification
By default, go-redis automatically sends the client library name and version during the connection process. This feature is available in redis-server as of version 7.2. As a result, the command is "fire and forget", meaning it should fail silently, in the case that the redis server does not support this feature.
#### 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.
To disable verification, set the `DisableIndentity` 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
})
```
#### 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!
@ -243,6 +291,14 @@ REDIS_PORT=9999 go test <your options>
## Contributors
> The go-redis project was originally initiated by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can
> use it to monitor applications and set up automatic alerts to receive notifications via email,
> Slack, Telegram, and others.
>
> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which
> demonstrates how you can use Uptrace to monitor go-redis.
Thanks to all the people who already contributed!
<a href="https://github.com/redis/go-redis/graphs/contributors">

@ -4,8 +4,20 @@ import "context"
type ACLCmdable interface {
ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd
ACLLog(ctx context.Context, count int64) *ACLLogCmd
ACLLogReset(ctx context.Context) *StatusCmd
ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd
ACLDelUser(ctx context.Context, username string) *IntCmd
ACLList(ctx context.Context) *StringSliceCmd
ACLCat(ctx context.Context) *StringSliceCmd
ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd
}
type ACLCatArgs struct {
Category string
}
func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd {
@ -33,3 +45,45 @@ func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd {
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd {
cmd := NewIntCmd(ctx, "acl", "deluser", username)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd {
args := make([]interface{}, 3+len(rules))
args[0] = "acl"
args[1] = "setuser"
args[2] = username
for i, rule := range rules {
args[i+3] = rule
}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "acl", "list")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "acl", "cat")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd {
// if there is a category passed, build new cmd, if there isn't - use the ACLCat method
if options != nil && options.Category != "" {
cmd := NewStringSliceCmd(ctx, "acl", "cat", options.Category)
_ = c(ctx, cmd)
return cmd
}
return c.ACLCat(ctx)
}

449
acl_commands_test.go Normal file

@ -0,0 +1,449 @@
package redis_test
import (
"context"
"github.com/redis/go-redis/v9"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
)
var TestUserName string = "goredis"
var _ = Describe("ACL", func() {
var client *redis.Client
var ctx context.Context
BeforeEach(func() {
ctx = context.Background()
opt := redisOptions()
client = redis.NewClient(opt)
})
It("should ACL LOG", Label("NonRedisEnterprise"), func() {
Expect(client.ACLLogReset(ctx).Err()).NotTo(HaveOccurred())
err := client.Do(ctx, "acl", "setuser", "test", ">test", "on", "allkeys", "+get").Err()
Expect(err).NotTo(HaveOccurred())
clientAcl := redis.NewClient(redisOptions())
clientAcl.Options().Username = "test"
clientAcl.Options().Password = "test"
clientAcl.Options().DB = 0
_ = clientAcl.Set(ctx, "mystring", "foo", 0).Err()
_ = clientAcl.HSet(ctx, "myhash", "foo", "bar").Err()
_ = clientAcl.SAdd(ctx, "myset", "foo", "bar").Err()
logEntries, err := client.ACLLog(ctx, 10).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(logEntries)).To(Equal(4))
for _, entry := range logEntries {
Expect(entry.Reason).To(Equal("command"))
Expect(entry.Context).To(Equal("toplevel"))
Expect(entry.Object).NotTo(BeEmpty())
Expect(entry.Username).To(Equal("test"))
Expect(entry.AgeSeconds).To(BeNumerically(">=", 0))
Expect(entry.ClientInfo).NotTo(BeNil())
Expect(entry.EntryID).To(BeNumerically(">=", 0))
Expect(entry.TimestampCreated).To(BeNumerically(">=", 0))
Expect(entry.TimestampLastUpdated).To(BeNumerically(">=", 0))
}
limitedLogEntries, err := client.ACLLog(ctx, 2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(limitedLogEntries)).To(Equal(2))
// cleanup after creating the user
err = client.Do(ctx, "acl", "deluser", "test").Err()
Expect(err).NotTo(HaveOccurred())
})
It("should ACL LOG RESET", Label("NonRedisEnterprise"), func() {
// Call ACL LOG RESET
resetCmd := client.ACLLogReset(ctx)
Expect(resetCmd.Err()).NotTo(HaveOccurred())
Expect(resetCmd.Val()).To(Equal("OK"))
// Verify that the log is empty after the reset
logEntries, err := client.ACLLog(ctx, 10).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(logEntries)).To(Equal(0))
})
})
var _ = Describe("ACL user commands", Label("NonRedisEnterprise"), func() {
var client *redis.Client
var ctx context.Context
BeforeEach(func() {
ctx = context.Background()
opt := redisOptions()
client = redis.NewClient(opt)
})
AfterEach(func() {
_, err := client.ACLDelUser(context.Background(), TestUserName).Result()
Expect(err).NotTo(HaveOccurred())
Expect(client.Close()).NotTo(HaveOccurred())
})
It("list only default user", func() {
res, err := client.ACLList(ctx).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(HaveLen(1))
Expect(res[0]).To(ContainSubstring("default"))
})
It("setuser and deluser", func() {
res, err := client.ACLList(ctx).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(HaveLen(1))
Expect(res[0]).To(ContainSubstring("default"))
add, err := client.ACLSetUser(ctx, TestUserName, "nopass", "on", "allkeys", "+set", "+get").Result()
Expect(err).NotTo(HaveOccurred())
Expect(add).To(Equal("OK"))
resAfter, err := client.ACLList(ctx).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resAfter).To(HaveLen(2))
Expect(resAfter[1]).To(ContainSubstring(TestUserName))
deletedN, err := client.ACLDelUser(ctx, TestUserName).Result()
Expect(err).NotTo(HaveOccurred())
Expect(deletedN).To(BeNumerically("==", 1))
resAfterDeletion, err := client.ACLList(ctx).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resAfterDeletion).To(HaveLen(1))
Expect(resAfterDeletion[0]).To(BeEquivalentTo(res[0]))
})
It("should acl dryrun", func() {
dryRun := client.ACLDryRun(ctx, "default", "get", "randomKey")
Expect(dryRun.Err()).NotTo(HaveOccurred())
Expect(dryRun.Val()).To(Equal("OK"))
})
})
var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() {
var client *redis.Client
var ctx context.Context
BeforeEach(func() {
ctx = context.Background()
opt := redisOptions()
opt.UnstableResp3 = true
client = redis.NewClient(opt)
})
AfterEach(func() {
_, err := client.ACLDelUser(context.Background(), TestUserName).Result()
Expect(err).NotTo(HaveOccurred())
Expect(client.Close()).NotTo(HaveOccurred())
})
It("reset permissions", func() {
add, err := client.ACLSetUser(ctx,
TestUserName,
"reset",
"nopass",
"on",
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(add).To(Equal("OK"))
connection := client.Conn()
authed, err := connection.AuthACL(ctx, TestUserName, "").Result()
Expect(err).NotTo(HaveOccurred())
Expect(authed).To(Equal("OK"))
_, err = connection.Get(ctx, "anykey").Result()
Expect(err).To(HaveOccurred())
})
It("add write permissions", func() {
add, err := client.ACLSetUser(ctx,
TestUserName,
"reset",
"nopass",
"on",
"~*",
"+SET",
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(add).To(Equal("OK"))
connection := client.Conn()
authed, err := connection.AuthACL(ctx, TestUserName, "").Result()
Expect(err).NotTo(HaveOccurred())
Expect(authed).To(Equal("OK"))
// can write
v, err := connection.Set(ctx, "anykey", "anyvalue", 0).Result()
Expect(err).ToNot(HaveOccurred())
Expect(v).To(Equal("OK"))
// but can't read
value, err := connection.Get(ctx, "anykey").Result()
Expect(err).To(HaveOccurred())
Expect(value).To(BeEmpty())
})
It("add read permissions", func() {
add, err := client.ACLSetUser(ctx,
TestUserName,
"reset",
"nopass",
"on",
"~*",
"+GET",
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(add).To(Equal("OK"))
connection := client.Conn()
authed, err := connection.AuthACL(ctx, TestUserName, "").Result()
Expect(err).NotTo(HaveOccurred())
Expect(authed).To(Equal("OK"))
// can read
value, err := connection.Get(ctx, "anykey").Result()
Expect(err).ToNot(HaveOccurred())
Expect(value).To(Equal("anyvalue"))
// but can't delete
del, err := connection.Del(ctx, "anykey").Result()
Expect(err).To(HaveOccurred())
Expect(del).ToNot(Equal(1))
})
It("add del permissions", func() {
add, err := client.ACLSetUser(ctx,
TestUserName,
"reset",
"nopass",
"on",
"~*",
"+DEL",
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(add).To(Equal("OK"))
connection := client.Conn()
authed, err := connection.AuthACL(ctx, TestUserName, "").Result()
Expect(err).NotTo(HaveOccurred())
Expect(authed).To(Equal("OK"))
// can read
del, err := connection.Del(ctx, "anykey").Result()
Expect(err).ToNot(HaveOccurred())
Expect(del).To(BeEquivalentTo(1))
})
It("set permissions for module commands", func() {
SkipBeforeRedisVersion(8, "permissions for modules are supported for Redis Version >=8")
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &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", "foo bar")
add, err := client.ACLSetUser(ctx,
TestUserName,
"reset",
"nopass",
"on",
"~*",
"+FT.SEARCH",
"-FT.DROPINDEX",
"+json.set",
"+json.get",
"-json.clear",
"+bf.reserve",
"-bf.info",
"+cf.reserve",
"+cms.initbydim",
"+topk.reserve",
"+tdigest.create",
"+ts.create",
"-ts.info",
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(add).To(Equal("OK"))
c := client.Conn()
authed, err := c.AuthACL(ctx, TestUserName, "").Result()
Expect(err).NotTo(HaveOccurred())
Expect(authed).To(Equal("OK"))
// has perm for search
Expect(c.FTSearch(ctx, "txt", "foo ~bar").Err()).NotTo(HaveOccurred())
// no perm for dropindex
err = c.FTDropIndex(ctx, "txt").Err()
Expect(err).ToNot(BeEmpty())
Expect(err.Error()).To(ContainSubstring("NOPERM"))
// json set and get have perm
Expect(c.JSONSet(ctx, "foo", "$", "\"bar\"").Err()).NotTo(HaveOccurred())
Expect(c.JSONGet(ctx, "foo", "$").Val()).To(BeEquivalentTo("[\"bar\"]"))
// no perm for json clear
err = c.JSONClear(ctx, "foo", "$").Err()
Expect(err).ToNot(BeEmpty())
Expect(err.Error()).To(ContainSubstring("NOPERM"))
// perm for reserve
Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred())
// no perm for info
err = c.BFInfo(ctx, "bloom").Err()
Expect(err).ToNot(BeEmpty())
Expect(err.Error()).To(ContainSubstring("NOPERM"))
// perm for cf.reserve
Expect(c.CFReserve(ctx, "cfres", 100).Err()).NotTo(HaveOccurred())
// perm for cms.initbydim
Expect(c.CMSInitByDim(ctx, "cmsdim", 100, 5).Err()).NotTo(HaveOccurred())
// perm for topk.reserve
Expect(c.TopKReserve(ctx, "topk", 10).Err()).NotTo(HaveOccurred())
// perm for tdigest.create
Expect(c.TDigestCreate(ctx, "tdc").Err()).NotTo(HaveOccurred())
// perm for ts.create
Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred())
// noperm for ts.info
err = c.TSInfo(ctx, "tsts").Err()
Expect(err).ToNot(BeEmpty())
Expect(err.Error()).To(ContainSubstring("NOPERM"))
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
})
It("set permissions for module categories", func() {
SkipBeforeRedisVersion(8, "permissions for modules are supported for Redis Version >=8")
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &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", "foo bar")
add, err := client.ACLSetUser(ctx,
TestUserName,
"reset",
"nopass",
"on",
"~*",
"+@search",
"+@json",
"+@bloom",
"+@cuckoo",
"+@topk",
"+@cms",
"+@timeseries",
"+@tdigest",
).Result()
Expect(err).NotTo(HaveOccurred())
Expect(add).To(Equal("OK"))
c := client.Conn()
authed, err := c.AuthACL(ctx, TestUserName, "").Result()
Expect(err).NotTo(HaveOccurred())
Expect(authed).To(Equal("OK"))
// has perm for search
Expect(c.FTSearch(ctx, "txt", "foo ~bar").Err()).NotTo(HaveOccurred())
// perm for dropindex
Expect(c.FTDropIndex(ctx, "txt").Err()).NotTo(HaveOccurred())
// json set and get have perm
Expect(c.JSONSet(ctx, "foo", "$", "\"bar\"").Err()).NotTo(HaveOccurred())
Expect(c.JSONGet(ctx, "foo", "$").Val()).To(BeEquivalentTo("[\"bar\"]"))
// perm for json clear
Expect(c.JSONClear(ctx, "foo", "$").Err()).NotTo(HaveOccurred())
// perm for reserve
Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred())
// perm for info
Expect(c.BFInfo(ctx, "bloom").Err()).NotTo(HaveOccurred())
// perm for cf.reserve
Expect(c.CFReserve(ctx, "cfres", 100).Err()).NotTo(HaveOccurred())
// perm for cms.initbydim
Expect(c.CMSInitByDim(ctx, "cmsdim", 100, 5).Err()).NotTo(HaveOccurred())
// perm for topk.reserve
Expect(c.TopKReserve(ctx, "topk", 10).Err()).NotTo(HaveOccurred())
// perm for tdigest.create
Expect(c.TDigestCreate(ctx, "tdc").Err()).NotTo(HaveOccurred())
// perm for ts.create
Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred())
// perm for ts.info
Expect(c.TSInfo(ctx, "tsts").Err()).NotTo(HaveOccurred())
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
})
})
var _ = Describe("ACL Categories", func() {
var client *redis.Client
var ctx context.Context
BeforeEach(func() {
ctx = context.Background()
opt := redisOptions()
client = redis.NewClient(opt)
})
AfterEach(func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
It("lists acl categories and subcategories", func() {
res, err := client.ACLCat(ctx).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(res)).To(BeNumerically(">", 20))
Expect(res).To(ContainElements(
"read",
"write",
"keyspace",
"dangerous",
"slow",
"set",
"sortedset",
"list",
"hash",
))
res, err = client.ACLCatArgs(ctx, &redis.ACLCatArgs{Category: "read"}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(ContainElement("get"))
})
It("lists acl categories and subcategories with Modules", func() {
SkipBeforeRedisVersion(8, "modules are included in acl for redis version >= 8")
aclTestCase := map[string]string{
"search": "FT.CREATE",
"bloom": "bf.add",
"json": "json.get",
"cuckoo": "cf.insert",
"cms": "cms.query",
"topk": "topk.list",
"tdigest": "tdigest.rank",
"timeseries": "ts.range",
}
var cats []interface{}
for cat, subitem := range aclTestCase {
cats = append(cats, cat)
res, err := client.ACLCatArgs(ctx, &redis.ACLCatArgs{
Category: cat,
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(ContainElement(subitem))
}
res, err := client.ACLCat(ctx).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(ContainElements(cats...))
})
})

@ -277,37 +277,41 @@ func BenchmarkXRead(b *testing.B) {
func newClusterScenario() *clusterScenario {
return &clusterScenario{
ports: []string{"8220", "8221", "8222", "8223", "8224", "8225"},
nodeIDs: make([]string, 6),
processes: make(map[string]*redisProcess, 6),
clients: make(map[string]*redis.Client, 6),
ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"},
nodeIDs: make([]string, 6),
clients: make(map[string]*redis.Client, 6),
}
}
var clusterBench *clusterScenario
func BenchmarkClusterPing(b *testing.B) {
if testing.Short() {
b.Skip("skipping in short mode")
}
ctx := context.Background()
cluster := newClusterScenario()
if err := startCluster(ctx, cluster); err != nil {
b.Fatal(err)
if clusterBench == nil {
clusterBench = newClusterScenario()
if err := configureClusterTopology(ctx, clusterBench); err != nil {
b.Fatal(err)
}
}
defer cluster.Close()
client := cluster.newClusterClient(ctx, redisClusterOptions())
client := clusterBench.newClusterClient(ctx, redisClusterOptions())
defer client.Close()
b.ResetTimer()
b.Run("cluster ping", func(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Ping(ctx).Err()
if err != nil {
b.Fatal(err)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Ping(ctx).Err()
if err != nil {
b.Fatal(err)
}
}
}
})
})
}
@ -317,23 +321,26 @@ func BenchmarkClusterDoInt(b *testing.B) {
}
ctx := context.Background()
cluster := newClusterScenario()
if err := startCluster(ctx, cluster); err != nil {
b.Fatal(err)
if clusterBench == nil {
clusterBench = newClusterScenario()
if err := configureClusterTopology(ctx, clusterBench); err != nil {
b.Fatal(err)
}
}
defer cluster.Close()
client := cluster.newClusterClient(ctx, redisClusterOptions())
client := clusterBench.newClusterClient(ctx, redisClusterOptions())
defer client.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Do(ctx, "SET", 10, 10).Err()
if err != nil {
b.Fatal(err)
b.Run("cluster do set int", func(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Do(ctx, "SET", 10, 10).Err()
if err != nil {
b.Fatal(err)
}
}
}
})
})
}
@ -343,26 +350,29 @@ func BenchmarkClusterSetString(b *testing.B) {
}
ctx := context.Background()
cluster := newClusterScenario()
if err := startCluster(ctx, cluster); err != nil {
b.Fatal(err)
if clusterBench == nil {
clusterBench = newClusterScenario()
if err := configureClusterTopology(ctx, clusterBench); err != nil {
b.Fatal(err)
}
}
defer cluster.Close()
client := cluster.newClusterClient(ctx, redisClusterOptions())
client := clusterBench.newClusterClient(ctx, redisClusterOptions())
defer client.Close()
value := string(bytes.Repeat([]byte{'1'}, 10000))
b.ResetTimer()
b.Run("cluster set string", func(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Set(ctx, "key", value, 0).Err()
if err != nil {
b.Fatal(err)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Set(ctx, "key", value, 0).Err()
if err != nil {
b.Fatal(err)
}
}
}
})
})
}
@ -372,21 +382,6 @@ func BenchmarkExecRingSetAddrsCmd(b *testing.B) {
ringShard2Name = "ringShardTwo"
)
for _, port := range []string{ringShard1Port, ringShard2Port} {
if _, err := startRedis(port); err != nil {
b.Fatal(err)
}
}
b.Cleanup(func() {
for _, p := range processes {
if err := p.Close(); err != nil {
b.Errorf("Failed to stop redis process: %v", err)
}
}
processes = nil
})
ring := redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
"ringShardOne": ":" + ringShard1Port,

@ -2,6 +2,7 @@ package redis
import (
"context"
"errors"
)
type BitMapCmdable interface {
@ -15,6 +16,7 @@ type BitMapCmdable interface {
BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd
BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd
BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd
BitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd
}
func (c cmdable) GetBit(ctx context.Context, key string, offset int64) *IntCmd {
@ -37,16 +39,26 @@ func (c cmdable) SetBit(ctx context.Context, key string, offset int64, value int
type BitCount struct {
Start, End int64
Unit string // BYTE(default) | BIT
}
const BitCountIndexByte string = "BYTE"
const BitCountIndexBit string = "BIT"
func (c cmdable) BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd {
args := []interface{}{"bitcount", key}
args := make([]any, 2, 5)
args[0] = "bitcount"
args[1] = key
if bitCount != nil {
args = append(
args,
bitCount.Start,
bitCount.End,
)
args = append(args, bitCount.Start, bitCount.End)
if bitCount.Unit != "" {
if bitCount.Unit != BitCountIndexByte && bitCount.Unit != BitCountIndexBit {
cmd := NewIntCmd(ctx)
cmd.SetErr(errors.New("redis: invalid bitcount index"))
return cmd
}
args = append(args, bitCount.Unit)
}
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)

98
bitmap_commands_test.go Normal file

@ -0,0 +1,98 @@
package redis_test
import (
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
"github.com/redis/go-redis/v9"
)
type bitCountExpected struct {
Start int64
End int64
Expected int64
}
var _ = Describe("BitCountBite", func() {
var client *redis.Client
key := "bit_count_test"
BeforeEach(func() {
client = redis.NewClient(redisOptions())
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
values := []int{0, 1, 0, 0, 1, 0, 1, 0, 1, 1}
for i, v := range values {
cmd := client.SetBit(ctx, key, int64(i), v)
Expect(cmd.Err()).NotTo(HaveOccurred())
}
})
AfterEach(func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
It("bit count bite", func() {
var expected = []bitCountExpected{
{0, 0, 0},
{0, 1, 1},
{0, 2, 1},
{0, 3, 1},
{0, 4, 2},
{0, 5, 2},
{0, 6, 3},
{0, 7, 3},
{0, 8, 4},
{0, 9, 5},
}
for _, e := range expected {
cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexBit})
Expect(cmd.Err()).NotTo(HaveOccurred())
Expect(cmd.Val()).To(Equal(e.Expected))
}
})
})
var _ = Describe("BitCountByte", func() {
var client *redis.Client
key := "bit_count_test"
BeforeEach(func() {
client = redis.NewClient(redisOptions())
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
values := []int{0, 0, 0, 0, 0, 0, 0, 1, 1, 1}
for i, v := range values {
cmd := client.SetBit(ctx, key, int64(i), v)
Expect(cmd.Err()).NotTo(HaveOccurred())
}
})
AfterEach(func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
It("bit count byte", func() {
var expected = []bitCountExpected{
{0, 0, 1},
{0, 1, 3},
}
for _, e := range expected {
cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexByte})
Expect(cmd.Err()).NotTo(HaveOccurred())
Expect(cmd.Val()).To(Equal(e.Expected))
}
})
It("bit count byte with no unit specified", func() {
var expected = []bitCountExpected{
{0, 0, 1},
{0, 1, 3},
}
for _, e := range expected {
cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End})
Expect(cmd.Err()).NotTo(HaveOccurred())
Expect(cmd.Val()).To(Equal(e.Expected))
}
})
})

@ -4,6 +4,7 @@ import "context"
type ClusterCmdable interface {
ClusterMyShardID(ctx context.Context) *StringCmd
ClusterMyID(ctx context.Context) *StringCmd
ClusterSlots(ctx context.Context) *ClusterSlotsCmd
ClusterShards(ctx context.Context) *ClusterShardsCmd
ClusterLinks(ctx context.Context) *ClusterLinksCmd
@ -35,6 +36,12 @@ func (c cmdable) ClusterMyShardID(ctx context.Context) *StringCmd {
return cmd
}
func (c cmdable) ClusterMyID(ctx context.Context) *StringCmd {
cmd := NewStringCmd(ctx, "cluster", "myid")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {
cmd := NewClusterSlotsCmd(ctx, "cluster", "slots")
_ = c(ctx, cmd)

@ -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 {
@ -573,6 +580,10 @@ func (cmd *StatusCmd) Result() (string, error) {
return cmd.val, cmd.err
}
func (cmd *StatusCmd) Bytes() ([]byte, error) {
return util.StringToBytes(cmd.val), cmd.err
}
func (cmd *StatusCmd) String() string {
return cmdString(cmd, cmd.val)
}
@ -1394,27 +1405,63 @@ func (cmd *MapStringSliceInterfaceCmd) Val() map[string][]interface{} {
}
func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) {
n, err := rd.ReadMapLen()
readType, err := rd.PeekReplyType()
if err != nil {
return err
}
cmd.val = make(map[string][]interface{}, n)
for i := 0; i < n; i++ {
k, err := rd.ReadString()
cmd.val = make(map[string][]interface{})
if readType == proto.RespMap {
n, err := rd.ReadMapLen()
if err != nil {
return err
}
nn, err := rd.ReadArrayLen()
if err != nil {
return err
}
cmd.val[k] = make([]interface{}, nn)
for j := 0; j < nn; j++ {
value, err := rd.ReadReply()
for i := 0; i < n; i++ {
k, err := rd.ReadString()
if err != nil {
return err
}
cmd.val[k][j] = value
nn, err := rd.ReadArrayLen()
if err != nil {
return err
}
cmd.val[k] = make([]interface{}, nn)
for j := 0; j < nn; j++ {
value, err := rd.ReadReply()
if err != nil {
return err
}
cmd.val[k][j] = value
}
}
} else if readType == proto.RespArray {
// RESP2 response
n, err := rd.ReadArrayLen()
if err != nil {
return err
}
for i := 0; i < n; i++ {
// Each entry in this array is itself an array with key details
itemLen, err := rd.ReadArrayLen()
if err != nil {
return err
}
key, err := rd.ReadString()
if err != nil {
return err
}
cmd.val[key] = make([]interface{}, 0, itemLen-1)
for j := 1; j < itemLen; j++ {
// Read the inner array for timestamp-value pairs
data, err := rd.ReadReply()
if err != nil {
return err
}
cmd.val[key] = append(cmd.val[key], data)
}
}
}
@ -3783,6 +3830,83 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
// -----------------------------------------------------------------------
// MapStringInterfaceCmd represents a command that returns a map of strings to interface{}.
type MapMapStringInterfaceCmd struct {
baseCmd
val map[string]interface{}
}
func NewMapMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapMapStringInterfaceCmd {
return &MapMapStringInterfaceCmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
},
}
}
func (cmd *MapMapStringInterfaceCmd) String() string {
return cmdString(cmd, cmd.val)
}
func (cmd *MapMapStringInterfaceCmd) SetVal(val map[string]interface{}) {
cmd.val = val
}
func (cmd *MapMapStringInterfaceCmd) Result() (map[string]interface{}, error) {
return cmd.val, cmd.err
}
func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} {
return cmd.val
}
// readReply will try to parse the reply from the proto.Reader for both resp2 and resp3
func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) {
data, err := rd.ReadReply()
if err != nil {
return err
}
resultMap := map[string]interface{}{}
switch midResponse := data.(type) {
case map[interface{}]interface{}: // resp3 will return map
for k, v := range midResponse {
stringKey, ok := k.(string)
if !ok {
return fmt.Errorf("redis: invalid map key %#v", k)
}
resultMap[stringKey] = v
}
case []interface{}: // resp2 will return array of arrays
n := len(midResponse)
for i := 0; i < n; i++ {
finalArr, ok := midResponse[i].([]interface{}) // final array that we need to transform to map
if !ok {
return fmt.Errorf("redis: unexpected response %#v", data)
}
m := len(finalArr)
if m%2 != 0 { // since this should be map, keys should be even number
return fmt.Errorf("redis: unexpected response %#v", data)
}
for j := 0; j < m; j += 2 {
stringKey, ok := finalArr[j].(string) // the first one
if !ok {
return fmt.Errorf("redis: invalid map key %#v", finalArr[i])
}
resultMap[stringKey] = finalArr[j+1] // second one is value
}
}
default:
return fmt.Errorf("redis: unexpected response %#v", data)
}
cmd.val = resultMap
return nil
}
//-----------------------------------------------------------------------
type MapStringInterfaceSliceCmd struct {
@ -4997,6 +5121,7 @@ type ClientInfo struct {
PSub int // number of pattern matching subscriptions
SSub int // redis version 7.0.3, number of shard channel subscriptions
Multi int // number of commands in a MULTI/EXEC context
Watch int // redis version 7.4 RC1, number of keys this client is currently watching.
QueryBuf int // qbuf, query buffer length (0 means no query pending)
QueryBufFree int // qbuf-free, free space of the query buffer (0 means the buffer is full)
ArgvMem int // incomplete arguments for the next command (already extracted from query buffer)
@ -5007,6 +5132,7 @@ type ClientInfo struct {
OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full)
OutputMemory int // omem, output buffer memory usage
TotalMemory int // tot-mem, total memory consumed by this client in its various buffers
IoThread int // io-thread id
Events string // file descriptor events (see below)
LastCmd string // cmd, last command played
User string // the authenticated username of the client
@ -5149,6 +5275,8 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) {
info.SSub, err = strconv.Atoi(val)
case "multi":
info.Multi, err = strconv.Atoi(val)
case "watch":
info.Watch, err = strconv.Atoi(val)
case "qbuf":
info.QueryBuf, err = strconv.Atoi(val)
case "qbuf-free":
@ -5183,6 +5311,8 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) {
info.LibName = val
case "lib-ver":
info.LibVer = val
case "io-thread":
info.IoThread, err = strconv.Atoi(val)
default:
return nil, fmt.Errorf("redis: unexpected client info key(%s)", key)
}
@ -5310,6 +5440,16 @@ type LibraryInfo struct {
LibVer *string
}
// WithLibraryName returns a valid LibraryInfo with library name only.
func WithLibraryName(libName string) LibraryInfo {
return LibraryInfo{LibName: &libName}
}
// WithLibraryVersion returns a valid LibraryInfo with library version only.
func WithLibraryVersion(libVer string) LibraryInfo {
return LibraryInfo{LibVer: &libVer}
}
// -------------------------------------------
type InfoCmd struct {
@ -5352,8 +5492,6 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error {
section := ""
scanner := bufio.NewScanner(strings.NewReader(val))
moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") {
@ -5364,6 +5502,7 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error {
cmd.val[section] = make(map[string]string)
} else if line != "" {
if section == "Modules" {
moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`)
kv := moduleRe.FindStringSubmatch(line)
if len(kv) == 3 {
cmd.val[section][kv[1]] = kv[2]
@ -5444,9 +5583,12 @@ func (cmd *MonitorCmd) readMonitor(rd *proto.Reader, cancel context.CancelFunc)
for {
cmd.mu.Lock()
st := cmd.status
pk, _ := rd.Peek(1)
cmd.mu.Unlock()
if pk, _ := rd.Peek(1); len(pk) != 0 && st == monitorStatusStart {
if len(pk) != 0 && st == monitorStatusStart {
cmd.mu.Lock()
line, err := rd.ReadString()
cmd.mu.Unlock()
if err != nil {
return err
}

@ -220,6 +220,7 @@ type Cmdable interface {
ProbabilisticCmdable
PubSubCmdable
ScriptingFunctionsCmdable
SearchCmdable
SetCmdable
SortedSetCmdable
StringCmdable
@ -315,7 +316,7 @@ func (c statefulCmdable) ClientSetInfo(ctx context.Context, info LibraryInfo) *S
var cmd *StatusCmd
if info.LibName != nil {
libName := fmt.Sprintf("go-redis(%s,%s)", *info.LibName, runtime.Version())
libName := fmt.Sprintf("go-redis(%s,%s)", *info.LibName, internal.ReplaceSpaces(runtime.Version()))
cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-NAME", libName)
} else {
cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-VER", *info.LibVer)

@ -193,6 +193,41 @@ var _ = Describe("Commands", func() {
Expect(r.Val()).To(Equal(int64(0)))
})
It("should ClientKillByFilter with MAXAGE", Label("NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
var s []string
started := make(chan bool)
done := make(chan bool)
go func() {
defer GinkgoRecover()
started <- true
blpop := client.BLPop(ctx, 0, "list")
Expect(blpop.Val()).To(Equal(s))
done <- true
}()
<-started
select {
case <-done:
Fail("BLPOP is not blocked.")
case <-time.After(1100 * time.Millisecond):
// ok
}
killed := client.ClientKillByFilter(ctx, "MAXAGE", "1")
Expect(killed.Err()).NotTo(HaveOccurred())
Expect(killed.Val()).To(BeNumerically(">=", 1))
select {
case <-done:
// ok
case <-time.After(200 * time.Millisecond):
Fail("BLPOP is still blocked.")
}
})
It("should ClientID", func() {
err := client.ClientID(ctx).Err()
Expect(err).NotTo(HaveOccurred())
@ -254,7 +289,7 @@ var _ = Describe("Commands", func() {
// Test setting the libName
libName := "go-redis"
libInfo := redis.LibraryInfo{LibName: &libName}
libInfo := redis.WithLibraryName(libName)
setInfo := pipe.ClientSetInfo(ctx, libInfo)
_, err := pipe.Exec(ctx)
@ -264,7 +299,7 @@ var _ = Describe("Commands", func() {
// Test setting the libVer
libVer := "vX.x"
libInfo = redis.LibraryInfo{LibVer: &libVer}
libInfo = redis.WithLibraryVersion(libVer)
setInfo = pipe.ClientSetInfo(ctx, libInfo)
_, err = pipe.Exec(ctx)
@ -316,6 +351,23 @@ var _ = Describe("Commands", func() {
Expect(val).NotTo(BeEmpty())
})
It("should ConfigGet Modules", func() {
SkipBeforeRedisVersion(8, "Config doesn't include modules before Redis 8")
expected := map[string]string{
"search-*": "search-timeout",
"ts-*": "ts-retention-policy",
"bf-*": "bf-error-rate",
"cf-*": "cf-initial-size",
}
for prefix, lookup := range expected {
val, err := client.ConfigGet(ctx, prefix).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).NotTo(BeEmpty())
Expect(val[lookup]).NotTo(BeEmpty())
}
})
It("should ConfigResetStat", Label("NonRedisEnterprise"), func() {
r := client.ConfigResetStat(ctx)
Expect(r.Err()).NotTo(HaveOccurred())
@ -334,6 +386,127 @@ var _ = Describe("Commands", func() {
Expect(configSet.Val()).To(Equal("OK"))
})
It("should ConfigGet with Modules", Label("NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(8, "config get won't return modules configs before redis 8")
configGet := client.ConfigGet(ctx, "*")
Expect(configGet.Err()).NotTo(HaveOccurred())
Expect(configGet.Val()).To(HaveKey("maxmemory"))
Expect(configGet.Val()).To(HaveKey("search-timeout"))
Expect(configGet.Val()).To(HaveKey("ts-retention-policy"))
Expect(configGet.Val()).To(HaveKey("bf-error-rate"))
Expect(configGet.Val()).To(HaveKey("cf-initial-size"))
})
It("should ConfigSet FT DIALECT", func() {
SkipBeforeRedisVersion(8, "config doesn't include modules before Redis 8")
defaultState, err := client.ConfigGet(ctx, "search-default-dialect").Result()
Expect(err).NotTo(HaveOccurred())
// set to 3
res, err := client.ConfigSet(ctx, "search-default-dialect", "3").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo("OK"))
defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result()
Expect(err).NotTo(HaveOccurred())
Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "3"}))
resGet, err := client.ConfigGet(ctx, "search-default-dialect").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resGet).To(BeEquivalentTo(map[string]string{"search-default-dialect": "3"}))
// set to 2
res, err = client.ConfigSet(ctx, "search-default-dialect", "2").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo("OK"))
defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result()
Expect(err).NotTo(HaveOccurred())
Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"}))
// set to 1
res, err = client.ConfigSet(ctx, "search-default-dialect", "1").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo("OK"))
defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result()
Expect(err).NotTo(HaveOccurred())
Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"}))
resGet, err = client.ConfigGet(ctx, "search-default-dialect").Result()
Expect(err).NotTo(HaveOccurred())
Expect(resGet).To(BeEquivalentTo(map[string]string{"search-default-dialect": "1"}))
// set to default
res, err = client.ConfigSet(ctx, "search-default-dialect", defaultState["search-default-dialect"]).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo("OK"))
})
It("should ConfigSet fail for ReadOnly", func() {
SkipBeforeRedisVersion(8, "Config doesn't include modules before Redis 8")
_, err := client.ConfigSet(ctx, "search-max-doctablesize", "100000").Result()
Expect(err).To(HaveOccurred())
})
It("should ConfigSet Modules", func() {
SkipBeforeRedisVersion(8, "Config doesn't include modules before Redis 8")
defaults := map[string]string{}
expected := map[string]string{
"search-timeout": "100",
"ts-retention-policy": "2",
"bf-error-rate": "0.13",
"cf-initial-size": "64",
}
// read the defaults to set them back later
for setting, _ := range expected {
val, err := client.ConfigGet(ctx, setting).Result()
Expect(err).NotTo(HaveOccurred())
defaults[setting] = val[setting]
}
// check if new values can be set
for setting, value := range expected {
val, err := client.ConfigSet(ctx, setting, value).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).NotTo(BeEmpty())
Expect(val).To(Equal("OK"))
}
for setting, value := range expected {
val, err := client.ConfigGet(ctx, setting).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).NotTo(BeEmpty())
Expect(val[setting]).To(Equal(value))
}
// set back to the defaults
for setting, value := range defaults {
val, err := client.ConfigSet(ctx, setting, value).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).NotTo(BeEmpty())
Expect(val).To(Equal("OK"))
}
})
It("should Fail ConfigSet Modules", func() {
SkipBeforeRedisVersion(8, "Config doesn't include modules before Redis 8")
expected := map[string]string{
"search-timeout": "-100",
"ts-retention-policy": "-10",
"bf-error-rate": "1.5",
"cf-initial-size": "-10",
}
for setting, value := range expected {
val, err := client.ConfigSet(ctx, setting, value).Result()
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring(setting)))
Expect(val).To(BeEmpty())
}
})
It("should ConfigRewrite", Label("NonRedisEnterprise"), func() {
configRewrite := client.ConfigRewrite(ctx)
Expect(configRewrite.Err()).NotTo(HaveOccurred())
@ -366,6 +539,59 @@ var _ = Describe("Commands", func() {
Expect(info.Val()).To(HaveLen(1))
})
It("should Info Modules", Label("redis.info"), func() {
SkipBeforeRedisVersion(8, "modules are included in info for Redis Version >= 8")
info := client.Info(ctx)
Expect(info.Err()).NotTo(HaveOccurred())
Expect(info.Val()).NotTo(BeNil())
info = client.Info(ctx, "search")
Expect(info.Err()).NotTo(HaveOccurred())
Expect(info.Val()).To(ContainSubstring("search"))
info = client.Info(ctx, "modules")
Expect(info.Err()).NotTo(HaveOccurred())
Expect(info.Val()).To(ContainSubstring("search"))
Expect(info.Val()).To(ContainSubstring("ReJSON"))
Expect(info.Val()).To(ContainSubstring("timeseries"))
Expect(info.Val()).To(ContainSubstring("bf"))
info = client.Info(ctx, "everything")
Expect(info.Err()).NotTo(HaveOccurred())
Expect(info.Val()).To(ContainSubstring("search"))
Expect(info.Val()).To(ContainSubstring("ReJSON"))
Expect(info.Val()).To(ContainSubstring("timeseries"))
Expect(info.Val()).To(ContainSubstring("bf"))
})
It("should InfoMap Modules", Label("redis.info"), func() {
SkipBeforeRedisVersion(8, "modules are included in info for Redis Version >= 8")
info := client.InfoMap(ctx)
Expect(info.Err()).NotTo(HaveOccurred())
Expect(info.Val()).NotTo(BeNil())
info = client.InfoMap(ctx, "search")
Expect(info.Err()).NotTo(HaveOccurred())
Expect(len(info.Val())).To(BeNumerically(">=", 2))
Expect(info.Val()["search_version"]).ToNot(BeNil())
info = client.InfoMap(ctx, "modules")
Expect(info.Err()).NotTo(HaveOccurred())
val := info.Val()
modules, ok := val["Modules"]
Expect(ok).To(BeTrue())
Expect(len(val)).To(BeNumerically(">=", 2))
Expect(val["search_version"]).ToNot(BeNil())
Expect(modules["search"]).ToNot(BeNil())
Expect(modules["ReJSON"]).ToNot(BeNil())
Expect(modules["timeseries"]).ToNot(BeNil())
Expect(modules["bf"]).ToNot(BeNil())
info = client.InfoMap(ctx, "everything")
Expect(info.Err()).NotTo(HaveOccurred())
Expect(len(info.Val())).To(BeNumerically(">=", 10))
})
It("should Info cpu", func() {
info := client.Info(ctx, "cpu")
Expect(info.Err()).NotTo(HaveOccurred())
@ -413,7 +639,6 @@ var _ = Describe("Commands", func() {
It("should Command", Label("NonRedisEnterprise"), func() {
cmds, err := client.Command(ctx).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(cmds)).To(BeNumerically("~", 240, 25))
cmd := cmds["mget"]
Expect(cmd.Name).To(Equal("mget"))
@ -483,8 +708,8 @@ var _ = Describe("Commands", func() {
})
})
Describe("debugging", func() {
PIt("should DebugObject", func() {
Describe("debugging", Label("NonRedisEnterprise"), func() {
It("should DebugObject", func() {
err := client.DebugObject(ctx, "foo").Err()
Expect(err).To(MatchError("ERR no such key"))
@ -682,7 +907,7 @@ var _ = Describe("Commands", func() {
Expect(get.Val()).To(Equal("hello"))
})
It("should Object", func() {
It("should Object", Label("NonRedisEnterprise"), func() {
start := time.Now()
set := client.Set(ctx, "key", "hello", 0)
Expect(set.Err()).NotTo(HaveOccurred())
@ -692,6 +917,11 @@ var _ = Describe("Commands", func() {
Expect(refCount.Err()).NotTo(HaveOccurred())
Expect(refCount.Val()).To(Equal(int64(1)))
client.ConfigSet(ctx, "maxmemory-policy", "volatile-lfu")
freq := client.ObjectFreq(ctx, "key")
Expect(freq.Err()).NotTo(HaveOccurred())
client.ConfigSet(ctx, "maxmemory-policy", "noeviction") // default
err := client.ObjectEncoding(ctx, "key").Err()
Expect(err).NotTo(HaveOccurred())
@ -1101,6 +1331,27 @@ var _ = Describe("Commands", func() {
keys, cursor, err := client.HScan(ctx, "myhash", 0, "", 0).Result()
Expect(err).NotTo(HaveOccurred())
// If we don't get at least two items back, it's really strange.
Expect(cursor).To(BeNumerically(">=", 2))
Expect(len(keys)).To(BeNumerically(">=", 2))
Expect(keys[0]).To(HavePrefix("key"))
Expect(keys[1]).To(Equal("hello"))
})
It("should HScan without values", Label("NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
for i := 0; i < 1000; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
keys, cursor, err := client.HScanNoValues(ctx, "myhash", 0, "", 0).Result()
Expect(err).NotTo(HaveOccurred())
// If we don't get at least two items back, it's really strange.
Expect(cursor).To(BeNumerically(">=", 2))
Expect(len(keys)).To(BeNumerically(">=", 2))
Expect(keys[0]).To(HavePrefix("key"))
Expect(keys[1]).To(HavePrefix("key"))
Expect(keys).NotTo(BeEmpty())
Expect(cursor).NotTo(BeZero())
})
@ -2038,12 +2289,6 @@ var _ = Describe("Commands", func() {
Expect(replace.Val()).To(Equal(int64(1)))
})
It("should acl dryrun", func() {
dryRun := client.ACLDryRun(ctx, "default", "get", "randomKey")
Expect(dryRun.Err()).NotTo(HaveOccurred())
Expect(dryRun.Val()).To(Equal("OK"))
})
It("should fail module loadex", Label("NonRedisEnterprise"), func() {
dryRun := client.ModuleLoadex(ctx, &redis.ModuleLoadexConfig{
Path: "/path/to/non-existent-library.so",
@ -2091,51 +2336,6 @@ var _ = Describe("Commands", func() {
Expect(args).To(Equal(expectedArgs))
})
It("should ACL LOG", Label("NonRedisEnterprise"), func() {
err := client.Do(ctx, "acl", "setuser", "test", ">test", "on", "allkeys", "+get").Err()
Expect(err).NotTo(HaveOccurred())
clientAcl := redis.NewClient(redisOptions())
clientAcl.Options().Username = "test"
clientAcl.Options().Password = "test"
clientAcl.Options().DB = 0
_ = clientAcl.Set(ctx, "mystring", "foo", 0).Err()
_ = clientAcl.HSet(ctx, "myhash", "foo", "bar").Err()
_ = clientAcl.SAdd(ctx, "myset", "foo", "bar").Err()
logEntries, err := client.ACLLog(ctx, 10).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(logEntries)).To(Equal(4))
for _, entry := range logEntries {
Expect(entry.Reason).To(Equal("command"))
Expect(entry.Context).To(Equal("toplevel"))
Expect(entry.Object).NotTo(BeEmpty())
Expect(entry.Username).To(Equal("test"))
Expect(entry.AgeSeconds).To(BeNumerically(">=", 0))
Expect(entry.ClientInfo).NotTo(BeNil())
Expect(entry.EntryID).To(BeNumerically(">=", 0))
Expect(entry.TimestampCreated).To(BeNumerically(">=", 0))
Expect(entry.TimestampLastUpdated).To(BeNumerically(">=", 0))
}
limitedLogEntries, err := client.ACLLog(ctx, 2).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(limitedLogEntries)).To(Equal(2))
})
It("should ACL LOG RESET", Label("NonRedisEnterprise"), func() {
// Call ACL LOG RESET
resetCmd := client.ACLLogReset(ctx)
Expect(resetCmd.Err()).NotTo(HaveOccurred())
Expect(resetCmd.Val()).To(Equal("OK"))
// Verify that the log is empty after the reset
logEntries, err := client.ACLLog(ctx, 10).Result()
Expect(err).NotTo(HaveOccurred())
Expect(len(logEntries)).To(Equal(0))
})
})
Describe("hashes", func() {
@ -2431,6 +2631,193 @@ var _ = Describe("Commands", func() {
Equal([]redis.KeyValue{{Key: "key2", Value: "hello2"}}),
))
})
It("should HStrLen", func() {
hSet := client.HSet(ctx, "hash", "key", "hello")
Expect(hSet.Err()).NotTo(HaveOccurred())
hStrLen := client.HStrLen(ctx, "hash", "key")
Expect(hStrLen.Err()).NotTo(HaveOccurred())
Expect(hStrLen.Val()).To(Equal(int64(len("hello"))))
nonHStrLen := client.HStrLen(ctx, "hash", "keyNon")
Expect(hStrLen.Err()).NotTo(HaveOccurred())
Expect(nonHStrLen.Val()).To(Equal(int64(0)))
hDel := client.HDel(ctx, "hash", "key")
Expect(hDel.Err()).NotTo(HaveOccurred())
Expect(hDel.Val()).To(Equal(int64(1)))
})
It("should HExpire", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
res, err := client.HExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(res).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
res, err = client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, 1, -2}))
})
It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
res, err := client.HPExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(res).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
res, err = client.HPExpire(ctx, "myhash", 10*time.Second, "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, 1, -2}))
})
It("should HExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
resEmpty, err := client.HExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
res, err := client.HExpireAt(ctx, "myhash", time.Now().Add(10*time.Second), "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, 1, -2}))
})
It("should HPExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
resEmpty, err := client.HPExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
res, err := client.HPExpireAt(ctx, "myhash", time.Now().Add(10*time.Second), "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, 1, -2}))
})
It("should HPersist", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
resEmpty, err := client.HPersist(ctx, "no_such_key", "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
res, err := client.HPersist(ctx, "myhash", "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{-1, -1, -2}))
res, err = client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, -2}))
res, err = client.HPersist(ctx, "myhash", "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, -1, -2}))
})
It("should HExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
resEmpty, err := client.HExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
res, err := client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, -2}))
res, err = client.HExpireTime(ctx, "myhash", "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res[0]).To(BeNumerically("~", time.Now().Add(10*time.Second).Unix(), 1))
})
It("should HPExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
resEmpty, err := client.HPExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
expireAt := time.Now().Add(10 * time.Second)
res, err := client.HPExpireAt(ctx, "myhash", expireAt, "key1", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, -2}))
res, err = client.HPExpireTime(ctx, "myhash", "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(BeEquivalentTo([]int64{expireAt.UnixMilli(), -1, -2}))
})
It("should HTTL", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
resEmpty, err := client.HTTL(ctx, "no_such_key", "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
res, err := client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, -2}))
res, err = client.HTTL(ctx, "myhash", "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{10, -1, -2}))
})
It("should HPTTL", Label("hash-expiration", "NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
resEmpty, err := client.HPTTL(ctx, "no_such_key", "field1", "field2", "field3").Result()
Expect(err).To(BeNil())
Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))
for i := 0; i < 100; i++ {
sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello")
Expect(sadd.Err()).NotTo(HaveOccurred())
}
res, err := client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]int64{1, -2}))
res, err = client.HPTTL(ctx, "myhash", "key1", "key2", "key200").Result()
Expect(err).NotTo(HaveOccurred())
Expect(res[0]).To(BeNumerically("~", 10*time.Second.Milliseconds(), 1))
})
})
Describe("hyperloglog", func() {
@ -5687,6 +6074,81 @@ var _ = Describe("Commands", func() {
Expect(err).To(Equal(redis.Nil))
})
It("should XRead LastEntry", Label("NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
res, err := client.XRead(ctx, &redis.XReadArgs{
Streams: []string{"stream"},
Count: 2, // we expect 1 message
ID: "+",
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]redis.XStream{
{
Stream: "stream",
Messages: []redis.XMessage{
{ID: "3-0", Values: map[string]interface{}{"tres": "troix"}},
},
},
}))
})
It("should XRead LastEntry from two streams", Label("NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
res, err := client.XRead(ctx, &redis.XReadArgs{
Streams: []string{"stream", "stream"},
ID: "+",
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal([]redis.XStream{
{
Stream: "stream",
Messages: []redis.XMessage{
{ID: "3-0", Values: map[string]interface{}{"tres": "troix"}},
},
},
{
Stream: "stream",
Messages: []redis.XMessage{
{ID: "3-0", Values: map[string]interface{}{"tres": "troix"}},
},
},
}))
})
It("should XRead LastEntry blocks", Label("NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
start := time.Now()
go func() {
defer GinkgoRecover()
time.Sleep(100 * time.Millisecond)
id, err := client.XAdd(ctx, &redis.XAddArgs{
Stream: "empty",
ID: "4-0",
Values: map[string]interface{}{"quatro": "quatre"},
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(id).To(Equal("4-0"))
}()
res, err := client.XRead(ctx, &redis.XReadArgs{
Streams: []string{"empty"},
Block: 500 * time.Millisecond,
ID: "+",
}).Result()
Expect(err).NotTo(HaveOccurred())
// Ensure that the XRead call with LastEntry option blocked for at least 100ms.
Expect(time.Since(start)).To(BeNumerically(">=", 100*time.Millisecond))
Expect(res).To(Equal([]redis.XStream{
{
Stream: "empty",
Messages: []redis.XMessage{
{ID: "4-0", Values: map[string]interface{}{"quatro": "quatre"}},
},
},
}))
})
Describe("group", func() {
BeforeEach(func() {
err := client.XGroupCreate(ctx, "stream", "group", "0").Err()
@ -6190,14 +6652,12 @@ var _ = Describe("Commands", func() {
res, err := client.ZRangeWithScores(ctx, "result", 0, -1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(res).To(ContainElement(redis.Z{
Score: 190.44242984775784,
Member: "Palermo",
}))
Expect(res).To(ContainElement(redis.Z{
Score: 56.4412578701582,
Member: "Catania",
}))
Expect(len(res)).To(Equal(2))
var palermo, catania redis.Z
Expect(res).To(ContainElement(HaveField("Member", "Palermo"), &palermo))
Expect(res).To(ContainElement(HaveField("Member", "Catania"), &catania))
Expect(palermo.Score).To(BeNumerically("~", 190, 1))
Expect(catania.Score).To(BeNumerically("~", 56, 1))
})
It("should search geo radius with options", func() {
@ -6509,16 +6969,13 @@ var _ = Describe("Commands", func() {
v, err := client.ZRangeWithScores(ctx, "key2", 0, -1).Result()
Expect(err).NotTo(HaveOccurred())
Expect(v).To(Equal([]redis.Z{
{
Score: 56.441257870158204,
Member: "Catania",
},
{
Score: 190.44242984775784,
Member: "Palermo",
},
}))
Expect(len(v)).To(Equal(2))
var palermo, catania redis.Z
Expect(v).To(ContainElement(HaveField("Member", "Palermo"), &palermo))
Expect(v).To(ContainElement(HaveField("Member", "Catania"), &catania))
Expect(palermo.Score).To(BeNumerically("~", 190, 1))
Expect(catania.Score).To(BeNumerically("~", 56, 1))
})
})
@ -6908,6 +7365,7 @@ var _ = Describe("Commands", func() {
})
It("Shows function stats", func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
defer client.FunctionKill(ctx)
// We can not run blocking commands in Redis functions, so we're using an infinite loop,

101
docker-compose.yml Normal file

@ -0,0 +1,101 @@
---
services:
redis:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2}
container_name: redis-standalone
environment:
- TLS_ENABLED=yes
- REDIS_CLUSTER=no
- PORT=6379
- TLS_PORT=6666
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
ports:
- 6379:6379
- 6666:6666 # TLS port
volumes:
- "./dockers/standalone:/redis/work"
profiles:
- standalone
- sentinel
- all-stack
- all
osscluster:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2}
container_name: redis-osscluster
environment:
- NODES=6
- PORT=16600
command: "--cluster-enabled yes"
ports:
- "16600-16605:16600-16605"
volumes:
- "./dockers/osscluster:/redis/work"
profiles:
- cluster
- all-stack
- all
sentinel-cluster:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2}
container_name: redis-sentinel-cluster
network_mode: "host"
environment:
- NODES=3
- TLS_ENABLED=yes
- REDIS_CLUSTER=no
- PORT=9121
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
#ports:
# - "9121-9123:9121-9123"
volumes:
- "./dockers/sentinel-cluster:/redis/work"
profiles:
- sentinel
- all-stack
- all
sentinel:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2}
container_name: redis-sentinel
depends_on:
- sentinel-cluster
environment:
- NODES=3
- REDIS_CLUSTER=no
- PORT=26379
command: ${REDIS_EXTRA_ARGS:---sentinel}
network_mode: "host"
#ports:
# - 26379:26379
# - 26380:26380
# - 26381:26381
volumes:
- "./dockers/sentinel.conf:/redis/config-default/redis.conf"
- "./dockers/sentinel:/redis/work"
profiles:
- sentinel
- all-stack
- all
ring-cluster:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2}
container_name: redis-ring-cluster
environment:
- NODES=3
- TLS_ENABLED=yes
- REDIS_CLUSTER=no
- PORT=6390
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
ports:
- 6390:6390
- 6391:6391
- 6392:6392
volumes:
- "./dockers/ring:/redis/work"
profiles:
- ring
- cluster
- all-stack
- all

6
dockers/.gitignore vendored Normal file

@ -0,0 +1,6 @@
osscluster/
ring/
standalone/
sentinel-cluster/
sentinel/

5
dockers/sentinel.conf Normal file

@ -0,0 +1,5 @@
sentinel resolve-hostnames yes
sentinel monitor go-redis-test 127.0.0.1 9121 2
sentinel down-after-milliseconds go-redis-test 5000
sentinel failover-timeout go-redis-test 60000
sentinel parallel-syncs go-redis-test 1

@ -0,0 +1,85 @@
// 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
// make sure we are working with fresh database
rdb.FlushDB(ctx)
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]
}

@ -0,0 +1,81 @@
// EXAMPLE: bitfield_tutorial
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_bf() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "bike:1:stats")
// REMOVE_END
// STEP_START bf
res1, err := rdb.BitField(ctx, "bike:1:stats",
"set", "u32", "#0", "1000",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res1) // >>> [0]
res2, err := rdb.BitField(ctx,
"bike:1:stats",
"incrby", "u32", "#0", "-50",
"incrby", "u32", "#1", "1",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res2) // >>> [950 1]
res3, err := rdb.BitField(ctx,
"bike:1:stats",
"incrby", "u32", "#0", "500",
"incrby", "u32", "#1", "1",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res3) // >>> [1450 2]
res4, err := rdb.BitField(ctx, "bike:1:stats",
"get", "u32", "#0",
"get", "u32", "#1",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res4) // >>> [1450 2]
// STEP_END
// Output:
// [0]
// [950 1]
// [1450 2]
// [1450 2]
}

@ -0,0 +1,96 @@
// 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
// make sure we are working with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
_, 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
}

@ -0,0 +1,200 @@
// EXAMPLE: cmds_generic
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"math"
"time"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_del_cmd() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "key1", "key2", "key3")
// REMOVE_END
// STEP_START del
delResult1, err := rdb.Set(ctx, "key1", "Hello", 0).Result()
if err != nil {
panic(err)
}
fmt.Println(delResult1) // >>> OK
delResult2, err := rdb.Set(ctx, "key2", "World", 0).Result()
if err != nil {
panic(err)
}
fmt.Println(delResult2) // >>> OK
delResult3, err := rdb.Del(ctx, "key1", "key2", "key3").Result()
if err != nil {
panic(err)
}
fmt.Println(delResult3) // >>> 2
// STEP_END
// Output:
// OK
// OK
// 2
}
func ExampleClient_expire_cmd() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "mykey")
// REMOVE_END
// STEP_START expire
expireResult1, err := rdb.Set(ctx, "mykey", "Hello", 0).Result()
if err != nil {
panic(err)
}
fmt.Println(expireResult1) // >>> OK
expireResult2, err := rdb.Expire(ctx, "mykey", 10*time.Second).Result()
if err != nil {
panic(err)
}
fmt.Println(expireResult2) // >>> true
expireResult3, err := rdb.TTL(ctx, "mykey").Result()
if err != nil {
panic(err)
}
fmt.Println(math.Round(expireResult3.Seconds())) // >>> 10
expireResult4, err := rdb.Set(ctx, "mykey", "Hello World", 0).Result()
if err != nil {
panic(err)
}
fmt.Println(expireResult4) // >>> OK
expireResult5, err := rdb.TTL(ctx, "mykey").Result()
if err != nil {
panic(err)
}
fmt.Println(expireResult5) // >>> -1ns
expireResult6, err := rdb.ExpireXX(ctx, "mykey", 10*time.Second).Result()
if err != nil {
panic(err)
}
fmt.Println(expireResult6) // >>> false
expireResult7, err := rdb.TTL(ctx, "mykey").Result()
if err != nil {
panic(err)
}
fmt.Println(expireResult7) // >>> -1ns
expireResult8, err := rdb.ExpireNX(ctx, "mykey", 10*time.Second).Result()
if err != nil {
panic(err)
}
fmt.Println(expireResult8) // >>> true
expireResult9, err := rdb.TTL(ctx, "mykey").Result()
if err != nil {
panic(err)
}
fmt.Println(math.Round(expireResult9.Seconds())) // >>> 10
// STEP_END
// Output:
// OK
// true
// 10
// OK
// -1ns
// false
// -1ns
// true
// 10
}
func ExampleClient_ttl_cmd() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "mykey")
// REMOVE_END
// STEP_START ttl
ttlResult1, err := rdb.Set(ctx, "mykey", "Hello", 10*time.Second).Result()
if err != nil {
panic(err)
}
fmt.Println(ttlResult1) // >>> OK
ttlResult2, err := rdb.TTL(ctx, "mykey").Result()
if err != nil {
panic(err)
}
fmt.Println(math.Round(ttlResult2.Seconds())) // >>> 10
// STEP_END
// Output:
// OK
// 10
}

249
doctests/cmds_hash_test.go Normal file

@ -0,0 +1,249 @@
// EXAMPLE: cmds_hash
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"sort"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_hset() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "myhash")
// REMOVE_END
// STEP_START hset
res1, err := rdb.HSet(ctx, "myhash", "field1", "Hello").Result()
if err != nil {
panic(err)
}
fmt.Println(res1) // >>> 1
res2, err := rdb.HGet(ctx, "myhash", "field1").Result()
if err != nil {
panic(err)
}
fmt.Println(res2) // >>> Hello
res3, err := rdb.HSet(ctx, "myhash",
"field2", "Hi",
"field3", "World",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res3) // >>> 2
res4, err := rdb.HGet(ctx, "myhash", "field2").Result()
if err != nil {
panic(err)
}
fmt.Println(res4) // >>> Hi
res5, err := rdb.HGet(ctx, "myhash", "field3").Result()
if err != nil {
panic(err)
}
fmt.Println(res5) // >>> World
res6, err := rdb.HGetAll(ctx, "myhash").Result()
if err != nil {
panic(err)
}
keys := make([]string, 0, len(res6))
for key, _ := range res6 {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("Key: %v, value: %v\n", key, res6[key])
}
// >>> Key: field1, value: Hello
// >>> Key: field2, value: Hi
// >>> Key: field3, value: World
// STEP_END
// Output:
// 1
// Hello
// 2
// Hi
// World
// Key: field1, value: Hello
// Key: field2, value: Hi
// Key: field3, value: World
}
func ExampleClient_hget() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "myhash")
// REMOVE_END
// STEP_START hget
res7, err := rdb.HSet(ctx, "myhash", "field1", "foo").Result()
if err != nil {
panic(err)
}
fmt.Println(res7) // >>> 1
res8, err := rdb.HGet(ctx, "myhash", "field1").Result()
if err != nil {
panic(err)
}
fmt.Println(res8) // >>> foo
res9, err := rdb.HGet(ctx, "myhash", "field2").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res9) // >>> <empty string>
// STEP_END
// Output:
// 1
// foo
// redis: nil
}
func ExampleClient_hgetall() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "myhash")
// REMOVE_END
// STEP_START hgetall
hGetAllResult1, err := rdb.HSet(ctx, "myhash",
"field1", "Hello",
"field2", "World",
).Result()
if err != nil {
panic(err)
}
fmt.Println(hGetAllResult1) // >>> 2
hGetAllResult2, err := rdb.HGetAll(ctx, "myhash").Result()
if err != nil {
panic(err)
}
keys := make([]string, 0, len(hGetAllResult2))
for key, _ := range hGetAllResult2 {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("Key: %v, value: %v\n", key, hGetAllResult2[key])
}
// >>> Key: field1, value: Hello
// >>> Key: field2, value: World
// STEP_END
// Output:
// 2
// Key: field1, value: Hello
// Key: field2, value: World
}
func ExampleClient_hvals() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "myhash")
// REMOVE_END
// STEP_START hvals
hValsResult1, err := rdb.HSet(ctx, "myhash",
"field1", "Hello",
"field2", "World",
).Result()
if err != nil {
panic(err)
}
fmt.Println(hValsResult1) // >>> 2
hValsResult2, err := rdb.HVals(ctx, "myhash").Result()
if err != nil {
panic(err)
}
sort.Strings(hValsResult2)
fmt.Println(hValsResult2) // >>> [Hello World]
// STEP_END
// Output:
// 2
// [Hello World]
}

@ -0,0 +1,76 @@
// EXAMPLE: cmds_servermgmt
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_cmd_flushall() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// STEP_START flushall
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Set(ctx, "testkey1", "1", 0)
rdb.Set(ctx, "testkey2", "2", 0)
rdb.Set(ctx, "testkey3", "3", 0)
// REMOVE_END
flushAllResult1, err := rdb.FlushAll(ctx).Result()
if err != nil {
panic(err)
}
fmt.Println(flushAllResult1) // >>> OK
flushAllResult2, err := rdb.Keys(ctx, "*").Result()
if err != nil {
panic(err)
}
fmt.Println(flushAllResult2) // >>> []
// STEP_END
// Output:
// OK
// []
}
func ExampleClient_cmd_info() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// STEP_START info
infoResult, err := rdb.Info(ctx).Result()
if err != nil {
panic(err)
}
// Check the first 8 characters (the full info string contains
// much more text than this).
fmt.Println(infoResult[:8]) // >>> # Server
// STEP_END
// Output:
// # Server
}

@ -0,0 +1,228 @@
// EXAMPLE: cmds_sorted_set
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_zadd_cmd() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "myzset")
// REMOVE_END
// STEP_START zadd
zAddResult1, err := rdb.ZAdd(ctx, "myzset",
redis.Z{Member: "one", Score: 1},
).Result()
if err != nil {
panic(err)
}
fmt.Println(zAddResult1) // >>> 1
zAddResult2, err := rdb.ZAdd(ctx, "myzset",
redis.Z{Member: "uno", Score: 1},
).Result()
if err != nil {
panic(err)
}
fmt.Println(zAddResult2)
zAddResult3, err := rdb.ZAdd(ctx, "myzset",
redis.Z{Member: "two", Score: 2},
redis.Z{Member: "three", Score: 3},
).Result()
if err != nil {
panic(err)
}
fmt.Println(zAddResult3) // >>> 2
zAddResult4, err := rdb.ZRangeWithScores(ctx, "myzset", 0, -1).Result()
if err != nil {
panic(err)
}
fmt.Println(zAddResult4) // >>> [{1 one} {1 uno} {2 two} {3 three}]
// STEP_END
// Output:
// 1
// 1
// 2
// [{1 one} {1 uno} {2 two} {3 three}]
}
func ExampleClient_zrange1() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "myzset")
// REMOVE_END
// STEP_START zrange1
zrangeResult1, err := rdb.ZAdd(ctx, "myzset",
redis.Z{Member: "one", Score: 1},
redis.Z{Member: "two", Score: 2},
redis.Z{Member: "three", Score: 3},
).Result()
if err != nil {
panic(err)
}
fmt.Println(zrangeResult1) // >>> 3
zrangeResult2, err := rdb.ZRange(ctx, "myzset", 0, -1).Result()
if err != nil {
panic(err)
}
fmt.Println(zrangeResult2) // >>> [one two three]
zrangeResult3, err := rdb.ZRange(ctx, "myzset", 2, 3).Result()
if err != nil {
panic(err)
}
fmt.Println(zrangeResult3) // >>> [three]
zrangeResult4, err := rdb.ZRange(ctx, "myzset", -2, -1).Result()
if err != nil {
panic(err)
}
fmt.Println(zrangeResult4) // >>> [two three]
// STEP_END
// Output:
// 3
// [one two three]
// [three]
// [two three]
}
func ExampleClient_zrange2() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "myzset")
// REMOVE_END
// STEP_START zrange2
zRangeResult5, err := rdb.ZAdd(ctx, "myzset",
redis.Z{Member: "one", Score: 1},
redis.Z{Member: "two", Score: 2},
redis.Z{Member: "three", Score: 3},
).Result()
if err != nil {
panic(err)
}
fmt.Println(zRangeResult5) // >>> 3
zRangeResult6, err := rdb.ZRangeWithScores(ctx, "myzset", 0, 1).Result()
if err != nil {
panic(err)
}
fmt.Println(zRangeResult6) // >>> [{1 one} {2 two}]
// STEP_END
// Output:
// 3
// [{1 one} {2 two}]
}
func ExampleClient_zrange3() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "myzset")
// REMOVE_END
// STEP_START zrange3
zRangeResult7, err := rdb.ZAdd(ctx, "myzset",
redis.Z{Member: "one", Score: 1},
redis.Z{Member: "two", Score: 2},
redis.Z{Member: "three", Score: 3},
).Result()
if err != nil {
panic(err)
}
fmt.Println(zRangeResult7) // >>> 3
zRangeResult8, err := rdb.ZRangeArgs(ctx,
redis.ZRangeArgs{
Key: "myzset",
ByScore: true,
Start: "(1",
Stop: "+inf",
Offset: 1,
Count: 1,
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(zRangeResult8) // >>> [three]
// STEP_END
// Output:
// 3
// [three]
}

@ -0,0 +1,59 @@
// EXAMPLE: cmds_string
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_cmd_incr() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "mykey")
// REMOVE_END
// STEP_START incr
incrResult1, err := rdb.Set(ctx, "mykey", "10", 0).Result()
if err != nil {
panic(err)
}
fmt.Println(incrResult1) // >>> OK
incrResult2, err := rdb.Incr(ctx, "mykey").Result()
if err != nil {
panic(err)
}
fmt.Println(incrResult2) // >>> 11
incrResult3, err := rdb.Get(ctx, "mykey").Result()
if err != nil {
panic(err)
}
fmt.Println(incrResult3) // >>> 11
// STEP_END
// Output:
// OK
// 11
// 11
}

@ -0,0 +1,86 @@
// EXAMPLE: cms_tutorial
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_cms() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "bikes:profit")
// REMOVE_END
// STEP_START cms
res1, err := rdb.CMSInitByProb(ctx, "bikes:profit", 0.001, 0.002).Result()
if err != nil {
panic(err)
}
fmt.Println(res1) // >>> OK
res2, err := rdb.CMSIncrBy(ctx, "bikes:profit",
"Smoky Mountain Striker", 100,
).Result()
if err != nil {
panic(err)
}
fmt.Println(res2) // >>> [100]
res3, err := rdb.CMSIncrBy(ctx, "bikes:profit",
"Rocky Mountain Racer", 200,
"Cloudy City Cruiser", 150,
).Result()
if err != nil {
panic(err)
}
fmt.Println(res3) // >>> [200 150]
res4, err := rdb.CMSQuery(ctx, "bikes:profit",
"Smoky Mountain Striker",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res4) // >>> [100]
res5, err := rdb.CMSInfo(ctx, "bikes:profit").Result()
if err != nil {
panic(err)
}
fmt.Printf("Width: %v, Depth: %v, Count: %v",
res5.Width, res5.Depth, res5.Count)
// >>> Width: 2000, Depth: 9, Count: 450
// STEP_END
// Output:
// OK
// [100]
// [200 150]
// [100]
// Width: 2000, Depth: 9, Count: 450
}

@ -0,0 +1,77 @@
// EXAMPLE: cuckoo_tutorial
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_cuckoo() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "bikes:models")
// REMOVE_END
// STEP_START cuckoo
res1, err := rdb.CFReserve(ctx, "bikes:models", 1000000).Result()
if err != nil {
panic(err)
}
fmt.Println(res1) // >>> OK
res2, err := rdb.CFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result()
if err != nil {
panic(err)
}
fmt.Println(res2) // >>> true
res3, err := rdb.CFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result()
if err != nil {
panic(err)
}
fmt.Println(res3) // >>> true
res4, err := rdb.CFExists(ctx, "bikes:models", "Terrible Bike Name").Result()
if err != nil {
panic(err)
}
fmt.Println(res4) // >>> false
res5, err := rdb.CFDel(ctx, "bikes:models", "Smoky Mountain Striker").Result()
if err != nil {
panic(err)
}
fmt.Println(res5) // >>> true
// STEP_END
// Output:
// OK
// true
// true
// false
// true
}

209
doctests/geo_index_test.go Normal file

@ -0,0 +1,209 @@
// EXAMPLE: geoindex
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_geoindex() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
Protocol: 2,
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.FTDropIndex(ctx, "productidx")
rdb.FTDropIndex(ctx, "geomidx")
rdb.Del(ctx, "product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4")
// REMOVE_END
// STEP_START create_geo_idx
geoCreateResult, err := rdb.FTCreate(ctx,
"productidx",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"product:"},
},
&redis.FieldSchema{
FieldName: "$.location",
As: "location",
FieldType: redis.SearchFieldTypeGeo,
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(geoCreateResult) // >>> OK
// STEP_END
// STEP_START add_geo_json
prd46885 := map[string]interface{}{
"description": "Navy Blue Slippers",
"price": 45.99,
"city": "Denver",
"location": "-104.991531, 39.742043",
}
gjResult1, err := rdb.JSONSet(ctx, "product:46885", "$", prd46885).Result()
if err != nil {
panic(err)
}
fmt.Println(gjResult1) // >>> OK
prd46886 := map[string]interface{}{
"description": "Bright Green Socks",
"price": 25.50,
"city": "Fort Collins",
"location": "-105.0618814,40.5150098",
}
gjResult2, err := rdb.JSONSet(ctx, "product:46886", "$", prd46886).Result()
if err != nil {
panic(err)
}
fmt.Println(gjResult2) // >>> OK
// STEP_END
// STEP_START geo_query
geoQueryResult, err := rdb.FTSearch(ctx, "productidx",
"@location:[-104.800644 38.846127 100 mi]",
).Result()
if err != nil {
panic(err)
}
fmt.Println(geoQueryResult)
// >>> {1 [{product:46885...
// STEP_END
// STEP_START create_gshape_idx
geomCreateResult, err := rdb.FTCreate(ctx, "geomidx",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"shape:"},
},
&redis.FieldSchema{
FieldName: "$.name",
As: "name",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.geom",
As: "geom",
FieldType: redis.SearchFieldTypeGeoShape,
GeoShapeFieldType: "FLAT",
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(geomCreateResult) // >>> OK
// STEP_END
// STEP_START add_gshape_json
shape1 := map[string]interface{}{
"name": "Green Square",
"geom": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))",
}
gmjResult1, err := rdb.JSONSet(ctx, "shape:1", "$", shape1).Result()
if err != nil {
panic(err)
}
fmt.Println(gmjResult1) // >>> OK
shape2 := map[string]interface{}{
"name": "Red Rectangle",
"geom": "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))",
}
gmjResult2, err := rdb.JSONSet(ctx, "shape:2", "$", shape2).Result()
if err != nil {
panic(err)
}
fmt.Println(gmjResult2) // >>> OK
shape3 := map[string]interface{}{
"name": "Blue Triangle",
"geom": "POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))",
}
gmjResult3, err := rdb.JSONSet(ctx, "shape:3", "$", shape3).Result()
if err != nil {
panic(err)
}
fmt.Println(gmjResult3) // >>> OK
shape4 := map[string]interface{}{
"name": "Purple Point",
"geom": "POINT (2 2)",
}
gmjResult4, err := rdb.JSONSet(ctx, "shape:4", "$", shape4).Result()
if err != nil {
panic(err)
}
fmt.Println(gmjResult4) // >>> OK
// STEP_END
// STEP_START gshape_query
geomQueryResult, err := rdb.FTSearchWithArgs(ctx, "geomidx",
"(-@name:(Green Square) @geom:[WITHIN $qshape])",
&redis.FTSearchOptions{
Params: map[string]interface{}{
"qshape": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))",
},
DialectVersion: 4,
Limit: 1,
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(geomQueryResult)
// >>> {1 [{shape:4...
// STEP_END
// Output:
// OK
// OK
// OK
// {1 [{product:46885 <nil> <nil> <nil> map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}]}]}
// OK
// OK
// OK
// OK
// OK
// {1 [{shape:4 <nil> <nil> <nil> map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]]}]}
}

@ -0,0 +1,143 @@
// EXAMPLE: geo_tutorial
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_geoadd() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "bikes:rentable")
// REMOVE_END
// STEP_START geoadd
res1, err := rdb.GeoAdd(ctx, "bikes:rentable",
&redis.GeoLocation{
Longitude: -122.27652,
Latitude: 37.805186,
Name: "station:1",
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res1) // >>> 1
res2, err := rdb.GeoAdd(ctx, "bikes:rentable",
&redis.GeoLocation{
Longitude: -122.2674626,
Latitude: 37.8062344,
Name: "station:2",
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res2) // >>> 1
res3, err := rdb.GeoAdd(ctx, "bikes:rentable",
&redis.GeoLocation{
Longitude: -122.2469854,
Latitude: 37.8104049,
Name: "station:3",
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res3) // >>> 1
// STEP_END
// Output:
// 1
// 1
// 1
}
func ExampleClient_geosearch() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "bikes:rentable")
_, err := rdb.GeoAdd(ctx, "bikes:rentable",
&redis.GeoLocation{
Longitude: -122.27652,
Latitude: 37.805186,
Name: "station:1",
}).Result()
if err != nil {
panic(err)
}
_, err = rdb.GeoAdd(ctx, "bikes:rentable",
&redis.GeoLocation{
Longitude: -122.2674626,
Latitude: 37.8062344,
Name: "station:2",
}).Result()
if err != nil {
panic(err)
}
_, err = rdb.GeoAdd(ctx, "bikes:rentable",
&redis.GeoLocation{
Longitude: -122.2469854,
Latitude: 37.8104049,
Name: "station:3",
}).Result()
if err != nil {
panic(err)
}
// REMOVE_END
// STEP_START geosearch
res4, err := rdb.GeoSearch(ctx, "bikes:rentable",
&redis.GeoSearchQuery{
Longitude: -122.27652,
Latitude: 37.805186,
Radius: 5,
RadiusUnit: "km",
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res4) // >>> [station:1 station:2 station:3]
// STEP_END
// Output:
// [station:1 station:2 station:3]
}

@ -0,0 +1,289 @@
// 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
// make sure we are working with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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]
}

@ -0,0 +1,77 @@
// EXAMPLE: hll_tutorial
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_pfadd() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "bikes", "commuter_bikes", "all_bikes")
// REMOVE_END
// STEP_START pfadd
res1, err := rdb.PFAdd(ctx, "bikes", "Hyperion", "Deimos", "Phoebe", "Quaoar").Result()
if err != nil {
panic(err)
}
fmt.Println(res1) // 1
res2, err := rdb.PFCount(ctx, "bikes").Result()
if err != nil {
panic(err)
}
fmt.Println(res2) // 4
res3, err := rdb.PFAdd(ctx, "commuter_bikes", "Salacia", "Mimas", "Quaoar").Result()
if err != nil {
panic(err)
}
fmt.Println(res3) // 1
res4, err := rdb.PFMerge(ctx, "all_bikes", "bikes", "commuter_bikes").Result()
if err != nil {
panic(err)
}
fmt.Println(res4) // OK
res5, err := rdb.PFCount(ctx, "all_bikes").Result()
if err != nil {
panic(err)
}
fmt.Println(res5) // 6
// STEP_END
// Output:
// 1
// 4
// 1
// OK
// 6
}

@ -0,0 +1,201 @@
// EXAMPLE: go_home_json
// HIDE_START
package example_commands_test
// HIDE_END
// STEP_START import
import (
"context"
"fmt"
"sort"
"github.com/redis/go-redis/v9"
)
// STEP_END
func ExampleClient_search_json() {
// STEP_START connect
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
Protocol: 2,
})
// STEP_END
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "user:1", "user:2", "user:3")
rdb.FTDropIndex(ctx, "idx:users")
// REMOVE_END
// STEP_START create_data
user1 := map[string]interface{}{
"name": "Paul John",
"email": "paul.john@example.com",
"age": 42,
"city": "London",
}
user2 := map[string]interface{}{
"name": "Eden Zamir",
"email": "eden.zamir@example.com",
"age": 29,
"city": "Tel Aviv",
}
user3 := map[string]interface{}{
"name": "Paul Zamir",
"email": "paul.zamir@example.com",
"age": 35,
"city": "Tel Aviv",
}
// STEP_END
// STEP_START make_index
_, err := rdb.FTCreate(
ctx,
"idx:users",
// Options:
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"user:"},
},
// Index schema fields:
&redis.FieldSchema{
FieldName: "$.name",
As: "name",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.city",
As: "city",
FieldType: redis.SearchFieldTypeTag,
},
&redis.FieldSchema{
FieldName: "$.age",
As: "age",
FieldType: redis.SearchFieldTypeNumeric,
},
).Result()
if err != nil {
panic(err)
}
// STEP_END
// STEP_START add_data
_, err = rdb.JSONSet(ctx, "user:1", "$", user1).Result()
if err != nil {
panic(err)
}
_, err = rdb.JSONSet(ctx, "user:2", "$", user2).Result()
if err != nil {
panic(err)
}
_, err = rdb.JSONSet(ctx, "user:3", "$", user3).Result()
if err != nil {
panic(err)
}
// STEP_END
// STEP_START query1
findPaulResult, err := rdb.FTSearch(
ctx,
"idx:users",
"Paul @age:[30 40]",
).Result()
if err != nil {
panic(err)
}
fmt.Println(findPaulResult)
// >>> {1 [{user:3 <nil> <nil> <nil> map[$:{"age":35,"city":"Tel Aviv"...
// STEP_END
// STEP_START query2
citiesResult, err := rdb.FTSearchWithArgs(
ctx,
"idx:users",
"Paul",
&redis.FTSearchOptions{
Return: []redis.FTSearchReturn{
{
FieldName: "$.city",
As: "city",
},
},
},
).Result()
if err != nil {
panic(err)
}
sort.Slice(citiesResult.Docs, func(i, j int) bool {
return citiesResult.Docs[i].Fields["city"] < citiesResult.Docs[j].Fields["city"]
})
for _, result := range citiesResult.Docs {
fmt.Println(result.Fields["city"])
}
// >>> London
// >>> Tel Aviv
// STEP_END
// STEP_START query3
aggOptions := redis.FTAggregateOptions{
GroupBy: []redis.FTAggregateGroupBy{
{
Fields: []interface{}{"@city"},
Reduce: []redis.FTAggregateReducer{
{
Reducer: redis.SearchCount,
As: "count",
},
},
},
},
}
aggResult, err := rdb.FTAggregateWithArgs(
ctx,
"idx:users",
"*",
&aggOptions,
).Result()
if err != nil {
panic(err)
}
sort.Slice(aggResult.Rows, func(i, j int) bool {
return aggResult.Rows[i].Fields["city"].(string) <
aggResult.Rows[j].Fields["city"].(string)
})
for _, row := range aggResult.Rows {
fmt.Printf("%v - %v\n",
row.Fields["city"], row.Fields["count"],
)
}
// >>> City: London - 1
// >>> City: Tel Aviv - 2
// STEP_END
// Output:
// {1 [{user:3 <nil> <nil> <nil> map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}]}]}
// London
// Tel Aviv
// London - 1
// Tel Aviv - 2
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,788 @@
// 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
// make sure we are working with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "bikes:repairs")
// REMOVE_END
// STEP_START ltrim
res27, err := rdb.RPush(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:1 bike:2 bike:3]
// STEP_END
// Output:
// 5
// OK
// [bike:1 bike:2 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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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]
}

@ -0,0 +1,182 @@
// EXAMPLE: pipe_trans_tutorial
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_transactions() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// make sure we are working with fresh database
rdb.FlushDB(ctx)
for i := 0; i < 5; i++ {
rdb.Del(ctx, fmt.Sprintf("seat:%d", i))
}
rdb.Del(ctx, "counter:1", "counter:2", "counter:3", "shellpath")
// REMOVE_END
// STEP_START basic_pipe
pipe := rdb.Pipeline()
for i := 0; i < 5; i++ {
pipe.Set(ctx, fmt.Sprintf("seat:%v", i), fmt.Sprintf("#%v", i), 0)
}
cmds, err := pipe.Exec(ctx)
if err != nil {
panic(err)
}
for _, c := range cmds {
fmt.Printf("%v;", c.(*redis.StatusCmd).Val())
}
fmt.Println("")
// >>> OK;OK;OK;OK;OK;
pipe = rdb.Pipeline()
get0Result := pipe.Get(ctx, "seat:0")
get3Result := pipe.Get(ctx, "seat:3")
get4Result := pipe.Get(ctx, "seat:4")
cmds, err = pipe.Exec(ctx)
// The results are available only after the pipeline
// has finished executing.
fmt.Println(get0Result.Val()) // >>> #0
fmt.Println(get3Result.Val()) // >>> #3
fmt.Println(get4Result.Val()) // >>> #4
// STEP_END
// STEP_START basic_pipe_pipelined
var pd0Result *redis.StatusCmd
var pd3Result *redis.StatusCmd
var pd4Result *redis.StatusCmd
cmds, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
pd0Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:0"))
pd3Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:3"))
pd4Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:4"))
return nil
})
if err != nil {
panic(err)
}
// The results are available only after the pipeline
// has finished executing.
fmt.Println(pd0Result.Val()) // >>> #0
fmt.Println(pd3Result.Val()) // >>> #3
fmt.Println(pd4Result.Val()) // >>> #4
// STEP_END
// STEP_START basic_trans
trans := rdb.TxPipeline()
trans.IncrBy(ctx, "counter:1", 1)
trans.IncrBy(ctx, "counter:2", 2)
trans.IncrBy(ctx, "counter:3", 3)
cmds, err = trans.Exec(ctx)
for _, c := range cmds {
fmt.Println(c.(*redis.IntCmd).Val())
}
// >>> 1
// >>> 2
// >>> 3
// STEP_END
// STEP_START basic_trans_txpipelined
var tx1Result *redis.IntCmd
var tx2Result *redis.IntCmd
var tx3Result *redis.IntCmd
cmds, err = rdb.TxPipelined(ctx, func(trans redis.Pipeliner) error {
tx1Result = trans.IncrBy(ctx, "counter:1", 1)
tx2Result = trans.IncrBy(ctx, "counter:2", 2)
tx3Result = trans.IncrBy(ctx, "counter:3", 3)
return nil
})
if err != nil {
panic(err)
}
fmt.Println(tx1Result.Val()) // >>> 2
fmt.Println(tx2Result.Val()) // >>> 4
fmt.Println(tx3Result.Val()) // >>> 6
// STEP_END
// STEP_START trans_watch
// Set initial value of `shellpath`.
rdb.Set(ctx, "shellpath", "/usr/syscmds/", 0)
const maxRetries = 1000
// Retry if the key has been changed.
for i := 0; i < maxRetries; i++ {
err := rdb.Watch(ctx,
func(tx *redis.Tx) error {
currentPath, err := rdb.Get(ctx, "shellpath").Result()
newPath := currentPath + ":/usr/mycmds/"
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Set(ctx, "shellpath", newPath, 0)
return nil
})
return err
},
"shellpath",
)
if err == nil {
// Success.
break
} else if err == redis.TxFailedErr {
// Optimistic lock lost. Retry the transaction.
continue
} else {
// Panic for any other error.
panic(err)
}
}
fmt.Println(rdb.Get(ctx, "shellpath").Val())
// >>> /usr/syscmds/:/usr/mycmds/
// STEP_END
// Output:
// OK;OK;OK;OK;OK;
// #0
// #3
// #4
// #0
// #3
// #4
// 1
// 2
// 3
// 2
// 4
// 6
// /usr/syscmds/:/usr/mycmds/
}

435
doctests/query_agg_test.go Normal file

@ -0,0 +1,435 @@
// EXAMPLE: query_agg
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"sort"
"github.com/redis/go-redis/v9"
)
func ExampleClient_query_agg() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
Protocol: 2,
})
// HIDE_END
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.FTDropIndex(ctx, "idx:bicycle")
rdb.FTDropIndex(ctx, "idx:email")
// REMOVE_END
_, err := rdb.FTCreate(ctx, "idx:bicycle",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"bicycle:"},
},
&redis.FieldSchema{
FieldName: "$.brand",
As: "brand",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.model",
As: "model",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.description",
As: "description",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.price",
As: "price",
FieldType: redis.SearchFieldTypeNumeric,
},
&redis.FieldSchema{
FieldName: "$.condition",
As: "condition",
FieldType: redis.SearchFieldTypeTag,
},
).Result()
if err != nil {
panic(err)
}
exampleJsons := []map[string]interface{}{
{
"pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " +
"-74.0610 40.6678, -74.0610 40.7578))",
"store_location": "-74.0060,40.7128",
"brand": "Velorim",
"model": "Jigger",
"price": 270,
"description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " +
"This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " +
"is the vehicle of choice for the rare tenacious little rider raring to go.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " +
"-118.2887 33.9872, -118.2887 34.0972))",
"store_location": "-118.2437,34.0522",
"brand": "Bicyk",
"model": "Hillcraft",
"price": 1200,
"description": "Kids want to ride with as little weight as possible. Especially " +
"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " +
"off a 24'' bike. The Hillcraft 26 is just the solution they need!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " +
"-87.6848 41.8231, -87.6848 41.9331))",
"store_location": "-87.6298,41.8781",
"brand": "Nord",
"model": "Chook air 5",
"price": 815,
"description": "The Chook Air 5 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. The lower top tube makes it easy to mount and dismount in any " +
"situation, giving your kids greater safety on the trails.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " +
"-80.2433 25.6967, -80.2433 25.8067))",
"store_location": "-80.1918,25.7617",
"brand": "Eva",
"model": "Eva 291",
"price": 3400,
"description": "The sister company to Nord, Eva launched in 2005 as the first " +
"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " +
"are optimized for the feminine physique using analytics from a body metrics database. " +
"If you like 29ers, try the Eva 291. Its a brand new bike for 2022.. This " +
"full-suspension, cross-country ride has been designed for velocity. The 291 has " +
"100mm of front and rear travel, a superlight aluminum frame and fast-rolling " +
"29-inch wheels. Yippee!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " +
"-122.4644 37.7099, -122.4644 37.8199))",
"store_location": "-122.4194,37.7749",
"brand": "Noka Bikes",
"model": "Kahuna",
"price": 3200,
"description": "Whether you want to try your hand at XC racing or are looking " +
"for a lively trail bike that's just as inspiring on the climbs as it is over rougher " +
"ground, the Wilder is one heck of a bike built specifically for short women. Both the " +
"frames and components have been tweaked to include a womens saddle, different bars " +
"and unique colourway.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " +
"-0.1778 51.4024, -0.1778 51.5524))",
"store_location": "-0.1278,51.5074",
"brand": "Breakout",
"model": "XBN 2.1 Alloy",
"price": 810,
"description": "The XBN 2.1 Alloy is our entry-level road bike but thats " +
"not to say that its a basic machine. With an internal weld aluminium frame, a full " +
"carbon fork, and the slick-shifting Claris gears from Shimanos, this is a bike which " +
"doesnt break the bank and delivers craved performance.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " +
"2.1767 48.5516, 2.1767 48.9016))",
"store_location": "2.3522,48.8566",
"brand": "ScramBikes",
"model": "WattBike",
"price": 2300,
"description": "The WattBike is the best e-bike for people who still " +
"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " +
"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " +
"charge. Its great for tackling hilly terrain or if you just fancy a more " +
"leisurely ride. With three working modes, you can choose between E-bike, " +
"assisted bicycle, and normal bike modes.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " +
"13.3260 52.2700, 13.3260 52.5700))",
"store_location": "13.4050,52.5200",
"brand": "Peaknetic",
"model": "Secto",
"price": 430,
"description": "If you struggle with stiff fingers or a kinked neck or " +
"back after a few minutes on the road, this lightweight, aluminum bike alleviates " +
"those issues and allows you to enjoy the ride. From the ergonomic grips to the " +
"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " +
"The rear-inclined seat tube facilitates stability by allowing you to put a foot " +
"on the ground to balance at a stop, and the low step-over frame makes it " +
"accessible for all ability and mobility levels. The saddle is very soft, with " +
"a wide back to support your hip joints and a cutout in the center to redistribute " +
"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " +
"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " +
"facilitate setting up the Roll Low-Entry as your preferred commuter, and the " +
"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " +
"1.9450 41.1987, 1.9450 41.4301))",
"store_location": "2.1734, 41.3851",
"brand": "nHill",
"model": "Summit",
"price": 1200,
"description": "This budget mountain bike from nHill performs well both " +
"on bike paths and on the trail. The fork with 100mm of travel absorbs rough " +
"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " +
"The Shimano Tourney drivetrain offered enough gears for finding a comfortable " +
"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " +
"Whether you want an affordable bike that you can take to work, but also take " +
"trail in mountains on the weekends or youre just after a stable, comfortable " +
"ride for the bike path, the Summit gives a good value for money.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " +
"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))",
"store_location": "12.4964,41.9028",
"model": "ThrillCycle",
"brand": "BikeShind",
"price": 815,
"description": "An artsy, retro-inspired bicycle thats as " +
"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " +
"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldnt " +
"suggest taking it to the mountains. Fenders protect you from mud, and a rear " +
"basket lets you transport groceries, flowers and books. The ThrillCycle comes " +
"with a limited lifetime warranty, so this little guy will last you long " +
"past graduation.",
"condition": "refurbished",
},
}
for i, json := range exampleJsons {
_, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result()
if err != nil {
panic(err)
}
}
// STEP_START agg1
res1, err := rdb.FTAggregateWithArgs(ctx,
"idx:bicycle",
"@condition:{new}",
&redis.FTAggregateOptions{
Apply: []redis.FTAggregateApply{
{
Field: "@price - (@price * 0.1)",
As: "discounted",
},
},
Load: []redis.FTAggregateLoad{
{Field: "__key"},
{Field: "price"},
},
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(len(res1.Rows)) // >>> 5
sort.Slice(res1.Rows, func(i, j int) bool {
return res1.Rows[i].Fields["__key"].(string) <
res1.Rows[j].Fields["__key"].(string)
})
for _, row := range res1.Rows {
fmt.Printf(
"__key=%v, discounted=%v, price=%v\n",
row.Fields["__key"],
row.Fields["discounted"],
row.Fields["price"],
)
}
// >>> __key=bicycle:0, discounted=243, price=270
// >>> __key=bicycle:5, discounted=729, price=810
// >>> __key=bicycle:6, discounted=2070, price=2300
// >>> __key=bicycle:7, discounted=387, price=430
// >>> __key=bicycle:8, discounted=1080, price=1200
// STEP_END
// STEP_START agg2
res2, err := rdb.FTAggregateWithArgs(ctx,
"idx:bicycle", "*",
&redis.FTAggregateOptions{
Load: []redis.FTAggregateLoad{
{Field: "price"},
},
Apply: []redis.FTAggregateApply{
{
Field: "@price<1000",
As: "price_category",
},
},
GroupBy: []redis.FTAggregateGroupBy{
{
Fields: []interface{}{"@condition"},
Reduce: []redis.FTAggregateReducer{
{
Reducer: redis.SearchSum,
Args: []interface{}{"@price_category"},
As: "num_affordable",
},
},
},
},
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(len(res2.Rows)) // >>> 3
sort.Slice(res2.Rows, func(i, j int) bool {
return res2.Rows[i].Fields["condition"].(string) <
res2.Rows[j].Fields["condition"].(string)
})
for _, row := range res2.Rows {
fmt.Printf(
"condition=%v, num_affordable=%v\n",
row.Fields["condition"],
row.Fields["num_affordable"],
)
}
// >>> condition=new, num_affordable=3
// >>> condition=refurbished, num_affordable=1
// >>> condition=used, num_affordable=1
// STEP_END
// STEP_START agg3
res3, err := rdb.FTAggregateWithArgs(ctx,
"idx:bicycle", "*",
&redis.FTAggregateOptions{
Apply: []redis.FTAggregateApply{
{
Field: "'bicycle'",
As: "type",
},
},
GroupBy: []redis.FTAggregateGroupBy{
{
Fields: []interface{}{"@type"},
Reduce: []redis.FTAggregateReducer{
{
Reducer: redis.SearchCount,
As: "num_total",
},
},
},
},
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(len(res3.Rows)) // >>> 1
for _, row := range res3.Rows {
fmt.Printf(
"type=%v, num_total=%v\n",
row.Fields["type"],
row.Fields["num_total"],
)
}
// type=bicycle, num_total=10
// STEP_END
// STEP_START agg4
res4, err := rdb.FTAggregateWithArgs(ctx,
"idx:bicycle", "*",
&redis.FTAggregateOptions{
Load: []redis.FTAggregateLoad{
{Field: "__key"},
},
GroupBy: []redis.FTAggregateGroupBy{
{
Fields: []interface{}{"@condition"},
Reduce: []redis.FTAggregateReducer{
{
Reducer: redis.SearchToList,
Args: []interface{}{"__key"},
As: "bicycles",
},
},
},
},
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(len(res4.Rows)) // >>> 3
sort.Slice(res4.Rows, func(i, j int) bool {
return res4.Rows[i].Fields["condition"].(string) <
res4.Rows[j].Fields["condition"].(string)
})
for _, row := range res4.Rows {
rowBikes := row.Fields["bicycles"].([]interface{})
bikes := make([]string, len(rowBikes))
for i, rowBike := range rowBikes {
bikes[i] = rowBike.(string)
}
sort.Slice(bikes, func(i, j int) bool {
return bikes[i] < bikes[j]
})
fmt.Printf(
"condition=%v, bicycles=%v\n",
row.Fields["condition"],
bikes,
)
}
// >>> condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8]
// >>> condition=refurbished, bicycles=[bicycle:9]
// >>> condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4]
// STEP_END
// Output:
// 5
// __key=bicycle:0, discounted=243, price=270
// __key=bicycle:5, discounted=729, price=810
// __key=bicycle:6, discounted=2070, price=2300
// __key=bicycle:7, discounted=387, price=430
// __key=bicycle:8, discounted=1080, price=1200
// 3
// condition=new, num_affordable=3
// condition=refurbished, num_affordable=1
// condition=used, num_affordable=1
// 1
// type=bicycle, num_total=10
// 3
// condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8]
// condition=refurbished, bicycles=[bicycle:9]
// condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4]
}

372
doctests/query_em_test.go Normal file

@ -0,0 +1,372 @@
// EXAMPLE: query_em
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"slices"
"strings"
"github.com/redis/go-redis/v9"
)
func ExampleClient_query_em() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
Protocol: 2,
})
// HIDE_END
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.FTDropIndex(ctx, "idx:bicycle")
rdb.FTDropIndex(ctx, "idx:email")
// REMOVE_END
_, err := rdb.FTCreate(ctx, "idx:bicycle",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"bicycle:"},
},
&redis.FieldSchema{
FieldName: "$.brand",
As: "brand",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.model",
As: "model",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.description",
As: "description",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.price",
As: "price",
FieldType: redis.SearchFieldTypeNumeric,
},
&redis.FieldSchema{
FieldName: "$.condition",
As: "condition",
FieldType: redis.SearchFieldTypeTag,
},
).Result()
if err != nil {
panic(err)
}
exampleJsons := []map[string]interface{}{
{
"pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " +
"-74.0610 40.6678, -74.0610 40.7578))",
"store_location": "-74.0060,40.7128",
"brand": "Velorim",
"model": "Jigger",
"price": 270,
"description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " +
"This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " +
"is the vehicle of choice for the rare tenacious little rider raring to go.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " +
"-118.2887 33.9872, -118.2887 34.0972))",
"store_location": "-118.2437,34.0522",
"brand": "Bicyk",
"model": "Hillcraft",
"price": 1200,
"description": "Kids want to ride with as little weight as possible. Especially " +
"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " +
"off a 24'' bike. The Hillcraft 26 is just the solution they need!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " +
"-87.6848 41.8231, -87.6848 41.9331))",
"store_location": "-87.6298,41.8781",
"brand": "Nord",
"model": "Chook air 5",
"price": 815,
"description": "The Chook Air 5 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. The lower top tube makes it easy to mount and dismount in any " +
"situation, giving your kids greater safety on the trails.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " +
"-80.2433 25.6967, -80.2433 25.8067))",
"store_location": "-80.1918,25.7617",
"brand": "Eva",
"model": "Eva 291",
"price": 3400,
"description": "The sister company to Nord, Eva launched in 2005 as the first " +
"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " +
"are optimized for the feminine physique using analytics from a body metrics database. " +
"If you like 29ers, try the Eva 291. Its a brand new bike for 2022.. This " +
"full-suspension, cross-country ride has been designed for velocity. The 291 has " +
"100mm of front and rear travel, a superlight aluminum frame and fast-rolling " +
"29-inch wheels. Yippee!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " +
"-122.4644 37.7099, -122.4644 37.8199))",
"store_location": "-122.4194,37.7749",
"brand": "Noka Bikes",
"model": "Kahuna",
"price": 3200,
"description": "Whether you want to try your hand at XC racing or are looking " +
"for a lively trail bike that's just as inspiring on the climbs as it is over rougher " +
"ground, the Wilder is one heck of a bike built specifically for short women. Both the " +
"frames and components have been tweaked to include a womens saddle, different bars " +
"and unique colourway.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " +
"-0.1778 51.4024, -0.1778 51.5524))",
"store_location": "-0.1278,51.5074",
"brand": "Breakout",
"model": "XBN 2.1 Alloy",
"price": 810,
"description": "The XBN 2.1 Alloy is our entry-level road bike but thats " +
"not to say that its a basic machine. With an internal weld aluminium frame, a full " +
"carbon fork, and the slick-shifting Claris gears from Shimanos, this is a bike which " +
"doesnt break the bank and delivers craved performance.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " +
"2.1767 48.5516, 2.1767 48.9016))",
"store_location": "2.3522,48.8566",
"brand": "ScramBikes",
"model": "WattBike",
"price": 2300,
"description": "The WattBike is the best e-bike for people who still " +
"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " +
"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " +
"charge. Its great for tackling hilly terrain or if you just fancy a more " +
"leisurely ride. With three working modes, you can choose between E-bike, " +
"assisted bicycle, and normal bike modes.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " +
"13.3260 52.2700, 13.3260 52.5700))",
"store_location": "13.4050,52.5200",
"brand": "Peaknetic",
"model": "Secto",
"price": 430,
"description": "If you struggle with stiff fingers or a kinked neck or " +
"back after a few minutes on the road, this lightweight, aluminum bike alleviates " +
"those issues and allows you to enjoy the ride. From the ergonomic grips to the " +
"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " +
"The rear-inclined seat tube facilitates stability by allowing you to put a foot " +
"on the ground to balance at a stop, and the low step-over frame makes it " +
"accessible for all ability and mobility levels. The saddle is very soft, with " +
"a wide back to support your hip joints and a cutout in the center to redistribute " +
"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " +
"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " +
"facilitate setting up the Roll Low-Entry as your preferred commuter, and the " +
"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " +
"1.9450 41.1987, 1.9450 41.4301))",
"store_location": "2.1734, 41.3851",
"brand": "nHill",
"model": "Summit",
"price": 1200,
"description": "This budget mountain bike from nHill performs well both " +
"on bike paths and on the trail. The fork with 100mm of travel absorbs rough " +
"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " +
"The Shimano Tourney drivetrain offered enough gears for finding a comfortable " +
"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " +
"Whether you want an affordable bike that you can take to work, but also take " +
"trail in mountains on the weekends or youre just after a stable, comfortable " +
"ride for the bike path, the Summit gives a good value for money.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " +
"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))",
"store_location": "12.4964,41.9028",
"model": "ThrillCycle",
"brand": "BikeShind",
"price": 815,
"description": "An artsy, retro-inspired bicycle thats as " +
"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " +
"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldnt " +
"suggest taking it to the mountains. Fenders protect you from mud, and a rear " +
"basket lets you transport groceries, flowers and books. The ThrillCycle comes " +
"with a limited lifetime warranty, so this little guy will last you long " +
"past graduation.",
"condition": "refurbished",
},
}
for i, json := range exampleJsons {
_, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result()
if err != nil {
panic(err)
}
}
// STEP_START em1
res1, err := rdb.FTSearch(ctx,
"idx:bicycle", "@price:[270 270]",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res1.Total) // >>> 1
for _, doc := range res1.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:0
res2, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle",
"*",
&redis.FTSearchOptions{
Filters: []redis.FTSearchFilter{
{
FieldName: "price",
Min: 270,
Max: 270,
},
},
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res2.Total) // >>> 1
for _, doc := range res2.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:0
// STEP_END
// STEP_START em2
res3, err := rdb.FTSearch(ctx,
"idx:bicycle", "@condition:{new}",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res3.Total) // >>> 5
docs := res3.Docs
slices.SortFunc(docs, func(a, b redis.Document) int {
return strings.Compare(a.ID, b.ID)
})
for _, doc := range docs {
fmt.Println(doc.ID)
}
// >>> bicycle:0
// >>> bicycle:5
// >>> bicycle:6
// >>> bicycle:7
// >>> bicycle:8
// STEP_END
// STEP_START em3
res4, err := rdb.FTCreate(ctx,
"idx:email",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"key:"},
},
&redis.FieldSchema{
FieldName: "$.email",
As: "email",
FieldType: redis.SearchFieldTypeTag,
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res4) // >>> OK
res5, err := rdb.JSONSet(ctx, "key:1", "$",
map[string]interface{}{
"email": "test@redis.com",
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res5) // >>> OK
res6, err := rdb.FTSearch(ctx, "idx:email",
"@email:{test\\@redis\\.com}",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res6.Total) // >>> 1
// STEP_END
// STEP_START em4
res7, err := rdb.FTSearch(ctx,
"idx:bicycle", "@description:\"rough terrain\"",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res7.Total) // >>> 1
for _, doc := range res7.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:8
// STEP_END
// Output:
// 1
// bicycle:0
// 1
// bicycle:0
// 5
// bicycle:0
// bicycle:5
// bicycle:6
// bicycle:7
// bicycle:8
// OK
// OK
// 1
// 1
// bicycle:8
}

333
doctests/query_ft_test.go Normal file

@ -0,0 +1,333 @@
// EXAMPLE: query_ft
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"sort"
"github.com/redis/go-redis/v9"
)
func ExampleClient_query_ft() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
Protocol: 2,
})
// HIDE_END
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.FTDropIndex(ctx, "idx:bicycle")
rdb.FTDropIndex(ctx, "idx:email")
// REMOVE_END
_, err := rdb.FTCreate(ctx, "idx:bicycle",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"bicycle:"},
},
&redis.FieldSchema{
FieldName: "$.brand",
As: "brand",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.model",
As: "model",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.description",
As: "description",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.price",
As: "price",
FieldType: redis.SearchFieldTypeNumeric,
},
&redis.FieldSchema{
FieldName: "$.condition",
As: "condition",
FieldType: redis.SearchFieldTypeTag,
},
).Result()
if err != nil {
panic(err)
}
exampleJsons := []map[string]interface{}{
{
"pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " +
"-74.0610 40.6678, -74.0610 40.7578))",
"store_location": "-74.0060,40.7128",
"brand": "Velorim",
"model": "Jigger",
"price": 270,
"description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " +
"This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " +
"is the vehicle of choice for the rare tenacious little rider raring to go.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " +
"-118.2887 33.9872, -118.2887 34.0972))",
"store_location": "-118.2437,34.0522",
"brand": "Bicyk",
"model": "Hillcraft",
"price": 1200,
"description": "Kids want to ride with as little weight as possible. Especially " +
"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " +
"off a 24'' bike. The Hillcraft 26 is just the solution they need!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " +
"-87.6848 41.8231, -87.6848 41.9331))",
"store_location": "-87.6298,41.8781",
"brand": "Nord",
"model": "Chook air 5",
"price": 815,
"description": "The Chook Air 5 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. The lower top tube makes it easy to mount and dismount in any " +
"situation, giving your kids greater safety on the trails.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " +
"-80.2433 25.6967, -80.2433 25.8067))",
"store_location": "-80.1918,25.7617",
"brand": "Eva",
"model": "Eva 291",
"price": 3400,
"description": "The sister company to Nord, Eva launched in 2005 as the first " +
"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " +
"are optimized for the feminine physique using analytics from a body metrics database. " +
"If you like 29ers, try the Eva 291. Its a brand new bike for 2022.. This " +
"full-suspension, cross-country ride has been designed for velocity. The 291 has " +
"100mm of front and rear travel, a superlight aluminum frame and fast-rolling " +
"29-inch wheels. Yippee!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " +
"-122.4644 37.7099, -122.4644 37.8199))",
"store_location": "-122.4194,37.7749",
"brand": "Noka Bikes",
"model": "Kahuna",
"price": 3200,
"description": "Whether you want to try your hand at XC racing or are looking " +
"for a lively trail bike that's just as inspiring on the climbs as it is over rougher " +
"ground, the Wilder is one heck of a bike built specifically for short women. Both the " +
"frames and components have been tweaked to include a womens saddle, different bars " +
"and unique colourway.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " +
"-0.1778 51.4024, -0.1778 51.5524))",
"store_location": "-0.1278,51.5074",
"brand": "Breakout",
"model": "XBN 2.1 Alloy",
"price": 810,
"description": "The XBN 2.1 Alloy is our entry-level road bike but thats " +
"not to say that its a basic machine. With an internal weld aluminium frame, a full " +
"carbon fork, and the slick-shifting Claris gears from Shimanos, this is a bike which " +
"doesnt break the bank and delivers craved performance.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " +
"2.1767 48.5516, 2.1767 48.9016))",
"store_location": "2.3522,48.8566",
"brand": "ScramBikes",
"model": "WattBike",
"price": 2300,
"description": "The WattBike is the best e-bike for people who still " +
"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " +
"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " +
"charge. Its great for tackling hilly terrain or if you just fancy a more " +
"leisurely ride. With three working modes, you can choose between E-bike, " +
"assisted bicycle, and normal bike modes.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " +
"13.3260 52.2700, 13.3260 52.5700))",
"store_location": "13.4050,52.5200",
"brand": "Peaknetic",
"model": "Secto",
"price": 430,
"description": "If you struggle with stiff fingers or a kinked neck or " +
"back after a few minutes on the road, this lightweight, aluminum bike alleviates " +
"those issues and allows you to enjoy the ride. From the ergonomic grips to the " +
"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " +
"The rear-inclined seat tube facilitates stability by allowing you to put a foot " +
"on the ground to balance at a stop, and the low step-over frame makes it " +
"accessible for all ability and mobility levels. The saddle is very soft, with " +
"a wide back to support your hip joints and a cutout in the center to redistribute " +
"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " +
"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " +
"facilitate setting up the Roll Low-Entry as your preferred commuter, and the " +
"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " +
"1.9450 41.1987, 1.9450 41.4301))",
"store_location": "2.1734, 41.3851",
"brand": "nHill",
"model": "Summit",
"price": 1200,
"description": "This budget mountain bike from nHill performs well both " +
"on bike paths and on the trail. The fork with 100mm of travel absorbs rough " +
"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " +
"The Shimano Tourney drivetrain offered enough gears for finding a comfortable " +
"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " +
"Whether you want an affordable bike that you can take to work, but also take " +
"trail in mountains on the weekends or youre just after a stable, comfortable " +
"ride for the bike path, the Summit gives a good value for money.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " +
"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))",
"store_location": "12.4964,41.9028",
"model": "ThrillCycle",
"brand": "BikeShind",
"price": 815,
"description": "An artsy, retro-inspired bicycle thats as " +
"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " +
"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldnt " +
"suggest taking it to the mountains. Fenders protect you from mud, and a rear " +
"basket lets you transport groceries, flowers and books. The ThrillCycle comes " +
"with a limited lifetime warranty, so this little guy will last you long " +
"past graduation.",
"condition": "refurbished",
},
}
for i, json := range exampleJsons {
_, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result()
if err != nil {
panic(err)
}
}
// STEP_START ft1
res1, err := rdb.FTSearch(ctx,
"idx:bicycle", "@description: kids",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res1.Total) // >>> 2
sort.Slice(res1.Docs, func(i, j int) bool {
return res1.Docs[i].ID < res1.Docs[j].ID
})
for _, doc := range res1.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:1
// >>> bicycle:2
// STEP_END
// STEP_START ft2
res2, err := rdb.FTSearch(ctx,
"idx:bicycle", "@model: ka*",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res2.Total) // >>> 1
for _, doc := range res2.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:4
// STEP_END
// STEP_START ft3
res3, err := rdb.FTSearch(ctx,
"idx:bicycle", "@brand: *bikes",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res3.Total) // >>> 2
sort.Slice(res3.Docs, func(i, j int) bool {
return res3.Docs[i].ID < res3.Docs[j].ID
})
for _, doc := range res3.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:4
// >>> bicycle:6
// STEP_END
// STEP_START ft4
res4, err := rdb.FTSearch(ctx,
"idx:bicycle", "%optamized%",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res4.Total) // >>> 1
for _, doc := range res4.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:3
// STEP_END
// STEP_START ft5
res5, err := rdb.FTSearch(ctx,
"idx:bicycle", "%%optamised%%",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res5.Total) // >>> 1
for _, doc := range res5.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:3
// STEP_END
// Output:
// 2
// bicycle:1
// bicycle:2
// 1
// bicycle:4
// 2
// bicycle:4
// bicycle:6
// 1
// bicycle:3
// 1
// bicycle:3
}

329
doctests/query_geo_test.go Normal file

@ -0,0 +1,329 @@
// EXAMPLE: query_geo
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"sort"
"github.com/redis/go-redis/v9"
)
func ExampleClient_query_geo() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
Protocol: 2,
})
// HIDE_END
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.FTDropIndex(ctx, "idx:bicycle")
// REMOVE_END
_, err := rdb.FTCreate(ctx, "idx:bicycle",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"bicycle:"},
},
&redis.FieldSchema{
FieldName: "$.brand",
As: "brand",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.model",
As: "model",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.description",
As: "description",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.price",
As: "price",
FieldType: redis.SearchFieldTypeNumeric,
},
&redis.FieldSchema{
FieldName: "$.condition",
As: "condition",
FieldType: redis.SearchFieldTypeTag,
},
&redis.FieldSchema{
FieldName: "$.store_location",
As: "store_location",
FieldType: redis.SearchFieldTypeGeo,
},
&redis.FieldSchema{
FieldName: "$.pickup_zone",
As: "pickup_zone",
FieldType: redis.SearchFieldTypeGeoShape,
GeoShapeFieldType: "FLAT",
},
).Result()
if err != nil {
panic(err)
}
exampleJsons := []map[string]interface{}{
{
"pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " +
"-74.0610 40.6678, -74.0610 40.7578))",
"store_location": "-74.0060,40.7128",
"brand": "Velorim",
"model": "Jigger",
"price": 270,
"description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " +
"This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " +
"is the vehicle of choice for the rare tenacious little rider raring to go.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " +
"-118.2887 33.9872, -118.2887 34.0972))",
"store_location": "-118.2437,34.0522",
"brand": "Bicyk",
"model": "Hillcraft",
"price": 1200,
"description": "Kids want to ride with as little weight as possible. Especially " +
"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " +
"off a 24'' bike. The Hillcraft 26 is just the solution they need!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " +
"-87.6848 41.8231, -87.6848 41.9331))",
"store_location": "-87.6298,41.8781",
"brand": "Nord",
"model": "Chook air 5",
"price": 815,
"description": "The Chook Air 5 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. The lower top tube makes it easy to mount and dismount in any " +
"situation, giving your kids greater safety on the trails.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " +
"-80.2433 25.6967, -80.2433 25.8067))",
"store_location": "-80.1918,25.7617",
"brand": "Eva",
"model": "Eva 291",
"price": 3400,
"description": "The sister company to Nord, Eva launched in 2005 as the first " +
"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " +
"are optimized for the feminine physique using analytics from a body metrics database. " +
"If you like 29ers, try the Eva 291. Its a brand new bike for 2022.. This " +
"full-suspension, cross-country ride has been designed for velocity. The 291 has " +
"100mm of front and rear travel, a superlight aluminum frame and fast-rolling " +
"29-inch wheels. Yippee!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " +
"-122.4644 37.7099, -122.4644 37.8199))",
"store_location": "-122.4194,37.7749",
"brand": "Noka Bikes",
"model": "Kahuna",
"price": 3200,
"description": "Whether you want to try your hand at XC racing or are looking " +
"for a lively trail bike that's just as inspiring on the climbs as it is over rougher " +
"ground, the Wilder is one heck of a bike built specifically for short women. Both the " +
"frames and components have been tweaked to include a womens saddle, different bars " +
"and unique colourway.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " +
"-0.1778 51.4024, -0.1778 51.5524))",
"store_location": "-0.1278,51.5074",
"brand": "Breakout",
"model": "XBN 2.1 Alloy",
"price": 810,
"description": "The XBN 2.1 Alloy is our entry-level road bike but thats " +
"not to say that its a basic machine. With an internal weld aluminium frame, a full " +
"carbon fork, and the slick-shifting Claris gears from Shimanos, this is a bike which " +
"doesnt break the bank and delivers craved performance.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " +
"2.1767 48.5516, 2.1767 48.9016))",
"store_location": "2.3522,48.8566",
"brand": "ScramBikes",
"model": "WattBike",
"price": 2300,
"description": "The WattBike is the best e-bike for people who still " +
"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " +
"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " +
"charge. Its great for tackling hilly terrain or if you just fancy a more " +
"leisurely ride. With three working modes, you can choose between E-bike, " +
"assisted bicycle, and normal bike modes.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " +
"13.3260 52.2700, 13.3260 52.5700))",
"store_location": "13.4050,52.5200",
"brand": "Peaknetic",
"model": "Secto",
"price": 430,
"description": "If you struggle with stiff fingers or a kinked neck or " +
"back after a few minutes on the road, this lightweight, aluminum bike alleviates " +
"those issues and allows you to enjoy the ride. From the ergonomic grips to the " +
"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " +
"The rear-inclined seat tube facilitates stability by allowing you to put a foot " +
"on the ground to balance at a stop, and the low step-over frame makes it " +
"accessible for all ability and mobility levels. The saddle is very soft, with " +
"a wide back to support your hip joints and a cutout in the center to redistribute " +
"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " +
"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " +
"facilitate setting up the Roll Low-Entry as your preferred commuter, and the " +
"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " +
"1.9450 41.1987, 1.9450 41.4301))",
"store_location": "2.1734, 41.3851",
"brand": "nHill",
"model": "Summit",
"price": 1200,
"description": "This budget mountain bike from nHill performs well both " +
"on bike paths and on the trail. The fork with 100mm of travel absorbs rough " +
"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " +
"The Shimano Tourney drivetrain offered enough gears for finding a comfortable " +
"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " +
"Whether you want an affordable bike that you can take to work, but also take " +
"trail in mountains on the weekends or youre just after a stable, comfortable " +
"ride for the bike path, the Summit gives a good value for money.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " +
"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))",
"store_location": "12.4964,41.9028",
"model": "ThrillCycle",
"brand": "BikeShind",
"price": 815,
"description": "An artsy, retro-inspired bicycle thats as " +
"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " +
"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldnt " +
"suggest taking it to the mountains. Fenders protect you from mud, and a rear " +
"basket lets you transport groceries, flowers and books. The ThrillCycle comes " +
"with a limited lifetime warranty, so this little guy will last you long " +
"past graduation.",
"condition": "refurbished",
},
}
for i, json := range exampleJsons {
_, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result()
if err != nil {
panic(err)
}
}
// STEP_START geo1
res1, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle", "@store_location:[$lon $lat $radius $units]",
&redis.FTSearchOptions{
Params: map[string]interface{}{
"lon": -0.1778,
"lat": 51.5524,
"radius": 20,
"units": "mi",
},
DialectVersion: 2,
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res1.Total) // >>> 1
for _, doc := range res1.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:5
// STEP_END
// STEP_START geo2
res2, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle",
"@pickup_zone:[CONTAINS $bike]",
&redis.FTSearchOptions{
Params: map[string]interface{}{
"bike": "POINT(-0.1278 51.5074)",
},
DialectVersion: 3,
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res2.Total) // >>> 1
for _, doc := range res2.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:5
// STEP_END
// STEP_START geo3
res3, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle",
"@pickup_zone:[WITHIN $europe]",
&redis.FTSearchOptions{
Params: map[string]interface{}{
"europe": "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))",
},
DialectVersion: 3,
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res3.Total) // >>> 5
sort.Slice(res3.Docs, func(i, j int) bool {
return res3.Docs[i].ID < res3.Docs[j].ID
})
for _, doc := range res3.Docs {
fmt.Println(doc.ID)
}
// >>> bicycle:5
// >>> bicycle:6
// >>> bicycle:7
// >>> bicycle:8
// >>> bicycle:9
// STEP_END
// Output:
// 1
// bicycle:5
// 1
// bicycle:5
// 5
// bicycle:5
// bicycle:6
// bicycle:7
// bicycle:8
// bicycle:9
}

@ -0,0 +1,394 @@
// EXAMPLE: query_range
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func ExampleClient_query_range() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
Protocol: 2,
})
// HIDE_END
// REMOVE_START
rdb.FTDropIndex(ctx, "idx:bicycle")
rdb.FTDropIndex(ctx, "idx:email")
// REMOVE_END
_, err := rdb.FTCreate(ctx, "idx:bicycle",
&redis.FTCreateOptions{
OnJSON: true,
Prefix: []interface{}{"bicycle:"},
},
&redis.FieldSchema{
FieldName: "$.brand",
As: "brand",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.model",
As: "model",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.description",
As: "description",
FieldType: redis.SearchFieldTypeText,
},
&redis.FieldSchema{
FieldName: "$.price",
As: "price",
FieldType: redis.SearchFieldTypeNumeric,
},
&redis.FieldSchema{
FieldName: "$.condition",
As: "condition",
FieldType: redis.SearchFieldTypeTag,
},
).Result()
if err != nil {
panic(err)
}
exampleJsons := []map[string]interface{}{
{
"pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " +
"-74.0610 40.6678, -74.0610 40.7578))",
"store_location": "-74.0060,40.7128",
"brand": "Velorim",
"model": "Jigger",
"price": 270,
"description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " +
"This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " +
"is the vehicle of choice for the rare tenacious little rider raring to go.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " +
"-118.2887 33.9872, -118.2887 34.0972))",
"store_location": "-118.2437,34.0522",
"brand": "Bicyk",
"model": "Hillcraft",
"price": 1200,
"description": "Kids want to ride with as little weight as possible. Especially " +
"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " +
"off a 24'' bike. The Hillcraft 26 is just the solution they need!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " +
"-87.6848 41.8231, -87.6848 41.9331))",
"store_location": "-87.6298,41.8781",
"brand": "Nord",
"model": "Chook air 5",
"price": 815,
"description": "The Chook Air 5 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. The lower top tube makes it easy to mount and dismount in any " +
"situation, giving your kids greater safety on the trails.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " +
"-80.2433 25.6967, -80.2433 25.8067))",
"store_location": "-80.1918,25.7617",
"brand": "Eva",
"model": "Eva 291",
"price": 3400,
"description": "The sister company to Nord, Eva launched in 2005 as the first " +
"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " +
"are optimized for the feminine physique using analytics from a body metrics database. " +
"If you like 29ers, try the Eva 291. Its a brand new bike for 2022.. This " +
"full-suspension, cross-country ride has been designed for velocity. The 291 has " +
"100mm of front and rear travel, a superlight aluminum frame and fast-rolling " +
"29-inch wheels. Yippee!",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " +
"-122.4644 37.7099, -122.4644 37.8199))",
"store_location": "-122.4194,37.7749",
"brand": "Noka Bikes",
"model": "Kahuna",
"price": 3200,
"description": "Whether you want to try your hand at XC racing or are looking " +
"for a lively trail bike that's just as inspiring on the climbs as it is over rougher " +
"ground, the Wilder is one heck of a bike built specifically for short women. Both the " +
"frames and components have been tweaked to include a womens saddle, different bars " +
"and unique colourway.",
"condition": "used",
},
{
"pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " +
"-0.1778 51.4024, -0.1778 51.5524))",
"store_location": "-0.1278,51.5074",
"brand": "Breakout",
"model": "XBN 2.1 Alloy",
"price": 810,
"description": "The XBN 2.1 Alloy is our entry-level road bike but thats " +
"not to say that its a basic machine. With an internal weld aluminium frame, a full " +
"carbon fork, and the slick-shifting Claris gears from Shimanos, this is a bike which " +
"doesnt break the bank and delivers craved performance.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " +
"2.1767 48.5516, 2.1767 48.9016))",
"store_location": "2.3522,48.8566",
"brand": "ScramBikes",
"model": "WattBike",
"price": 2300,
"description": "The WattBike is the best e-bike for people who still " +
"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " +
"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " +
"charge. Its great for tackling hilly terrain or if you just fancy a more " +
"leisurely ride. With three working modes, you can choose between E-bike, " +
"assisted bicycle, and normal bike modes.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " +
"13.3260 52.2700, 13.3260 52.5700))",
"store_location": "13.4050,52.5200",
"brand": "Peaknetic",
"model": "Secto",
"price": 430,
"description": "If you struggle with stiff fingers or a kinked neck or " +
"back after a few minutes on the road, this lightweight, aluminum bike alleviates " +
"those issues and allows you to enjoy the ride. From the ergonomic grips to the " +
"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " +
"The rear-inclined seat tube facilitates stability by allowing you to put a foot " +
"on the ground to balance at a stop, and the low step-over frame makes it " +
"accessible for all ability and mobility levels. The saddle is very soft, with " +
"a wide back to support your hip joints and a cutout in the center to redistribute " +
"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " +
"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " +
"facilitate setting up the Roll Low-Entry as your preferred commuter, and the " +
"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " +
"1.9450 41.1987, 1.9450 41.4301))",
"store_location": "2.1734, 41.3851",
"brand": "nHill",
"model": "Summit",
"price": 1200,
"description": "This budget mountain bike from nHill performs well both " +
"on bike paths and on the trail. The fork with 100mm of travel absorbs rough " +
"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " +
"The Shimano Tourney drivetrain offered enough gears for finding a comfortable " +
"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " +
"Whether you want an affordable bike that you can take to work, but also take " +
"trail in mountains on the weekends or youre just after a stable, comfortable " +
"ride for the bike path, the Summit gives a good value for money.",
"condition": "new",
},
{
"pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " +
"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))",
"store_location": "12.4964,41.9028",
"model": "ThrillCycle",
"brand": "BikeShind",
"price": 815,
"description": "An artsy, retro-inspired bicycle thats as " +
"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " +
"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldnt " +
"suggest taking it to the mountains. Fenders protect you from mud, and a rear " +
"basket lets you transport groceries, flowers and books. The ThrillCycle comes " +
"with a limited lifetime warranty, so this little guy will last you long " +
"past graduation.",
"condition": "refurbished",
},
}
for i, json := range exampleJsons {
_, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result()
if err != nil {
panic(err)
}
}
// STEP_START range1
res1, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle", "@price:[500 1000]",
&redis.FTSearchOptions{
Return: []redis.FTSearchReturn{
{
FieldName: "price",
},
},
SortBy: []redis.FTSearchSortBy{
{
FieldName: "price",
Asc: true,
},
},
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res1.Total) // >>> 3
for _, doc := range res1.Docs {
fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"])
}
// >>> bicycle:2 : price 815
// >>> bicycle:5 : price 810
// >>> bicycle:9 : price 815
// STEP_END
// STEP_START range2
res2, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle", "*",
&redis.FTSearchOptions{
Filters: []redis.FTSearchFilter{
{
FieldName: "price",
Min: 500,
Max: 1000,
},
},
Return: []redis.FTSearchReturn{
{
FieldName: "price",
},
},
SortBy: []redis.FTSearchSortBy{
{
FieldName: "price",
Asc: true,
},
},
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res2.Total) // >>> 3
for _, doc := range res2.Docs {
fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"])
}
// >>> bicycle:2 : price 815
// >>> bicycle:5 : price 810
// >>> bicycle:9 : price 815
// STEP_END
// STEP_START range3
res3, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle", "*",
&redis.FTSearchOptions{
Return: []redis.FTSearchReturn{
{
FieldName: "price",
},
},
SortBy: []redis.FTSearchSortBy{
{
FieldName: "price",
Asc: true,
},
},
Filters: []redis.FTSearchFilter{
{
FieldName: "price",
Min: "(1000",
Max: "+inf",
},
},
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res3.Total) // >>> 5
for _, doc := range res3.Docs {
fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"])
}
// >>> bicycle:1 : price 1200
// >>> bicycle:4 : price 3200
// >>> bicycle:6 : price 2300
// >>> bicycle:3 : price 3400
// >>> bicycle:8 : price 1200
// STEP_END
// STEP_START range4
res4, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle",
"@price:[-inf 2000]",
&redis.FTSearchOptions{
Return: []redis.FTSearchReturn{
{
FieldName: "price",
},
},
SortBy: []redis.FTSearchSortBy{
{
FieldName: "price",
Asc: true,
},
},
LimitOffset: 0,
Limit: 5,
},
).Result()
if err != nil {
panic(err)
}
fmt.Println(res4.Total) // >>> 7
for _, doc := range res4.Docs {
fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"])
}
// >>> bicycle:0 : price 270
// >>> bicycle:7 : price 430
// >>> bicycle:5 : price 810
// >>> bicycle:2 : price 815
// >>> bicycle:9 : price 815
// STEP_END
// Output:
// 3
// bicycle:5 : price 810
// bicycle:2 : price 815
// bicycle:9 : price 815
// 3
// bicycle:5 : price 810
// bicycle:2 : price 815
// bicycle:9 : price 815
// 5
// bicycle:1 : price 1200
// bicycle:8 : price 1200
// bicycle:6 : price 2300
// bicycle:4 : price 3200
// bicycle:3 : price 3400
// 7
// bicycle:0 : price 270
// bicycle:7 : price 430
// bicycle:5 : price 810
// bicycle:2 : price 815
// bicycle:9 : price 815
}

@ -21,6 +21,8 @@ func ExampleClient_Set_and_get() {
// HIDE_END
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test
if errFlush != nil {
panic(errFlush)

@ -0,0 +1,473 @@
// EXAMPLE: sets_tutorial
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"sort"
"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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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)
}
// Sort the strings in the slice to make sure the output is lexicographical
sort.Strings(res10)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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)
}
// Sort the strings in the slice to make sure the output is lexicographical
sort.Strings(res13)
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
// start with fresh database
rdb.FlushDB(ctx)
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)
}
// Sort the strings in the slice to make sure the output is lexicographical
sort.Strings(res15)
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)
}
// Sort the strings in the slice to make sure the output is lexicographical
sort.Strings(res18)
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
// start with fresh database
rdb.FlushDB(ctx)
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.
}

@ -0,0 +1,453 @@
// 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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,181 @@
// 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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
}

@ -0,0 +1,261 @@
// 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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
// start with fresh database
rdb.FlushDB(ctx)
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
}

@ -0,0 +1,77 @@
// EXAMPLE: topk_tutorial
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// HIDE_END
func ExampleClient_topk() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// REMOVE_START
// start with fresh database
rdb.FlushDB(ctx)
rdb.Del(ctx, "bikes:keywords")
// REMOVE_END
// STEP_START topk
res1, err := rdb.TopKReserve(ctx, "bikes:keywords", 5).Result()
if err != nil {
panic(err)
}
fmt.Println(res1) // >>> OK
res2, err := rdb.TopKAdd(ctx, "bikes:keywords",
"store",
"seat",
"handlebars",
"handles",
"pedals",
"tires",
"store",
"seat",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res2) // >>> [ handlebars ]
res3, err := rdb.TopKList(ctx, "bikes:keywords").Result()
if err != nil {
panic(err)
}
fmt.Println(res3) // [store seat pedals tires handles]
res4, err := rdb.TopKQuery(ctx, "bikes:keywords", "store", "handlebars").Result()
if err != nil {
panic(err)
}
fmt.Println(res4) // [true false]
// STEP_END
// Output:
// OK
// [ handlebars ]
// [store seat pedals tires handles]
// [true false]
}

@ -7,6 +7,7 @@ import (
"net"
"strings"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
)
@ -37,6 +38,15 @@ type Error interface {
var _ Error = proto.RedisError("")
func isContextError(err error) bool {
switch err {
case context.Canceled, context.DeadlineExceeded:
return true
default:
return false
}
}
func shouldRetry(err error, retryTimeout bool) bool {
switch err {
case io.EOF, io.ErrUnexpectedEOF:
@ -129,7 +139,9 @@ func isMovedError(err error) (moved bool, ask bool, addr string) {
if ind == -1 {
return false, false, ""
}
addr = s[ind+1:]
addr = internal.GetAddr(addr)
return
}

@ -5,12 +5,12 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/redis/go-redis/v9 v9.4.0
github.com/redis/go-redis/v9 v9.7.1
go.uber.org/zap v1.24.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect

@ -1,8 +1,8 @@
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

@ -4,9 +4,9 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.4.0
require github.com/redis/go-redis/v9 v9.7.1
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

@ -1,6 +1,6 @@
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

@ -0,0 +1,7 @@
# Example for setting struct fields as hash fields
To run this example:
```shell
go run .
```

@ -0,0 +1,15 @@
module github.com/redis/go-redis/example/scan-struct
go 1.18
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/davecgh/go-spew v1.1.1
github.com/redis/go-redis/v9 v9.6.2
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

@ -0,0 +1,10 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

129
example/hset-struct/main.go Normal file

@ -0,0 +1,129 @@
package main
import (
"context"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/redis/go-redis/v9"
)
type Model struct {
Str1 string `redis:"str1"`
Str2 string `redis:"str2"`
Str3 *string `redis:"str3"`
Str4 *string `redis:"str4"`
Bytes []byte `redis:"bytes"`
Int int `redis:"int"`
Int2 *int `redis:"int2"`
Int3 *int `redis:"int3"`
Bool bool `redis:"bool"`
Bool2 *bool `redis:"bool2"`
Bool3 *bool `redis:"bool3"`
Bool4 *bool `redis:"bool4,omitempty"`
Time time.Time `redis:"time"`
Time2 *time.Time `redis:"time2"`
Time3 *time.Time `redis:"time3"`
Ignored struct{} `redis:"-"`
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
_ = rdb.FlushDB(ctx).Err()
t := time.Date(2025, 02, 8, 0, 0, 0, 0, time.UTC)
data := Model{
Str1: "hello",
Str2: "world",
Str3: ToPtr("hello"),
Str4: nil,
Bytes: []byte("this is bytes !"),
Int: 123,
Int2: ToPtr(0),
Int3: nil,
Bool: true,
Bool2: ToPtr(false),
Bool3: nil,
Time: t,
Time2: ToPtr(t),
Time3: nil,
Ignored: struct{}{},
}
// Set some fields.
if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {
rdb.HMSet(ctx, "key", data)
return nil
}); err != nil {
panic(err)
}
var model1, model2 Model
// Scan all fields into the model.
if err := rdb.HGetAll(ctx, "key").Scan(&model1); err != nil {
panic(err)
}
// Or scan a subset of the fields.
if err := rdb.HMGet(ctx, "key", "str1", "int").Scan(&model2); err != nil {
panic(err)
}
spew.Dump(model1)
// Output:
// (main.Model) {
// Str1: (string) (len=5) "hello",
// Str2: (string) (len=5) "world",
// Str3: (*string)(0xc000016970)((len=5) "hello"),
// Str4: (*string)(0xc000016980)(""),
// Bytes: ([]uint8) (len=15 cap=16) {
// 00000000 74 68 69 73 20 69 73 20 62 79 74 65 73 20 21 |this is bytes !|
// },
// Int: (int) 123,
// Int2: (*int)(0xc000014568)(0),
// Int3: (*int)(0xc000014560)(0),
// Bool: (bool) true,
// Bool2: (*bool)(0xc000014570)(false),
// Bool3: (*bool)(0xc000014548)(false),
// Bool4: (*bool)(<nil>),
// Time: (time.Time) 2025-02-08 00:00:00 +0000 UTC,
// Time2: (*time.Time)(0xc0000122a0)(2025-02-08 00:00:00 +0000 UTC),
// Time3: (*time.Time)(0xc000012288)(0001-01-01 00:00:00 +0000 UTC),
// Ignored: (struct {}) {
// }
// }
spew.Dump(model2)
// Output:
// (main.Model) {
// Str1: (string) (len=5) "hello",
// Str2: (string) "",
// Str3: (*string)(<nil>),
// Str4: (*string)(<nil>),
// Bytes: ([]uint8) <nil>,
// Int: (int) 123,
// Int2: (*int)(<nil>),
// Int3: (*int)(<nil>),
// Bool: (bool) false,
// Bool2: (*bool)(<nil>),
// Bool3: (*bool)(<nil>),
// Bool4: (*bool)(<nil>),
// Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC,
// Time2: (*time.Time)(<nil>),
// Time3: (*time.Time)(<nil>),
// Ignored: (struct {}) {
// }
// }
}
func ToPtr[T any](v T) *T {
return &v
}

@ -4,9 +4,9 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.4.0
require github.com/redis/go-redis/v9 v9.7.1
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

@ -1,6 +1,6 @@
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

@ -9,39 +9,37 @@ 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.4.0
github.com/redis/go-redis/v9 v9.4.0
github.com/redis/go-redis/extra/redisotel/v9 v9.7.1
github.com/redis/go-redis/v9 v9.7.1
github.com/uptrace/uptrace-go v1.21.0
go.opentelemetry.io/otel v1.21.0
go.opentelemetry.io/otel v1.22.0
)
require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-logr/logr v1.4.1 // indirect
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.4.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/sdk v1.22.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/grpc v1.60.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)

@ -1,516 +1,68 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/uptrace/uptrace-go v1.16.0 h1:yB9vt1hBYYoXWExNx0okubLOjd339d7lH+/5o+Lp+MY=
github.com/uptrace/uptrace-go v1.16.0/go.mod h1:Ssc5wLpoL+9V0qkT5FtrIiru9SY4xb7q1UVLjSpxpCg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/uptrace/uptrace-go v1.21.0 h1:oJoUjhiVT7aiuoG6B3ClVHtJozLn3cK9hQt8U5dQO1M=
github.com/uptrace/uptrace-go v1.21.0/go.mod h1:/aXAFGKOqeAFBqWa1xtzLnGX2xJm1GScqz9NJ0TJjLM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 h1:m9ReioVPIffxjJlGNRd0d5poy+9oTro3D+YbiEzUDOc=
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1/go.mod h1:CANkrsXNzqOKXfOomu2zhOmc1/J5UZK9SGjrat6ZCG0=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0=
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI=
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.17.0 h1:eU0ffpYuEY7eQ75K+nKr9CI5KcY8h+GPk/9DDlEO1NI=
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.17.0/go.mod h1:9P5RK5JS2sjKepuCkqFwPp3etwV/57E0eigLw18Mn1k=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 h1:f6BwB2OACc3FCbYVznctQ9V6KK7Vq6CjmYXJ7DeSs4E=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0/go.mod h1:UqL5mZ3qs6XYhDnZaW1Ps4upD+PX6LipH40AoeuIlwU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.43.0 h1:f+VtlQwREKbGdbq/Mx/xMDLrPktBZ1+5PzNMrYSsdXo=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.43.0/go.mod h1:V0A1wlhxQUdvqQk+vMA5+NwT7I6AFSyQv1EXLQBb8dM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0 h1:rm+Fizi7lTM2UefJ1TO347fSRcwmIsUAaZmYmIGBRAo=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0/go.mod h1:sWFbI3jJ+6JdjOVepA5blpv/TJ20Hw+26561iMbWcwU=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 h1:+XWJd3jf75RXJq29mxbuXhCXFDG3S3R4vBUeSI2P7tE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0/go.mod h1:hqgzBPTf4yONMFgdZvL/bK42R/iinTyVQtiWihs3SZc=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs=
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo=
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

@ -4,9 +4,9 @@ go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.4.0
require github.com/redis/go-redis/v9 v9.7.1
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

@ -1,6 +1,6 @@
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

@ -6,10 +6,10 @@ replace github.com/redis/go-redis/v9 => ../..
require (
github.com/davecgh/go-spew v1.1.1
github.com/redis/go-redis/v9 v9.4.0
github.com/redis/go-redis/v9 v9.7.1
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

@ -1,7 +1,7 @@
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=

@ -11,8 +11,12 @@ import (
type Model struct {
Str1 string `redis:"str1"`
Str2 string `redis:"str2"`
Str3 *string `redis:"str3"`
Bytes []byte `redis:"bytes"`
Int int `redis:"int"`
Int2 *int `redis:"int2"`
Bool bool `redis:"bool"`
Bool2 *bool `redis:"bool2"`
Ignored struct{} `redis:"-"`
}
@ -22,13 +26,18 @@ func main() {
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
_ = rdb.FlushDB(ctx).Err()
// Set some fields.
if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {
rdb.HSet(ctx, "key", "str1", "hello")
rdb.HSet(ctx, "key", "str2", "world")
rdb.HSet(ctx, "key", "str3", "")
rdb.HSet(ctx, "key", "int", 123)
rdb.HSet(ctx, "key", "int2", 0)
rdb.HSet(ctx, "key", "bool", 1)
rdb.HSet(ctx, "key", "bool2", 0)
rdb.HSet(ctx, "key", "bytes", []byte("this is bytes !"))
return nil
}); err != nil {
panic(err)
@ -47,5 +56,28 @@ func main() {
}
spew.Dump(model1)
// Output:
// (main.Model) {
// Str1: (string) (len=5) "hello",
// Str2: (string) (len=5) "world",
// Bytes: ([]uint8) (len=15 cap=16) {
// 00000000 74 68 69 73 20 69 73 20 62 79 74 65 73 20 21 |this is bytes !|
// },
// Int: (int) 123,
// Bool: (bool) true,
// Ignored: (struct {}) {
// }
// }
spew.Dump(model2)
// Output:
// (main.Model) {
// Str1: (string) (len=5) "hello",
// Str2: (string) "",
// Bytes: ([]uint8) <nil>,
// Int: (int) 123,
// Bool: (bool) false,
// Ignored: (struct {}) {
// }
// }
}

@ -154,7 +154,7 @@ func ExampleClient() {
// missing_key does not exist
}
func ExampleConn() {
func ExampleConn_name() {
conn := rdb.Conn()
err := conn.ClientSetName(ctx, "foobar").Err()
@ -175,6 +175,28 @@ func ExampleConn() {
// Output: foobar
}
func ExampleConn_client_setInfo_libraryVersion() {
conn := rdb.Conn()
err := conn.ClientSetInfo(ctx, redis.WithLibraryVersion("1.2.3")).Err()
if err != nil {
panic(err)
}
// Open other connections.
for i := 0; i < 10; i++ {
go rdb.Ping(ctx)
}
s, err := conn.ClientInfo(ctx).Result()
if err != nil {
panic(err)
}
fmt.Println(s.LibVer)
// Output: 1.2.3
}
func ExampleClient_Set() {
// Last argument is expiration. Zero means the key has no
// expiration time.
@ -460,7 +482,7 @@ func ExampleClient_Watch() {
return err
}
// Actual opperation (local in optimistic lock).
// Actual operation (local in optimistic lock).
n++
// Operation is committed only if the watched keys remain unchanged.

@ -1,14 +1,21 @@
module github.com/redis/go-redis/extra/rediscensus/v9
go 1.15
go 1.19
replace github.com/redis/go-redis/v9 => ../..
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd
require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.4.0
github.com/redis/go-redis/v9 v9.4.0
github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1
github.com/redis/go-redis/v9 v9.7.1
go.opencensus.io v0.24.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
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.

@ -1,12 +1,10 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

@ -1,11 +1,18 @@
module github.com/redis/go-redis/extra/rediscmd/v9
go 1.15
go 1.19
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/bsm/ginkgo/v2 v2.7.0
github.com/bsm/gomega v1.26.0
github.com/redis/go-redis/v9 v9.4.0
github.com/bsm/ginkgo/v2 v2.12.0
github.com/bsm/gomega v1.27.10
github.com/redis/go-redis/v9 v9.7.1
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)
retract v9.5.3 // This version was accidentally released.

@ -1,8 +1,8 @@
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

@ -4,7 +4,7 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
)

@ -7,18 +7,20 @@ 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.4.0
github.com/redis/go-redis/v9 v9.4.0
go.opentelemetry.io/otel v1.16.0
go.opentelemetry.io/otel/metric v1.16.0
go.opentelemetry.io/otel/sdk v1.16.0
go.opentelemetry.io/otel/trace v1.16.0
github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1
github.com/redis/go-redis/v9 v9.7.1
go.opentelemetry.io/otel v1.22.0
go.opentelemetry.io/otel/metric v1.22.0
go.opentelemetry.io/otel/sdk v1.22.0
go.opentelemetry.io/otel/trace v1.22.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/sys v0.16.0 // indirect
)
retract v9.5.3 // This version was accidentally released.

@ -1,28 +1,26 @@
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

@ -1,61 +0,0 @@
package redisotel
import (
"context"
"testing"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
"github.com/redis/go-redis/v9"
)
type providerFunc func(name string, opts ...trace.TracerOption) trace.Tracer
func (fn providerFunc) Tracer(name string, opts ...trace.TracerOption) trace.Tracer {
return fn(name, opts...)
}
func TestNewWithTracerProvider(t *testing.T) {
invoked := false
tp := providerFunc(func(name string, opts ...trace.TracerOption) trace.Tracer {
invoked = true
return otel.GetTracerProvider().Tracer(name, opts...)
})
_ = newTracingHook("", WithTracerProvider(tp))
if !invoked {
t.Fatalf("did not call custom TraceProvider")
}
}
func TestWithDBStatement(t *testing.T) {
provider := sdktrace.NewTracerProvider()
hook := newTracingHook(
"",
WithTracerProvider(provider),
WithDBStatement(false),
)
ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
cmd := redis.NewCmd(ctx, "ping")
defer span.End()
processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()
for _, attr := range attrs {
if attr.Key == semconv.DBStatementKey {
t.Fatal("Attribute with db statement should not exist")
}
}
return nil
})
err := processHook(ctx, cmd)
if err != nil {
t.Fatal(err)
}
}

@ -5,11 +5,12 @@ import (
"fmt"
"net"
"runtime"
"strconv"
"strings"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
"github.com/redis/go-redis/extra/rediscmd/v9"
@ -25,22 +26,21 @@ func InstrumentTracing(rdb redis.UniversalClient, opts ...TracingOption) error {
case *redis.Client:
opt := rdb.Options()
connString := formatDBConnString(opt.Network, opt.Addr)
opts = addServerAttributes(opts, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
return nil
case *redis.ClusterClient:
rdb.AddHook(newTracingHook("", opts...))
rdb.OnNewNode(func(rdb *redis.Client) {
opt := rdb.Options()
opts = addServerAttributes(opts, opt.Addr)
connString := formatDBConnString(opt.Network, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
})
return nil
case *redis.Ring:
rdb.AddHook(newTracingHook("", opts...))
rdb.OnNewNode(func(rdb *redis.Client) {
opt := rdb.Options()
opts = addServerAttributes(opts, opt.Addr)
connString := formatDBConnString(opt.Network, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
})
@ -72,7 +72,7 @@ func newTracingHook(connString string, opts ...TracingOption) *tracingHook {
)
}
if connString != "" {
conf.attrs = append(conf.attrs, semconv.DBConnectionStringKey.String(connString))
conf.attrs = append(conf.attrs, semconv.DBConnectionString(connString))
}
return &tracingHook{
@ -87,10 +87,6 @@ func newTracingHook(connString string, opts ...TracingOption) *tracingHook {
func (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
if !trace.SpanFromContext(ctx).IsRecording() {
return hook(ctx, network, addr)
}
ctx, span := th.conf.tracer.Start(ctx, "redis.dial", th.spanOpts...)
defer span.End()
@ -105,22 +101,18 @@ func (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook {
func (th *tracingHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
if !trace.SpanFromContext(ctx).IsRecording() {
return hook(ctx, cmd)
}
fn, file, line := funcFileLine("github.com/redis/go-redis")
attrs := make([]attribute.KeyValue, 0, 8)
attrs = append(attrs,
semconv.CodeFunctionKey.String(fn),
semconv.CodeFilepathKey.String(file),
semconv.CodeLineNumberKey.Int(line),
semconv.CodeFunction(fn),
semconv.CodeFilepath(file),
semconv.CodeLineNumber(line),
)
if th.conf.dbStmtEnabled {
cmdString := rediscmd.CmdString(cmd)
attrs = append(attrs, semconv.DBStatementKey.String(cmdString))
attrs = append(attrs, semconv.DBStatement(cmdString))
}
opts := th.spanOpts
@ -141,23 +133,19 @@ func (th *tracingHook) ProcessPipelineHook(
hook redis.ProcessPipelineHook,
) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
if !trace.SpanFromContext(ctx).IsRecording() {
return hook(ctx, cmds)
}
fn, file, line := funcFileLine("github.com/redis/go-redis")
attrs := make([]attribute.KeyValue, 0, 8)
attrs = append(attrs,
semconv.CodeFunctionKey.String(fn),
semconv.CodeFilepathKey.String(file),
semconv.CodeLineNumberKey.Int(line),
semconv.CodeFunction(fn),
semconv.CodeFilepath(file),
semconv.CodeLineNumber(line),
attribute.Int("db.redis.num_cmd", len(cmds)),
)
summary, cmdsString := rediscmd.CmdsString(cmds)
if th.conf.dbStmtEnabled {
attrs = append(attrs, semconv.DBStatementKey.String(cmdsString))
attrs = append(attrs, semconv.DBStatement(cmdsString))
}
opts := th.spanOpts
@ -213,3 +201,28 @@ func funcFileLine(pkg string) (string, string, int) {
return fn, file, line
}
// Database span attributes semantic conventions recommended server address and port
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/#connection-level-attributes
func addServerAttributes(opts []TracingOption, addr string) []TracingOption {
host, portString, err := net.SplitHostPort(addr)
if err != nil {
return opts
}
opts = append(opts, WithAttributes(
semconv.ServerAddress(host),
))
// Parse the port string to an integer
port, err := strconv.Atoi(portString)
if err != nil {
return opts
}
opts = append(opts, WithAttributes(
semconv.ServerPort(port),
))
return opts
}

@ -0,0 +1,240 @@
package redisotel
import (
"context"
"fmt"
"net"
"testing"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
"github.com/redis/go-redis/v9"
)
type providerFunc func(name string, opts ...trace.TracerOption) trace.TracerProvider
func (fn providerFunc) TracerProvider(name string, opts ...trace.TracerOption) trace.TracerProvider {
return fn(name, opts...)
}
func TestNewWithTracerProvider(t *testing.T) {
invoked := false
tp := providerFunc(func(name string, opts ...trace.TracerOption) trace.TracerProvider {
invoked = true
return otel.GetTracerProvider()
})
_ = newTracingHook("redis-hook", WithTracerProvider(tp.TracerProvider("redis-test")))
if !invoked {
t.Fatalf("did not call custom TraceProvider")
}
}
func TestWithDBStatement(t *testing.T) {
provider := sdktrace.NewTracerProvider()
hook := newTracingHook(
"",
WithTracerProvider(provider),
WithDBStatement(false),
)
ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
cmd := redis.NewCmd(ctx, "ping")
defer span.End()
processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()
for _, attr := range attrs {
if attr.Key == semconv.DBStatementKey {
t.Fatal("Attribute with db statement should not exist")
}
}
return nil
})
err := processHook(ctx, cmd)
if err != nil {
t.Fatal(err)
}
}
func TestTracingHook_DialHook(t *testing.T) {
imsb := tracetest.NewInMemoryExporter()
provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))
hook := newTracingHook(
"redis://localhost:6379",
WithTracerProvider(provider),
)
tests := []struct {
name string
errTest error
}{
{"nil error", nil},
{"test error", fmt.Errorf("test error")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer imsb.Reset()
dialHook := hook.DialHook(func(ctx context.Context, network, addr string) (conn net.Conn, err error) {
return nil, tt.errTest
})
if _, err := dialHook(context.Background(), "tcp", "localhost:6379"); err != tt.errTest {
t.Fatal(err)
}
assertEqual(t, 1, len(imsb.GetSpans()))
spanData := imsb.GetSpans()[0]
assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name)
assertEqual(t, "redis.dial", spanData.Name)
assertEqual(t, trace.SpanKindClient, spanData.SpanKind)
assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis)
assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379"))
if tt.errTest == nil {
assertEqual(t, 0, len(spanData.Events))
assertEqual(t, codes.Unset, spanData.Status.Code)
assertEqual(t, "", spanData.Status.Description)
return
}
assertEqual(t, 1, len(spanData.Events))
assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString"))
assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error()))
assertEqual(t, codes.Error, spanData.Status.Code)
assertEqual(t, tt.errTest.Error(), spanData.Status.Description)
})
}
}
func TestTracingHook_ProcessHook(t *testing.T) {
imsb := tracetest.NewInMemoryExporter()
provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))
hook := newTracingHook(
"redis://localhost:6379",
WithTracerProvider(provider),
)
tests := []struct {
name string
errTest error
}{
{"nil error", nil},
{"test error", fmt.Errorf("test error")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer imsb.Reset()
cmd := redis.NewCmd(context.Background(), "ping")
processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
return tt.errTest
})
assertEqual(t, tt.errTest, processHook(context.Background(), cmd))
assertEqual(t, 1, len(imsb.GetSpans()))
spanData := imsb.GetSpans()[0]
assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name)
assertEqual(t, "ping", spanData.Name)
assertEqual(t, trace.SpanKindClient, spanData.SpanKind)
assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis)
assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379"))
assertAttributeContains(t, spanData.Attributes, semconv.DBStatementKey.String("ping"))
if tt.errTest == nil {
assertEqual(t, 0, len(spanData.Events))
assertEqual(t, codes.Unset, spanData.Status.Code)
assertEqual(t, "", spanData.Status.Description)
return
}
assertEqual(t, 1, len(spanData.Events))
assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString"))
assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error()))
assertEqual(t, codes.Error, spanData.Status.Code)
assertEqual(t, tt.errTest.Error(), spanData.Status.Description)
})
}
}
func TestTracingHook_ProcessPipelineHook(t *testing.T) {
imsb := tracetest.NewInMemoryExporter()
provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))
hook := newTracingHook(
"redis://localhost:6379",
WithTracerProvider(provider),
)
tests := []struct {
name string
errTest error
}{
{"nil error", nil},
{"test error", fmt.Errorf("test error")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer imsb.Reset()
cmds := []redis.Cmder{
redis.NewCmd(context.Background(), "ping"),
redis.NewCmd(context.Background(), "ping"),
}
processHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error {
return tt.errTest
})
assertEqual(t, tt.errTest, processHook(context.Background(), cmds))
assertEqual(t, 1, len(imsb.GetSpans()))
spanData := imsb.GetSpans()[0]
assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name)
assertEqual(t, "redis.pipeline ping", spanData.Name)
assertEqual(t, trace.SpanKindClient, spanData.SpanKind)
assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis)
assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379"))
assertAttributeContains(t, spanData.Attributes, semconv.DBStatementKey.String("ping\nping"))
if tt.errTest == nil {
assertEqual(t, 0, len(spanData.Events))
assertEqual(t, codes.Unset, spanData.Status.Code)
assertEqual(t, "", spanData.Status.Description)
return
}
assertEqual(t, 1, len(spanData.Events))
assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString"))
assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error()))
assertEqual(t, codes.Error, spanData.Status.Code)
assertEqual(t, tt.errTest.Error(), spanData.Status.Description)
})
}
}
func assertEqual(t *testing.T, expected, actual interface{}) {
t.Helper()
if expected != actual {
t.Fatalf("expected %v, got %v", expected, actual)
}
}
func assertAttributeContains(t *testing.T, attrs []attribute.KeyValue, attr attribute.KeyValue) {
t.Helper()
for _, a := range attrs {
if a == attr {
return
}
}
t.Fatalf("attribute %v not found", attr)
}

@ -1,17 +1,17 @@
module github.com/redis/go-redis/extra/redisprometheus/v9
go 1.17
go 1.19
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/prometheus/client_golang v1.14.0
github.com/redis/go-redis/v9 v9.4.0
github.com/redis/go-redis/v9 v9.7.1
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
@ -19,5 +19,7 @@ require (
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
golang.org/x/sys v0.4.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)
retract v9.5.3 // This version was accidentally released.

@ -1,9 +1,9 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -29,5 +29,5 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=

@ -34,6 +34,7 @@ func libCodeWithConfig(libName string) string {
return fmt.Sprintf(lib, libName)
}
// TODO: Drop Gears
var _ = Describe("RedisGears commands", Label("gears"), func() {
ctx := context.TODO()
var client *redis.Client
@ -49,6 +50,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() {
})
It("should TFunctionLoad, TFunctionLoadArgs and TFunctionDelete ", Label("gears", "tfunctionload"), func() {
SkipAfterRedisVersion(7.4, "gears are not working in later versions")
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
@ -58,6 +60,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() {
Expect(resultAdd).To(BeEquivalentTo("OK"))
})
It("should TFunctionList", Label("gears", "tfunctionlist"), func() {
SkipAfterRedisVersion(7.4, "gears are not working in later versions")
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
@ -71,6 +74,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() {
})
It("should TFCall", Label("gears", "tfcall"), func() {
SkipAfterRedisVersion(7.4, "gears are not working in later versions")
var resultAdd interface{}
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
@ -81,6 +85,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() {
})
It("should TFCallArgs", Label("gears", "tfcallargs"), func() {
SkipAfterRedisVersion(7.4, "gears are not working in later versions")
var resultAdd interface{}
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
@ -92,6 +97,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() {
})
It("should TFCallASYNC", Label("gears", "TFCallASYNC"), func() {
SkipAfterRedisVersion(7.4, "gears are not working in later versions")
var resultAdd interface{}
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
@ -102,6 +108,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() {
})
It("should TFCallASYNCArgs", Label("gears", "TFCallASYNCargs"), func() {
SkipAfterRedisVersion(7.4, "gears are not working in later versions")
var resultAdd interface{}
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())

@ -19,6 +19,7 @@ type GenericCmdable interface {
Keys(ctx context.Context, pattern string) *StringSliceCmd
Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd
Move(ctx context.Context, key string, db int) *BoolCmd
ObjectFreq(ctx context.Context, key string) *IntCmd
ObjectRefCount(ctx context.Context, key string) *IntCmd
ObjectEncoding(ctx context.Context, key string) *StringCmd
ObjectIdleTime(ctx context.Context, key string) *DurationCmd
@ -159,6 +160,12 @@ func (c cmdable) Move(ctx context.Context, key string, db int) *BoolCmd {
return cmd
}
func (c cmdable) ObjectFreq(ctx context.Context, key string) *IntCmd {
cmd := NewIntCmd(ctx, "object", "freq", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ObjectRefCount(ctx context.Context, key string) *IntCmd {
cmd := NewIntCmd(ctx, "object", "refcount", key)
_ = c(ctx, cmd)

7
go.mod

@ -5,6 +5,11 @@ go 1.18
require (
github.com/bsm/ginkgo/v2 v2.12.0
github.com/bsm/gomega v1.27.10
github.com/cespare/xxhash/v2 v2.2.0
github.com/cespare/xxhash/v2 v2.3.0
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
)
retract (
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.
)

4
go.sum

@ -2,7 +2,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

@ -1,6 +1,9 @@
package redis
import "context"
import (
"context"
"time"
)
type HashCmdable interface {
HDel(ctx context.Context, key string, fields ...string) *IntCmd
@ -16,9 +19,24 @@ type HashCmdable interface {
HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd
HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd
HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
HVals(ctx context.Context, key string) *StringSliceCmd
HRandField(ctx context.Context, key string, count int) *StringSliceCmd
HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd
HStrLen(ctx context.Context, key, field string) *IntCmd
HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd
HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd
HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd
HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd
HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd
HExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd
HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd
HPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd
HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd
HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd
HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd
HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd
HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd
}
func (c cmdable) HDel(ctx context.Context, key string, fields ...string) *IntCmd {
@ -172,3 +190,267 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HStrLen(ctx context.Context, key, field string) *IntCmd {
cmd := NewIntCmd(ctx, "hstrlen", key, field)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
args := []interface{}{"hscan", key, cursor}
if match != "" {
args = append(args, "match", match)
}
if count > 0 {
args = append(args, "count", count)
}
args = append(args, "novalues")
cmd := NewScanCmd(ctx, c, args...)
_ = c(ctx, cmd)
return cmd
}
type HExpireArgs struct {
NX bool
XX bool
GT bool
LT bool
}
// HExpire - Sets the expiration time for specified fields in a hash in seconds.
// 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/
func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration), "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// 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/
func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration)}
// only if one argument is true, we can add it to the args
// if more than one argument is true, it will cause an error
if expirationArgs.NX {
args = append(args, "NX")
} else if expirationArgs.XX {
args = append(args, "XX")
} else if expirationArgs.GT {
args = append(args, "GT")
} else if expirationArgs.LT {
args = append(args, "LT")
}
args = append(args, "FIELDS", len(fields))
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPExpire - Sets the expiration time for specified fields in a hash in milliseconds.
// Similar to HExpire, it accepts a key, an expiration duration in milliseconds, a struct with expiration condition flags, and a list of fields.
// The command modifies the standard time.Duration to milliseconds for the Redis command.
// For more information - https://redis.io/commands/hpexpire/
func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration), "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration)}
// only if one argument is true, we can add it to the args
// if more than one argument is true, it will cause an error
if expirationArgs.NX {
args = append(args, "NX")
} else if expirationArgs.XX {
args = append(args, "XX")
} else if expirationArgs.GT {
args = append(args, "GT")
} else if expirationArgs.LT {
args = append(args, "LT")
}
args = append(args, "FIELDS", len(fields))
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in seconds.
// Takes a key, a UNIX timestamp, a struct of conditional flags, and a list of fields.
// The command sets absolute expiration times based on the UNIX timestamp provided.
// For more information - https://redis.io/commands/hexpireat/
func (c cmdable) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIREAT", key, tm.Unix(), "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIREAT", key, tm.Unix()}
// only if one argument is true, we can add it to the args
// if more than one argument is true, it will cause an error
if expirationArgs.NX {
args = append(args, "NX")
} else if expirationArgs.XX {
args = append(args, "XX")
} else if expirationArgs.GT {
args = append(args, "GT")
} else if expirationArgs.LT {
args = append(args, "LT")
}
args = append(args, "FIELDS", len(fields))
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in milliseconds.
// Similar to HExpireAt but for timestamps in milliseconds. It accepts the same parameters and adjusts the UNIX time to milliseconds.
// For more information - https://redis.io/commands/hpexpireat/
func (c cmdable) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond), "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond)}
// only if one argument is true, we can add it to the args
// if more than one argument is true, it will cause an error
if expirationArgs.NX {
args = append(args, "NX")
} else if expirationArgs.XX {
args = append(args, "XX")
} else if expirationArgs.GT {
args = append(args, "GT")
} else if expirationArgs.LT {
args = append(args, "LT")
}
args = append(args, "FIELDS", len(fields))
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPersist - Removes the expiration time from specified fields in a hash.
// Accepts a key and the fields themselves.
// This command ensures that each field specified will have its expiration removed if present.
// For more information - https://redis.io/commands/hpersist/
func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPERSIST", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in seconds.
// Requires a key and the fields themselves to fetch their expiration timestamps.
// This command returns the expiration times for each field or error/status codes for each field as specified.
// For more information - https://redis.io/commands/hexpiretime/
func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRETIME", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in milliseconds.
// Similar to HExpireTime, adjusted for timestamps in milliseconds. It requires the same parameters.
// Provides the expiration timestamp for each field in milliseconds.
// For more information - https://redis.io/commands/hexpiretime/
func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRETIME", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HTTL - Retrieves the remaining time to live for specified fields in a hash in seconds.
// Requires a key and the fields themselves. It returns the TTL for each specified field.
// This command fetches the TTL in seconds for each field or returns error/status codes as appropriate.
// For more information - https://redis.io/commands/httl/
func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HTTL", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPTTL - Retrieves the remaining time to live for specified fields in a hash in milliseconds.
// Similar to HTTL, but returns the TTL in milliseconds. It requires a key and the specified fields.
// This command provides the TTL in milliseconds for each field or returns error/status codes as needed.
// For more information - https://redis.io/commands/hpttl/
func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPTTL", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}

@ -168,9 +168,12 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
return nil, ErrClosed
}
p.connsMu.Lock()
if p.cfg.MaxActiveConns > 0 && p.poolSize >= p.cfg.MaxActiveConns {
p.connsMu.Unlock()
return nil, ErrPoolExhausted
}
p.connsMu.Unlock()
cn, err := p.dialConn(ctx, pooled)
if err != nil {
@ -180,6 +183,11 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
p.connsMu.Lock()
defer p.connsMu.Unlock()
if p.cfg.MaxActiveConns > 0 && p.poolSize >= p.cfg.MaxActiveConns {
_ = cn.Close()
return nil, ErrPoolExhausted
}
p.conns = append(p.conns, cn)
if pooled {
// If pool is full remove the cn on next Put.

@ -292,8 +292,8 @@ var _ = Describe("race", func() {
BeforeEach(func() {
C, N = 10, 1000
if testing.Short() {
C = 4
N = 100
C = 2
N = 50
}
})

@ -66,56 +66,95 @@ func (w *Writer) WriteArg(v interface{}) error {
case string:
return w.string(v)
case *string:
if v == nil {
return w.string("")
}
return w.string(*v)
case []byte:
return w.bytes(v)
case int:
return w.int(int64(v))
case *int:
if v == nil {
return w.int(0)
}
return w.int(int64(*v))
case int8:
return w.int(int64(v))
case *int8:
if v == nil {
return w.int(0)
}
return w.int(int64(*v))
case int16:
return w.int(int64(v))
case *int16:
if v == nil {
return w.int(0)
}
return w.int(int64(*v))
case int32:
return w.int(int64(v))
case *int32:
if v == nil {
return w.int(0)
}
return w.int(int64(*v))
case int64:
return w.int(v)
case *int64:
if v == nil {
return w.int(0)
}
return w.int(*v)
case uint:
return w.uint(uint64(v))
case *uint:
if v == nil {
return w.uint(0)
}
return w.uint(uint64(*v))
case uint8:
return w.uint(uint64(v))
case *uint8:
if v == nil {
return w.string("")
}
return w.uint(uint64(*v))
case uint16:
return w.uint(uint64(v))
case *uint16:
if v == nil {
return w.uint(0)
}
return w.uint(uint64(*v))
case uint32:
return w.uint(uint64(v))
case *uint32:
if v == nil {
return w.uint(0)
}
return w.uint(uint64(*v))
case uint64:
return w.uint(v)
case *uint64:
if v == nil {
return w.uint(0)
}
return w.uint(*v)
case float32:
return w.float(float64(v))
case *float32:
if v == nil {
return w.float(0)
}
return w.float(float64(*v))
case float64:
return w.float(v)
case *float64:
if v == nil {
return w.float(0)
}
return w.float(*v)
case bool:
if v {
@ -123,6 +162,9 @@ func (w *Writer) WriteArg(v interface{}) error {
}
return w.int(0)
case *bool:
if v == nil {
return w.int(0)
}
if *v {
return w.int(1)
}
@ -130,8 +172,19 @@ func (w *Writer) WriteArg(v interface{}) error {
case time.Time:
w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)
return w.bytes(w.numBuf)
case *time.Time:
if v == nil {
v = &time.Time{}
}
w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)
return w.bytes(w.numBuf)
case time.Duration:
return w.int(v.Nanoseconds())
case *time.Duration:
if v == nil {
return w.int(0)
}
return w.int(v.Nanoseconds())
case encoding.BinaryMarshaler:
b, err := v.MarshalBinary()
if err != nil {

@ -111,36 +111,61 @@ var _ = Describe("WriteArg", func() {
wr = proto.NewWriter(buf)
})
t := time.Date(2025, 2, 8, 00, 00, 00, 0, time.UTC)
args := map[any]string{
"hello": "$5\r\nhello\r\n",
int(10): "$2\r\n10\r\n",
util.ToPtr(int(10)): "$2\r\n10\r\n",
int8(10): "$2\r\n10\r\n",
util.ToPtr(int8(10)): "$2\r\n10\r\n",
int16(10): "$2\r\n10\r\n",
util.ToPtr(int16(10)): "$2\r\n10\r\n",
int32(10): "$2\r\n10\r\n",
util.ToPtr(int32(10)): "$2\r\n10\r\n",
int64(10): "$2\r\n10\r\n",
util.ToPtr(int64(10)): "$2\r\n10\r\n",
uint(10): "$2\r\n10\r\n",
util.ToPtr(uint(10)): "$2\r\n10\r\n",
uint8(10): "$2\r\n10\r\n",
util.ToPtr(uint8(10)): "$2\r\n10\r\n",
uint16(10): "$2\r\n10\r\n",
util.ToPtr(uint16(10)): "$2\r\n10\r\n",
uint32(10): "$2\r\n10\r\n",
util.ToPtr(uint32(10)): "$2\r\n10\r\n",
uint64(10): "$2\r\n10\r\n",
util.ToPtr(uint64(10)): "$2\r\n10\r\n",
float32(10.3): "$18\r\n10.300000190734863\r\n",
util.ToPtr(float32(10.3)): "$18\r\n10.300000190734863\r\n",
float64(10.3): "$4\r\n10.3\r\n",
util.ToPtr(float64(10.3)): "$4\r\n10.3\r\n",
bool(true): "$1\r\n1\r\n",
bool(false): "$1\r\n0\r\n",
util.ToPtr(bool(true)): "$1\r\n1\r\n",
util.ToPtr(bool(false)): "$1\r\n0\r\n",
"hello": "$5\r\nhello\r\n",
util.ToPtr("hello"): "$5\r\nhello\r\n",
(*string)(nil): "$0\r\n\r\n",
int(10): "$2\r\n10\r\n",
util.ToPtr(int(10)): "$2\r\n10\r\n",
(*int)(nil): "$1\r\n0\r\n",
int8(10): "$2\r\n10\r\n",
util.ToPtr(int8(10)): "$2\r\n10\r\n",
(*int8)(nil): "$1\r\n0\r\n",
int16(10): "$2\r\n10\r\n",
util.ToPtr(int16(10)): "$2\r\n10\r\n",
(*int16)(nil): "$1\r\n0\r\n",
int32(10): "$2\r\n10\r\n",
util.ToPtr(int32(10)): "$2\r\n10\r\n",
(*int32)(nil): "$1\r\n0\r\n",
int64(10): "$2\r\n10\r\n",
util.ToPtr(int64(10)): "$2\r\n10\r\n",
(*int64)(nil): "$1\r\n0\r\n",
uint(10): "$2\r\n10\r\n",
util.ToPtr(uint(10)): "$2\r\n10\r\n",
(*uint)(nil): "$1\r\n0\r\n",
uint8(10): "$2\r\n10\r\n",
util.ToPtr(uint8(10)): "$2\r\n10\r\n",
(*uint8)(nil): "$0\r\n\r\n",
uint16(10): "$2\r\n10\r\n",
util.ToPtr(uint16(10)): "$2\r\n10\r\n",
(*uint16)(nil): "$1\r\n0\r\n",
uint32(10): "$2\r\n10\r\n",
util.ToPtr(uint32(10)): "$2\r\n10\r\n",
(*uint32)(nil): "$1\r\n0\r\n",
uint64(10): "$2\r\n10\r\n",
util.ToPtr(uint64(10)): "$2\r\n10\r\n",
(*uint64)(nil): "$1\r\n0\r\n",
float32(10.3): "$18\r\n10.300000190734863\r\n",
util.ToPtr(float32(10.3)): "$18\r\n10.300000190734863\r\n",
(*float32)(nil): "$1\r\n0\r\n",
float64(10.3): "$4\r\n10.3\r\n",
util.ToPtr(float64(10.3)): "$4\r\n10.3\r\n",
(*float64)(nil): "$1\r\n0\r\n",
bool(true): "$1\r\n1\r\n",
bool(false): "$1\r\n0\r\n",
util.ToPtr(bool(true)): "$1\r\n1\r\n",
util.ToPtr(bool(false)): "$1\r\n0\r\n",
(*bool)(nil): "$1\r\n0\r\n",
time.Time(t): "$20\r\n2025-02-08T00:00:00Z\r\n",
util.ToPtr(time.Time(t)): "$20\r\n2025-02-08T00:00:00Z\r\n",
(*time.Time)(nil): "$20\r\n0001-01-01T00:00:00Z\r\n",
time.Duration(time.Second): "$10\r\n1000000000\r\n",
util.ToPtr(time.Duration(time.Second)): "$10\r\n1000000000\r\n",
(*time.Duration)(nil): "$1\r\n0\r\n",
(encoding.BinaryMarshaler)(&MyType{}): "$5\r\nhello\r\n",
(encoding.BinaryMarshaler)(nil): "$0\r\n\r\n",
}
for arg, expect := range args {

@ -2,6 +2,9 @@ package internal
import (
"context"
"net"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9/internal/util"
@ -44,3 +47,82 @@ func isLower(s string) bool {
}
return true
}
func ReplaceSpaces(s string) string {
// Pre-allocate a builder with the same length as s to minimize allocations.
// This is a basic optimization; adjust the initial size based on your use case.
var builder strings.Builder
builder.Grow(len(s))
for _, char := range s {
if char == ' ' {
// Replace space with a hyphen.
builder.WriteRune('-')
} else {
// Copy the character as-is.
builder.WriteRune(char)
}
}
return builder.String()
}
func GetAddr(addr string) string {
ind := strings.LastIndexByte(addr, ':')
if ind == -1 {
return ""
}
if strings.IndexByte(addr, '.') != -1 {
return addr
}
if addr[0] == '[' {
return addr
}
return net.JoinHostPort(addr[:ind], addr[ind+1:])
}
func ToInteger(val interface{}) int {
switch v := val.(type) {
case int:
return v
case int64:
return int(v)
case string:
i, _ := strconv.Atoi(v)
return i
default:
return 0
}
}
func ToFloat(val interface{}) float64 {
switch v := val.(type) {
case float64:
return v
case string:
f, _ := strconv.ParseFloat(v, 64)
return f
default:
return 0.0
}
}
func ToString(val interface{}) string {
if str, ok := val.(string); ok {
return str
}
return ""
}
func ToStringSlice(val interface{}) []string {
if arr, ok := val.([]interface{}); ok {
result := make([]string, len(arr))
for i, v := range arr {
result[i] = ToString(v)
}
return result
}
return nil
}

74
internal/util_test.go Normal file

@ -0,0 +1,74 @@
package internal
import (
"strings"
"testing"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
)
func BenchmarkToLowerStd(b *testing.B) {
str := "AaBbCcDdEeFfGgHhIiJjKk"
for i := 0; i < b.N; i++ {
_ = strings.ToLower(str)
}
}
// util.ToLower is 3x faster than strings.ToLower.
func BenchmarkToLowerInternal(b *testing.B) {
str := "AaBbCcDdEeFfGgHhIiJjKk"
for i := 0; i < b.N; i++ {
_ = ToLower(str)
}
}
func TestToLower(t *testing.T) {
It("toLower", func() {
str := "AaBbCcDdEeFfGg"
Expect(ToLower(str)).To(Equal(strings.ToLower(str)))
str = "ABCDE"
Expect(ToLower(str)).To(Equal(strings.ToLower(str)))
str = "ABCDE"
Expect(ToLower(str)).To(Equal(strings.ToLower(str)))
str = "abced"
Expect(ToLower(str)).To(Equal(strings.ToLower(str)))
})
}
func TestIsLower(t *testing.T) {
It("isLower", func() {
str := "AaBbCcDdEeFfGg"
Expect(isLower(str)).To(BeFalse())
str = "ABCDE"
Expect(isLower(str)).To(BeFalse())
str = "abcdefg"
Expect(isLower(str)).To(BeTrue())
})
}
func TestGetAddr(t *testing.T) {
It("getAddr", func() {
str := "127.0.0.1:1234"
Expect(GetAddr(str)).To(Equal(str))
str = "[::1]:1234"
Expect(GetAddr(str)).To(Equal(str))
str = "[fd01:abcd::7d03]:6379"
Expect(GetAddr(str)).To(Equal(str))
Expect(GetAddr("::1:1234")).To(Equal("[::1]:1234"))
Expect(GetAddr("fd01:abcd::7d03:6379")).To(Equal("[fd01:abcd::7d03]:6379"))
Expect(GetAddr("127.0.0.1")).To(Equal(""))
Expect(GetAddr("127")).To(Equal(""))
})
}

@ -85,6 +85,7 @@ var _ = Describe("ScanIterator", func() {
})
It("should hscan across multiple pages", func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
Expect(hashSeed(71)).NotTo(HaveOccurred())
var vals []string
@ -96,6 +97,23 @@ var _ = Describe("ScanIterator", func() {
Expect(vals).To(HaveLen(71 * 2))
Expect(vals).To(ContainElement("K01"))
Expect(vals).To(ContainElement("K71"))
Expect(vals).To(ContainElement("x"))
})
It("should hscan without values across multiple pages", Label("NonRedisEnterprise"), func() {
SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images")
Expect(hashSeed(71)).NotTo(HaveOccurred())
var vals []string
iter := client.HScanNoValues(ctx, hashKey, 0, "", 10).Iterator()
for iter.Next(ctx) {
vals = append(vals, iter.Val())
}
Expect(iter.Err()).NotTo(HaveOccurred())
Expect(vals).To(HaveLen(71))
Expect(vals).To(ContainElement("K01"))
Expect(vals).To(ContainElement("K71"))
Expect(vals).NotTo(ContainElement("x"))
})
It("should scan to page borders", func() {

Some files were not shown because too many files have changed in this diff Show More