Repository: segmentio/parquet-go (go)

Issue:
Panic with generic reader and `any`
Hey, it says in the API docs:

> The type parameter T should be a map, struct, or any.

But I'm trying this with the following code:

```go
pFile, err := os.Open("./resources/pokemon_fans.parquet")
require.NoError(t, err)

pRdr := parquet.NewGenericReader[any](pFile)
```

And it panics with "panic: runtime error: invalid memory address or nil pointer dereference", pointing to: `github.com/segmentio/parquet-go/schema.go:126`. Using the now deprecated `NewReader` function seems to work as I'd expect.

Pull request description:
fix issue 252
Fixes #252 

The PR fixes the panic reported when using `parquet.GenericReader[any]`, as well as another panic occurring when using `parquet.GenericWriter[any]`, and adds examples of how to use these constructs.

Patch:
diff --git a/reader_go18.go b/reader_go18.go
index 80cd94a..bbbc225 100644
--- a/reader_go18.go
+++ b/reader_go18.go
@@ -32,21 +32,27 @@ func NewGenericReader[T any](input io.ReaderAt, options ...ReaderOption) *Generi
 		panic(err)
 	}
 
-	t := typeOf[T]()
-	if c.Schema == nil {
-		c.Schema = schemaOf(dereference(t))
-	}
-
 	f, err := openFile(input)
 	if err != nil {
 		panic(err)
 	}
 
+	rowGroup := fileRowGroupOf(f)
+
+	t := typeOf[T]()
+	if c.Schema == nil {
+		if t == nil {
+			c.Schema = rowGroup.Schema()
+		} else {
+			c.Schema = schemaOf(dereference(t))
+		}
+	}
+
 	r := &GenericReader[T]{
 		base: Reader{
 			file: reader{
 				schema:   c.Schema,
-				rowGroup: fileRowGroupOf(f),
+				rowGroup: rowGroup,
 			},
 		},
 	}
@@ -68,7 +74,11 @@ func NewGenericRowGroupReader[T any](rowGroup RowGroup, options ...ReaderOption)
 
 	t := typeOf[T]()
 	if c.Schema == nil {
-		c.Schema = schemaOf(dereference(t))
+		if t == nil {
+			c.Schema = rowGroup.Schema()
+		} else {
+			c.Schema = schemaOf(dereference(t))
+		}
 	}
 
 	r := &GenericReader[T]{
@@ -151,6 +161,9 @@ var (
 type readFunc[T any] func(*GenericReader[T], []T) (int, error)
 
 func readFuncOf[T any](t reflect.Type, schema *Schema) readFunc[T] {
+	if t == nil {
+		return (*GenericReader[T]).readRows
+	}
 	switch t.Kind() {
 	case reflect.Interface, reflect.Map:
 		return (*GenericReader[T]).readRows
diff --git a/row.go b/row.go
index d135fb8..f87df25 100644
--- a/row.go
+++ b/row.go
@@ -663,10 +663,36 @@ func reconstructFuncOfGroup(columnIndex int16, node Node) (int16, reconstructFun
 	return columnIndex, func(value reflect.Value, levels levels, row Row) (Row, error) {
 		var err error
 
-		for i, f := range funcs {
-			if row, err = f(fields[i].Value(value), levels, row); err != nil {
-				err = fmt.Errorf("%s → %w", fields[i].Name(), err)
-				break
+		if value.Kind() == reflect.Interface && value.IsNil() {
+			value.Set(reflect.MakeMap(reflect.TypeOf((map[string]interface{})(nil))))
+			value = value.Elem()
+		}
+
+		if value.Kind() == reflect.Map {
+			elemType := value.Type().Elem()
+			name := reflect.New(reflect.TypeOf("")).Elem()
+			elem := reflect.New(elemType).Elem()
+			zero := reflect.Zero(elemType)
+
+			if value.Len() > 0 {
+				value.Set(reflect.MakeMap(value.Type()))
+			}
+
+			for i, f := range funcs {
+				name.SetString(fields[i].Name())
+				if row, err = f(elem, levels, row); err != nil {
+					err = fmt.Errorf("%s → %w", name, err)
+					break
+				}
+				value.SetMapIndex(name, elem)
+				elem.Set(zero)
+			}
+		} else {
+			for i, f := range funcs {
+				if row, err = f(fields[i].Value(value), levels, row); err != nil {
+					err = fmt.Errorf("%s → %w", fields[i].Name(), err)
+					break
+				}
 			}
 		}
 
diff --git a/value.go b/value.go
index 89eb621..81516d5 100644
--- a/value.go
+++ b/value.go
@@ -671,7 +671,7 @@ func assignValue(dst reflect.Value, src Value) error {
 				return nil
 			}
 		default:
-			val = reflect.ValueOf(v)
+			val = reflect.ValueOf(string(v))
 		}
 
 	case FixedLenByteArray:
diff --git a/writer_go18.go b/writer_go18.go
index ca6e01b..8fe9834 100644
--- a/writer_go18.go
+++ b/writer_go18.go
@@ -70,9 +70,11 @@ func NewGenericWriter[T any](output io.Writer, options ...WriterOption) *Generic
 		panic(err)
 	}
 
+	schema := config.Schema
 	t := typeOf[T]()
-	schema := schemaOf(dereference(t))
-	if config.Schema == nil {
+
+	if schema == nil && t != nil {
+		schema = schemaOf(dereference(t))
 		config.Schema = schema
 	}
 
@@ -90,6 +92,9 @@ func NewGenericWriter[T any](output io.Writer, options ...WriterOption) *Generic
 type writeFunc[T any] func(*GenericWriter[T], []T) (int, error)
 
 func writeFuncOf[T any](t reflect.Type, schema *Schema) writeFunc[T] {
+	if t == nil {
+		return (*GenericWriter[T]).writeAny
+	}
 	switch t.Kind() {
 	case reflect.Interface, reflect.Map:
 		return (*GenericWriter[T]).writeRows
@@ -189,6 +194,16 @@ func (w *GenericWriter[T]) writeRows(rows []T) (int, error) {
 	return w.base.WriteRows(w.base.rowbuf)
 }
 
+func (w *GenericWriter[T]) writeAny(rows []T) (n int, err error) {
+	for i := range rows {
+		if err = w.base.Write(rows[i]); err != nil {
+			return n, err
+		}
+		n++
+	}
+	return n, nil
+}
+
 var (
 	_ RowWriterWithSchema = (*GenericWriter[any])(nil)
 	_ RowReaderFrom       = (*GenericWriter[any])(nil)


Test patch:
diff --git a/parquet_go18_test.go b/parquet_go18_test.go
index cf658b3..a974e8d 100644
--- a/parquet_go18_test.go
+++ b/parquet_go18_test.go
@@ -3,6 +3,7 @@
 package parquet_test
 
 import (
+	"bytes"
 	"fmt"
 	"log"
 
@@ -48,3 +49,70 @@ func ExampleWriteFile() {
 
 	// Output:
 }
+
+func ExampleRead_any() {
+	type Row struct{ FirstName, LastName string }
+
+	buf := new(bytes.Buffer)
+	err := parquet.Write(buf, []Row{
+		{FirstName: "Luke", LastName: "Skywalker"},
+		{FirstName: "Han", LastName: "Solo"},
+		{FirstName: "R2", LastName: "D2"},
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	file := bytes.NewReader(buf.Bytes())
+
+	rows, err := parquet.Read[any](file, file.Size())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for _, row := range rows {
+		fmt.Printf("%q\n", row)
+	}
+
+	// Output:
+	// map["FirstName":"Luke" "LastName":"Skywalker"]
+	// map["FirstName":"Han" "LastName":"Solo"]
+	// map["FirstName":"R2" "LastName":"D2"]
+}
+
+func ExampleWrite_any() {
+	schema := parquet.SchemaOf(struct {
+		FirstName string
+		LastName  string
+	}{})
+
+	buf := new(bytes.Buffer)
+	err := parquet.Write[any](
+		buf,
+		[]any{
+			map[string]string{"FirstName": "Luke", "LastName": "Skywalker"},
+			map[string]string{"FirstName": "Han", "LastName": "Solo"},
+			map[string]string{"FirstName": "R2", "LastName": "D2"},
+		},
+		schema,
+	)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	file := bytes.NewReader(buf.Bytes())
+
+	rows, err := parquet.Read[any](file, file.Size())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for _, row := range rows {
+		fmt.Printf("%q\n", row)
+	}
+
+	// Output:
+	// map["FirstName":"Luke" "LastName":"Skywalker"]
+	// map["FirstName":"Han" "LastName":"Solo"]
+	// map["FirstName":"R2" "LastName":"D2"]
+}