Repository: polarsignals/frostdb
Pull request #659 (2023-12-26 07:19:15)

Move (*Build[T]) to internal package
Reasons

- It is useful in tests. Tying it to dynparquet introduces cyclic dependency
- It is not directly related to dynparquet
- Discourage direct use and promote `GenericTable`

I got tired constructing arrow records in tests, its messy and error prone. This way we can dog food the implementation fostering evolution and more use case coverage.

I had to move `dynparquet.Sample` to `samples` package in order to move the tests. 


Patch:
diff --git a/dynparquet/example.go b/dynparquet/example.go
index 6493514..8f6df4b 100644
--- a/dynparquet/example.go
+++ b/dynparquet/example.go
@@ -1,32 +1,27 @@
 package dynparquet
 
 import (
-	"fmt"
-	"sort"
-	"testing"
-
-	"github.com/apache/arrow/go/v14/arrow"
-	"github.com/apache/arrow/go/v14/arrow/array"
-	"github.com/apache/arrow/go/v14/arrow/memory"
-	"github.com/google/uuid"
 	"github.com/parquet-go/parquet-go"
-	"google.golang.org/protobuf/proto"
 
-	schemapb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/schema/v1alpha1"
-	schemav2pb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/schema/v1alpha2"
+	"github.com/polarsignals/frostdb/samples"
 )
 
-type Sample struct {
-	ExampleType string            `frostdb:"example_type,rle_dict,asc(0)"`
-	Labels      map[string]string `frostdb:"labels,rle_dict,asc(1),null_first"`
-	Stacktrace  []uuid.UUID       `frostdb:"stacktrace,rle_dict,asc(3),null_first"`
-	Timestamp   int64             `frostdb:"timestamp,asc(2)"`
-	Value       int64             `frostdb:"value"`
-}
+type (
+	Samples = samples.Samples
+	Sample  = samples.Sample
+)
 
-type Samples []Sample
+var (
+	SampleDefinition          = samples.SampleDefinition
+	NewTestSamples            = samples.NewTestSamples
+	PrehashedSampleDefinition = samples.PrehashedSampleDefinition
+	SampleDefinitionWithFloat = samples.SampleDefinitionWithFloat
+	NewNestedSampleSchema     = samples.NewNestedSampleSchema
+	LabelColumn               = samples.LabelColumn
+	GenerateTestSamples       = samples.GenerateTestSamples
+)
 
-func (s Samples) ToBuffer(schema *Schema) (*Buffer, error) {
+func ToBuffer(s Samples, schema *Schema) (*Buffer, error) {
 	names := s.SampleLabelNames()
 
 	pb, err := schema.NewBuffer(map[string][]string{
@@ -49,276 +44,6 @@ func (s Samples) ToBuffer(schema *Schema) (*Buffer, error) {
 	return pb, nil
 }
 
-func (s Samples) ToRecord() (arrow.Record, error) {
-	fields := []arrow.Field{
-		{
-			Name: "example_type",
-			Type: &arrow.DictionaryType{
-				IndexType: &arrow.Int32Type{},
-				ValueType: &arrow.BinaryType{},
-			},
-		},
-	}
-	seen := map[string]struct{}{}
-	for _, smpl := range s {
-		for lbl := range smpl.Labels {
-			if _, ok := seen[lbl]; ok {
-				continue
-			}
-			seen[lbl] = struct{}{}
-			fields = append(fields,
-				arrow.Field{
-					Name:     "labels." + lbl,
-					Nullable: true,
-					Type: &arrow.DictionaryType{
-						IndexType: &arrow.Int32Type{},
-						ValueType: &arrow.BinaryType{},
-					},
-				},
-			)
-		}
-	}
-	// Sort the labels fields since they're a map
-	sort.Slice(fields, func(i, j int) bool {
-		return fields[i].Name < fields[j].Name
-	})
-	fields = append(fields, arrow.Field{
-		Name: "stacktrace",
-		Type: &arrow.DictionaryType{
-			IndexType: &arrow.Int32Type{},
-			ValueType: &arrow.BinaryType{},
-		},
-	},
-	)
-	fields = append(fields, arrow.Field{Name: "timestamp", Type: arrow.PrimitiveTypes.Int64})
-	fields = append(fields, arrow.Field{Name: "value", Type: arrow.PrimitiveTypes.Int64})
-	schema := arrow.NewSchema(fields, nil)
-
-	bld := array.NewRecordBuilder(memory.NewGoAllocator(), schema)
-	defer bld.Release()
-
-	numLabels := schema.NumFields() - 4
-
-	for _, sample := range s {
-		if err := bld.Field(0).(*array.BinaryDictionaryBuilder).Append([]byte(sample.ExampleType)); err != nil {
-			return nil, fmt.Errorf("failed to append example type: %v", err)
-		}
-		for i := 0; i < numLabels; i++ {
-			found := false
-			for lbl, value := range sample.Labels {
-				if "labels."+lbl == schema.Field(i+1).Name {
-					if err := bld.Field(i + 1).(*array.BinaryDictionaryBuilder).Append([]byte(value)); err != nil {
-						return nil, fmt.Errorf("failed to append value: %v", err)
-					}
-					found = true
-					break
-				}
-			}
-
-			if !found {
-				bld.Field(i + 1).AppendNull()
-			}
-		}
-		if err := bld.Field(1 + numLabels).(*array.BinaryDictionaryBuilder).Append(ExtractLocationIDs(sample.Stacktrace)); err != nil {
-			return nil, fmt.Errorf("failed to append stacktrace: %v", err)
-		}
-		bld.Field(2 + numLabels).(*array.Int64Builder).Append(sample.Timestamp)
-		bld.Field(3 + numLabels).(*array.Int64Builder).Append(sample.Value)
-	}
-
-	return bld.NewRecord(), nil
-}
-
-func (s Samples) SampleLabelNames() []string {
-	names := []string{}
-	seen := map[string]struct{}{}
-
-	for _, sample := range s {
-		for label := range sample.Labels {
-			if _, ok := seen[label]; !ok {
-				names = append(names, label)
-				seen[label] = struct{}{}
-			}
-		}
-	}
-	sort.Strings(names)
-
-	return names
-}
-
-func (s Sample) ToParquetRow(labelNames []string) parquet.Row {
-	// The order of these appends is important. Parquet values must be in the
-	// order of the schema and the schema orders columns by their names.
-
-	nameNumber := len(labelNames)
-	labelLen := len(s.Labels)
-	row := make([]parquet.Value, 0, nameNumber+3)
-
-	row = append(row, parquet.ValueOf(s.ExampleType).Level(0, 0, 0))
-
-	i, j := 0, 0
-	for i < nameNumber {
-		if value, ok := s.Labels[labelNames[i]]; ok {
-			row = append(row, parquet.ValueOf(value).Level(0, 1, i+1))
-			i++
-			j++
-			if j >= labelLen {
-				for ; i < nameNumber; i++ {
-					row = append(row, parquet.ValueOf(nil).Level(0, 0, i+1))
-				}
-				break
-			}
-		} else {
-			row = append(row, parquet.ValueOf(nil).Level(0, 0, i+1))
-			i++
-		}
-	}
-	row = append(row, parquet.ValueOf(ExtractLocationIDs(s.Stacktrace)).Level(0, 0, nameNumber+1))
-	row = append(row, parquet.ValueOf(s.Timestamp).Level(0, 0, nameNumber+2))
-	row = append(row, parquet.ValueOf(s.Value).Level(0, 0, nameNumber+3))
-
-	return row
-}
-
-func ExtractLocationIDs(locs []uuid.UUID) []byte {
-	b := make([]byte, len(locs)*16) // UUID are 16 bytes thus multiply by 16
-	index := 0
-	for i := len(locs) - 1; i >= 0; i-- {
-		copy(b[index:index+16], locs[i][:])
-		index += 16
-	}
-	return b
-}
-
-func PrehashedSampleDefinition() *schemapb.Schema {
-	return &schemapb.Schema{
-		Name: "test",
-		Columns: []*schemapb.Column{{
-			Name: "example_type",
-			StorageLayout: &schemapb.StorageLayout{
-				Type:     schemapb.StorageLayout_TYPE_STRING,
-				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
-			},
-			Dynamic: false,
-		}, {
-			Name: "labels",
-			StorageLayout: &schemapb.StorageLayout{
-				Type:     schemapb.StorageLayout_TYPE_STRING,
-				Nullable: true,
-				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
-			},
-			Dynamic: true,
-			Prehash: true,
-		}, {
-			Name: "stacktrace",
-			StorageLayout: &schemapb.StorageLayout{
-				Type:     schemapb.StorageLayout_TYPE_STRING,
-				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
-			},
-			Dynamic: false,
-			Prehash: true,
-		}, {
-			Name: "timestamp",
-			StorageLayout: &schemapb.StorageLayout{
-				Type: schemapb.StorageLayout_TYPE_INT64,
-			},
-			Dynamic: false,
-		}, {
-			Name: "value",
-			StorageLayout: &schemapb.StorageLayout{
-				Type: schemapb.StorageLayout_TYPE_INT64,
-			},
-			Dynamic: false,
-		}},
-		SortingColumns: []*schemapb.SortingColumn{{
-			Name:      "example_type",
-			Direction: schemapb.SortingColumn_DIRECTION_ASCENDING,
-		}, {
-			Name:       "labels",
-			Direction:  schemapb.SortingColumn_DIRECTION_ASCENDING,
-			NullsFirst: true,
-		}, {
-			Name:      "timestamp",
-			Direction: schemapb.SortingColumn_DIRECTION_ASCENDING,
-		}, {
-			Name:       "stacktrace",
-			Direction:  schemapb.SortingColumn_DIRECTION_ASCENDING,
-			NullsFirst: true,
-		}},
-	}
-}
-
-func SampleDefinition() *schemapb.Schema {
-	return &schemapb.Schema{
-		Name: "test",
-		Columns: []*schemapb.Column{{
-			Name: "example_type",
-			StorageLayout: &schemapb.StorageLayout{
-				Type:     schemapb.StorageLayout_TYPE_STRING,
-				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
-			},
-			Dynamic: false,
-		}, {
-			Name: "labels",
-			StorageLayout: &schemapb.StorageLayout{
-				Type:     schemapb.StorageLayout_TYPE_STRING,
-				Nullable: true,
-				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
-			},
-			Dynamic: true,
-		}, {
-			Name: "stacktrace",
-			StorageLayout: &schemapb.StorageLayout{
-				Type:     schemapb.StorageLayout_TYPE_STRING,
-				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
-			},
-			Dynamic: false,
-		}, {
-			Name: "timestamp",
-			StorageLayout: &schemapb.StorageLayout{
-				Type: schemapb.StorageLayout_TYPE_INT64,
-			},
-			Dynamic: false,
-		}, {
-			Name: "value",
-			StorageLayout: &schemapb.StorageLayout{
-				Type: schemapb.StorageLayout_TYPE_INT64,
-			},
-			Dynamic: false,
-		}},
-		SortingColumns: []*schemapb.SortingColumn{{
-			Name:      "example_type",
-			Direction: schemapb.SortingColumn_DIRECTION_ASCENDING,
-		}, {
-			Name:       "labels",
-			Direction:  schemapb.SortingColumn_DIRECTION_ASCENDING,
-			NullsFirst: true,
-		}, {
-			Name:      "timestamp",
-			Direction: schemapb.SortingColumn_DIRECTION_ASCENDING,
-		}, {
-			Name:       "stacktrace",
-			Direction:  schemapb.SortingColumn_DIRECTION_ASCENDING,
-			NullsFirst: true,
-		}},
-	}
-}
-
-// Adds a float column to the SampleDefinition to be able to test
-// aggregations with float values.
-func SampleDefinitionWithFloat() *schemapb.Schema {
-	sample := SampleDefinition()
-	sample.Columns = append(sample.Columns, &schemapb.Column{
-		Name: "floatvalue",
-		StorageLayout: &schemapb.StorageLayout{
-			Type:     schemapb.StorageLayout_TYPE_DOUBLE,
-			Nullable: true,
-		},
-		Dynamic: false,
-	})
-	return sample
-}
-
 func NewSampleSchema() *Schema {
 	s, err := SchemaFromDefinition(SampleDefinition())
 	if err != nil {
@@ -326,160 +51,3 @@ func NewSampleSchema() *Schema {
 	}
 	return s
 }
-
-func NewTestSamples() Samples {
-	return Samples{
-		{
-			ExampleType: "cpu",
-			Labels: map[string]string{
-				"node": "test3",
-			},
-			Stacktrace: []uuid.UUID{
-				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
-				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2},
-			},
-			Timestamp: 2,
-			Value:     5,
-		}, {
-			ExampleType: "cpu",
-			Labels: map[string]string{
-				"namespace": "default",
-				"pod":       "test1",
-			},
-			Stacktrace: []uuid.UUID{
-				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
-				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2},
-			},
-			Timestamp: 2,
-			Value:     3,
-		}, {
-			ExampleType: "cpu",
-			Labels: map[string]string{
-				"container": "test2",
-				"namespace": "default",
-			},
-			Stacktrace: []uuid.UUID{
-				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
-				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2},
-			},
-			Timestamp: 2,
-			Value:     3,
-		},
-	}
-}
-
-func GenerateTestSamples(n int) Samples {
-	s := Samples{}
-	for i := 0; i < n; i++ {
-		s = append(s,
-			Sample{
-				ExampleType: "cpu",
-				Labels: map[string]string{
-					"node": "test3",
-				},
-				Stacktrace: []uuid.UUID{
-					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
-					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2},
-				},
-				Timestamp: int64(i),
-				Value:     int64(i),
-			})
-	}
-	return s
-}
-
-func NestedListDef(name string, layout *schemav2pb.StorageLayout) *schemav2pb.Node_Group {
-	return &schemav2pb.Node_Group{
-		Group: &schemav2pb.Group{
-			Name: name,
-			Nodes: []*schemav2pb.Node{ // NOTE that this nested group structure for a list is for backwards compatability: https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists
-				{
-					Type: &schemav2pb.Node_Group{
-						Group: &schemav2pb.Group{
-							Name:     "list",
-							Repeated: true,
-							Nodes: []*schemav2pb.Node{
-								{
-									Type: &schemav2pb.Node_Leaf{
-										Leaf: &schemav2pb.Leaf{
-											Name:          "element",
-											StorageLayout: layout,
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-}
-
-func LabelColumn(name string) *schemav2pb.Node {
-	return &schemav2pb.Node{
-		Type: &schemav2pb.Node_Group{
-			Group: &schemav2pb.Group{
-				Name: "labels",
-				Nodes: []*schemav2pb.Node{
-					{
-						Type: &schemav2pb.Node_Leaf{
-							Leaf: &schemav2pb.Leaf{
-								Name: name,
-								StorageLayout: &schemav2pb.StorageLayout{
-									Type:     schemav2pb.StorageLayout_TYPE_STRING,
-									Nullable: true,
-									Encoding: schemav2pb.StorageLayout_ENCODING_RLE_DICTIONARY,
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-}
-
-func NewNestedSampleSchema(t testing.TB) proto.Message {
-	t.Helper()
-	return &schemav2pb.Schema{
-		Root: &schemav2pb.Group{
-			Name: "nested",
-			Nodes: []*schemav2pb.Node{
-				{
-					Type: &schemav2pb.Node_Group{
-						Group: &schemav2pb.Group{
-							Name:  "labels",
-							Nodes: []*schemav2pb.Node{},
-						},
-					},
-				},
-				{
-					Type: NestedListDef("timestamps", &schemav2pb.StorageLayout{
-						Type:     schemav2pb.StorageLayout_TYPE_INT64,
-						Nullable: true,
-						Encoding: schemav2pb.StorageLayout_ENCODING_RLE_DICTIONARY,
-					}),
-				},
-				{
-					Type: NestedListDef("values", &schemav2pb.StorageLayout{
-						Type:     schemav2pb.StorageLayout_TYPE_INT64,
-						Nullable: true,
-						Encoding: schemav2pb.StorageLayout_ENCODING_RLE_DICTIONARY,
-					}),
-				},
-			},
-		},
-		SortingColumns: []*schemav2pb.SortingColumn{
-			{
-				Path:       "labels",
-				Direction:  schemav2pb.SortingColumn_DIRECTION_ASCENDING,
-				NullsFirst: true,
-			},
-			{
-				Path:      "timestamp",
-				Direction: schemav2pb.SortingColumn_DIRECTION_ASCENDING,
-			},
-		},
-	}
-}
diff --git a/dynparquet/record_builder.go b/internal/records/record_builder.go
similarity index 97%
rename from dynparquet/record_builder.go
rename to internal/records/record_builder.go
index 8ff1641..8540702 100644
--- a/dynparquet/record_builder.go
+++ b/internal/records/record_builder.go
@@ -1,7 +1,8 @@
-package dynparquet
+package records
 
 import (
 	"reflect"
+	"regexp"
 	"slices"
 	"sort"
 	"strconv"
@@ -13,6 +14,7 @@ import (
 	"github.com/google/uuid"
 
 	schemapb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/schema/v1alpha1"
+	"github.com/polarsignals/frostdb/samples"
 )
 
 const (
@@ -737,7 +739,7 @@ func newUUIDSliceField(mem memory.Allocator, name string) (f *fieldBuilderFunc)
 	}
 	bd := b.(*array.BinaryDictionaryBuilder)
 	f.buildFunc = func(v reflect.Value) error {
-		return bd.Append(ExtractLocationIDs(v.Interface().([]uuid.UUID)))
+		return bd.Append(samples.ExtractLocationIDs(v.Interface().([]uuid.UUID)))
 	}
 	return
 }
@@ -794,3 +796,14 @@ var uuidSliceType = reflect.TypeOf([]uuid.UUID{})
 func isUUIDSlice(typ reflect.Type) bool {
 	return typ.AssignableTo(uuidSliceType)
 }
+
+var (
+	matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
+	matchAllCap   = regexp.MustCompile("([a-z0-9])([A-Z])")
+)
+
+func ToSnakeCase(str string) string {
+	snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
+	snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
+	return strings.ToLower(snake)
+}
diff --git a/samples/example.go b/samples/example.go
new file mode 100644
index 0000000..0dd862e
--- /dev/null
+++ b/samples/example.go
@@ -0,0 +1,454 @@
+package samples
+
+import (
+	"fmt"
+	"sort"
+	"testing"
+
+	"github.com/apache/arrow/go/v14/arrow"
+	"github.com/apache/arrow/go/v14/arrow/array"
+	"github.com/apache/arrow/go/v14/arrow/memory"
+	"github.com/google/uuid"
+	"github.com/parquet-go/parquet-go"
+	"google.golang.org/protobuf/proto"
+
+	schemapb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/schema/v1alpha1"
+	schemav2pb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/schema/v1alpha2"
+)
+
+type Sample struct {
+	ExampleType string            `frostdb:"example_type,rle_dict,asc(0)"`
+	Labels      map[string]string `frostdb:"labels,rle_dict,asc(1),null_first"`
+	Stacktrace  []uuid.UUID       `frostdb:"stacktrace,rle_dict,asc(3),null_first"`
+	Timestamp   int64             `frostdb:"timestamp,asc(2)"`
+	Value       int64             `frostdb:"value"`
+}
+
+type Samples []Sample
+
+func (s Samples) ToRecord() (arrow.Record, error) {
+	fields := []arrow.Field{
+		{
+			Name: "example_type",
+			Type: &arrow.DictionaryType{
+				IndexType: &arrow.Int32Type{},
+				ValueType: &arrow.BinaryType{},
+			},
+		},
+	}
+	seen := map[string]struct{}{}
+	for _, smpl := range s {
+		for lbl := range smpl.Labels {
+			if _, ok := seen[lbl]; ok {
+				continue
+			}
+			seen[lbl] = struct{}{}
+			fields = append(fields,
+				arrow.Field{
+					Name:     "labels." + lbl,
+					Nullable: true,
+					Type: &arrow.DictionaryType{
+						IndexType: &arrow.Int32Type{},
+						ValueType: &arrow.BinaryType{},
+					},
+				},
+			)
+		}
+	}
+	// Sort the labels fields since they're a map
+	sort.Slice(fields, func(i, j int) bool {
+		return fields[i].Name < fields[j].Name
+	})
+	fields = append(fields, arrow.Field{
+		Name: "stacktrace",
+		Type: &arrow.DictionaryType{
+			IndexType: &arrow.Int32Type{},
+			ValueType: &arrow.BinaryType{},
+		},
+	},
+	)
+	fields = append(fields, arrow.Field{Name: "timestamp", Type: arrow.PrimitiveTypes.Int64})
+	fields = append(fields, arrow.Field{Name: "value", Type: arrow.PrimitiveTypes.Int64})
+	schema := arrow.NewSchema(fields, nil)
+
+	bld := array.NewRecordBuilder(memory.NewGoAllocator(), schema)
+	defer bld.Release()
+
+	numLabels := schema.NumFields() - 4
+
+	for _, sample := range s {
+		if err := bld.Field(0).(*array.BinaryDictionaryBuilder).Append([]byte(sample.ExampleType)); err != nil {
+			return nil, fmt.Errorf("failed to append example type: %v", err)
+		}
+		for i := 0; i < numLabels; i++ {
+			found := false
+			for lbl, value := range sample.Labels {
+				if "labels."+lbl == schema.Field(i+1).Name {
+					if err := bld.Field(i + 1).(*array.BinaryDictionaryBuilder).Append([]byte(value)); err != nil {
+						return nil, fmt.Errorf("failed to append value: %v", err)
+					}
+					found = true
+					break
+				}
+			}
+
+			if !found {
+				bld.Field(i + 1).AppendNull()
+			}
+		}
+		if err := bld.Field(1 + numLabels).(*array.BinaryDictionaryBuilder).Append(ExtractLocationIDs(sample.Stacktrace)); err != nil {
+			return nil, fmt.Errorf("failed to append stacktrace: %v", err)
+		}
+		bld.Field(2 + numLabels).(*array.Int64Builder).Append(sample.Timestamp)
+		bld.Field(3 + numLabels).(*array.Int64Builder).Append(sample.Value)
+	}
+
+	return bld.NewRecord(), nil
+}
+
+func (s Samples) SampleLabelNames() []string {
+	names := []string{}
+	seen := map[string]struct{}{}
+
+	for _, sample := range s {
+		for label := range sample.Labels {
+			if _, ok := seen[label]; !ok {
+				names = append(names, label)
+				seen[label] = struct{}{}
+			}
+		}
+	}
+	sort.Strings(names)
+
+	return names
+}
+
+func (s Sample) ToParquetRow(labelNames []string) parquet.Row {
+	// The order of these appends is important. Parquet values must be in the
+	// order of the schema and the schema orders columns by their names.
+
+	nameNumber := len(labelNames)
+	labelLen := len(s.Labels)
+	row := make([]parquet.Value, 0, nameNumber+3)
+
+	row = append(row, parquet.ValueOf(s.ExampleType).Level(0, 0, 0))
+
+	i, j := 0, 0
+	for i < nameNumber {
+		if value, ok := s.Labels[labelNames[i]]; ok {
+			row = append(row, parquet.ValueOf(value).Level(0, 1, i+1))
+			i++
+			j++
+			if j >= labelLen {
+				for ; i < nameNumber; i++ {
+					row = append(row, parquet.ValueOf(nil).Level(0, 0, i+1))
+				}
+				break
+			}
+		} else {
+			row = append(row, parquet.ValueOf(nil).Level(0, 0, i+1))
+			i++
+		}
+	}
+	row = append(row, parquet.ValueOf(ExtractLocationIDs(s.Stacktrace)).Level(0, 0, nameNumber+1))
+	row = append(row, parquet.ValueOf(s.Timestamp).Level(0, 0, nameNumber+2))
+	row = append(row, parquet.ValueOf(s.Value).Level(0, 0, nameNumber+3))
+
+	return row
+}
+
+func ExtractLocationIDs(locs []uuid.UUID) []byte {
+	b := make([]byte, len(locs)*16) // UUID are 16 bytes thus multiply by 16
+	index := 0
+	for i := len(locs) - 1; i >= 0; i-- {
+		copy(b[index:index+16], locs[i][:])
+		index += 16
+	}
+	return b
+}
+
+func PrehashedSampleDefinition() *schemapb.Schema {
+	return &schemapb.Schema{
+		Name: "test",
+		Columns: []*schemapb.Column{{
+			Name: "example_type",
+			StorageLayout: &schemapb.StorageLayout{
+				Type:     schemapb.StorageLayout_TYPE_STRING,
+				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
+			},
+			Dynamic: false,
+		}, {
+			Name: "labels",
+			StorageLayout: &schemapb.StorageLayout{
+				Type:     schemapb.StorageLayout_TYPE_STRING,
+				Nullable: true,
+				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
+			},
+			Dynamic: true,
+			Prehash: true,
+		}, {
+			Name: "stacktrace",
+			StorageLayout: &schemapb.StorageLayout{
+				Type:     schemapb.StorageLayout_TYPE_STRING,
+				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
+			},
+			Dynamic: false,
+			Prehash: true,
+		}, {
+			Name: "timestamp",
+			StorageLayout: &schemapb.StorageLayout{
+				Type: schemapb.StorageLayout_TYPE_INT64,
+			},
+			Dynamic: false,
+		}, {
+			Name: "value",
+			StorageLayout: &schemapb.StorageLayout{
+				Type: schemapb.StorageLayout_TYPE_INT64,
+			},
+			Dynamic: false,
+		}},
+		SortingColumns: []*schemapb.SortingColumn{{
+			Name:      "example_type",
+			Direction: schemapb.SortingColumn_DIRECTION_ASCENDING,
+		}, {
+			Name:       "labels",
+			Direction:  schemapb.SortingColumn_DIRECTION_ASCENDING,
+			NullsFirst: true,
+		}, {
+			Name:      "timestamp",
+			Direction: schemapb.SortingColumn_DIRECTION_ASCENDING,
+		}, {
+			Name:       "stacktrace",
+			Direction:  schemapb.SortingColumn_DIRECTION_ASCENDING,
+			NullsFirst: true,
+		}},
+	}
+}
+
+func SampleDefinition() *schemapb.Schema {
+	return &schemapb.Schema{
+		Name: "test",
+		Columns: []*schemapb.Column{{
+			Name: "example_type",
+			StorageLayout: &schemapb.StorageLayout{
+				Type:     schemapb.StorageLayout_TYPE_STRING,
+				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
+			},
+			Dynamic: false,
+		}, {
+			Name: "labels",
+			StorageLayout: &schemapb.StorageLayout{
+				Type:     schemapb.StorageLayout_TYPE_STRING,
+				Nullable: true,
+				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
+			},
+			Dynamic: true,
+		}, {
+			Name: "stacktrace",
+			StorageLayout: &schemapb.StorageLayout{
+				Type:     schemapb.StorageLayout_TYPE_STRING,
+				Encoding: schemapb.StorageLayout_ENCODING_RLE_DICTIONARY,
+			},
+			Dynamic: false,
+		}, {
+			Name: "timestamp",
+			StorageLayout: &schemapb.StorageLayout{
+				Type: schemapb.StorageLayout_TYPE_INT64,
+			},
+			Dynamic: false,
+		}, {
+			Name: "value",
+			StorageLayout: &schemapb.StorageLayout{
+				Type: schemapb.StorageLayout_TYPE_INT64,
+			},
+			Dynamic: false,
+		}},
+		SortingColumns: []*schemapb.SortingColumn{{
+			Name:      "example_type",
+			Direction: schemapb.SortingColumn_DIRECTION_ASCENDING,
+		}, {
+			Name:       "labels",
+			Direction:  schemapb.SortingColumn_DIRECTION_ASCENDING,
+			NullsFirst: true,
+		}, {
+			Name:      "timestamp",
+			Direction: schemapb.SortingColumn_DIRECTION_ASCENDING,
+		}, {
+			Name:       "stacktrace",
+			Direction:  schemapb.SortingColumn_DIRECTION_ASCENDING,
+			NullsFirst: true,
+		}},
+	}
+}
+
+// Adds a float column to the SampleDefinition to be able to test
+// aggregations with float values.
+func SampleDefinitionWithFloat() *schemapb.Schema {
+	sample := SampleDefinition()
+	sample.Columns = append(sample.Columns, &schemapb.Column{
+		Name: "floatvalue",
+		StorageLayout: &schemapb.StorageLayout{
+			Type:     schemapb.StorageLayout_TYPE_DOUBLE,
+			Nullable: true,
+		},
+		Dynamic: false,
+	})
+	return sample
+}
+
+func NewTestSamples() Samples {
+	return Samples{
+		{
+			ExampleType: "cpu",
+			Labels: map[string]string{
+				"node": "test3",
+			},
+			Stacktrace: []uuid.UUID{
+				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
+				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2},
+			},
+			Timestamp: 2,
+			Value:     5,
+		}, {
+			ExampleType: "cpu",
+			Labels: map[string]string{
+				"namespace": "default",
+				"pod":       "test1",
+			},
+			Stacktrace: []uuid.UUID{
+				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
+				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2},
+			},
+			Timestamp: 2,
+			Value:     3,
+		}, {
+			ExampleType: "cpu",
+			Labels: map[string]string{
+				"container": "test2",
+				"namespace": "default",
+			},
+			Stacktrace: []uuid.UUID{
+				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
+				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2},
+			},
+			Timestamp: 2,
+			Value:     3,
+		},
+	}
+}
+
+func GenerateTestSamples(n int) Samples {
+	s := Samples{}
+	for i := 0; i < n; i++ {
+		s = append(s,
+			Sample{
+				ExampleType: "cpu",
+				Labels: map[string]string{
+					"node": "test3",
+				},
+				Stacktrace: []uuid.UUID{
+					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
+					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2},
+				},
+				Timestamp: int64(i),
+				Value:     int64(i),
+			})
+	}
+	return s
+}
+
+func NestedListDef(name string, layout *schemav2pb.StorageLayout) *schemav2pb.Node_Group {
+	return &schemav2pb.Node_Group{
+		Group: &schemav2pb.Group{
+			Name: name,
+			Nodes: []*schemav2pb.Node{ // NOTE that this nested group structure for a list is for backwards compatability: https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists
+				{
+					Type: &schemav2pb.Node_Group{
+						Group: &schemav2pb.Group{
+							Name:     "list",
+							Repeated: true,
+							Nodes: []*schemav2pb.Node{
+								{
+									Type: &schemav2pb.Node_Leaf{
+										Leaf: &schemav2pb.Leaf{
+											Name:          "element",
+											StorageLayout: layout,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func LabelColumn(name string) *schemav2pb.Node {
+	return &schemav2pb.Node{
+		Type: &schemav2pb.Node_Group{
+			Group: &schemav2pb.Group{
+				Name: "labels",
+				Nodes: []*schemav2pb.Node{
+					{
+						Type: &schemav2pb.Node_Leaf{
+							Leaf: &schemav2pb.Leaf{
+								Name: name,
+								StorageLayout: &schemav2pb.StorageLayout{
+									Type:     schemav2pb.StorageLayout_TYPE_STRING,
+									Nullable: true,
+									Encoding: schemav2pb.StorageLayout_ENCODING_RLE_DICTIONARY,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func NewNestedSampleSchema(t testing.TB) proto.Message {
+	t.Helper()
+	return &schemav2pb.Schema{
+		Root: &schemav2pb.Group{
+			Name: "nested",
+			Nodes: []*schemav2pb.Node{
+				{
+					Type: &schemav2pb.Node_Group{
+						Group: &schemav2pb.Group{
+							Name:  "labels",
+							Nodes: []*schemav2pb.Node{},
+						},
+					},
+				},
+				{
+					Type: NestedListDef("timestamps", &schemav2pb.StorageLayout{
+						Type:     schemav2pb.StorageLayout_TYPE_INT64,
+						Nullable: true,
+						Encoding: schemav2pb.StorageLayout_ENCODING_RLE_DICTIONARY,
+					}),
+				},
+				{
+					Type: NestedListDef("values", &schemav2pb.StorageLayout{
+						Type:     schemav2pb.StorageLayout_TYPE_INT64,
+						Nullable: true,
+						Encoding: schemav2pb.StorageLayout_ENCODING_RLE_DICTIONARY,
+					}),
+				},
+			},
+		},
+		SortingColumns: []*schemav2pb.SortingColumn{
+			{
+				Path:       "labels",
+				Direction:  schemav2pb.SortingColumn_DIRECTION_ASCENDING,
+				NullsFirst: true,
+			},
+			{
+				Path:      "timestamp",
+				Direction: schemav2pb.SortingColumn_DIRECTION_ASCENDING,
+			},
+		},
+	}
+}
diff --git a/table.go b/table.go
index 4d84e2c..0eacdf7 100644
--- a/table.go
+++ b/table.go
@@ -37,6 +37,7 @@ import (
 	tablepb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/table/v1alpha1"
 	walpb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/wal/v1alpha1"
 	"github.com/polarsignals/frostdb/index"
+	"github.com/polarsignals/frostdb/internal/records"
 	"github.com/polarsignals/frostdb/parts"
 	"github.com/polarsignals/frostdb/pqarrow"
 	"github.com/polarsignals/frostdb/query/logicalplan"
@@ -163,10 +164,73 @@ type completedBlock struct {
 	tx     uint64
 }
 
+// GenericTable is a wrapper around *Table that writes structs of type T. It
+// consist of  a generic arrow.Record builder that ingests structs of type T.
+// The generated record is then  passed to (*Table).InsertRecord.
+//
+// Struct tag `frostdb` is used to pass options for the schema for T.
+//
+// This api is opinionated.
+//
+//   - Nested Columns are not supported
+//
+// # Tags
+//
+// Use `frostdb` to define tags that customizes field values. You can express
+// everything needed to construct schema v1alpha1.
+//
+// Tags are defined as a comma separated list. The first item is the column
+// name. Column name is optional, when omitted it is derived from the field name
+// (snake_cased)
+//
+// Supported Tags
+//
+//	    delta_binary_packed | Delta binary packed encoding.
+//	                 brotli | Brotli compression.
+//	                    asc | Sorts in ascending order.Use asc(n) where n is an integer for sorting order.
+//	                   gzip | GZIP compression.
+//	                 snappy | Snappy compression.
+//	delta_length_byte_array | Delta Length Byte Array encoding.
+//	       delta_byte_array | Delta Byte Array encoding.
+//	                   desc | Sorts in descending order.Use desc(n) where n is an integer for sorting order
+//	                lz4_raw | LZ4_RAW compression.
+//	               pre_hash | Prehash the column before storing it.
+//	             null_first | When used wit asc nulls are smallest and with des nulls are largest.
+//	                   zstd | ZSTD compression.
+//	               rle_dict | Dictionary run-length encoding.
+//	                  plain | Plain encoding.
+//
+// Example tagged Sample struct
+//
+//	type Sample struct {
+//		ExampleType string      `frostdb:"example_type,rle_dict,asc(0)"`
+//		Labels      []Label     `frostdb:"labels,rle_dict,null,dyn,asc(1),null_first"`
+//		Stacktrace  []uuid.UUID `frostdb:"stacktrace,rle_dict,asc(3),null_first"`
+//		Timestamp   int64       `frostdb:"timestamp,asc(2)"`
+//		Value       int64       `frostdb:"value"`
+//	}
+//
+// # Dynamic columns
+//
+// Field of type map<string, T> is a dynamic column by default.
+//
+//	type Example struct {
+//		// Use supported tags to customize the column value
+//		Labels map[string]string `frostdb:"labels"`
+//	}
+//
+// # Repeated columns
+//
+// Fields of type []int64, []float64, []bool, and []string are supported. These
+// are represented as arrow.LIST.
+//
+// Generated schema for the repeated columns applies all supported tags. By
+// default repeated fields are nullable. You can safely pass nil slices for
+// repeated columns.
 type GenericTable[T any] struct {
 	*Table
 	mu    sync.Mutex
-	build *dynparquet.Build[T]
+	build *records.Build[T]
 }
 
 func (t *GenericTable[T]) Release() {
@@ -185,7 +249,7 @@ func (t *GenericTable[T]) Write(ctx context.Context, values ...T) error {
 }
 
 func NewGenericTable[T any](db *DB, name string, mem memory.Allocator, options ...TableOption) (*GenericTable[T], error) {
-	build := dynparquet.NewBuild[T](mem)
+	build := records.NewBuild[T](mem)
 	table, err := db.Table(name, NewTableConfig(build.Schema(name), options...))
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/dynparquet/reader_test.go b/dynparquet/reader_test.go
index 58edc47..41552b3 100644
--- a/dynparquet/reader_test.go
+++ b/dynparquet/reader_test.go
@@ -11,7 +11,7 @@ import (
 func TestReader(t *testing.T) {
 	schema := NewSampleSchema()
 	samples := NewTestSamples()
-	buf, err := samples.ToBuffer(schema)
+	buf, err := ToBuffer(samples, schema)
 	require.NoError(t, err)
 
 	b := bytes.NewBuffer(nil)
@@ -33,7 +33,7 @@ func TestReader(t *testing.T) {
 func TestSerializedReader(t *testing.T) {
 	schema := NewSampleSchema()
 	samples := NewTestSamples()
-	buf, err := samples.ToBuffer(schema)
+	buf, err := ToBuffer(samples, schema)
 	require.NoError(t, err)
 
 	b := bytes.NewBuffer(nil)
diff --git a/dynparquet/row_test.go b/dynparquet/row_test.go
index 31f2e6c..a715bef 100644
--- a/dynparquet/row_test.go
+++ b/dynparquet/row_test.go
@@ -101,7 +101,7 @@ func TestLess(t *testing.T) {
 	rowGroups := []DynamicRowGroup{}
 	for _, sample := range samples {
 		s := Samples{sample}
-		rg, err := s.ToBuffer(schema)
+		rg, err := ToBuffer(s, schema)
 		require.NoError(t, err)
 		rowGroups = append(rowGroups, rg)
 	}
@@ -161,7 +161,7 @@ func TestLess(t *testing.T) {
 		modifiedSample := samples[0]
 		modifiedSample.Timestamp++
 
-		rg, err := (Samples{modifiedSample}).ToBuffer(schema)
+		rg, err := ToBuffer(Samples{modifiedSample}, schema)
 		require.NoError(t, err)
 		row4 := &DynamicRows{
 			Schema:         rg.Schema(),
@@ -195,7 +195,7 @@ func TestLess(t *testing.T) {
 			Timestamp: 2,
 			Value:     5,
 		}
-		rg, err := Samples{sample}.ToBuffer(schema)
+		rg, err := ToBuffer(Samples{sample}, schema)
 		require.NoError(t, err)
 		row1 := &DynamicRows{
 			Schema:         rg.Schema(),
@@ -209,7 +209,7 @@ func TestLess(t *testing.T) {
 
 		// Add 1 to the last stacktrace byte.
 		sample.Stacktrace[1][15] = 0x3
-		rg, err = Samples{sample}.ToBuffer(schema)
+		rg, err = ToBuffer(Samples{sample}, schema)
 		require.NoError(t, err)
 		row2 := &DynamicRows{
 			Schema:         rg.Schema(),
@@ -252,7 +252,7 @@ func TestLessWithDynamicSchemas(t *testing.T) {
 	rowGroups := []DynamicRowGroup{}
 	for _, sample := range samples {
 		s := Samples{sample}
-		rg, err := s.ToBuffer(schema)
+		rg, err := ToBuffer(s, schema)
 		require.NoError(t, err)
 		rowGroups = append(rowGroups, rg)
 	}
diff --git a/dynparquet/schema_test.go b/dynparquet/schema_test.go
index 7c2a1f8..5b5f8a1 100644
--- a/dynparquet/schema_test.go
+++ b/dynparquet/schema_test.go
@@ -21,7 +21,7 @@ func TestMergeRowBatches(t *testing.T) {
 	rowGroups := []DynamicRowGroup{}
 	for _, sample := range samples {
 		s := Samples{sample}
-		rg, err := s.ToBuffer(schema)
+		rg, err := ToBuffer(s, schema)
 		require.NoError(t, err)
 		rowGroups = append(rowGroups, rg)
 	}
@@ -198,7 +198,7 @@ func TestMultipleIterations(t *testing.T) {
 		Value:     3,
 	}}
 
-	dbuf, err := samples.ToBuffer(schema)
+	dbuf, err := ToBuffer(samples, schema)
 	require.NoError(t, err)
 
 	buf := dbuf.buffer
@@ -271,7 +271,7 @@ func Test_SchemaFromParquetFile(t *testing.T) {
 		Value:     3,
 	}}
 
-	dbuf, err := samples.ToBuffer(schema)
+	dbuf, err := ToBuffer(samples, schema)
 	require.NoError(t, err)
 
 	b := bytes.NewBuffer(nil)
diff --git a/dynparquet/record_builder_test.go b/internal/records/record_builder_test.go
similarity index 90%
rename from dynparquet/record_builder_test.go
rename to internal/records/record_builder_test.go
index d283287..71a8bac 100644
--- a/dynparquet/record_builder_test.go
+++ b/internal/records/record_builder_test.go
@@ -1,4 +1,4 @@
-package dynparquet
+package records
 
 import (
 	"testing"
@@ -7,29 +7,31 @@ import (
 	"github.com/stretchr/testify/require"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/proto"
+
+	"github.com/polarsignals/frostdb/samples"
 )
 
 func TestBuild(t *testing.T) {
 	t.Run("NewBuild", func(t *testing.T) {
-		b := NewBuild[Sample](memory.DefaultAllocator)
+		b := NewBuild[samples.Sample](memory.DefaultAllocator)
 		defer b.Release()
 
-		ptr := NewBuild[*Sample](memory.DefaultAllocator)
+		ptr := NewBuild[*samples.Sample](memory.DefaultAllocator)
 		defer ptr.Release()
 	})
 
 	t.Run("Schema", func(t *testing.T) {
-		b := NewBuild[Sample](memory.DefaultAllocator)
+		b := NewBuild[samples.Sample](memory.DefaultAllocator)
 		defer b.Release()
 		got := b.Schema("test")
-		want := SampleDefinition()
+		want := samples.SampleDefinition()
 		require.True(t, proto.Equal(want, got))
 	})
 
 	t.Run("NewRecord", func(t *testing.T) {
-		b := NewBuild[Sample](memory.DefaultAllocator)
+		b := NewBuild[samples.Sample](memory.DefaultAllocator)
 		defer b.Release()
-		samples := NewTestSamples()
+		samples := samples.NewTestSamples()
 		err := b.Append(samples...)
 		require.Nil(t, err)
 		r := b.NewRecord()
@@ -176,9 +178,9 @@ func BenchmarkBuild_Append_Then_NewRecord(b *testing.B) {
 	// NewRecord
 	//
 	// They are separate methods because we can't ignore benefits of buffering.
-	build := NewBuild[Sample](memory.DefaultAllocator)
+	build := NewBuild[samples.Sample](memory.DefaultAllocator)
 	defer build.Release()
-	samples := NewTestSamples()
+	samples := samples.NewTestSamples()
 	b.ReportAllocs()
 	for i := 0; i < b.N; i++ {
 		_ = build.Append(samples...)
diff --git a/parts/part_test.go b/parts/part_test.go
index e25644c..f286376 100644
--- a/parts/part_test.go
+++ b/parts/part_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/polarsignals/frostdb/dynparquet"
 	schemapb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/schema/v1alpha1"
+	"github.com/polarsignals/frostdb/internal/records"
 	"github.com/polarsignals/frostdb/pqarrow"
 )
 
@@ -84,7 +85,7 @@ func TestFindMaximumNonOverlappingSet(t *testing.T) {
 	} {
 		t.Run(tc.name, func(t *testing.T) {
 			parts := make([]Part, len(tc.ranges))
-			b := dynparquet.NewBuild[dataModel](memory.NewGoAllocator())
+			b := records.NewBuild[dataModel](memory.NewGoAllocator())
 			schema, err := dynparquet.SchemaFromDefinition(b.Schema("test_schema"))
 			require.NoError(t, err)
 			defer b.Release()
diff --git a/pqarrow/arrow_test.go b/pqarrow/arrow_test.go
index 783c12e..ee6777c 100644
--- a/pqarrow/arrow_test.go
+++ b/pqarrow/arrow_test.go
@@ -32,7 +32,7 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 		Value:     1,
 	}}
 
-	buf0, err := samples.ToBuffer(dynSchema)
+	buf0, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
@@ -49,7 +49,7 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 		Value:     2,
 	}}
 
-	buf1, err := samples.ToBuffer(dynSchema)
+	buf1, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
@@ -66,7 +66,7 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 		Value:     3,
 	}}
 
-	buf2, err := samples.ToBuffer(dynSchema)
+	buf2, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
@@ -82,7 +82,7 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 		Value:     2,
 	}}
 
-	buf3, err := samples.ToBuffer(dynSchema)
+	buf3, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
@@ -99,7 +99,7 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 		Value:     3,
 	}}
 
-	buf4, err := samples.ToBuffer(dynSchema)
+	buf4, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	ctx := context.Background()
@@ -184,7 +184,7 @@ func TestMergeToArrow(t *testing.T) {
 		Value:     3,
 	}}
 
-	buf1, err := samples.ToBuffer(dynSchema)
+	buf1, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
@@ -200,7 +200,7 @@ func TestMergeToArrow(t *testing.T) {
 		Value:     2,
 	}}
 
-	buf2, err := samples.ToBuffer(dynSchema)
+	buf2, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
@@ -217,7 +217,7 @@ func TestMergeToArrow(t *testing.T) {
 		Value:     3,
 	}}
 
-	buf3, err := samples.ToBuffer(dynSchema)
+	buf3, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	merge, err := dynSchema.MergeDynamicRowGroups([]dynparquet.DynamicRowGroup{buf1, buf2, buf3})
@@ -317,7 +317,7 @@ func BenchmarkParquetToArrow(b *testing.B) {
 		})
 	}
 
-	buf, err := samples.ToBuffer(dynSchema)
+	buf, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(b, err)
 
 	ctx := context.Background()
@@ -434,7 +434,7 @@ func TestDistinctBinaryExprOptimization(t *testing.T) {
 		Value:     2,
 	}}
 
-	buf, err := samples.ToBuffer(dynSchema)
+	buf, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	ctx := context.Background()
@@ -530,7 +530,7 @@ func TestDistinctBinaryExprOptimizationMixed(t *testing.T) {
 		Value:     0,
 	}}
 
-	buf, err := samples.ToBuffer(dynSchema)
+	buf, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	ctx := context.Background()
@@ -830,7 +830,7 @@ func Test_ParquetToArrow(t *testing.T) {
 		})
 	}
 
-	buf, err := samples.ToBuffer(dynSchema)
+	buf, err := dynparquet.ToBuffer(samples, dynSchema)
 	require.NoError(t, err)
 
 	ctx := context.Background()
diff --git a/table_test.go b/table_test.go
index c8ca3f9..2969298 100644
--- a/table_test.go
+++ b/table_test.go
@@ -749,7 +749,7 @@ func Test_RowWriter(t *testing.T) {
 
 	// Write 17(8,9) rows, expect 3 row groups of 5 rows and 1 row group of 2 rows
 	samples := dynparquet.GenerateTestSamples(8)
-	buf, err := samples.ToBuffer(table.Schema())
+	buf, err := dynparquet.ToBuffer(samples, table.Schema())
 	require.NoError(t, err)
 	rows := buf.Rows()
 	_, err = rowWriter.writeRows(rows)
@@ -757,7 +757,7 @@ func Test_RowWriter(t *testing.T) {
 	require.NoError(t, rows.Close())
 
 	samples = dynparquet.GenerateTestSamples(9)
-	buf, err = samples.ToBuffer(table.Schema())
+	buf, err = dynparquet.ToBuffer(samples, table.Schema())
 	require.NoError(t, err)
 	rows = buf.Rows()
 	_, err = rowWriter.writeRows(rows)