From e03f62137357a60b2868681063c81c0234c3e69e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:18:29 +0000 Subject: [PATCH 1/3] Initial plan From 9d03ad3219791fbc712598f2976d9db4c1843a84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:32:29 +0000 Subject: [PATCH 2/3] Fix OpenAPI format for date/datetime/time/uuid/byte types across all data sources - Add GetOpenApiFormatFromSystemType() to TypeHelper: maps CLR types + DbType to OpenAPI format strings (date-time, date, time, uuid, byte) per OAS 3.0 spec - Update CreateComponentSchema and CreateSpRequestComponentSchema in OpenApiDocumentor to populate the 'format' field using the new helper - Add DateOnly to _systemTypeToJsonDataTypeMap and _systemTypeToDbTypeMap so that PostgreSQL (Npgsql 8) and MySQL (MySqlConnector 2+) date columns work correctly - Add DateOnly to GetEdmPrimitiveTypeFromSystemType for OData $filter support - Add DateOnly to SchemaConverter.GetGraphQLTypeFromSystemType to prevent startup failures when GraphQL is enabled and date columns are present - Add unit tests covering all per-database CLR type / DbType combinations --- .../Services/OpenAPI/OpenApiDocumentor.cs | 3 + src/Core/Services/TypeHelper.cs | 57 +++++++++++++++++ .../Sql/SchemaConverter.cs | 1 + .../CLRtoJsonValueTypeUnitTests.cs | 61 +++++++++++++++++++ 4 files changed, 122 insertions(+) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 979da52eb6..a21d025e6d 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -1414,10 +1414,12 @@ private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary EdmPrimitiveTypeKind.DateTimeOffset, "DateTimeOffset" => EdmPrimitiveTypeKind.DateTimeOffset, "Date" => EdmPrimitiveTypeKind.Date, + "DateOnly" => EdmPrimitiveTypeKind.Date, "TimeOnly" => EdmPrimitiveTypeKind.TimeOfDay, "TimeSpan" => EdmPrimitiveTypeKind.TimeOfDay, _ => throw new ArgumentException($"Column type" + @@ -203,6 +206,60 @@ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromITypeNode(ITypeNode co return type; } + /// + /// Returns the OpenAPI format string for the given system type and optional DbType. + /// The format provides additional semantic information for string-typed fields in the OpenAPI schema. + /// + /// CLR type + /// Optional DbType used to distinguish date-only (DbType.Date) from date-time types. + /// + /// OpenAPI format string (e.g. "date-time", "date", "time", "uuid", "byte"), + /// or null if no standard format applies to the type. + public static string? GetOpenApiFormatFromSystemType(Type type, DbType? dbType = null) + { + // Resolve underlying type for nullable value types (e.g. DateTime? → DateTime). + Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type); + if (nullableUnderlyingType is not null) + { + type = nullableUnderlyingType; + } + + if (type == typeof(DateTime)) + { + // DbType.Date corresponds to a date-only SQL type (e.g. SQL Server "date"). + return dbType == DbType.Date ? "date" : "date-time"; + } + + if (type == typeof(DateOnly)) + { + // DateOnly is reported by Npgsql 8 (PostgreSQL "date") and MySqlConnector 2+ + // (MySQL "DATE") as the native CLR type for date-only columns. + return "date"; + } + + if (type == typeof(DateTimeOffset)) + { + return "date-time"; + } + + if (type == typeof(TimeOnly) || type == typeof(TimeSpan)) + { + return "time"; + } + + if (type == typeof(Guid)) + { + return "uuid"; + } + + if (type == typeof(byte[])) + { + return "byte"; + } + + return null; + } + /// /// Converts the .NET Framework (System/CLR) type to JsonDataType. /// Primitive data types in the OpenAPI standard (OAS) are based on the types supported diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 622376dc13..847a8b1522 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -573,6 +573,7 @@ public static string GetGraphQLTypeFromSystemType(Type type) "Decimal" => DECIMAL_TYPE, "Boolean" => BOOLEAN_TYPE, "DateTime" => DATETIME_TYPE, + "DateOnly" => DATETIME_TYPE, "DateTimeOffset" => DATETIME_TYPE, "Byte[]" => BYTEARRAY_TYPE, "TimeOnly" => LOCALTIME_TYPE, diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index c54fe6db3d..b3dd145b5e 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -3,6 +3,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Data; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Service.Exceptions; @@ -105,10 +106,70 @@ private static IEnumerable GetTestData_SupportedSystemTypesMapToJsonVa [DataRow(typeof(Guid?))] [DataRow(typeof(TimeOnly?))] [DataRow(typeof(TimeSpan?))] + [DataRow(typeof(DateOnly?))] [DataTestMethod] public void ResolveUnderlyingTypeForNullableValueType(Type nullableType) { Assert.AreNotEqual(notExpected: JsonDataType.Undefined, actual: TypeHelper.GetJsonDataTypeFromSystemType(nullableType)); Assert.IsNotNull(TypeHelper.GetDbTypeFromSystemType(nullableType)); } + + /// + /// Validates that TypeHelper.GetOpenApiFormatFromSystemType returns the expected + /// OpenAPI format string for each supported CLR type and DbType combination. + /// This covers all supported DAB data sources (MSSQL, PostgreSQL, MySQL): + /// - MSSQL: uses DbType.Date to distinguish SQL date from datetime/datetime2 + /// - PostgreSQL (Npgsql 8): date columns report DateOnly as their CLR type + /// - MySQL (MySqlConnector 2+): date columns report DateOnly as their CLR type + /// + [DataTestMethod] + // date-time: DateTime without DbType (PostgreSQL timestamp, MySQL datetime/timestamp) + [DataRow(typeof(DateTime), null, "date-time", + DisplayName = "DateTime with no DbType → date-time (PostgreSQL timestamp, MySQL datetime)")] + // date-time: DateTime with DateTime DbType (MSSQL datetime, smalldatetime) + [DataRow(typeof(DateTime), DbType.DateTime, "date-time", + DisplayName = "DateTime with DbType.DateTime → date-time (MSSQL datetime/smalldatetime)")] + // date-time: DateTime with DateTime2 DbType (MSSQL datetime2) + [DataRow(typeof(DateTime), DbType.DateTime2, "date-time", + DisplayName = "DateTime with DbType.DateTime2 → date-time (MSSQL datetime2)")] + // date: DateTime with Date DbType (MSSQL date type) + [DataRow(typeof(DateTime), DbType.Date, "date", + DisplayName = "DateTime with DbType.Date → date (MSSQL date)")] + // date: DateOnly (PostgreSQL date via Npgsql 8, MySQL DATE via MySqlConnector 2+) + [DataRow(typeof(DateOnly), null, "date", + DisplayName = "DateOnly with no DbType → date (PostgreSQL date/Npgsql 8, MySQL DATE/MySqlConnector 2+)")] + // date: nullable DateOnly? resolves to DateOnly + [DataRow(typeof(DateOnly?), null, "date", + DisplayName = "DateOnly? → date (nullable date column)")] + // date-time: DateTimeOffset (MSSQL datetimeoffset, PostgreSQL timestamptz) + [DataRow(typeof(DateTimeOffset), null, "date-time", + DisplayName = "DateTimeOffset → date-time (MSSQL datetimeoffset, PostgreSQL timestamptz)")] + // date-time: nullable DateTime? resolves to DateTime + [DataRow(typeof(DateTime?), DbType.DateTime, "date-time", + DisplayName = "DateTime? → date-time (nullable datetime column)")] + // time: TimeOnly (MSSQL time, PostgreSQL time, MySQL time) + [DataRow(typeof(TimeOnly), null, "time", + DisplayName = "TimeOnly → time (MSSQL/PostgreSQL/MySQL time columns)")] + // time: TimeSpan maps to time as well + [DataRow(typeof(TimeSpan), null, "time", + DisplayName = "TimeSpan → time")] + // uuid: Guid (MSSQL uniqueidentifier, PostgreSQL uuid, MySQL char(36)) + [DataRow(typeof(Guid), null, "uuid", + DisplayName = "Guid → uuid (MSSQL uniqueidentifier, PostgreSQL uuid)")] + // byte: byte[] (MSSQL binary/varbinary/image/timestamp, PostgreSQL bytea, MySQL binary/varbinary/blob) + [DataRow(typeof(byte[]), null, "byte", + DisplayName = "byte[] → byte (binary/bytea columns)")] + // No format for plain string, int, bool, etc. + [DataRow(typeof(string), null, null, + DisplayName = "string → null (no format)")] + [DataRow(typeof(int), null, null, + DisplayName = "int → null (no format)")] + [DataRow(typeof(bool), null, null, + DisplayName = "bool → null (no format)")] + public void GetOpenApiFormatFromSystemType_ReturnsExpectedFormat(Type type, DbType? dbType, string? expectedFormat) + { + string? actualFormat = TypeHelper.GetOpenApiFormatFromSystemType(type, dbType); + Assert.AreEqual(expectedFormat, actualFormat, + $"Expected OpenAPI format '{expectedFormat ?? "null"}' for type {type.Name} with DbType {dbType?.ToString() ?? "null"}."); + } } From a198d4a26396d53fbb718217d531770e0f2e42da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:36:46 +0000 Subject: [PATCH 3/3] Fix code review: use null format (not empty string) and fix XML doc href --- src/Core/Services/OpenAPI/OpenApiDocumentor.cs | 4 ++-- src/Core/Services/TypeHelper.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index a21d025e6d..11b0345eb4 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -1486,13 +1486,13 @@ private static OpenApiSchema CreateComponentSchema( if (metadataProvider.TryGetBackingColumn(entityName, field, out string? backingColumnValue) && !string.IsNullOrEmpty(backingColumnValue)) { string typeMetadata = string.Empty; - string formatMetadata = string.Empty; + string? formatMetadata = null; string? fieldDescription = null; if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef)) { typeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower(); - formatMetadata = TypeHelper.GetOpenApiFormatFromSystemType(columnDef.SystemType, columnDef.DbType) ?? string.Empty; + formatMetadata = TypeHelper.GetOpenApiFormatFromSystemType(columnDef.SystemType, columnDef.DbType); } if (entityConfig?.Fields != null) diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 8386c4efd0..55ee022fcc 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -212,7 +212,7 @@ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromITypeNode(ITypeNode co /// /// CLR type /// Optional DbType used to distinguish date-only (DbType.Date) from date-time types. - /// + /// /// OpenAPI format string (e.g. "date-time", "date", "time", "uuid", "byte"), /// or null if no standard format applies to the type. public static string? GetOpenApiFormatFromSystemType(Type type, DbType? dbType = null)