Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/Config/DatabasePrimitives/DatabaseObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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?.ToUpperInvariant(),
Name?.ToUpperInvariant());
}

/// <summary>
Expand Down
92 changes: 92 additions & 0 deletions src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,98 @@ string relationshipEntity
configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object);
}

/// <summary>
/// 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.
/// </summary>
[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<string, Entity> 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, "", 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<ILogger<RuntimeConfigValidator>>().Object);
Mock<ISqlMetadataProvider> _sqlMetadataProvider = new();

Dictionary<string, DatabaseObject> 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<string>(), It.IsAny<string>(), out discard)).Returns(true);

Mock<IMetadataProviderFactory> _metadataProviderFactory = new();
_metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny<string>())).Returns(_sqlMetadataProvider.Object);

// Mock ForeignKeyPair to be defined in DB with the custom schema
// The schema comparison should be case-insensitive
_sqlMetadataProvider.Setup(x =>
x.VerifyForeignKeyExistsInDB(
It.Is<DatabaseTable>(t => string.Equals(t.SchemaName, expectedSchema, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Name, expectedTable, StringComparison.OrdinalIgnoreCase)),
It.Is<DatabaseTable>(t => string.Equals(t.SchemaName, expectedSchema, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Name, "TEST_SOURCE1", StringComparison.OrdinalIgnoreCase))
)).Returns(true);

_sqlMetadataProvider.Setup(x =>
x.VerifyForeignKeyExistsInDB(
It.Is<DatabaseTable>(t => string.Equals(t.SchemaName, expectedSchema, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Name, expectedTable, StringComparison.OrdinalIgnoreCase)),
It.Is<DatabaseTable>(t => string.Equals(t.SchemaName, expectedSchema, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Name, "TEST_SOURCE2", StringComparison.OrdinalIgnoreCase))
)).Returns(true);

// Validation should pass with custom schema
configValidator.ValidateRelationships(runtimeConfig, _metadataProviderFactory.Object);
}

/// <summary>
/// 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
Expand Down