diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index d449c396c0..7c9b505e96 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -318,7 +318,8 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction configEntity: entity, entities: entities, rolesAllowedForEntity: rolesAllowedForEntity, - rolesAllowedForFields: rolesAllowedForFields); + rolesAllowedForFields: rolesAllowedForFields, + databaseType: sqlMetadataProvider.GetDatabaseType()); if (databaseObject.SourceType is not EntitySourceType.StoredProcedure) { @@ -560,7 +561,8 @@ private Dictionary GenerateObjectDefinitionsFo configEntity: linkingEntity, entities: new(new Dictionary()), rolesAllowedForEntity: new List(), - rolesAllowedForFields: new Dictionary>() + rolesAllowedForFields: new Dictionary>(), + databaseType: sqlMetadataProvider.GetDatabaseType() ); linkingObjectTypes.Add(linkingEntityName, node); diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 257b1c1eb6..b10682e8c4 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -48,7 +48,8 @@ public static ObjectTypeDefinitionNode GenerateObjectTypeDefinitionForDatabaseOb Entity configEntity, RuntimeEntities entities, IEnumerable rolesAllowedForEntity, - IDictionary> rolesAllowedForFields) + IDictionary> rolesAllowedForFields, + DatabaseType databaseType = DatabaseType.MSSQL) { ObjectTypeDefinitionNode objectDefinitionNode; switch (databaseObject.SourceType) @@ -69,7 +70,8 @@ public static ObjectTypeDefinitionNode GenerateObjectTypeDefinitionForDatabaseOb configEntity: configEntity, entities: entities, rolesAllowedForEntity: rolesAllowedForEntity, - rolesAllowedForFields: rolesAllowedForFields); + rolesAllowedForFields: rolesAllowedForFields, + databaseType: databaseType); break; default: throw new DataApiBuilderException( @@ -170,7 +172,8 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView Entity configEntity, RuntimeEntities entities, IEnumerable rolesAllowedForEntity, - IDictionary> rolesAllowedForFields) + IDictionary> rolesAllowedForFields, + DatabaseType databaseType = DatabaseType.MSSQL) { Dictionary fieldDefinitionNodes = new(); SourceDefinition sourceDefinition = databaseObject.SourceDefinition; @@ -225,7 +228,8 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView databaseObject, entities, relationshipName, - relationship); + relationship, + databaseType); fieldDefinitionNodes.Add(relationshipField.Name.Value, relationshipField); } } @@ -474,13 +478,14 @@ private static FieldDefinitionNode GenerateFieldForRelationship( DatabaseObject databaseObject, RuntimeEntities entities, string relationshipName, - EntityRelationship relationship) + EntityRelationship relationship, + DatabaseType databaseType = DatabaseType.MSSQL) { // Generate the field that represents the relationship to ObjectType, so you can navigate through it // and walk the graph. string targetEntityName = relationship.TargetEntity.Split('.').Last(); Entity referencedEntity = entities[targetEntityName]; - bool isNullableRelationship = FindNullabilityOfRelationship(entityName, databaseObject, targetEntityName); + bool isNullableRelationship = FindNullabilityOfRelationship(entityName, databaseObject, targetEntityName, databaseType); INullableTypeNode targetField = relationship.Cardinality switch { @@ -631,8 +636,15 @@ public static IValueNode CreateValueNodeFromDbObjectMetadata(object metadataValu private static bool FindNullabilityOfRelationship( string entityName, DatabaseObject databaseObject, - string targetEntityName) + string targetEntityName, + DatabaseType databaseType = DatabaseType.MSSQL) { + // DWSQL does not enforce foreign key constraints, so relationship fields are always nullable. + if (databaseType == DatabaseType.DWSQL) + { + return true; + } + bool isNullableRelationship = false; SourceDefinition sourceDefinition = databaseObject.SourceDefinition; if (// Retrieve all the relationship information for the source entity which is backed by this table definition diff --git a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs index 164c9d6f05..76d0701e57 100644 --- a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs +++ b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs @@ -361,6 +361,21 @@ public void ForeignKeyFieldHasCorrectNullability(bool isNullable) Assert.AreEqual(expected: isNullable, actual: field.Type is INullableTypeNode); } + // A NOT NULL foreign-key column yields a non-nullable relationship field for MSSQL, but a + // nullable one for DWSQL since DWSQL does not enforce foreign key constraints. + [DataRow(DatabaseType.DWSQL, Cardinality.One, true, DisplayName = "Many-to-one relationship field is nullable for DWSQL despite NOT NULL FK.")] + [DataRow(DatabaseType.DWSQL, Cardinality.Many, true, DisplayName = "Cardinality.Many relationship field is nullable for DWSQL despite NOT NULL FK metadata.")] + [DataRow(DatabaseType.MSSQL, Cardinality.One, false, DisplayName = "Many-to-one relationship field is non-nullable for MSSQL with NOT NULL FK.")] + [DataRow(DatabaseType.MSSQL, Cardinality.Many, false, DisplayName = "Cardinality.Many relationship field is non-nullable for MSSQL with NOT NULL FK metadata.")] + [TestMethod] + public void RelationshipFieldIsNullableForDwSqlDespiteNonNullableForeignKey(DatabaseType databaseType, Cardinality cardinality, bool expectedNullable) + { + ObjectTypeDefinitionNode od = + GenerateObjectWithRelationship(cardinality, isNullableRelationship: false, databaseType: databaseType); + FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == FIELD_NAME_FOR_TARGET); + Assert.AreEqual(expectedNullable, field.Type is INullableTypeNode); + } + [TestMethod] public void WhenForeignKeyDefinedButNoRelationship_GraphQLWontModelIt() { @@ -756,7 +771,7 @@ public static Entity GenerateEmptyEntity(string entityName) ); } - private static ObjectTypeDefinitionNode GenerateObjectWithRelationship(Cardinality cardinality, bool isNullableRelationship = false) + private static ObjectTypeDefinitionNode GenerateObjectWithRelationship(Cardinality cardinality, bool isNullableRelationship = false, DatabaseType databaseType = DatabaseType.MSSQL) { SourceDefinition table = GenerateTableWithForeignKeyDefinition(isNullableRelationship); @@ -786,7 +801,8 @@ private static ObjectTypeDefinitionNode GenerateObjectWithRelationship(Cardinali dbObject, configEntity, new(new Dictionary() { { TARGET_ENTITY, relationshipEntity } }), rolesAllowedForEntity: GetRolesAllowedForEntity(), - rolesAllowedForFields: GetFieldToRolesMap() + rolesAllowedForFields: GetFieldToRolesMap(), + databaseType: databaseType ); }