From f61772dace28d53f7ff35fe5e1b502dc8757b6a8 Mon Sep 17 00:00:00 2001 From: James Abbott Date: Mon, 2 Sep 2024 10:26:13 +0100 Subject: [PATCH 1/3] feat: Add the ability to compare existing index definitions to the projected one for a type. --- README.md | 15 ++++ src/Redis.OM/Modeling/RedisIndex.cs | 68 ++++++++++++++++++- .../RediSearchTests/RedisIndexTests.cs | 59 ++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 59e2daf8..7ae193f5 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,21 @@ var provider = new RedisConnectionProvider("redis://localhost:6379"); provider.Connection.CreateIndex(typeof(Customer)); ``` +Redis OM provides limited support for schema and data migration at this time. We provide a small extension method `IndexDefinitionEquals` on the `RedisIndexInfo` type that you may opt in to use 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 (definition.IndexDefinitionEquals(typeof(Customer)) == false) +{ + 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: diff --git a/src/Redis.OM/Modeling/RedisIndex.cs b/src/Redis.OM/Modeling/RedisIndex.cs index db1e8560..459f36fc 100644 --- a/src/Redis.OM/Modeling/RedisIndex.cs +++ b/src/Redis.OM/Modeling/RedisIndex.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Redis.OM; using Redis.OM.Modeling; @@ -8,8 +9,73 @@ namespace Redis.OM.Modeling /// /// A utility class for serializing objects into Redis Indices. /// - internal static class RedisIndex + public static class RedisIndex { + /// + /// Verifies whether the given index schema definition matches the current definition. + /// + /// The index definition. + /// The type to be indexed. + /// 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.. + 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(); + + if (a.Identifier == null) + { + return Array.Empty(); + } + + 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)); + } + /// /// Pull out the Document attribute from a Type. /// diff --git a/test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs b/test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs index 2da6b10d..925099ed 100644 --- a/test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs +++ b/test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Redis.OM.Contracts; using Redis.OM.Modeling; using Xunit; @@ -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 { @@ -198,5 +227,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))); + } } } From b53819761a73f6ee9fbadde47f6cba22bd44e83f Mon Sep 17 00:00:00 2001 From: James Abbott Date: Mon, 2 Sep 2024 10:38:03 +0100 Subject: [PATCH 2/3] fix(ci): docker-compose should now be docker compose (v2) --- .github/workflows/dotnet-core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index d1fa608e..7da3bb4b 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -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 \ No newline at end of file + run: docker compose -f ./docker/docker-compose.yaml run dotnet \ No newline at end of file From d5688360881508e7d66beeff97c45de44cd746dd Mon Sep 17 00:00:00 2001 From: slorello89 Date: Fri, 4 Oct 2024 09:13:39 -0400 Subject: [PATCH 3/3] adding `IsIndexCurrent` and tests --- README.md | 7 ++--- src/Redis.OM/RediSearchCommands.cs | 26 ++++++++++++++++- .../RediSearchTests/RedisIndexTests.cs | 28 +++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7ae193f5..45bb0e65 100644 --- a/README.md +++ b/README.md @@ -107,18 +107,17 @@ var provider = new RedisConnectionProvider("redis://localhost:6379"); provider.Connection.CreateIndex(typeof(Customer)); ``` -Redis OM provides limited support for schema and data migration at this time. We provide a small extension method `IndexDefinitionEquals` on the `RedisIndexInfo` type that you may opt in to use to determine when to re-create your indexes when your types change. An example implementation of this would look like: +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 (definition.IndexDefinitionEquals(typeof(Customer)) == false) +if (!provider.Connection.IsIndexCurrent(typeof(Customer))) { provider.Connection.DropIndex(typeof(Customer)); + provider.Connection.CreateIndex(typeof(Customer)); } - -provider.Connection.CreateIndex(typeof(Customer)); ``` diff --git a/src/Redis.OM/RediSearchCommands.cs b/src/Redis.OM/RediSearchCommands.cs index fad794ff..6d4d21d8 100644 --- a/src/Redis.OM/RediSearchCommands.cs +++ b/src/Redis.OM/RediSearchCommands.cs @@ -178,7 +178,7 @@ public static async Task 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; } @@ -268,6 +268,30 @@ public static bool DropIndexAndAssociatedRecords(this IRedisConnection connectio } } + /// + /// Check if the index exists and is up to date. + /// + /// The connection. + /// The type the index was built from. + /// Whether or not the index is current. + public static bool IsIndexCurrent(this IRedisConnection connection, Type type) + { + var definition = connection.GetIndexInfo(type); + return definition?.IndexDefinitionEquals(type) ?? false; + } + + /// + /// Check if the index exists and is up to date. + /// + /// The connection. + /// The type the index was built from. + /// Whether or not the index is current. + public static async Task IsIndexCurrentAsync(this IRedisConnection connection, Type type) + { + var definition = await connection.GetIndexInfoAsync(type).ConfigureAwait(false); + return definition?.IndexDefinitionEquals(type) ?? false; + } + /// /// Search redis with the given query. /// diff --git a/test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs b/test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs index 925099ed..fc1b7785 100644 --- a/test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs +++ b/test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs @@ -217,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() {