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


##########
csharp/src/Client/AdbcDataReader.cs:
##########
@@ -244,6 +257,27 @@ public override string GetName(int ordinal)
             return this.schema.GetFieldByIndex(ordinal)?.Name ?? string.Empty;
         }
 
+        /// <summary>
+        /// Gets the Arrow array for a column in the current record batch. 
Columns with an
+        /// extension type known only from field metadata are wrapped in the 
extension array.
+        /// </summary>
+        /// <param name="ordinal">The zero-based column ordinal.</param>
+        public IArrowArray GetArrowArray(int ordinal)
+        {
+            IArrowArray? column = this.columns[ordinal];
+            if (column == null)
+            {
+                column = this.RecordBatch.Column(ordinal);

Review Comment:
   After Read() and GetValue(0), Close()/Dispose() releases the record batch 
but leaves the cached column populated. Subsequent GetValue() or 
GetArrowArray() calls bypass the RecordBatch check and access a disposed array, 
potentially backed by released native memory. Validate RecordBatch before 
consulting the cache, and add a read/access/close/access regression test.



##########
csharp/src/Client/ExtensionTypes.cs:
##########
@@ -0,0 +1,186 @@
+/*
+* 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);
+
+        /// <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.
+        /// 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 ||
+                !field.HasMetadata ||
+                !field.Metadata.TryGetValue(ExtensionNameKey, out string? 
name))
+            {
+                return field;

Review Comment:
   Extension metadata is resolved only on top-level fields. For a native struct 
with an arrow.uuid child stored as fixed_size_binary(16), the parent returns 
unchanged here. ParseStructArray then returns byte[] for that child under 
StructBehavior.Strict, while the equivalent managed struct containing a 
GuidArray returns Guid. Resolve nested fields and wrap their storage arrays 
too; add a regression test for a UUID child inside a struct.



##########
csharp/src/Client/AdbcCommand.cs:
##########
@@ -382,6 +395,17 @@ private void BindParameters()
                             }
                             parameters[i] = doubleBuilder.Build();
                             break;
+                        case DbType.Guid:
+                            var guidBuilder = new GuidArray.Builder();
+                            switch (param.Value)
+                            {
+                                case null: guidBuilder.AppendNull(); break;

Review Comment:
   Both new binding branches reject Value = DBNull.Value, the standard ADO.NET 
database-null representation. AdbcParameter.Value passes it through unchanged, 
so Guid.Parse or DateTimeOffset.Parse fails and ConvertValue throws 
NotSupportedException instead of binding SQL NULL. Handle DBNull alongside null 
in both the Guid and DateTimeOffset cases, keep the resulting field nullable, 
and test both null parameters.



##########
csharp/src/Client/ExtensionTypes.cs:
##########
@@ -0,0 +1,186 @@
+/*
+* 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);
+
+        /// <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.
+        /// 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 ||
+                !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);
+        }
+
+        /// <summary>
+        /// Gets a value converter for the given type, adding the client's 
handling of
+        /// variants to the conversions supplied by <see 
cref="IArrowArrayExtensions"/>.
+        /// </summary>
+        public static Func<IArrowArray, int, object?> 
GetValueConverter(IArrowType type, StructBehavior structBehavior)
+        {
+            if (type is VariantType)
+            {
+                if (structBehavior == StructBehavior.JsonString)
+                {
+                    return (array, index) => array.IsNull(index) ? null : 
VariantJsonWriter.ToJson(GetVariantValue((VariantArray)array, index));
+                }
+                return (array, index) => array.IsNull(index) ? null : 
ToObject(GetVariantValue((VariantArray)array, index));
+            }
+
+            StructResultType structResultType = structBehavior == 
StructBehavior.JsonString ? StructResultType.JsonString : 
StructResultType.Object;
+            return type.GetValueConverter(structResultType);
+        }
+
+        private static VariantValue GetVariantValue(VariantArray array, int 
index)
+        {
+            return array.IsShredded ? array.GetLogicalVariantValue(index) : 
array.GetVariantValue(index);
+        }
+
+        /// <summary>
+        /// Converts a variant value to the equivalent .NET value. Objects 
become
+        /// <see cref="Dictionary{TKey, TValue}"/> and arrays become <see 
cref="List{T}"/>.
+        /// </summary>
+        internal static object? ToObject(VariantValue value)
+        {
+            if (value.IsObject)
+            {
+                Dictionary<string, object?> result = new Dictionary<string, 
object?>();
+                foreach (KeyValuePair<string, VariantValue> field in 
value.AsObject())
+                {
+                    result.Add(field.Key, ToObject(field.Value));
+                }
+                return result;
+            }
+
+            if (value.IsArray)
+            {
+                IReadOnlyList<VariantValue> elements = value.AsArray();
+                List<object?> result = new List<object?>(elements.Count);
+                foreach (VariantValue element in elements)
+                {
+                    result.Add(ToObject(element));
+                }
+                return result;
+            }
+
+            switch (value.PrimitiveType)
+            {
+                case VariantPrimitiveType.NullType:
+                    return null;
+                case VariantPrimitiveType.BooleanTrue:
+                case VariantPrimitiveType.BooleanFalse:
+                    return value.AsBoolean();
+                case VariantPrimitiveType.Int8:
+                    return value.AsInt8();
+                case VariantPrimitiveType.Int16:
+                    return value.AsInt16();
+                case VariantPrimitiveType.Int32:
+                    return value.AsInt32();
+                case VariantPrimitiveType.Int64:
+                    return value.AsInt64();
+                case VariantPrimitiveType.Float:
+                    return value.AsFloat();
+                case VariantPrimitiveType.Double:
+                    return value.AsDouble();
+                case VariantPrimitiveType.Decimal4:
+                case VariantPrimitiveType.Decimal8:
+                    return value.AsDecimal();
+                case VariantPrimitiveType.Decimal16:
+                    return value.AsSqlDecimal();
+                case VariantPrimitiveType.Date:
+                    return value.AsDate();
+                case VariantPrimitiveType.Timestamp:
+                    return value.AsTimestamp();
+                case VariantPrimitiveType.TimestampNtz:
+                    return value.AsTimestampNtz();
+                case VariantPrimitiveType.TimestampTzNanos:
+                    return s_unixEpoch.AddTicks(value.AsTimestampTzNanos() / 
100);
+                case VariantPrimitiveType.TimestampNtzNanos:
+                    return 
s_unixEpoch.UtcDateTime.AddTicks(value.AsTimestampNtzNanos() / 100);

Review Comment:
   Under StructBehavior.Strict, TimestampNtzNanos becomes a DateTime with 
Kind=Utc because UtcDateTime supplies a UTC epoch and AddTicks preserves its 
kind. This labels a timezone-less value as a UTC instant, affecting 
serialization and timezone conversions. Use an unspecified-kind epoch and add a 
test for the returned DateTime.Kind.



-- 
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