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

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


The following commit(s) were added to refs/heads/main by this push:
     new 3799e71  GH-610: Preserve dictionary replacements when reading tables 
(#611)
3799e71 is described below

commit 3799e717cac12e6891d55d41fb004cf4167de216
Author: Jacob Quinn <[email protected]>
AuthorDate: Tue Oct 27 10:50:22 2026 -0600

    GH-610: Preserve dictionary replacements when reading tables (#611)
    
    Addresses #610. A dictionary replacement could overtake queued
    record-batch tasks, causing earlier rows to use later dictionary values.
    Wait for outstanding batch construction before each dictionary update,
    outside the dictionary lock, and clear completed tasks. Use the same
    chained dictionary storage as Arrow.Stream so later deltas keep column
    types stable.
    
    Dictionary updates become synchronization points; consecutive record
    batches with unchanged dictionaries still run in parallel. Dictionary
    payloads are not copied. This fixes the current 2.8 reader; the separate
    3.0 rewrite in #609 already handles replacement through its own decoder.
    
    Validation: the new regression failed six checks on unchanged main and
    passes all 12 after the fix on one and four threads. It covers
    replacement followed by delta, compressed input, and multiple input
    buffers. Frozen PyArrow V4/V5 fixtures also return correct values. A
    channel-gated probe verifies four unchanged-dictionary batch tasks start
    before any completes. Full suites passed 66,207 assertions each on Julia
    1.9.4 and 1.12.6 using CI's local ArrowTypes setup. Formatting checks
    passed.
    
    Co-authored by Codex
---
 src/table.jl     | 21 ++++++---------------
 test/runtests.jl | 24 ++++++++++++++++++++++++
 2 files changed, 30 insertions(+), 15 deletions(-)

diff --git a/src/table.jl b/src/table.jl
index de8bfc3..606f2ab 100644
--- a/src/table.jl
+++ b/src/table.jl
@@ -502,6 +502,9 @@ function Table(blobs::Vector{ArrowBlob}; convert::Bool=true)
                     )
                 end
             elseif header isa Meta.DictionaryBatch
+                # Earlier record batches must capture their dictionaries 
before an update.
+                _waitall(tasks)
+                empty!(tasks)
                 id = header.id
                 recordbatch = header.data
                 @debug "parsing dictionary batch message: id = $id, 
compression = $(recordbatch.compression)"
@@ -522,20 +525,7 @@ function Table(blobs::Vector{ArrowBlob}; 
convert::Bool=true)
                             convert,
                         )
                         dictencoding = dictencodings[id]
-                        if typeof(dictencoding.data) <: ChainedVector
-                            append!(dictencoding.data, values)
-                        else
-                            A = ChainedVector([dictencoding.data, values])
-                            S =
-                                field.dictionary.indexType === nothing ? Int32 
:
-                                juliaeltype(field, field.dictionary.indexType, 
false)
-                            dictencodings[id] = 
DictEncoding{eltype(A),S,typeof(A)}(
-                                id,
-                                A,
-                                field.dictionary.isOrdered,
-                                values.metadata,
-                            )
-                        end
+                        append!(dictencoding.data, values)
                         continue
                     end
                     # new dictencoding or replace
@@ -551,7 +541,8 @@ function Table(blobs::Vector{ArrowBlob}; convert::Bool=true)
                         Int64(1),
                         convert,
                     )
-                    A = values
+                    # Keep the dictionary storage type stable when later 
deltas arrive.
+                    A = ChainedVector([values])
                     S =
                         field.dictionary.indexType === nothing ? Int32 :
                         juliaeltype(field, field.dictionary.indexType, false)
diff --git a/test/runtests.jl b/test/runtests.jl
index 315d1b6..4f7d28b 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -149,6 +149,30 @@ end
             @test isequal(collect(str)[2].col1, [missing, 11])
         end
 
+        @testset "# 610: dictionary replacement between record batches" begin
+            for compress in (nothing, :lz4, :zstd)
+                first = (a=Arrow.DictEncode(["alpha", "beta"]),)
+                second = (a=Arrow.DictEncode(["gamma", "delta"]),)
+                third = (a=Arrow.DictEncode(["gamma", "epsilon"]),)
+                b1 = take!(Arrow.tobuffer(Tables.partitioner((first, first)); 
compress))
+                b2 = take!(Arrow.tobuffer(Tables.partitioner((second, third)); 
compress))
+
+                # Replace the first stream's EOS with the second stream after 
its schema.
+                _, (pos, _) = iterate(Arrow.BatchIterator(Arrow.ArrowBlob(b2, 
1, nothing)))
+                bytes = vcat(b1[1:(end - 8)], b2[pos:end])
+                batches = Arrow.BatchIterator(Arrow.ArrowBlob(bytes, 1, 
nothing))
+                @test count(batches) do batch
+                    header = batch.msg.header
+                    header isa Arrow.Meta.DictionaryBatch && header.isDelta
+                end == 1
+
+                expected = [first.a.data, first.a.data, second.a.data, 
third.a.data]
+                @test Arrow.Table(bytes).a == vcat(expected...)
+                @test Arrow.Table([b1, b2]).a == vcat(expected...)
+                @test [collect(batch.a) for batch in Arrow.Stream(bytes)] == 
expected
+            end
+        end
+
         @testset "# dictionary batch isDelta" begin
             t = (
                 col1=Int64[1, 2, 3, 4],

Reply via email to