Files
go-redis/search_aliaslist_integration_test.go
Nedyalko Dyakovandofekshenawa 279615082b feat(search): add FT.ALIASLIST support (#3925)
* feat(search): add FT.ALIASLIST support (Redis 8.10)

- FTAliasList(ctx, index) returns the aliases of an index as an
  unordered, server-deduplicated collection; an alias-free index
  yields an empty result, not an error
- server errors (SEARCH_INDEX_NOT_FOUND, NOPERM) propagate unchanged;
  the argument must be an index name, aliases are not resolved
- wire-format unit test plus a RESP2/RESP3 integration test that gates
  on the live server supporting the command (the REDIS_VERSION float
  cannot tell 8.10 from 8.1)
- runnable example under example/ft-aliaslist

* fix(search): route FT.ALIASLIST as keyless in cluster

The static FT policy table covers aliasadd/aliasupdate/aliasdel but
not aliaslist, so the default cluster resolver returned nil and a cold
command-info cache fell back to treating the index name as a key.
Register it keyless and read-only like the other alias commands.

* test(search): version-gate FT.ALIASLIST specs

Replace the standalone probe test (unknown-command sniffing plus the
REDIS_ALIASLIST_TEST_ADDR env var) with Ginkgo specs in the main suite,
gated on SkipBeforeRedisVersion("8.10"), which compares major.minor as
integers and so distinguishes 8.10 from 8.1. The specs cover RESP2 and
RESP3 and follow the suite conventions (FlushDB per spec, :6379).

* chore(deps): bump grpc to v1.82.1 in otel examples

govulncheck fails every PR on GO-2026-6061 in google.golang.org/grpc
v1.80.0 (indirect via the OTLP exporters); v1.82.1 carries the fix.

* fix(search): route policy-table keyless commands on cold cache

ClusterClient.process computes the slot before routeAndRun consults
the policy table, and with a cold command-info cache
cmdFirstKeyPosWithInfo fell back to treating argument 1 as a key — so
FT.ALIASLIST (and the other keyless FT commands) hashed the index name
into a slot for the first calls of a process. Consult the static
policy table there: entries with default request routing and a keyless
response policy resolve to keyless. Key-carrying (ft.suglen) and
specially-routed (ft.cursor) commands keep the old fallback.

---------

Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com>
2026-07-28 19:14:28 +03:00

103 lines
3.2 KiB
Go

package redis_test
import (
"context"
"fmt"
"strings"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
"github.com/redis/go-redis/v9"
)
// FT.ALIASLIST ships in Redis 8.10 (Query Engine 8.10.0). The specs gate on
// SkipBeforeRedisVersion, which compares major.minor as integers, and run
// under both RESP2 and RESP3 against the suite's standard server.
var _ = Describe("FT.ALIASLIST", Label("search", "ftaliaslist"), func() {
ctx := context.TODO()
for _, protocol := range []int{2, 3} {
protocol := protocol
Describe(fmt.Sprintf("RESP%d", protocol), func() {
var client *redis.Client
index := fmt.Sprintf("aliaslist-test-idx-resp%d", protocol)
aliases := []string{
fmt.Sprintf("aliaslist-test-a1-resp%d", protocol),
fmt.Sprintf("aliaslist-test-a2-resp%d", protocol),
}
createIndex := func() {
Expect(client.FTCreate(ctx, index, &redis.FTCreateOptions{
OnHash: true,
Prefix: []interface{}{fmt.Sprintf("aliaslist-test-doc-resp%d:", protocol)},
}, &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}).Err()).NotTo(HaveOccurred())
}
BeforeEach(func() {
SkipBeforeRedisVersion("8.10", "FT.ALIASLIST requires Redis 8.10+")
client = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: protocol})
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
})
AfterEach(func() {
if client == nil {
return
}
// Aliases are global, not per-database; FLUSHDB alone does not
// remove them unless their index is dropped with them.
for _, a := range aliases {
_ = client.FTAliasDel(ctx, a).Err()
}
_ = client.FTDropIndex(ctx, index).Err()
Expect(client.Close()).NotTo(HaveOccurred())
client = nil
})
It("returns an empty collection for an index with no aliases", func() {
createIndex()
// An existing index with no aliases yields an empty
// collection, not an error.
got, err := client.FTAliasList(ctx, index).Result()
Expect(err).NotTo(HaveOccurred())
Expect(got).To(BeEmpty())
})
It("lists all aliases associated with the index", func() {
createIndex()
for _, a := range aliases {
Expect(client.FTAliasAdd(ctx, index, a).Err()).NotTo(HaveOccurred())
}
// The reply is an unordered collection; do not assert order.
got, err := client.FTAliasList(ctx, index).Result()
Expect(err).NotTo(HaveOccurred())
Expect(got).To(ConsistOf(aliases[0], aliases[1]))
})
It("propagates the server index-not-found error for a missing index", func() {
err := client.FTAliasList(ctx, fmt.Sprintf("aliaslist-test-missing-resp%d", protocol)).Err()
Expect(err).To(HaveOccurred())
Expect(strings.ToLower(err.Error())).To(ContainSubstring("index not found"))
})
It("does not resolve an alias passed in place of the index", func() {
createIndex()
Expect(client.FTAliasAdd(ctx, index, aliases[0]).Err()).NotTo(HaveOccurred())
// The argument must be the index name; the server does not
// resolve aliases here and replies with the same
// index-not-found error a missing index produces.
err := client.FTAliasList(ctx, aliases[0]).Err()
Expect(err).To(HaveOccurred())
Expect(strings.ToLower(err.Error())).To(ContainSubstring("index not found"))
})
})
}
})