From ff7be907e70bd28dbbafb9d804ee58e1ffb41cd8 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 12 Jul 2026 00:30:49 -0700 Subject: [PATCH 1/7] Add logic to cast each SQL query parameter to its system type --- .../BaseSqlQueryStructure.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 99a5b1e72c..e36d54b41d 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -84,6 +84,21 @@ public BaseSqlQueryStructure( } } + /// + public override string MakeDbConnectionParam(object? value, string? paramName = null, bool lengthOverride = false) + { + if (!string.IsNullOrEmpty(paramName) && value is string stringValue && GetUnderlyingSourceDefinition().Columns.ContainsKey(paramName)) + { + Type columnSystemType = GetColumnSystemType(paramName); + if (columnSystemType != typeof(string)) + { + value = GetParamAsSystemType(stringValue, paramName, columnSystemType); + } + } + + return base.MakeDbConnectionParam(value, paramName, lengthOverride); + } + /// /// For UPDATE (OVERWRITE) operation /// Adds result of (SourceDefinition.Columns minus MutationFields) to UpdateOperations with null values @@ -421,9 +436,9 @@ protected List GenerateOutputColumns() /// Tries to parse the string parameter to the given system type /// Useful for inferring parameter types for columns or procedure parameters /// - /// - /// - /// + /// The string value to parse. + /// The target system type for the parsed value. + /// The parameter parsed as the requested system type. /// protected static object ParseParamAsSystemType(string param, Type systemType) { From d7e7c4ec965f9348a2ce83a71030802a2ea7f559 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 12 Jul 2026 10:28:48 -0700 Subject: [PATCH 2/7] Add guard so that parameter casting to system type only occurs for PostgreSQL --- .../Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index e36d54b41d..a23869b9ff 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -87,7 +87,10 @@ public BaseSqlQueryStructure( /// public override string MakeDbConnectionParam(object? value, string? paramName = null, bool lengthOverride = false) { - if (!string.IsNullOrEmpty(paramName) && value is string stringValue && GetUnderlyingSourceDefinition().Columns.ContainsKey(paramName)) + if (MetadataProvider.GetDatabaseType() is DatabaseType.PostgreSQL && + !string.IsNullOrEmpty(paramName) && + value is string stringValue && + GetUnderlyingSourceDefinition().Columns.ContainsKey(paramName)) { Type columnSystemType = GetColumnSystemType(paramName); if (columnSystemType != typeof(string)) From de8b8bd168bae42be21e348c3be2e584fce9f29a Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Mon, 13 Jul 2026 09:39:15 -0700 Subject: [PATCH 3/7] Refine exception messages for database metadata queries to more accurately describe the source of the exception --- .../Services/MetadataProviders/SqlMetadataProvider.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index d9a71a1b66..412deb28d5 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -158,7 +158,7 @@ public virtual string GetSchemaName(string entityName) { if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject)) { - throw new DataApiBuilderException(message: $"Table Definition for {entityName} has not been inferred.", + throw new DataApiBuilderException(message: $"Database object for entity '{entityName}' has not been inferred.", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } @@ -176,7 +176,7 @@ public string GetDatabaseObjectName(string entityName) { if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject)) { - throw new DataApiBuilderException(message: $"Table Definition for {entityName} has not been inferred.", + throw new DataApiBuilderException(message: $"Database object for entity '{entityName}' has not been inferred.", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } @@ -189,7 +189,7 @@ public SourceDefinition GetSourceDefinition(string entityName) { if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject)) { - throw new DataApiBuilderException(message: $"Table Definition for {entityName} has not been inferred.", + throw new DataApiBuilderException(message: $"Source definition for entity '{entityName}' has not been inferred.", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } @@ -202,7 +202,7 @@ public StoredProcedureDefinition GetStoredProcedureDefinition(string entityName) { if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject)) { - throw new DataApiBuilderException(message: $"Stored Procedure Definition for {entityName} has not been inferred.", + throw new DataApiBuilderException(message: $"Stored procedure definition for entity '{entityName}' has not been inferred.", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } @@ -1706,7 +1706,7 @@ private async Task ValidateDatabaseConnection() /// /// Using a data adapter, obtains the schema of the given table name - /// and adds the corresponding entity in the data set. + /// and adds the corresponding DataTable to the entities data set. /// private async Task FillSchemaForTableAsync( string schemaName, From b0e0d708627090890269c96c34fa62360a49d1e3 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 15 Jul 2026 09:49:05 -0700 Subject: [PATCH 4/7] Update some comments and clean up some code --- src/Core/Services/GraphQLSchemaCreator.cs | 16 +++++----------- .../GraphQLTypes/SupportedTypes.cs | 4 ++-- .../Sql/SchemaConverter.cs | 6 +++--- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index a7b51d8827..d449c396c0 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -295,9 +295,9 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction Dictionary> rolesAllowedForFields = new(); SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); bool isStoredProcedure = entity.Source.Type is EntitySourceType.StoredProcedure; + EntityActionOperation operation = isStoredProcedure ? EntityActionOperation.Execute : EntityActionOperation.Read; foreach (string column in sourceDefinition.Columns.Keys) { - EntityActionOperation operation = isStoredProcedure ? EntityActionOperation.Execute : EntityActionOperation.Read; IEnumerable roles = _authorizationResolver.GetRolesForField(entityName, field: column, operation: operation); if (!rolesAllowedForFields.TryAdd(key: column, value: roles)) { @@ -309,7 +309,6 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction } } - // The roles allowed for Fields are the roles allowed to READ the fields, so any role that has a read definition for the field. // Only add objectTypeDefinition for GraphQL if it has a role definition defined for access. if (rolesAllowedForEntity.Any()) { @@ -397,23 +396,18 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes); } - // Return a list of all the object types to be exposed in the schema. - Dictionary fields = new(); - - // Add the DBOperationResult type to the schema NameNode nameNode = new(value: GraphQLUtils.DB_OPERATION_RESULT_TYPE); - FieldDefinitionNode field = GetDbOperationResultField(); - - fields.TryAdd(GraphQLUtils.DB_OPERATION_RESULT_FIELD_NAME, field); + // Add the DBOperationResult type to the schema objectTypes.Add(GraphQLUtils.DB_OPERATION_RESULT_TYPE, new ObjectTypeDefinitionNode( location: null, name: nameNode, description: null, new List(), new List(), - fields.Values.ToImmutableList())); + ImmutableList.Create(GetDbOperationResultField()))); + // Return a list of all the object types to be exposed in the schema. List nodes = new(objectTypes.Values); nodes.AddRange(enumTypes.Values); return new DocumentNode(nodes); @@ -748,7 +742,7 @@ private static FieldDefinitionNode GetDbOperationResultField() DocumentNode cosmosResult = GenerateCosmosGraphQLObjects(cosmosDataSourceNames, inputObjects); DocumentNode sqlResult = GenerateSqlGraphQLObjects(sql, inputObjects); // Create Root node with definitions from both cosmos and sql. - DocumentNode root = new(cosmosResult.Definitions.Concat(sqlResult.Definitions).ToImmutableList()); + DocumentNode root = cosmosResult.WithDefinitions(cosmosResult.Definitions.Concat(sqlResult.Definitions).ToImmutableList()); // Merge the inputobjectType definitions from cosmos and sql onto the root. return (root.WithDefinitions(root.Definitions.Concat(inputObjects.Values).ToImmutableList()), inputObjects); diff --git a/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs b/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs index c66c64a18d..1f29946fb4 100644 --- a/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs +++ b/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs @@ -6,7 +6,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes /// /// Only used to group the supported type names under a class with a relevant name. /// The type names mentioned here are Hotchocolate scalar built in types. - /// The corresponding SQL type name may be different for e.g. UUID maps to Guid as the SQL type. + /// The corresponding SQL type name may be different for e.g. UUID maps to Guid as the .NET type. /// public static class SupportedHotChocolateTypes { @@ -32,7 +32,6 @@ public static class SupportedHotChocolateTypes // new name so the generated schema does not depend on a deprecated scalar. public const string BYTEARRAY_TYPE = "Base64String"; public const string DATETIME_TYPE = "DateTime"; - public const string DATETIMEOFFSET_TYPE = "DateTimeOffset"; public const string LOCALTIME_TYPE = "LocalTime"; public const string TIME_TYPE = "Time"; } @@ -46,6 +45,7 @@ public static class SupportedDateTimeTypes public const string DATE_TYPE = "date"; public const string SMALLDATETIME_TYPE = "smalldatetime"; public const string DATETIME2_TYPE = "datetime2"; + public const string DATETIMEOFFSET_TYPE = "DateTimeOffset"; } /// diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 622376dc13..2de8f1f09f 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -39,8 +39,8 @@ public enum AggregationType /// Runtime config information for the table. /// Key/Value Collection mapping entity name to the entity object, /// currently used to lookup relationship metadata. - /// Roles to add to authorize directive at the object level (applies to query/read ops). - /// Roles to add to authorize directive at the field level (applies to mutations). + /// Roles to add to authorize directive at the object level. + /// Roles to add to authorize directive at the field level. /// A GraphQL object type to be provided to a Hot Chocolate GraphQL document. public static ObjectTypeDefinitionNode GenerateObjectTypeDefinitionForDatabaseObject( string entityName, @@ -179,7 +179,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView List directives = new(); if (sourceDefinition.PrimaryKey.Contains(columnName)) { - directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName, new ArgumentNode("databaseType", column.SystemType.Name))); + directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName)); } if (column.IsReadOnly) From a654dd53d8875e3ba11a1c27f70050e27a1bbae5 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 15 Jul 2026 13:32:19 -0700 Subject: [PATCH 5/7] Update tests to reflect modified error messages --- src/Service.Tests/Configuration/ConfigurationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 247685793f..8903f7ab77 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -1847,8 +1847,8 @@ public async Task TestSqlMetadataValidationForEntitiesWithInvalidSource() List exceptionMessagesList = configValidator.ConfigValidationExceptions.Select(x => x.Message).ToList(); Assert.IsTrue(exceptionMessagesList.Contains("The entity Book does not have a valid source object.")); Assert.IsTrue(exceptionMessagesList.Contains("The entity Publisher does not have a valid source object.")); - Assert.IsTrue(exceptionMessagesList.Contains("Table Definition for Book has not been inferred.")); - Assert.IsTrue(exceptionMessagesList.Contains("Table Definition for Publisher has not been inferred.")); + Assert.IsTrue(exceptionMessagesList.Contains("Database object for entity 'Book' has not been inferred.")); + Assert.IsTrue(exceptionMessagesList.Contains("Database object for entity 'Publisher' has not been inferred.")); Assert.IsTrue(exceptionMessagesList.Contains("Could not infer database object for source entity: Publisher in relationship: books. Check if the entity: Publisher is correctly defined in the config.")); Assert.IsTrue(exceptionMessagesList.Contains("Could not infer database object for target entity: Book in relationship: books. Check if the entity: Book is correctly defined in the config.")); } From 877e3c251ff0e3f768f762105a09a47579a211bf Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 15 Jul 2026 23:43:49 -0700 Subject: [PATCH 6/7] Add special parsing when the parameter is a .NET DateTime type --- .../Sql Query Structures/BaseSqlQueryStructure.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 6198c4343a..3544f6a2da 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -98,6 +98,14 @@ value is string stringValue && { value = GetParamAsSystemType(stringValue, paramName, columnSystemType); } + + // Npgsql requires DateTime with Kind=Unspecified for 'timestamp without time zone' columns. + // ParseParamAsSystemType returns Kind=Utc (via .UtcDateTime), which causes PostgreSQL to + // apply a UTC-to-local offset during comparison, producing incorrect filter results. + if (value is DateTime dtValue && dtValue.Kind == DateTimeKind.Utc && columnSystemType == typeof(DateTime)) + { + value = DateTime.SpecifyKind(dtValue, DateTimeKind.Unspecified); + } } return base.MakeDbConnectionParam(value, paramName, lengthOverride); From c4f7fc43739f7f4a5923ce78ab072ba886b3176c Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 15 Jul 2026 23:45:21 -0700 Subject: [PATCH 7/7] Unignore DateTime GQL tests for PostgreSQL and fix SQL query generation for PostgreSQL etsts --- .../PostgreSqlGQLSupportedTypesTests.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs index b9459668d7..d06c5dea3c 100644 --- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs @@ -67,7 +67,7 @@ public async Task PG_real_graphql_single_filter_expectedValues( [DataRow(BOOLEAN_TYPE, "'false'", "false")] [DataRow(STRING_TYPE, "lksa;jdflasdf;alsdflksdfkldj", "\"lksa;jdflasdf;alsdflksdfkldj\"")] [DataTestMethod] - public async Task PGSQL_real_graphql_in_filter_expectedValues( + public async Task PGSQL_graphql_in_filter_expectedValues( string type, string sqlValue, string gqlValue) @@ -99,7 +99,7 @@ protected override string MakeQueryOnTypeTable( string orderBy = "id", string limit = "1") { - string formattedSelect = limit.Equals("1") ? "SELECT to_jsonb(subq3) AS DATA" : "SELECT json_agg(to_jsonb(subq3)) AS DATA"; + string formattedSelect = limit.Equals("1") ? "SELECT to_jsonb(subq3) AS DATA" : "SELECT COALESCE(json_agg(to_jsonb(subq3)), '[]'::json) AS DATA"; return @" " + formattedSelect + @" @@ -141,15 +141,5 @@ private static string ProperlyFormatTypeTableColumn(string columnName) return columnName; } } - - /// - /// Bypass DateTime GQL tests for PostreSql - /// - [DataTestMethod] - [Ignore] - public new void QueryTypeColumnFilterAndOrderByDateTime(string type, string filterOperator, string sqlValue, string gqlValue, string queryOperator) - { - Assert.Inconclusive("Test skipped for PostgreSql."); - } } }