From cc30b879b8a773533bab4267fec8994f17088f91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:40:10 +0000 Subject: [PATCH 1/4] Initial plan From a7bf76963d9c770a96e59002d64c8f429c809f2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:52:41 +0000 Subject: [PATCH 2/4] Add relationship support for entities of type view (MSSQL/DWSQL) Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- .../DatabasePrimitives/DatabaseObject.cs | 51 +++++++--- .../Configurations/RuntimeConfigValidator.cs | 26 +++-- .../Resolvers/MultipleCreateOrderHelper.cs | 4 +- .../BaseSqlQueryStructure.cs | 8 +- src/Core/Services/GraphQLSchemaCreator.cs | 4 +- .../CosmosSqlMetadataProvider.cs | 4 +- .../MetadataProviders/ISqlMetadataProvider.cs | 4 +- .../MetadataProviders/SqlMetadataProvider.cs | 95 ++++++++++--------- .../Sql/SchemaConverter.cs | 4 +- 9 files changed, 121 insertions(+), 79 deletions(-) diff --git a/src/Config/DatabasePrimitives/DatabaseObject.cs b/src/Config/DatabasePrimitives/DatabaseObject.cs index 8636e8c005..420ac94da9 100644 --- a/src/Config/DatabasePrimitives/DatabaseObject.cs +++ b/src/Config/DatabasePrimitives/DatabaseObject.cs @@ -277,7 +277,7 @@ public ColumnDefinition(Type systemType) } } -[DebuggerDisplay("Relationship: {RelationshipName} ReferencingDbTable = {Pair.ReferencingDbTable.FullName} (Count = {ReferencingColumns.Count}), ReferencedDbTable = {Pair.ReferencedDbTable.FullName} (Count = {ReferencedColumns.Count})")] +[DebuggerDisplay("Relationship: {RelationshipName} ReferencingDbObject = {Pair.ReferencingDbObject.FullName} (Count = {ReferencingColumns.Count}), ReferencedDbObject = {Pair.ReferencedDbObject.FullName} (Count = {ReferencedColumns.Count})")] public class ForeignKeyDefinition { public string SourceEntityName { get; set; } = string.Empty; @@ -388,7 +388,7 @@ public override int GetHashCode() } } -[DebuggerDisplay("ReferencingDbTable = {ReferencingDbTable.FullName}, ReferencedDbTable = {ReferencedDbTable.FullName}")] +[DebuggerDisplay("ReferencingDbObject = {ReferencingDbObject.FullName}, ReferencedDbObject = {ReferencedDbObject.FullName}")] public class RelationShipPair { /// @@ -399,26 +399,47 @@ public class RelationShipPair public RelationShipPair() { } public RelationShipPair( - DatabaseTable referencingDbObject, - DatabaseTable referencedDbObject) + DatabaseObject referencingDbObject, + DatabaseObject referencedDbObject) { - ReferencingDbTable = referencingDbObject; - ReferencedDbTable = referencedDbObject; + ReferencingDbObject = referencingDbObject; + ReferencedDbObject = referencedDbObject; } public RelationShipPair( string relationshipName, - DatabaseTable referencingDbObject, - DatabaseTable referencedDbObject) + DatabaseObject referencingDbObject, + DatabaseObject referencedDbObject) { RelationshipName = relationshipName; - ReferencingDbTable = referencingDbObject; - ReferencedDbTable = referencedDbObject; + ReferencingDbObject = referencingDbObject; + ReferencedDbObject = referencedDbObject; } - public DatabaseTable ReferencingDbTable { get; set; } = new(); + /// + /// The database object (table or view) that is the referencing side of the relationship. + /// + public DatabaseObject ReferencingDbObject { get; set; } = new DatabaseTable(); + + /// + /// The database object (table or view) that is the referenced side of the relationship. + /// + public DatabaseObject ReferencedDbObject { get; set; } = new DatabaseTable(); - public DatabaseTable ReferencedDbTable { get; set; } = new(); + // Backward compatibility properties - kept for serialization and test compatibility + [JsonIgnore] + public DatabaseTable ReferencingDbTable + { + get => ReferencingDbObject as DatabaseTable ?? new DatabaseTable(ReferencingDbObject.SchemaName, ReferencingDbObject.Name); + set => ReferencingDbObject = value; + } + + [JsonIgnore] + public DatabaseTable ReferencedDbTable + { + get => ReferencedDbObject as DatabaseTable ?? new DatabaseTable(ReferencedDbObject.SchemaName, ReferencedDbObject.Name); + set => ReferencedDbObject = value; + } public override bool Equals(object? other) { @@ -428,13 +449,13 @@ public override bool Equals(object? other) public bool Equals(RelationShipPair? other) { return other != null && - ReferencedDbTable.Equals(other.ReferencedDbTable) && - ReferencingDbTable.Equals(other.ReferencingDbTable); + ReferencedDbObject.Equals(other.ReferencedDbObject) && + ReferencingDbObject.Equals(other.ReferencingDbObject); } public override int GetHashCode() { return HashCode.Combine( - ReferencedDbTable, ReferencingDbTable); + ReferencedDbObject, ReferencingDbObject); } } diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index ec97a48e4c..78e92cf51a 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -323,10 +323,22 @@ public void ValidateRelationshipConfigCorrectness(RuntimeConfig runtimeConfig) if (entity.Source.Type is not EntitySourceType.Table && entity.Relationships is not null && entity.Relationships.Count > 0) { - HandleOrRecordException(new DataApiBuilderException( - message: $"Cannot define relationship for entity: {entityName}", - statusCode: HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); + // Views are allowed to have relationships only for MSSQL and DWSQL databases. + // Stored procedures cannot have relationships. + string databaseNameForEntity = runtimeConfig.GetDataSourceNameFromEntityName(entityName); + DatabaseType dbType = runtimeConfig.GetDataSourceFromDataSourceName(databaseNameForEntity).DatabaseType; + + bool isViewWithRelationshipAllowed = entity.Source.Type is EntitySourceType.View + && (dbType is DatabaseType.MSSQL or DatabaseType.DWSQL); + + if (!isViewWithRelationshipAllowed) + { + HandleOrRecordException(new DataApiBuilderException( + message: $"Cannot define relationship for entity: {entityName}. " + + $"Relationships are only supported for tables, or for views when using MSSQL or DWSQL databases.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); + } } string databaseName = runtimeConfig.GetDataSourceNameFromEntityName(entityName); @@ -1067,10 +1079,12 @@ public void ValidateRelationships(RuntimeConfig runtimeConfig, IMetadataProvider continue; } - DatabaseTable sourceDatabaseObject = (DatabaseTable)sourceObject; - DatabaseTable targetDatabaseObject = (DatabaseTable)targetObject; + // Source and target objects can be tables or views (for MSSQL/DWSQL) + DatabaseObject sourceDatabaseObject = sourceObject; + DatabaseObject targetDatabaseObject = targetObject; if (relationship.LinkingObject is not null) { + // Linking object must remain a table (string linkingTableSchema, string linkingTableName) = sqlMetadataProvider.ParseSchemaAndDbTableName(relationship.LinkingObject)!; DatabaseTable linkingDatabaseObject = new(linkingTableSchema, linkingTableName); diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index 83c2ebd003..5fa7b995b9 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -159,7 +159,7 @@ private static bool TryDetermineReferencingEntityBasedOnEntityRelationshipMetada continue; } - string referencingEntityNameForThisFK = targetEntityForeignKey.Pair.ReferencingDbTable.Equals(sourceDbTable) ? sourceEntityName : targetEntityName; + string referencingEntityNameForThisFK = targetEntityForeignKey.Pair.ReferencingDbObject.Equals(sourceDbTable) ? sourceEntityName : targetEntityName; referencingEntityNames.Add(referencingEntityNameForThisFK); } @@ -350,7 +350,7 @@ private static RelationshipFields GetRelationshipFieldsInSourceAndTarget( continue; } - if (targetEntityForeignKey.Pair.ReferencingDbTable.Equals(sourceDbTable)) + if (targetEntityForeignKey.Pair.ReferencingDbObject.Equals(sourceDbTable)) { relationshipFieldsInSource = targetEntityForeignKey.ReferencingColumns; relationshipFieldsInTarget = targetEntityForeignKey.ReferencedColumns; diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 99a5b1e72c..3fcc0ba6ae 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -260,7 +260,7 @@ public void AddJoinPredicatesForRelatedEntity( { // First identify which side of the relationship, this fk definition // is looking at. - if (foreignKeyDefinition.Pair.ReferencingDbTable.Equals(DatabaseObject)) + if (foreignKeyDefinition.Pair.ReferencingDbObject.Equals(DatabaseObject)) { // Case where fk in parent entity references the nested entity. // Verify this is a valid fk definition before adding the join predicate. @@ -274,7 +274,7 @@ public void AddJoinPredicatesForRelatedEntity( foreignKeyDefinition.ReferencedColumns)); } } - else if (foreignKeyDefinition.Pair.ReferencingDbTable.Equals(relatedEntityDbObject)) + else if (foreignKeyDefinition.Pair.ReferencingDbObject.Equals(relatedEntityDbObject)) { // Case where fk in nested entity references the parent entity. if (foreignKeyDefinition.ReferencingColumns.Count > 0 @@ -290,7 +290,7 @@ public void AddJoinPredicatesForRelatedEntity( else { DatabaseObject associativeTableDbObject = - foreignKeyDefinition.Pair.ReferencingDbTable; + foreignKeyDefinition.Pair.ReferencingDbObject; // Case when the linking object is the referencing table if (!associativeTableAndAliases.TryGetValue( associativeTableDbObject, @@ -302,7 +302,7 @@ public void AddJoinPredicatesForRelatedEntity( associativeTableAndAliases.Add(associativeTableDbObject, associativeTableAlias); } - if (foreignKeyDefinition.Pair.ReferencedDbTable.Equals(DatabaseObject)) + if (foreignKeyDefinition.Pair.ReferencedDbObject.Equals(DatabaseObject)) { subQuery.Predicates.AddRange(CreateJoinPredicates( associativeTableAlias, diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 90e918c833..a507f8cf2e 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -398,7 +398,7 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencingDbTable.Equals(sourceDbo)); + && fk.Pair.ReferencingDbObject.Equals(sourceDbo)); sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbo); // Find the foreignkeys in which the target entity is the referencing object, i.e. source entity is the referenced object. @@ -406,7 +406,7 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencingDbTable.Equals(targetDbo)); + && fk.Pair.ReferencingDbObject.Equals(targetDbo)); ForeignKeyDefinition? sourceReferencingFKInfo = sourceReferencingForeignKeysInfo.FirstOrDefault(); if (sourceReferencingFKInfo is not null) diff --git a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs index 61ffeeab09..ca71db4d89 100644 --- a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs @@ -498,8 +498,8 @@ public IQueryBuilder GetQueryBuilder() } public bool VerifyForeignKeyExistsInDB( - DatabaseTable databaseTableA, - DatabaseTable databaseTableB) + DatabaseObject databaseObjectA, + DatabaseObject databaseObjectB) { throw new NotImplementedException(); } diff --git a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs index 83989b645a..d862c60305 100644 --- a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -28,8 +28,8 @@ public interface ISqlMetadataProvider string GetSchemaName(string entityName); bool VerifyForeignKeyExistsInDB( - DatabaseTable databaseObjectA, - DatabaseTable databaseObjectB); + DatabaseObject databaseObjectA, + DatabaseObject databaseObjectB); /// /// Obtains the underlying source object's name (SQL table or Cosmos container). diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 8553e08136..382222d1bd 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -748,9 +748,9 @@ protected void PopulateDatabaseObjectForEntity( EntityToDatabaseObject.Add(entityName, sourceObject); - if (entity.Relationships is not null && entity.Source.Type is EntitySourceType.Table) + if (entity.Relationships is not null && entity.Source.Type is EntitySourceType.Table or EntitySourceType.View) { - ProcessRelationships(entityName, entity, (DatabaseTable)sourceObject, sourceObjects); + ProcessRelationships(entityName, entity, sourceObject, sourceObjects); } } } @@ -780,22 +780,23 @@ private static EntitySourceType GetEntitySourceType(string entityName, Entity en /// Adds a foreign key definition for each of the nested entities /// specified in the relationships section of this entity /// to gather the referencing and referenced columns from the database at a later stage. - /// Sets the referencing and referenced tables based on the kind of relationship. + /// Sets the referencing and referenced database objects (tables or views) based on the kind of relationship. /// A linking object encountered is used as the referencing table /// for the foreign key definition. /// When no foreign key is defined in the database for the relationship, /// the relationship.source.fields and relationship.target.fields are mandatory. /// Initializing a FKDefinition indicates to find the foreign key - /// between the referencing and referenced tables. + /// between the referencing and referenced database objects. /// - /// - /// - /// + /// Name of the source entity. + /// Entity configuration. + /// Database object (table or view) backing the entity. + /// Collection of all database objects. /// private void ProcessRelationships( string entityName, Entity entity, - DatabaseTable databaseTable, + DatabaseObject databaseObject, Dictionary sourceObjects) { SourceDefinition sourceDefinition = GetSourceDefinition(entityName); @@ -824,9 +825,17 @@ private void ProcessRelationships( } (targetSchemaName, targetDbTableName) = ParseSchemaAndDbTableName(targetEntity.Source.Object)!; - DatabaseTable targetDbTable = new(targetSchemaName, targetDbTableName); + + // Create appropriate database object based on target entity's source type + DatabaseObject targetDbObject = targetEntity.Source.Type switch + { + EntitySourceType.View => new DatabaseView(targetSchemaName, targetDbTableName), + _ => new DatabaseTable(targetSchemaName, targetDbTableName) + }; + // If a linking object is specified, // give that higher preference and add two foreign keys for this targetEntity. + // Note: Linking objects must remain tables (not views) per the requirement. if (relationship.LinkingObject is not null) { (linkingTableSchema, linkingTableName) = ParseSchemaAndDbTableName(relationship.LinkingObject)!; @@ -835,8 +844,8 @@ private void ProcessRelationships( sourceEntityName: entityName, relationshipName: relationshipName, targetEntityName: targetEntityName, - referencingDbTable: linkingDbTable, - referencedDbTable: databaseTable, + referencingDbObject: linkingDbTable, + referencedDbObject: databaseObject, referencingColumns: relationship.LinkingSourceFields, referencedColumns: relationship.SourceFields, referencingEntityRole: RelationshipRole.Linking, @@ -847,8 +856,8 @@ private void ProcessRelationships( sourceEntityName: entityName, relationshipName: relationshipName, targetEntityName: targetEntityName, - referencingDbTable: linkingDbTable, - referencedDbTable: targetDbTable, + referencingDbObject: linkingDbTable, + referencedDbObject: targetDbObject, referencingColumns: relationship.LinkingTargetFields, referencedColumns: relationship.TargetFields, referencingEntityRole: RelationshipRole.Linking, @@ -902,8 +911,8 @@ private void ProcessRelationships( sourceEntityName: entityName, relationshipName: relationshipName, targetEntityName, - referencingDbTable: databaseTable, - referencedDbTable: targetDbTable, + referencingDbObject: databaseObject, + referencedDbObject: targetDbObject, referencingColumns: relationship.SourceFields, referencedColumns: relationship.TargetFields, referencingEntityRole: RelationshipRole.Source, @@ -919,8 +928,8 @@ private void ProcessRelationships( sourceEntityName: entityName, relationshipName: relationshipName, targetEntityName, - referencingDbTable: targetDbTable, - referencedDbTable: databaseTable, + referencingDbObject: targetDbObject, + referencedDbObject: databaseObject, referencingColumns: relationship.TargetFields, referencedColumns: relationship.SourceFields, referencingEntityRole: RelationshipRole.Target, @@ -943,8 +952,8 @@ private void ProcessRelationships( sourceEntityName: entityName, relationshipName: relationshipName, targetEntityName, - referencingDbTable: targetDbTable, - referencedDbTable: databaseTable, + referencingDbObject: targetDbObject, + referencedDbObject: databaseObject, referencingColumns: relationship.TargetFields, referencedColumns: relationship.SourceFields, referencingEntityRole: RelationshipRole.Target, @@ -981,8 +990,8 @@ private static void AddForeignKeyForTargetEntity( string sourceEntityName, string relationshipName, string targetEntityName, - DatabaseTable referencingDbTable, - DatabaseTable referencedDbTable, + DatabaseObject referencingDbObject, + DatabaseObject referencedDbObject, string[]? referencingColumns, string[]? referencedColumns, RelationshipRole referencingEntityRole, @@ -998,8 +1007,8 @@ private static void AddForeignKeyForTargetEntity( Pair = new() { RelationshipName = relationshipName, - ReferencingDbTable = referencingDbTable, - ReferencedDbTable = referencedDbTable + ReferencingDbObject = referencingDbObject, + ReferencedDbObject = referencedDbObject } }; @@ -1862,8 +1871,8 @@ IEnumerable> foreignKeysForAllTargetEntities { foreach (ForeignKeyDefinition fk in fkDefinitionsForTargetEntity) { - schemaNames.Add(fk.Pair.ReferencingDbTable.SchemaName); - tableNames.Add(fk.Pair.ReferencingDbTable.Name); + schemaNames.Add(fk.Pair.ReferencingDbObject.SchemaName); + tableNames.Add(fk.Pair.ReferencingDbObject.Name); sourceNameToSourceDefinition.TryAdd(dbObject.Name, sourceDefinition); } } @@ -2101,8 +2110,8 @@ private List GetValidatedFKs(List fK // 2. configResolvedFkDefinition doesn't match a database fk definition -> added to the list of // validated FK definitions because it's not already added. bool doesFkExistInDatabase = VerifyForeignKeyExistsInDB( - databaseTableA: configResolvedFkDefinition.Pair.ReferencingDbTable, - databaseTableB: configResolvedFkDefinition.Pair.ReferencedDbTable); + databaseObjectA: configResolvedFkDefinition.Pair.ReferencingDbObject, + databaseObjectB: configResolvedFkDefinition.Pair.ReferencedDbObject); if (!doesFkExistInDatabase) { @@ -2123,13 +2132,13 @@ private List GetValidatedFKs(List fK /// /// Returns whether the supplied foreign key definition denotes a self-joining relationship - /// by checking whether the backing tables are the same. + /// by checking whether the backing database objects are the same. /// /// ForeignKeyDefinition representing a relationship. /// true when the ForeignKeyDefinition represents a self-joining relationship private static bool IsSelfJoiningRelationship(ForeignKeyDefinition fkDefinition) { - return fkDefinition.Pair.ReferencedDbTable.FullName.Equals(fkDefinition.Pair.ReferencingDbTable.FullName); + return fkDefinition.Pair.ReferencedDbObject.FullName.Equals(fkDefinition.Pair.ReferencingDbObject.FullName); } /// @@ -2148,15 +2157,15 @@ private static bool DoesConfiguredRelationshipOverrideDatabaseFkConstraint(Forei /// Returns whether DAB has resolved a foreign key from the database /// linking databaseTableA and databaseTableB. /// A database foreign key definition explicitly denotes the referencing table and the referenced table. - /// This function creates two RelationShipPair objects, interchanging which datatable is referencing - /// and which table is referenced, so that DAB can definitevly identify whether a database foreign key exists. + /// This function creates two RelationShipPair objects, interchanging which database object is referencing + /// and which database object is referenced, so that DAB can definitevly identify whether a database foreign key exists. /// - When DAB pre-processes relationships in the config, DAB creates two foreign key definition objects - /// because the config doesn't tell DAB which table is referencing vs referenced. This function is called when + /// because the config doesn't tell DAB which database object is referencing vs referenced. This function is called when /// DAB is determining which of the two FK definitions to keep. /// public bool VerifyForeignKeyExistsInDB( - DatabaseTable databaseTableA, - DatabaseTable databaseTableB) + DatabaseObject databaseObjectA, + DatabaseObject databaseObjectB) { if (PairToFkDefinition is null) { @@ -2164,12 +2173,12 @@ public bool VerifyForeignKeyExistsInDB( } RelationShipPair pairAB = new( - referencingDbObject: databaseTableA, - referencedDbObject: databaseTableB); + referencingDbObject: databaseObjectA, + referencedDbObject: databaseObjectB); RelationShipPair pairBA = new( - referencingDbObject: databaseTableB, - referencedDbObject: databaseTableA); + referencingDbObject: databaseObjectB, + referencedDbObject: databaseObjectA); return (PairToFkDefinition.ContainsKey(pairAB) || PairToFkDefinition.ContainsKey(pairBA)); } @@ -2201,11 +2210,9 @@ public bool TryGetFKDefinition( bool isMToNRelationship = false) { if (GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbObject) && - GetEntityNamesAndDbObjects().TryGetValue(referencingEntityName, out DatabaseObject? referencingDbObject) && - GetEntityNamesAndDbObjects().TryGetValue(referencedEntityName, out DatabaseObject? referencedDbObject)) + GetEntityNamesAndDbObjects().TryGetValue(referencingEntityName, out DatabaseObject? referencingDbObjResult) && + GetEntityNamesAndDbObjects().TryGetValue(referencedEntityName, out DatabaseObject? referencedDbObjResult)) { - DatabaseTable referencingDbTable = (DatabaseTable)referencingDbObject; - DatabaseTable referencedDbTable = (DatabaseTable)referencedDbObject; SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; RelationShipPair referencingReferencedPair; List fKDefinitions = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; @@ -2217,13 +2224,13 @@ public bool TryGetFKDefinition( { foreignKeyDefinition = fKDefinitions.FirstOrDefault( - fk => string.Equals(referencedDbTable.FullName, fk.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase) + fk => string.Equals(referencedDbObjResult.FullName, fk.Pair.ReferencedDbObject.FullName, StringComparison.OrdinalIgnoreCase) && fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0)!; } else { - referencingReferencedPair = new(referencingDbTable, referencedDbTable); + referencingReferencedPair = new(referencingDbObjResult, referencedDbObjResult); foreignKeyDefinition = fKDefinitions.FirstOrDefault( fk => fk.Pair.Equals(referencingReferencedPair) && fk.ReferencingColumns.Count > 0 diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 76057a76dc..a38e511972 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -633,7 +633,7 @@ private static bool FindNullabilityOfRelationship( listOfForeignKeys.Where(fk => fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencingDbTable.Equals(databaseObject)) + && fk.Pair.ReferencingDbObject.Equals(databaseObject)) .ToArray(); // Find the foreign keys in which the source entity is the referenced object. @@ -641,7 +641,7 @@ private static bool FindNullabilityOfRelationship( listOfForeignKeys.Where(fk => fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencedDbTable.Equals(databaseObject)) + && fk.Pair.ReferencedDbObject.Equals(databaseObject)) .ToArray(); // The source entity should at least be a referencing or referenced db object or both From eecbf8b9cb01d301c43939cbe2eaabb37b6601f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:01:27 +0000 Subject: [PATCH 3/4] Fix serialization and update property usages for view relationship support Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- .../DatabasePrimitives/DatabaseObject.cs | 51 ++++++------------- .../Configurations/RuntimeConfigValidator.cs | 16 ++++-- .../Resolvers/MultipleCreateOrderHelper.cs | 4 +- .../BaseSqlQueryStructure.cs | 8 +-- src/Core/Services/GraphQLSchemaCreator.cs | 4 +- .../MetadataProviders/SqlMetadataProvider.cs | 47 ++++++++++++----- .../Sql/SchemaConverter.cs | 4 +- 7 files changed, 71 insertions(+), 63 deletions(-) diff --git a/src/Config/DatabasePrimitives/DatabaseObject.cs b/src/Config/DatabasePrimitives/DatabaseObject.cs index 420ac94da9..8636e8c005 100644 --- a/src/Config/DatabasePrimitives/DatabaseObject.cs +++ b/src/Config/DatabasePrimitives/DatabaseObject.cs @@ -277,7 +277,7 @@ public ColumnDefinition(Type systemType) } } -[DebuggerDisplay("Relationship: {RelationshipName} ReferencingDbObject = {Pair.ReferencingDbObject.FullName} (Count = {ReferencingColumns.Count}), ReferencedDbObject = {Pair.ReferencedDbObject.FullName} (Count = {ReferencedColumns.Count})")] +[DebuggerDisplay("Relationship: {RelationshipName} ReferencingDbTable = {Pair.ReferencingDbTable.FullName} (Count = {ReferencingColumns.Count}), ReferencedDbTable = {Pair.ReferencedDbTable.FullName} (Count = {ReferencedColumns.Count})")] public class ForeignKeyDefinition { public string SourceEntityName { get; set; } = string.Empty; @@ -388,7 +388,7 @@ public override int GetHashCode() } } -[DebuggerDisplay("ReferencingDbObject = {ReferencingDbObject.FullName}, ReferencedDbObject = {ReferencedDbObject.FullName}")] +[DebuggerDisplay("ReferencingDbTable = {ReferencingDbTable.FullName}, ReferencedDbTable = {ReferencedDbTable.FullName}")] public class RelationShipPair { /// @@ -399,47 +399,26 @@ public class RelationShipPair public RelationShipPair() { } public RelationShipPair( - DatabaseObject referencingDbObject, - DatabaseObject referencedDbObject) + DatabaseTable referencingDbObject, + DatabaseTable referencedDbObject) { - ReferencingDbObject = referencingDbObject; - ReferencedDbObject = referencedDbObject; + ReferencingDbTable = referencingDbObject; + ReferencedDbTable = referencedDbObject; } public RelationShipPair( string relationshipName, - DatabaseObject referencingDbObject, - DatabaseObject referencedDbObject) + DatabaseTable referencingDbObject, + DatabaseTable referencedDbObject) { RelationshipName = relationshipName; - ReferencingDbObject = referencingDbObject; - ReferencedDbObject = referencedDbObject; + ReferencingDbTable = referencingDbObject; + ReferencedDbTable = referencedDbObject; } - /// - /// The database object (table or view) that is the referencing side of the relationship. - /// - public DatabaseObject ReferencingDbObject { get; set; } = new DatabaseTable(); - - /// - /// The database object (table or view) that is the referenced side of the relationship. - /// - public DatabaseObject ReferencedDbObject { get; set; } = new DatabaseTable(); + public DatabaseTable ReferencingDbTable { get; set; } = new(); - // Backward compatibility properties - kept for serialization and test compatibility - [JsonIgnore] - public DatabaseTable ReferencingDbTable - { - get => ReferencingDbObject as DatabaseTable ?? new DatabaseTable(ReferencingDbObject.SchemaName, ReferencingDbObject.Name); - set => ReferencingDbObject = value; - } - - [JsonIgnore] - public DatabaseTable ReferencedDbTable - { - get => ReferencedDbObject as DatabaseTable ?? new DatabaseTable(ReferencedDbObject.SchemaName, ReferencedDbObject.Name); - set => ReferencedDbObject = value; - } + public DatabaseTable ReferencedDbTable { get; set; } = new(); public override bool Equals(object? other) { @@ -449,13 +428,13 @@ public override bool Equals(object? other) public bool Equals(RelationShipPair? other) { return other != null && - ReferencedDbObject.Equals(other.ReferencedDbObject) && - ReferencingDbObject.Equals(other.ReferencingDbObject); + ReferencedDbTable.Equals(other.ReferencedDbTable) && + ReferencingDbTable.Equals(other.ReferencingDbTable); } public override int GetHashCode() { return HashCode.Combine( - ReferencedDbObject, ReferencingDbObject); + ReferencedDbTable, ReferencingDbTable); } } diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index 78e92cf51a..1360a3fd1d 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -1082,6 +1082,14 @@ public void ValidateRelationships(RuntimeConfig runtimeConfig, IMetadataProvider // Source and target objects can be tables or views (for MSSQL/DWSQL) DatabaseObject sourceDatabaseObject = sourceObject; DatabaseObject targetDatabaseObject = targetObject; + + // For RelationShipPair comparisons, we convert to DatabaseTable since the comparison + // only uses schema and name (not the actual type) + DatabaseTable sourceDatabaseTable = sourceDatabaseObject as DatabaseTable + ?? new DatabaseTable(sourceDatabaseObject.SchemaName, sourceDatabaseObject.Name); + DatabaseTable targetDatabaseTable = targetDatabaseObject as DatabaseTable + ?? new DatabaseTable(targetDatabaseObject.SchemaName, targetDatabaseObject.Name); + if (relationship.LinkingObject is not null) { // Linking object must remain a table @@ -1117,8 +1125,8 @@ public void ValidateRelationships(RuntimeConfig runtimeConfig, IMetadataProvider string sourceDBOName = sqlMetadataProvider.EntityToDatabaseObject[entityName].FullName; string targetDBOName = sqlMetadataProvider.EntityToDatabaseObject[relationship.TargetEntity].FullName; string cardinality = relationship.Cardinality.ToString().ToLower(); - RelationShipPair linkedSourceRelationshipPair = new(linkingDatabaseObject, sourceDatabaseObject); - RelationShipPair linkedTargetRelationshipPair = new(linkingDatabaseObject, targetDatabaseObject); + RelationShipPair linkedSourceRelationshipPair = new(linkingDatabaseObject, sourceDatabaseTable); + RelationShipPair linkedTargetRelationshipPair = new(linkingDatabaseObject, targetDatabaseTable); ForeignKeyDefinition? fKDef; string referencedSourceColumns = relationship.SourceFields is not null ? string.Join(",", relationship.SourceFields) : sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedSourceRelationshipPair, out fKDef) ? @@ -1164,8 +1172,8 @@ public void ValidateRelationships(RuntimeConfig runtimeConfig, IMetadataProvider if (relationship.LinkingObject is null && !_runtimeConfigProvider.IsLateConfigured) { - RelationShipPair sourceTargetRelationshipPair = new(sourceDatabaseObject, targetDatabaseObject); - RelationShipPair targetSourceRelationshipPair = new(targetDatabaseObject, sourceDatabaseObject); + RelationShipPair sourceTargetRelationshipPair = new(sourceDatabaseTable, targetDatabaseTable); + RelationShipPair targetSourceRelationshipPair = new(targetDatabaseTable, sourceDatabaseTable); string sourceDBOName = sqlMetadataProvider.EntityToDatabaseObject[entityName].FullName; string targetDBOName = sqlMetadataProvider.EntityToDatabaseObject[relationship.TargetEntity].FullName; string cardinality = relationship.Cardinality.ToString().ToLower(); diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index 5fa7b995b9..83c2ebd003 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -159,7 +159,7 @@ private static bool TryDetermineReferencingEntityBasedOnEntityRelationshipMetada continue; } - string referencingEntityNameForThisFK = targetEntityForeignKey.Pair.ReferencingDbObject.Equals(sourceDbTable) ? sourceEntityName : targetEntityName; + string referencingEntityNameForThisFK = targetEntityForeignKey.Pair.ReferencingDbTable.Equals(sourceDbTable) ? sourceEntityName : targetEntityName; referencingEntityNames.Add(referencingEntityNameForThisFK); } @@ -350,7 +350,7 @@ private static RelationshipFields GetRelationshipFieldsInSourceAndTarget( continue; } - if (targetEntityForeignKey.Pair.ReferencingDbObject.Equals(sourceDbTable)) + if (targetEntityForeignKey.Pair.ReferencingDbTable.Equals(sourceDbTable)) { relationshipFieldsInSource = targetEntityForeignKey.ReferencingColumns; relationshipFieldsInTarget = targetEntityForeignKey.ReferencedColumns; diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 3fcc0ba6ae..99a5b1e72c 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -260,7 +260,7 @@ public void AddJoinPredicatesForRelatedEntity( { // First identify which side of the relationship, this fk definition // is looking at. - if (foreignKeyDefinition.Pair.ReferencingDbObject.Equals(DatabaseObject)) + if (foreignKeyDefinition.Pair.ReferencingDbTable.Equals(DatabaseObject)) { // Case where fk in parent entity references the nested entity. // Verify this is a valid fk definition before adding the join predicate. @@ -274,7 +274,7 @@ public void AddJoinPredicatesForRelatedEntity( foreignKeyDefinition.ReferencedColumns)); } } - else if (foreignKeyDefinition.Pair.ReferencingDbObject.Equals(relatedEntityDbObject)) + else if (foreignKeyDefinition.Pair.ReferencingDbTable.Equals(relatedEntityDbObject)) { // Case where fk in nested entity references the parent entity. if (foreignKeyDefinition.ReferencingColumns.Count > 0 @@ -290,7 +290,7 @@ public void AddJoinPredicatesForRelatedEntity( else { DatabaseObject associativeTableDbObject = - foreignKeyDefinition.Pair.ReferencingDbObject; + foreignKeyDefinition.Pair.ReferencingDbTable; // Case when the linking object is the referencing table if (!associativeTableAndAliases.TryGetValue( associativeTableDbObject, @@ -302,7 +302,7 @@ public void AddJoinPredicatesForRelatedEntity( associativeTableAndAliases.Add(associativeTableDbObject, associativeTableAlias); } - if (foreignKeyDefinition.Pair.ReferencedDbObject.Equals(DatabaseObject)) + if (foreignKeyDefinition.Pair.ReferencedDbTable.Equals(DatabaseObject)) { subQuery.Predicates.AddRange(CreateJoinPredicates( associativeTableAlias, diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index a507f8cf2e..90e918c833 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -398,7 +398,7 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencingDbObject.Equals(sourceDbo)); + && fk.Pair.ReferencingDbTable.Equals(sourceDbo)); sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbo); // Find the foreignkeys in which the target entity is the referencing object, i.e. source entity is the referenced object. @@ -406,7 +406,7 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencingDbObject.Equals(targetDbo)); + && fk.Pair.ReferencingDbTable.Equals(targetDbo)); ForeignKeyDefinition? sourceReferencingFKInfo = sourceReferencingForeignKeysInfo.FirstOrDefault(); if (sourceReferencingFKInfo is not null) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 382222d1bd..b5ac7262c6 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -998,6 +998,14 @@ private static void AddForeignKeyForTargetEntity( RelationshipRole referencedEntityRole, RelationshipMetadata relationshipData) { + // RelationShipPair uses DatabaseTable for serialization compatibility. + // We convert DatabaseObject to DatabaseTable using schema and name. + // The Equals comparison works correctly since it only compares schema and name. + DatabaseTable referencingDbTable = referencingDbObject as DatabaseTable + ?? new DatabaseTable(referencingDbObject.SchemaName, referencingDbObject.Name); + DatabaseTable referencedDbTable = referencedDbObject as DatabaseTable + ?? new DatabaseTable(referencedDbObject.SchemaName, referencedDbObject.Name); + ForeignKeyDefinition foreignKeyDefinition = new() { SourceEntityName = sourceEntityName, @@ -1007,8 +1015,8 @@ private static void AddForeignKeyForTargetEntity( Pair = new() { RelationshipName = relationshipName, - ReferencingDbObject = referencingDbObject, - ReferencedDbObject = referencedDbObject + ReferencingDbTable = referencingDbTable, + ReferencedDbTable = referencedDbTable } }; @@ -1871,8 +1879,8 @@ IEnumerable> foreignKeysForAllTargetEntities { foreach (ForeignKeyDefinition fk in fkDefinitionsForTargetEntity) { - schemaNames.Add(fk.Pair.ReferencingDbObject.SchemaName); - tableNames.Add(fk.Pair.ReferencingDbObject.Name); + schemaNames.Add(fk.Pair.ReferencingDbTable.SchemaName); + tableNames.Add(fk.Pair.ReferencingDbTable.Name); sourceNameToSourceDefinition.TryAdd(dbObject.Name, sourceDefinition); } } @@ -2110,8 +2118,8 @@ private List GetValidatedFKs(List fK // 2. configResolvedFkDefinition doesn't match a database fk definition -> added to the list of // validated FK definitions because it's not already added. bool doesFkExistInDatabase = VerifyForeignKeyExistsInDB( - databaseObjectA: configResolvedFkDefinition.Pair.ReferencingDbObject, - databaseObjectB: configResolvedFkDefinition.Pair.ReferencedDbObject); + databaseObjectA: configResolvedFkDefinition.Pair.ReferencingDbTable, + databaseObjectB: configResolvedFkDefinition.Pair.ReferencedDbTable); if (!doesFkExistInDatabase) { @@ -2138,7 +2146,7 @@ private List GetValidatedFKs(List fK /// true when the ForeignKeyDefinition represents a self-joining relationship private static bool IsSelfJoiningRelationship(ForeignKeyDefinition fkDefinition) { - return fkDefinition.Pair.ReferencedDbObject.FullName.Equals(fkDefinition.Pair.ReferencingDbObject.FullName); + return fkDefinition.Pair.ReferencedDbTable.FullName.Equals(fkDefinition.Pair.ReferencingDbTable.FullName); } /// @@ -2172,13 +2180,20 @@ public bool VerifyForeignKeyExistsInDB( return false; } + // Convert DatabaseObject to DatabaseTable for RelationShipPair comparison + // The comparison only uses schema and name, so this is safe + DatabaseTable dbTableA = databaseObjectA as DatabaseTable + ?? new DatabaseTable(databaseObjectA.SchemaName, databaseObjectA.Name); + DatabaseTable dbTableB = databaseObjectB as DatabaseTable + ?? new DatabaseTable(databaseObjectB.SchemaName, databaseObjectB.Name); + RelationShipPair pairAB = new( - referencingDbObject: databaseObjectA, - referencedDbObject: databaseObjectB); + referencingDbObject: dbTableA, + referencedDbObject: dbTableB); RelationShipPair pairBA = new( - referencingDbObject: databaseObjectB, - referencedDbObject: databaseObjectA); + referencingDbObject: dbTableB, + referencedDbObject: dbTableA); return (PairToFkDefinition.ContainsKey(pairAB) || PairToFkDefinition.ContainsKey(pairBA)); } @@ -2214,6 +2229,12 @@ public bool TryGetFKDefinition( GetEntityNamesAndDbObjects().TryGetValue(referencedEntityName, out DatabaseObject? referencedDbObjResult)) { SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; + // Convert DatabaseObject to DatabaseTable for RelationShipPair comparison + DatabaseTable referencingDbTable = referencingDbObjResult as DatabaseTable + ?? new DatabaseTable(referencingDbObjResult.SchemaName, referencingDbObjResult.Name); + DatabaseTable referencedDbTable = referencedDbObjResult as DatabaseTable + ?? new DatabaseTable(referencedDbObjResult.SchemaName, referencedDbObjResult.Name); + RelationShipPair referencingReferencedPair; List fKDefinitions = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; @@ -2224,13 +2245,13 @@ public bool TryGetFKDefinition( { foreignKeyDefinition = fKDefinitions.FirstOrDefault( - fk => string.Equals(referencedDbObjResult.FullName, fk.Pair.ReferencedDbObject.FullName, StringComparison.OrdinalIgnoreCase) + fk => string.Equals(referencedDbObjResult.FullName, fk.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase) && fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0)!; } else { - referencingReferencedPair = new(referencingDbObjResult, referencedDbObjResult); + referencingReferencedPair = new(referencingDbTable, referencedDbTable); foreignKeyDefinition = fKDefinitions.FirstOrDefault( fk => fk.Pair.Equals(referencingReferencedPair) && fk.ReferencingColumns.Count > 0 diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index a38e511972..76057a76dc 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -633,7 +633,7 @@ private static bool FindNullabilityOfRelationship( listOfForeignKeys.Where(fk => fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencingDbObject.Equals(databaseObject)) + && fk.Pair.ReferencingDbTable.Equals(databaseObject)) .ToArray(); // Find the foreign keys in which the source entity is the referenced object. @@ -641,7 +641,7 @@ private static bool FindNullabilityOfRelationship( listOfForeignKeys.Where(fk => fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencedDbObject.Equals(databaseObject)) + && fk.Pair.ReferencedDbTable.Equals(databaseObject)) .ToArray(); // The source entity should at least be a referencing or referenced db object or both From 216ebf9a39a5a2141dafa052ffb51df7e9f5dbc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:05:06 +0000 Subject: [PATCH 4/4] Fix spelling in comment Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- src/Core/Services/MetadataProviders/SqlMetadataProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index b5ac7262c6..9473f0207f 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -2166,7 +2166,7 @@ private static bool DoesConfiguredRelationshipOverrideDatabaseFkConstraint(Forei /// linking databaseTableA and databaseTableB. /// A database foreign key definition explicitly denotes the referencing table and the referenced table. /// This function creates two RelationShipPair objects, interchanging which database object is referencing - /// and which database object is referenced, so that DAB can definitevly identify whether a database foreign key exists. + /// and which database object is referenced, so that DAB can definitively identify whether a database foreign key exists. /// - When DAB pre-processes relationships in the config, DAB creates two foreign key definition objects /// because the config doesn't tell DAB which database object is referencing vs referenced. This function is called when /// DAB is determining which of the two FK definitions to keep.