10000 Fix handling of DateOnly values in .NET 6 #501 (#505) · DarkWanderer/ClickHouse.Client@4007d45 · GitHub
[go: up one dir, main page]

Skip to content
This repository was archived by the owner on Jun 22, 2025. It is now read-only.

Commit 4007d45

Browse files
authored
Fix handling of DateOnly values in .NET 6 #501 (#505)
1 parent 7d2f3fa commit 4007d45

File tree

7 files changed

+73
-47
lines changed

7 files changed

+73
-47
lines changed

ClickHouse.Client.Tests/BulkCopyTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,38 @@ public async Task ShouldExecuteSingleValueInsertViaBulkCopy(string clickHouseTyp
5858
Assert.AreEqual(insertedValue, data, "Original and actually inserted values differ");
5959
}
6060

61+
62+
#if NET6_0_OR_GREATER
63+
[Test]
64+
[Parallelizable]
65+
[RequiredFeature(Feature.Date32)]
66+
public async Task ShouldInsertDateOnly()
67+
{
68+
var targetTable = "test.bulk_dateonly";
69+
70+
await connection.ExecuteStatementAsync($"TRUNCATE TABLE IF EXISTS {targetTable}");
71+
await connection.ExecuteStatementAsync($"CREATE TABLE IF NOT EXISTS {targetTable} (value Date32) ENGINE Memory");
72+
73+
using var bulkCopy = new ClickHouseBulkCopy(connection)
74+
{
75+
DestinationTableName = targetTable,
76+
MaxDegreeOfParallelism = 2,
77+
BatchSize = 100
78+
};
79+
80+
await bulkCopy.InitAsync();
81+
await bulkCopy.WriteToServerAsync(Enumerable.Repeat(new object[] { new DateOnly(1999, 12, 31) }, 1));
82+
83+
Assert.AreEqual(1, bulkCopy.RowsWritten);
84+
85+
using var reader = await connection.ExecuteReaderAsync($"SELECT * from {targetTable}");
86+
Assert.IsTrue(reader.Read(), "Cannot read inserted data");
87+
reader.AssertHasFieldCount(1);
88+
var data = reader.GetValue(0);
89+
Assert.AreEqual(new DateTime(1999, 12, 31), data, "Original and actually inserted values differ");
90+
}
91+
#endif
92+
6193
[Test]
6294
[Explicit("Infinite loop test")]
6395
public async Task ShouldExecuteMultipleBulkInsertions()

ClickHouse.Client/ADO/Readers/ClickHouseDataReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ internal ClickHouseType GetEffectiveClickHouseType(int ordinal)
9797
public override DateTime GetDateTime(int ordinal) => (DateTime)GetValue(ordinal);
98 6D4E 98

9999
public virtual DateTimeOffset GetDateTimeOffset(int ordinal) => GetEffectiveClickHouseType(ordinal) is AbstractDateTimeType adt ?
100-
adt.ToDateTimeOffset(GetDateTime(ordinal)) : throw new InvalidCastException();
100+
adt.CoerceToDateTimeOffset(GetDateTime(ordinal)) : throw new InvalidCastException();
101101

102102
public override decimal GetDecimal(int ordinal)
103103
{

ClickHouse.Client/Types/AbstractDateTimeType.cs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,53 @@
33

44
namespace ClickHouse.Client.Types;
55

6-
internal abstract class AbstractDateTimeType : ParameterizedType
6+
public static class DateTimeConversions
77
{
8-
// DateTime.UnixEpoch is not available on .NET 4.8
98
public static readonly DateTime DateTimeEpochStart = DateTimeOffset.FromUnixTimeSeconds(0).UtcDateTime;
109

1110
#if NET6_0_OR_GREATER
12-
public static readonly DateOnly DateOnlyEpochStart = new DateOnly(1970, 1, 1);
11+
public static readonly DateOnly DateOnlyEpochStart = new(1970, 1, 1);
1312
#endif
1413

15-
public override Type FrameworkType => typeof(DateTime);
16-
17-
public DateTimeZone TimeZone { get; set; }
18-
19-
public DateTime FromUnixTimeTicks(long ticks) => ToDateTime(Instant.FromUnixTimeTicks(ticks));
14+
public static int ToUnixTimeDays(this DateTimeOffset dto)
15+
{
16+
return (int)(dto.Date - DateTimeEpochStart.Date).TotalDays;
17+
}
2018

21-
public DateTime FromUnixTimeSeconds(long seconds) => ToDateTime(Instant.FromUnixTimeSeconds(seconds));
19+
public static DateTime FromUnixTimeDays(int days) => DateTimeEpochStart.AddDays(days);
20+
}
2221

23-
public ZonedDateTime ToZonedDateTime(DateTime dateTime)
22+
internal abstract class AbstractDateTimeType : ParameterizedType
23+
{
24+
public DateTimeOffset CoerceToDateTimeOffset(object value)
2425
{
25-
return TimeZone.AtLeniently(LocalDateTime.FromDateTime(dateTime));
26+
return value switch
27+
{
28+
#if NET6_0_OR_GREATER
29+
DateOnly date => new DateTimeOffset(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero),
30+
#endif
31+
DateTimeOffset v => v,
32+
DateTime dt => TimeZoneOrUtc.AtLeniently(LocalDateTime.FromDateTime(dt)).ToDateTimeOffset(),
33+
OffsetDateTime o => o.ToDateTimeOffset(),
34+
ZonedDateTime z => z.ToDateTimeOffset(),
35+
Instant i => ToDateTimeOffset(i),
36+
_ => throw new NotSupportedException()
37+
};
2638
}
2739

28-
public DateTimeOffset ToDateTimeOffset(DateTime dateTime) => ToZonedDateTime(dateTime).ToDateTimeOffset();
40+
public override Type FrameworkType => typeof(DateTime);
41+
42+
public DateTimeZone TimeZone { get; set; }
43+
44+
public DateTimeZone TimeZoneOrUtc => TimeZone ?? DateTimeZone.Utc;
2945

3046
public override string ToString() => TimeZone == null ? $"{Name}" : $"{Name}({TimeZone.Id})";
3147

32-
private DateTime ToDateTime(Instant instant)
48+
private DateTimeOffset ToDateTimeOffset(Instant instant) => instant.InZone(TimeZoneOrUtc).ToDateTimeOffset();
49+
50+
public DateTime ToDateTime(Instant instant)
3351
{
34-
// Special case for ETC/GMT timezone. TODO: support other aliases like Etc/Universal
35-
if (TimeZone == null)
36-
return instant.ToDateTimeUtc();
37-
var zonedDateTime = instant.InZone(TimeZone);
52+
var zonedDateTime = instant.InZone(TimeZoneOrUtc);
3853
if (zonedDateTime.Offset.Ticks == 0)
3954
return zonedDateTime.ToDateTimeUtc();
4055
else

ClickHouse.Client/Types/Date32Type.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,12 @@ internal class Date32Type : DateType
1010

1111
public override string ToString() => "Date32";
1212

13-
public override object Read(ExtendedBinaryReader reader)
14-
{
15-
var days = reader.ReadInt32();
16-
return DateTimeEpochStart.AddDays(days);
17-
}
13+
public override object Read(ExtendedBinaryReader reader) => DateTimeConversions.FromUnixTimeDays(reader.ReadInt32());
1814

1915
public override ParameterizedType Parse(SyntaxTreeNode typeName, Func<SyntaxTreeNode, ClickHouseType> parseClickHouseTypeFunc, TypeSettings settings) => throw new NotImplementedException();
2016

2117
public override void Write(ExtendedBinaryWriter writer, object value)
2218
{
23-
var sinceEpoch = ((DateTime)value).Date - DateTimeEpochStart;
24-
writer.Write(Convert.ToInt32(sinceEpoch.TotalDays));
19+
writer.Write(CoerceToDateTimeOffset(value).ToUnixTimeDays());
2520
}
2621
}

ClickHouse.Client/Types/DateTime64Type.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public DateTime FromClickHouseTicks(long clickHouseTicks)
1919
{
2020
// Convert ClickHouse variable precision ticks into "standard" .NET 100ns ones
2121
var ticks = MathUtils.ShiftDecimalPlaces(clickHouseTicks, 7 - Scale);
22-
return FromUnixTimeTicks(ticks);
22+
return ToDateTime(Instant.FromUnixTimeTicks(ticks));
2323
}
2424

2525
public long ToClickHouseTicks(Instant instant) => MathUtils.ShiftDecimalPlaces(instant.ToUnixTimeTicks(), Scale - 7);
@@ -47,12 +47,6 @@ public override ParameterizedType Parse(SyntaxTreeNode node, Func<SyntaxTreeNode
4747

4848
public override void Write(ExtendedBinaryWriter writer, object value)
4949
{
50-
var instant = value switch
51-
{
52-
DateTimeOffset dto => Instant.FromDateTimeOffset(dto),
53-
DateTime dt => ToZonedDateTime(dt).ToInstant(),
54-
_ => throw new ArgumentException("Cannot convert value to datetime"),
55-
};
56-
writer.Write(ToClickHouseTicks(instant));
50+
writer.Write(ToClickHouseTicks(Instant.FromDateTimeOffset(CoerceToDateTimeOffset(value))));
5751
}
5852
}

ClickHouse.Client/Types/DateTimeType.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ public override ParameterizedType Parse(SyntaxTreeNode node, Func<SyntaxTreeNode
2222
return new DateTimeType { TimeZone = timeZone };
2323
}
2424

25-
public override object Read(ExtendedBinaryReader reader) => FromUnixTimeSeconds(reader.ReadUInt32());
25+
public override object Read(ExtendedBinaryReader reader) => ToDateTime(Instant.FromUnixTimeSeconds(reader.ReadUInt32()));
2626

2727
public override void Write(ExtendedBinaryWriter writer, object value)
2828
{
29-
var dto = value is DateTimeOffset offset ? offset : ToDateTimeOffset((DateTime)value);
30-
writer.Write((int)dto.ToUnixTimeSeconds());
29+
writer.Write((int)CoerceToDateTimeOffset(value).ToUnixTimeSeconds());
3130
}
3231
}

ClickHouse.Client/Types/DateType.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,12 @@ internal class DateType : AbstractDateTimeType
1010

1111
public override string ToString() => "Date";
1212

13-
public override object Read(ExtendedBinaryReader reader) => DateTimeEpochStart.AddDays(reader.ReadUInt16());
13+
public override object Read(ExtendedBinaryReader reader) => DateTimeConversions.FromUnixTimeDays(reader.ReadUInt16());
1414

1515
public override ParameterizedType Parse(SyntaxTreeNode typeName, Func<SyntaxTreeNode, ClickHouseType> parseClickHouseTypeFunc, TypeSettings settings) => throw new NotImplementedException();
1616

1717
public override void Write(ExtendedBinaryWriter writer, object value)
1818
{
19-
#if NET6_0_OR_GREATER
20-
if (value is DateOnly @do)
21-
{
22-
var delta = @do.DayNumber - DateOnlyEpochStart.DayNumber;
23-
writer.Write((ushort)delta);
24-
return;
25-
}
26-
#endif
27-
var sinceEpoch = ((DateTime)value).Date - DateTimeEpochStart;
28-
writer.Write(Convert.ToUInt16(sinceEpoch.TotalDays));
19+
writer.Write(Convert.ToUInt16(CoerceToDateTimeOffset(value).ToUnixTimeDays()));
2920
}
3021
}

0 commit comments

Comments
 (0)
0