Repository: parquet-go/parquet-go (go)

Issue:
The minvalue of the column statistic incorrectly ignores empty strings
Hi. 
When I used the following statement `select from xxx where colume="-xxx"` to query, the written entries could not be queried. However, I changed it to `select from xxx where colume like "%-xxx"` and it could be queried. Finally, I found that this column recorded the wrong minimum value, which caused the query engine to skip the group when using =, while like is a global scan, so it can be found.

What ends up happening is that this code creates this bug

https://github.com/parquet-go/parquet-go/blob/main/writer.go#L1575
```
var existingMaxValue, existingMinValue Value

if c.columnChunk.MetaData.Statistics.MaxValue != nil && c.columnChunk.MetaData.Statistics.MinValue != nil {
        existingMaxValue = c.columnType.Kind().Value(c.columnChunk.MetaData.Statistics.MaxValue)
        existingMinValue = c.columnType.Kind().Value(c.columnChunk.MetaData.Statistics.MinValue)
}

if existingMaxValue.isNull() || c.columnType.Compare(maxValue, existingMaxValue) > 0 {
        buf := c.columnChunk.MetaData.Statistics.MaxValue[:0]
        c.columnChunk.MetaData.Statistics.MaxValue = maxValue.AppendBytes(buf)
}


if existingMinValue.isNull() || c.columnType.Compare(minValue, existingMinValue) < 0 {
        buf := c.columnChunk.MetaData.Statistics.MinValue[:0]
        // When minValue is an empty byte array (empty string), minValue.AppendBytes(buf) returns nil, causing c.columnChunk.MetaData.Statistics.MinValue to nil.
        // In the next loop, because c.columnChunk.MetaData.Statistics.MinValue is nil, existingMaxValue and existingMinValue are both nil Value, resulting in the current MinValue overwriting c.columnChunk.MetaData.Statistics.MinValue.
        c.columnChunk.MetaData.Statistics.MinValue = minValue.AppendBytes(buf)
}
```

This seems to be because the underlying data structure `minValue.AppendBytes(buf)` cannot distinguish between nil and an empty byte array. I only found that this is affected here, but I don’t know if there are similar problems elsewhere, so I am unable to fully fix this problem.

This is a very serious bug, which will cause the query engine to be unable to query data. Please fix this bug as quickly as possible!!!

Thank you so much !

Pull request:
The minvalue of the column statistic incorrectly ignores empty string
fix #130 

Patch:
diff --git a/writer.go b/writer.go
index d3526d4..e261bb6 100644
--- a/writer.go
+++ b/writer.go
@@ -1581,11 +1581,19 @@ func (c *writerColumn) recordPageStats(headerSize int32, header *format.PageHead
 
 			if existingMaxValue.isNull() || c.columnType.Compare(maxValue, existingMaxValue) > 0 {
 				buf := c.columnChunk.MetaData.Statistics.MaxValue[:0]
+				// if maxValue is empty string, c.columnChunk.MetaData.Statistics.MaxValue should be []bytes{}, but nil
+				if buf == nil && maxValue.Kind() == ByteArray && len(maxValue.ByteArray()) == 0 {
+					buf = make([]byte, 0)
+				}
 				c.columnChunk.MetaData.Statistics.MaxValue = maxValue.AppendBytes(buf)
 			}
 
 			if existingMinValue.isNull() || c.columnType.Compare(minValue, existingMinValue) < 0 {
 				buf := c.columnChunk.MetaData.Statistics.MinValue[:0]
+				// same as above
+				if buf == nil && minValue.Kind() == ByteArray && len(minValue.ByteArray()) == 0 {
+					buf = make([]byte, 0)
+				}
 				c.columnChunk.MetaData.Statistics.MinValue = minValue.AppendBytes(buf)
 			}
 		}


Test patch:
diff --git a/writer_test.go b/writer_test.go
index 60ead50..a770e30 100644
--- a/writer_test.go
+++ b/writer_test.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"os/exec"
 	"reflect"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -1203,3 +1204,38 @@ func TestSetKeyValueMetadataOverwritesExisting(t *testing.T) {
 		t.Errorf("expected %q, got %q", testValue, value)
 	}
 }
+
+func TestColumnMaxValueAndMinValue(t *testing.T) {
+	type testStruct struct {
+		A string `parquet:"a,plain"`
+	}
+
+	tests := make([]testStruct, 100)
+	tests[0] = testStruct{A: ""}
+	for i := 1; i < 100; i++ {
+		tests[i] = testStruct{A: strconv.Itoa(i)}
+	}
+	schema := parquet.SchemaOf(&testStruct{})
+	b := bytes.NewBuffer(nil)
+	config := parquet.DefaultWriterConfig()
+	config.PageBufferSize = 256
+	w := parquet.NewGenericWriter[testStruct](b, schema, config)
+	_, _ = w.Write(tests[0:50])
+	_, _ = w.Write(tests[50:100])
+	_ = w.Close()
+
+	f, err := parquet.OpenFile(bytes.NewReader(b.Bytes()), int64(b.Len()))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(f.RowGroups()) != 1 {
+		t.Fatalf("wrong number of row groups in parquet file: want=1 got=%d", f.RowGroups())
+	}
+	statistics := f.Metadata().RowGroups[0].Columns[0].MetaData.Statistics
+	if string(statistics.MinValue) != "" {
+		t.Fatalf("wrong min value of row groups in parquet file: want=' '() got=%s", string(statistics.MinValue))
+	}
+	if string(statistics.MaxValue) != "99" {
+		t.Fatalf("wrong max value of row groups in parquet file: want=99 got=%s", string(statistics.MaxValue))
+	}
+}