diff --git a/src/EFCore.Sqlite.Core/Diagnostics/Internal/SqliteLoggingDefinitions.cs b/src/EFCore.Sqlite.Core/Diagnostics/Internal/SqliteLoggingDefinitions.cs index dcca3a8e86c..04a2a0a7977 100644 --- a/src/EFCore.Sqlite.Core/Diagnostics/Internal/SqliteLoggingDefinitions.cs +++ b/src/EFCore.Sqlite.Core/Diagnostics/Internal/SqliteLoggingDefinitions.cs @@ -162,4 +162,12 @@ public class SqliteLoggingDefinitions : RelationalLoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public EventDefinitionBase? LogFormatWarning; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public EventDefinitionBase? LogExpressionIndexSkipped; } diff --git a/src/EFCore.Sqlite.Core/Diagnostics/SqliteEventId.cs b/src/EFCore.Sqlite.Core/Diagnostics/SqliteEventId.cs index cbbbc989107..966b6aae76b 100644 --- a/src/EFCore.Sqlite.Core/Diagnostics/SqliteEventId.cs +++ b/src/EFCore.Sqlite.Core/Diagnostics/SqliteEventId.cs @@ -51,7 +51,8 @@ private enum Id UniqueConstraintFound = CoreEventId.ProviderDesignBaseId + 9, InferringTypes = CoreEventId.ProviderDesignBaseId + 10, OutOfRangeWarning = CoreEventId.ProviderDesignBaseId + 11, - FormatWarning = CoreEventId.ProviderDesignBaseId + 12 + FormatWarning = CoreEventId.ProviderDesignBaseId + 12, + ExpressionIndexSkippedWarning = CoreEventId.ProviderDesignBaseId + 13 } private static readonly string ValidationPrefix = DbLoggerCategory.Model.Validation.Name + "."; @@ -257,4 +258,12 @@ private static EventId MakeScaffoldingId(Id id) /// This event is in the category. /// public static readonly EventId FormatWarning = MakeScaffoldingId(Id.FormatWarning); + + /// + /// An index defined on an expression was skipped because indexes on expressions are not supported. + /// + /// + /// This event is in the category. + /// + public static readonly EventId ExpressionIndexSkippedWarning = MakeScaffoldingId(Id.ExpressionIndexSkippedWarning); } diff --git a/src/EFCore.Sqlite.Core/EFCore.Sqlite.Core.baseline.json b/src/EFCore.Sqlite.Core/EFCore.Sqlite.Core.baseline.json index 2b11cf805e2..25093d61fea 100644 --- a/src/EFCore.Sqlite.Core/EFCore.Sqlite.Core.baseline.json +++ b/src/EFCore.Sqlite.Core/EFCore.Sqlite.Core.baseline.json @@ -205,6 +205,9 @@ { "Member": "static readonly Microsoft.Extensions.Logging.EventId ConflictingValueGenerationStrategiesWarning" }, + { + "Member": "static readonly Microsoft.Extensions.Logging.EventId ExpressionIndexSkippedWarning" + }, { "Member": "static readonly Microsoft.Extensions.Logging.EventId ForeignKeyFound" }, diff --git a/src/EFCore.Sqlite.Core/Extensions/Internal/SqliteLoggerExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/Internal/SqliteLoggerExtensions.cs index dd1125ed14b..8b67598dfb9 100644 --- a/src/EFCore.Sqlite.Core/Extensions/Internal/SqliteLoggerExtensions.cs +++ b/src/EFCore.Sqlite.Core/Extensions/Internal/SqliteLoggerExtensions.cs @@ -522,4 +522,25 @@ public static void FormatWarning( // No DiagnosticsSource events because these are purely design-time messages } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static void ExpressionIndexSkippedWarning( + this IDiagnosticsLogger diagnostics, + string? indexName, + string? tableName) + { + var definition = SqliteResources.LogExpressionIndexSkipped(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, indexName, tableName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } } diff --git a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs index dcd4c1e7ddb..efd800819d0 100644 --- a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs +++ b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs @@ -207,6 +207,31 @@ public static EventDefinition LogConflictingValu return (EventDefinition)definition; } + /// + /// Skipping index '{indexName}' on table '{tableName}', since it is defined on an expression and indexes on expressions are not supported. + /// + public static EventDefinition LogExpressionIndexSkipped(IDiagnosticsLogger logger) + { + var definition = ((Diagnostics.Internal.SqliteLoggingDefinitions)logger.Definitions).LogExpressionIndexSkipped; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((Diagnostics.Internal.SqliteLoggingDefinitions)logger.Definitions).LogExpressionIndexSkipped, + logger, + static logger => new EventDefinition( + logger.Options, + SqliteEventId.ExpressionIndexSkippedWarning, + LogLevel.Warning, + "SqliteEventId.ExpressionIndexSkippedWarning", + level => LoggerMessage.Define( + level, + SqliteEventId.ExpressionIndexSkippedWarning, + _resourceManager.GetString("LogExpressionIndexSkipped")!))); + } + + return (EventDefinition)definition; + } + /// /// Skipping foreign key with identity '{id}' on table '{tableName}' since principal table '{principalTableName}' was not found in the model. This usually happens when the principal table was not included in the selection set. /// diff --git a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx index eac5a4b9fda..7bfef86fcee 100644 --- a/src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx +++ b/src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx @@ -155,6 +155,10 @@ Both the SqliteValueGenerationStrategy '{generationStrategy}' and '{otherGenerationStrategy}' have been set on property '{propertyName}' on entity type '{entityName}'. Configuring two strategies is usually unintentional and will likely result in a database error. Warning SqliteEventId.ConflictingValueGenerationStrategiesWarning string string string string + + Skipping index '{indexName}' on table '{tableName}', since it is defined on an expression and indexes on expressions are not supported. + Warning SqliteEventId.ExpressionIndexSkippedWarning string? string? + Skipping foreign key with identity '{id}' on table '{tableName}' since principal table '{principalTableName}' was not found in the model. This usually happens when the principal table was not included in the selection set. Warning SqliteEventId.ForeignKeyReferencesMissingTableWarning string? string? string? diff --git a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs index 6ed2892dd23..21bf488a227 100644 --- a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs +++ b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs @@ -1062,6 +1062,7 @@ ORDER BY "seq" _logger.IndexFound(index.Name, table.Name, index.IsUnique); + var isExpressionIndex = false; using (var command2 = connection.CreateCommand()) { command2.CommandText = @@ -1080,6 +1081,13 @@ ORDER BY "seqno" using var reader2 = command2.ExecuteReader(); while (reader2.Read()) { + if (reader2.IsDBNull(0)) + { + // The key is an expression rather than a column, which can't be represented in the model + isExpressionIndex = true; + break; + } + var name = reader2.GetString(0); var column = table.Columns.FirstOrDefault(c => c.Name == name) ?? table.Columns.FirstOrDefault(c => c.Name.Equals(name, StringComparison.Ordinal)); @@ -1090,6 +1098,12 @@ ORDER BY "seqno" } } + if (isExpressionIndex) + { + _logger.ExpressionIndexSkippedWarning(index.Name, table.Name); + continue; + } + table.Indexes.Add(index); } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs index 5e5f33e71c0..80ca60bf868 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/SqliteDatabaseModelFactoryTest.cs @@ -1364,6 +1364,138 @@ Id2 text }, "DROP TABLE UniqueIndex;"); + [Fact] + public void Skip_index_on_expression_with_warning() + => Test( + @" +CREATE TABLE ExpressionIndexTable ( + Id integer PRIMARY KEY, + Name text +); + +CREATE INDEX IX_LOWER_NAME on ExpressionIndexTable (lower(Name)); +CREATE INDEX IX_NAME on ExpressionIndexTable (Name);", + [], + [], + dbModel => + { + var index = Assert.Single(dbModel.Tables.Single().Indexes); + Assert.Equal("IX_NAME", index.Name); + Assert.Equal(["Name"], index.Columns.Select(ic => ic.Name).ToList()); + + var (_, id, message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); + + var definition = SqliteResources.LogExpressionIndexSkipped(new TestLogger()); + Assert.Equal(definition.EventId, id); + Assert.Equal(definition.GenerateMessage("IX_LOWER_NAME", "ExpressionIndexTable"), message); + }, + "DROP TABLE ExpressionIndexTable;"); + + [Fact] + public void Skip_index_mixing_columns_and_expressions_with_warning() + => Test( + @" +CREATE TABLE MixedIndexTable ( + Id integer PRIMARY KEY, + Name text +); + +CREATE UNIQUE INDEX IX_MIXED on MixedIndexTable (Name, (Name || 'x'));", + [], + [], + dbModel => + { + Assert.Empty(dbModel.Tables.Single().Indexes); + + var (level, id, _, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); + Assert.Equal(LogLevel.Warning, level); + Assert.Equal(SqliteEventId.ExpressionIndexSkippedWarning, id); + }, + "DROP TABLE MixedIndexTable;"); + + [Fact] + public void Skip_only_the_index_on_expression_and_keep_the_other_indexes() + => Test( + @" +CREATE TABLE SeveralIndexesTable ( + Id integer PRIMARY KEY, + Name text +); + +CREATE INDEX IX_NAME on SeveralIndexesTable (Name); +CREATE INDEX IX_EXPR on SeveralIndexesTable (lower(Name)); +CREATE INDEX IX_OTHER on SeveralIndexesTable (Id, Name);", + [], + [], + dbModel => + { + Assert.Equal(["IX_NAME", "IX_OTHER"], dbModel.Tables.Single().Indexes.Select(i => i.Name).Order().ToList()); + + var (_, id, message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); + var definition = SqliteResources.LogExpressionIndexSkipped(new TestLogger()); + Assert.Equal(definition.EventId, id); + Assert.Equal(definition.GenerateMessage("IX_EXPR", "SeveralIndexesTable"), message); + }, + "DROP TABLE SeveralIndexesTable;"); + + [Fact] + public void Skip_all_indexes_on_expressions_with_a_warning_for_each() + => Test( + @" +CREATE TABLE OnlyExpressionIndexesTable ( + Id integer PRIMARY KEY, + Name text, + Amount int +); + +CREATE INDEX ""IX_i'q"" on OnlyExpressionIndexesTable (lower(Name)); +CREATE UNIQUE INDEX IX_ABS on OnlyExpressionIndexesTable (abs(Amount));", + [], + [], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + Assert.Empty(table.Indexes); + + var definition = SqliteResources.LogExpressionIndexSkipped(new TestLogger()); + var messages = Fixture.ListLoggerFactory.Log + .Where(t => t.Level == LogLevel.Warning) + .Select(t => t.Message!) + .Order() + .ToList(); + + Assert.Equal( + new[] + { + definition.GenerateMessage("IX_ABS", "OnlyExpressionIndexesTable"), + definition.GenerateMessage("IX_i'q", "OnlyExpressionIndexesTable") + }.Order().ToList(), + messages); + }, + "DROP TABLE OnlyExpressionIndexesTable;"); + + [Fact] + public void Skip_index_on_expression_but_keep_unique_constraint() + => Test( + @" +CREATE TABLE ConstraintAndExpressionTable ( + Id integer PRIMARY KEY, + Code text, + UNIQUE (Code) +); + +CREATE INDEX IX_EXPR on ConstraintAndExpressionTable (lower(Code));", + [], + [], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + Assert.Empty(table.Indexes); + Assert.Equal(["Code"], Assert.Single(table.UniqueConstraints).Columns.Select(c => c.Name).ToList()); + Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); + }, + "DROP TABLE ConstraintAndExpressionTable;"); + #endregion #region ForeignKeyFacets