Fix many-to-many relationship queries with custom schema linking objects#3106
Fix many-to-many relationship queries with custom schema linking objects#3106
Conversation
|
@copilot why is the PR not published yet? |
The PR is published and ready for review. The branch |
|
@copilot the PR is still in draft state. |
I don't have the ability to change the PR from draft to ready for review. This requires marking the PR as "Ready for review" in the GitHub UI. A maintainer or the PR author would need to click the "Ready for review" button on the PR page to publish it. |
There was a problem hiding this comment.
Pull request overview
Fixes SQL Server GraphQL many-to-many relationship failures when the linking object is in a non-dbo schema by making DatabaseObject equality/hash behavior case-insensitive so FK lookups succeed despite casing differences between config and database metadata.
Changes:
- Update
DatabaseObject.Equals()to useStringComparison.OrdinalIgnoreCasefor schema/object name comparisons. - Update
DatabaseObject.GetHashCode()to align with case-insensitive equality. - Add a unit test intended to cover many-to-many relationships with linking objects in custom schemas and varying casing.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/Config/DatabasePrimitives/DatabaseObject.cs | Makes DatabaseObject equality case-insensitive to prevent FK lookup misses due to casing. |
| src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs | Adds a new data-driven test for custom-schema linking objects in M:N relationships. |
|
|
||
| RuntimeConfig runtimeConfig = new( | ||
| Schema: "UnitTestSchema", | ||
| DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, "", Options: null), |
There was a problem hiding this comment.
new DataSource(DatabaseType: ..., "", Options: null) mixes named and positional arguments (positional arguments can’t follow named ones), which will not compile. Make the connection string argument named (e.g., ConnectionString:) or switch to fully positional arguments.
| DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, "", Options: null), | |
| DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, ConnectionString: "", Options: null), |
| _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)) |
There was a problem hiding this comment.
This test uses It.Is<DatabaseTable>(t => string.Equals(..., OrdinalIgnoreCase)), so it will pass even if DatabaseObject.Equals/GetHashCode remain case-sensitive. To actually validate the regression/fix, set up the mock with concrete new DatabaseTable(...) arguments that differ in casing from what ParseSchemaAndDbTableName returns (or directly test dictionary lookups on RelationShipPair/DatabaseObject using mixed-case keys).
| _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)) | |
| // Use concrete DatabaseTable instances with differing casing so that | |
| // Moq relies on DatabaseTable.Equals for argument matching. | |
| DatabaseTable expectedLinkingTable = new(expectedSchema, expectedTable); | |
| 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 |
| SchemaName?.ToUpperInvariant(), | ||
| Name?.ToUpperInvariant()); |
There was a problem hiding this comment.
GetHashCode() uppercases strings, which allocates and can diverge from StringComparison.OrdinalIgnoreCase semantics for some Unicode edge cases. Prefer using StringComparer.OrdinalIgnoreCase.GetHashCode(...) (and combining the resulting ints) to avoid allocations and ensure hash/equality consistency.
| SchemaName?.ToUpperInvariant(), | |
| Name?.ToUpperInvariant()); | |
| SchemaName is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(SchemaName), | |
| Name is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(Name)); |
Why make this change?
GraphQL queries with M:N relationships fail with SQL error 208 ("Invalid object name") when the linking object uses a custom schema (e.g.,
"linkingObject": "mySchema.books_authors"). The query works only if a table with the same name exists indbo.What is this change?
DatabaseObject.Equals()andGetHashCode()used case-sensitive string comparison. SQL Server schema/table names are case-insensitive, causing FK definition lookups to fail when database-returned names differ in casing from config-specified names.StringComparison.OrdinalIgnoreCaseforEquals()andToUpperInvariant()forGetHashCode()How was this tested?
Added
TestRelationshipWithLinkingObjectInCustomSchemawith test cases for:mySchema.TEST_SOURCE_LINK)MYSCHEMA.TEST_SOURCE_LINK)myschema.test_source_link)Sample Request(s)
With config:
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.