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


##########
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:
   Fixed in fc097d76f. `GetArrowArray` now checks `RecordBatch` (which throws 
once the reader is closed) before consulting the cache, and `Dispose` clears 
the cache. `ClosedReaderDoesNotReturnCachedColumns` covers read → access → 
close → access for both `GetValue` and `GetArrowArray`.



##########
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:
   Fixed in fc097d76f. No `DbType` branch handled `DBNull.Value` before this PR 
either, so rather than special-casing the two new branches, `BindParameters` 
now treats `DBNull.Value` as null for every type. The field's nullability uses 
the normalized value too. `BindDBNullParameter` covers Guid, DateTimeOffset, 
Int32 and String.



##########
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:
   Fixed in fc097d76f. `ExtensionTypes.Resolve` now resolves fields nested in 
struct, list, large list and fixed-size list types. When a column's type 
changes, the reader rebuilds its `ArrayData` tree with the resolved types and 
lets `ArrowArrayFactory` create the extension arrays at every level, sharing 
the original buffers. `NestedGuidFromStorageWithExtensionMetadata` covers a 
uuid child in a struct (Strict and JsonString) and in a list.
   
   The list case also exposed apache/arrow-dotnet#468: 
`ListArray.GetSlicedValues` returns null when the values are an 
`ExtensionArray`. The core list converter works around it by slicing directly.



##########
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:
   Fixed in fc097d76f. It now uses an epoch with `DateTimeKind.Unspecified`, 
matching Arrow's own `VariantValue.AsTimestampNtz`. 
`VariantTimestampNtzIsUnspecified` checks the returned `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