diff --git a/src/EFCore.Jet/Diagnostics/Internal/JetLoggingDefinitions.cs b/src/EFCore.Jet/Diagnostics/Internal/JetLoggingDefinitions.cs index 6af230d..e0ac7fe 100644 --- a/src/EFCore.Jet/Diagnostics/Internal/JetLoggingDefinitions.cs +++ b/src/EFCore.Jet/Diagnostics/Internal/JetLoggingDefinitions.cs @@ -116,6 +116,14 @@ namespace EntityFrameworkCore.Jet.Diagnostics.Internal /// public EventDefinitionBase LogFoundIndex; + /// + /// 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 LogSkippedIndex; + /// /// 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 diff --git a/src/EFCore.Jet/Diagnostics/JetEventId.cs b/src/EFCore.Jet/Diagnostics/JetEventId.cs index 4d74275..10c939e 100644 --- a/src/EFCore.Jet/Diagnostics/JetEventId.cs +++ b/src/EFCore.Jet/Diagnostics/JetEventId.cs @@ -62,7 +62,8 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics IndexFound, ForeignKeyFound, ForeignKeyPrincipalColumnMissingWarning, - ReflexiveConstraintIgnored + ReflexiveConstraintIgnored, + IndexSkipped, } private static readonly string _validationPrefix = DbLoggerCategory.Model.Validation.Name + "."; @@ -212,6 +213,12 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// public static readonly EventId IndexFound = MakeScaffoldingId(Id.IndexFound); + /// + /// An index was skipped. + /// This event is in the category. + /// + public static readonly EventId IndexSkipped = MakeScaffoldingId(Id.IndexSkipped); + /// /// A foreign key was found. /// This event is in the category. diff --git a/src/EFCore.Jet/Internal/JetLoggerExtensions.cs b/src/EFCore.Jet/Internal/JetLoggerExtensions.cs index 120d277..8bc598b 100644 --- a/src/EFCore.Jet/Internal/JetLoggerExtensions.cs +++ b/src/EFCore.Jet/Internal/JetLoggerExtensions.cs @@ -307,6 +307,28 @@ namespace EntityFrameworkCore.Jet.Internal // 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 IndexSkipped( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] string indexName, + [NotNull] string tableName, + bool unique) + { + var definition = JetResources.LogFoundIndex(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, indexName, tableName, unique); + } + + // 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 diff --git a/src/EFCore.Jet/Properties/JetStrings.Designer.cs b/src/EFCore.Jet/Properties/JetStrings.Designer.cs index 128284b..60aab8c 100644 --- a/src/EFCore.Jet/Properties/JetStrings.Designer.cs +++ b/src/EFCore.Jet/Properties/JetStrings.Designer.cs @@ -592,7 +592,30 @@ namespace EntityFrameworkCore.Jet.Internal return (EventDefinition)definition; } + + /// + /// Skipped index with name: {indexName}, table: {tableName}, is unique: {isUnique}. + /// + public static EventDefinition LogSkippedIndex([NotNull] IDiagnosticsLogger logger) + { + var definition = ((Diagnostics.Internal.JetLoggingDefinitions)logger.Definitions).LogSkippedIndex; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((Diagnostics.Internal.JetLoggingDefinitions)logger.Definitions).LogSkippedIndex, + () => new EventDefinition( + logger.Options, + JetEventId.IndexSkipped, + LogLevel.Debug, + "JetEventId.SkippedIndex", + level => LoggerMessage.Define( + level, + JetEventId.IndexFound, + _resourceManager.GetString("LogSkippedIndex")))); + } + return (EventDefinition)definition; + } /// /// Found primary key with name: {primaryKeyName}, table: {tableName}. /// diff --git a/src/EFCore.Jet/Properties/JetStrings.resx b/src/EFCore.Jet/Properties/JetStrings.resx index 546f9d1..2635ebc 100644 --- a/src/EFCore.Jet/Properties/JetStrings.resx +++ b/src/EFCore.Jet/Properties/JetStrings.resx @@ -207,6 +207,10 @@ Found index with name: {indexName}, table: {tableName}, is unique: {isUnique}. Debug JetEventId.IndexFound string string bool + + Skipped index with name: {indexName}, table: {tableName}, is unique: {isUnique} as the name is equal to an existing foreign key and would result in a runtime error. + Debug JetEventId.IndexSkipped string string bool + Found primary key with name: {primaryKeyName}, table: {tableName}. Debug JetEventId.PrimaryKeyFound string string diff --git a/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs b/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs index 0b07bec..2c424c8 100644 --- a/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs +++ b/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs @@ -168,8 +168,8 @@ namespace EntityFrameworkCore.Jet.Scaffolding.Internal if (tables.Count > 0) { GetColumns(connection, tables); - GetIndexes(connection, tables); GetRelations(connection, tables); + GetIndexes(connection, tables); } return tables; @@ -372,35 +372,52 @@ namespace EntityFrameworkCore.Jet.Scaffolding.Internal table.PrimaryKey = primaryKey; indexOrKey = primaryKey; } - else if (indexType == "UNIQUE" && - !nullable) + else { - var uniqueConstraint = new DatabaseUniqueConstraint + var isUnique = indexType == "UNIQUE"; + + if (isUnique && + !nullable) { - Table = table, - Name = indexName, - }; + var uniqueConstraint = new DatabaseUniqueConstraint + { + Table = table, + Name = indexName, + }; - _logger.UniqueConstraintFound(indexName, tableName); + _logger.UniqueConstraintFound(indexName, tableName); - table.UniqueConstraints.Add(uniqueConstraint); - indexOrKey = uniqueConstraint; - } - else - { - var index = new DatabaseIndex + table.UniqueConstraints.Add(uniqueConstraint); + indexOrKey = uniqueConstraint; + } + else { - Table = table, - Name = indexName, - IsUnique = indexType == "UNIQUE", - }; + // In contrast to SQL Standard, MS Access will implicitly create an index for every FK + // constraint. + // According to https://docs.microsoft.com/en-us/office/client-developer/access/desktop-database-reference/constraint-clause-microsoft-access-sql, + // this behavior can be disabled, but manuall creating an index with the same name as an + // FK would still results in a runtime error. + // We therefore skip indexes with the same name as existing FKs. + if (table.ForeignKeys.Any(fk => fk.Name == indexName)) + { + _logger.IndexSkipped(indexName, tableName, isUnique); + continue; + } + + var index = new DatabaseIndex + { + Table = table, + Name = indexName, + IsUnique = isUnique, + }; - _logger.IndexFound(indexName, tableName, index.IsUnique); + _logger.IndexFound(indexName, tableName, index.IsUnique); - table.Indexes.Add(index); - indexOrKey = index; + table.Indexes.Add(index); + indexOrKey = index; + } } - + foreach (var indexColumn in indexColumns) { var columnName = indexColumn.GetValueOrDefault("COLUMN_NAME"); diff --git a/test/EFCore.Jet.FunctionalTests/Scaffolding/JetDatabaseModelFactoryTest.cs b/test/EFCore.Jet.FunctionalTests/Scaffolding/JetDatabaseModelFactoryTest.cs index 1a7805b..02fa1da 100644 --- a/test/EFCore.Jet.FunctionalTests/Scaffolding/JetDatabaseModelFactoryTest.cs +++ b/test/EFCore.Jet.FunctionalTests/Scaffolding/JetDatabaseModelFactoryTest.cs @@ -1475,7 +1475,46 @@ CREATE TABLE DependentTable ( DROP TABLE DependentTable; DROP TABLE PrincipalTable;"); } + + /// + /// In contrast to SQL Standard, MS Access will implicitly create an index for every FK constraint. + /// According to https://docs.microsoft.com/en-us/office/client-developer/access/desktop-database-reference/constraint-clause-microsoft-access-sql, + /// this behavior can be disabled, but manuall creating an index with the same name as an FK would still results + /// in a runtime error. + /// We therefore skip indexes with the same name as existing FKs. + /// + [ConditionalFact] + public void EnsureNoForeignKeyIndexCreationOperationsAreCreated() + { + Test( + @" +CREATE TABLE PrincipalTable ( + Id int PRIMARY KEY +); +CREATE TABLE DependentTable ( + Id int PRIMARY KEY, + IndexColumn int, + ForeignKeyId int, + CONSTRAINT MYFK FOREIGN KEY (ForeignKeyId) REFERENCES PrincipalTable(Id) ON DELETE CASCADE +); + CREATE INDEX `IX_DependentTable_IndexColumn` ON `DependentTable` (`IndexColumn`); +", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var table = dbModel.Tables.Single(t => t.Name == "DependentTable"); + + Assert.Equal(1, table.ForeignKeys.Count); + Assert.Equal(1, table.Indexes.Count); + Assert.False(table.Indexes.Any(i => i.Name == "MYFK")); + }, + @" +DROP TABLE DependentTable; +DROP TABLE PrincipalTable;"); + } + [ConditionalFact] public void Set_referential_action_for_foreign_key() {