diff --git a/ClickHouse.Driver.Tests/ADO/DataReaderTests.cs b/ClickHouse.Driver.Tests/ADO/DataReaderTests.cs index c4c6394d..e94d8415 100644 --- a/ClickHouse.Driver.Tests/ADO/DataReaderTests.cs +++ b/ClickHouse.Driver.Tests/ADO/DataReaderTests.cs @@ -1,4 +1,5 @@ -using System.Data; +using System; +using System.Data; using System.Linq; using System.Threading.Tasks; using ClickHouse.Driver.ADO.Readers; @@ -200,4 +201,13 @@ public async Task ShouldEnumerateRows() Assert.That(rows, Is.EqualTo(Enumerable.Range(0, 100)).AsCollection); ClassicAssert.IsFalse(reader.Read()); } + + [Test] + public async Task ShouldReadTimeSpanWhenIntervalNanosecond() + { + using var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync("SELECT toIntervalNanosecond(100) as value"); + ClassicAssert.IsTrue(reader.Read()); + Assert.That(reader.GetTimeSpan(0), Is.EqualTo(TimeSpan.FromTicks(1))); + ClassicAssert.IsFalse(reader.Read()); + } } diff --git a/ClickHouse.Driver.Tests/ORM/DapperTests.cs b/ClickHouse.Driver.Tests/ORM/DapperTests.cs index 3fce8be0..86f6ff46 100644 --- a/ClickHouse.Driver.Tests/ORM/DapperTests.cs +++ b/ClickHouse.Driver.Tests/ORM/DapperTests.cs @@ -46,6 +46,8 @@ private static bool ShouldBeSupportedByDapper(string clickHouseType) return false; if (clickHouseType.Contains("Nested")) return false; + if (clickHouseType.StartsWith("Interval")) // TimeSpan does not implement IConvertible + return false; switch (clickHouseType) { case "UUID": @@ -253,13 +255,13 @@ updated DateTime DEFAULT now() // Verify the insert worked and defaults were applied var result = await connection.QueryAsync("SELECT * FROM test.dapper_except"); var row = result.Single() as IDictionary; - + Assert.That(row, Is.Not.Null); Assert.That(row.Count, Is.EqualTo(5)); // All 5 columns should be present Assert.That(row["id"], Is.EqualTo(100)); Assert.That(row["name"], Is.EqualTo("dapper-test")); Assert.That(row["value"], Is.EqualTo(123.45)); - + // Verify default timestamps were set var created = (DateTime)row["created"]; var updated = (DateTime)row["updated"]; diff --git a/ClickHouse.Driver.Tests/SQL/SqlSimpleSelectTests.cs b/ClickHouse.Driver.Tests/SQL/SqlSimpleSelectTests.cs index 7124cbb4..ae57748e 100644 --- a/ClickHouse.Driver.Tests/SQL/SqlSimpleSelectTests.cs +++ b/ClickHouse.Driver.Tests/SQL/SqlSimpleSelectTests.cs @@ -116,6 +116,7 @@ public async Task ShouldSelectNumericTypes() .Where(dt => dt.Contains("Int") || dt.Contains("Float")) .Where(dt => !dt.Contains("128") || TestUtilities.SupportedFeatures.HasFlag(Feature.WideTypes)) .Where(dt => !dt.Contains("256") || TestUtilities.SupportedFeatures.HasFlag(Feature.WideTypes)) + .Where(dt => !dt.StartsWith("Interval")) .Select(dt => $"to{dt}(55)") .ToArray(); @@ -244,7 +245,7 @@ public async Task ShouldGetValueDecimal() [TestCaseSource(typeof(SqlSimpleSelectTests), nameof(SimpleSelectTypes))] public async Task ShouldExecuteRandomDataSelectQuery(string type) { - if (type.StartsWith("Nested") || type == "Nothing" || type.StartsWith("Variant") || type.StartsWith("Json")) + if (type.StartsWith("Nested") || type == "Nothing" || type.StartsWith("Variant") || type.StartsWith("Json") || type.StartsWith("Interval")) Assert.Ignore($"Type {type} not supported by generateRandom"); using var reader = await connection.ExecuteReaderAsync($"SELECT * FROM generateRandom('value {type.Replace("'", "\\'")}', 10, 10, 10) LIMIT 100"); diff --git a/ClickHouse.Driver.Tests/Types/TypeMappingTests.cs b/ClickHouse.Driver.Tests/Types/TypeMappingTests.cs index 88653ee0..ac817d2e 100644 --- a/ClickHouse.Driver.Tests/Types/TypeMappingTests.cs +++ b/ClickHouse.Driver.Tests/Types/TypeMappingTests.cs @@ -47,6 +47,8 @@ public class TypeMappingTests [TestCase("DateTime64(3)", ExpectedResult = typeof(DateTime))] [TestCase("DateTime64(3, 'Etc/UTC')", ExpectedResult = typeof(DateTime))] + [TestCase("IntervalNanosecond", ExpectedResult = typeof(TimeSpan))] + [TestCase("Map(String, Int32)", ExpectedResult = typeof(Dictionary))] [TestCase("Map(Tuple(Int32, Int32), Int32)", ExpectedResult = typeof(Dictionary, int>))] @@ -77,6 +79,9 @@ public class TypeMappingTests [TestCase(typeof(DateTime), ExpectedResult = "DateTime")] + // TODO: What if we map all intervals to TimeSpan? + [TestCase(typeof(TimeSpan), ExpectedResult = "IntervalNanosecond")] + [TestCase(typeof(IPAddress), ExpectedResult = "IPv4")] [TestCase(typeof(Guid), ExpectedResult = "UUID")] diff --git a/ClickHouse.Driver.Tests/Utilities/TestUtilities.cs b/ClickHouse.Driver.Tests/Utilities/TestUtilities.cs index 068d0cb1..2e20f609 100644 --- a/ClickHouse.Driver.Tests/Utilities/TestUtilities.cs +++ b/ClickHouse.Driver.Tests/Utilities/TestUtilities.cs @@ -195,6 +195,8 @@ public static IEnumerable GetDataTypeSamples() yield return new DataTypeSample("DateTime64(7, 'UTC')", typeof(DateTime), "toDateTime64('2043-03-01 18:34:04.4444444', 9, 'UTC')", new DateTime(644444444444444444, DateTimeKind.Utc)); yield return new DataTypeSample("DateTime64(7, 'Pacific/Fiji')", typeof(DateTime), "toDateTime64('2043-03-01 18:34:04.4444444', 9, 'Pacific/Fiji')", new DateTime(644444444444444444, DateTimeKind.Unspecified)); + yield return new DataTypeSample("IntervalNanosecond", typeof(TimeSpan), "toIntervalNanosecond(123456700)", TimeSpan.FromTicks(1234567)); + yield return new DataTypeSample("Decimal32(3)", typeof(ClickHouseDecimal), "toDecimal32(123.45, 3)", new ClickHouseDecimal(123.450m)); yield return new DataTypeSample("Decimal32(3)", typeof(ClickHouseDecimal), "toDecimal32(-123.45, 3)", new ClickHouseDecimal(-123.450m)); diff --git a/ClickHouse.Driver/ADO/Readers/ClickHouseDataReader.cs b/ClickHouse.Driver/ADO/Readers/ClickHouseDataReader.cs index 878d92f2..c467f59a 100644 --- a/ClickHouse.Driver/ADO/Readers/ClickHouseDataReader.cs +++ b/ClickHouse.Driver/ADO/Readers/ClickHouseDataReader.cs @@ -191,6 +191,9 @@ public override bool IsDBNull(int ordinal) // Custom extension public BigInteger GetBigInteger(int ordinal) => (BigInteger)GetValue(ordinal); + // Custom extension + public virtual TimeSpan GetTimeSpan(int ordinal) => (TimeSpan)GetValue(ordinal); + public override bool Read() { if (reader.PeekChar() == -1) diff --git a/ClickHouse.Driver/Formats/HttpParameterFormatter.cs b/ClickHouse.Driver/Formats/HttpParameterFormatter.cs index 1ed99769..19f1baf1 100644 --- a/ClickHouse.Driver/Formats/HttpParameterFormatter.cs +++ b/ClickHouse.Driver/Formats/HttpParameterFormatter.cs @@ -48,6 +48,9 @@ internal static string Format(ClickHouseType type, object value, bool quote) case DateType dt: return Convert.ToDateTime(value, CultureInfo.InvariantCulture).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + case IntervalNanosecondType dt: + return (((TimeSpan)value).Ticks * IntervalNanosecondType.NanosecondsPerTick).ToString(CultureInfo.InvariantCulture); + case StringType st: case FixedStringType tt: case Enum8Type e8t: diff --git a/ClickHouse.Driver/Types/IntervalNanosecondType.cs b/ClickHouse.Driver/Types/IntervalNanosecondType.cs new file mode 100644 index 00000000..b45f1ae7 --- /dev/null +++ b/ClickHouse.Driver/Types/IntervalNanosecondType.cs @@ -0,0 +1,22 @@ +using System; +using ClickHouse.Driver.Formats; + +namespace ClickHouse.Driver.Types; + +internal class IntervalNanosecondType : IntervalType +{ +#if NET7_0_OR_GREATER + public const long NanosecondsPerTick = TimeSpan.NanosecondsPerTick; +#else + public const long NanosecondsPerTick = 100; +#endif + + public override Type FrameworkType => typeof(TimeSpan); + + // Anything less than 100 nanoseconds will be truncated to 0 as TimeSpan's smallest unit is 1 tick (100 nanoseconds) + public override object Read(ExtendedBinaryReader reader) => TimeSpan.FromTicks(reader.ReadInt64() / NanosecondsPerTick); + + public override string ToString() => "IntervalNanosecond"; + + public override void Write(ExtendedBinaryWriter writer, object value) => writer.Write(((TimeSpan)value).Ticks * NanosecondsPerTick); +} diff --git a/ClickHouse.Driver/Types/IntervalType.cs b/ClickHouse.Driver/Types/IntervalType.cs new file mode 100644 index 00000000..9322205d --- /dev/null +++ b/ClickHouse.Driver/Types/IntervalType.cs @@ -0,0 +1,6 @@ +namespace ClickHouse.Driver.Types; + +internal abstract class IntervalType : ClickHouseType +{ + public virtual bool Signed => true; +} diff --git a/ClickHouse.Driver/Types/TypeConverter.cs b/ClickHouse.Driver/Types/TypeConverter.cs index f4018168..9c6746f9 100644 --- a/ClickHouse.Driver/Types/TypeConverter.cs +++ b/ClickHouse.Driver/Types/TypeConverter.cs @@ -143,6 +143,9 @@ static TypeConverter() RegisterParameterizedType(); RegisterParameterizedType(); + // Interval types + RegisterPlainType(); + // Special 'nothing' type RegisterPlainType();