Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ NRedisStack.Search.SearchResult.Warnings.get -> string![]!
NRedisStack.Search.AggregationResult.Warnings.get -> string![]!
NRedisStack.Search.HybridSearchResult.Warnings.get -> string![]!
NRedisStack.Search.AggregationRequest.AddScores() -> NRedisStack.Search.AggregationRequest!
NRedisStack.ISearchCommands.AliasList(string! index) -> StackExchange.Redis.RedisResult![]!
NRedisStack.ISearchCommandsAsync.AliasListAsync(string! index) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisResult![]!>!
NRedisStack.SearchCommands.AliasList(string! index) -> StackExchange.Redis.RedisResult![]!
NRedisStack.SearchCommandsAsync.AliasListAsync(string! index) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisResult![]!>!
static NRedisStack.SearchCommandBuilder.AliasList(string! index) -> NRedisStack.RedisStackCommands.SerializedCommand!
8 changes: 8 additions & 0 deletions src/NRedisStack/Search/ISearchCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ public interface ISearchCommands
/// <remarks><seealso href="https://redis.io/commands/ft.aliasdel"/></remarks>
bool AliasUpdate(string alias, string index);

/// <summary>
/// List all aliases associated with the given index.
/// </summary>
/// <param name="index">The index name. The index must be created using FT.CREATE; an alias name is not accepted.</param>
/// <returns>Array with the alias names associated with the index (unordered; empty when the index has no aliases).</returns>
/// <remarks><seealso href="https://redis.io/commands/ft.aliaslist"/></remarks>
RedisResult[] AliasList(string index);

/// <summary>
/// Add a new attribute to the index
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/NRedisStack/Search/ISearchCommandsAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ public interface ISearchCommandsAsync
/// <remarks><seealso href="https://redis.io/commands/ft.aliasdel"/></remarks>
Task<bool> AliasUpdateAsync(string alias, string index);

/// <summary>
/// List all aliases associated with the given index.
/// </summary>
/// <param name="index">The index name. The index must be created using FT.CREATE; an alias name is not accepted.</param>
/// <returns>Array with the alias names associated with the index (unordered; empty when the index has no aliases).</returns>
/// <remarks><seealso href="https://redis.io/commands/ft.aliaslist"/></remarks>
Task<RedisResult[]> AliasListAsync(string index);

/// <summary>
/// Add a new attribute to the index
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/NRedisStack/Search/Literals/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ internal class FT
public const string ALIASADD = "FT.ALIASADD";
public const string ALIASDEL = "FT.ALIASDEL";
public const string ALIASUPDATE = "FT.ALIASUPDATE";
public const string ALIASLIST = "FT.ALIASLIST";
public const string ALTER = "FT.ALTER";
public const string CONFIG = "FT.CONFIG";
public const string CREATE = "FT.CREATE";
Expand Down
5 changes: 5 additions & 0 deletions src/NRedisStack/Search/SearchCommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public static SerializedCommand AliasUpdate(string alias, string index)
{
return new(FT.ALIASUPDATE, alias, index);
}

public static SerializedCommand AliasList(string index)
{
return new(FT.ALIASLIST, index);
}
public static SerializedCommand Alter(string index, Schema schema, bool skipInitialScan = false)
{
List<object> args = [index];
Expand Down
6 changes: 6 additions & 0 deletions src/NRedisStack/Search/SearchCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ public bool AliasUpdate(string alias, string index)
return db.Execute(SearchCommandBuilder.AliasUpdate(alias, index)).OKtoBoolean();
}

/// <inheritdoc/>
public RedisResult[] AliasList(string index)
{
return db.Execute(SearchCommandBuilder.AliasList(index)).ToArray();
}

/// <inheritdoc/>
public bool Alter(string index, Schema schema, bool skipInitialScan = false)
{
Expand Down
6 changes: 6 additions & 0 deletions src/NRedisStack/Search/SearchCommandsAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ public async Task<bool> AliasUpdateAsync(string alias, string index)
return (await _db.ExecuteAsync(SearchCommandBuilder.AliasUpdate(alias, index))).OKtoBoolean();
}

/// <inheritdoc/>
public async Task<RedisResult[]> AliasListAsync(string index)
{
return (await _db.ExecuteAsync(SearchCommandBuilder.AliasList(index))).ToArray();
}

/// <inheritdoc/>
public async Task<bool> AlterAsync(string index, Schema schema, bool skipInitialScan = false)
{
Expand Down
100 changes: 82 additions & 18 deletions tests/NRedisStack.Tests/Search/SearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ private static void SkipClusterPre8(string endpointId)
&& EndpointsFixture.RedisVersion.Minor < 4, "Ignoring cluster tests for FT.SEARCH pre Redis 8.4");
}

// Legacy cluster servers reject alias commands wholesale with CROSSSLOT; the first
// FT.ALIASADD acts as an environment probe, so skip the test when it fails that way.
private static void AliasAddOrSkipOnCrossSlot(SearchCommands ft, string alias, string index)
{
try
{
Assert.True(ft.AliasAdd(alias, index));
}
catch (RedisServerException rse)
{
Assert.SkipWhen(rse.Message.StartsWith("CROSSSLOT"), "legacy failure");
throw;
}
}

private static async Task AliasAddOrSkipOnCrossSlotAsync(SearchCommands ft, string alias, string index)
{
try
{
Assert.True(await ft.AliasAddAsync(alias, index));
}
catch (RedisServerException rse)
{
Assert.SkipWhen(rse.Message.StartsWith("CROSSSLOT"), "legacy failure");
throw;
}
}

private void AddDocument(IDatabase db, Document doc)
{
var hash = doc.GetProperties()
Expand Down Expand Up @@ -501,15 +529,7 @@ public void TestAlias(string endpointId)
doc.Add("field1", "value");
AddDocument(db, "doc1", doc);

try
{
Assert.True(ft.AliasAdd("ALIAS1", index));
}
catch (RedisServerException rse)
{
Assert.SkipWhen(rse.Message.StartsWith("CROSSSLOT"), "legacy failure");
throw;
}
AliasAddOrSkipOnCrossSlot(ft, "ALIAS1", index);

SearchResult res1 = ft.Search("ALIAS1", new Query("*").ReturnFields("field1"));
Assert.Equal(1, res1.TotalResults);
Expand Down Expand Up @@ -540,15 +560,7 @@ public async Task TestAliasAsync(string endpointId)
doc.Add("field1", "value");
AddDocument(db, "doc1", doc);

try
{
Assert.True(await ft.AliasAddAsync("ALIAS1", index));
}
catch (RedisServerException rse)
{
Assert.SkipWhen(rse.Message.StartsWith("CROSSSLOT"), "legacy failure");
throw;
}
await AliasAddOrSkipOnCrossSlotAsync(ft, "ALIAS1", index);

SearchResult res1 = ft.Search("ALIAS1", new Query("*").ReturnFields("field1"));
Assert.Equal(1, res1.TotalResults);
Expand All @@ -564,6 +576,58 @@ public async Task TestAliasAsync(string endpointId)
await Assert.ThrowsAsync<RedisServerException>(async () => await ft.AliasDelAsync("ALIAS2"));
}

[SkipIfRedisTheory(Comparison.LessThan, "8.10.0")]
[MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))]
public void TestAliasList(string endpointId)
{
IDatabase db = GetCleanDatabase(endpointId);
var ft = db.FT();
Assert.True(ft.Create(index, FTCreateParams.CreateParams(), new Schema().AddTextField("field1")));

// Ordering is not part of the contract, so compare sorted.
string[] Aliases() => ft.AliasList(index).Select(a => a.ToString()).OrderBy(a => a).ToArray();

// An existing index with no aliases returns an empty collection, not an error.
Assert.Empty(ft.AliasList(index));

AliasAddOrSkipOnCrossSlot(ft, "ALIAS1", index);
Assert.Equal(new[] { "ALIAS1" }, Aliases());

Assert.True(ft.AliasAdd("ALIAS2", index));
Assert.Equal(new[] { "ALIAS1", "ALIAS2" }, Aliases());

Assert.True(ft.AliasDel("ALIAS1"));
Assert.Equal(new[] { "ALIAS2" }, Aliases());

// A non-existent index propagates the server error unchanged.
Assert.Throws<RedisServerException>(() => ft.AliasList("nonexisting-index"));
}

[SkipIfRedisTheory(Comparison.LessThan, "8.10.0")]
[MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))]
public async Task TestAliasListAsync(string endpointId)
{
IDatabase db = GetCleanDatabase(endpointId);
var ft = db.FT();
Assert.True(ft.Create(index, FTCreateParams.CreateParams(), new Schema().AddTextField("field1")));

// Ordering is not part of the contract, so compare sorted.
async Task<string[]> Aliases() => (await ft.AliasListAsync(index)).Select(a => a.ToString()).OrderBy(a => a).ToArray();

Assert.Empty(await ft.AliasListAsync(index));

await AliasAddOrSkipOnCrossSlotAsync(ft, "ALIAS1", index);
Assert.Equal(new[] { "ALIAS1" }, await Aliases());

Assert.True(await ft.AliasAddAsync("ALIAS2", index));
Assert.Equal(new[] { "ALIAS1", "ALIAS2" }, await Aliases());

Assert.True(await ft.AliasDelAsync("ALIAS1"));
Assert.Equal(new[] { "ALIAS2" }, await Aliases());

await Assert.ThrowsAsync<RedisServerException>(async () => await ft.AliasListAsync("nonexisting-index"));
}

[Theory]
[MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))]
public void TestApplyAndFilterAggregations(string endpointId)
Expand Down
Loading