Skip to content

Commit e5fbfbb

Browse files
Json, Dynamic, and type parsing improvements (#95)
* Add BinaryTypeDecoder for binary type parsing * Improve Json support * Expand test cases with new types and composite type generation * Expand and improve Json type tests * Expand and improve Dynamic tests * re-enable connection test * Improve json binary writing of various numeric types, and add more json tests * Pass type settings when reading dynamic type * Update release notes
1 parent 2a64245 commit e5fbfbb

File tree

15 files changed

+1240
-162
lines changed

15 files changed

+1240
-162
lines changed

ClickHouse.Driver.Tests/ADO/ConnectionTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ public void ShouldFetchSchemaTables()
177177
}
178178

179179
[Test]
180-
[Ignore("Needs support for named tuple parameters")]
181180
public void ShouldFetchSchemaDatabaseColumns()
182181
{
183182
var schema = connection.GetSchema("Columns", ["system"]);

ClickHouse.Driver.Tests/BulkCopy/BulkCopyTests.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,114 @@ public async Task ShouldInsertJson()
497497
}
498498
}
499499

500+
[Test]
501+
[RequiredFeature(Feature.Json)]
502+
public async Task ShouldInsertJsonWithComplexTypes()
503+
{
504+
var targetTable = "test." + SanitizeTableName($"bulk_json_complex");
505+
await connection.ExecuteStatementAsync($"DROP TABLE IF EXISTS {targetTable}");
506+
await connection.ExecuteStatementAsync($"CREATE TABLE IF NOT EXISTS {targetTable} (value JSON) ENGINE Memory");
507+
508+
using var bulkCopy = new ClickHouseBulkCopy(connection)
509+
{
510+
DestinationTableName = targetTable,
511+
};
512+
513+
var jsonObject = new JsonObject
514+
{
515+
["boolValue"] = true,
516+
["numberValue"] = 42.5,
517+
["stringValue"] = "hello",
518+
["nullValue"] = null,
519+
["arrayValueDouble"] = new JsonArray(1.5, 2.5, 3.5),
520+
["arrayValueInt"] = new JsonArray(1, 2, 3),
521+
["nestedObject"] = new JsonObject
522+
{
523+
["innerString"] = "nested",
524+
["innerNumber"] = 123,
525+
["innerLong"] = 124L,
526+
},
527+
};
528+
529+
await bulkCopy.InitAsync();
530+
await bulkCopy.WriteToServerAsync([[jsonObject]]);
531+
532+
using var reader = await connection.ExecuteReaderAsync($"SELECT * from {targetTable}");
533+
Assert.That(reader.Read(), Is.True);
534+
535+
var result = (JsonObject)reader.GetValue(0);
536+
537+
Assert.That((bool)result["boolValue"], Is.EqualTo(true));
538+
Assert.That((double)result["numberValue"], Is.EqualTo(42.5));
539+
Assert.That((string)result["stringValue"], Is.EqualTo("hello"));
540+
Assert.That(result["nullValue"], Is.Null);
541+
Assert.That(JsonNode.DeepEquals(result["arrayValueDouble"], new JsonArray(1.5, 2.5, 3.5)), Is.True);
542+
Assert.That(JsonNode.DeepEquals(result["arrayValueInt"], new JsonArray(1, 2, 3)), Is.True);
543+
Assert.That(JsonNode.DeepEquals(result["nestedObject"], new JsonObject { ["innerString"] = "nested", ["innerNumber"] = 123, ["innerLong"] = 124L }), Is.True);
544+
545+
Assert.That(reader.Read(), Is.False);
546+
}
547+
548+
[Test]
549+
[RequiredFeature(Feature.Json)]
550+
public async Task ShouldInsertJsonFromString()
551+
{
552+
var targetTable = "test." + SanitizeTableName($"bulk_json_string");
553+
await connection.ExecuteStatementAsync($"DROP TABLE IF EXISTS {targetTable}");
554+
await connection.ExecuteStatementAsync($"CREATE TABLE IF NOT EXISTS {targetTable} (value JSON) ENGINE Memory");
555+
556+
using var bulkCopy = new ClickHouseBulkCopy(connection)
557+
{
558+
DestinationTableName = targetTable,
559+
};
560+
561+
var jsonString = "{\"name\": \"test\", \"count\": 42, \"active\": true}";
562+
563+
await bulkCopy.InitAsync();
564+
await bulkCopy.WriteToServerAsync([[jsonString]]);
565+
566+
using var reader = await connection.ExecuteReaderAsync($"SELECT * from {targetTable}");
567+
Assert.That(reader.Read(), Is.True);
568+
569+
var result = (JsonObject)reader.GetValue(0);
570+
571+
Assert.That((string)result["name"], Is.EqualTo("test"));
572+
Assert.That((long)result["count"], Is.EqualTo(42));
573+
Assert.That((bool)result["active"], Is.EqualTo(true));
574+
575+
Assert.That(reader.Read(), Is.False);
576+
}
577+
578+
[Test]
579+
[RequiredFeature(Feature.Json)]
580+
public async Task ShouldInsertJsonFromAnonymousObject()
581+
{
582+
var targetTable = "test." + SanitizeTableName($"bulk_json_anon");
583+
await connection.ExecuteStatementAsync($"DROP TABLE IF EXISTS {targetTable}");
584+
await connection.ExecuteStatementAsync($"CREATE TABLE IF NOT EXISTS {targetTable} (value JSON) ENGINE Memory");
585+
586+
using var bulkCopy = new ClickHouseBulkCopy(connection)
587+
{
588+
DestinationTableName = targetTable,
589+
};
590+
591+
var obj = new { name = "test", count = 42, active = true, arrayBool = new bool[] { true, false} };
592+
593+
await bulkCopy.InitAsync();
594+
await bulkCopy.WriteToServerAsync([[obj]]);
595+
596+
using var reader = await connection.ExecuteReaderAsync($"SELECT * from {targetTable}");
597+
Assert.That(reader.Read(), Is.True);
598+
599+
var result = (JsonObject)reader.GetValue(0);
600+
601+
Assert.That((string)result["name"], Is.EqualTo("test"));
602+
Assert.That((long)result["count"], Is.EqualTo(42));
603+
Assert.That((bool)result["active"], Is.EqualTo(true));
604+
Assert.That(JsonNode.DeepEquals(result["arrayBool"], new JsonArray(true, false)), Is.True);
605+
606+
Assert.That(reader.Read(), Is.False);
607+
}
500608

501609
[Test]
502610
public async Task ShouldInsertTupleWithEnum()

ClickHouse.Driver.Tests/SQL/SqlParameterizedSelectTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Threading.Tasks;
66
using ClickHouse.Driver.ADO;
7+
using ClickHouse.Driver.Types;
78
using ClickHouse.Driver.Utility;
89
using NUnit.Framework;
910

@@ -40,7 +41,7 @@ public async Task ShouldExecuteParameterizedCompareWithTypeDetection(string exam
4041
using var command = connection.CreateCommand();
4142
command.CommandText = $"SELECT {exampleExpression} as expected, {{var:{clickHouseType}}} as actual, expected = actual as equals";
4243
command.AddParameter("var", value);
43-
44+
//TODO step through this test and see exactly what happens with HttpParameterFormatter and ClickHouseDbParameter, they seem to be doing duplicate work
4445
var result = (await command.ExecuteReaderAsync()).GetEnsureSingleRow();
4546
TestUtilities.AssertEqual(result[0], result[1]);
4647

@@ -74,6 +75,7 @@ public async Task ShouldExecuteParameterizedCompareWithExplicitType(string examp
7475
{
7576
if (clickHouseType.StartsWith("Enum"))
7677
clickHouseType = "String";
78+
7779
using var command = connection.CreateCommand();
7880
command.CommandText = $"SELECT {exampleExpression} as expected, {{var:{clickHouseType}}} as actual, expected = actual as equals";
7981
command.AddParameter("var", clickHouseType, value);

ClickHouse.Driver.Tests/Types/DynamicTests.cs

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,87 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Net;
5+
using System.Numerics;
6+
using System.Text.Json;
7+
using System.Text.Json.Nodes;
48
using System.Threading.Tasks;
59
using ClickHouse.Driver.ADO;
610
using ClickHouse.Driver.ADO.Readers;
11+
using ClickHouse.Driver.Numerics;
712
using ClickHouse.Driver.Tests.Attributes;
13+
using ClickHouse.Driver.Types;
814
using ClickHouse.Driver.Utility;
915

1016
namespace ClickHouse.Driver.Tests.Types;
1117

1218
public class DynamicTests : AbstractConnectionTestFixture
1319
{
20+
public static IEnumerable<TestCaseData> DirectDynamicCastQueries
21+
{
22+
get
23+
{
24+
foreach (var sample in TestUtilities.GetDataTypeSamples().Where(s => ShouldBeSupportedInDynamic(s.ClickHouseType)))
25+
{
26+
yield return new TestCaseData(sample.ExampleExpression, sample.ClickHouseType, sample.ExampleValue)
27+
.SetName($"Direct_{sample.ClickHouseType}_{sample.ExampleValue}");
28+
}
29+
30+
// Some additional test cases for dynamic specifically
31+
// JSON with complex type hints
32+
yield return new TestCaseData(
33+
"'{\"a\": 1}'",
34+
"Json(max_dynamic_paths=10, max_dynamic_types=3, a Int64, SKIP path.to.skip, SKIP REGEXP 'regex.path.*')",
35+
new JsonObject { ["a"] = 1L }
36+
).SetName("Direct_Json_Complex");
37+
38+
yield return new TestCaseData(
39+
"1::Int32",
40+
"Dynamic",
41+
1
42+
).SetName("Nested_Dynamic");
43+
}
44+
}
45+
46+
[Test]
47+
[RequiredFeature(Feature.Dynamic)]
48+
[TestCaseSource(typeof(DynamicTests), nameof(DirectDynamicCastQueries))]
49+
public async Task ShouldParseDirectDynamicCast(string valueSql, string clickHouseType, object expectedValue)
50+
{
51+
// Direct cast to Dynamic without going through JSON
52+
using var reader =
53+
(ClickHouseDataReader)await connection.ExecuteReaderAsync(
54+
$"SELECT ({valueSql}::{clickHouseType})::Dynamic");
55+
56+
ClassicAssert.IsTrue(reader.Read());
57+
var result = reader.GetValue(0);
58+
TestUtilities.AssertEqual(expectedValue, result);
59+
ClassicAssert.IsFalse(reader.Read());
60+
}
61+
62+
private static bool ShouldBeSupportedInDynamic(string clickHouseType)
63+
{
64+
// Geo types not supported
65+
if (clickHouseType is "Point" or "Ring" or "Polygon" or "MultiPolygon" or "Nothing")
66+
{
67+
return false;
68+
}
69+
70+
return true;
71+
}
72+
1473
public static IEnumerable<TestCaseData> SimpleSelectQueries => TestUtilities.GetDataTypeSamples()
1574
.Where(s => ShouldBeSupportedInJson(s.ClickHouseType))
16-
.Select(sample => GetTestCaseData(sample.ExampleExpression, sample.ClickHouseType, sample.ExampleValue));
75+
.Select(sample => GetTestCaseData(sample.ExampleExpression, sample.ClickHouseType, sample.ExampleValue))
76+
.Where(x => x != null);
1777

1878
[Test]
1979
[RequiredFeature(Feature.Dynamic)]
2080
[TestCaseSource(typeof(DynamicTests), nameof(SimpleSelectQueries))]
21-
public async Task ShouldMatchFrameworkType(string valueSql, Type frameworkType)
81+
public async Task ShouldMatchFrameworkTypeViaJson(string valueSql, Type frameworkType)
2282
{
83+
// This query returns the value as Dynamic type via JSON. The dynamicType may or may not match the actual type provided.
84+
// eg IPv4 will be a String.
2385
using var reader =
2486
(ClickHouseDataReader) await connection.ExecuteReaderAsync(
2587
$"select json.value from (select map('value', {valueSql})::JSON as json)");
@@ -37,6 +99,11 @@ private static TestCaseData GetTestCaseData(string exampleExpression, string cli
3799
return new TestCaseData(exampleExpression, typeof(DateTime));
38100
}
39101

102+
if (clickHouseType.StartsWith("Time"))
103+
{
104+
return new TestCaseData(exampleExpression, typeof(string));
105+
}
106+
40107
if (clickHouseType.StartsWith("Int") || clickHouseType.StartsWith("UInt"))
41108
{
42109
return new TestCaseData(exampleExpression, typeof(long));
@@ -46,11 +113,6 @@ private static TestCaseData GetTestCaseData(string exampleExpression, string cli
46113
{
47114
return new TestCaseData(exampleExpression, typeof(string));
48115
}
49-
50-
if (clickHouseType.StartsWith("Time"))
51-
{
52-
return new TestCaseData(exampleExpression, typeof(TimeSpan));
53-
}
54116

55117
if (clickHouseType.StartsWith("Float"))
56118
{
@@ -72,16 +134,28 @@ floatRemainder is 0
72134
{
73135
case "Array(Int32)" or "Array(Nullable(Int32))":
74136
return new TestCaseData(exampleExpression, typeof(long?[]));
137+
case "Array(Float32)" or "Array(Nullable(Float32))":
138+
return new TestCaseData(exampleExpression, typeof(double?[]));
75139
case "Array(String)":
76140
return new TestCaseData(exampleExpression, typeof(string[]));
77-
case "IPv4" or "IPv6" or "String" or "UUID":
141+
case "Array(Bool)":
142+
return new TestCaseData(exampleExpression, typeof(bool?[]));
143+
case "String" or "UUID":
78144
return new TestCaseData(exampleExpression, typeof(string));
79145
case "Nothing":
80146
return new TestCaseData(exampleExpression, typeof(DBNull));
81147
case "Bool":
82148
return new TestCaseData(exampleExpression, typeof(bool));
149+
case "IPv4" or "IPv6":
150+
return new TestCaseData(exampleExpression, typeof(string));
83151
}
84152

153+
if (clickHouseType.StartsWith("Array"))
154+
{
155+
// Array handling is already covered above, we don't need to re-do it for every element type
156+
return null;
157+
}
158+
85159
throw new ArgumentException($"{clickHouseType} not supported");
86160
}
87161

@@ -100,12 +174,6 @@ private static bool ShouldBeSupportedInJson(string clickHouseType)
100174
return false;
101175
}
102176

103-
// Time and Time64 are not currently supported
104-
if (clickHouseType.StartsWith("Time"))
105-
{
106-
return false;
107-
}
108-
109177
switch (clickHouseType)
110178
{
111179
case "Int128":

0 commit comments

Comments
 (0)