Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
f6d4483
add DateTime and DateTimeOffset types to the system type to json type…
seantleonard Jun 21, 2023
070e419
Update JsonValueType handling for OpenApiDocumentor, and improved sql…
seantleonard Jun 23, 2023
4296015
Cherry pick from working branch to mitigate big merge conflict resolu…
seantleonard Jul 6, 2023
110afbe
Addressing review feedback: used file scoped namespaces. updated enum…
seantleonard Jul 6, 2023
04319b4
Merge branch 'main' into dev/seantleonard/openapi_systemtojsondatatypes
seantleonard Jul 6, 2023
191bace
Merge branch 'main' into dev/seantleonard/openapi_systemtojsondatatypes
seantleonard Jul 7, 2023
afda68d
add additional unit test demonstrating proper resolution of nullable …
seantleonard Jul 7, 2023
29a8b8e
Address review feedback: revert filescoped namespace change to highli…
seantleonard Jul 7, 2023
1f826e9
Merge branch 'dev/seantleonard/openapi_systemtojsondatatypes' of http…
seantleonard Jul 7, 2023
9e914ab
Update explanation that SQL Server data types also apply to Azure SQL DB
seantleonard Jul 7, 2023
19186d9
clearer comments.
seantleonard Jul 7, 2023
9e3c9b6
Updated failure messages and comments for CLRtoJsonValueTypeUnitTests;
seantleonard Jul 7, 2023
e816f4f
removed [typeof(DateTimeOffset)] = DbType.DateTimeOffset, from _syst…
seantleonard Jul 7, 2023
53cd4ca
update indentation on sqltypeconstants and add additional test to cov…
seantleonard Jul 10, 2023
647e6f9
Merge branch 'main' into dev/seantleonard/openapi_systemtojsondatatypes
seantleonard Jul 10, 2023
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
52 changes: 52 additions & 0 deletions src/Core/Models/SqlTypeConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Service.Models;

/// <summary>
/// String literal representation of SQL Server data types that are returned by Microsoft.Data.SqlClient.
/// </summary>
/// <seealso cref="https://github.com/dotnet/SqlClient/blob/main/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Config/Constants.cs"/>
public static class SqlTypeConstants
{
/// <summary>
/// SqlDbType names ordered by corresponding SqlDbType.
/// Keys are lower case to match formatting of SQL Server INFORMATION_SCHEMA DATA_TYPE column value.
/// Sourced directly from internal/private SmiMetaData.cs in Microsoft.Data.SqlClient
/// Value indicates whether DAB engine supports the SqlDbType.
/// </summary>
/// <seealso cref="https://github.com/dotnet/SqlClient/blob/2b31810ce69b88d707450e2059ee8fbde63f774f/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SmiMetaData.cs#L637-L674"/>
public static readonly Dictionary<string, bool> SupportedSqlDbTypes = new()
{
{ "bigint", true }, // SqlDbType.BigInt
{ "binary", true }, // SqlDbType.Binary
{ "bit", true }, // SqlDbType.Bit
{ "char", true }, // SqlDbType.Char
{ "datetime", true }, // SqlDbType.DateTime
{ "decimal", true }, // SqlDbType.Decimal
{ "float", true }, // SqlDbType.Float
{ "image", true }, // SqlDbType.Image
{ "int", true }, // SqlDbType.Int
{ "money", true }, // SqlDbType.Money
{ "nchar", true }, // SqlDbType.NChar
{ "ntext", true }, // SqlDbType.NText
{ "nvarchar", true }, // SqlDbType.NVarChar
{ "real", true }, // SqlDbType.Real
{ "uniqueidentifier", true }, // SqlDbType.UniqueIdentifier
{ "smalldatetime", true }, // SqlDbType.SmallDateTime
{ "smallint", true }, // SqlDbType.SmallInt
{ "smallmoney", true }, // SqlDbType.SmallMoney
{ "text", true }, // SqlDbType.Text
{ "timestamp", true }, // SqlDbType.Timestamp
{ "tinyint", true }, // SqlDbType.TinyInt
{ "varbinary", true }, // SqlDbType.VarBinary
{ "varchar", true }, // SqlDbType.VarChar
{ "sql_variant", false }, // SqlDbType.Variant (unsupported)
{ "xml", false }, // SqlDbType.Xml (unsupported)
{ "date", true }, // SqlDbType.Date
{ "time", true }, // SqlDbType.Time
{ "datetime2", true }, // SqlDbType.DateTime2
{ "datetimeoffset", true }, // SqlDbType.DateTimeOffset
{ "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported)
};
}
54 changes: 4 additions & 50 deletions src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Service.Exceptions;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -34,57 +32,13 @@ public override string GetDefaultSchemaName()
}

/// <summary>
/// Takes a string version of an MS SQL data type and returns its .NET common language runtime (CLR) counterpart
/// As per https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-data-type-mappings
/// Takes a string version of an SQL Server data type (also applies to Azure SQL DB)
/// and returns its .NET common language runtime (CLR) counterpart
/// As per https://docs.microsoft.com/dotnet/framework/data/adonet/sql-server-data-type-mappings
/// </summary>
public override Type SqlToCLRType(string sqlType)
{
switch (sqlType)
{
case "bigint":
case "numeric":
return typeof(decimal);
case "bit":
return typeof(bool);
case "smallint":
return typeof(short);
case "real":
case "decimal":
case "smallmoney":
case "money":
return typeof(decimal);
case "int":
return typeof(int);
case "tinyint":
return typeof(byte);
case "float":
return typeof(float);
case "date":
case "datetime2":
case "smalldatetime":
case "datetime":
case "time":
return typeof(DateTime);
case "datetimeoffset":
return typeof(DateTimeOffset);
case "char":
case "varchar":
case "text":
case "nchar":
case "nvarchar":
case "ntext":
return typeof(string);
case "binary":
case "varbinary":
case "image":
return typeof(byte[]);
case "uniqueidentifier":
return typeof(Guid);
default:
throw new DataApiBuilderException(message: $"Tried to convert unsupported data type: {sqlType}",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
}
return TypeHelper.GetSystemTypeFromSqlDbType(sqlType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ private async Task FillSchemaForStoredProcedureAsync(
foreach (DataRow row in parameterMetadata.Rows)
{
// row["DATA_TYPE"] has value type string so a direct cast to System.Type is not supported.
// See https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-data-type-mappings
Type systemType = SqlToCLRType((string)row["DATA_TYPE"]);
// Add to parameters dictionary without the leading @ sign
storedProcedureDefinition.Parameters.TryAdd(((string)row["PARAMETER_NAME"])[1..],
Expand Down
6 changes: 3 additions & 3 deletions src/Core/Services/OpenAPI/OpenApiDocumentor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ private Tuple<string, List<OpenApiParameter>> CreatePrimaryKeyPathComponentAndPa
{
OpenApiSchema parameterSchema = new()
{
Type = columnDef is not null ? TypeHelper.SystemTypeToJsonDataType(columnDef.SystemType) : string.Empty
Type = columnDef is not null ? TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower() : string.Empty
};

OpenApiParameter openApiParameter = new()
Expand Down Expand Up @@ -855,9 +855,9 @@ private OpenApiSchema CreateComponentSchema(string entityName, HashSet<string> f
{
string typeMetadata = string.Empty;
string formatMetadata = string.Empty;
if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef) && columnDef is not null)
if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef))
{
typeMetadata = TypeHelper.SystemTypeToJsonDataType(columnDef.SystemType).ToString().ToLower();
typeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower();
}

properties.Add(field, new OpenApiSchema()
Expand Down
148 changes: 110 additions & 38 deletions src/Core/Services/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@
// Licensed under the MIT License.

using System.Data;
using System.Net;
using Azure.DataApiBuilder.Core.Services.OpenAPI;
using Azure.DataApiBuilder.Service.Exceptions;

namespace Azure.DataApiBuilder.Core.Services
{
/// <summary>
/// Helper class used to resolve CLR Type to the associated DbType or JsonDataType
/// Type mapping helpers to convert between SQL Server types, .NET Framework types, and Json value types.
/// </summary>
/// <seealso cref="https://learn.microsoft.com/dotnet/framework/data/adonet/sql-server-data-type-mappings"/>
public static class TypeHelper
{
/// <summary>
/// Maps .NET Framework types to DbType enum
/// </summary>
private static Dictionary<Type, DbType> _systemTypeToDbTypeMap = new()
{
[typeof(byte)] = DbType.Byte,
Expand Down Expand Up @@ -47,22 +53,10 @@ public static class TypeHelper
};

/// <summary>
/// Returns the DbType for given system type.
/// </summary>
/// <param name="systemType">The system type for which the DbType is to be determined.</param>
/// <returns>DbType for the given system type.</returns>
public static DbType? GetDbTypeFromSystemType(Type systemType)
{
if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType))
{
return null;
}

return dbType;
}

/// <summary>
/// Enables lookup of JsonDataType given a CLR Type.
/// Maps .NET Framework type (System/CLR type) to JsonDataType.
/// Unnecessary to add nullable types because GetJsonDataTypeFromSystemType()
/// (the helper used to access key/values in this dictionary)
/// resolves the underlying type when a nullable type is used for lookup.
/// </summary>
private static Dictionary<Type, JsonDataType> _systemTypeToJsonDataTypeMap = new()
{
Expand All @@ -82,40 +76,118 @@ public static class TypeHelper
[typeof(char)] = JsonDataType.String,
[typeof(Guid)] = JsonDataType.String,
[typeof(byte[])] = JsonDataType.String,
[typeof(byte?)] = JsonDataType.String,
[typeof(sbyte?)] = JsonDataType.String,
[typeof(short?)] = JsonDataType.Number,
[typeof(ushort?)] = JsonDataType.Number,
[typeof(int?)] = JsonDataType.Number,
[typeof(uint?)] = JsonDataType.Number,
[typeof(long?)] = JsonDataType.Number,
[typeof(ulong?)] = JsonDataType.Number,
[typeof(float?)] = JsonDataType.Number,
[typeof(double?)] = JsonDataType.Number,
[typeof(decimal?)] = JsonDataType.Number,
[typeof(bool?)] = JsonDataType.Boolean,
[typeof(char?)] = JsonDataType.String,
[typeof(Guid?)] = JsonDataType.String,
[typeof(object)] = JsonDataType.Object
[typeof(TimeSpan)] = JsonDataType.String,
[typeof(object)] = JsonDataType.Object,
[typeof(DateTime)] = JsonDataType.String,
[typeof(DateTimeOffset)] = JsonDataType.String
};

/// <summary>
/// Maps SqlDbType enum to .NET Framework type (System type).
/// </summary>
private static readonly Dictionary<SqlDbType, Type> _sqlDbTypeToType = new()
{
[SqlDbType.BigInt] = typeof(long),
[SqlDbType.Binary] = typeof(byte),
[SqlDbType.Bit] = typeof(bool),
[SqlDbType.Char] = typeof(string),
[SqlDbType.Date] = typeof(DateTime),
[SqlDbType.DateTime] = typeof(DateTime),
[SqlDbType.DateTime2] = typeof(DateTime),
[SqlDbType.DateTimeOffset] = typeof(DateTimeOffset),
[SqlDbType.Decimal] = typeof(decimal),
[SqlDbType.Float] = typeof(double),
[SqlDbType.Image] = typeof(byte[]),
[SqlDbType.Int] = typeof(int),
[SqlDbType.Money] = typeof(decimal),
[SqlDbType.NChar] = typeof(char),
[SqlDbType.NText] = typeof(string),
[SqlDbType.NVarChar] = typeof(string),
[SqlDbType.Real] = typeof(float),
[SqlDbType.SmallDateTime] = typeof(DateTime),
[SqlDbType.SmallInt] = typeof(short),
[SqlDbType.SmallMoney] = typeof(decimal),
[SqlDbType.Text] = typeof(string),
[SqlDbType.Time] = typeof(TimeSpan),
[SqlDbType.Timestamp] = typeof(byte[]),
[SqlDbType.TinyInt] = typeof(byte),
[SqlDbType.UniqueIdentifier] = typeof(Guid),
[SqlDbType.VarBinary] = typeof(byte[]),
[SqlDbType.VarChar] = typeof(string)
};

/// <summary>
/// Converts the CLR type to JsonDataType
/// to meet the data type requirement set by the OpenAPI specification.
/// The value returned is formatted for the OpenAPI spec "type" property.
/// Converts the .NET Framework (System/CLR) type to JsonDataType.
/// Primitive data types in the OpenAPI standard (OAS) are based on the types supported
/// by the JSON Schema Specification Wright Draft 00.
/// The value returned is formatted for use in the OpenAPI spec "type" property.
/// </summary>
/// <param name="type">CLR type</param>
/// <seealso cref="https://spec.openapis.org/oas/v3.0.1#data-types"/>
/// <returns>Formatted JSON type name in lower case: e.g. number, string, boolean, etc.</returns>
public static string SystemTypeToJsonDataType(Type type)
public static JsonDataType GetJsonDataTypeFromSystemType(Type type)
{
// Get the underlying type argument if the 'type' argument is a nullable type.
Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type);

// Will not be null when the input argument 'type' is a closed generic nullable type.
if (nullableUnderlyingType is not null)
{
type = nullableUnderlyingType;
}

if (!_systemTypeToJsonDataTypeMap.TryGetValue(type, out JsonDataType openApiJsonTypeName))
{
openApiJsonTypeName = JsonDataType.Undefined;
}

string formattedOpenApiTypeName = openApiJsonTypeName.ToString().ToLower();
return formattedOpenApiTypeName;
return openApiJsonTypeName;
}

/// <summary>
/// Returns the DbType for given system type.
/// </summary>
/// <param name="systemType">The system type for which the DbType is to be determined.</param>
/// <returns>DbType for the given system type. Null when no mapping exists.</returns>
public static DbType? GetDbTypeFromSystemType(Type systemType)
{
if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType))
{
return null;
}

return dbType;
}

/// <summary>
/// Converts the string representation of a SQL Server data type that can be parsed into SqlDbType enum
/// to the corrsponding .NET Framework/CLR type as documented by the SQL Server data type mappings article.
/// The SQL Server database engine type and SqlDbType enum map 1:1 when character casing is ignored.
/// e.g. SQL DB type 'bigint' maps to SqlDbType enum 'BigInt' in a case-insensitive match.
/// There are some mappings in the SQL Server data type mappings table which do not map after ignoring casing, however
/// those mappings are outdated and don't accommodate newly added SqlDbType enum values.
/// e.g. The documentation table shows SQL server type 'binary' maps to SqlDbType enum 'VarBinary',
/// however SqlDbType.Binary now exists.
/// </summary>
/// <param name="dbTypeName">String value sourced from the DATA_TYPE column in the Procedure Parameters or Columns
/// schema collections.</param>
/// <seealso cref="https://learn.microsoft.com/dotnet/framework/data/adonet/sql-server-schema-collections#columns"/>
/// <seealso cref="https://learn.microsoft.com/dotnet/framework/data/adonet/sql-server-schema-collections#procedure-parameters"/>
/// <exception>Failed type conversion.</exception>"
public static Type GetSystemTypeFromSqlDbType(string sqlDbTypeName)
{
if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType))
{
if (_sqlDbTypeToType.TryGetValue(sqlDbType, out Type? value))
{
return value;
}
}

throw new DataApiBuilderException(
message: $"Tried to convert unsupported data type: {sqlDbTypeName}",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError);
}
}
}
Loading