diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index ec97a48e4c..1360a3fd1d 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,20 @@ 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; + + // 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 (string linkingTableSchema, string linkingTableName) = sqlMetadataProvider.ParseSchemaAndDbTableName(relationship.LinkingObject)!; DatabaseTable linkingDatabaseObject = new(linkingTableSchema, linkingTableName); @@ -1103,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) ? @@ -1150,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/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..9473f0207f 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,14 +990,22 @@ private static void AddForeignKeyForTargetEntity( string sourceEntityName, string relationshipName, string targetEntityName, - DatabaseTable referencingDbTable, - DatabaseTable referencedDbTable, + DatabaseObject referencingDbObject, + DatabaseObject referencedDbObject, string[]? referencingColumns, string[]? referencedColumns, RelationshipRole referencingEntityRole, 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, @@ -2101,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( - databaseTableA: configResolvedFkDefinition.Pair.ReferencingDbTable, - databaseTableB: configResolvedFkDefinition.Pair.ReferencedDbTable); + databaseObjectA: configResolvedFkDefinition.Pair.ReferencingDbTable, + databaseObjectB: configResolvedFkDefinition.Pair.ReferencedDbTable); if (!doesFkExistInDatabase) { @@ -2123,7 +2140,7 @@ 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 @@ -2148,28 +2165,35 @@ 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 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 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) { 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: databaseTableA, - referencedDbObject: databaseTableB); + referencingDbObject: dbTableA, + referencedDbObject: dbTableB); RelationShipPair pairBA = new( - referencingDbObject: databaseTableB, - referencedDbObject: databaseTableA); + referencingDbObject: dbTableB, + referencedDbObject: dbTableA); return (PairToFkDefinition.ContainsKey(pairAB) || PairToFkDefinition.ContainsKey(pairBA)); } @@ -2201,12 +2225,16 @@ 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; + // 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]; @@ -2217,7 +2245,7 @@ public bool TryGetFKDefinition( { foreignKeyDefinition = fKDefinitions.FirstOrDefault( - fk => string.Equals(referencedDbTable.FullName, fk.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase) + fk => string.Equals(referencedDbObjResult.FullName, fk.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase) && fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0)!; }