Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Core/Services/OpenAPI/OpenApiDocumentor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1414,10 +1414,12 @@ private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary<string, P
string parameter = kvp.Key;
ParameterDefinition def = kvp.Value;
string typeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(def.SystemType).ToString().ToLower();
string? formatMetadata = TypeHelper.GetOpenApiFormatFromSystemType(def.SystemType, def.DbType);

properties.Add(parameter, new OpenApiSchema()
{
Type = typeMetadata,
Format = formatMetadata,
Description = def.Description,
Default = def.Default is not null ? new OpenApiString(def.Default) : null
});
Expand Down Expand Up @@ -1484,12 +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);
}

if (entityConfig?.Fields != null)
Expand Down
57 changes: 57 additions & 0 deletions src/Core/Services/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static class TypeHelper
[typeof(byte[])] = DbType.Binary,
[typeof(TimeOnly)] = DbType.Time,
[typeof(TimeSpan)] = DbType.Time,
[typeof(DateOnly)] = DbType.Date,
[typeof(object)] = DbType.Object
};

Expand Down Expand Up @@ -77,6 +78,7 @@ public static class TypeHelper
[typeof(TimeOnly)] = JsonDataType.String,
[typeof(object)] = JsonDataType.Object,
[typeof(DateTime)] = JsonDataType.String,
[typeof(DateOnly)] = JsonDataType.String,
[typeof(DateTimeOffset)] = JsonDataType.String
};

Expand Down Expand Up @@ -157,6 +159,7 @@ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromSystemType(Type column
"DateTime" => EdmPrimitiveTypeKind.DateTimeOffset,
"DateTimeOffset" => EdmPrimitiveTypeKind.DateTimeOffset,
"Date" => EdmPrimitiveTypeKind.Date,
"DateOnly" => EdmPrimitiveTypeKind.Date,
"TimeOnly" => EdmPrimitiveTypeKind.TimeOfDay,
"TimeSpan" => EdmPrimitiveTypeKind.TimeOfDay,
_ => throw new ArgumentException($"Column type" +
Expand Down Expand Up @@ -203,6 +206,60 @@ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromITypeNode(ITypeNode co
return type;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="type">CLR type</param>
/// <param name="dbType">Optional DbType used to distinguish date-only (DbType.Date) from date-time types.</param>
/// <seealso href="https://spec.openapis.org/oas/v3.0.1#data-types"/>
/// <returns>OpenAPI format string (e.g. "date-time", "date", "time", "uuid", "byte"),
/// or null if no standard format applies to the type.</returns>
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;
}

/// <summary>
/// Converts the .NET Framework (System/CLR) type to JsonDataType.
/// Primitive data types in the OpenAPI standard (OAS) are based on the types supported
Expand Down
1 change: 1 addition & 0 deletions src/Service.GraphQLBuilder/Sql/SchemaConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -105,10 +106,70 @@ private static IEnumerable<object[]> 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));
}

/// <summary>
/// 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
/// </summary>
[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"}.");
}
}