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 b9f4e362 feat(parquet): add WithDictionaryCostFallback writer property 
(#1036)
b9f4e362 is described below

commit b9f4e3623dc22d5994cca6f5a37752fbbe11c2d5
Author: Tom Frank <[email protected]>
AuthorDate: Fri Aug 7 18:33:50 2026 +0300

    feat(parquet): add WithDictionaryCostFallback writer property (#1036)
    
    ### Rationale for this change
    
    The pre-first-page dictionary cost check (`checkDictionarySizeLimit`,
    mirroring parquet-mr's `FallbackValuesWriter.shouldFallBack`) compares
    **uncompressed** sizes: the dictionary is discarded when `dictSize +
    encodedSize >= rawSize`. That comparison only predicts on-disk cost when
    pages are stored uncompressed. With a page compressor configured it can
    discard dictionaries that clearly win after compression — bit-packed
    dictionary indices are near-incompressible but small, while
    PLAIN-encoded values often compress far less than the raw-size
    comparison assumes. In our benchmarks (ZSTD level 3), a mid-cardinality
    `Time64` column written PLAIN after the fallback was 2× larger than the
    same column dictionary-encoded (7.1 MB vs 3.6 MB compressed);
    mid-cardinality integer columns lost 20%+.
    
    ### What changes are included in this PR?
    
    - **The cost fallback is now codec-aware by default**: it only runs for
    columns written without page compression, where the uncompressed-size
    comparison is sound. For columns with a codec configured the dictionary
    is kept (still bounded by the `DictionaryPageSizeLimit` fallback, which
    is unaffected). Uncompressed columns keep today's behavior exactly.
    - **A per-column override**: `parquet.WithDictionaryCostFallbackFor(path
    string, enabled bool)` / `WithDictionaryCostFallbackPath(path
    ColumnPath, enabled bool)`, stored as a tri-state
    `DictionaryCostFallback *bool` on `ColumnProperties` (nil = codec-based
    default). It overrides the default in either direction: `false` keeps an
    explicitly requested dictionary for an uncompressed column, `true`
    forces the check for a compressed one. Useful for callers that decide
    dictionary usage from their own measurements (e.g. trial-encoding a
    sample of the actual data with the actual codec).
    - `WriterProperties.DictionaryCostFallbackEnabledFor(path string) bool`
    resolves the per-column override, falling back to the codec-based
    default.
    
    ### Are these changes tested?
    
    Yes — `parquet/file/dict_cost_fallback_test.go` covers: (1) an
    uncompressed column still discards a non-paying dictionary by default,
    (2) global and (3) per-column compression keep the dictionary by
    default, (4) a per-column disable keeps an uncompressed column's
    dictionary while (5) a disable scoped to a different column leaves the
    fallback active, (6) a per-column enable forces the check for a
    compressed column, and (7) the `DictionaryPageSizeLimit` fallback still
    applies when the cost fallback is off. The existing
    `TestDictFallbackDiscardsOrphanDict` snappy variants now force the check
    via the per-column option to keep exercising the orphan-discard path
    under a codec. Existing `parquet`, `parquet/file`, and
    `parquet/internal/encoding` suites pass.
    
    ### Are there any user-facing changes?
    
    For columns written with a compression codec, the cost-based dictionary
    fallback no longer runs by default — a requested dictionary is kept
    unless it overflows `DictionaryPageSizeLimit`. Uncompressed columns are
    unchanged. New writer properties: `WithDictionaryCostFallbackFor` /
    `WithDictionaryCostFallbackPath`, and a `DictionaryCostFallback *bool`
    field on `ColumnProperties`.
---
 parquet/file/column_writer_types.gen.go      |  72 ++++++++++++----
 parquet/file/column_writer_types.gen.go.tmpl |   9 +-
 parquet/file/dict_cost_fallback_test.go      | 124 +++++++++++++++++++++++++++
 parquet/file/dict_fallback_repro_test.go     |   9 ++
 parquet/writer_properties.go                 |  71 ++++++++++++---
 5 files changed, 257 insertions(+), 28 deletions(-)

diff --git a/parquet/file/column_writer_types.gen.go 
b/parquet/file/column_writer_types.gen.go
index d0dc0a5c..bab348a3 100644
--- a/parquet/file/column_writer_types.gen.go
+++ b/parquet/file/column_writer_types.gen.go
@@ -245,8 +245,13 @@ func (w *Int32ColumnChunkWriter) 
checkDictionarySizeLimit() {
        // plus the encoded indices meet or exceed the raw input bytes, fall 
back
        // to PLAIN now and discard the dictionary — avoiding the 
mid-cardinality
        // case where a dict page stays in the file alongside PLAIN pages 
without
-       // any net compression win.
-       if !w.dictPageWritten && len(w.pages) == 0 {
+       // any net compression win. The comparison uses uncompressed sizes, 
which
+       // is only meaningful when pages are stored uncompressed: with a codec
+       // configured, the near-incompressible dict indices routinely beat PLAIN
+       // pages that compress well. The check therefore runs only for columns
+       // written without page compression, unless overridden per column with
+       // WithDictionaryCostFallbackFor.
+       if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
                rawSize := dictEnc.ObservedRawSize()
                encodedSize := dictEnc.EstimatedDataEncodedSize()
                dictSize := int64(dictEnc.DictEncodedSize())
@@ -507,8 +512,13 @@ func (w *Int64ColumnChunkWriter) 
checkDictionarySizeLimit() {
        // plus the encoded indices meet or exceed the raw input bytes, fall 
back
        // to PLAIN now and discard the dictionary — avoiding the 
mid-cardinality
        // case where a dict page stays in the file alongside PLAIN pages 
without
-       // any net compression win.
-       if !w.dictPageWritten && len(w.pages) == 0 {
+       // any net compression win. The comparison uses uncompressed sizes, 
which
+       // is only meaningful when pages are stored uncompressed: with a codec
+       // configured, the near-incompressible dict indices routinely beat PLAIN
+       // pages that compress well. The check therefore runs only for columns
+       // written without page compression, unless overridden per column with
+       // WithDictionaryCostFallbackFor.
+       if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
                rawSize := dictEnc.ObservedRawSize()
                encodedSize := dictEnc.EstimatedDataEncodedSize()
                dictSize := int64(dictEnc.DictEncodedSize())
@@ -769,8 +779,13 @@ func (w *Int96ColumnChunkWriter) 
checkDictionarySizeLimit() {
        // plus the encoded indices meet or exceed the raw input bytes, fall 
back
        // to PLAIN now and discard the dictionary — avoiding the 
mid-cardinality
        // case where a dict page stays in the file alongside PLAIN pages 
without
-       // any net compression win.
-       if !w.dictPageWritten && len(w.pages) == 0 {
+       // any net compression win. The comparison uses uncompressed sizes, 
which
+       // is only meaningful when pages are stored uncompressed: with a codec
+       // configured, the near-incompressible dict indices routinely beat PLAIN
+       // pages that compress well. The check therefore runs only for columns
+       // written without page compression, unless overridden per column with
+       // WithDictionaryCostFallbackFor.
+       if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
                rawSize := dictEnc.ObservedRawSize()
                encodedSize := dictEnc.EstimatedDataEncodedSize()
                dictSize := int64(dictEnc.DictEncodedSize())
@@ -1031,8 +1046,13 @@ func (w *Float32ColumnChunkWriter) 
checkDictionarySizeLimit() {
        // plus the encoded indices meet or exceed the raw input bytes, fall 
back
        // to PLAIN now and discard the dictionary — avoiding the 
mid-cardinality
        // case where a dict page stays in the file alongside PLAIN pages 
without
-       // any net compression win.
-       if !w.dictPageWritten && len(w.pages) == 0 {
+       // any net compression win. The comparison uses uncompressed sizes, 
which
+       // is only meaningful when pages are stored uncompressed: with a codec
+       // configured, the near-incompressible dict indices routinely beat PLAIN
+       // pages that compress well. The check therefore runs only for columns
+       // written without page compression, unless overridden per column with
+       // WithDictionaryCostFallbackFor.
+       if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
                rawSize := dictEnc.ObservedRawSize()
                encodedSize := dictEnc.EstimatedDataEncodedSize()
                dictSize := int64(dictEnc.DictEncodedSize())
@@ -1293,8 +1313,13 @@ func (w *Float64ColumnChunkWriter) 
checkDictionarySizeLimit() {
        // plus the encoded indices meet or exceed the raw input bytes, fall 
back
        // to PLAIN now and discard the dictionary — avoiding the 
mid-cardinality
        // case where a dict page stays in the file alongside PLAIN pages 
without
-       // any net compression win.
-       if !w.dictPageWritten && len(w.pages) == 0 {
+       // any net compression win. The comparison uses uncompressed sizes, 
which
+       // is only meaningful when pages are stored uncompressed: with a codec
+       // configured, the near-incompressible dict indices routinely beat PLAIN
+       // pages that compress well. The check therefore runs only for columns
+       // written without page compression, unless overridden per column with
+       // WithDictionaryCostFallbackFor.
+       if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
                rawSize := dictEnc.ObservedRawSize()
                encodedSize := dictEnc.EstimatedDataEncodedSize()
                dictSize := int64(dictEnc.DictEncodedSize())
@@ -1701,8 +1726,13 @@ func (w *BooleanColumnChunkWriter) 
checkDictionarySizeLimit() {
        // plus the encoded indices meet or exceed the raw input bytes, fall 
back
        // to PLAIN now and discard the dictionary — avoiding the 
mid-cardinality
        // case where a dict page stays in the file alongside PLAIN pages 
without
-       // any net compression win.
-       if !w.dictPageWritten && len(w.pages) == 0 {
+       // any net compression win. The comparison uses uncompressed sizes, 
which
+       // is only meaningful when pages are stored uncompressed: with a codec
+       // configured, the near-incompressible dict indices routinely beat PLAIN
+       // pages that compress well. The check therefore runs only for columns
+       // written without page compression, unless overridden per column with
+       // WithDictionaryCostFallbackFor.
+       if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
                rawSize := dictEnc.ObservedRawSize()
                encodedSize := dictEnc.EstimatedDataEncodedSize()
                dictSize := int64(dictEnc.DictEncodedSize())
@@ -2073,8 +2103,13 @@ func (w *ByteArrayColumnChunkWriter) 
checkDictionarySizeLimit() {
        // plus the encoded indices meet or exceed the raw input bytes, fall 
back
        // to PLAIN now and discard the dictionary — avoiding the 
mid-cardinality
        // case where a dict page stays in the file alongside PLAIN pages 
without
-       // any net compression win.
-       if !w.dictPageWritten && len(w.pages) == 0 {
+       // any net compression win. The comparison uses uncompressed sizes, 
which
+       // is only meaningful when pages are stored uncompressed: with a codec
+       // configured, the near-incompressible dict indices routinely beat PLAIN
+       // pages that compress well. The check therefore runs only for columns
+       // written without page compression, unless overridden per column with
+       // WithDictionaryCostFallbackFor.
+       if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
                rawSize := dictEnc.ObservedRawSize()
                encodedSize := dictEnc.EstimatedDataEncodedSize()
                dictSize := int64(dictEnc.DictEncodedSize())
@@ -2453,8 +2488,13 @@ func (w *FixedLenByteArrayColumnChunkWriter) 
checkDictionarySizeLimit() {
        // plus the encoded indices meet or exceed the raw input bytes, fall 
back
        // to PLAIN now and discard the dictionary — avoiding the 
mid-cardinality
        // case where a dict page stays in the file alongside PLAIN pages 
without
-       // any net compression win.
-       if !w.dictPageWritten && len(w.pages) == 0 {
+       // any net compression win. The comparison uses uncompressed sizes, 
which
+       // is only meaningful when pages are stored uncompressed: with a codec
+       // configured, the near-incompressible dict indices routinely beat PLAIN
+       // pages that compress well. The check therefore runs only for columns
+       // written without page compression, unless overridden per column with
+       // WithDictionaryCostFallbackFor.
+       if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
                rawSize := dictEnc.ObservedRawSize()
                encodedSize := dictEnc.EstimatedDataEncodedSize()
                dictSize := int64(dictEnc.DictEncodedSize())
diff --git a/parquet/file/column_writer_types.gen.go.tmpl 
b/parquet/file/column_writer_types.gen.go.tmpl
index 30c1e9e3..6988c468 100644
--- a/parquet/file/column_writer_types.gen.go.tmpl
+++ b/parquet/file/column_writer_types.gen.go.tmpl
@@ -574,8 +574,13 @@ func (w *{{.Name}}ColumnChunkWriter) 
checkDictionarySizeLimit() {
   // plus the encoded indices meet or exceed the raw input bytes, fall back
   // to PLAIN now and discard the dictionary — avoiding the mid-cardinality
   // case where a dict page stays in the file alongside PLAIN pages without
-  // any net compression win.
-  if !w.dictPageWritten && len(w.pages) == 0 {
+  // any net compression win. The comparison uses uncompressed sizes, which
+  // is only meaningful when pages are stored uncompressed: with a codec
+  // configured, the near-incompressible dict indices routinely beat PLAIN
+  // pages that compress well. The check therefore runs only for columns
+  // written without page compression, unless overridden per column with
+  // WithDictionaryCostFallbackFor.
+  if !w.dictPageWritten && len(w.pages) == 0 && 
w.props.DictionaryCostFallbackEnabledFor(w.descr.Path()) {
     rawSize := dictEnc.ObservedRawSize()
     encodedSize := dictEnc.EstimatedDataEncodedSize()
     dictSize := int64(dictEnc.DictEncodedSize())
diff --git a/parquet/file/dict_cost_fallback_test.go 
b/parquet/file/dict_cost_fallback_test.go
new file mode 100644
index 00000000..90cb0792
--- /dev/null
+++ b/parquet/file/dict_cost_fallback_test.go
@@ -0,0 +1,124 @@
+// 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.
+
+package file_test
+
+import (
+       "bytes"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/compress"
+       "github.com/apache/arrow-go/v18/parquet/file"
+       "github.com/apache/arrow-go/v18/parquet/internal/encoding"
+       "github.com/apache/arrow-go/v18/parquet/metadata"
+       "github.com/apache/arrow-go/v18/parquet/schema"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+// writeAllDistinctInt64Chunk writes one buffered row group with a single int64
+// column of all-distinct values, for which the dictionary plus its indices can
+// never beat the raw PLAIN size, and returns the resulting column chunk 
metadata.
+func writeAllDistinctInt64Chunk(t *testing.T, props *parquet.WriterProperties) 
*metadata.ColumnChunkMetaData {
+       t.Helper()
+
+       sink := encoding.NewBufferWriter(0, memory.DefaultAllocator)
+       fields := schema.FieldList{schema.NewInt64Node("col", 
parquet.Repetitions.Required, -1)}
+       sc, err := schema.NewGroupNode("schema", parquet.Repetitions.Required, 
fields, -1)
+       require.NoError(t, err)
+
+       writer := file.NewParquetWriter(sink, sc, file.WithWriterProps(props))
+       rgw := writer.AppendBufferedRowGroup()
+       cwr, err := rgw.Column(0)
+       require.NoError(t, err)
+       cw := cwr.(*file.Int64ColumnChunkWriter)
+
+       values := make([]int64, 1000)
+       for i := range values {
+               values[i] = int64(i)*1_000_003 + 7
+       }
+       _, err = cw.WriteBatch(values, nil, nil)
+       require.NoError(t, err)
+       require.NoError(t, rgw.Close())
+       require.NoError(t, writer.Close())
+
+       buffer := sink.Finish()
+       t.Cleanup(buffer.Release)
+       reader, err := file.NewParquetReader(bytes.NewReader(buffer.Bytes()))
+       require.NoError(t, err)
+       t.Cleanup(func() { reader.Close() })
+
+       require.EqualValues(t, 1, reader.NumRowGroups())
+       chunk, err := reader.RowGroup(0).MetaData().ColumnChunk(0)
+       require.NoError(t, err)
+       return chunk
+}
+
+func TestDictionaryCostFallback(t *testing.T) {
+       t.Run("uncompressed discards non-paying dictionary by default", func(t 
*testing.T) {
+               chunk := writeAllDistinctInt64Chunk(t, 
parquet.NewWriterProperties())
+               assert.False(t, chunk.HasDictionaryPage())
+       })
+
+       t.Run("compressed keeps dictionary by default", func(t *testing.T) {
+               chunk := writeAllDistinctInt64Chunk(t, 
parquet.NewWriterProperties(
+                       parquet.WithCompression(compress.Codecs.Snappy),
+               ))
+               assert.True(t, chunk.HasDictionaryPage())
+       })
+
+       t.Run("per-column compression keeps that column's dictionary", func(t 
*testing.T) {
+               chunk := writeAllDistinctInt64Chunk(t, 
parquet.NewWriterProperties(
+                       parquet.WithCompressionFor("col", 
compress.Codecs.Snappy),
+               ))
+               assert.True(t, chunk.HasDictionaryPage())
+       })
+
+       t.Run("per-column disable keeps an uncompressed column's dictionary", 
func(t *testing.T) {
+               chunk := writeAllDistinctInt64Chunk(t, 
parquet.NewWriterProperties(
+                       parquet.WithDictionaryFor("col", true),
+                       parquet.WithDictionaryCostFallbackFor("col", false),
+               ))
+               assert.True(t, chunk.HasDictionaryPage())
+       })
+
+       t.Run("per-column disable for another column leaves the fallback 
active", func(t *testing.T) {
+               chunk := writeAllDistinctInt64Chunk(t, 
parquet.NewWriterProperties(
+                       parquet.WithDictionaryFor("col", true),
+                       parquet.WithDictionaryCostFallbackFor("other", false),
+               ))
+               assert.False(t, chunk.HasDictionaryPage())
+       })
+
+       t.Run("per-column enable forces the check for a compressed column", 
func(t *testing.T) {
+               chunk := writeAllDistinctInt64Chunk(t, 
parquet.NewWriterProperties(
+                       parquet.WithCompression(compress.Codecs.Snappy),
+                       parquet.WithDictionaryCostFallbackFor("col", true),
+               ))
+               assert.False(t, chunk.HasDictionaryPage())
+       })
+
+       t.Run("page size limit fallback still applies when the cost fallback is 
off", func(t *testing.T) {
+               chunk := writeAllDistinctInt64Chunk(t, 
parquet.NewWriterProperties(
+                       parquet.WithDictionaryFor("col", true),
+                       parquet.WithDictionaryCostFallbackFor("col", false),
+                       parquet.WithDictionaryPageSizeLimit(512),
+               ))
+               assert.False(t, chunk.HasDictionaryPage())
+       })
+}
diff --git a/parquet/file/dict_fallback_repro_test.go 
b/parquet/file/dict_fallback_repro_test.go
index f82b3253..c6ed14cb 100644
--- a/parquet/file/dict_fallback_repro_test.go
+++ b/parquet/file/dict_fallback_repro_test.go
@@ -66,6 +66,11 @@ func runDictFallbackNoDictPageCheck(t *testing.T, version 
parquet.Version, codec
                dictPageSizeLimit: dictPageSizeLimit,
                dataPageSize:      dataPageSize,
                codec:             codec,
+               // The pre-first-page cost check is what discards the 
dictionary before
+               // any dict data page is cut here. It only runs by default for
+               // uncompressed columns, so force it for the compressed 
variants to keep
+               // exercising the orphan-discard path (parquet-mr parity) under 
a codec.
+               forceCostFallback: codec != compress.Codecs.Uncompressed,
        }
 
        knobsOn := knobs
@@ -243,6 +248,7 @@ type writerKnobs struct {
        dictPageSizeLimit int64
        dataPageSize      int64
        codec             compress.Compression
+       forceCostFallback bool
 }
 
 type pageInfo struct {
@@ -294,6 +300,9 @@ func writeByteArrayColumn(t *testing.T, values 
[]parquet.ByteArray, version parq
        if version == parquet.V2_LATEST {
                opts = append(opts, 
parquet.WithDataPageVersion(parquet.DataPageV2))
        }
+       if knobs.forceCostFallback {
+               opts = append(opts, parquet.WithDictionaryCostFallbackFor("v", 
true))
+       }
        props := parquet.NewWriterProperties(opts...)
 
        var buf bytes.Buffer
diff --git a/parquet/writer_properties.go b/parquet/writer_properties.go
index 4327bcf1..9cedc873 100644
--- a/parquet/writer_properties.go
+++ b/parquet/writer_properties.go
@@ -67,9 +67,14 @@ const (
 
 // ColumnProperties defines the encoding, codec, and so on for a given column.
 type ColumnProperties struct {
-       Encoding                   Encoding
-       Codec                      compress.Compression
-       DictionaryEnabled          bool
+       Encoding          Encoding
+       Codec             compress.Compression
+       DictionaryEnabled bool
+       // DictionaryCostFallback overrides the codec-based default of the
+       // pre-first-page cost-based dictionary fallback; nil keeps the default
+       // (enabled only for columns written without page compression). See
+       // WithDictionaryCostFallbackFor.
+       DictionaryCostFallback     *bool
        StatsEnabled               bool
        PageIndexEnabled           bool
        MaxStatsSize               int64
@@ -121,6 +126,7 @@ type writerPropConfig struct {
        codecs                     map[string]compress.Compression
        compressLevel              map[string]int
        dictEnabled                map[string]bool
+       dictCostFallback           map[string]bool
        statsEnabled               map[string]bool
        indexEnabled               map[string]bool
        bloomFilterNDVs            map[string]int64
@@ -167,6 +173,29 @@ func WithDictionaryPageSizeLimit(limit int64) 
WriterProperty {
        }
 }
 
+// WithDictionaryCostFallbackFor controls the cost-based dictionary fallback 
for the
+// given column path: before the first data page of a column chunk is cut, the 
writer
+// discards the dictionary and falls back to the plain encoding when the 
dictionary
+// plus the encoded indices are not smaller than the raw values (mirroring
+// parquet-mr's shouldFallBack). That comparison is done on uncompressed 
sizes, so by
+// default it only runs for columns written without page compression — for 
columns
+// with a codec configured the dictionary is kept, since near-incompressible
+// dictionary indices routinely beat plain pages that compress well. This 
option
+// overrides the codec-based default in either direction: false keeps an 
explicitly
+// requested dictionary for an uncompressed column, true forces the check for a
+// compressed one. The DictionaryPageSizeLimit fallback always stays in effect.
+func WithDictionaryCostFallbackFor(path string, enabled bool) WriterProperty {
+       return func(cfg *writerPropConfig) {
+               cfg.dictCostFallback[path] = enabled
+       }
+}
+
+// WithDictionaryCostFallbackPath is like WithDictionaryCostFallbackFor, but 
takes
+// a ColumnPath type.
+func WithDictionaryCostFallbackPath(path ColumnPath, enabled bool) 
WriterProperty {
+       return WithDictionaryCostFallbackFor(path.String(), enabled)
+}
+
 // WithBatchSize specifies the number of rows to use for batch writes to 
columns
 func WithBatchSize(batch int64) WriterProperty {
        return func(cfg *writerPropConfig) {
@@ -560,6 +589,7 @@ func NewWriterProperties(opts ...WriterProperty) 
*WriterProperties {
                codecs:                     
make(map[string]compress.Compression),
                compressLevel:              make(map[string]int),
                dictEnabled:                make(map[string]bool),
+               dictCostFallback:           make(map[string]bool),
                statsEnabled:               make(map[string]bool),
                indexEnabled:               make(map[string]bool),
                bloomFilterNDVs:            make(map[string]int64),
@@ -598,6 +628,10 @@ func NewWriterProperties(opts ...WriterProperty) 
*WriterProperties {
                get(key).DictionaryEnabled = value
        }
 
+       for key, value := range cfg.dictCostFallback {
+               get(key).DictionaryCostFallback = &value
+       }
+
        for key, value := range cfg.statsEnabled {
                get(key).StatsEnabled = value
        }
@@ -635,13 +669,14 @@ func (w *WriterProperties) FileEncryptionProperties() 
*FileEncryptionProperties
        return w.encryptionProps
 }
 
-func (w *WriterProperties) Allocator() memory.Allocator      { return w.mem }
-func (w *WriterProperties) CreatedBy() string                { return 
w.createdBy }
-func (w *WriterProperties) RootName() string                 { return 
w.rootName }
-func (w *WriterProperties) RootRepetition() Repetition       { return 
w.rootRepetition }
-func (w *WriterProperties) WriteBatchSize() int64            { return 
w.batchSize }
-func (w *WriterProperties) DataPageSize() int64              { return 
w.pageSize }
-func (w *WriterProperties) DictionaryPageSizeLimit() int64   { return 
w.dictPagesize }
+func (w *WriterProperties) Allocator() memory.Allocator    { return w.mem }
+func (w *WriterProperties) CreatedBy() string              { return 
w.createdBy }
+func (w *WriterProperties) RootName() string               { return w.rootName 
}
+func (w *WriterProperties) RootRepetition() Repetition     { return 
w.rootRepetition }
+func (w *WriterProperties) WriteBatchSize() int64          { return 
w.batchSize }
+func (w *WriterProperties) DataPageSize() int64            { return w.pageSize 
}
+func (w *WriterProperties) DictionaryPageSizeLimit() int64 { return 
w.dictPagesize }
+
 func (w *WriterProperties) Version() Version                 { return 
w.parquetVersion }
 func (w *WriterProperties) DataPageVersion() DataPageVersion { return 
w.dataPageVersion }
 func (w *WriterProperties) MaxRowGroupLength() int64         { return 
w.maxRowGroupLen }
@@ -732,6 +767,22 @@ func (w *WriterProperties) DictionaryEnabledFor(path 
string) bool {
        return w.defColumnProps.DictionaryEnabled
 }
 
+// DictionaryCostFallbackEnabledFor returns whether the pre-first-page 
cost-based
+// dictionary fallback applies for the given column path: an explicit 
per-column
+// WithDictionaryCostFallbackFor setting wins, otherwise the fallback is 
enabled
+// only when the column is written without page compression, since the cost
+// comparison is done on uncompressed sizes.
+func (w *WriterProperties) DictionaryCostFallbackEnabledFor(path string) bool {
+       p, ok := w.columnProps[path]
+       if !ok {
+               return w.defColumnProps.Codec == compress.Codecs.Uncompressed
+       }
+       if p.DictionaryCostFallback != nil {
+               return *p.DictionaryCostFallback
+       }
+       return p.Codec == compress.Codecs.Uncompressed
+}
+
 // DictionaryEnabledPath is the same as DictionaryEnabledFor but takes a 
ColumnPath object.
 func (w *WriterProperties) DictionaryEnabledPath(path ColumnPath) bool {
        return w.DictionaryEnabledFor(path.String())

Reply via email to