This is an automated email from the ASF dual-hosted git repository.

CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new 2042317c2d chore(csharp): add variant tests using DuckDB (#4944)
2042317c2d is described below

commit 2042317c2d80387a754394e8b52265e9e1bc856f
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Thu Oct 22 13:10:09 2026 -0700

    chore(csharp): add variant tests using DuckDB (#4944)
    
    Add tests for reading and ingesting variants using DuckDB. (These are
    skipped pre-DuckDB 2.0.)
    Also fixes path concatenation of DuckDB driver file path.
---
 csharp/Benchmarks/Benchmarks.csproj                |   2 +-
 .../Apache.Arrow.Adbc.Testing.csproj               |   2 +-
 .../Apache.Arrow.Adbc.Tests/ImportedDuckDbTests.cs | 208 ++++++++++++++++++++-
 3 files changed, 209 insertions(+), 3 deletions(-)

diff --git a/csharp/Benchmarks/Benchmarks.csproj 
b/csharp/Benchmarks/Benchmarks.csproj
index 34066b2388..3df35b57f1 100644
--- a/csharp/Benchmarks/Benchmarks.csproj
+++ b/csharp/Benchmarks/Benchmarks.csproj
@@ -34,7 +34,7 @@
 
   <Target Name="CopyDuckDb" AfterTargets="Build">
     <PropertyGroup>
-      
<DuckDbPackagePath>$(NuGetPackageRoot)duckdb.net.bindings.full\$(DuckDbVersion)</DuckDbPackagePath>
+      <DuckDbPackagePath>$([System.IO.Path]::Combine('$(NuGetPackageRoot)', 
'duckdb.net.bindings.full', '$(DuckDbVersion)'))</DuckDbPackagePath>
     </PropertyGroup>
     <Copy 
Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))'
 == 'true'" 
SourceFiles="$(DuckDbPackagePath)\runtimes\win-$(ProcessArchitecture)\native\duckdb.dll"
 DestinationFolder="$(OutputPath)" />
     <Copy 
Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'
 == 'true'" 
SourceFiles="$(DuckDbPackagePath)\runtimes\linux-$(ProcessArchitecture)\native\libduckdb.so"
 DestinationFolder="$(OutputPath)" />
diff --git 
a/csharp/test/Apache.Arrow.Adbc.Tests/Apache.Arrow.Adbc.Testing.csproj 
b/csharp/test/Apache.Arrow.Adbc.Tests/Apache.Arrow.Adbc.Testing.csproj
index 295fd4fe5b..ea39d273bb 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/Apache.Arrow.Adbc.Testing.csproj
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/Apache.Arrow.Adbc.Testing.csproj
@@ -44,7 +44,7 @@
 
   <Target Name="CopyDuckDb" AfterTargets="Build">
     <PropertyGroup>
-      
<DuckDbPackagePath>$(NuGetPackageRoot)duckdb.net.bindings.full\$(DuckDbVersion)</DuckDbPackagePath>
+      <DuckDbPackagePath>$([System.IO.Path]::Combine('$(NuGetPackageRoot)', 
'duckdb.net.bindings.full', '$(DuckDbVersion)'))</DuckDbPackagePath>
     </PropertyGroup>
     <Copy 
Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))'
 == 'true'" 
SourceFiles="$(DuckDbPackagePath)\runtimes\win-$(ProcessArchitecture)\native\duckdb.dll"
 DestinationFolder="$(OutputPath)" />
     <Copy 
Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'
 == 'true'" 
SourceFiles="$(DuckDbPackagePath)\runtimes\linux-$(ProcessArchitecture)\native\libduckdb.so"
 DestinationFolder="$(OutputPath)" />
diff --git a/csharp/test/Apache.Arrow.Adbc.Tests/ImportedDuckDbTests.cs 
b/csharp/test/Apache.Arrow.Adbc.Tests/ImportedDuckDbTests.cs
index b29cc86d52..59f3b9ce2a 100644
--- a/csharp/test/Apache.Arrow.Adbc.Tests/ImportedDuckDbTests.cs
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/ImportedDuckDbTests.cs
@@ -23,18 +23,23 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using Apache.Arrow.Ipc;
+using Apache.Arrow.Operations.Shredding;
+using Apache.Arrow.Scalars.Variant;
 using Apache.Arrow.Types;
 using Xunit;
+using Xunit.Abstractions;
 
 namespace Apache.Arrow.Adbc.Tests
 {
     public class ImportedDuckDbTests : IClassFixture<DuckDbFixture>
     {
         readonly DuckDbFixture _duckDb;
+        readonly ITestOutputHelper _output;
 
-        public ImportedDuckDbTests(DuckDbFixture duckDb)
+        public ImportedDuckDbTests(DuckDbFixture duckDb, ITestOutputHelper 
output)
         {
             _duckDb = duckDb;
+            _output = output;
         }
 
         [Fact]
@@ -214,6 +219,198 @@ namespace Apache.Arrow.Adbc.Tests
             Assert.Equal(6, GetResultCount(statement3, "SELECT * from 
main.ingested"));
         }
 
+        [SkippableFact]
+        public async Task ReadVariant()
+        {
+            using var database = _duckDb.OpenDatabase("readvariant.db");
+            using var connection = database.Connect(null);
+            using var statement = connection.CreateStatement();
+
+            RequireVariantSupport(statement);
+            statement.SqlQuery = """
+                SELECT v FROM (VALUES
+                    (1, 42::VARIANT),
+                    (2, 'hello'::VARIANT),
+                    (3, {'name': 'widget', 'sizes': [1, 2]}::VARIANT),
+                    (4, NULL::VARIANT)
+                ) AS variants(id, v) ORDER BY id
+                """;
+            using var stream = statement.ExecuteQuery().Stream;
+            Assert.NotNull(stream);
+            Field field = stream.Schema.FieldsList[0];
+            Assert.Equal("arrow.parquet.variant", 
field.Metadata["ARROW:extension:name"]);
+
+            using var batch = await stream.ReadNextRecordBatchAsync();
+            Assert.NotNull(batch);
+            // The C Data Interface imports extension arrays as storage plus 
field metadata.
+            var variants = new VariantArray(batch.Column(0));
+            Assert.Equal(4, variants.Length);
+            Assert.Equal(42, variants.GetVariantValue(0).AsInt32());
+            Assert.Equal("hello", variants.GetVariantValue(1).AsString());
+            var obj = variants.GetVariantValue(2).AsObject();
+            Assert.Equal("widget", obj["name"].AsString());
+            Assert.Equal(new[] { 1, 2 }, obj["sizes"].AsArray().Select(value 
=> value.AsInt32()));
+            Assert.True(variants.IsNull(3));
+            Assert.Null(await stream.ReadNextRecordBatchAsync());
+        }
+
+        [SkippableFact]
+        public async Task IngestVariant()
+        {
+            using var database = _duckDb.OpenDatabase("ingestvariant.db");
+            using var connection = database.Connect(null);
+            using var statement = connection.CreateStatement();
+            RequireVariantSupport(statement);
+            using var variants = new VariantArray.Builder()
+                .Append(VariantValue.FromInt8(42))
+                .Append(VariantValue.FromString("hello"))
+                .Append(VariantValue.FromObject(new Dictionary<string, 
VariantValue>
+                {
+                    { "name", VariantValue.FromString("widget") },
+                    { "sizes", 
VariantValue.FromArray(VariantValue.FromInt8(1), VariantValue.FromInt8(2)) },
+                }))
+                .Append(VariantValue.Null)
+                .AppendNull()
+                .Build();
+            var schema = new Schema([
+                new Field("id", Int32Type.Default, false),
+                new Field("v", variants.ExtensionType, true),
+            ], null);
+            using var batch = new RecordBatch(schema, [
+                new Int32Array.Builder().AppendRange([1, 2, 3, 4, 5]).Build(),
+                variants,
+            ], variants.Length);
+            using (var ingest = connection.BulkIngest("variants", 
BulkIngestMode.Create))
+            {
+                ingest.Bind(batch, schema);
+                ingest.ExecuteUpdate();
+            }
+
+            Assert.Equal("VARIANT", GetScalarString(statement, """
+                SELECT data_type FROM information_schema.columns
+                WHERE table_name = 'variants' AND column_name = 'v'
+                """));
+
+            statement.SqlQuery = "SELECT v FROM variants ORDER BY id";
+            using var stream = statement.ExecuteQuery().Stream;
+            Assert.NotNull(stream);
+            using var result = await stream.ReadNextRecordBatchAsync();
+            Assert.NotNull(result);
+            Assert.Equal("arrow.parquet.variant", 
stream.Schema.FieldsList[0].Metadata["ARROW:extension:name"]);
+            var values = new VariantArray(result.Column(0));
+            Assert.Equal(variants.Length, values.Length);
+            for (int i = 0; i < variants.Length; i++)
+            {
+                Assert.Equal(variants.IsNull(i), values.IsNull(i));
+                if (!variants.IsNull(i))
+                {
+                    Assert.Equal(variants.GetVariantValue(i), 
values.GetVariantValue(i));
+                }
+            }
+            Assert.Null(await stream.ReadNextRecordBatchAsync());
+        }
+
+        [SkippableFact]
+        public async Task ReadShreddedVariant()
+        {
+            using var database = 
_duckDb.OpenDatabase("readshreddedvariant.db");
+            using var connection = database.Connect(null);
+            using var statement = connection.CreateStatement();
+            RequireVariantSupport(statement);
+
+            statement.SqlQuery = "SET variant_minimum_shredding_size = 0";
+            statement.ExecuteUpdate();
+            statement.SqlQuery = "CREATE TABLE variants(id INTEGER, v 
VARIANT)";
+            statement.ExecuteUpdate();
+            statement.SqlQuery = """
+                INSERT INTO variants VALUES
+                    (1, {'name': 'first', 'value': 1, 'extra': [1, 
2]}::VARIANT),
+                    (2, {'name': 'second', 'value': 2}::VARIANT),
+                    (3, {'name': 'third', 'value': 3}::VARIANT),
+                    (4, 'residual'::VARIANT),
+                    (5, NULL::VARIANT)
+                """;
+            statement.ExecuteUpdate();
+            statement.SqlQuery = "CHECKPOINT";
+            statement.ExecuteUpdate();
+            Assert.Equal("SHREDDED_VECTOR", GetScalarString(statement, "SELECT 
DISTINCT vector_type(v) FROM variants"));
+
+            statement.SqlQuery = "SELECT v FROM variants ORDER BY id";
+            using var stream = statement.ExecuteQuery().Stream;
+            Assert.NotNull(stream);
+            Assert.Equal("arrow.parquet.variant", 
stream.Schema.FieldsList[0].Metadata["ARROW:extension:name"]);
+            using var batch = await stream.ReadNextRecordBatchAsync();
+            Assert.NotNull(batch);
+            var variants = new VariantArray(batch.Column(0));
+
+            // DuckDB exports internally shredded values as unshredded Arrow 
variants.
+            Assert.False(variants.IsShredded);
+            Assert.Equal(5, variants.Length);
+            Assert.Equal(new[] { "first", "second", "third" },
+                Enumerable.Range(0, 3).Select(i => 
variants.GetVariantValue(i).AsObject()["name"].AsString()));
+            Assert.Equal(new[] { 1, 2, 3 },
+                Enumerable.Range(0, 3).Select(i => 
variants.GetVariantValue(i).AsObject()["value"].AsInt32()));
+            Assert.Equal(new[] { 1, 2 },
+                
variants.GetVariantValue(0).AsObject()["extra"].AsArray().Select(value => 
value.AsInt32()));
+            Assert.Equal("residual", variants.GetVariantValue(3).AsString());
+            Assert.True(variants.IsNull(4));
+            Assert.Null(await stream.ReadNextRecordBatchAsync());
+        }
+
+        [SkippableTheory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void IngestShreddedVariantIsNotSupported(bool shredObject)
+        {
+            using var database = 
_duckDb.OpenDatabase($"ingestshreddedvariant{shredObject}.db");
+            using var connection = database.Connect(null);
+            using var statement = connection.CreateStatement();
+            RequireVariantSupport(statement);
+
+            ShredSchema shredSchema = shredObject
+                ? ShredSchema.ForObject(new Dictionary<string, ShredSchema>
+                {
+                    { "value", ShredSchema.Primitive(ShredType.Int32) },
+                })
+                : ShredSchema.Primitive(ShredType.Int32);
+            VariantValue typed = shredObject
+                ? VariantValue.FromObject(new Dictionary<string, VariantValue>
+                {
+                    { "value", VariantValue.FromInt32(42) },
+                    { "extra", VariantValue.FromString("unshredded field") },
+                })
+                : VariantValue.FromInt32(42);
+            VariantValue[] expected = [typed, 
VariantValue.FromString("residual"), VariantValue.Null];
+            var (metadata, rows) = VariantShredder.Shred(expected, 
shredSchema);
+            using var variants = 
ShreddedVariantArrayBuilder.Build(shredSchema, metadata, rows);
+            Assert.True(variants.IsShredded);
+            IArrowArray? typedValues = variants.TypedValueArray;
+            Assert.NotNull(typedValues);
+            Assert.False(typedValues.IsNull(0));
+            Assert.True(typedValues.IsNull(1));
+            Assert.False(variants.GetValueBytes(1).IsEmpty);
+            if (shredObject)
+            {
+                Assert.False(variants.GetValueBytes(0).IsEmpty);
+            }
+            for (int i = 0; i < expected.Length; i++)
+            {
+                Assert.Equal(expected[i], variants.GetLogicalVariantValue(i));
+            }
+
+            var schema = new Schema([new Field("v", variants.ExtensionType, 
true)], null);
+            using var batch = new RecordBatch(schema, [variants], 
variants.Length);
+            using var ingest = connection.BulkIngest("variants", 
BulkIngestMode.Create);
+            AdbcException exception = Assert.ThrowsAny<AdbcException>(() =>
+            {
+                ingest.Bind(batch, schema);
+                ingest.ExecuteUpdate();
+            });
+            _output.WriteLine($"Shredded ingestion: {exception.Status}: 
{exception.Message}");
+            Assert.Contains("typed_value", exception.Message);
+            Assert.Contains("not supported", exception.Message);
+        }
+
         [Fact]
         public void PrepareAndBind()
         {
@@ -684,6 +881,15 @@ namespace Apache.Arrow.Adbc.Tests
             Assert.True(query.IsCompleted, "The query was not cancelled");
         }
 
+        private void RequireVariantSupport(AdbcStatement statement)
+        {
+            string? version = GetScalarString(statement, "SELECT version()");
+            Assert.NotNull(version);
+            _output.WriteLine($"DuckDB version: {version}");
+            Skip.If(Version.Parse(version.TrimStart('v').Split('-')[0]).Major 
< 2,
+                "Reading and ingesting arrow.parquet.variant requires DuckDB 
2.0 or later.");
+        }
+
         private static string? GetScalarString(AdbcStatement statement, string 
query)
         {
             statement.SqlQuery = query;

Reply via email to