diff --git a/csharp/Directory.Packages.props b/csharp/Directory.Packages.props
index 21ddc682e..e70c60d7a 100644
--- a/csharp/Directory.Packages.props
+++ b/csharp/Directory.Packages.props
@@ -16,6 +16,7 @@
true
+ true
@@ -33,6 +34,7 @@
+
@@ -44,5 +46,7 @@
+
+
diff --git a/csharp/src/AdbcDrivers.Databricks.csproj b/csharp/src/AdbcDrivers.Databricks.csproj
index 39ded3cc1..ba96d82cd 100644
--- a/csharp/src/AdbcDrivers.Databricks.csproj
+++ b/csharp/src/AdbcDrivers.Databricks.csproj
@@ -1,9 +1,21 @@
- netstandard2.0;net472;net8.0
- netstandard2.0;net8.0
+ netstandard2.0;net472;net8.0;net10.0
+ netstandard2.0;net8.0;net10.0
readme.md
+
+
+
+ true
+ true
+
diff --git a/csharp/src/ComplexTypeSerializingStream.cs b/csharp/src/ComplexTypeSerializingStream.cs
index d2b0b9868..3af9f1656 100644
--- a/csharp/src/ComplexTypeSerializingStream.cs
+++ b/csharp/src/ComplexTypeSerializingStream.cs
@@ -16,16 +16,17 @@
using System;
using System.Collections.Generic;
+using System.IO;
+using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
-using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
+using AdbcDrivers.Databricks.StatementExecution;
using Apache.Arrow;
using Apache.Arrow.Adbc.Extensions;
using Apache.Arrow.Ipc;
using Apache.Arrow.Types;
-using AdbcDrivers.Databricks.StatementExecution;
namespace AdbcDrivers.Databricks
{
@@ -75,7 +76,7 @@ internal sealed class ComplexTypeSerializingStream : IArrowArrayStream
// a double quote becomes \" (not ") and non-ASCII / < > & are emitted verbatim
// rather than \uXXXX-escaped. The output is still valid JSON; "unsafe" only refers to
// embedding directly in HTML, which is the consuming application's concern, not ours.
- private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
+ private static readonly JsonWriterOptions WriterOptions = new JsonWriterOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
@@ -139,12 +140,27 @@ private RecordBatch ConvertComplexColumns(RecordBatch batch)
private static StringArray SerializeToStringArray(IArrowArray array)
{
StringArray.Builder builder = new StringArray.Builder();
+
+ // Write each value with a manual Utf8JsonWriter rather than JsonSerializer.Serialize.
+ // The value graph (ToObject) is a closed set of Arrow scalar types, lists, and
+ // dictionaries, so we can emit it without reflection — keeping this trim- and
+ // NativeAOT-safe. A single stream/writer pair is reused across rows to avoid
+ // per-row allocations.
+ using MemoryStream stream = new MemoryStream();
+ using Utf8JsonWriter writer = new Utf8JsonWriter(stream, WriterOptions);
for (int i = 0; i < array.Length; i++)
{
if (array.IsNull(i))
+ {
builder.AppendNull();
- else
- builder.Append(JsonSerializer.Serialize(ToObject(array, i), JsonOptions));
+ continue;
+ }
+
+ stream.SetLength(0);
+ writer.Reset(stream);
+ SerializeStructuredValue(writer, array, i);
+ writer.Flush();
+ builder.Append(Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Length));
}
return builder.Build();
}
@@ -175,40 +191,106 @@ private static HashSet DetectComplexColumns(Schema schema)
// --- JSON serialization helpers ---
- private static object? ToObject(IArrowArray array, int index)
+ private static void SerializeStructuredValue(Utf8JsonWriter writer, IArrowArray array, int index)
{
if (array.IsNull(index))
- return null;
+ {
+ writer.WriteNullValue();
+ return;
+ }
- // Handle complex types with recursive traversal, and types needing specific
- // string formatting. All other primitives delegate to ValueAt().
- return array switch
+ switch (array.Data.DataType.TypeId)
{
- ListArray la => ToListOrMap(la, index),
- StructArray sa => ToDict(sa, index),
+ case ArrowTypeId.List:
+ case ArrowTypeId.Map: SerializeListOrMap(writer, (ListArray)array, index); break;
+ case ArrowTypeId.Struct: SerializeDict(writer, (StructArray)array, index); break;
// DECIMAL: emit as a bare JSON number (not a quoted string) so the output matches
// the JDBC driver and is valid JSON. The decimal's string form is written raw so
// values beyond C# decimal's ~28-digit range (DECIMAL(38, …)) keep full precision.
- Decimal128Array dec => RawNumber(dec.GetString(index)),
- Decimal256Array dec => RawNumber(dec.GetString(index)),
- Date32Array d32 => d32.GetDateTime(index)?.ToString("yyyy-MM-dd"),
+ case ArrowTypeId.Decimal32: writer.WriteRawValue(((Decimal32Array)array).GetString(index)); break;
+ case ArrowTypeId.Decimal64: writer.WriteRawValue(((Decimal64Array)array).GetString(index)); break;
+ case ArrowTypeId.Decimal128: writer.WriteRawValue(((Decimal128Array)array).GetString(index)); break;
+ case ArrowTypeId.Decimal256: writer.WriteRawValue(((Decimal256Array)array).GetString(index)); break;
+ case ArrowTypeId.Date32: writer.WriteStringValue(((Date32Array)array).GetDateTime(index)!.Value.ToString("yyyy-MM-dd")); break;
+ case ArrowTypeId.Date64: writer.WriteStringValue(((Date64Array)array).GetDateTime(index)!.Value.ToString("yyyy-MM-dd")); break;
// INTERVAL: native YearMonth/Duration arrays serialize to {} via System.Text.Json
// (no public properties). Render the same "Y-M" / "D HH:MM:SS.nnnnnnnnn" strings
// IntervalSerializingStream produces for top-level interval columns.
- YearMonthIntervalArray ym => IntervalSerializingStream.FormatYearMonth(ym.GetValue(index)!.Value.Months),
- DurationArray dur => IntervalSerializingStream.FormatDuration(dur.GetValue(index)!.Value, ((DurationType)dur.Data.DataType).Unit),
- _ => array.ValueAt(index, StructResultType.Object) // int, long, double, float, bool, string, timestamp, etc.
- };
- }
+ case ArrowTypeId.Interval:
+ switch (((IntervalType)array.Data.DataType).Unit)
+ {
+ case IntervalUnit.DayTime:
+ var dayTime = ((DayTimeIntervalArray)array).GetValue(index)!.Value;
+ var timeSpan = TimeSpan.FromDays(dayTime.Days) + TimeSpan.FromMilliseconds(dayTime.Milliseconds);
+ writer.WriteStringValue(timeSpan.ToString());
+ break;
+ case IntervalUnit.MonthDayNanosecond:
+ var monthDayNano = ((MonthDayNanosecondIntervalArray)array).GetValue(index)!.Value;
+ timeSpan = TimeSpan.FromDays(monthDayNano.Days) + TimeSpan.FromTicks(monthDayNano.Nanoseconds / 100);
+ writer.WriteStringValue(IntervalSerializingStream.FormatYearMonth(monthDayNano.Months) + " " + timeSpan.ToString());
+ break;
+ case IntervalUnit.YearMonth:
+ writer.WriteStringValue(IntervalSerializingStream.FormatYearMonth(
+ ((YearMonthIntervalArray)array).GetValue(index)!.Value.Months));
+ break;
+ default: writer.WriteNullValue(); break;
+ }
+ break;
+ case ArrowTypeId.Duration:
+ DurationArray dur = (DurationArray)array;
+ writer.WriteStringValue(IntervalSerializingStream.FormatDuration(dur.GetValue(index)!.Value, ((DurationType)dur.Data.DataType).Unit));
+ break;
- ///
- /// Wraps a numeric string as a raw JSON number node so emits it
- /// unquoted (e.g. 1, not "1") with full precision.
- ///
- private static JsonNode? RawNumber(string? numericText) =>
- numericText == null ? null : JsonNode.Parse(numericText);
+ case ArrowTypeId.Boolean: writer.WriteBooleanValue(((BooleanArray)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.Double: writer.WriteNumberValue(((DoubleArray)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.Float: writer.WriteNumberValue(((FloatArray)array).GetValue(index)!.Value); break;
+#if NET5_0_OR_GREATER
+ case ArrowTypeId.HalfFloat:
+ var halfValue = ((HalfFloatArray)array).GetValue(index)!.Value;
+ writer.WriteNumberValue((double)halfValue); break;
+#endif
+ case ArrowTypeId.Int8: writer.WriteNumberValue(((Int8Array)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.Int16: writer.WriteNumberValue(((Int16Array)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.Int32: writer.WriteNumberValue(((Int32Array)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.Int64: writer.WriteNumberValue(((Int64Array)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.String: writer.WriteStringValue(((StringArray)array).GetString(index)); break;
+ case ArrowTypeId.LargeString: writer.WriteStringValue(((LargeStringArray)array).GetString(index)); break;
+#if NET6_0_OR_GREATER
+ case ArrowTypeId.Time32: writer.WriteStringValue(((Time32Array)array).GetTime(index)!.Value.ToString("HH:mm:ss.ffffff")); break;
+ case ArrowTypeId.Time64: writer.WriteStringValue(((Time64Array)array).GetTime(index)!.Value.ToString("HH:mm:ss.ffffff")); break;
+#else
+ case ArrowTypeId.Time32:
+ Time32Array time32Array = (Time32Array)array;
+ int time32 = time32Array.GetValue(index)!.Value;
+ switch (((Time32Type)time32Array.Data.DataType).Unit)
+ {
+ case TimeUnit.Second: writer.WriteStringValue(TimeSpan.FromSeconds(time32).ToString()); break;
+ case TimeUnit.Millisecond: writer.WriteStringValue(TimeSpan.FromMilliseconds(time32).ToString()); break;
+ default: writer.WriteNullValue(); break;
+ };
+ break;
+ case ArrowTypeId.Time64:
+ Time64Array time64Array = (Time64Array)array;
+ long time64 = time64Array.GetValue(index)!.Value;
+ switch (((Time64Type)time64Array.Data.DataType).Unit)
+ {
+ case TimeUnit.Microsecond: writer.WriteStringValue(TimeSpan.FromTicks(time64 * 10).ToString()); break;
+ case TimeUnit.Nanosecond: writer.WriteStringValue(TimeSpan.FromTicks(time64 / 100).ToString()); break;
+ default: writer.WriteNullValue(); break;
+ };
+ break;
+#endif
+ case ArrowTypeId.Timestamp: writer.WriteStringValue(((TimestampArray)array).GetTimestamp(index)!.Value); break;
+ case ArrowTypeId.UInt8: writer.WriteNumberValue(((UInt8Array)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.UInt16: writer.WriteNumberValue(((UInt16Array)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.UInt32: writer.WriteNumberValue(((UInt32Array)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.UInt64: writer.WriteNumberValue(((UInt64Array)array).GetValue(index)!.Value); break;
+ case ArrowTypeId.Binary: writer.WriteBase64StringValue(((BinaryArray)array).GetBytes(index)); break;
+ default: writer.WriteNullValue(); break;
+ }
+ }
- private static object ToListOrMap(ListArray listArray, int index)
+ private static void SerializeListOrMap(Utf8JsonWriter writer, ListArray listArray, int index)
{
IArrowArray values = listArray.Values;
int start = (int)listArray.ValueOffsets[index];
@@ -216,12 +298,16 @@ private static object ToListOrMap(ListArray listArray, int index)
// Arrow MAP is stored as List>
if (values is StructArray structValues && IsMapStruct(structValues))
- return ToMapDict(structValues, start, end);
+ {
+ SerializeMapDict(writer, structValues, start, end);
+ return;
+ }
+ writer.WriteStartArray();
List