Files
Marc Gravell 6d48ef664a Re-enable DateOnly/TimeOnly support, fixing the defects that got it disabled (#2228)
DateOnly/TimeOnly support (#2051) was compiled out in 907a4d9 pending
#2071/#2072. This re-enables it with the root causes fixed, and teaches the
read paths that the date/time family has no IConvertible bridge - which box
a date or time column yields is a provider/version decision (Npgsql 10:
DateOnly/TimeOnly; SqlClient and Npgsql 9: DateTime/TimeSpan), so both
shapes must convert:

- GetFieldValue<T> is no longer demanded of a column whose reported type
  needs a family conversion (a datetime column into a DateOnly member was
  the #2072 breakage; SqlDecimal-style entries, where GetFieldValue from a
  differently-reported column is the whole point, keep it);
- the member, scalar and Parse<T> paths all convert DateOnly/TimeOnly <->
  DateTime/TimeSpan in both directions (IL for members, object-level
  elsewhere), fixing reads against Npgsql 10 date/time columns (#2226);
- Query<DateOnly>/<TimeOnly> in scalar form no longer silently return
  default(T) (#2227): the typeMap entries make them simple types again;
- the same-TypeCode direct-unbox shortcut is restricted to distinct codes:
  TypeCode.Object matching TypeCode.Object says nothing (TimeSpan into
  TimeOnly? threw), while object on either side (sql_variant columns,
  dynamic members) keeps the direct unbox;
- the scalar conversion fallback keeps Convert.ChangeType's strict null
  contract (TestConversionExceptionMessages pins it).

The parked DateTimeOnlyTests are re-enabled, with new tests for the #2072
shape (datetime column into DateOnly member), the #2227 scalar shape, and
date-as-DateTime regression guards; PostgresqlTests gains the Npgsql-10
interchange matrix ([FactPostgresql], so it runs where the documented
container is available).

Fixes #2072, #2226, #2227, #1728; expected to also resolve #2071.
2026-08-20 15:20:51 +01:00

118 lines
4.8 KiB
C#

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 string Name { get; set; } = "";
public DateOnly Date { get; set; }
public TimeOnly Time { get; set; }
public DateOnly? NDate { get; set; }
public TimeOnly? NTime { get; set; }
}
[Fact]
public void TypedInOut()
{
var now = DateTime.Now;
var args = new HazDateTimeOnly
{
Name = nameof(TypedInOut),
Date = DateOnly.FromDateTime(now),
Time = TimeOnly.FromDateTime(now),
NDate = DateOnly.FromDateTime(now),
NTime = TimeOnly.FromDateTime(now),
};
var row = connection.QuerySingle<HazDateTimeOnly>("select @name as [Name], @date as [Date], @time as [Time], @ndate as [NDate], @ntime as [NTime]", args);
Assert.Equal(args.Name, row.Name);
Assert.Equal(args.Date, row.Date);
Assert.Equal(args.Time, row.Time);
Assert.Equal(args.NDate, row.NDate);
Assert.Equal(args.NTime, row.NTime);
}
[Fact]
public async Task TypedInOutAsync()
{
var now = DateTime.Now;
var args = new HazDateTimeOnly
{
Name = nameof(TypedInOutAsync),
Date = DateOnly.FromDateTime(now),
Time = TimeOnly.FromDateTime(now),
NDate = DateOnly.FromDateTime(now),
NTime = TimeOnly.FromDateTime(now),
};
var row = await connection.QuerySingleAsync<HazDateTimeOnly>("select @name as [Name], @date as [Date], @time as [Time], @ndate as [NDate], @ntime as [NTime]", args);
Assert.Equal(args.Name, row.Name);
Assert.Equal(args.Date, row.Date);
Assert.Equal(args.Time, row.Time);
Assert.Equal(args.NDate, row.NDate);
Assert.Equal(args.NTime, row.NTime);
}
[Fact]
public void UntypedInOut()
{
var now = DateTime.Now;
var args = new DynamicParameters();
var name = nameof(UntypedInOut);
var date = DateOnly.FromDateTime(now);
var time = TimeOnly.FromDateTime(now);
args.Add("name", name);
args.Add("date", date);
args.Add("time", time);
var row = connection.QuerySingle<dynamic>("select @name as [Name], @date as [Date], @time as [Time]", args);
Assert.Equal(name, (string)row.Name);
// 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));
}
[Fact] // #2072: a datetime column into a DateOnly member - the provider boxes DateTime,
// so this must convert rather than demand GetFieldValue<DateOnly> of a datetime column
public void MembersFromDateTimeAndTimeSpanColumns()
{
var row = connection.QuerySingle<HazDateTimeOnly>(
"select 'x' as [Name], cast('2019-10-01' as datetime) as [Date], cast('03:03:03' as time) as [Time], cast('2019-10-02' as datetime) as [NDate], cast('04:04:04' as time) as [NTime]");
Assert.Equal(new DateOnly(2019, 10, 1), row.Date);
Assert.Equal(new TimeOnly(3, 3, 3), row.Time);
Assert.Equal(new DateOnly(2019, 10, 2), row.NDate);
Assert.Equal(new TimeOnly(4, 4, 4), row.NTime);
}
[Fact] // #2227: the scalar form must not silently yield default(T)
public void ScalarDateOnlyAndTimeOnly()
{
Assert.Equal(new DateOnly(2021, 1, 1), connection.QuerySingle<DateOnly>("select cast('2021-01-01' as date)"));
Assert.Equal(new TimeOnly(3, 3, 3), connection.QuerySingle<TimeOnly>("select cast('03:03:03' as time)"));
Assert.Equal(new DateOnly(2021, 1, 1), connection.QuerySingle<DateOnly?>("select cast('2021-01-01' as date)"));
}
[Fact] // the pre-DateOnly reading of a date column must keep working
public void DateColumnAsDateTime()
{
Assert.Equal(new DateTime(2021, 1, 1), connection.QuerySingle<DateTime>("select cast('2021-01-01' as date)"));
Assert.Equal(new DateTime(2021, 1, 1), connection.QuerySingle<HazDateTime>("select cast('2021-01-01' as date) as [When]").When);
}
public class HazDateTime
{
public DateTime When { get; set; }
}
}
#endif