Skip to content

Commit 37f7662

Browse files
uglidemgravell
andauthored
Add FT.ALIASLIST search command support (#518)
* Add FT.ALIASLIST search command support Add support for the FT.ALIASLIST command (Query Engine 8.10), which lists all aliases associated with a given index. - Add ALIASLIST constant and SearchCommandBuilder.AliasList(index) - Expose AliasList / AliasListAsync on ISearchCommands and ISearchCommandsAsync (returning RedisResult[], matching _List/TagVals) - Register the new public API surface in PublicAPI.Unshipped.txt - Add sync and async integration tests (guarded to Redis 8.10+) * Clean up tests --------- Co-authored-by: Marc Gravell <marc.gravell@gmail.com>
1 parent 62e2d19 commit 37f7662

8 files changed

Lines changed: 121 additions & 18 deletions

File tree

‎src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
#nullable enable
2+
NRedisStack.ISearchCommands.AliasList(string! index) -> StackExchange.Redis.RedisResult![]!
3+
NRedisStack.ISearchCommandsAsync.AliasListAsync(string! index) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisResult![]!>!
4+
NRedisStack.SearchCommands.AliasList(string! index) -> StackExchange.Redis.RedisResult![]!
5+
NRedisStack.SearchCommandsAsync.AliasListAsync(string! index) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisResult![]!>!
6+
static NRedisStack.SearchCommandBuilder.AliasList(string! index) -> NRedisStack.RedisStackCommands.SerializedCommand!
27
NRedisStack.Search.Aggregation.CollectReducer
38
override NRedisStack.Search.Aggregation.CollectReducer.Name.get -> string!
49
NRedisStack.Search.Aggregation.CollectReducer.Fields(params string![]! fields) -> NRedisStack.Search.Aggregation.CollectReducer!

‎src/NRedisStack/Search/ISearchCommands.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ public interface ISearchCommands
6262
/// <remarks><seealso href="https://redis.io/commands/ft.aliasdel"/></remarks>
6363
bool AliasUpdate(string alias, string index);
6464

65+
/// <summary>
66+
/// List all aliases associated with the given index.
67+
/// </summary>
68+
/// <param name="index">The index name. The index must be created using FT.CREATE; an alias name is not accepted.</param>
69+
/// <returns>Array with the alias names associated with the index (unordered; empty when the index has no aliases).</returns>
70+
/// <remarks><seealso href="https://redis.io/commands/ft.aliaslist"/></remarks>
71+
RedisResult[] AliasList(string index);
72+
6573
/// <summary>
6674
/// Add a new attribute to the index
6775
/// </summary>

‎src/NRedisStack/Search/ISearchCommandsAsync.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ public interface ISearchCommandsAsync
6363
/// <remarks><seealso href="https://redis.io/commands/ft.aliasdel"/></remarks>
6464
Task<bool> AliasUpdateAsync(string alias, string index);
6565

66+
/// <summary>
67+
/// List all aliases associated with the given index.
68+
/// </summary>
69+
/// <param name="index">The index name. The index must be created using FT.CREATE; an alias name is not accepted.</param>
70+
/// <returns>Array with the alias names associated with the index (unordered; empty when the index has no aliases).</returns>
71+
/// <remarks><seealso href="https://redis.io/commands/ft.aliaslist"/></remarks>
72+
Task<RedisResult[]> AliasListAsync(string index);
73+
6674
/// <summary>
6775
/// Add a new attribute to the index
6876
/// </summary>

‎src/NRedisStack/Search/Literals/Commands.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ internal class FT
77
public const string ALIASADD = "FT.ALIASADD";
88
public const string ALIASDEL = "FT.ALIASDEL";
99
public const string ALIASUPDATE = "FT.ALIASUPDATE";
10+
public const string ALIASLIST = "FT.ALIASLIST";
1011
public const string ALTER = "FT.ALTER";
1112
public const string CONFIG = "FT.CONFIG";
1213
public const string CREATE = "FT.CREATE";

‎src/NRedisStack/Search/SearchCommandBuilder.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public static SerializedCommand AliasUpdate(string alias, string index)
3333
{
3434
return new(FT.ALIASUPDATE, alias, index);
3535
}
36+
37+
public static SerializedCommand AliasList(string index)
38+
{
39+
return new(FT.ALIASLIST, index);
40+
}
3641
public static SerializedCommand Alter(string index, Schema schema, bool skipInitialScan = false)
3742
{
3843
List<object> args = [index];

‎src/NRedisStack/Search/SearchCommands.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ public bool AliasUpdate(string alias, string index)
8787
return db.Execute(SearchCommandBuilder.AliasUpdate(alias, index)).OKtoBoolean();
8888
}
8989

90+
/// <inheritdoc/>
91+
public RedisResult[] AliasList(string index)
92+
{
93+
return db.Execute(SearchCommandBuilder.AliasList(index)).ToArray();
94+
}
95+
9096
/// <inheritdoc/>
9197
public bool Alter(string index, Schema schema, bool skipInitialScan = false)
9298
{

‎src/NRedisStack/Search/SearchCommandsAsync.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ public async Task<bool> AliasUpdateAsync(string alias, string index)
129129
return (await _db.ExecuteAsync(SearchCommandBuilder.AliasUpdate(alias, index))).OKtoBoolean();
130130
}
131131

132+
/// <inheritdoc/>
133+
public async Task<RedisResult[]> AliasListAsync(string index)
134+
{
135+
return (await _db.ExecuteAsync(SearchCommandBuilder.AliasList(index))).ToArray();
136+
}
137+
132138
/// <inheritdoc/>
133139
public async Task<bool> AlterAsync(string index, Schema schema, bool skipInitialScan = false)
134140
{

‎tests/NRedisStack.Tests/Search/SearchTests.cs‎

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,34 @@ private static void SkipClusterPre8(string endpointId)
3737
&& EndpointsFixture.RedisVersion.Minor < 4, "Ignoring cluster tests for FT.SEARCH pre Redis 8.4");
3838
}
3939

40+
// Legacy cluster servers reject alias commands wholesale with CROSSSLOT; the first
41+
// FT.ALIASADD acts as an environment probe, so skip the test when it fails that way.
42+
private static void AliasAddOrSkipOnCrossSlot(SearchCommands ft, string alias, string index)
43+
{
44+
try
45+
{
46+
Assert.True(ft.AliasAdd(alias, index));
47+
}
48+
catch (RedisServerException rse)
49+
{
50+
Assert.SkipWhen(rse.Message.StartsWith("CROSSSLOT"), "legacy failure");
51+
throw;
52+
}
53+
}
54+
55+
private static async Task AliasAddOrSkipOnCrossSlotAsync(SearchCommands ft, string alias, string index)
56+
{
57+
try
58+
{
59+
Assert.True(await ft.AliasAddAsync(alias, index));
60+
}
61+
catch (RedisServerException rse)
62+
{
63+
Assert.SkipWhen(rse.Message.StartsWith("CROSSSLOT"), "legacy failure");
64+
throw;
65+
}
66+
}
67+
4068
private void AddDocument(IDatabase db, Document doc)
4169
{
4270
var hash = doc.GetProperties()
@@ -568,15 +596,7 @@ public void TestAlias(string endpointId)
568596
doc.Add("field1", "value");
569597
AddDocument(db, "doc1", doc);
570598

571-
try
572-
{
573-
Assert.True(ft.AliasAdd("ALIAS1", index));
574-
}
575-
catch (RedisServerException rse)
576-
{
577-
Assert.SkipWhen(rse.Message.StartsWith("CROSSSLOT"), "legacy failure");
578-
throw;
579-
}
599+
AliasAddOrSkipOnCrossSlot(ft, "ALIAS1", index);
580600

581601
SearchResult res1 = ft.Search("ALIAS1", new Query("*").ReturnFields("field1"));
582602
Assert.Equal(1, res1.TotalResults);
@@ -607,15 +627,7 @@ public async Task TestAliasAsync(string endpointId)
607627
doc.Add("field1", "value");
608628
AddDocument(db, "doc1", doc);
609629

610-
try
611-
{
612-
Assert.True(await ft.AliasAddAsync("ALIAS1", index));
613-
}
614-
catch (RedisServerException rse)
615-
{
616-
Assert.SkipWhen(rse.Message.StartsWith("CROSSSLOT"), "legacy failure");
617-
throw;
618-
}
630+
await AliasAddOrSkipOnCrossSlotAsync(ft, "ALIAS1", index);
619631

620632
SearchResult res1 = ft.Search("ALIAS1", new Query("*").ReturnFields("field1"));
621633
Assert.Equal(1, res1.TotalResults);
@@ -631,6 +643,58 @@ public async Task TestAliasAsync(string endpointId)
631643
await Assert.ThrowsAsync<RedisServerException>(async () => await ft.AliasDelAsync("ALIAS2"));
632644
}
633645

646+
[SkipIfRedisTheory(Comparison.LessThan, "8.10.0")]
647+
[MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))]
648+
public void TestAliasList(string endpointId)
649+
{
650+
IDatabase db = GetCleanDatabase(endpointId);
651+
var ft = db.FT();
652+
Assert.True(ft.Create(index, FTCreateParams.CreateParams(), new Schema().AddTextField("field1")));
653+
654+
// Ordering is not part of the contract, so compare sorted.
655+
string[] Aliases() => ft.AliasList(index).Select(a => a.ToString()).OrderBy(a => a).ToArray();
656+
657+
// An existing index with no aliases returns an empty collection, not an error.
658+
Assert.Empty(ft.AliasList(index));
659+
660+
AliasAddOrSkipOnCrossSlot(ft, "ALIAS1", index);
661+
Assert.Equal(new[] { "ALIAS1" }, Aliases());
662+
663+
Assert.True(ft.AliasAdd("ALIAS2", index));
664+
Assert.Equal(new[] { "ALIAS1", "ALIAS2" }, Aliases());
665+
666+
Assert.True(ft.AliasDel("ALIAS1"));
667+
Assert.Equal(new[] { "ALIAS2" }, Aliases());
668+
669+
// A non-existent index propagates the server error unchanged.
670+
Assert.Throws<RedisServerException>(() => ft.AliasList("nonexisting-index"));
671+
}
672+
673+
[SkipIfRedisTheory(Comparison.LessThan, "8.10.0")]
674+
[MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))]
675+
public async Task TestAliasListAsync(string endpointId)
676+
{
677+
IDatabase db = GetCleanDatabase(endpointId);
678+
var ft = db.FT();
679+
Assert.True(ft.Create(index, FTCreateParams.CreateParams(), new Schema().AddTextField("field1")));
680+
681+
// Ordering is not part of the contract, so compare sorted.
682+
async Task<string[]> Aliases() => (await ft.AliasListAsync(index)).Select(a => a.ToString()).OrderBy(a => a).ToArray();
683+
684+
Assert.Empty(await ft.AliasListAsync(index));
685+
686+
await AliasAddOrSkipOnCrossSlotAsync(ft, "ALIAS1", index);
687+
Assert.Equal(new[] { "ALIAS1" }, await Aliases());
688+
689+
Assert.True(await ft.AliasAddAsync("ALIAS2", index));
690+
Assert.Equal(new[] { "ALIAS1", "ALIAS2" }, await Aliases());
691+
692+
Assert.True(await ft.AliasDelAsync("ALIAS1"));
693+
Assert.Equal(new[] { "ALIAS2" }, await Aliases());
694+
695+
await Assert.ThrowsAsync<RedisServerException>(async () => await ft.AliasListAsync("nonexisting-index"));
696+
}
697+
634698
[Theory]
635699
[MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))]
636700
public void TestApplyAndFilterAggregations(string endpointId)

0 commit comments

Comments
 (0)