Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement DateOnly/TimeOnly #2051

Merged
merged 1 commit into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dapper/Dapper.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<PackageTags>orm;sql;micro-orm</PackageTags>
<Description>A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..</Description>
<Authors>Sam Saffron;Marc Gravell;Nick Craver</Authors>
<TargetFrameworks>net461;netstandard2.0;net5.0;net7.0</TargetFrameworks>
<TargetFrameworks>net461;netstandard2.0;net5.0;net6.0;net7.0</TargetFrameworks>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
Expand Down
6 changes: 6 additions & 0 deletions Dapper/PublicAPI/net6.0/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#nullable enable
Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask
Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable<dynamic!>!
Dapper.SqlMapper.GridReader.ReadUnbufferedAsync<T>() -> System.Collections.Generic.IAsyncEnumerable<T>!
static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable<dynamic!>!
static Dapper.SqlMapper.QueryUnbufferedAsync<T>(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable<T>!
1 change: 1 addition & 0 deletions Dapper/PublicAPI/net6.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
55 changes: 33 additions & 22 deletions Dapper/SqlMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ public TypeMapEntry(DbType dbType, TypeMapEntryFlags flags)
public bool Equals(TypeMapEntry other) => other.DbType == DbType && other.Flags == Flags;
public static readonly TypeMapEntry
DoNotSet = new((DbType)(-2), TypeMapEntryFlags.None),
DoNotSetFieldValue = new((DbType)(-2), TypeMapEntryFlags.UseGetFieldValue),
DecimalFieldValue = new(DbType.Decimal, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue),
StringFieldValue = new(DbType.String, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue),
BinaryFieldValue = new(DbType.Binary, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue);
Expand All @@ -202,7 +203,11 @@ public static readonly TypeMapEntry

static SqlMapper()
{
typeMap = new Dictionary<Type, TypeMapEntry>(41)
typeMap = new Dictionary<Type, TypeMapEntry>(41
#if NET6_0_OR_GREATER
+ 4 // {Date|Time}Only[?]
#endif
)
{
[typeof(byte)] = DbType.Byte,
[typeof(sbyte)] = DbType.SByte,
Expand Down Expand Up @@ -245,6 +250,12 @@ static SqlMapper()
[typeof(SqlDecimal?)] = TypeMapEntry.DecimalFieldValue,
[typeof(SqlMoney)] = TypeMapEntry.DecimalFieldValue,
[typeof(SqlMoney?)] = TypeMapEntry.DecimalFieldValue,
#if NET6_0_OR_GREATER
[typeof(DateOnly)] = TypeMapEntry.DoNotSetFieldValue,
[typeof(TimeOnly)] = TypeMapEntry.DoNotSetFieldValue,
[typeof(DateOnly?)] = TypeMapEntry.DoNotSetFieldValue,
[typeof(TimeOnly?)] = TypeMapEntry.DoNotSetFieldValue,
#endif
};
ResetTypeHandlers(false);
}
Expand All @@ -257,7 +268,7 @@ static SqlMapper()
[MemberNotNull(nameof(typeHandlers))]
private static void ResetTypeHandlers(bool clone)
{
typeHandlers = new Dictionary<Type, ITypeHandler>();
typeHandlers = [];
AddTypeHandlerImpl(typeof(DataTable), new DataTableHandler(), clone);
AddTypeHandlerImpl(typeof(XmlDocument), new XmlDocumentHandler(), clone);
AddTypeHandlerImpl(typeof(XDocument), new XDocumentHandler(), clone);
Expand Down Expand Up @@ -370,10 +381,10 @@ public static void AddTypeHandlerImpl(Type type, ITypeHandler? handler, bool clo
var newCopy = clone ? new Dictionary<Type, ITypeHandler>(snapshot) : snapshot;

#pragma warning disable 618
typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache<int>.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, new object?[] { handler });
typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache<int>.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]);
if (secondary is not null)
{
typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache<int>.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, new object?[] { handler });
typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache<int>.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]);
}
#pragma warning restore 618
if (handler is null)
Expand Down Expand Up @@ -1240,7 +1251,7 @@ internal enum Row
SingleOrDefault = 3
}

private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = Array.Empty<int>();
private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = [];
private static void ThrowMultipleRows(Row row)
{
_ = row switch
Expand Down Expand Up @@ -2538,7 +2549,7 @@ internal static IList<LiteralToken> GetLiteralTokens(string sql)
filterParams = !CompiledRegex.LegacyParameter.IsMatch(identity.Sql);
}

var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true);
var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, [typeof(IDbCommand), typeof(object)], type, true);

var il = dm.GetILGenerator();

Expand Down Expand Up @@ -2909,7 +2920,7 @@ internal static IList<LiteralToken> GetLiteralTokens(string sql)
{
if (locals is null)
{
locals = new Dictionary<Type, LocalBuilder>();
locals = [];
local = null;
}
else
Expand Down Expand Up @@ -2946,14 +2957,14 @@ internal static IList<LiteralToken> GetLiteralTokens(string sql)
{
typeof(bool), typeof(sbyte), typeof(byte), typeof(ushort), typeof(short),
typeof(uint), typeof(int), typeof(ulong), typeof(long), typeof(float), typeof(double), typeof(decimal)
}.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), new[] { typeof(IFormatProvider) })!);
}.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), [typeof(IFormatProvider)])!);

private static MethodInfo? GetToString(TypeCode typeCode)
{
return toStrings.TryGetValue(typeCode, out MethodInfo? method) ? method : null;
}

private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), new Type[] { typeof(string), typeof(string) })!,
private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), [typeof(string), typeof(string)])!,
InvariantCulture = typeof(CultureInfo).GetProperty(nameof(CultureInfo.InvariantCulture), BindingFlags.Public | BindingFlags.Static)!.GetGetMethod()!;

private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action<IDbCommand, object?>? paramReader)
Expand Down Expand Up @@ -3117,7 +3128,7 @@ private static DbDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefi
return factory(index);
}
// cache of ReadViaGetFieldValueFactory<T> for per-value T
static readonly Hashtable s_ReadViaGetFieldValueCache = new();
static readonly Hashtable s_ReadViaGetFieldValueCache = [];

static Func<DbDataReader, object> UnderlyingReadViaGetFieldValueFactory<T>(int index)
=> reader => reader.IsDBNull(index) ? null! : reader.GetFieldValue<T>(index)!;
Expand Down Expand Up @@ -3147,14 +3158,14 @@ private static T Parse<T>(object? value)
}

private static readonly MethodInfo
enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), new Type[] { typeof(Type), typeof(string), typeof(bool) })!,
enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), [typeof(Type), typeof(string), typeof(bool)])!,
getItem = typeof(DbDataReader).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int))
.Select(p => p.GetGetMethod()).First()!,
getFieldValueT = typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetFieldValue),
BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(int) }, null)!,
BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!,
isDbNull = typeof(DbDataReader).GetMethod(nameof(DbDataReader.IsDBNull),
BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(int) }, null)!;
BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!;

/// <summary>
/// Gets type-map for the given type
Expand Down Expand Up @@ -3191,7 +3202,7 @@ public static ITypeMap GetTypeMap(Type type)
}

// use Hashtable to get free lockless reading
private static readonly Hashtable _typeMaps = new();
private static readonly Hashtable _typeMaps = [];

/// <summary>
/// Set custom mapping for type deserializers
Expand Down Expand Up @@ -3263,7 +3274,7 @@ public static void SetTypeMap(Type type, ITypeMap? map)
private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary<Type, LocalBuilder>? locals, Type type, bool initAndLoad)
{
if (type is null) throw new ArgumentNullException(nameof(type));
locals ??= new Dictionary<Type, LocalBuilder>();
locals ??= [];
if (!locals.TryGetValue(type, out LocalBuilder? found))
{
found = il.DeclareLocal(type);
Expand Down Expand Up @@ -3294,7 +3305,7 @@ private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary<Type, Lo
}

var returnType = type.IsValueType ? typeof(object) : type;
var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(DbDataReader) }, type, true);
var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, [typeof(DbDataReader)], type, true);
var il = dm.GetILGenerator();

if (IsValueTuple(type))
Expand Down Expand Up @@ -3403,7 +3414,7 @@ private static void GenerateValueTupleDeserializer(Type valueTupleType, DbDataRe

if (nullableUnderlyingType is not null)
{
var nullableTupleConstructor = valueTupleType.GetConstructor(new[] { nullableUnderlyingType });
var nullableTupleConstructor = valueTupleType.GetConstructor([nullableUnderlyingType]);

il.Emit(OpCodes.Newobj, nullableTupleConstructor!);
}
Expand Down Expand Up @@ -3668,7 +3679,7 @@ private static void LoadReaderValueViaGetFieldValue(ILGenerator il, int index, T
if (underlyingType != memberType)
{
// Nullable<T>; wrap it
il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { underlyingType })!); // stack is now [...][T?]
il.Emit(OpCodes.Newobj, memberType.GetConstructor([underlyingType])!); // stack is now [...][T?]
}
}

Expand Down Expand Up @@ -3731,13 +3742,13 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind

if (nullUnderlyingType is not null)
{
il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })!); // stack is now [...][typed-value]
il.Emit(OpCodes.Newobj, memberType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value]
}
}
else if (memberType.FullName == LinqBinary)
{
il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [...][byte-array]
il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) })!);// stack is now [...][binary]
il.Emit(OpCodes.Newobj, memberType.GetConstructor([typeof(byte[])])!);// stack is now [...][binary]
}
else
{
Expand All @@ -3762,7 +3773,7 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind
FlexibleConvertBoxedFromHeadOfStack(il, colType, nullUnderlyingType ?? unboxType, null);
if (nullUnderlyingType is not null)
{
il.Emit(OpCodes.Newobj, unboxType.GetConstructor(new[] { nullUnderlyingType })!); // stack is now [...][typed-value]
il.Emit(OpCodes.Newobj, unboxType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value]
}
}
}
Expand Down Expand Up @@ -3846,7 +3857,7 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro
il.Emit(OpCodes.Ldtoken, via ?? to); // stack is now [target][target][value][member-type-token]
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null); // stack is now [target][target][value][member-type]
il.EmitCall(OpCodes.Call, InvariantCulture, null); // stack is now [target][target][value][member-type][culture]
il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ChangeType), new Type[] { typeof(object), typeof(Type), typeof(IFormatProvider) })!, null); // stack is now [target][target][boxed-member-type-value]
il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ChangeType), [typeof(object), typeof(Type), typeof(IFormatProvider)])!, null); // stack is now [target][target][boxed-member-type-value]
il.Emit(OpCodes.Unbox_Any, to); // stack is now [target][target][typed-value]
}
}
Expand Down
67 changes: 67 additions & 0 deletions tests/Dapper.Tests/DateTimeOnlyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Threading.Tasks;
using Xunit;

#if NET6_0_OR_GREATER
namespace Dapper.Tests;

/* we do **NOT** expect this to work against System.Data
[Collection("DateTimeOnlyTests")]
public sealed class SystemSqlClientDateTimeOnlyTests : DateTimeOnlyTests<SystemSqlClientProvider> { }
*/
#if MSSQLCLIENT
[Collection("DateTimeOnlyTests")]
public sealed class MicrosoftSqlClientDateTimeOnlyTests : DateTimeOnlyTests<MicrosoftSqlClientProvider> { }
#endif
public abstract class DateTimeOnlyTests<TProvider> : TestBase<TProvider> where TProvider : DatabaseProvider
{
public class HazDateTimeOnly
{
public DateOnly Date { get; set; }
public TimeOnly Time { get; set; }
}

[Fact]
public void TypedInOut()
{
var now = DateTime.Now;
var args = new HazDateTimeOnly
{
Date = DateOnly.FromDateTime(now),
Time = TimeOnly.FromDateTime(now),
};
var row = connection.QuerySingle<HazDateTimeOnly>("select @date as [Date], @time as [Time]", args);
Assert.Equal(args.Date, row.Date);
Assert.Equal(args.Time, row.Time);
}

[Fact]
public async Task TypedInOutAsync()
{
var now = DateTime.Now;
var args = new HazDateTimeOnly
{
Date = DateOnly.FromDateTime(now),
Time = TimeOnly.FromDateTime(now),
};
var row = await connection.QuerySingleAsync<HazDateTimeOnly>("select @date as [Date], @time as [Time]", args);
Assert.Equal(args.Date, row.Date);
Assert.Equal(args.Time, row.Time);
}

[Fact]
public void UntypedInOut()
{
var now = DateTime.Now;
var args = new DynamicParameters();
var date = DateOnly.FromDateTime(now);
var time = TimeOnly.FromDateTime(now);
args.Add("date", date);
args.Add("time", time);
var row = connection.QuerySingle<dynamic>("select @date as [Date], @time as [Time]", args);
// untyped, observation is that these come back as DateTime and TimeSpan
Assert.Equal(date, DateOnly.FromDateTime((DateTime)row.Date));
Assert.Equal(time, TimeOnly.FromTimeSpan((TimeSpan)row.Time));
}
}
#endif
2 changes: 1 addition & 1 deletion tests/Dapper.Tests/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,7 @@ public void Issue1164_OverflowExceptionForUInt64()

private class Issue1164Object<T>
{
public T Value;
public T Value = default!;
}

internal record struct One(int OID);
Expand Down