Skip to content

Commit

Permalink
implement DateOnly/TimeOnly (#2051)
Browse files Browse the repository at this point in the history
fix #1715
adds net6 TFM
  • Loading branch information
mgravell committed Mar 7, 2024
1 parent 87eb033 commit 402f20c
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 24 deletions.
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

0 comments on commit 402f20c

Please sign in to comment.