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