From 6f07936beef44965d01335fcf11842a83b993aec Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Fri, 13 Mar 2026 11:27:28 -0700 Subject: [PATCH 01/12] Add support for parsing DateTime values --- src/Core/Services/ExecutionHelper.cs | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs index fc2e3e9da1..6562d4a333 100644 --- a/src/Core/Services/ExecutionHelper.cs +++ b/src/Core/Services/ExecutionHelper.cs @@ -397,9 +397,74 @@ private static bool TryGetPropertyFromParent( SupportedHotChocolateTypes.SINGLE_TYPE => value is IntValueNode intValueNode ? intValueNode.ToSingle() : ((FloatValueNode)value).ToSingle(), SupportedHotChocolateTypes.FLOAT_TYPE => value is IntValueNode intValueNode ? intValueNode.ToDouble() : ((FloatValueNode)value).ToDouble(), SupportedHotChocolateTypes.DECIMAL_TYPE => value is IntValueNode intValueNode ? intValueNode.ToDecimal() : ((FloatValueNode)value).ToDecimal(), + SupportedHotChocolateTypes.DATETIME_TYPE => ParseDateTimeValue(value.Value), + SupportedHotChocolateTypes.DATETIMEOFFSET_TYPE => ParseDateTimeOffsetValue(value.Value), SupportedHotChocolateTypes.UUID_TYPE => Guid.TryParse(value.Value!.ToString(), out Guid guidValue) ? guidValue : value.Value, _ => value.Value }; + + static object? ParseDateTimeValue(object? raw) + { + if (raw is null) + { + return null; + } + + if (raw is DateTime dt) + { + return dt; + } + + if (raw is DateTimeOffset dto) + { + return dto.UtcDateTime; + } + + if (raw is string s) + { + // HotChocolate DateTime inputs are supplied as strings; parse them so DB providers + // (notably PostgreSQL) receive a typed parameter instead of text. + if (DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset parsedDto)) + { + return parsedDto.UtcDateTime; + } + + if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsedDt)) + { + return parsedDt; + } + } + + return raw; + } + + static object? ParseDateTimeOffsetValue(object? raw) + { + if (raw is null) + { + return null; + } + + if (raw is DateTimeOffset dto) + { + return dto; + } + + if (raw is DateTime dt) + { + return new DateTimeOffset(dt); + } + + if (raw is string s) + { + if (DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset parsedDto)) + { + return parsedDto; + } + } + + return raw; + } } /// From 25b40081dc9e007a096d7107d73b189360ebf795 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Tue, 21 Apr 2026 08:52:32 -0700 Subject: [PATCH 02/12] Update parsing to reflect that only support (for now) is for DATETIME_TYPE and ensure that timezone is configured to UTC for standardization --- src/Core/Resolvers/PostgreSqlExecutor.cs | 5 +++ src/Core/Services/ExecutionHelper.cs | 51 ++++++------------------ 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/Core/Resolvers/PostgreSqlExecutor.cs b/src/Core/Resolvers/PostgreSqlExecutor.cs index 70fa0f1079..3c525329c7 100644 --- a/src/Core/Resolvers/PostgreSqlExecutor.cs +++ b/src/Core/Resolvers/PostgreSqlExecutor.cs @@ -84,6 +84,11 @@ private void ConfigurePostgreSqlQueryExecutor() { NpgsqlConnectionStringBuilder builder = new(dataSource.ConnectionString); + if (string.IsNullOrEmpty(builder.Timezone)) + { + builder.Timezone = "UTC"; + } + if (_runtimeConfigProvider.IsLateConfigured) { builder.SslMode = SslMode.VerifyFull; diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs index 6562d4a333..beabdbbcd1 100644 --- a/src/Core/Services/ExecutionHelper.cs +++ b/src/Core/Services/ExecutionHelper.cs @@ -63,7 +63,7 @@ public async ValueTask ExecuteQueryAsync(IMiddlewareContext context) IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(ds.DatabaseType); IDictionary parameters = GetParametersFromContext(context); - + if (context.Selection.Type.IsListType()) { Tuple, IMetadata?> result = @@ -398,7 +398,6 @@ private static bool TryGetPropertyFromParent( SupportedHotChocolateTypes.FLOAT_TYPE => value is IntValueNode intValueNode ? intValueNode.ToDouble() : ((FloatValueNode)value).ToDouble(), SupportedHotChocolateTypes.DECIMAL_TYPE => value is IntValueNode intValueNode ? intValueNode.ToDecimal() : ((FloatValueNode)value).ToDecimal(), SupportedHotChocolateTypes.DATETIME_TYPE => ParseDateTimeValue(value.Value), - SupportedHotChocolateTypes.DATETIMEOFFSET_TYPE => ParseDateTimeOffsetValue(value.Value), SupportedHotChocolateTypes.UUID_TYPE => Guid.TryParse(value.Value!.ToString(), out Guid guidValue) ? guidValue : value.Value, _ => value.Value }; @@ -412,7 +411,12 @@ private static bool TryGetPropertyFromParent( if (raw is DateTime dt) { - return dt; + return dt.Kind switch + { + DateTimeKind.Utc => dt, + DateTimeKind.Unspecified => DateTime.SpecifyKind(dt, DateTimeKind.Utc), + _ => dt.ToUniversalTime() + }; } if (raw is DateTimeOffset dto) @@ -423,44 +427,15 @@ private static bool TryGetPropertyFromParent( if (raw is string s) { // HotChocolate DateTime inputs are supplied as strings; parse them so DB providers - // (notably PostgreSQL) receive a typed parameter instead of text. - if (DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset parsedDto)) + // (notably PostgreSQL) receive a typed UTC parameter instead of text. + if (DateTimeOffset.TryParse( + s, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out DateTimeOffset parsedDto)) { return parsedDto.UtcDateTime; } - - if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsedDt)) - { - return parsedDt; - } - } - - return raw; - } - - static object? ParseDateTimeOffsetValue(object? raw) - { - if (raw is null) - { - return null; - } - - if (raw is DateTimeOffset dto) - { - return dto; - } - - if (raw is DateTime dt) - { - return new DateTimeOffset(dt); - } - - if (raw is string s) - { - if (DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset parsedDto)) - { - return parsedDto; - } } return raw; From c986ce416b04bd9058462f94455e2ea7dd74caaf Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 22 Apr 2026 12:29:30 -0700 Subject: [PATCH 03/12] Unit tests to verify correctness --- .../UnitTests/ExecutionHelperUnitTests.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs diff --git a/src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs b/src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs new file mode 100644 index 0000000000..5c1547cb25 --- /dev/null +++ b/src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Azure.DataApiBuilder.Service.Services; +using HotChocolate.Execution; +using HotChocolate.Language; +using HotChocolate.Types; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Azure.DataApiBuilder.Service.Tests.UnitTests; + +[TestClass] +public class ExecutionHelperUnitTests +{ + [TestMethod] + public void ExtractValueFromIValueNode_DateTimeLiteral_ReturnsUtcDateTime() + { + Mock argumentSchema = CreateArgumentSchema(new DateTimeType()); + Mock variables = new(); + + object result = ExecutionHelper.ExtractValueFromIValueNode( + new StringValueNode("2026-04-22T10:15:30-07:00"), + argumentSchema.Object, + variables.Object); + + Assert.IsInstanceOfType(result); + Assert.AreEqual( + new DateTime(2026, 4, 22, 17, 15, 30, DateTimeKind.Utc), + (DateTime)result); + } + + [TestMethod] + public void ExtractValueFromIValueNode_DateTimeVariable_ReturnsUtcDateTime() + { + Mock argumentSchema = CreateArgumentSchema(new DateTimeType()); + Mock variables = new(); + variables + .Setup(v => v.GetValue("createdAt")) + .Returns(new StringValueNode("2026-04-22T10:15:30Z")); + + object result = ExecutionHelper.ExtractValueFromIValueNode( + new VariableNode("createdAt"), + argumentSchema.Object, + variables.Object); + + Assert.IsInstanceOfType(result); + Assert.AreEqual( + new DateTime(2026, 4, 22, 10, 15, 30, DateTimeKind.Utc), + (DateTime)result); + } + + private static Mock CreateArgumentSchema(IInputType type) + { + Mock argumentSchema = new(); + argumentSchema.SetupGet(a => a.Type).Returns(type); + return argumentSchema; + } +} \ No newline at end of file From 573320505ff82467b58a79d413b3056084ad894b Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 22 Apr 2026 12:30:19 -0700 Subject: [PATCH 04/12] Remove unnecessary call to DateTimeStyles.AdjustToUniversal because we are anyways returning UtcDateTime --- src/Core/Services/ExecutionHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs index beabdbbcd1..c49349362a 100644 --- a/src/Core/Services/ExecutionHelper.cs +++ b/src/Core/Services/ExecutionHelper.cs @@ -431,7 +431,7 @@ private static bool TryGetPropertyFromParent( if (DateTimeOffset.TryParse( s, CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + DateTimeStyles.AssumeUniversal, out DateTimeOffset parsedDto)) { return parsedDto.UtcDateTime; From 2ba861d5aae0d99ff58736c079a22f7754862c5e Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 22 Apr 2026 14:45:43 -0700 Subject: [PATCH 05/12] Add a TODO comment to look into support for SupportedHotChocolateTypes.DATETIMEOFFSET_TYPE --- src/Core/Services/ExecutionHelper.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs index c49349362a..b2cb430ee0 100644 --- a/src/Core/Services/ExecutionHelper.cs +++ b/src/Core/Services/ExecutionHelper.cs @@ -388,6 +388,9 @@ private static bool TryGetPropertyFromParent( return value.Value; } + // TODO: Look into why there is no support for SupportedHotChocolateTypes.DATETIMEOFFSET_TYPE translation in argumentSchema. + // This is more of a semantic issue than a logical one because we can still do appropriate parsing even if every date/time + // (with offset or not) is treated as SupportedHotChocolateTypes.DATETIME_TYPE but it is worth looking into regardless. return argumentSchema.Type.TypeName() switch { SupportedHotChocolateTypes.BYTE_TYPE => ((IntValueNode)value).ToByte(), From 6faa834fc3691e542685d034517925b1ccee1296 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Thu, 23 Apr 2026 11:21:28 -0700 Subject: [PATCH 06/12] Add integration tests to test end-to-end flow Co-authored-by: Copilot --- .../PostgreSqlGQLSupportedTypesTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs index b9459668d7..61123182c8 100644 --- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs @@ -86,6 +86,29 @@ public async Task PGSQL_real_graphql_in_filter_expectedValues( await QueryTypeColumnFilterAndOrderBy(type, "in", sqlValue, gqlValue, "IN"); } + /// + /// PostgreSQL datetime filter tests with timezone offsets. + /// Verifies that GraphQL datetime arguments are normalized to UTC before filtering. + /// + [DataRow(DATETIME_TYPE, "eq", "'1999-01-08 10:23:54'", "\"1999-01-08T05:23:54-05:00\"", "=", + DisplayName = "DateTime filter eq converts -05:00 offset to UTC.")] + [DataRow(DATETIME_TYPE, "gte", "'1999-01-08 10:23:54'", "\"1999-01-08T05:23:54-05:00\"", ">=", + DisplayName = "DateTime filter gte converts -05:00 offset to UTC.")] + [DataRow(DATETIME_TYPE, "eq", "'1999-01-08 10:23:54'", "\"1999-01-08T15:53:54+05:30\"", "=", + DisplayName = "DateTime filter eq converts +05:30 offset to UTC.")] + [DataRow(DATETIME_TYPE, "eq", "'1999-01-08 10:23:54'", "\"1999-01-08T10:23:54Z\"", "=", + DisplayName = "DateTime filter eq preserves UTC input.")] + [DataTestMethod] + public async Task PGSQL_real_graphql_datetime_filter_offset_expectedValues( + string type, + string filterOperator, + string sqlValue, + string gqlValue, + string queryOperator) + { + await QueryTypeColumnFilterAndOrderBy(type, filterOperator, sqlValue, gqlValue, queryOperator); + } + protected override string MakeQueryOnTypeTable(List queryFields, int id) { return MakeQueryOnTypeTable(queryFields, filterValue: id.ToString(), filterField: "id"); From ad50cd9e8471acf0add7f48d3924a99e218e0322 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 26 Apr 2026 16:06:38 -0700 Subject: [PATCH 07/12] Remove todo comment --- src/Core/Services/ExecutionHelper.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs index b2cb430ee0..c49349362a 100644 --- a/src/Core/Services/ExecutionHelper.cs +++ b/src/Core/Services/ExecutionHelper.cs @@ -388,9 +388,6 @@ private static bool TryGetPropertyFromParent( return value.Value; } - // TODO: Look into why there is no support for SupportedHotChocolateTypes.DATETIMEOFFSET_TYPE translation in argumentSchema. - // This is more of a semantic issue than a logical one because we can still do appropriate parsing even if every date/time - // (with offset or not) is treated as SupportedHotChocolateTypes.DATETIME_TYPE but it is worth looking into regardless. return argumentSchema.Type.TypeName() switch { SupportedHotChocolateTypes.BYTE_TYPE => ((IntValueNode)value).ToByte(), From 8d17086289747566572518251c22d00f4b91a33a Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 29 Apr 2026 13:54:05 -0700 Subject: [PATCH 08/12] Added some new integration tests for DateTime GraphQL filtering Co-authored-by: Copilot --- .../PostgreSqlGQLSupportedTypesTests.cs | 125 +++++++++++++++++- 1 file changed, 120 insertions(+), 5 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs index 61123182c8..02e4d16870 100644 --- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes.SupportedDateTimeTypes; @@ -89,15 +90,26 @@ public async Task PGSQL_real_graphql_in_filter_expectedValues( /// /// PostgreSQL datetime filter tests with timezone offsets. /// Verifies that GraphQL datetime arguments are normalized to UTC before filtering. + /// Tests all comparison operators (eq, neq, gt, gte, lt, lte) with offset and offset-less inputs. /// [DataRow(DATETIME_TYPE, "eq", "'1999-01-08 10:23:54'", "\"1999-01-08T05:23:54-05:00\"", "=", - DisplayName = "DateTime filter eq converts -05:00 offset to UTC.")] - [DataRow(DATETIME_TYPE, "gte", "'1999-01-08 10:23:54'", "\"1999-01-08T05:23:54-05:00\"", ">=", - DisplayName = "DateTime filter gte converts -05:00 offset to UTC.")] + DisplayName = "DateTime eq converts -05:00 offset to UTC.")] [DataRow(DATETIME_TYPE, "eq", "'1999-01-08 10:23:54'", "\"1999-01-08T15:53:54+05:30\"", "=", - DisplayName = "DateTime filter eq converts +05:30 offset to UTC.")] + DisplayName = "DateTime eq converts +05:30 offset to UTC.")] [DataRow(DATETIME_TYPE, "eq", "'1999-01-08 10:23:54'", "\"1999-01-08T10:23:54Z\"", "=", - DisplayName = "DateTime filter eq preserves UTC input.")] + DisplayName = "DateTime eq preserves UTC input.")] + [DataRow(DATETIME_TYPE, "eq", "'1999-01-08 10:23:54'", "\"1999-01-08T10:23:54\"", "=", + DisplayName = "DateTime eq treats offset-less input as UTC.")] + [DataRow(DATETIME_TYPE, "neq", "'1999-01-08 10:23:54'", "\"1999-01-08T05:23:54-05:00\"", "!=", + DisplayName = "DateTime neq converts -05:00 offset to UTC.")] + [DataRow(DATETIME_TYPE, "gt", "'1999-01-08 10:23:53'", "\"1999-01-08T05:23:53-05:00\"", ">", + DisplayName = "DateTime gt converts -05:00 offset to UTC.")] + [DataRow(DATETIME_TYPE, "gte", "'1999-01-08 10:23:54'", "\"1999-01-08T05:23:54-05:00\"", ">=", + DisplayName = "DateTime gte converts -05:00 offset to UTC.")] + [DataRow(DATETIME_TYPE, "lt", "'1999-01-08 10:23:55'", "\"1999-01-08T05:23:55-05:00\"", "<", + DisplayName = "DateTime lt converts -05:00 offset to UTC.")] + [DataRow(DATETIME_TYPE, "lte", "'1999-01-08 10:23:54'", "\"1999-01-08T05:23:54-05:00\"", "<=", + DisplayName = "DateTime lte converts -05:00 offset to UTC.")] [DataTestMethod] public async Task PGSQL_real_graphql_datetime_filter_offset_expectedValues( string type, @@ -109,6 +121,90 @@ public async Task PGSQL_real_graphql_datetime_filter_offset_expectedValues( await QueryTypeColumnFilterAndOrderBy(type, filterOperator, sqlValue, gqlValue, queryOperator); } + [TestMethod] + public async Task PGSQL_real_graphql_datetime_in_filter_offset_expectedValues() + { + const string field = "datetime_types"; + string gqlQuery = @"{ + supportedTypes(first: 100, orderBy: { typeid: ASC }, filter: { + " + field + @": { + in: [""1999-01-08T05:23:54-05:00"", ""1900-01-01T00:00:00Z""] + } + }) { + items { + typeid, " + field + @" + } + } + }"; + + string dbQuery = MakeQueryOnTypeTable( + queryFields: new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, + filterValue: "('1999-01-08 10:23:54', '1900-01-01 00:00:00')", + filterOperator: "IN", + filterField: field, + orderBy: "id", + limit: "100"); + + JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, "supportedTypes", isAuthenticated: false); + string expected = await GetDatabaseResultAsync(dbQuery); + + PerformTestEqualsForExtendedTypes(DATETIME_TYPE, expected, actual.GetProperty("items").ToString()); + } + + [TestMethod] + public async Task PGSQL_real_graphql_datetime_and_filter_offset_expectedValues() + { + const string field = "datetime_types"; + string gqlQuery = @"{ + supportedTypes(first: 100, orderBy: { typeid: ASC }, filter: { + and: [ + { " + field + @": { gte: ""1999-01-08T05:23:54-05:00"" } }, + { " + field + @": { lte: ""1999-01-08T05:23:54-05:00"" } } + ] + }) { + items { + typeid, " + field + @" + } + } + }"; + + string dbQuery = MakeQueryOnTypeTableWithWhereClause( + queryFields: new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, + whereClause: field + " >= '1999-01-08 10:23:54' AND " + field + " <= '1999-01-08 10:23:54'"); + + JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, "supportedTypes", isAuthenticated: false); + string expected = await GetDatabaseResultAsync(dbQuery); + + PerformTestEqualsForExtendedTypes(DATETIME_TYPE, expected, actual.GetProperty("items").ToString()); + } + + [TestMethod] + public async Task PGSQL_real_graphql_datetime_or_filter_offset_expectedValues() + { + const string field = "datetime_types"; + string gqlQuery = @"{ + supportedTypes(first: 100, orderBy: { typeid: ASC }, filter: { + or: [ + { " + field + @": { eq: ""1900-01-01T00:00:00Z"" } }, + { " + field + @": { eq: ""1999-01-08T05:23:54-05:00"" } } + ] + }) { + items { + typeid, " + field + @" + } + } + }"; + + string dbQuery = MakeQueryOnTypeTableWithWhereClause( + queryFields: new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, + whereClause: field + " = '1900-01-01 00:00:00' OR " + field + " = '1999-01-08 10:23:54'"); + + JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, "supportedTypes", isAuthenticated: false); + string expected = await GetDatabaseResultAsync(dbQuery); + + PerformTestEqualsForExtendedTypes(DATETIME_TYPE, expected, actual.GetProperty("items").ToString()); + } + protected override string MakeQueryOnTypeTable(List queryFields, int id) { return MakeQueryOnTypeTable(queryFields, filterValue: id.ToString(), filterField: "id"); @@ -165,6 +261,25 @@ private static string ProperlyFormatTypeTableColumn(string columnName) } } + private static string MakeQueryOnTypeTableWithWhereClause( + List queryFields, + string whereClause, + string orderBy = "id", + string limit = "100") + { + string formattedSelect = limit.Equals("1") ? "SELECT to_jsonb(subq3) AS DATA" : "SELECT json_agg(to_jsonb(subq3)) AS DATA"; + + return @" + " + formattedSelect + @" + FROM + (SELECT " + string.Join(", ", queryFields.Select(field => ProperlyFormatTypeTableColumn(field.BackingColumnName) + $" AS {field.Alias}")) + @" + FROM public.type_table AS table0 + WHERE " + whereClause + @" + ORDER BY " + orderBy + @" asc + LIMIT " + limit + @") AS subq3 + "; + } + /// /// Bypass DateTime GQL tests for PostreSql /// From fbc4e07215074726c57a5f46d9917dbebbaa0bbd Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 29 Apr 2026 14:22:00 -0700 Subject: [PATCH 09/12] Remove some integration tests Co-authored-by: Copilot --- .../PostgreSqlGQLSupportedTypesTests.cs | 104 ------------------ 1 file changed, 104 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs index 02e4d16870..8ed1c63210 100644 --- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes.SupportedDateTimeTypes; @@ -121,90 +120,6 @@ public async Task PGSQL_real_graphql_datetime_filter_offset_expectedValues( await QueryTypeColumnFilterAndOrderBy(type, filterOperator, sqlValue, gqlValue, queryOperator); } - [TestMethod] - public async Task PGSQL_real_graphql_datetime_in_filter_offset_expectedValues() - { - const string field = "datetime_types"; - string gqlQuery = @"{ - supportedTypes(first: 100, orderBy: { typeid: ASC }, filter: { - " + field + @": { - in: [""1999-01-08T05:23:54-05:00"", ""1900-01-01T00:00:00Z""] - } - }) { - items { - typeid, " + field + @" - } - } - }"; - - string dbQuery = MakeQueryOnTypeTable( - queryFields: new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, - filterValue: "('1999-01-08 10:23:54', '1900-01-01 00:00:00')", - filterOperator: "IN", - filterField: field, - orderBy: "id", - limit: "100"); - - JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, "supportedTypes", isAuthenticated: false); - string expected = await GetDatabaseResultAsync(dbQuery); - - PerformTestEqualsForExtendedTypes(DATETIME_TYPE, expected, actual.GetProperty("items").ToString()); - } - - [TestMethod] - public async Task PGSQL_real_graphql_datetime_and_filter_offset_expectedValues() - { - const string field = "datetime_types"; - string gqlQuery = @"{ - supportedTypes(first: 100, orderBy: { typeid: ASC }, filter: { - and: [ - { " + field + @": { gte: ""1999-01-08T05:23:54-05:00"" } }, - { " + field + @": { lte: ""1999-01-08T05:23:54-05:00"" } } - ] - }) { - items { - typeid, " + field + @" - } - } - }"; - - string dbQuery = MakeQueryOnTypeTableWithWhereClause( - queryFields: new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, - whereClause: field + " >= '1999-01-08 10:23:54' AND " + field + " <= '1999-01-08 10:23:54'"); - - JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, "supportedTypes", isAuthenticated: false); - string expected = await GetDatabaseResultAsync(dbQuery); - - PerformTestEqualsForExtendedTypes(DATETIME_TYPE, expected, actual.GetProperty("items").ToString()); - } - - [TestMethod] - public async Task PGSQL_real_graphql_datetime_or_filter_offset_expectedValues() - { - const string field = "datetime_types"; - string gqlQuery = @"{ - supportedTypes(first: 100, orderBy: { typeid: ASC }, filter: { - or: [ - { " + field + @": { eq: ""1900-01-01T00:00:00Z"" } }, - { " + field + @": { eq: ""1999-01-08T05:23:54-05:00"" } } - ] - }) { - items { - typeid, " + field + @" - } - } - }"; - - string dbQuery = MakeQueryOnTypeTableWithWhereClause( - queryFields: new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, - whereClause: field + " = '1900-01-01 00:00:00' OR " + field + " = '1999-01-08 10:23:54'"); - - JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, "supportedTypes", isAuthenticated: false); - string expected = await GetDatabaseResultAsync(dbQuery); - - PerformTestEqualsForExtendedTypes(DATETIME_TYPE, expected, actual.GetProperty("items").ToString()); - } - protected override string MakeQueryOnTypeTable(List queryFields, int id) { return MakeQueryOnTypeTable(queryFields, filterValue: id.ToString(), filterField: "id"); @@ -261,25 +176,6 @@ private static string ProperlyFormatTypeTableColumn(string columnName) } } - private static string MakeQueryOnTypeTableWithWhereClause( - List queryFields, - string whereClause, - string orderBy = "id", - string limit = "100") - { - string formattedSelect = limit.Equals("1") ? "SELECT to_jsonb(subq3) AS DATA" : "SELECT json_agg(to_jsonb(subq3)) AS DATA"; - - return @" - " + formattedSelect + @" - FROM - (SELECT " + string.Join(", ", queryFields.Select(field => ProperlyFormatTypeTableColumn(field.BackingColumnName) + $" AS {field.Alias}")) + @" - FROM public.type_table AS table0 - WHERE " + whereClause + @" - ORDER BY " + orderBy + @" asc - LIMIT " + limit + @") AS subq3 - "; - } - /// /// Bypass DateTime GQL tests for PostreSql /// From c6eba206c20a0309407fe9fb32551f8aab64dc7c Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 3 May 2026 20:08:19 -0700 Subject: [PATCH 10/12] Fix PostgreSQL query construction in the case of no matching rows Co-authored-by: Copilot --- .../PostgreSqlGQLSupportedTypesTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs index 8ed1c63210..15f886cdd5 100644 --- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs @@ -133,7 +133,9 @@ 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 + @" From 0310910fadb82eb630d092e6c042137237a00370 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 3 May 2026 20:10:22 -0700 Subject: [PATCH 11/12] Prevent bypass of DateTime GQL tests for PostgreSQL --- .../PostgreSqlGQLSupportedTypesTests.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs index 15f886cdd5..70954b23fb 100644 --- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs @@ -177,15 +177,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."); - } } } From bafffc5ea6e664e28aa30b881a24eae75f460df9 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Wed, 6 May 2026 11:45:47 -0700 Subject: [PATCH 12/12] Fix formatting issues flagged by Azure pipelines --- src/Core/Services/ExecutionHelper.cs | 2 +- src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs index c49349362a..86e67b889c 100644 --- a/src/Core/Services/ExecutionHelper.cs +++ b/src/Core/Services/ExecutionHelper.cs @@ -63,7 +63,7 @@ public async ValueTask ExecuteQueryAsync(IMiddlewareContext context) IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(ds.DatabaseType); IDictionary parameters = GetParametersFromContext(context); - + if (context.Selection.Type.IsListType()) { Tuple, IMetadata?> result = diff --git a/src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs b/src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs index 5c1547cb25..953ef0353b 100644 --- a/src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs +++ b/src/Service.Tests/UnitTests/ExecutionHelperUnitTests.cs @@ -57,4 +57,4 @@ private static Mock CreateArgumentSchema(IInputType type) argumentSchema.SetupGet(a => a.Type).Returns(type); return argumentSchema; } -} \ No newline at end of file +}