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)!;
}