diff --git a/csharp/src/ArrowTypeParser.cs b/csharp/src/ArrowTypeParser.cs index 4b2a44ef..867c34af 100644 --- a/csharp/src/ArrowTypeParser.cs +++ b/csharp/src/ArrowTypeParser.cs @@ -19,6 +19,7 @@ using Apache.Arrow; using Apache.Arrow.Types; using AdbcDrivers.Databricks.StatementExecution; +using AdbcDrivers.HiveServer2.Hive2; namespace AdbcDrivers.Databricks { @@ -57,9 +58,21 @@ internal static class ArrowTypeParser /// true: native nested Arrow types parsed from /// the manifest's type_text (see ). /// - /// Primitives (including INTERVAL, which is always string-typed) ignore the flag. + /// + /// controls how the conversion-sensitive + /// scalar types — DATE / DECIMAL / TIMESTAMP / FLOAT — are surfaced, mirroring + /// : + /// + /// scalar (default): DATE→Date32, DECIMAL→Decimal128, + /// TIMESTAMP→Timestamp, FLOAT→Float (native Arrow types). + /// none: DATE/DECIMAL/TIMESTAMP→String, + /// FLOAT→Double (widening). Paired with + /// which converts the native arrays to match. + /// + /// Other primitives (BOOLEAN/INT/BIGINT/STRING/BINARY/INTERVAL/NULL) ignore the flag. + /// /// - internal static IArrowType MapToArrowType(string typeText, bool enableComplexDatatypeSupport) + internal static IArrowType MapToArrowType(string typeText, bool enableComplexDatatypeSupport, DataTypeConversion dataTypeConversion) { var baseType = ColumnMetadataHelper.GetBaseTypeName(typeText).ToUpperInvariant(); if (baseType is "ARRAY" or "MAP" or "STRUCT") @@ -68,9 +81,16 @@ internal static IArrowType MapToArrowType(string typeText, bool enableComplexDat ? ParseComplexType(typeText) : StringType.Default; } - return MapPrimitiveType(typeText); + return MapPrimitiveType(typeText, dataTypeConversion); } + /// + /// Backward-compatible overload that defaults to . + /// Existing callers (e.g. unit tests that pre-date PECO-3060) keep their current behaviour. + /// + internal static IArrowType MapToArrowType(string typeText, bool enableComplexDatatypeSupport) + => MapToArrowType(typeText, enableComplexDatatypeSupport, DataTypeConversion.Scalar); + /// /// Parses into a native Arrow type. Returns /// on any parse failure — callers can rely on this, @@ -89,8 +109,11 @@ internal static IArrowType ParseComplexType(string typeText) /// Used by for top-level columns and by /// for primitive leaves inside ARRAY/MAP/STRUCT. /// - private static IArrowType MapPrimitiveType(string typeText) + /// The manifest type text (may include parameters like DECIMAL(10,2)). + /// Controls DATE/DECIMAL/TIMESTAMP/FLOAT handling — see . + private static IArrowType MapPrimitiveType(string typeText, DataTypeConversion dataTypeConversion) { + bool convertScalar = dataTypeConversion.HasFlag(DataTypeConversion.Scalar); var baseType = ColumnMetadataHelper.GetBaseTypeName(typeText).ToUpperInvariant(); return baseType switch { @@ -99,13 +122,17 @@ private static IArrowType MapPrimitiveType(string typeText) "SHORT" or "SMALLINT" => Int16Type.Default, "INT" or "INTEGER" => Int32Type.Default, "LONG" or "BIGINT" => Int64Type.Default, - "FLOAT" or "REAL" => FloatType.Default, + // FLOAT: scalar→Float (native), none→Double (widening), matching HiveServer2SchemaParser. + "FLOAT" or "REAL" => convertScalar ? FloatType.Default : DoubleType.Default, "DOUBLE" => DoubleType.Default, - "DECIMAL" or "NUMERIC" => ParseDecimalType(typeText), + // DECIMAL: scalar→Decimal128, none→String, matching HiveServer2SchemaParser. + "DECIMAL" or "NUMERIC" => convertScalar ? ParseDecimalType(typeText) : StringType.Default, "STRING" or "VARCHAR" or "CHAR" => StringType.Default, "BINARY" or "VARBINARY" => BinaryType.Default, - "DATE" => Date32Type.Default, - "TIMESTAMP" or "TIMESTAMP_NTZ" or "TIMESTAMP_LTZ" => TimestampType.Default, + // DATE: scalar→Date32, none→String, matching HiveServer2SchemaParser. + "DATE" => convertScalar ? Date32Type.Default : StringType.Default, + // TIMESTAMP: scalar→Timestamp, none→String, matching HiveServer2SchemaParser. + "TIMESTAMP" or "TIMESTAMP_NTZ" or "TIMESTAMP_LTZ" => convertScalar ? TimestampType.Default : StringType.Default, // INTERVAL is converted to string by IntervalSerializingStream; StringType is the output contract. "INTERVAL" => StringType.Default, "NULL" or "VOID" => NullType.Default, @@ -113,6 +140,15 @@ private static IArrowType MapPrimitiveType(string typeText) }; } + /// + /// Backward-compatible overload defaulting to + /// for the recursive complex-type parser, which always uses native scalar mapping + /// for leaves regardless of the user's flag (Thrift behaves the same — the flag + /// only governs the schema's top-level type for the affected scalars). + /// + private static IArrowType MapPrimitiveType(string typeText) + => MapPrimitiveType(typeText, DataTypeConversion.Scalar); + private static IArrowType ParseDecimalType(string typeText) { int precision = 38; diff --git a/csharp/src/ScalarConversionStream.cs b/csharp/src/ScalarConversionStream.cs new file mode 100644 index 00000000..00e1414e --- /dev/null +++ b/csharp/src/ScalarConversionStream.cs @@ -0,0 +1,274 @@ +/* +* Copyright (c) 2025 ADBC Drivers Contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Apache.Arrow; +using Apache.Arrow.Ipc; +using Apache.Arrow.Types; +using AdbcDrivers.Databricks.StatementExecution; +using AdbcDrivers.HiveServer2.Hive2; + +namespace AdbcDrivers.Databricks +{ + /// + /// PECO-3060: Wraps an and converts native Arrow + /// arrays for the conversion-sensitive scalar types into the shape Thrift's + /// data_type_conv=none mode produces, so SEA and Thrift agree on the output + /// contract regardless of protocol. + /// + /// Active only when the user sets adbc.spark.data_type_conv=none + /// (default is scalar, which keeps native types and bypasses this stream). + /// Mirrors : + /// + /// + /// DATE (Date32Array) → "yyyy-MM-dd" string + /// TIMESTAMP (TimestampArray) → "yyyy-MM-dd HH:mm:ss[.fffffff]" string + /// DECIMAL (Decimal128Array) → plain string (preserves precision) + /// FLOAT (FloatArray) → widened to DoubleArray + /// + /// + /// The declared schema (from TryGetSchemaFromManifest) already reports + /// StringType / DoubleType for these columns; this stream only converts the data + /// arrays so Arrow's strongly-typed contract holds. + /// + /// Column detection: Uses the same Spark:DataType:SqlName + /// metadata pattern as and + /// — the manifest schema embeds the raw + /// SQL type text on every field, which is reliable across inline / CloudFetch / empty paths. + /// + internal sealed class ScalarConversionStream : IArrowArrayStream + { + private readonly IArrowArrayStream _inner; + private readonly Schema _schema; + // index -> kind of conversion to apply. We do this once up front so per-batch + // work is just a dictionary lookup. + private readonly Dictionary _conversions; + + private enum ScalarConversion + { + DateToString, + TimestampToString, + DecimalToString, + FloatToDouble, + } + + public ScalarConversionStream(IArrowArrayStream inner) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + _schema = inner.Schema; + _conversions = DetectConversions(_schema); + } + + public Schema Schema => _schema; + + public async ValueTask ReadNextRecordBatchAsync(CancellationToken cancellationToken = default) + { + RecordBatch? batch = await _inner.ReadNextRecordBatchAsync(cancellationToken).ConfigureAwait(false); + if (batch == null) return null; + if (_conversions.Count == 0) return batch; + return ConvertColumns(batch); + } + + public void Dispose() => _inner.Dispose(); + + private static Dictionary DetectConversions(Schema schema) + { + var result = new Dictionary(); + for (int i = 0; i < schema.FieldsList.Count; i++) + { + Field field = schema.FieldsList[i]; + if (field.Metadata == null) continue; + if (!field.Metadata.TryGetValue(ColumnMetadataHelper.ArrowMetadataKey, out string? sqlName) || sqlName == null) + continue; + + // Match by SQL name prefix to keep the detection logic in sync with + // HiveServer2SchemaParser (which switches on TTypeId). Parametrised types + // like DECIMAL(10,2) and TIMESTAMP_NTZ are handled by prefix checks. + var upper = sqlName.ToUpperInvariant(); + if (upper.StartsWith("DATE", StringComparison.Ordinal) && !upper.StartsWith("DATETIME", StringComparison.Ordinal)) + { + result[i] = ScalarConversion.DateToString; + } + else if (upper.StartsWith("TIMESTAMP", StringComparison.Ordinal)) + { + result[i] = ScalarConversion.TimestampToString; + } + else if (upper.StartsWith("DECIMAL", StringComparison.Ordinal) || upper.StartsWith("NUMERIC", StringComparison.Ordinal)) + { + result[i] = ScalarConversion.DecimalToString; + } + else if (upper.Equals("FLOAT", StringComparison.Ordinal) || upper.Equals("REAL", StringComparison.Ordinal)) + { + result[i] = ScalarConversion.FloatToDouble; + } + } + return result; + } + + private RecordBatch ConvertColumns(RecordBatch batch) + { + var arrays = new IArrowArray[batch.ColumnCount]; + for (int i = 0; i < batch.ColumnCount; i++) + { + arrays[i] = _conversions.TryGetValue(i, out var conv) + ? Convert(batch.Column(i), conv) + : batch.Column(i); + } + return new RecordBatch(_schema, arrays, batch.Length); + } + + private static IArrowArray Convert(IArrowArray array, ScalarConversion conv) + { + return conv switch + { + ScalarConversion.DateToString => ConvertDateToString(array), + ScalarConversion.TimestampToString => ConvertTimestampToString(array), + ScalarConversion.DecimalToString => ConvertDecimalToString(array), + ScalarConversion.FloatToDouble => ConvertFloatToDouble(array), + _ => array, + }; + } + + private static StringArray ConvertDateToString(IArrowArray array) + { + var builder = new StringArray.Builder(); + if (array is Date32Array d32) + { + for (int i = 0; i < d32.Length; i++) + { + if (d32.IsNull(i)) { builder.AppendNull(); continue; } + DateTime? dt = d32.GetDateTime(i); + builder.Append(dt?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + } + } + else if (array is Date64Array d64) + { + for (int i = 0; i < d64.Length; i++) + { + if (d64.IsNull(i)) { builder.AppendNull(); continue; } + DateTime? dt = d64.GetDateTime(i); + builder.Append(dt?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + } + } + else + { + // Defensive: column was detected as DATE via SqlName metadata but the + // underlying array isn't a Date{32,64}Array. Null-fill to keep row counts + // consistent rather than throw. + NullFill(builder, array.Length); + } + return builder.Build(); + } + + private static StringArray ConvertTimestampToString(IArrowArray array) + { + var builder = new StringArray.Builder(); + if (array is TimestampArray ts) + { + var unit = ((TimestampType)ts.Data.DataType).Unit; + for (int i = 0; i < ts.Length; i++) + { + if (ts.IsNull(i)) { builder.AppendNull(); continue; } + DateTimeOffset? dto = ts.GetTimestamp(i); + builder.Append(dto.HasValue ? FormatTimestamp(dto.Value, unit) : null); + } + } + else + { + NullFill(builder, array.Length); + } + return builder.Build(); + } + + /// + /// Formats a timestamp as Thrift would emit it for none mode: "yyyy-MM-dd HH:mm:ss" + /// with a fractional-seconds suffix matching the column unit (omitted for second + /// precision). The value is rendered in UTC because SEA returns TIMESTAMP / TIMESTAMP_NTZ + /// values without timezone info and Thrift renders them as wall-clock strings. + /// + internal static string FormatTimestamp(DateTimeOffset value, TimeUnit unit) + { + // Use UTC wall-clock representation (matches Thrift's TIMESTAMP_NTZ rendering). + var utc = value.UtcDateTime; + string format = unit switch + { + TimeUnit.Second => "yyyy-MM-dd HH:mm:ss", + TimeUnit.Millisecond => "yyyy-MM-dd HH:mm:ss.fff", + TimeUnit.Nanosecond => "yyyy-MM-dd HH:mm:ss.fffffff", + _ => "yyyy-MM-dd HH:mm:ss.ffffff", // Microsecond (SEA default) and unknown + }; + return utc.ToString(format, CultureInfo.InvariantCulture); + } + + private static StringArray ConvertDecimalToString(IArrowArray array) + { + var builder = new StringArray.Builder(); + if (array is Decimal128Array dec) + { + for (int i = 0; i < dec.Length; i++) + { + if (dec.IsNull(i)) { builder.AppendNull(); continue; } + // Decimal128Array.GetString preserves the full precision/scale of the + // declared type — exactly what Thrift returns for none mode. + builder.Append(dec.GetString(i)); + } + } + else + { + NullFill(builder, array.Length); + } + return builder.Build(); + } + + private static DoubleArray ConvertFloatToDouble(IArrowArray array) + { + var builder = new DoubleArray.Builder().Reserve(array.Length); + if (array is FloatArray f) + { + for (int i = 0; i < f.Length; i++) + { + if (f.IsNull(i)) { builder.AppendNull(); continue; } + float? v = f.GetValue(i); + builder.Append(v.HasValue ? (double)v.Value : (double?)null); + } + } + else if (array is DoubleArray d) + { + // Already double — return as-is by rebuilding so the caller's contract + // is preserved (DoubleArray output regardless of input). + for (int i = 0; i < d.Length; i++) + { + if (d.IsNull(i)) { builder.AppendNull(); continue; } + builder.Append(d.GetValue(i)); + } + } + else + { + for (int i = 0; i < array.Length; i++) builder.AppendNull(); + } + return builder.Build(); + } + + private static void NullFill(StringArray.Builder builder, int length) + { + for (int i = 0; i < length; i++) builder.AppendNull(); + } + } +} diff --git a/csharp/src/StatementExecution/StatementExecutionConnection.cs b/csharp/src/StatementExecution/StatementExecutionConnection.cs index 7b6361ae..1bb7b46e 100644 --- a/csharp/src/StatementExecution/StatementExecutionConnection.cs +++ b/csharp/src/StatementExecution/StatementExecutionConnection.cs @@ -73,6 +73,7 @@ internal class StatementExecutionConnection : TracingConnection, IGetObjectsData private readonly string _traceParentHeaderName; private readonly bool _traceStateEnabled; private readonly bool _enableComplexDatatypeSupport; + private readonly DataTypeConversion _dataTypeConversion; // Authentication support private readonly string? _identityFederationClientId; @@ -242,6 +243,13 @@ private StatementExecutionConnection( _traceStateEnabled = PropertyHelper.GetBooleanPropertyWithValidation(properties, DatabricksParameters.TraceStateEnabled, false); _enableComplexDatatypeSupport = PropertyHelper.GetBooleanPropertyWithValidation(properties, DatabricksParameters.EnableComplexDatatypeSupport, false); + // PECO-3060: Mirror the Thrift path's handling of adbc.spark.data_type_conv + // (see SparkHttpConnection.ValidateOptions). scalar (default) keeps native + // Arrow types for DATE/DECIMAL/TIMESTAMP/FLOAT; none surfaces them as strings + // (and widens FLOAT to DOUBLE), matching HiveServer2SchemaParser.GetArrowType. + properties.TryGetValue(SparkParameters.DataTypeConv, out string? dataTypeConv); + _dataTypeConversion = DataTypeConversionParser.Parse(dataTypeConv); + // Authentication configuration if (properties.TryGetValue(DatabricksParameters.IdentityFederationClientId, out string? identityFederationClientId)) { @@ -936,6 +944,13 @@ public override void Dispose() // TracingConnection provides IActivityTracer implementation internal bool EnableComplexDatatypeSupport => _enableComplexDatatypeSupport; + /// + /// The parsed scalar data-type conversion mode. Mirrors HiveServer2Connection.DataTypeConversion — + /// scalar (default) keeps native types, none surfaces DATE/DECIMAL/TIMESTAMP as strings (and + /// widens FLOAT to DOUBLE) so SEA matches Thrift's behaviour. See PECO-3060. + /// + internal DataTypeConversion DataTypeConversion => _dataTypeConversion; + public override string AssemblyVersion => GetType().Assembly.GetName().Version?.ToString() ?? "1.0.0"; public override string AssemblyName => "AdbcDrivers.Databricks"; } diff --git a/csharp/src/StatementExecution/StatementExecutionStatement.cs b/csharp/src/StatementExecution/StatementExecutionStatement.cs index b910097a..1661a79e 100644 --- a/csharp/src/StatementExecution/StatementExecutionStatement.cs +++ b/csharp/src/StatementExecution/StatementExecutionStatement.cs @@ -69,6 +69,9 @@ internal class StatementExecutionStatement : TracingStatement // Complex type configuration private readonly bool _enableComplexDatatypeSupport; + // Scalar data-type conversion mode (PECO-3060) — mirrors the connection setting. + private readonly DataTypeConversion _dataTypeConversion; + // Connection reference for metadata queries private readonly StatementExecutionConnection _connection; @@ -186,6 +189,7 @@ public StatementExecutionStatement( _lz4BufferPool = lz4BufferPool ?? throw new ArgumentNullException(nameof(lz4BufferPool)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _enableComplexDatatypeSupport = connection.EnableComplexDatatypeSupport; + _dataTypeConversion = connection.DataTypeConversion; // Match Thrift: statement starts with connection's default catalog. // When enableMultipleCatalogSupport=true, this is the catalog from config (e.g. "main"). @@ -402,6 +406,15 @@ private async Task ExecuteQueryInternalAsync(CancellationToken canc reader = new ComplexTypeSerializingStream(reader); } + // PECO-3060: when data_type_conv=none, surface DATE/DECIMAL/TIMESTAMP as strings + // and widen FLOAT to DOUBLE so SEA matches Thrift's HiveServer2SchemaParser semantics. + // Bypass when scalar (default) — the native types declared in the manifest schema + // are already the desired output. + if (!_dataTypeConversion.HasFlag(DataTypeConversion.Scalar)) + { + reader = new ScalarConversionStream(reader); + } + // Get schema from reader var schema = reader.Schema; @@ -615,7 +628,7 @@ private Schema GetSchemaFromManifest(ResultManifest manifest) foreach (var column in manifest.Schema.Columns) { var typeText = column.TypeText ?? string.Empty; - var arrowType = ArrowTypeParser.MapToArrowType(typeText, _enableComplexDatatypeSupport); + var arrowType = ArrowTypeParser.MapToArrowType(typeText, _enableComplexDatatypeSupport, _dataTypeConversion); var metadata = new Dictionary { [ColumnMetadataHelper.ArrowMetadataKey] = typeText diff --git a/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs b/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs index 88d0ea7c..13ae9f25 100644 --- a/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs +++ b/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs @@ -16,8 +16,11 @@ using System; using System.Collections.Generic; +using Apache.Arrow; using Apache.Arrow.Adbc; +using Apache.Arrow.Types; using AdbcDrivers.HiveServer2.Spark; +using AdbcDrivers.HiveServer2.Hive2; using Apache.Arrow.Adbc.Tests; using Xunit; using Xunit.Abstractions; @@ -43,12 +46,19 @@ private void SkipIfNotConfigured() // and OAuth M2M (client_credentials) flow (implemented in PECO-2857). } - private AdbcConnection CreateRestConnection() + private AdbcConnection CreateRestConnection(IReadOnlyDictionary? extraProperties = null) { var properties = new Dictionary { [DatabricksParameters.Protocol] = "rest", }; + if (extraProperties != null) + { + foreach (var kv in extraProperties) + { + properties[kv.Key] = kv.Value; + } + } // Use URI if available (connection will parse host and warehouse ID from it) if (!string.IsNullOrEmpty(TestConfiguration.Uri)) @@ -432,5 +442,90 @@ public void ExecuteQuery_WithNullValues_HandlesNullsCorrectly() Assert.NotNull(batch); Assert.Equal(1, batch.Length); } + + /// + /// PECO-3060: Verifies that the SEA path honors adbc.spark.data_type_conv=none + /// (the M3 parameter gap). When set, DATE / DECIMAL / TIMESTAMP columns must surface + /// as StringType — matching the Thrift behaviour driven by + /// HiveServer2SchemaParser.GetArrowType (none → StringType for those types). + /// + [SkippableFact] + public void ExecuteQuery_DataTypeConv_None_SerializesScalarTypesToStrings() + { + SkipIfNotConfigured(); + + var extra = new Dictionary + { + [SparkParameters.DataTypeConv] = DataTypeConversionOptions.None, + }; + using var connection = CreateRestConnection(extra); + using var statement = connection.CreateStatement(); + + // Single-row SELECT covering each conversion-sensitive scalar type. + // Cheap query — no warehouse scan, INLINE result. + statement.SqlQuery = + "SELECT " + + "CAST('2024-01-15' AS DATE) AS date_col, " + + "CAST('2024-01-15 10:20:30.123' AS TIMESTAMP) AS ts_col, " + + "CAST(123.456 AS DECIMAL(10,3)) AS dec_col"; + + var result = statement.ExecuteQuery(); + Assert.NotNull(result); + using var reader = result.Stream; + Assert.NotNull(reader); + + var schema = reader.Schema; + Assert.NotNull(schema); + Assert.Equal(3, schema.FieldsList.Count); + + // Honest signal: with data_type_conv=none, all three conversion-sensitive + // columns must be exposed as StringType — same as Thrift's HiveServer2SchemaParser + // does for none mode. + Assert.Equal(ArrowTypeId.String, schema.GetFieldByName("date_col").DataType.TypeId); + Assert.Equal(ArrowTypeId.String, schema.GetFieldByName("ts_col").DataType.TypeId); + Assert.Equal(ArrowTypeId.String, schema.GetFieldByName("dec_col").DataType.TypeId); + + // The data arrays must agree with the declared schema (Arrow contract). + var batch = reader.ReadNextRecordBatchAsync().Result; + Assert.NotNull(batch); + Assert.Equal(1, batch!.Length); + Assert.IsType(batch.Column("date_col")); + Assert.IsType(batch.Column("ts_col")); + Assert.IsType(batch.Column("dec_col")); + } + + /// + /// PECO-3060: Sanity check the default (scalar) mode on SEA — DATE / DECIMAL / + /// TIMESTAMP columns must continue to surface as their native Arrow types so + /// existing callers are unaffected. + /// + [SkippableFact] + public void ExecuteQuery_DataTypeConv_Scalar_KeepsNativeTypes() + { + SkipIfNotConfigured(); + + var extra = new Dictionary + { + [SparkParameters.DataTypeConv] = DataTypeConversionOptions.Scalar, + }; + using var connection = CreateRestConnection(extra); + using var statement = connection.CreateStatement(); + + statement.SqlQuery = + "SELECT " + + "CAST('2024-01-15' AS DATE) AS date_col, " + + "CAST('2024-01-15 10:20:30.123' AS TIMESTAMP) AS ts_col, " + + "CAST(123.456 AS DECIMAL(10,3)) AS dec_col"; + + var result = statement.ExecuteQuery(); + Assert.NotNull(result); + using var reader = result.Stream; + Assert.NotNull(reader); + + var schema = reader.Schema; + Assert.Equal(ArrowTypeId.Date32, schema.GetFieldByName("date_col").DataType.TypeId); + Assert.Equal(ArrowTypeId.Timestamp, schema.GetFieldByName("ts_col").DataType.TypeId); + Assert.Equal(ArrowTypeId.Decimal128, schema.GetFieldByName("dec_col").DataType.TypeId); + } } }