Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add the ability to compare existing index definitions to the projected one for a type #479

Merged
merged 3 commits into from
Oct 4, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/dotnet-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ jobs:
- name: fetch-models
run: sh fetch-models.sh
- name: execute
run: docker-compose -f ./docker/docker-compose.yaml run dotnet
run: docker compose -f ./docker/docker-compose.yaml run dotnet
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ var provider = new RedisConnectionProvider("redis://localhost:6379");
provider.Connection.CreateIndex(typeof(Customer));
```

Redis OM provides limited support for schema migration at this time. You can check if the index definition in Redis matches your current index definition using the `IsIndexCurrent` method on the `RedisConnection`. Then you may use that output to determine when to re-create your indexes when your types change. An example implementation of this would look like:

```csharp
var provider = new RedisConnectionProvider("redis://localhost:6379");
var definition = provider.Connection.GetIndexInfo(typeof(Customer));

if (!provider.Connection.IsIndexCurrent(typeof(Customer)))
{
provider.Connection.DropIndex(typeof(Customer));
provider.Connection.CreateIndex(typeof(Customer));
}
```


### Indexing Embedded Documents

There are two methods for indexing embedded documents with Redis.OM, an embedded document is a complex object, e.g. if our `Customer` model had an `Address` property with the following model:
Expand Down
68 changes: 67 additions & 1 deletion src/Redis.OM/Modeling/RedisIndex.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Redis.OM;
using Redis.OM.Modeling;

Expand All @@ -8,8 +9,73 @@ namespace Redis.OM.Modeling
/// <summary>
/// A utility class for serializing objects into Redis Indices.
/// </summary>
internal static class RedisIndex
public static class RedisIndex
{
/// <summary>
/// Verifies whether the given index schema definition matches the current definition.
/// </summary>
/// <param name="redisIndexInfo">The index definition.</param>
/// <param name="type">The type to be indexed.</param>
/// <returns>A bool indicating whether the current index definition has drifted from the current definition, which may be used to determine when to re-create an index..</returns>
public static bool IndexDefinitionEquals(this RedisIndexInfo redisIndexInfo, Type type)
{
var serialisedDefinition = SerializeIndex(type);
var existingSet = redisIndexInfo.Attributes?.Select(a => (Property: a.Attribute!, a.Type!)).OrderBy(a => a.Property);
var isJson = redisIndexInfo.IndexDefinition?.Identifier == "JSON";

if (serialisedDefinition.Length < 5)
{
throw new ArgumentException($"Could not parse the index definition for type: {type.Name}.");
}

if (redisIndexInfo.IndexName != serialisedDefinition[0])
{
return false;
}

if (redisIndexInfo.IndexDefinition?.Identifier?.Equals(serialisedDefinition[2], StringComparison.OrdinalIgnoreCase) == false)
{
return false;
}

if (redisIndexInfo.IndexDefinition?.Prefixes.FirstOrDefault().Equals(serialisedDefinition[5]) == false)
{
return false;
}

var target = redisIndexInfo.Attributes?.SelectMany(a =>
{
var attr = new List<string>();

if (a.Identifier == null)
{
return Array.Empty<string>();
}

if (isJson)
{
attr.Add(a.Identifier);
attr.Add("AS");
}

attr.Add(a.Attribute!);

if (a.Type != null)
{
attr.Add(a.Type);
}

if (a.Sortable == true)
{
attr.Add("SORTABLE");
}

return attr.ToArray();
});

return target.SequenceEqual(serialisedDefinition.Skip(7));
}

/// <summary>
/// Pull out the Document attribute from a Type.
/// </summary>
Expand Down
26 changes: 25 additions & 1 deletion src/Redis.OM/RediSearchCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public static async Task<bool> CreateIndexAsync(this IRedisConnection connection
try
{
var indexName = type.SerializeIndex().First();
var redisReply = await connection.ExecuteAsync("FT.INFO", indexName);
var redisReply = await connection.ExecuteAsync("FT.INFO", indexName).ConfigureAwait(false);
var redisIndexInfo = new RedisIndexInfo(redisReply);
return redisIndexInfo;
}
Expand Down Expand Up @@ -268,6 +268,30 @@ public static bool DropIndexAndAssociatedRecords(this IRedisConnection connectio
}
}

/// <summary>
/// Check if the index exists and is up to date.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="type">The type the index was built from.</param>
/// <returns>Whether or not the index is current.</returns>
public static bool IsIndexCurrent(this IRedisConnection connection, Type type)
{
var definition = connection.GetIndexInfo(type);
return definition?.IndexDefinitionEquals(type) ?? false;
}

/// <summary>
/// Check if the index exists and is up to date.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="type">The type the index was built from.</param>
/// <returns>Whether or not the index is current.</returns>
public static async Task<bool> IsIndexCurrentAsync(this IRedisConnection connection, Type type)
{
var definition = await connection.GetIndexInfoAsync(type).ConfigureAwait(false);
return definition?.IndexDefinitionEquals(type) ?? false;
}

/// <summary>
/// Search redis with the given query.
/// </summary>
Expand Down
87 changes: 87 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Redis.OM.Contracts;
using Redis.OM.Modeling;
using Xunit;

Expand All @@ -20,6 +21,34 @@ public class TestPersonClassHappyPath
public string[] NickNames { get; set; }
}

[Document(IndexName = "TestPersonClassHappyPath-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Hash)]
public class TestPersonClassHappyPathWithMutatedDefinition
{
public string Name { get; set; }
[Indexed(Sortable = true)]
public int Age { get; set; }
public double Height { get; set; }
}

[Document(IndexName = "SerialisedJson-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Json)]
public class SerialisedJsonType
{
[Searchable(Sortable = true)]
public string Name { get; set; }

public int Age { get; set; }
}

[Document(IndexName = "SerialisedJson-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Json)]
public class SerialisedJsonTypeNotMatch
{
[Searchable(Sortable = true)]
public string Name { get; set; }

[Indexed(Sortable = true)]
public int Age { get; set; }
}

[Document(IndexName = "TestPersonClassHappyPath-idx", StorageType = StorageType.Hash, Prefixes = new []{"Person:"})]
public class TestPersonClassOverridenPrefix
{
Expand Down Expand Up @@ -188,6 +217,34 @@ public void TestGetIndexInfoWhichDoesNotExist()
Assert.Null(indexInfo);
}

[Fact]
public void TestCheckIndexUpToDate()
{
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
var provider = new RedisConnectionProvider($"redis://{host}");
var connection = provider.Connection;
connection.DropIndex(typeof(SerialisedJsonType));
Assert.False(connection.IsIndexCurrent(typeof(SerialisedJsonType)));

connection.CreateIndex(typeof(SerialisedJsonType));
Assert.False(connection.IsIndexCurrent(typeof(SerialisedJsonTypeNotMatch)));
Assert.True(connection.IsIndexCurrent(typeof(SerialisedJsonType)));
}

[Fact]
public async Task TestCheckIndexUpToDateAsync()
{
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
var provider = new RedisConnectionProvider($"redis://{host}");
var connection = provider.Connection;
await connection.DropIndexAsync(typeof(SerialisedJsonType));
Assert.False(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonType)));

await connection.CreateIndexAsync(typeof(SerialisedJsonType));
Assert.False(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonTypeNotMatch)));
Assert.True(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonType)));
}

[Fact]
public async Task TestGetIndexInfoWhichDoesNotExistAsync()
{
Expand All @@ -198,5 +255,35 @@ public async Task TestGetIndexInfoWhichDoesNotExistAsync()
var indexInfo = await connection.GetIndexInfoAsync(typeof(TestPersonClassHappyPath));
Assert.Null(indexInfo);
}

[Fact]
public async Task TestGetIndexInfoWhichDoesNotMatchExisting()
{
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
var provider = new RedisConnectionProvider($"redis://{host}");
var connection = provider.Connection;

await connection.DropIndexAsync(typeof(TestPersonClassHappyPath));
await connection.CreateIndexAsync(typeof(TestPersonClassHappyPath));
var indexInfo = await connection.GetIndexInfoAsync(typeof(TestPersonClassHappyPath));

Assert.False(indexInfo.IndexDefinitionEquals(typeof(TestPersonClassHappyPathWithMutatedDefinition)));
Assert.True(indexInfo.IndexDefinitionEquals(typeof(TestPersonClassHappyPath)));
}

[Fact]
public async Task TestGetIndexInfoWhichDoesNotMatchExistingJson()
{
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
var provider = new RedisConnectionProvider($"redis://{host}");
var connection = provider.Connection;

await connection.DropIndexAsync(typeof(SerialisedJsonType));
await connection.CreateIndexAsync(typeof(SerialisedJsonType));
var indexInfo = await connection.GetIndexInfoAsync(typeof(SerialisedJsonType));

Assert.False(indexInfo.IndexDefinitionEquals(typeof(SerialisedJsonTypeNotMatch)));
Assert.True(indexInfo.IndexDefinitionEquals(typeof(SerialisedJsonType)));
}
}
}
Loading