diff --git a/src/Config/DatabasePrimitives/DatabaseObject.cs b/src/Config/DatabasePrimitives/DatabaseObject.cs index 8636e8c005..be1eff45ba 100644 --- a/src/Config/DatabasePrimitives/DatabaseObject.cs +++ b/src/Config/DatabasePrimitives/DatabaseObject.cs @@ -43,13 +43,15 @@ public override bool Equals(object? other) public bool Equals(DatabaseObject? other) { return other is not null && - SchemaName.Equals(other.SchemaName) && - Name.Equals(other.Name); + string.Equals(SchemaName, other.SchemaName, StringComparison.OrdinalIgnoreCase) && + string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); } public override int GetHashCode() { - return HashCode.Combine(SchemaName, Name); + return HashCode.Combine( + SchemaName is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(SchemaName), + Name is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(Name)); } /// diff --git a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs index a2ba7924d7..05561e4cf9 100644 --- a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs @@ -437,6 +437,115 @@ string relationshipEntity configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object); } + /// + /// Test method to verify that many-to-many relationships work correctly when the linking object + /// is in a custom schema (not dbo). This test validates that schema names are correctly compared + /// using case-insensitive comparison, which is important for SQL Server where schema names are + /// case-insensitive. + /// + [DataRow("mySchema.TEST_SOURCE_LINK", "mySchema", "TEST_SOURCE_LINK", DisplayName = "Linking object with custom schema")] + [DataRow("MYSCHEMA.TEST_SOURCE_LINK", "MYSCHEMA", "TEST_SOURCE_LINK", DisplayName = "Linking object with uppercase custom schema")] + [DataRow("myschema.test_source_link", "myschema", "test_source_link", DisplayName = "Linking object with lowercase schema and table")] + [DataTestMethod] + public void TestRelationshipWithLinkingObjectInCustomSchema( + string linkingObject, + string expectedSchema, + string expectedTable + ) + { + // Creating an EntityMap with two sample entities + Dictionary entityMap = GetSampleEntityMap( + sourceEntity: "SampleEntity1", + targetEntity: "SampleEntity2", + sourceFields: new string[] { "sourceField" }, + targetFields: new string[] { "targetField" }, + linkingObject: linkingObject, + linkingSourceFields: new string[] { "linkingSourceField" }, + linkingTargetFields: new string[] { "linkingTargetField" } + ); + + RuntimeConfig runtimeConfig = new( + Schema: "UnitTestSchema", + DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, ConnectionString: "", Options: null), + Runtime: new( + Rest: new(), + GraphQL: new(), + Mcp: new(), + Host: new(null, null) + ), + Entities: new(entityMap) + ); + + // Mocking EntityToDatabaseObject - entities are in the custom schema as well + MockFileSystem fileSystem = new(); + FileSystemRuntimeConfigLoader loader = new(fileSystem); + RuntimeConfigProvider provider = new(loader) { IsLateConfigured = true }; + RuntimeConfigValidator configValidator = new(provider, fileSystem, new Mock>().Object); + Mock _sqlMetadataProvider = new(); + + Dictionary mockDictionaryForEntityDatabaseObject = new() + { + { + "SampleEntity1", + new DatabaseTable(expectedSchema, "TEST_SOURCE1") + }, + { + "SampleEntity2", + new DatabaseTable(expectedSchema, "TEST_SOURCE2") + } + }; + + _sqlMetadataProvider.Setup(x => x.EntityToDatabaseObject).Returns(mockDictionaryForEntityDatabaseObject); + + // To mock the schema name and dbObjectName for linkingObject + _sqlMetadataProvider.Setup(x => + x.ParseSchemaAndDbTableName(linkingObject)).Returns((expectedSchema, expectedTable)); + + string discard; + _sqlMetadataProvider.Setup(x => x.TryGetExposedColumnName(It.IsAny(), It.IsAny(), out discard)).Returns(true); + + Mock _metadataProviderFactory = new(); + _metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny())).Returns(_sqlMetadataProvider.Object); + + // Mock ForeignKeyPair to be defined in DB with the custom schema + // The schema comparison should be case-insensitive + // Use concrete DatabaseTable instances with differing casing so that + // Moq relies on DatabaseTable.Equals for argument matching. + // Linking table uses lowercase to ensure case-insensitive comparison is working. + DatabaseTable expectedLinkingTable = new(expectedSchema.ToLowerInvariant(), expectedTable.ToLowerInvariant()); + DatabaseTable expectedSource1Table = new(expectedSchema.ToUpperInvariant(), "TEST_SOURCE1"); + DatabaseTable expectedSource2Table = new(expectedSchema.ToUpperInvariant(), "TEST_SOURCE2"); + + _sqlMetadataProvider.Setup(x => + x.VerifyForeignKeyExistsInDB( + expectedLinkingTable, + expectedSource1Table + )).Returns(true); + + _sqlMetadataProvider.Setup(x => + x.VerifyForeignKeyExistsInDB( + expectedLinkingTable, + expectedSource2Table + )).Returns(true); + + // Validation should pass with custom schema + configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object); + + // Verify that VerifyForeignKeyExistsInDB is never called with 'dbo' schema, + // guarding against the original dbo-fallback regression. + _sqlMetadataProvider.Verify(x => + x.VerifyForeignKeyExistsInDB( + It.Is(t => string.Equals(t.SchemaName, "dbo", StringComparison.OrdinalIgnoreCase)), + It.IsAny() + ), Times.Never); + + _sqlMetadataProvider.Verify(x => + x.VerifyForeignKeyExistsInDB( + It.IsAny(), + It.Is(t => string.Equals(t.SchemaName, "dbo", StringComparison.OrdinalIgnoreCase)) + ), Times.Never); + } + /// /// Test method to check that an exception is thrown when the relationship is one-many /// or many-one (determined by the linking object being null), while both SourceFields