Skip to content
Open
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
96 changes: 96 additions & 0 deletions Insight.Database.Core/CodeGenerator/DbParameterGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ static class DbParameterGenerator
{ typeof(DateTime?), DbType.DateTime },
{ typeof(DateTimeOffset?), DbType.DateTimeOffset },
{ typeof(TimeSpan?), DbType.Time },
#if NET6_0_OR_GREATER
{ typeof(DateOnly), DbType.Date },
{ typeof(DateOnly?), DbType.Date },
{ typeof(TimeOnly), DbType.Time },
{ typeof(TimeOnly?), DbType.Time },
#endif
{ TypeHelper.LinqBinaryType, DbType.Binary },
};

Expand Down Expand Up @@ -297,6 +303,27 @@ private static Action<IDbCommand, object> CreateClassInputParameterGenerator(IDb
Label readyToSetLabel = il.DefineLabel();
ClassPropInfo.EmitGetValue(type, mapping.PathToMember, il, readyToSetLabel);

#if NET6_0_OR_GREATER
// Convert DateOnly to DateTime and TimeOnly to TimeSpan FIRST
// so that TimeOnly→TimeSpan can then be converted to DateTime if needed
var underlyingType = Nullable.GetUnderlyingType(memberType) ?? memberType;
if (underlyingType == typeof(DateOnly))
{
// Call helper method to convert DateOnly to DateTime
il.Emit(OpCodes.Call, typeof(DbParameterGenerator).GetMethod("ConvertDateOnlyToDateTime", BindingFlags.NonPublic | BindingFlags.Static));
}
else if (underlyingType == typeof(TimeOnly))
{
// Call helper method to convert TimeOnly to TimeSpan
il.Emit(OpCodes.Call, typeof(DbParameterGenerator).GetMethod("ConvertTimeOnlyToTimeSpan", BindingFlags.NonPublic | BindingFlags.Static));
}
else if (underlyingType == typeof(object))
{
// For object type, check runtime type and convert if needed
il.Emit(OpCodes.Call, typeof(DbParameterGenerator).GetMethod("ConvertDateTimeOnlyTypes", BindingFlags.NonPublic | BindingFlags.Static));
}
#endif

// special conversions for timespan to datetime
if ((sqlType == DbType.Time && dbParameter.DbType != DbType.Time) ||
(dbParameter.DbType == DbType.DateTime || dbParameter.DbType == DbType.DateTime2 || dbParameter.DbType == DbType.DateTimeOffset))
Expand Down Expand Up @@ -468,6 +495,57 @@ private static void SetParameterStringValue(IDbDataParameter parameter, object v
}
}

#if NET6_0_OR_GREATER
/// <summary>
/// Convert DateOnly to DateTime.
/// </summary>
/// <param name="value">The DateOnly value.</param>
/// <returns>The DateTime value.</returns>
private static object ConvertDateOnlyToDateTime(object value)
{
if (value == null || value is DBNull)
return DBNull.Value;

var dateOnly = (DateOnly)value;
return dateOnly.ToDateTime(TimeOnly.MinValue);
}

/// <summary>
/// Convert TimeOnly to TimeSpan.
/// </summary>
/// <param name="value">The TimeOnly value.</param>
/// <returns>The TimeSpan value.</returns>
private static object ConvertTimeOnlyToTimeSpan(object value)
{
if (value == null || value is DBNull)
return DBNull.Value;

var timeOnly = (TimeOnly)value;
return timeOnly.ToTimeSpan();
}

/// <summary>
/// Convert DateOnly/TimeOnly to DateTime/TimeSpan if needed (checks runtime type).
/// </summary>
/// <param name="value">The value to potentially convert.</param>
/// <returns>The converted value or original value.</returns>
private static object ConvertDateTimeOnlyTypes(object value)
{
if (value == null || value is DBNull)
return value;

var valueType = value.GetType();
var underlyingType = Nullable.GetUnderlyingType(valueType) ?? valueType;

if (underlyingType == typeof(DateOnly))
return ConvertDateOnlyToDateTime(value);
else if (underlyingType == typeof(TimeOnly))
return ConvertTimeOnlyToTimeSpan(value);

return value;
}
#endif

/// <summary>
/// Create a parameter generator for a dynamic object.
/// </summary>
Expand Down Expand Up @@ -510,6 +588,24 @@ private static Action<IDbCommand, object> CreateDynamicInputParameterGenerator(I
}
}

// Convert DateOnly/TimeOnly to DateTime/TimeSpan before setting parameter value
#if NET6_0_OR_GREATER
if (value != null && !(value is DBNull))
{
var valueType = value.GetType();
var underlyingType = Nullable.GetUnderlyingType(valueType) ?? valueType;

if (underlyingType == typeof(DateOnly))
{
value = ConvertDateOnlyToDateTime(value);
}
else if (underlyingType == typeof(TimeOnly))
{
value = ConvertTimeOnlyToTimeSpan(value);
}
}
#endif

p.Value = value;

// if it's a string, fill in the length
Expand Down
22 changes: 22 additions & 0 deletions Insight.Database.Core/CodeGenerator/DbReaderDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ static DbReaderDeserializer()
_simpleDeserializers.TryAdd(typeof(DateTime), GetValueDeserializer<DateTime>());
_simpleDeserializers.TryAdd(typeof(DateTimeOffset), GetValueDeserializer<DateTimeOffset>());
_simpleDeserializers.TryAdd(typeof(TimeSpan), GetValueDeserializer(typeof(TimeSpan)));
#if NET6_0_OR_GREATER
_simpleDeserializers.TryAdd(typeof(DateOnly), GetValueDeserializer(typeof(DateOnly)));
_simpleDeserializers.TryAdd(typeof(TimeOnly), GetValueDeserializer(typeof(TimeOnly)));
#endif

_simpleDeserializers.TryAdd(typeof(byte?), GetValueDeserializer<byte?>());
_simpleDeserializers.TryAdd(typeof(short?), GetValueDeserializer<short?>());
Expand All @@ -85,6 +89,10 @@ static DbReaderDeserializer()
_simpleDeserializers.TryAdd(typeof(DateTime?), GetValueDeserializer<DateTime?>());
_simpleDeserializers.TryAdd(typeof(DateTimeOffset?), GetValueDeserializer<DateTimeOffset?>());
_simpleDeserializers.TryAdd(typeof(TimeSpan?), GetValueDeserializer(typeof(TimeSpan?)));
#if NET6_0_OR_GREATER
_simpleDeserializers.TryAdd(typeof(DateOnly?), GetValueDeserializer(typeof(DateOnly?)));
_simpleDeserializers.TryAdd(typeof(TimeOnly?), GetValueDeserializer(typeof(TimeOnly?)));
#endif
}
#endregion

Expand Down Expand Up @@ -251,6 +259,20 @@ private static Delegate CreateValueDeserializer(Type type)
// convert whatever object it is to a boxed timespan
il.Emit(OpCodes.Call, typeof(TypeConverterGenerator).GetMethod("SqlObjectToTimeSpan"));
}
#if NET6_0_OR_GREATER
else if (type == typeof(DateOnly) || type == typeof(DateOnly?))
{
// convert DateTime to DateOnly
il.Emit(OpCodes.Unbox_Any, typeof(DateTime));
il.Emit(OpCodes.Call, typeof(DateOnly).GetMethod("FromDateTime", new[] { typeof(DateTime) }));
il.Emit(OpCodes.Box, typeof(DateOnly));
}
else if (type == typeof(TimeOnly) || type == typeof(TimeOnly?))
{
// convert SQL value (DateTime or TimeSpan) to TimeOnly
il.Emit(OpCodes.Call, typeof(TypeConverterGenerator).GetMethod("SqlObjectToTimeOnly"));
}
#endif

// not null, so unbox it and return it
if (underlyingType != null)
Expand Down
55 changes: 55 additions & 0 deletions Insight.Database.Core/CodeGenerator/TypeConverterGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,34 @@ public static void EmitConvertValue(ILGenerator il, string memberName, Type sour

// after: stack => [target][xDocument]
}
#if NET6_0_OR_GREATER
else if (underlyingTargetType == typeof(DateOnly))
{
// Convert DateTime to DateOnly
// before: stack => [target][object-value]
il.Emit(OpCodes.Unbox_Any, typeof(DateTime));
il.Emit(OpCodes.Call, typeof(DateOnly).GetMethod("FromDateTime", new[] { typeof(DateTime) }));

// if the target is nullable, then construct the nullable
if (Nullable.GetUnderlyingType(targetType) != null)
il.Emit(OpCodes.Newobj, targetType.GetConstructor(new[] { typeof(DateOnly) }));

// after: stack => [target][DateOnly or DateOnly?]
}
else if (underlyingTargetType == typeof(TimeOnly))
{
// Convert DateTime or TimeSpan to TimeOnly
// before: stack => [target][object-value]
il.Emit(OpCodes.Call, typeof(TypeConverterGenerator).GetMethod("SqlObjectToTimeOnly"));
il.Emit(OpCodes.Unbox_Any, typeof(TimeOnly));

// if the target is nullable, then construct the nullable
if (Nullable.GetUnderlyingType(targetType) != null)
il.Emit(OpCodes.Newobj, targetType.GetConstructor(new[] { typeof(TimeOnly) }));

// after: stack => [target][TimeOnly or TimeOnly?]
}
#endif
else if (serializer != null && serializer.CanDeserialize(sourceType, targetType))
{
// we are getting a string from the database, but the target is not a string, and it's a reference type
Expand Down Expand Up @@ -380,6 +408,33 @@ public static object SqlObjectToTimeSpan(object o)
// We don't know how to convert it. Let .NET handle it.
return o;
}

#if NET6_0_OR_GREATER
/// <summary>
/// Converts a SQL DateTime or TimeSpan to a .NET TimeOnly.
/// </summary>
/// <param name="o">The object to convert.</param>
/// <returns>The corresponding .NET TimeOnly.</returns>
public static object SqlObjectToTimeOnly(object o)
{
if (o == null)
return null;

// Convert DateTime to TimeSpan first, then to TimeOnly
if (o is DateTime)
{
var timeSpan = SqlDateTimeToTimeSpan((DateTime)o);
return TimeOnly.FromTimeSpan(timeSpan);
}

// If it's already a TimeSpan, convert to TimeOnly
if (o is TimeSpan)
return TimeOnly.FromTimeSpan((TimeSpan)o);

// Already TimeOnly or unknown type
return o;
}
#endif
#endregion

#region Code Generation Helpers
Expand Down
4 changes: 4 additions & 0 deletions Insight.Database.Core/CodeGenerator/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ public static bool IsAtomicType(Type type)
if (type == typeof(DateTimeOffset)) return true;
if (type == typeof(Guid)) return true;
if (type == typeof(TimeSpan)) return true;
#if NET6_0_OR_GREATER
if (type == typeof(DateOnly)) return true;
if (type == typeof(TimeOnly)) return true;
#endif

// all of the primitive types, array, etc. are atomic
return type.GetTypeInfo().IsPrimitive;
Expand Down
56 changes: 54 additions & 2 deletions Insight.Tests/TypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ public void TestTypes()
NullableData<DateTimeOffset>.Test(DateTimeOffset.Now, _connection, "datetimeoffset");
NullableData<TimeSpan>.Test(TimeSpan.Parse("00:00:00"), _connection, "time");
NullableData<TimeSpan>.Test(TimeSpan.Parse("00:00:00"), _connection, "datetime");
#if NET6_0_OR_GREATER
NullableData<DateOnly>.Test(DateOnly.FromDateTime(DateTime.Now), _connection, "date");
NullableData<TimeOnly>.Test(TimeOnly.FromTimeSpan(TimeSpan.Parse("14:30:00")), _connection, "time");
#endif

// class types
Data<string>.Test("foo", _connection, "varchar(128)");
Expand All @@ -225,7 +229,55 @@ public void TestTypes()
}

class TestData { public int P; }
#endregion

#if NET6_0_OR_GREATER
[Test]
public void TestDateOnlyType()
{
var _connection = Connection();
var testDate = new DateOnly(2024, 3, 15);

// Test basic round-trip
var result = _connection.QuerySql<DateOnly>("SELECT @p", new { p = testDate }).First();
ClassicAssert.AreEqual(testDate, result);

// Test with SQL date conversion
var result2 = _connection.QuerySql<DateOnly>("SELECT CONVERT(date, @p)", new { p = testDate }).First();
ClassicAssert.AreEqual(testDate, result2);

// Test nullable DateOnly
var nullableResult = _connection.QuerySql<DateOnly?>("SELECT @p", new { p = (DateOnly?)testDate }).First();
ClassicAssert.AreEqual(testDate, nullableResult);

// Test null DateOnly
var nullResult = _connection.QuerySql<DateOnly?>("SELECT @p", new { p = (DateOnly?)null }).First();
ClassicAssert.IsNull(nullResult);
}

[Test]
public void TestTimeOnlyType()
{
var _connection = Connection();
var testTime = new TimeOnly(14, 30, 45);

// Test basic round-trip
var result = _connection.QuerySql<TimeOnly>("SELECT @p", new { p = testTime }).First();
ClassicAssert.AreEqual(testTime, result);

// Test with SQL time conversion
var result2 = _connection.QuerySql<TimeOnly>("SELECT CONVERT(time, @p)", new { p = testTime }).First();
ClassicAssert.AreEqual(testTime, result2);

// Test nullable TimeOnly
var nullableResult = _connection.QuerySql<TimeOnly?>("SELECT @p", new { p = (TimeOnly?)testTime }).First();
ClassicAssert.AreEqual(testTime, nullableResult);

// Test null TimeOnly
var nullResult = _connection.QuerySql<TimeOnly?>("SELECT @p", new { p = (TimeOnly?)null }).First();
ClassicAssert.IsNull(nullResult);
}
#endif
#endregion

#region Single Column Tests
[Test]
Expand Down Expand Up @@ -1072,7 +1124,7 @@ public void DateTimeShouldSerializeProperlyToTextQueries()
var p = new { @ConfirmTime = s };

var result = Connection().QuerySql<DateTime>(sql, p).FirstOrDefault();
ClassicAssert.AreEqual(DateTime.Parse(s), result);
ClassicAssert.AreEqual(DateTime.Parse(s, System.Globalization.CultureInfo.InvariantCulture), result);
}
#endregion

Expand Down