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

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


The following commit(s) were added to refs/heads/main by this push:
     new f5b0c6ba feat(compute): add dictionary_encode (#1140)
f5b0c6ba is described below

commit f5b0c6ba5ac20b744cb53bedc6e1c4374e412612
Author: Minh Vu <[email protected]>
AuthorDate: Mon Aug 17 22:23:08 2026 +0200

    feat(compute): add dictionary_encode (#1140)
    
    ### Rationale for this change
    
    Arrow-Go currently has a vector hash kernel for unique, but not for
    dictionary encoding. The existing memo table already assigns stable
    positions to distinct values, so dictionary encoding can build on that
    state and provide a useful output form for repeated values.
    
    ### What changes are included in this PR?
    
    - Add the dictionary_encode compute function and
    DictionaryEncodeOptions.
    - Support primitive, binary, temporal, decimal, interval, fixed-size
    binary, and boolean input types covered by the existing vector hash
    kernels.
    - Use int32 dictionary indexes and preserve first-seen dictionary order.
    - Mask nulls by default. With NullEncodingEncode, null is included in
    the dictionary and encoded as a regular index.
    - Keep one shared dictionary across chunked input while returning one
    index per input value.
    - Pass through already dictionary-encoded input instead of creating a
    nested dictionary type.
    - Register boolean inputs in the shared vector hash kernels, which also
    adds `unique(bool)` support while preserving its existing
    null-as-a-distinct-value behavior.
    - Correct the `unique` documentation to state that null is treated as a
    distinct value.
    
    ### Are these changes tested?
    
    - go test ./arrow/compute/... -count=1
    - Added coverage for repeated and all-null values in both null modes,
    chunked input, dictionary input, dictionary order, output cardinality,
    boolean unique and dictionary encoding, sliced boolean input, and
    memo-table resizing.
    
    ### Are there any user-facing changes?
    
    Yes. This adds the public compute.DictionaryEncode function,
    DictionaryEncodeOptions, and the NullEncodingMask and NullEncodingEncode
    options. It also adds boolean input support to unique through the shared
    vector hash registration.
---
 arrow/array/util.go                                |  15 +-
 arrow/compute/executor.go                          |  29 +-
 arrow/compute/executor_test.go                     | 116 ++++
 arrow/compute/expression.go                        |   9 +-
 arrow/compute/expression_test.go                   |  53 ++
 arrow/compute/internal/kernels/helpers.go          |  10 +-
 arrow/compute/internal/kernels/vector_hash.go      | 378 ++++++++++-
 arrow/compute/internal/kernels/vector_hash_test.go |  78 +++
 arrow/compute/vector_hash.go                       |  70 +-
 arrow/compute/vector_hash_test.go                  | 749 +++++++++++++++++++++
 10 files changed, 1482 insertions(+), 25 deletions(-)

diff --git a/arrow/array/util.go b/arrow/array/util.go
index 1b303bf6..afd0f39b 100644
--- a/arrow/array/util.go
+++ b/arrow/array/util.go
@@ -327,9 +327,18 @@ func GetDictArrayData(mem memory.Allocator, valueType 
arrow.DataType, memoTable
 
        switch tbl := memoTable.(type) {
        case hashing.NumericMemoTable:
-               nbytes := tbl.TypeTraits().BytesRequired(dictLen)
-               buffers[1].Resize(nbytes)
-               tbl.WriteOutSubset(startOffset, buffers[1].Bytes())
+               if valueType.ID() == arrow.BOOL {
+                       values := make([]uint8, dictLen)
+                       tbl.CopyValuesSubset(startOffset, values)
+                       
buffers[1].Resize(int(bitutil.BytesForBits(int64(dictLen))))
+                       for i, value := range values {
+                               bitutil.SetBitTo(buffers[1].Bytes(), i, value 
!= 0)
+                       }
+               } else {
+                       nbytes := tbl.TypeTraits().BytesRequired(dictLen)
+                       buffers[1].Resize(nbytes)
+                       tbl.WriteOutSubset(startOffset, buffers[1].Bytes())
+               }
        case *hashing.BinaryMemoTable:
                switch valueType.ID() {
                case arrow.BINARY, arrow.STRING:
diff --git a/arrow/compute/executor.go b/arrow/compute/executor.go
index 095a7e15..1728e49e 100644
--- a/arrow/compute/executor.go
+++ b/arrow/compute/executor.go
@@ -1002,18 +1002,30 @@ func (v *vectorExecutor) WrapResults(ctx 
context.Context, out <-chan Datum, hasC
                output Datum
                acc    []arrow.Array
        )
+       releaseAccumulated := func() {
+               for _, c := range acc {
+                       c.Release()
+               }
+               acc = nil
+       }
 
        toChunked := func() {
                out := output.(ArrayLikeDatum).Chunks()
                acc = make([]arrow.Array, 0, len(out))
+               isChunked := output.Kind() == KindChunked
                for _, o := range out {
                        if o.Len() > 0 {
+                               if isChunked {
+                                       // ChunkedDatum.Chunks returns borrowed 
references.
+                                       o.Retain()
+                               }
                                acc = append(acc, o)
+                       } else if !isChunked {
+                               // ArrayDatum.Chunks creates an owned array.
+                               o.Release()
                        }
                }
-               if output.Kind() != KindChunked {
-                       output.Release()
-               }
+               output.Release()
                output = nil
        }
 
@@ -1023,6 +1035,9 @@ func (v *vectorExecutor) WrapResults(ctx context.Context, 
out <-chan Datum, hasC
                return nil
        case output = <-out:
                if output == nil || ctx.Err() != nil {
+                       if output != nil {
+                               output.Release()
+                       }
                        return nil
                }
 
@@ -1038,6 +1053,10 @@ func (v *vectorExecutor) WrapResults(ctx 
context.Context, out <-chan Datum, hasC
                case <-ctx.Done():
                        // context is done, either cancelled or a timeout.
                        // either way, we end early and return what we've got 
so far.
+                       if output == nil {
+                               releaseAccumulated()
+                               return nil
+                       }
                        return output
                case o, ok := <-out:
                        if !ok { // channel closed, wrap it up
@@ -1045,9 +1064,7 @@ func (v *vectorExecutor) WrapResults(ctx context.Context, 
out <-chan Datum, hasC
                                        return output
                                }
 
-                               for _, c := range acc {
-                                       defer c.Release()
-                               }
+                               defer releaseAccumulated()
 
                                chkd := arrow.NewChunked(v.outType, acc)
                                defer chkd.Release()
diff --git a/arrow/compute/executor_test.go b/arrow/compute/executor_test.go
new file mode 100644
index 00000000..a8194108
--- /dev/null
+++ b/arrow/compute/executor_test.go
@@ -0,0 +1,116 @@
+// 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.
+
+//go:build go1.18
+
+package compute
+
+import (
+       "context"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/compute/exec"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/stretchr/testify/require"
+)
+
+type signalChunkedDatum struct {
+       *ChunkedDatum
+       chunksCalled chan struct{}
+}
+
+func (d *signalChunkedDatum) Chunks() []arrow.Array {
+       close(d.chunksCalled)
+       return d.Value.Chunks()
+}
+
+func TestVectorExecutorWrapResultsReleasesEmptyChunkedOutput(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       builder := array.NewInt32Builder(mem)
+       builder.Append(42)
+       value := builder.NewInt32Array()
+       builder.Release()
+       defer value.Release()
+
+       empty := array.NewSlice(value, 0, 0)
+       defer empty.Release()
+       nonEmpty := array.NewSlice(value, 0, 1)
+       defer nonEmpty.Release()
+
+       chunked := arrow.NewChunked(value.DataType(), []arrow.Array{empty, 
nonEmpty})
+       output := make(chan Datum, 1)
+       output <- &ChunkedDatum{Value: chunked}
+       close(output)
+
+       executor := &vectorExecutor{
+               nonAggExecImpl: nonAggExecImpl{
+                       kernel:  &exec.VectorKernel{OutputChunked: true},
+                       outType: value.DataType(),
+               },
+       }
+
+       result := executor.WrapResults(context.Background(), output, true)
+       require.NotNil(t, result)
+       require.Equal(t, KindChunked, result.Kind())
+
+       resultChunked := result.(*ChunkedDatum).Value
+       require.Len(t, resultChunked.Chunks(), 1)
+       require.Equal(t, int32(42), 
resultChunked.Chunk(0).(*array.Int32).Value(0))
+       result.Release()
+}
+
+func TestVectorExecutorWrapResultsReleasesChunkedOutputOnCancellation(t 
*testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       builder := array.NewInt32Builder(mem)
+       builder.Append(42)
+       value := builder.NewInt32Array()
+       builder.Release()
+       defer value.Release()
+
+       chunked := arrow.NewChunked(value.DataType(), []arrow.Array{value})
+       output := make(chan Datum)
+       ctx, cancel := context.WithCancel(context.Background())
+       defer cancel()
+
+       executor := &vectorExecutor{
+               nonAggExecImpl: nonAggExecImpl{
+                       kernel:  &exec.VectorKernel{OutputChunked: true},
+                       outType: value.DataType(),
+               },
+       }
+
+       datum := &signalChunkedDatum{
+               ChunkedDatum: &ChunkedDatum{Value: chunked},
+               chunksCalled: make(chan struct{}),
+       }
+       result := make(chan Datum, 1)
+       go func() {
+               result <- executor.WrapResults(ctx, output, true)
+       }()
+
+       output <- datum
+       <-datum.chunksCalled
+       cancel()
+
+       require.Nil(t, <-result)
+       close(output)
+}
diff --git a/arrow/compute/expression.go b/arrow/compute/expression.go
index dcf572e7..a1927366 100644
--- a/arrow/compute/expression.go
+++ b/arrow/compute/expression.go
@@ -578,6 +578,7 @@ var (
        funcOptsTypes  = []FunctionOptions{
                SetLookupOptions{}, ArithmeticOptions{}, CastOptions{},
                FilterOptions{}, NullOptions{}, StrptimeOptions{}, 
MakeStructOptions{},
+               DictionaryEncodeOptions{},
                CumulativeOptions{},
        }
 )
@@ -954,7 +955,13 @@ func DeserializeExpr(mem memory.Allocator, buf 
*memory.Buffer) (Expression, erro
                                                        return nil, 
errors.New("options scalar typename must be binary")
                                                }
 
-                                               optionsVal := 
reflect.New(funcOptionsMap[string(typname.(*scalar.Binary).Data())]).Interface()
+                                               typeName := 
string(typname.(*scalar.Binary).Data())
+                                               optionsType, ok := 
funcOptionsMap[typeName]
+                                               if !ok {
+                                                       return nil, 
fmt.Errorf("%w: unknown function options type %q", arrow.ErrInvalid, typeName)
+                                               }
+
+                                               optionsVal := 
reflect.New(optionsType).Interface()
                                                if err := 
scalar.FromScalarWithAllocator(optsScalar.(*scalar.Struct), optionsVal, mem); 
err != nil {
                                                        return nil, err
                                                }
diff --git a/arrow/compute/expression_test.go b/arrow/compute/expression_test.go
index 37c58ad6..a28c795a 100644
--- a/arrow/compute/expression_test.go
+++ b/arrow/compute/expression_test.go
@@ -20,6 +20,7 @@
 package compute_test
 
 import (
+       "fmt"
        "testing"
 
        "github.com/apache/arrow-go/v18/arrow"
@@ -28,8 +29,13 @@ import (
        "github.com/apache/arrow-go/v18/arrow/memory"
        "github.com/apache/arrow-go/v18/arrow/scalar"
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
 )
 
+type unknownFunctionOptions struct{}
+
+func (unknownFunctionOptions) TypeName() string { return 
"UnknownFunctionOptions" }
+
 type privateFunctionOptions struct {
        value int
 }
@@ -414,3 +420,50 @@ func TestExpressionSerializationRoundTrip(t *testing.T) {
                })
        }
 }
+
+func TestDictionaryEncodeOptionsSerializationRoundTrip(t *testing.T) {
+       for _, behavior := range []compute.NullEncodingBehavior{
+               compute.NullEncodingMask,
+               compute.NullEncodingEncode,
+       } {
+               t.Run(fmt.Sprintf("null encoding %d", behavior), func(t 
*testing.T) {
+                       mem := 
memory.NewCheckedAllocator(memory.NewGoAllocator())
+                       defer mem.AssertSize(t, 0)
+
+                       expr := compute.NewCall(
+                               "dictionary_encode",
+                               
[]compute.Expression{compute.NewFieldRef("values")},
+                               &compute.DictionaryEncodeOptions{NullEncoding: 
behavior},
+                       )
+                       defer expr.Release()
+
+                       serialized, err := compute.SerializeExpr(expr, mem)
+                       require.NoError(t, err)
+                       defer serialized.Release()
+
+                       roundTripped, err := compute.DeserializeExpr(mem, 
serialized)
+                       require.NoError(t, err)
+                       defer roundTripped.Release()
+                       require.True(t, expr.Equals(roundTripped))
+               })
+       }
+}
+
+func TestDeserializeExprRejectsUnknownOptions(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       expr := compute.NewCall(
+               "dictionary_encode",
+               []compute.Expression{compute.NewFieldRef("values")},
+               unknownFunctionOptions{},
+       )
+       defer expr.Release()
+
+       serialized, err := compute.SerializeExpr(expr, mem)
+       require.NoError(t, err)
+       defer serialized.Release()
+
+       _, err = compute.DeserializeExpr(mem, serialized)
+       assert.ErrorIs(t, err, arrow.ErrInvalid)
+}
diff --git a/arrow/compute/internal/kernels/helpers.go 
b/arrow/compute/internal/kernels/helpers.go
index ef5f0bb4..5daa31a1 100644
--- a/arrow/compute/internal/kernels/helpers.go
+++ b/arrow/compute/internal/kernels/helpers.go
@@ -851,6 +851,10 @@ func (v *validityBuilder) Finish() (buf *memory.Buffer) {
        return
 }
 
+func (v *validityBuilder) reset() {
+       v.bitLength, v.falseCount = 0, 0
+}
+
 type execBufBuilder struct {
        mem    memory.Allocator
        buffer *memory.Buffer
@@ -876,6 +880,10 @@ func (bldr *execBufBuilder) unsafeAppend(data []byte) {
        bldr.sz += len(data)
 }
 
+func (bldr *execBufBuilder) reset() {
+       bldr.sz = 0
+}
+
 func (bldr *execBufBuilder) finish() (buf *memory.Buffer) {
        if bldr.buffer == nil {
                buf = memory.NewBufferBytes(nil)
@@ -883,7 +891,7 @@ func (bldr *execBufBuilder) finish() (buf *memory.Buffer) {
        }
        bldr.buffer.Resize(bldr.sz)
        buf = bldr.buffer
-       bldr.buffer, bldr.sz = nil, 0
+       bldr.buffer, bldr.data, bldr.sz = nil, nil, 0
        return
 }
 
diff --git a/arrow/compute/internal/kernels/vector_hash.go 
b/arrow/compute/internal/kernels/vector_hash.go
index ca6184eb..00f48173 100644
--- a/arrow/compute/internal/kernels/vector_hash.go
+++ b/arrow/compute/internal/kernels/vector_hash.go
@@ -23,9 +23,11 @@ import (
 
        "github.com/apache/arrow-go/v18/arrow"
        "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/bitutil"
        "github.com/apache/arrow-go/v18/arrow/compute/exec"
        "github.com/apache/arrow-go/v18/arrow/internal/debug"
        "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/arrow/scalar"
        "github.com/apache/arrow-go/v18/internal/bitutils"
        "github.com/apache/arrow-go/v18/internal/hashing"
 )
@@ -78,11 +80,172 @@ func (emptyAction) ShouldEncodeNulls() bool           { 
return true }
 
 type uniqueAction = emptyAction
 
+type NullEncodingBehavior int8
+
+const (
+       // NullEncodingMask keeps null input values null in the indices array.
+       // It is the zero value and default behavior.
+       NullEncodingMask NullEncodingBehavior = iota
+       // NullEncodingEncode adds null input values to the dictionary as a 
regular entry.
+       NullEncodingEncode
+)
+
+type DictionaryEncodeOptions struct {
+       // NullEncoding controls how null input values are represented.
+       NullEncoding NullEncodingBehavior `compute:"null_encoding_behavior"`
+}
+
+func (DictionaryEncodeOptions) TypeName() string { return 
"DictionaryEncodeOptions" }
+
+func (opts DictionaryEncodeOptions) ToScalar() (scalar.Scalar, error) {
+       var encoded uint32
+       switch opts.NullEncoding {
+       case NullEncodingEncode:
+               encoded = 0
+       case NullEncodingMask:
+               encoded = 1
+       default:
+               return nil, fmt.Errorf("%w: invalid null encoding behavior %d", 
arrow.ErrInvalid, opts.NullEncoding)
+       }
+
+       return scalar.NewStructScalarWithNames(
+               []scalar.Scalar{
+                       scalar.NewUint32Scalar(encoded),
+                       
scalar.NewBinaryScalar(memory.NewBufferBytes([]byte(opts.TypeName())), 
arrow.BinaryTypes.Binary),
+               },
+               []string{"null_encoding_behavior", "_type_name"},
+       )
+}
+
+func (opts *DictionaryEncodeOptions) FromStructScalar(sc *scalar.Struct) error 
{
+       value, err := sc.Field("null_encoding_behavior")
+       if err != nil {
+               return err
+       }
+
+       encoded, ok := value.(*scalar.Uint32)
+       if !ok || !encoded.IsValid() {
+               return fmt.Errorf("%w: null_encoding_behavior must be a valid 
uint32 scalar", arrow.ErrInvalid)
+       }
+
+       var behavior NullEncodingBehavior
+       switch encoded.Value {
+       case 0:
+               behavior = NullEncodingEncode
+       case 1:
+               behavior = NullEncodingMask
+       default:
+               return fmt.Errorf("%w: invalid null encoding behavior %d", 
arrow.ErrInvalid, encoded.Value)
+       }
+
+       opts.NullEncoding = behavior
+       return nil
+}
+
+type dictionaryEncodeAction struct {
+       nullEncoding NullEncodingBehavior
+       indices      *bufferBuilder[int32]
+       validity     validityBuilder
+       length       int
+       nulls        int
+       err          error
+}
+
+func (a *dictionaryEncodeAction) Reset() error {
+       a.indices.reset()
+       a.validity.reset()
+       a.length = 0
+       a.nulls = 0
+       a.err = nil
+       return nil
+}
+
+func (a *dictionaryEncodeAction) Reserve(n int) error {
+       a.indices.reserve(n)
+       a.validity.Reserve(int64(n))
+       return nil
+}
+
+func (a *dictionaryEncodeAction) appendIndex(idx int, valid bool) {
+       if !valid {
+               idx = 0
+       }
+       if idx < 0 || int64(idx) > int64(1<<31-1) {
+               if a.err == nil {
+                       a.err = fmt.Errorf("%w: dictionary index %d does not 
fit in int32", arrow.ErrInvalid, idx)
+               }
+               return
+       }
+
+       a.indices.unsafeAppend(int32(idx))
+       a.validity.UnsafeAppend(valid)
+       a.length++
+       if !valid {
+               a.nulls++
+       }
+}
+
+func (a *dictionaryEncodeAction) Flush(out *exec.ExecResult) error {
+       if a.err != nil {
+               return a.err
+       }
+
+       out.Len = int64(a.length)
+       out.Nulls = int64(a.nulls)
+       if a.length != 0 {
+               out.Buffers[1].WrapBuffer(a.indices.finish())
+       } else {
+               a.indices.finish().Release()
+       }
+
+       validity := a.validity.Finish()
+       if a.nulls != 0 {
+               out.Buffers[0].WrapBuffer(validity)
+       } else if validity != nil {
+               validity.Release()
+       }
+
+       a.length = 0
+       a.nulls = 0
+       return nil
+}
+
+func (a *dictionaryEncodeAction) FlushFinal(*exec.ExecResult) error {
+       return nil
+}
+
+func (a *dictionaryEncodeAction) ObserveFound(idx int) {
+       a.appendIndex(idx, true)
+}
+
+func (a *dictionaryEncodeAction) ObserveNotFound(idx int) error {
+       a.appendIndex(idx, true)
+       return a.err
+}
+
+func (a *dictionaryEncodeAction) ObserveNullFound(idx int) {
+       if a.nullEncoding == NullEncodingMask {
+               a.appendIndex(0, false)
+       } else {
+               a.appendIndex(idx, true)
+       }
+}
+
+func (a *dictionaryEncodeAction) ObserveNullNotFound(idx int) error {
+       a.ObserveNullFound(idx)
+       return a.err
+}
+
+func (a *dictionaryEncodeAction) ShouldEncodeNulls() bool {
+       return a.nullEncoding == NullEncodingEncode
+}
+
 type regularHashState struct {
-       mem       memory.Allocator
-       typ       arrow.DataType
-       memoTable hashing.MemoTable
-       action    Action
+       mem          memory.Allocator
+       typ          arrow.DataType
+       memoTable    hashing.MemoTable
+       action       Action
+       memoReleased bool
 
        doAppend func(Action, hashing.MemoTable, *exec.ArraySpan) error
 }
@@ -92,7 +255,16 @@ func (rhs *regularHashState) Allocator() memory.Allocator { 
return rhs.mem }
 func (rhs *regularHashState) ValueType() arrow.DataType { return rhs.typ }
 
 func (rhs *regularHashState) Reset() error {
-       rhs.memoTable.Reset()
+       if rhs.memoReleased {
+               memoTable, err := newMemoTable(rhs.mem, rhs.typ.ID())
+               if err != nil {
+                       return err
+               }
+               rhs.memoTable = memoTable
+               rhs.memoReleased = false
+       } else {
+               rhs.memoTable.Reset()
+       }
        return rhs.action.Reset()
 }
 
@@ -114,6 +286,10 @@ func (rhs *regularHashState) GetDictionary() 
(arrow.ArrayData, error) {
 }
 
 func doAppendBinary[OffsetT int32 | int64](action Action, memo 
hashing.MemoTable, arr *exec.ArraySpan) error {
+       if arr.Len == 0 {
+               return nil
+       }
+
        var (
                bitmap            = arr.Buffers[0].Buf
                offsets           = exec.GetSpanOffsets[OffsetT](arr, 1)
@@ -142,6 +318,7 @@ func doAppendBinary[OffsetT int32 | int64](action Action, 
memo hashing.MemoTable
                        idx, found := memo.GetOrInsertNull()
                        if found {
                                action.ObserveNullFound(idx)
+                               return nil
                        }
                        return action.ObserveNullNotFound(idx)
                })
@@ -173,6 +350,7 @@ func doAppendFixedSize(action Action, memo 
hashing.MemoTable, arr *exec.ArraySpa
                        idx, found := memo.GetOrInsertNull()
                        if found {
                                action.ObserveNullFound(idx)
+                               return nil
                        }
                        return action.ObserveNullNotFound(idx)
                })
@@ -200,6 +378,43 @@ func doAppendNumeric[T arrow.IntType | arrow.UintType | 
arrow.FloatType](action
                        idx, found := memo.GetOrInsertNull()
                        if found {
                                action.ObserveNullFound(idx)
+                               return nil
+                       }
+                       return action.ObserveNullNotFound(idx)
+               })
+}
+
+func doAppendBoolean(action Action, memo hashing.MemoTable, arr 
*exec.ArraySpan) error {
+       if arr.Len == 0 {
+               return nil
+       }
+
+       values := arr.Buffers[1].Buf
+       shouldEncodeNulls := action.ShouldEncodeNulls()
+       return bitutils.VisitBitBlocksShort(arr.Buffers[0].Buf, arr.Offset, 
arr.Len,
+               func(pos int64) error {
+                       value := uint8(0)
+                       if bitutil.BitIsSet(values, int(arr.Offset+pos)) {
+                               value = 1
+                       }
+                       idx, found, err := memo.GetOrInsert(value)
+                       if err != nil {
+                               return err
+                       }
+                       if found {
+                               action.ObserveFound(idx)
+                               return nil
+                       }
+                       return action.ObserveNotFound(idx)
+               }, func() error {
+                       if !shouldEncodeNulls {
+                               return action.ObserveNullNotFound(-1)
+                       }
+
+                       idx, found := memo.GetOrInsertNull()
+                       if found {
+                               action.ObserveNullFound(idx)
+                               return nil
                        }
                        return action.ObserveNullNotFound(idx)
                })
@@ -217,6 +432,7 @@ func (nhs *nullHashState) Allocator() memory.Allocator { 
return nhs.mem }
 func (nhs *nullHashState) ValueType() arrow.DataType { return nhs.typ }
 
 func (nhs *nullHashState) Reset() error {
+       nhs.seenNull = false
        return nhs.action.Reset()
 }
 
@@ -254,6 +470,39 @@ func (nhs *nullHashState) GetDictionary() 
(arrow.ArrayData, error) {
        return data, nil
 }
 
+func dictionaryEncodeIdentity(_ *exec.KernelCtx, batch *exec.ExecSpan, out 
*exec.ExecResult) error {
+       data := batch.Values[0].Array.MakeData()
+       defer data.Release()
+       out.TakeOwnership(data)
+       return nil
+}
+
+func dictionaryEncodeIdentityChunked(_ *exec.KernelCtx, batch 
[]*arrow.Chunked, _ *exec.ExecResult) ([]*exec.ExecResult, error) {
+       if len(batch) != 1 {
+               return nil, fmt.Errorf("%w: dictionary_encode expects one 
input", arrow.ErrInvalid)
+       }
+
+       chunks := batch[0].Chunks()
+       if len(chunks) == 0 {
+               result := &exec.ExecResult{}
+               exec.FillZeroLength(batch[0].DataType(), result)
+               return []*exec.ExecResult{result}, nil
+       }
+
+       results := make([]*exec.ExecResult, 0, len(chunks))
+       for _, chunk := range chunks {
+               result := &exec.ExecResult{}
+               result.TakeOwnership(chunk.Data())
+               results = append(results, result)
+       }
+       return results, nil
+}
+
+func initDictionaryEncodeIdentity(_ *exec.KernelCtx, args exec.KernelInitArgs) 
(exec.KernelState, error) {
+       _, err := parseDictionaryEncodeOptions(args.Options)
+       return nil, err
+}
+
 type dictionaryHashState struct {
        indicesKernel HashState
        dictionary    arrow.Array
@@ -329,10 +578,14 @@ func (dhs *dictionaryHashState) Append(ctx 
*exec.KernelCtx, arr *exec.ArraySpan)
 func nullHashInit(actionInit initAction) exec.KernelInitFn {
        return func(ctx *exec.KernelCtx, args exec.KernelInitArgs) 
(exec.KernelState, error) {
                mem := exec.GetAllocator(ctx.Ctx)
+               action, err := actionInit(args.Inputs[0], args.Options, mem)
+               if err != nil {
+                       return nil, err
+               }
                ret := &nullHashState{
                        mem:    mem,
                        typ:    args.Inputs[0],
-                       action: actionInit(args.Inputs[0], args.Options, mem),
+                       action: action,
                }
                ret.Reset()
                return ret, nil
@@ -341,7 +594,7 @@ func nullHashInit(actionInit initAction) exec.KernelInitFn {
 
 func newMemoTable(mem memory.Allocator, dt arrow.Type) (hashing.MemoTable, 
error) {
        switch dt {
-       case arrow.INT8, arrow.UINT8:
+       case arrow.BOOL, arrow.INT8, arrow.UINT8:
                return hashing.NewMemoTable[uint8](0), nil
        case arrow.INT16, arrow.UINT16:
                return hashing.NewMemoTable[uint16](0), nil
@@ -367,6 +620,10 @@ func newMemoTable(mem memory.Allocator, dt arrow.Type) 
(hashing.MemoTable, error
 func regularHashInit(dt arrow.DataType, actionInit initAction, appendFn 
func(Action, hashing.MemoTable, *exec.ArraySpan) error) exec.KernelInitFn {
        return func(ctx *exec.KernelCtx, args exec.KernelInitArgs) 
(exec.KernelState, error) {
                mem := exec.GetAllocator(ctx.Ctx)
+               action, err := actionInit(args.Inputs[0], args.Options, mem)
+               if err != nil {
+                       return nil, err
+               }
                memoTable, err := newMemoTable(mem, dt.ID())
                if err != nil {
                        return nil, err
@@ -376,7 +633,7 @@ func regularHashInit(dt arrow.DataType, actionInit 
initAction, appendFn func(Act
                        mem:       mem,
                        typ:       args.Inputs[0],
                        memoTable: memoTable,
-                       action:    actionInit(args.Inputs[0], args.Options, 
mem),
+                       action:    action,
                        doAppend:  appendFn,
                }
                ret.Reset()
@@ -415,12 +672,14 @@ func dictionaryHashInit(actionInit initAction) 
exec.KernelInitFn {
        }
 }
 
-type initAction func(arrow.DataType, any, memory.Allocator) Action
+type initAction func(arrow.DataType, any, memory.Allocator) (Action, error)
 
 func getHashInit(typeID arrow.Type, actionInit initAction) exec.KernelInitFn {
        switch typeID {
        case arrow.NULL:
                return nullHashInit(actionInit)
+       case arrow.BOOL:
+               return regularHashInit(arrow.FixedWidthTypes.Boolean, 
actionInit, doAppendBoolean)
        case arrow.INT8, arrow.UINT8:
                return regularHashInit(arrow.PrimitiveTypes.Uint8, actionInit, 
doAppendNumeric[uint8])
        case arrow.INT16, arrow.UINT16:
@@ -514,6 +773,13 @@ func uniqueFinalizeDictionary(ctx *exec.KernelCtx, result 
[]*exec.ArraySpan) (ou
 
 func addHashKernels(base exec.VectorKernel, actionInit initAction, outTy 
exec.OutputType) []exec.VectorKernel {
        kernels := make([]exec.VectorKernel, 0)
+       base.Init = getHashInit(arrow.BOOL, actionInit)
+       base.Signature = &exec.KernelSignature{
+               InputTypes: 
[]exec.InputType{exec.NewExactInput(arrow.FixedWidthTypes.Boolean)},
+               OutType:    outTy,
+       }
+       kernels = append(kernels, base)
+
        for _, ty := range primitiveTypes {
                base.Init = getHashInit(ty.ID(), actionInit)
                base.Signature = &exec.KernelSignature{
@@ -538,8 +804,81 @@ func addHashKernels(base exec.VectorKernel, actionInit 
initAction, outTy exec.Ou
        return kernels
 }
 
-func initUnique(dt arrow.DataType, _ any, mem memory.Allocator) Action {
-       return uniqueAction{mem: mem, dt: dt}
+func initUnique(dt arrow.DataType, _ any, mem memory.Allocator) (Action, 
error) {
+       return uniqueAction{mem: mem, dt: dt}, nil
+}
+
+func parseDictionaryEncodeOptions(options any) (DictionaryEncodeOptions, 
error) {
+       opts := DictionaryEncodeOptions{}
+       switch v := options.(type) {
+       case nil:
+       case DictionaryEncodeOptions:
+               opts = v
+       case *DictionaryEncodeOptions:
+               if v != nil {
+                       opts = *v
+               }
+       default:
+               return opts, fmt.Errorf("%w: expected DictionaryEncodeOptions, 
got %T", arrow.ErrInvalid, options)
+       }
+
+       if opts.NullEncoding != NullEncodingMask && opts.NullEncoding != 
NullEncodingEncode {
+               return opts, fmt.Errorf("%w: invalid null encoding behavior 
%d", arrow.ErrInvalid, opts.NullEncoding)
+       }
+       return opts, nil
+}
+
+func initDictionaryEncode(_ arrow.DataType, options any, mem memory.Allocator) 
(Action, error) {
+       opts, err := parseDictionaryEncodeOptions(options)
+       if err != nil {
+               return nil, err
+       }
+
+       return &dictionaryEncodeAction{
+               nullEncoding: opts.NullEncoding,
+               indices:      newBufferBuilder[int32](mem),
+               validity:     validityBuilder{mem: mem},
+       }, nil
+}
+
+var outputDictionaryType = exec.NewComputedOutputType(func(_ *exec.KernelCtx, 
args []arrow.DataType) (arrow.DataType, error) {
+       if len(args) != 1 {
+               return nil, fmt.Errorf("%w: dictionary_encode expects one input 
type", arrow.ErrInvalid)
+       }
+       return &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int32,
+               ValueType: args[0],
+       }, nil
+})
+
+func dictionaryEncodeFinalize(ctx *exec.KernelCtx, results []*exec.ArraySpan) 
([]*exec.ArraySpan, error) {
+       impl, ok := ctx.State.(HashState)
+       if !ok {
+               return nil, fmt.Errorf("%w: HashState in invalid state", 
arrow.ErrInvalid)
+       }
+       defer releaseHashMemo(impl)
+
+       dict, err := impl.GetDictionary()
+       if err != nil {
+               return nil, err
+       }
+       defer dict.Release()
+
+       for _, result := range results {
+               var dictSpan exec.ArraySpan
+               dictSpan.TakeOwnership(dict)
+               result.SetDictionary(&dictSpan)
+       }
+       return results, nil
+}
+
+func releaseHashMemo(hash HashState) {
+       if state, ok := hash.(*regularHashState); ok {
+               if memo, ok := state.memoTable.(*hashing.BinaryMemoTable); ok 
&& !state.memoReleased {
+                       memo.Release()
+                       state.memoReleased = true
+               }
+       }
 }
 
 func GetVectorHashKernels() (unique, valueCounts, dictEncode 
[]exec.VectorKernel) {
@@ -561,5 +900,22 @@ func GetVectorHashKernels() (unique, valueCounts, 
dictEncode []exec.VectorKernel
        }
        unique = append(unique, base)
 
+       // dictionary encode
+       base.Finalize = dictionaryEncodeFinalize
+       base.OutputChunked = true
+       base.NullHandling = exec.NullComputedNoPrealloc
+       base.MemAlloc = exec.MemNoPrealloc
+       dictEncode = addHashKernels(base, initDictionaryEncode, 
outputDictionaryType)
+       identity := exec.NewVectorKernelWithSig(
+               &exec.KernelSignature{
+                       InputTypes: 
[]exec.InputType{exec.NewIDInput(arrow.DICTIONARY)},
+                       OutType:    OutputFirstType,
+               },
+               dictionaryEncodeIdentity,
+               initDictionaryEncodeIdentity)
+       identity.CanExecuteChunkWise = false
+       identity.ExecChunked = dictionaryEncodeIdentityChunked
+       dictEncode = append(dictEncode, identity)
+
        return
 }
diff --git a/arrow/compute/internal/kernels/vector_hash_test.go 
b/arrow/compute/internal/kernels/vector_hash_test.go
new file mode 100644
index 00000000..9f0549d4
--- /dev/null
+++ b/arrow/compute/internal/kernels/vector_hash_test.go
@@ -0,0 +1,78 @@
+// 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.
+
+//go:build go1.18
+
+package kernels
+
+import (
+       "context"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/compute/exec"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/stretchr/testify/require"
+)
+
+func TestDictionaryEncodeStateResetAfterFinalize(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       ctx := &exec.KernelCtx{Ctx: exec.WithAllocator(context.Background(), 
mem)}
+       inputBuilder := array.NewStringBuilder(mem)
+       inputBuilder.AppendValues([]string{"foo", "bar", "foo"}, nil)
+       input := inputBuilder.NewStringArray()
+       inputBuilder.Release()
+       defer input.Release()
+
+       state, err := getHashInit(arrow.STRING, initDictionaryEncode)(ctx, 
exec.KernelInitArgs{
+               Inputs:  []arrow.DataType{arrow.BinaryTypes.String},
+               Options: DictionaryEncodeOptions{},
+       })
+       require.NoError(t, err)
+       ctx.State = state
+       hash := state.(HashState)
+
+       inputSpan := &exec.ArraySpan{}
+       inputSpan.SetMembers(input.Data())
+       outputType := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int32,
+               ValueType: input.DataType(),
+       }
+
+       encodeOnce := func() {
+               require.NoError(t, hash.Append(ctx, inputSpan))
+               result := &exec.ArraySpan{Type: outputType}
+               require.NoError(t, hash.Flush(result))
+
+               results, err := dictionaryEncodeFinalize(ctx, 
[]*exec.ArraySpan{result})
+               require.NoError(t, err)
+               for _, result := range results {
+                       result.Release()
+               }
+       }
+
+       encodeOnce()
+       stateImpl := state.(*regularHashState)
+       require.True(t, stateImpl.memoReleased)
+
+       require.NoError(t, hash.Reset())
+       require.False(t, stateImpl.memoReleased)
+
+       encodeOnce()
+}
diff --git a/arrow/compute/vector_hash.go b/arrow/compute/vector_hash.go
index facdd1ff..017bf4de 100644
--- a/arrow/compute/vector_hash.go
+++ b/arrow/compute/vector_hash.go
@@ -20,19 +20,44 @@ package compute
 
 import (
        "context"
+       "fmt"
 
        "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
        "github.com/apache/arrow-go/v18/arrow/compute/internal/kernels"
 )
 
 var (
        uniqueDoc = FunctionDoc{
-               Summary:     "Compute unique elements",
-               Description: "Return an array with distinct values. Nulls in 
the input are ignored",
+               Summary: "Compute unique elements",
+               Description: "Return an array with distinct values.\n" +
+                       "Nulls in the input are considered a distinct value",
+               ArgNames: []string{"array"},
+       }
+       dictionaryEncodeDoc = FunctionDoc{
+               Summary: "Dictionary encode an array",
+               Description: "Return a dictionary-encoded array with the 
distinct values in the dictionary.\n" +
+                       "If the input is already dictionary encoded, it is 
returned unchanged,\n" +
+                       "including its index type and null representation.\n" +
+                       "Newly encoded arrays use int32 dictionary indices.",
                ArgNames:    []string{"array"},
+               OptionsType: "DictionaryEncodeOptions",
        }
 )
 
+// NullEncodingBehavior controls how null input values are represented.
+type NullEncodingBehavior = kernels.NullEncodingBehavior
+
+const (
+       // NullEncodingMask keeps null input values null in the indices array.
+       NullEncodingMask = kernels.NullEncodingMask
+       // NullEncodingEncode adds null input values to the dictionary as a 
regular entry.
+       NullEncodingEncode = kernels.NullEncodingEncode
+)
+
+// DictionaryEncodeOptions controls dictionary encoding behavior.
+type DictionaryEncodeOptions = kernels.DictionaryEncodeOptions
+
 func Unique(ctx context.Context, values Datum) (Datum, error) {
        return CallFunction(ctx, "unique", nil, values)
 }
@@ -47,8 +72,38 @@ func UniqueArray(ctx context.Context, values arrow.Array) 
(arrow.Array, error) {
        return out.(*ArrayDatum).MakeArray(), nil
 }
 
+// DictionaryEncode returns a dictionary-encoded version of values.
+// Newly encoded arrays use int32 indices. For newly encoded arrays, nulls are
+// masked unless NullEncodingEncode is selected. Existing dictionary arrays are
+// returned unchanged, including their index type and null representation.
+func DictionaryEncode(ctx context.Context, opts DictionaryEncodeOptions, 
values Datum) (Datum, error) {
+       return CallFunction(ctx, "dictionary_encode", &opts, values)
+}
+
+// DictionaryEncodeArray returns a dictionary-encoded version of values.
+func DictionaryEncodeArray(ctx context.Context, opts DictionaryEncodeOptions, 
values arrow.Array) (arrow.Array, error) {
+       datum, err := DictionaryEncode(ctx, opts, &ArrayDatum{Value: 
values.Data()})
+       if err != nil {
+               return nil, err
+       }
+       defer datum.Release()
+
+       switch out := datum.(type) {
+       case *ArrayDatum:
+               return out.MakeArray(), nil
+       case *ChunkedDatum:
+               return array.Concatenate(out.Chunks(), GetAllocator(ctx))
+       default:
+               return nil, fmt.Errorf(
+                       "%w: dictionary_encode returned unexpected datum kind 
%s",
+                       arrow.ErrInvalid,
+                       datum.Kind(),
+               )
+       }
+}
+
 func RegisterVectorHash(reg FunctionRegistry) {
-       unique, _, _ := kernels.GetVectorHashKernels()
+       unique, _, dictEncode := kernels.GetVectorHashKernels()
        uniqFn := NewVectorFunction("unique", Unary(), uniqueDoc)
        for _, vd := range unique {
                if err := uniqFn.AddKernel(vd); err != nil {
@@ -56,4 +111,13 @@ func RegisterVectorHash(reg FunctionRegistry) {
                }
        }
        reg.AddFunction(uniqFn, false)
+
+       dictFn := NewVectorFunction("dictionary_encode", Unary(), 
dictionaryEncodeDoc)
+       dictFn.SetDefaultOptions(&DictionaryEncodeOptions{})
+       for _, vd := range dictEncode {
+               if err := dictFn.AddKernel(vd); err != nil {
+                       panic(err)
+               }
+       }
+       reg.AddFunction(dictFn, false)
 }
diff --git a/arrow/compute/vector_hash_test.go 
b/arrow/compute/vector_hash_test.go
index 1c1b0179..2dc44f8c 100644
--- a/arrow/compute/vector_hash_test.go
+++ b/arrow/compute/vector_hash_test.go
@@ -29,6 +29,7 @@ import (
        "github.com/apache/arrow-go/v18/arrow/decimal128"
        "github.com/apache/arrow-go/v18/arrow/decimal256"
        "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/arrow/scalar"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
        "github.com/stretchr/testify/suite"
@@ -296,6 +297,27 @@ func TestHashKernels(t *testing.T) {
        suite.Run(t, &BinaryTypeHashKernelSuite[[]byte]{dt: 
arrow.BinaryTypes.LargeBinary})
 }
 
+func TestUniqueBoolean(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       input, _, err := array.FromJSON(mem, arrow.FixedWidthTypes.Boolean,
+               strings.NewReader(`[false, null, true, false, null, true]`))
+       require.NoError(t, err)
+       defer input.Release()
+       expected, _, err := array.FromJSON(mem, arrow.FixedWidthTypes.Boolean,
+               strings.NewReader(`[false, null, true]`))
+       require.NoError(t, err)
+       defer expected.Release()
+
+       result, err := compute.UniqueArray(ctx, input)
+       require.NoError(t, err)
+       defer result.Release()
+
+       assert.True(t, array.Equal(expected, result))
+}
+
 func TestUniqueTimeTimestamp(t *testing.T) {
        mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
        defer mem.AssertSize(t, 0)
@@ -515,3 +537,730 @@ func TestDictionaryUnique(t *testing.T) {
                })
        }
 }
+
+func TestDictionaryEncode(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       values, _, err := array.FromJSON(mem, arrow.BinaryTypes.String,
+               strings.NewReader(`["foo", "bar", "foo", null, "bar", null]`))
+       require.NoError(t, err)
+       defer values.Release()
+
+       out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{},
+               &compute.ArrayDatum{Value: values.Data()})
+       require.NoError(t, err)
+       defer out.Release()
+
+       result := out.(*compute.ArrayDatum).MakeArray().(*array.Dictionary)
+       defer result.Release()
+
+       require.Equal(t, values.Len(), result.Len())
+       require.Equal(t, &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int32,
+               ValueType: arrow.BinaryTypes.String,
+       }, result.DataType())
+       require.Equal(t, 2, result.Dictionary().Len())
+       require.Equal(t, "foo", result.Dictionary().ValueStr(0))
+       require.Equal(t, "bar", result.Dictionary().ValueStr(1))
+       assert.Equal(t, []int32{0, 1, 0, 0, 1, 0},
+               
arrow.Int32Traits.CastFromBytes(result.Indices().Data().Buffers()[1].Bytes()))
+       assert.True(t, result.IsNull(3))
+       assert.True(t, result.IsNull(5))
+
+       encoded, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{
+               NullEncoding: compute.NullEncodingEncode,
+       }, &compute.ArrayDatum{Value: values.Data()})
+       require.NoError(t, err)
+       defer encoded.Release()
+
+       encodedResult := 
encoded.(*compute.ArrayDatum).MakeArray().(*array.Dictionary)
+       defer encodedResult.Release()
+
+       require.Equal(t, values.Len(), encodedResult.Len())
+       assert.Equal(t, 0, encodedResult.NullN())
+       require.Equal(t, 3, encodedResult.Dictionary().Len())
+       assert.Equal(t, 1, encodedResult.Dictionary().NullN())
+       assert.Equal(t, []int32{0, 1, 0, 2, 1, 2},
+               
arrow.Int32Traits.CastFromBytes(encodedResult.Indices().Data().Buffers()[1].Bytes()))
+       for i := 0; i < values.Len(); i++ {
+               assert.True(t, encodedResult.IsValid(i))
+               idx := encodedResult.GetValueIndex(i)
+               if values.IsNull(i) {
+                       assert.True(t, encodedResult.Dictionary().IsNull(idx))
+               } else {
+                       assert.Equal(t, values.ValueStr(i), 
encodedResult.Dictionary().ValueStr(idx))
+               }
+       }
+
+       _, err = compute.DictionaryEncode(ctx, compute.DictionaryEncodeOptions{
+               NullEncoding: compute.NullEncodingBehavior(99),
+       }, &compute.ArrayDatum{Value: values.Data()})
+       require.ErrorIs(t, err, arrow.ErrInvalid)
+}
+
+func TestDictionaryEncodeArrayWithSmallExecChunkSize(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       values, _, err := array.FromJSON(
+               mem,
+               arrow.PrimitiveTypes.Int32,
+               strings.NewReader(`[1, 2, 1, 3, 2]`),
+       )
+       require.NoError(t, err)
+       defer values.Release()
+
+       execCtx := compute.DefaultExecCtx()
+       execCtx.ChunkSize = 2
+       ctx := compute.SetExecCtx(
+               compute.WithAllocator(context.Background(), mem),
+               execCtx,
+       )
+
+       result, err := compute.DictionaryEncodeArray(
+               ctx,
+               compute.DictionaryEncodeOptions{},
+               values,
+       )
+       require.NoError(t, err)
+       defer result.Release()
+
+       encoded := result.(*array.Dictionary)
+       require.Equal(t, values.Len(), encoded.Len())
+       require.Equal(t, 3, encoded.Dictionary().Len())
+       assert.Equal(t, []int32{0, 1, 0, 2, 1},
+               
arrow.Int32Traits.CastFromBytes(encoded.Indices().Data().Buffers()[1].Bytes()))
+}
+
+func TestDictionaryEncodeBoolean(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       values, _, err := array.FromJSON(mem, arrow.FixedWidthTypes.Boolean,
+               strings.NewReader(`[false, true, false, null, true]`))
+       require.NoError(t, err)
+       defer values.Release()
+
+       tests := []struct {
+               name      string
+               nullMode  compute.NullEncodingBehavior
+               dict      []bool
+               dictLen   int
+               indices   []int32
+               nullCount int
+       }{
+               {
+                       name:      "masked nulls",
+                       nullMode:  compute.NullEncodingMask,
+                       dict:      []bool{false, true},
+                       dictLen:   2,
+                       indices:   []int32{0, 1, 0, 0, 1},
+                       nullCount: 1,
+               },
+               {
+                       name:      "encoded nulls",
+                       nullMode:  compute.NullEncodingEncode,
+                       dict:      []bool{false, true},
+                       dictLen:   3,
+                       indices:   []int32{0, 1, 0, 2, 1},
+                       nullCount: 0,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       result, err := compute.DictionaryEncodeArray(ctx, 
compute.DictionaryEncodeOptions{
+                               NullEncoding: tc.nullMode,
+                       }, values)
+                       require.NoError(t, err)
+                       defer result.Release()
+
+                       encoded := result.(*array.Dictionary)
+                       dict := encoded.Dictionary().(*array.Boolean)
+                       require.Equal(t, tc.dictLen, dict.Len())
+                       for i, value := range tc.dict {
+                               assert.Equal(t, value, dict.Value(i))
+                       }
+                       assert.Equal(t, tc.indices,
+                               
arrow.Int32Traits.CastFromBytes(encoded.Indices().Data().Buffers()[1].Bytes()))
+                       assert.Equal(t, tc.nullCount, encoded.NullN())
+                       if tc.nullMode == compute.NullEncodingMask {
+                               assert.True(t, encoded.IsNull(3))
+                       } else {
+                               assert.True(t, encoded.IsValid(3))
+                               assert.True(t, 
dict.IsNull(encoded.GetValueIndex(3)))
+                       }
+               })
+       }
+}
+
+func TestDictionaryEncodeBooleanSlicedInput(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       values, _, err := array.FromJSON(mem, arrow.FixedWidthTypes.Boolean,
+               strings.NewReader(`[true, false, true, null, true, false, 
true]`))
+       require.NoError(t, err)
+       defer values.Release()
+
+       input := array.NewSlice(values, 1, 6)
+       defer input.Release()
+       require.Equal(t, 1, input.Data().Offset())
+
+       tests := []struct {
+               name      string
+               nullMode  compute.NullEncodingBehavior
+               dict      []bool
+               dictLen   int
+               indices   []int32
+               nullCount int
+       }{
+               {
+                       name:      "masked nulls",
+                       nullMode:  compute.NullEncodingMask,
+                       dict:      []bool{false, true},
+                       dictLen:   2,
+                       indices:   []int32{0, 1, 0, 1, 0},
+                       nullCount: 1,
+               },
+               {
+                       name:      "encoded nulls",
+                       nullMode:  compute.NullEncodingEncode,
+                       dict:      []bool{false, true},
+                       dictLen:   3,
+                       indices:   []int32{0, 1, 2, 1, 0},
+                       nullCount: 0,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       result, err := compute.DictionaryEncodeArray(ctx, 
compute.DictionaryEncodeOptions{
+                               NullEncoding: tc.nullMode,
+                       }, input)
+                       require.NoError(t, err)
+                       defer result.Release()
+
+                       encoded := result.(*array.Dictionary)
+                       dict := encoded.Dictionary().(*array.Boolean)
+                       require.Equal(t, tc.dictLen, dict.Len())
+                       for i, value := range tc.dict {
+                               assert.Equal(t, value, dict.Value(i))
+                       }
+                       assert.Equal(t, tc.indices,
+                               
arrow.Int32Traits.CastFromBytes(encoded.Indices().Data().Buffers()[1].Bytes()))
+                       assert.Equal(t, tc.nullCount, encoded.NullN())
+                       if tc.nullMode == compute.NullEncodingMask {
+                               assert.True(t, encoded.IsNull(2))
+                       } else {
+                               assert.True(t, encoded.IsValid(2))
+                               assert.True(t, dict.IsNull(len(tc.dict)))
+                               assert.True(t, 
dict.IsNull(encoded.GetValueIndex(2)))
+                       }
+               })
+       }
+}
+
+func TestDictionaryEncodeResizesMemoTable(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       inputValues := make([]int32, 200)
+       for i := range inputValues {
+               inputValues[i] = int32(i % 100)
+       }
+
+       builder := array.NewInt32Builder(mem)
+       builder.AppendValues(inputValues, nil)
+       input := builder.NewInt32Array()
+       builder.Release()
+       defer input.Release()
+
+       result, err := compute.DictionaryEncodeArray(ctx, 
compute.DictionaryEncodeOptions{}, input)
+       require.NoError(t, err)
+       defer result.Release()
+
+       encoded := result.(*array.Dictionary)
+       dictionary := encoded.Dictionary().(*array.Int32)
+       indices := encoded.Indices().(*array.Int32)
+
+       expectedDictionary := inputValues[:100]
+       require.Equal(t, expectedDictionary, dictionary.Int32Values())
+       require.Equal(t, inputValues, indices.Int32Values())
+}
+
+func TestDictionaryEncodeArraySlicedInput(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       values, _, err := array.FromJSON(mem, arrow.BinaryTypes.String,
+               strings.NewReader(`["ignored", "foo", null, "bar", "foo", 
"ignored"]`))
+       require.NoError(t, err)
+       defer values.Release()
+
+       input := array.NewSlice(values, 1, 5)
+       defer input.Release()
+
+       result, err := compute.DictionaryEncodeArray(ctx, 
compute.DictionaryEncodeOptions{}, input)
+       require.NoError(t, err)
+       defer result.Release()
+
+       encoded := result.(*array.Dictionary)
+       require.Equal(t, 4, encoded.Len())
+       require.Equal(t, 2, encoded.Dictionary().Len())
+       assert.Equal(t, "foo", encoded.Dictionary().ValueStr(0))
+       assert.Equal(t, "bar", encoded.Dictionary().ValueStr(1))
+       assert.Equal(t, []int32{0, 0, 1, 0},
+               
arrow.Int32Traits.CastFromBytes(encoded.Indices().Data().Buffers()[1].Bytes()))
+       assert.True(t, encoded.IsNull(1))
+}
+
+func TestDictionaryEncodePreservesTimestampType(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       dt := &arrow.TimestampType{Unit: arrow.Second, TimeZone: "UTC"}
+       builder := array.NewTimestampBuilder(mem, dt)
+       builder.AppendValues([]arrow.Timestamp{1, 2, 1}, nil)
+       input := builder.NewArray()
+       builder.Release()
+       defer input.Release()
+
+       result, err := compute.DictionaryEncodeArray(ctx, 
compute.DictionaryEncodeOptions{}, input)
+       require.NoError(t, err)
+       defer result.Release()
+
+       encoded := result.(*array.Dictionary)
+       require.True(t, arrow.TypeEqual(dt, encoded.Dictionary().DataType()))
+       require.Equal(t, 2, encoded.Dictionary().Len())
+}
+
+func TestDictionaryEncodeZeroChunkedArray(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       input := arrow.NewChunked(arrow.BinaryTypes.String, nil)
+       defer input.Release()
+
+       out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{},
+               &compute.ChunkedDatum{Value: input})
+       require.NoError(t, err)
+       defer out.Release()
+
+       result := out.(*compute.ChunkedDatum).Value
+       assert.Equal(t, 0, result.Len())
+       assert.Empty(t, result.Chunks())
+       assert.True(t, arrow.TypeEqual(&arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int32,
+               ValueType: arrow.BinaryTypes.String,
+       }, result.DataType()))
+}
+
+func TestDictionaryEncodeZeroChunkedDictionaryInput(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       dictType := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int8,
+               ValueType: arrow.BinaryTypes.String,
+       }
+       input := arrow.NewChunked(dictType, nil)
+       defer input.Release()
+
+       out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{},
+               &compute.ChunkedDatum{Value: input})
+       require.NoError(t, err)
+       defer out.Release()
+
+       result := out.(*compute.ChunkedDatum).Value
+       require.Equal(t, 0, result.Len())
+       require.Empty(t, result.Chunks())
+       require.True(t, arrow.TypeEqual(dictType, result.DataType()))
+}
+
+func TestDictionaryEncodeChunked(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       first, _, err := array.FromJSON(mem, arrow.PrimitiveTypes.Int32, 
strings.NewReader(`[1, 2, 1]`))
+       require.NoError(t, err)
+       defer first.Release()
+       second, _, err := array.FromJSON(mem, arrow.PrimitiveTypes.Int32, 
strings.NewReader(`[2, 3, null]`))
+       require.NoError(t, err)
+       defer second.Release()
+
+       input := arrow.NewChunked(arrow.PrimitiveTypes.Int32, 
[]arrow.Array{first, second})
+       defer input.Release()
+
+       out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{},
+               &compute.ChunkedDatum{Value: input})
+       require.NoError(t, err)
+       defer out.Release()
+
+       result := out.(*compute.ChunkedDatum).Value
+       require.Len(t, result.Chunks(), 2)
+       require.Equal(t, first.Len(), result.Chunk(0).Len())
+       require.Equal(t, second.Len(), result.Chunk(1).Len())
+       assert.Equal(t, []int32{0, 1, 0},
+               
arrow.Int32Traits.CastFromBytes(result.Chunk(0).(*array.Dictionary).Indices().Data().Buffers()[1].Bytes()))
+       assert.Equal(t, []int32{1, 2, 0},
+               
arrow.Int32Traits.CastFromBytes(result.Chunk(1).(*array.Dictionary).Indices().Data().Buffers()[1].Bytes()))
+       assert.True(t, result.Chunk(1).IsNull(2))
+       for _, chunk := range result.Chunks() {
+               encoded := chunk.(*array.Dictionary)
+               assert.True(t, array.Equal(encoded.Dictionary(), 
result.Chunk(0).(*array.Dictionary).Dictionary()))
+       }
+}
+
+func TestDictionaryEncodeChunkedBinary(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       types := []struct {
+               name string
+               typ  arrow.DataType
+       }{
+               {name: "string", typ: arrow.BinaryTypes.String},
+               {name: "large string", typ: arrow.BinaryTypes.LargeString},
+       }
+       modes := []struct {
+               name      string
+               nullMode  compute.NullEncodingBehavior
+               dict      []string
+               indices   [][]int32
+               nullCount []int
+       }{
+               {
+                       name:      "masked nulls",
+                       nullMode:  compute.NullEncodingMask,
+                       dict:      []string{"a", "b", "c"},
+                       indices:   [][]int32{{0, 1, 0, 0}, {1, 2, 0, 0}},
+                       nullCount: []int{1, 1},
+               },
+               {
+                       name:      "encoded nulls",
+                       nullMode:  compute.NullEncodingEncode,
+                       dict:      []string{"a", "b", "(null)", "c"},
+                       indices:   [][]int32{{0, 1, 2, 0}, {1, 3, 2, 0}},
+                       nullCount: []int{0, 0},
+               },
+       }
+
+       for _, typ := range types {
+               for _, mode := range modes {
+                       t.Run(typ.name+"/"+mode.name, func(t *testing.T) {
+                               first, _, err := array.FromJSON(mem, typ.typ, 
strings.NewReader(`["a", "b", null, "a"]`))
+                               require.NoError(t, err)
+                               defer first.Release()
+                               second, _, err := array.FromJSON(mem, typ.typ, 
strings.NewReader(`["b", "c", null, "a"]`))
+                               require.NoError(t, err)
+                               defer second.Release()
+
+                               input := arrow.NewChunked(typ.typ, 
[]arrow.Array{first, second})
+                               defer input.Release()
+
+                               out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{
+                                       NullEncoding: mode.nullMode,
+                               }, &compute.ChunkedDatum{Value: input})
+                               require.NoError(t, err)
+                               defer out.Release()
+
+                               result := out.(*compute.ChunkedDatum).Value
+                               require.Len(t, result.Chunks(), 2)
+                               require.True(t, 
arrow.TypeEqual(result.DataType(), &arrow.DictionaryType{
+                                       IndexType: arrow.PrimitiveTypes.Int32,
+                                       ValueType: typ.typ,
+                               }))
+                               for i, chunk := range result.Chunks() {
+                                       encoded := chunk.(*array.Dictionary)
+                                       require.Equal(t, len(mode.dict), 
encoded.Dictionary().Len())
+                                       for j, value := range mode.dict {
+                                               assert.Equal(t, value, 
encoded.Dictionary().ValueStr(j))
+                                       }
+                                       assert.Equal(t, mode.indices[i],
+                                               
arrow.Int32Traits.CastFromBytes(encoded.Indices().Data().Buffers()[1].Bytes()))
+                                       assert.Equal(t, mode.nullCount[i], 
encoded.NullN())
+                                       if mode.nullMode == 
compute.NullEncodingMask {
+                                               assert.True(t, 
encoded.IsNull(2))
+                                       } else {
+                                               assert.True(t, 
encoded.IsValid(2))
+                                               assert.True(t, 
encoded.Dictionary().IsNull(encoded.GetValueIndex(2)))
+                                       }
+                               }
+                       })
+               }
+       }
+}
+
+func TestDictionaryEncodeNullArray(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       input := array.NewNull(3)
+       defer input.Release()
+
+       tests := []struct {
+               name         string
+               nullEncoding compute.NullEncodingBehavior
+               dictLen      int
+               nullCount    int
+       }{
+               {name: "masked", nullEncoding: compute.NullEncodingMask, 
dictLen: 1, nullCount: 3},
+               {name: "encoded", nullEncoding: compute.NullEncodingEncode, 
dictLen: 1, nullCount: 0},
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{
+                               NullEncoding: tc.nullEncoding,
+                       }, &compute.ArrayDatum{Value: input.Data()})
+                       require.NoError(t, err)
+                       defer out.Release()
+
+                       result := 
out.(*compute.ArrayDatum).MakeArray().(*array.Dictionary)
+                       defer result.Release()
+                       require.Equal(t, input.Len(), result.Len())
+                       assert.Equal(t, tc.dictLen, result.Dictionary().Len())
+                       assert.Equal(t, tc.nullCount, result.NullN())
+                       if tc.nullEncoding == compute.NullEncodingEncode {
+                               for i := 0; i < result.Len(); i++ {
+                                       assert.True(t, result.IsValid(i))
+                               }
+                       }
+               })
+       }
+}
+
+func TestDictionaryEncodeDictionaryInput(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       dictType := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int8,
+               ValueType: arrow.BinaryTypes.String,
+       }
+       input, err := array.DictArrayFromJSON(mem, dictType, `[0, null, 1]`, 
`["foo", "bar"]`)
+       require.NoError(t, err)
+       defer input.Release()
+
+       out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{},
+               &compute.ArrayDatum{Value: input.Data()})
+       require.NoError(t, err)
+       defer out.Release()
+
+       result := out.(*compute.ArrayDatum).MakeArray().(*array.Dictionary)
+       defer result.Release()
+       require.True(t, arrow.TypeEqual(dictType, result.DataType()))
+       require.Equal(t, input.Len(), result.Len())
+       assert.True(t, array.Equal(input, result))
+}
+
+func TestDictionaryEncodeOptionsUseCanonicalFieldName(t *testing.T) {
+       for _, tt := range []struct {
+               name     string
+               behavior compute.NullEncodingBehavior
+               encoded  uint32
+       }{
+               {name: "encode", behavior: compute.NullEncodingEncode, encoded: 
0},
+               {name: "mask", behavior: compute.NullEncodingMask, encoded: 1},
+       } {
+               t.Run(tt.name, func(t *testing.T) {
+                       mem := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                       defer mem.AssertSize(t, 0)
+
+                       encoded, err := 
scalar.ToScalar(compute.DictionaryEncodeOptions{
+                               NullEncoding: tt.behavior,
+                       }, mem)
+                       require.NoError(t, err)
+                       if releasable, ok := encoded.(interface{ Release() }); 
ok {
+                               defer releasable.Release()
+                       }
+
+                       options := encoded.(*scalar.Struct)
+                       field, err := options.Field("null_encoding_behavior")
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.encoded, 
field.(*scalar.Uint32).Value)
+                       _, err = options.Field("null_encoding")
+                       assert.Error(t, err)
+               })
+       }
+}
+
+func TestDictionaryEncodeOptionsDecodeCanonicalValues(t *testing.T) {
+       for _, tt := range []struct {
+               name     string
+               encoded  uint32
+               behavior compute.NullEncodingBehavior
+       }{
+               {name: "encode", encoded: 0, behavior: 
compute.NullEncodingEncode},
+               {name: "mask", encoded: 1, behavior: compute.NullEncodingMask},
+       } {
+               t.Run(tt.name, func(t *testing.T) {
+                       encoded, err := scalar.NewStructScalarWithNames(
+                               
[]scalar.Scalar{scalar.NewUint32Scalar(tt.encoded)},
+                               []string{"null_encoding_behavior"},
+                       )
+                       require.NoError(t, err)
+                       defer encoded.Release()
+
+                       var options compute.DictionaryEncodeOptions
+                       require.NoError(t, scalar.FromScalar(encoded, &options))
+                       assert.Equal(t, tt.behavior, options.NullEncoding)
+               })
+       }
+}
+
+func TestDictionaryEncodeOptionsRejectInvalidCanonicalValue(t *testing.T) {
+       encoded, err := scalar.NewStructScalarWithNames(
+               []scalar.Scalar{scalar.NewUint32Scalar(2)},
+               []string{"null_encoding_behavior"},
+       )
+       require.NoError(t, err)
+       defer encoded.Release()
+
+       var options compute.DictionaryEncodeOptions
+       err = scalar.FromScalar(encoded, &options)
+       assert.ErrorIs(t, err, arrow.ErrInvalid)
+}
+
+func TestDictionaryEncodeOptionsRejectInvalidSerialization(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       _, err := scalar.ToScalar(compute.DictionaryEncodeOptions{
+               NullEncoding: compute.NullEncodingBehavior(42),
+       }, mem)
+       assert.ErrorIs(t, err, arrow.ErrInvalid)
+}
+
+func TestDictionaryEncodeDictionaryInputWithEmptyIndices(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       dictType := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int8,
+               ValueType: arrow.BinaryTypes.String,
+       }
+       input, err := array.DictArrayFromJSON(mem, dictType, `[]`, `["foo", 
"bar"]`)
+       require.NoError(t, err)
+       defer input.Release()
+
+       out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{},
+               &compute.ArrayDatum{Value: input.Data()})
+       require.NoError(t, err)
+       defer out.Release()
+
+       result := out.(*compute.ArrayDatum).MakeArray().(*array.Dictionary)
+       defer result.Release()
+       require.Equal(t, 0, result.Len())
+       require.True(t, arrow.TypeEqual(dictType, result.DataType()))
+       require.Equal(t, 2, result.Dictionary().Len())
+       assert.Equal(t, "foo", result.Dictionary().ValueStr(0))
+       assert.Equal(t, "bar", result.Dictionary().ValueStr(1))
+}
+
+func TestDictionaryEncodeChunkedDictionaryInput(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       dictType := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int8,
+               ValueType: arrow.BinaryTypes.String,
+       }
+       first, err := array.DictArrayFromJSON(mem, dictType, `[0, 1]`, `["foo", 
"bar"]`)
+       require.NoError(t, err)
+       defer first.Release()
+       second, err := array.DictArrayFromJSON(mem, dictType, `[0, 1]`, 
`["bar", "baz"]`)
+       require.NoError(t, err)
+       defer second.Release()
+
+       input := arrow.NewChunked(dictType, []arrow.Array{first, second})
+       defer input.Release()
+
+       out, err := compute.DictionaryEncode(ctx, 
compute.DictionaryEncodeOptions{},
+               &compute.ChunkedDatum{Value: input})
+       require.NoError(t, err)
+       defer out.Release()
+
+       result := out.(*compute.ChunkedDatum).Value
+       require.Len(t, result.Chunks(), 2)
+       assert.True(t, array.Equal(first, result.Chunk(0)))
+       assert.True(t, array.Equal(second, result.Chunk(1)))
+       assert.Equal(t, "foo", 
result.Chunk(0).(*array.Dictionary).Dictionary().ValueStr(0))
+       assert.Equal(t, "baz", 
result.Chunk(1).(*array.Dictionary).Dictionary().ValueStr(1))
+}
+
+func TestDictionaryEncodeArrayDictionaryInputWithSmallExecChunkSize(t 
*testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       dictType := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int8,
+               ValueType: arrow.BinaryTypes.String,
+       }
+       input, err := array.DictArrayFromJSON(mem, dictType, `[0, 1, 0, null, 
1]`, `["foo", "bar"]`)
+       require.NoError(t, err)
+       defer input.Release()
+
+       execCtx := compute.DefaultExecCtx()
+       execCtx.ChunkSize = 2
+       ctx := compute.SetExecCtx(
+               compute.WithAllocator(context.Background(), mem),
+               execCtx,
+       )
+
+       result, err := compute.DictionaryEncodeArray(
+               ctx,
+               compute.DictionaryEncodeOptions{},
+               input,
+       )
+       require.NoError(t, err)
+       defer result.Release()
+
+       require.True(t, arrow.TypeEqual(dictType, result.DataType()))
+       assert.True(t, array.Equal(input, result))
+}
+
+func TestDictionaryEncodeDictionaryInputRejectsInvalidOptions(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       dictType := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int8,
+               ValueType: arrow.BinaryTypes.String,
+       }
+       input, err := array.DictArrayFromJSON(mem, dictType, `[0, 1]`, `["foo", 
"bar"]`)
+       require.NoError(t, err)
+       defer input.Release()
+
+       _, err = compute.DictionaryEncode(ctx, compute.DictionaryEncodeOptions{
+               NullEncoding: compute.NullEncodingBehavior(99),
+       }, &compute.ArrayDatum{Value: input.Data()})
+       require.ErrorIs(t, err, arrow.ErrInvalid)
+}
+
+func TestDictionaryEncodeFunctionValidation(t *testing.T) {
+       for _, name := range []string{"unique", "dictionary_encode"} {
+               t.Run(name, func(t *testing.T) {
+                       fn, ok := 
compute.GetFunctionRegistry().GetFunction(name)
+                       require.True(t, ok)
+                       require.NoError(t, fn.Validate())
+               })
+       }
+}

Reply via email to