Copilot commented on code in PR #4919:
URL: https://github.com/apache/arrow-adbc/pull/4919#discussion_r4307778240


##########
csharp/src/Client/SchemaConverter.cs:
##########
@@ -31,13 +31,13 @@ internal class SchemaConverter
         /// Converts an Arrow <see cref="Schema"/> to a
         /// <see cref="DataTable"/> schema.
         /// </summary>
-        /// <param name="schema">The Arrow schema</param>
+        /// <param name="fields">The fields of the Arrow schema</param>
         /// <param name="adbcStatement">The AdbcStatement to use</param>
         /// <exception cref="ArgumentNullException"></exception>
-        public static DataTable ConvertArrowSchema(Schema schema, 
AdbcStatement adbcStatement, DecimalBehavior decimalBehavior, StructBehavior 
structBehavior)
+        public static DataTable ConvertArrowSchema(IReadOnlyList<Field> 
fields, AdbcStatement adbcStatement, DecimalBehavior decimalBehavior, 
StructBehavior structBehavior)

Review Comment:
   This signature introduces `IReadOnlyList<Field>` but the file does not 
import `System.Collections.Generic` (and the repository disables implicit 
usings in `csharp/Directory.Build.props:43`). The client project therefore 
fails to compile with `IReadOnlyList<>` unresolved. Add the using or fully 
qualify the type.



##########
csharp/src/Apache.Arrow.Adbc/Extensions/IArrowArrayExtensions.cs:
##########
@@ -156,16 +157,31 @@ public static class IArrowArrayExtensions
                     }
                 case ArrowTypeId.Binary:
                     return ((BinaryArray)arrowArray).GetBytes(index).ToArray();
+                case ArrowTypeId.FixedSizedBinary:
+                    return 
((FixedSizeBinaryArray)arrowArray).GetBytes(index).ToArray();
                 case ArrowTypeId.List:
-                    return ((ListArray)arrowArray).GetSlicedValues(index);
+                    return GetListValues((ListArray)arrowArray, index);
                 case ArrowTypeId.Struct:
                     StructArray structArray = (StructArray)arrowArray;
                     return resultType == StructResultType.JsonString ? 
SerializeToJson(structArray, index) : ParseStructArray(structArray, index);
+                case ArrowTypeId.Extension:
+                    switch (arrowArray)
+                    {
+                        case GuidArray guidArray:
+                            return guidArray.GetGuid(index);
+                        case Bool8Array bool8Array:
+                            return bool8Array.GetValue(index);
+                        case TimestampWithOffsetArray timestampWithOffsetArray:
+                            return timestampWithOffsetArray.GetValue(index);
+                        case ExtensionArray extensionArray:
+                            // Unrecognized extension types are surfaced as 
their storage values.
+                            return ValueAt(extensionArray.Storage, index, 
resultType);

Review Comment:
   This fallback is also used when ParseStructArray walks a struct containing a 
resolved arrow.parquet.variant child. Unlike a top-level variant, that child is 
reduced to its storage array here, so StructBehavior.JsonString/Strict returns 
the variant's storage representation instead of VariantJsonWriter output or the 
logical Dictionary/List/scalar value. Handle VariantArray in the recursive 
conversion path as well.



##########
csharp/src/Client/AdbcCommand.cs:
##########
@@ -298,204 +298,231 @@ private void BindParameters()
                 for (int i = 0; i < fields.Length; i++)
                 {
                     AdbcParameter param = 
(AdbcParameter)_dbParameterCollection[i];
+                    object? value = param.Value is DBNull ? null : param.Value;
                     switch (param.DbType)
                     {
                         case DbType.Binary:
                             var binaryBuilder = new BinaryArray.Builder();
-                            switch (param.Value)
+                            switch (value)
                             {
                                 case null: binaryBuilder.AppendNull(); break;
                                 case byte[] array: 
binaryBuilder.Append(array.AsSpan()); break;
-                                default: throw new 
NotSupportedException($"Values of type {param.Value.GetType().Name} cannot be 
bound as binary");
+                                default: throw new 
NotSupportedException($"Values of type {value.GetType().Name} cannot be bound 
as binary");
                             }
                             parameters[i] = binaryBuilder.Build();
                             break;
                         case DbType.Boolean:
                             var boolBuilder = new BooleanArray.Builder();
-                            switch (param.Value)
+                            switch (value)
                             {
                                 case null: boolBuilder.AppendNull(); break;
                                 case bool boolValue: 
boolBuilder.Append(boolValue); break;
-                                default: 
boolBuilder.Append(ConvertValue(param.Value, Convert.ToBoolean, 
DbType.Boolean)); break;
+                                default: 
boolBuilder.Append(ConvertValue(value, Convert.ToBoolean, DbType.Boolean)); 
break;
                             }
                             parameters[i] = boolBuilder.Build();
                             break;
                         case DbType.Byte:
                             var uint8Builder = new UInt8Array.Builder();
-                            switch (param.Value)
+                            switch (value)
                             {
                                 case null: uint8Builder.AppendNull(); break;
                                 case byte byteValue: 
uint8Builder.Append(byteValue); break;
-                                default: 
uint8Builder.Append(ConvertValue(param.Value, Convert.ToByte, DbType.Byte)); 
break;
+                                default: 
uint8Builder.Append(ConvertValue(value, Convert.ToByte, DbType.Byte)); break;
                             }
                             parameters[i] = uint8Builder.Build();
                             break;
                         case DbType.Date:
                             var dateBuilder = new Date32Array.Builder();
-                            switch (param.Value)
+                            switch (value)
                             {
                                 case null: dateBuilder.AppendNull(); break;
                                 case DateTime datetime: 
dateBuilder.Append(datetime); break;
 #if NET5_0_OR_GREATER
                                 case DateOnly dateonly: 
dateBuilder.Append(dateonly); break;
 #endif
-                                default: 
dateBuilder.Append(ConvertValue(param.Value, Convert.ToDateTime, DbType.Date)); 
break;
+                                default: 
dateBuilder.Append(ConvertValue(value, Convert.ToDateTime, DbType.Date)); break;
                             }
                             parameters[i] = dateBuilder.Build();
                             break;
                         case DbType.DateTime:
                             var timestampBuilder = new 
TimestampArray.Builder();
-                            switch (param.Value)
+                            switch (value)
                             {
                                 case null: timestampBuilder.AppendNull(); 
break;
                                 case DateTime datetime: 
timestampBuilder.Append(datetime); break;
-                                default: 
timestampBuilder.Append(ConvertValue(param.Value, Convert.ToDateTime, 
DbType.DateTime)); break;
+                                default: 
timestampBuilder.Append(ConvertValue(value, Convert.ToDateTime, 
DbType.DateTime)); break;
                             }
                             parameters[i] = timestampBuilder.Build();
                             break;
+                        case DbType.DateTimeOffset:
+                            // Bound as a UTC timestamp, which every driver 
understands; the
+                            // original offset is not preserved.
+                            var timestampOffsetBuilder = new 
TimestampArray.Builder(TimeUnit.Microsecond, "UTC");
+                            switch (value)
+                            {
+                                case null: 
timestampOffsetBuilder.AppendNull(); break;
+                                case DateTimeOffset dateTimeOffset: 
timestampOffsetBuilder.Append(dateTimeOffset); break;
+                                case DateTime datetime: 
timestampOffsetBuilder.Append(new DateTimeOffset(datetime)); break;
+                                default: 
timestampOffsetBuilder.Append(ConvertValue(value, v => 
DateTimeOffset.Parse(Convert.ToString(v, CultureInfo.InvariantCulture)!, 
CultureInfo.InvariantCulture), DbType.DateTimeOffset)); break;
+                            }
+                            parameters[i] = timestampOffsetBuilder.Build();
+                            break;
                         case DbType.Decimal:
-                            var value = param.Value switch
+                            var decimalValue = value switch
                             {
                                 null => (SqlDecimal?)null,
                                 SqlDecimal sqlDecimal => sqlDecimal,
                                 decimal d => new SqlDecimal(d),
-                                _ => new SqlDecimal(ConvertValue(param.Value, 
Convert.ToDecimal, DbType.Decimal)),
+                                _ => new SqlDecimal(ConvertValue(value, 
Convert.ToDecimal, DbType.Decimal)),
                             };
-                            var decimalBuilder = new 
Decimal128Array.Builder(new Decimal128Type(value?.Precision ?? 10, value?.Scale 
?? 0));
-                            if (value is null)
+                            var decimalBuilder = new 
Decimal128Array.Builder(new Decimal128Type(decimalValue?.Precision ?? 10, 
decimalValue?.Scale ?? 0));
+                            if (decimalValue is null)
                             {
                                 decimalBuilder.AppendNull();
                             }
                             else
                             {
-                                decimalBuilder.Append(value.Value);
+                                decimalBuilder.Append(decimalValue.Value);
                             }
                             parameters[i] = decimalBuilder.Build();
                             break;
                         case DbType.Double:
                             var doubleBuilder = new DoubleArray.Builder();
-                            switch (param.Value)
+                            switch (value)
                             {
                                 case null: doubleBuilder.AppendNull(); break;
                                 case double dbl: doubleBuilder.Append(dbl); 
break;
-                                default: 
doubleBuilder.Append(ConvertValue(param.Value, Convert.ToDouble, 
DbType.Double)); break;
+                                default: 
doubleBuilder.Append(ConvertValue(value, Convert.ToDouble, DbType.Double)); 
break;
                             }
                             parameters[i] = doubleBuilder.Build();
                             break;
+                        case DbType.Guid:
+                            var guidBuilder = new GuidArray.Builder();
+                            switch (value)
+                            {
+                                case null: guidBuilder.AppendNull(); break;
+                                case Guid guid: guidBuilder.Append(guid); 
break;
+                                case byte[] bytes when bytes.Length == 16: 
guidBuilder.Append(new Guid(bytes)); break;

Review Comment:
   `GuidArray` stores UUID bytes in RFC 4122 big-endian order, but `new 
Guid(bytes)` interprets the first three fields using .NET's legacy 
little-endian layout. A 16-byte canonical `arrow.uuid` parameter will therefore 
be byte-swapped before binding (for example, the bytes returned by 
`GuidArray.GuidToBytes(guid)` will not bind back to `guid`). Convert canonical 
bytes with `GuidArray.RFC4122ToGuid(bytes)` instead.



##########
csharp/src/Client/SchemaConverter.cs:
##########
@@ -214,6 +215,23 @@ public static Type GetArrowType(Field f, DecimalBehavior 
decimalBehavior, Struct
                     }
                     goto default;
 
+                case ArrowTypeId.Extension:
+                    switch (f.DataType)
+                    {
+                        case GuidType:
+                            return typeof(Guid);
+                        case Bool8Type:
+                            return typeof(bool);
+                        case TimestampWithOffsetType:
+                            return typeof(DateTimeOffset);
+                        case VariantType:
+                            return structBehavior == StructBehavior.JsonString 
? typeof(string) : typeof(object);
+                        default:
+                            // Unrecognized extension types are surfaced as 
their storage values.
+                            ExtensionType extensionType = 
(ExtensionType)f.DataType;
+                            return ConvertArrowType(new Field(f.Name, 
extensionType.StorageType, f.IsNullable), decimalBehavior, structBehavior);

Review Comment:
   The new nested extension resolution can produce a ListType whose value type 
is GuidType, Bool8Type, TimestampWithOffsetType, or VariantType, but 
ConvertArrowType still routes lists through GetArrowArrayType(valueType), whose 
switch has no extension cases. Calling GetFieldType, GetColumnSchema, or 
GetSchemaTable for a list of one of these extensions therefore throws 
InvalidCastException; update list/array-type conversion to handle resolved 
extension children.



##########
csharp/src/Client/ExtensionTypes.cs:
##########
@@ -0,0 +1,259 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 Apache.Arrow.Adbc.Extensions;
+using Apache.Arrow.Operations.Shredding;
+using Apache.Arrow.Operations.VariantJson;
+using Apache.Arrow.Scalars.Variant;
+using Apache.Arrow.Types;
+
+namespace Apache.Arrow.Adbc.Client
+{
+    /// <summary>
+    /// Support for Arrow extension types in the ADO.NET client.
+    /// </summary>
+    internal static class ExtensionTypes
+    {
+        internal const string ExtensionNameKey = "ARROW:extension:name";
+        internal const string ExtensionMetadataKey = 
"ARROW:extension:metadata";
+
+        private static readonly DateTimeOffset s_unixEpoch = new 
DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
+        private static readonly DateTime s_unixEpochUnspecified = new 
DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
+
+        /// <summary>
+        /// The extension types the client understands without any 
registration by the caller.
+        /// </summary>
+        private static readonly ExtensionTypeRegistry s_builtIn = 
CreateBuiltInRegistry();
+
+        private static ExtensionTypeRegistry CreateBuiltInRegistry()
+        {
+            ExtensionTypeRegistry registry = new ExtensionTypeRegistry();
+            registry.Register(GuidExtensionDefinition.Instance);
+            registry.Register(Bool8ExtensionDefinition.Instance);
+            registry.Register(TimestampWithOffsetExtensionDefinition.Instance);
+            registry.Register(VariantExtensionDefinition.Instance);
+            return registry;
+        }
+
+        /// <summary>
+        /// Arrays imported through the C Data Interface carry extension types 
only as field
+        /// metadata over their storage type. Returns a field whose type is 
the corresponding
+        /// <see cref="ExtensionType"/> when the extension is known, or the 
original field otherwise.
+        /// Fields nested in struct and list types are resolved too.
+        /// Definitions registered with <see 
cref="ExtensionTypeRegistry.Default"/> take precedence
+        /// over the built-in ones.
+        /// </summary>
+        public static Field Resolve(Field field)
+        {
+            if (field.DataType is ExtensionType)
+            {
+                return field;
+            }
+
+            IArrowType resolvedType = ResolveChildren(field.DataType);
+            if (!ReferenceEquals(resolvedType, field.DataType))
+            {
+                field = new Field(field.Name, resolvedType, field.IsNullable, 
field.Metadata);
+            }
+
+            if (!field.HasMetadata ||
+                !field.Metadata.TryGetValue(ExtensionNameKey, out string? 
name))
+            {
+                return field;
+            }
+
+            if (!ExtensionTypeRegistry.Default.TryGetDefinition(name, out 
ExtensionDefinition definition) &&
+                !s_builtIn.TryGetDefinition(name, out definition))
+            {
+                return field;
+            }
+
+            field.Metadata.TryGetValue(ExtensionMetadataKey, out string? 
metadata);
+            if (!definition.TryCreateType(field.DataType, metadata ?? 
string.Empty, out ExtensionType extensionType))
+            {
+                return field;
+            }
+
+            return new Field(field.Name, extensionType, field.IsNullable, 
field.Metadata);
+        }
+
+        private static IArrowType ResolveChildren(IArrowType type)
+        {
+            if (type is not NestedType nestedType)
+            {
+                return type;
+            }
+
+            Field[] fields = new Field[nestedType.Fields.Count];
+            bool changed = false;
+            for (int i = 0; i < fields.Length; i++)
+            {
+                fields[i] = Resolve(nestedType.Fields[i]);
+                changed |= !ReferenceEquals(fields[i], nestedType.Fields[i]);
+            }
+
+            if (!changed)
+            {
+                return type;
+            }
+
+            switch (type)
+            {
+                case StructType:
+                    return new StructType(fields);
+                case ListType:
+                    return new ListType(fields[0]);
+                case LargeListType:
+                    return new LargeListType(fields[0]);
+                case FixedSizeListType fixedSizeListType:
+                    return new FixedSizeListType(fields[0], 
fixedSizeListType.ListSize);

Review Comment:
   Resolving nested UUID/bool8/other extensions creates types such as 
`ListType(GuidType)`, but `SchemaConverter.ConvertArrowType` sends every list 
value type to `GetArrowArrayType`, which has no extension cases. Consequently a 
reader can read the list value but `GetFieldType`, `GetColumnSchema`, or 
`GetSchemaTable` throws for `list<uuid>` (and the other extension element 
types). Add schema mappings for extension element arrays (and their storage 
fallback) and cover the schema APIs in the nested-extension test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to