Repository: polarsignals/frostdb
Pull request #628 (2023-12-14 11:06:05)

pqarrow/arrowutils: Add SortRecord and ReorderRecord
This is extract from a previous PR #461.


metalmatze: I 100% agree with your list. 
Would you like us to add those before merging this PR? 
Even with this small feature set some use cases benefit from this already. 

For now, I'll make the parameter a `cols []int` for the future. 
metalmatze: I've updated to additionally add support for sorting `NULL`. It always gets sorted to the back.
metalmatze: You're correct with the `*array.Binary` column. It's not covered in the unit tests (yet). 
This was added for an internal experiment. I'm fine removing it from the sort code in this PR for now.

Patch:
diff --git a/dynparquet/example.go b/dynparquet/example.go
index 55335d1..4fa4675 100644
--- a/dynparquet/example.go
+++ b/dynparquet/example.go
@@ -16,17 +16,12 @@ import (
 	schemav2pb "github.com/polarsignals/frostdb/gen/proto/go/frostdb/schema/v1alpha2"
 )
 
-type Label struct {
-	Name  string
-	Value string
-}
-
 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"`
+	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
@@ -66,14 +61,14 @@ func (s Samples) ToRecord() (arrow.Record, error) {
 	}
 	seen := map[string]struct{}{}
 	for _, smpl := range s {
-		for _, lbl := range smpl.Labels {
-			if _, ok := seen[lbl.Name]; ok {
+		for lbl := range smpl.Labels {
+			if _, ok := seen[lbl]; ok {
 				continue
 			}
-			seen[lbl.Name] = struct{}{}
+			seen[lbl] = struct{}{}
 			fields = append(fields,
 				arrow.Field{
-					Name:     "labels." + lbl.Name,
+					Name:     "labels." + lbl,
 					Nullable: true,
 					Type: &arrow.DictionaryType{
 						IndexType: &arrow.Int32Type{},
@@ -106,9 +101,9 @@ func (s Samples) ToRecord() (arrow.Record, error) {
 		}
 		for i := 0; i < numLabels; i++ {
 			found := false
-			for _, lbl := range sample.Labels {
-				if "labels."+lbl.Name == schema.Field(i+1).Name {
-					if err := bld.Field(i + 1).(*array.BinaryDictionaryBuilder).Append([]byte(lbl.Value)); err != nil {
+			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
@@ -135,10 +130,10 @@ func (s Samples) SampleLabelNames() []string {
 	seen := map[string]struct{}{}
 
 	for _, sample := range s {
-		for _, label := range sample.Labels {
-			if _, ok := seen[label.Name]; !ok {
-				names = append(names, label.Name)
-				seen[label.Name] = struct{}{}
+		for label := range sample.Labels {
+			if _, ok := seen[label]; !ok {
+				names = append(names, label)
+				seen[label] = struct{}{}
 			}
 		}
 	}
@@ -159,11 +154,10 @@ func (s Sample) ToParquetRow(labelNames []string) parquet.Row {
 
 	i, j := 0, 0
 	for i < nameNumber {
-		if labelNames[i] == s.Labels[j].Name {
-			row = append(row, parquet.ValueOf(s.Labels[j].Value).Level(0, 1, i+1))
+		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))
@@ -333,10 +327,9 @@ func NewTestSamples() Samples {
 	return Samples{
 		{
 			ExampleType: "cpu",
-			Labels: []Label{{
-				Name:  "node",
-				Value: "test3",
-			}},
+			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},
@@ -345,13 +338,10 @@ func NewTestSamples() Samples {
 			Value:     5,
 		}, {
 			ExampleType: "cpu",
-			Labels: []Label{{
-				Name:  "namespace",
-				Value: "default",
-			}, {
-				Name:  "pod",
-				Value: "test1",
-			}},
+			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},
@@ -360,13 +350,10 @@ func NewTestSamples() Samples {
 			Value:     3,
 		}, {
 			ExampleType: "cpu",
-			Labels: []Label{{
-				Name:  "container",
-				Value: "test2",
-			}, {
-				Name:  "namespace",
-				Value: "default",
-			}},
+			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},
@@ -383,10 +370,9 @@ func GenerateTestSamples(n int) Samples {
 		s = append(s,
 			Sample{
 				ExampleType: "cpu",
-				Labels: []Label{{
-					Name:  "node",
-					Value: "test3",
-				}},
+				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},
diff --git a/dynparquet/record_builder.go b/dynparquet/record_builder.go
index f60ba51..017e1d8 100644
--- a/dynparquet/record_builder.go
+++ b/dynparquet/record_builder.go
@@ -27,8 +27,6 @@ const (
 //
 // This api is opinionated.
 //
-//   - Dynamic columns are supported but only for  maps and struct slices
-//
 //   - Nested Columns are not supported
 //
 // # Tags
@@ -52,7 +50,6 @@ const (
 //	                   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.
-//	                    dyn | Whether the column can dynamically expand.
 //	                   null | Nullable column.
 //	             null_first | When used wit asc nulls are smallest and with des nulls are largest.
 //	                   zstd | ZSTD compression.
@@ -71,19 +68,11 @@ const (
 //
 // # Dynamic columns
 //
-// Any field of type map<string, T> is a dynamic column by default. You can  also use
-// a list of structs with two fields , the first field of string type will be
-// used as column nave and the second field will be used as column value.
+// Field of type map<string, T> is a dynamic column by default.
 //
 //	type Example struct {
-//		// Adding dyn tag is optional for map fields. Map keys must be string any other
-//		// type will result in a panic.
-//		// Use supported tags to customize the column value
-//		Dyn00 map[string]string `frostdb:"dyn_00"`
-//
-//		// It is required to set dyn tag. Omitting the dyn tag will result in a panic.
 //		// Use supported tags to customize the column value
-//		Labels []Label `frostdb:"labels,dyn"`
+//		Labels map[string]string `frostdb:"labels"`
 //	}
 //
 // # Repeated columns
@@ -112,7 +101,6 @@ func NewBuild[T any](mem memory.Allocator) *Build[T] {
 	for i := 0; i < r.NumField(); i++ {
 		f := r.Field(i)
 		var (
-			dynamic    bool
 			typ        arrow.DataType
 			dictionary bool
 			preHash    bool
@@ -130,8 +118,6 @@ func NewBuild[T any](mem memory.Allocator) *Build[T] {
 		if tag != "" {
 			walkTag(tag, func(key, value string) {
 				switch key {
-				case "dyn":
-					dynamic = true
 				case "null":
 					null = true
 				case "null_first":
@@ -172,7 +158,6 @@ func NewBuild[T any](mem memory.Allocator) *Build[T] {
 		}
 		fr := &fieldRecord{
 			name:        name,
-			dynamic:     dynamic,
 			preHash:     preHash,
 			nullable:    null,
 			sort:        sortColumn,
@@ -198,22 +183,6 @@ func NewBuild[T any](mem memory.Allocator) *Build[T] {
 			case isUUIDSlice(fty):
 				fr.typ = schemapb.StorageLayout_TYPE_STRING
 				fr.build = newUUIDSliceField(mem, name)
-			case dynamic:
-				elem := fty.Elem()
-				for elem.Kind() == reflect.Ptr {
-					elem = elem.Elem()
-				}
-				if elem.Kind() != reflect.Struct {
-					panic("frostdb/dynschema: only structs are supported for struct list ")
-				}
-				if elem.NumField() != 2 {
-					panic("frostdb/dynschema: mismatch number of fields for slices columns ")
-				}
-				typ, fr.typ = baseType(elem.Field(1).Type, dictionary)
-				fr.dynamic = true
-				// Dynamic columns are nullable
-				fr.nullable = true
-				fr.build = newMapFieldBuilder(newFieldFunc(typ, mem, name))
 			default:
 				typ, styp = baseType(fty.Elem(), dictionary)
 				fr.typ = styp
diff --git a/go.mod b/go.mod
index 1f4c7b0..fdc58a5 100644
--- a/go.mod
+++ b/go.mod
@@ -27,6 +27,7 @@ require (
 )
 
 require (
+	github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect
 	github.com/andybalholm/brotli v1.0.5 // indirect
 	github.com/benbjohnson/clock v1.3.5 // indirect
 	github.com/benbjohnson/immutable v0.4.0 // indirect
diff --git a/go.sum b/go.sum
index 2f84ddd..1282c14 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,6 @@
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU=
+github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
 github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo=
 github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
 github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
@@ -79,8 +81,6 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/parquet-go/parquet-go v0.19.1-0.20231129084429-9010539a4f7a h1:NxS5GxNgZa5nJeLjJFidbzhwn+YuhdV5pXHtOw7VKB8=
-github.com/parquet-go/parquet-go v0.19.1-0.20231129084429-9010539a4f7a/go.mod h1:4YfUo8TkoGoqwzhA/joZKZ8f77wSMShOLHESY4Ys0bY=
 github.com/parquet-go/parquet-go v0.20.0 h1:a6tV5XudF893P1FMuyp01zSReXbBelquKQgRxBgJ29w=
 github.com/parquet-go/parquet-go v0.20.0/go.mod h1:4YfUo8TkoGoqwzhA/joZKZ8f77wSMShOLHESY4Ys0bY=
 github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
diff --git a/pqarrow/arrowutils/sort.go b/pqarrow/arrowutils/sort.go
new file mode 100644
index 0000000..e4e3f7f
--- /dev/null
+++ b/pqarrow/arrowutils/sort.go
@@ -0,0 +1,94 @@
+package arrowutils
+
+import (
+	"context"
+	"fmt"
+	"sort"
+
+	"github.com/apache/arrow/go/v14/arrow"
+	"github.com/apache/arrow/go/v14/arrow/array"
+	"github.com/apache/arrow/go/v14/arrow/compute"
+	"github.com/apache/arrow/go/v14/arrow/memory"
+)
+
+// SortRecord sorts the given record's rows by the given column. Currently only supports int64, string and binary columns.
+func SortRecord(mem memory.Allocator, r arrow.Record, cols []int) (*array.Int64, error) {
+	if len(cols) > 1 {
+		return nil, fmt.Errorf("sorting by multiple columns isn't implemented yet")
+	}
+	indicesBuilder := array.NewInt64Builder(mem)
+
+	if r.NumRows() == 0 {
+		return indicesBuilder.NewInt64Array(), nil
+	}
+	if r.NumRows() == 1 {
+		indicesBuilder.Append(0)
+		return indicesBuilder.NewInt64Array(), nil
+	}
+
+	indices := make([]int64, r.NumRows())
+	// populate indices
+	for i := range indices {
+		indices[i] = int64(i)
+	}
+
+	switch c := r.Column(cols[0]).(type) {
+	case *array.Int64:
+		sort.Sort(orderedSorter[int64]{array: c, indices: indices})
+	case *array.String:
+		sort.Sort(orderedSorter[string]{array: c, indices: indices})
+	default:
+		return nil, fmt.Errorf("unsupported column type for sorting %T", c)
+	}
+
+	indicesBuilder.Reserve(len(indices))
+	for _, i := range indices {
+		indicesBuilder.Append(i)
+	}
+
+	return indicesBuilder.NewInt64Array(), nil
+}
+
+// ReorderRecord reorders the given record's rows by the given indices.
+// This is a wrapper around compute.Take which handles the type castings.
+func ReorderRecord(ctx context.Context, r arrow.Record, indices arrow.Array) (arrow.Record, error) {
+	res, err := compute.Take(
+		ctx,
+		*compute.DefaultTakeOptions(),
+		compute.NewDatum(r),
+		compute.NewDatum(indices),
+	)
+	if err != nil {
+		return nil, err
+	}
+	return res.(*compute.RecordDatum).Value, nil
+}
+
+type orderedArray[T int64 | float64 | string] interface {
+	Value(int) T
+	IsNull(int) bool
+	Len() int
+}
+
+type orderedSorter[T int64 | float64 | string] struct {
+	array   orderedArray[T]
+	indices []int64
+}
+
+func (s orderedSorter[T]) Len() int {
+	return s.array.Len()
+}
+
+func (s orderedSorter[T]) Less(i, j int) bool {
+	if s.array.IsNull(int(s.indices[i])) {
+		return false
+	}
+	if s.array.IsNull(int(s.indices[j])) {
+		return true
+	}
+	return s.array.Value(int(s.indices[i])) < s.array.Value(int(s.indices[j]))
+}
+
+func (s orderedSorter[T]) Swap(i, j int) {
+	s.indices[i], s.indices[j] = s.indices[j], s.indices[i]
+}


Test patch:
diff --git a/aggregate_test.go b/aggregate_test.go
index f067510..d4b6588 100644
--- a/aggregate_test.go
+++ b/aggregate_test.go
@@ -38,8 +38,8 @@ func TestAggregateInconsistentSchema(t *testing.T) {
 	require.NoError(t, err)
 
 	samples := dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
+		Labels: map[string]string{
+			"label1": "value1",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -48,8 +48,8 @@ func TestAggregateInconsistentSchema(t *testing.T) {
 		Timestamp: 1,
 		Value:     1,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -58,8 +58,8 @@ func TestAggregateInconsistentSchema(t *testing.T) {
 		Timestamp: 2,
 		Value:     2,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -161,9 +161,9 @@ func TestAggregationProjection(t *testing.T) {
 	require.NoError(t, err)
 
 	samples := dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -172,10 +172,10 @@ func TestAggregationProjection(t *testing.T) {
 		Timestamp: 1,
 		Value:     1,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -184,10 +184,10 @@ func TestAggregationProjection(t *testing.T) {
 		Timestamp: 2,
 		Value:     2,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -354,9 +354,9 @@ func BenchmarkAggregation(b *testing.B) {
 		samples := make(dynparquet.Samples, 0, 10_000)
 		for i := 0; i < cap(samples); i++ {
 			samples = append(samples, dynparquet.Sample{
-				Labels: []dynparquet.Label{
-					{Name: "label1", Value: "value1"},
-					{Name: "label2", Value: "value" + strconv.Itoa(i%3)},
+				Labels: map[string]string{
+					"label1": "value1",
+					"label2": "value" + strconv.Itoa(i%3),
 				},
 				Stacktrace: []uuid.UUID{
 					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
diff --git a/bench_test.go b/bench_test.go
index a10f6c3..c2082fd 100644
--- a/bench_test.go
+++ b/bench_test.go
@@ -421,10 +421,9 @@ func NewTestSamples(num int) dynparquet.Samples {
 		samples = append(samples,
 			dynparquet.Sample{
 				ExampleType: "cpu",
-				Labels: []dynparquet.Label{{
-					Name:  "node",
-					Value: "test3",
-				}},
+				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},
diff --git a/db_test.go b/db_test.go
index 2940905..ad31d03 100644
--- a/db_test.go
+++ b/db_test.go
@@ -125,9 +125,9 @@ func TestDBWithWAL(t *testing.T) {
 
 	samples := dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -137,10 +137,10 @@ func TestDBWithWAL(t *testing.T) {
 		Value:     1,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -150,10 +150,10 @@ func TestDBWithWAL(t *testing.T) {
 		Value:     2,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -171,9 +171,9 @@ func TestDBWithWAL(t *testing.T) {
 
 	samples = dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -191,10 +191,10 @@ func TestDBWithWAL(t *testing.T) {
 
 	samples = dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -285,9 +285,9 @@ func Test_DB_WithStorage(t *testing.T) {
 
 	samples := dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -297,10 +297,10 @@ func Test_DB_WithStorage(t *testing.T) {
 		Value:     1,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -310,10 +310,10 @@ func Test_DB_WithStorage(t *testing.T) {
 		Value:     2,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -424,9 +424,9 @@ func Test_DB_ColdStart(t *testing.T) {
 			samples := dynparquet.Samples{
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -437,9 +437,9 @@ func Test_DB_ColdStart(t *testing.T) {
 				},
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -450,9 +450,9 @@ func Test_DB_ColdStart(t *testing.T) {
 				},
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -685,9 +685,9 @@ func Test_DB_Filter_Block(t *testing.T) {
 			samples := dynparquet.Samples{
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -698,9 +698,9 @@ func Test_DB_Filter_Block(t *testing.T) {
 				},
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -711,9 +711,9 @@ func Test_DB_Filter_Block(t *testing.T) {
 				},
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -970,9 +970,9 @@ func Test_DB_Block_Optimization(t *testing.T) {
 			samples := dynparquet.Samples{
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -983,9 +983,9 @@ func Test_DB_Block_Optimization(t *testing.T) {
 				},
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -996,9 +996,9 @@ func Test_DB_Block_Optimization(t *testing.T) {
 				},
 				{
 					ExampleType: "test",
-					Labels: []dynparquet.Label{
-						{Name: "label1", Value: "value1"},
-						{Name: "label2", Value: "value2"},
+					Labels: map[string]string{
+						"label1": "value1",
+						"label2": "value2",
 					},
 					Stacktrace: []uuid.UUID{
 						{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -1119,9 +1119,9 @@ func Test_DB_TableWrite_DynamicSchema(t *testing.T) {
 	samples := dynparquet.Samples{
 		{
 			ExampleType: "test",
-			Labels: []dynparquet.Label{
-				{Name: "label1", Value: "value1"},
-				{Name: "label2", Value: "value2"},
+			Labels: map[string]string{
+				"label1": "value1",
+				"label2": "value2",
 			},
 			Stacktrace: []uuid.UUID{
 				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -1132,10 +1132,10 @@ func Test_DB_TableWrite_DynamicSchema(t *testing.T) {
 		},
 		{
 			ExampleType: "test",
-			Labels: []dynparquet.Label{
-				{Name: "label1", Value: "value1"},
-				{Name: "label2", Value: "value2"},
-				{Name: "label3", Value: "value3"},
+			Labels: map[string]string{
+				"label1": "value1",
+				"label2": "value2",
+				"label3": "value3",
 			},
 			Stacktrace: []uuid.UUID{
 				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -1146,9 +1146,9 @@ func Test_DB_TableWrite_DynamicSchema(t *testing.T) {
 		},
 		{
 			ExampleType: "test",
-			Labels: []dynparquet.Label{
-				{Name: "label1", Value: "value1"},
-				{Name: "label2", Value: "value2"},
+			Labels: map[string]string{
+				"label1": "value1",
+				"label2": "value2",
 			},
 			Stacktrace: []uuid.UUID{
 				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -1219,9 +1219,9 @@ func Test_DB_TableWrite_ArrowRecord(t *testing.T) {
 		samples := dynparquet.Samples{
 			{
 				ExampleType: "test",
-				Labels: []dynparquet.Label{
-					{Name: "label1", Value: "value1"},
-					{Name: "label2", Value: "value2"},
+				Labels: map[string]string{
+					"label1": "value1",
+					"label2": "value2",
 				},
 				Stacktrace: []uuid.UUID{
 					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -1232,10 +1232,10 @@ func Test_DB_TableWrite_ArrowRecord(t *testing.T) {
 			},
 			{
 				ExampleType: "test",
-				Labels: []dynparquet.Label{
-					{Name: "label1", Value: "value1"},
-					{Name: "label2", Value: "value2"},
-					{Name: "label3", Value: "value3"},
+				Labels: map[string]string{
+					"label1": "value1",
+					"label2": "value2",
+					"label3": "value3",
 				},
 				Stacktrace: []uuid.UUID{
 					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -1246,9 +1246,9 @@ func Test_DB_TableWrite_ArrowRecord(t *testing.T) {
 			},
 			{
 				ExampleType: "test",
-				Labels: []dynparquet.Label{
-					{Name: "label1", Value: "value1"},
-					{Name: "label2", Value: "value2"},
+				Labels: map[string]string{
+					"label1": "value1",
+					"label2": "value2",
 				},
 				Stacktrace: []uuid.UUID{
 					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -2026,9 +2026,9 @@ func Test_DB_All(t *testing.T) {
 
 	samples := dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -2038,10 +2038,10 @@ func Test_DB_All(t *testing.T) {
 		Value:     1,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -2051,10 +2051,10 @@ func Test_DB_All(t *testing.T) {
 		Value:     2,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -2107,9 +2107,9 @@ func Test_DB_PrehashedStorage(t *testing.T) {
 
 	samples := dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -2119,10 +2119,10 @@ func Test_DB_PrehashedStorage(t *testing.T) {
 		Value:     1,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -2132,10 +2132,10 @@ func Test_DB_PrehashedStorage(t *testing.T) {
 		Value:     2,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
diff --git a/dynparquet/row_test.go b/dynparquet/row_test.go
index 1baec65..31f2e6c 100644
--- a/dynparquet/row_test.go
+++ b/dynparquet/row_test.go
@@ -187,10 +187,7 @@ func TestLess(t *testing.T) {
 		schema := NewSampleSchema()
 		sample := Sample{
 			ExampleType: "cpu",
-			Labels: []Label{{
-				Name:  "node",
-				Value: "test3",
-			}},
+			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},
@@ -231,8 +228,8 @@ func TestLess(t *testing.T) {
 func TestLessWithDynamicSchemas(t *testing.T) {
 	schema := NewSampleSchema()
 	samples := Samples{{
-		Labels: []Label{
-			{Name: "label12", Value: "value12"},
+		Labels: map[string]string{
+			"label12": "value12",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -241,8 +238,8 @@ func TestLessWithDynamicSchemas(t *testing.T) {
 		Timestamp: 2,
 		Value:     2,
 	}, {
-		Labels: []Label{
-			{Name: "label14", Value: "value14"},
+		Labels: map[string]string{
+			"label14": "value14",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
diff --git a/dynparquet/schema_test.go b/dynparquet/schema_test.go
index b81f71a..7c2a1f8 100644
--- a/dynparquet/schema_test.go
+++ b/dynparquet/schema_test.go
@@ -162,9 +162,9 @@ func TestMultipleIterations(t *testing.T) {
 	schema := NewSampleSchema()
 
 	samples := Samples{{
-		Labels: []Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -173,10 +173,10 @@ func TestMultipleIterations(t *testing.T) {
 		Timestamp: 1,
 		Value:     1,
 	}, {
-		Labels: []Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -185,10 +185,10 @@ func TestMultipleIterations(t *testing.T) {
 		Timestamp: 2,
 		Value:     2,
 	}, {
-		Labels: []Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -235,9 +235,9 @@ func Test_SchemaFromParquetFile(t *testing.T) {
 	schema := NewSampleSchema()
 
 	samples := Samples{{
-		Labels: []Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -246,10 +246,10 @@ func Test_SchemaFromParquetFile(t *testing.T) {
 		Timestamp: 1,
 		Value:     1,
 	}, {
-		Labels: []Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -258,10 +258,10 @@ func Test_SchemaFromParquetFile(t *testing.T) {
 		Timestamp: 2,
 		Value:     2,
 	}, {
-		Labels: []Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
diff --git a/pqarrow/arrow_test.go b/pqarrow/arrow_test.go
index d39a88d..783c12e 100644
--- a/pqarrow/arrow_test.go
+++ b/pqarrow/arrow_test.go
@@ -20,9 +20,9 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 	dynSchema := dynparquet.NewSampleSchema()
 
 	samples := dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -36,10 +36,10 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -53,10 +53,10 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -70,9 +70,9 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -86,10 +86,10 @@ func TestDifferentSchemasToArrow(t *testing.T) {
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -148,9 +148,9 @@ func TestMergeToArrow(t *testing.T) {
 	dynSchema := dynparquet.NewSampleSchema()
 
 	samples := dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -159,10 +159,10 @@ func TestMergeToArrow(t *testing.T) {
 		Timestamp: 1,
 		Value:     1,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -171,10 +171,10 @@ func TestMergeToArrow(t *testing.T) {
 		Timestamp: 2,
 		Value:     2,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -188,9 +188,9 @@ func TestMergeToArrow(t *testing.T) {
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -204,10 +204,10 @@ func TestMergeToArrow(t *testing.T) {
 	require.NoError(t, err)
 
 	samples = dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -304,9 +304,9 @@ func BenchmarkParquetToArrow(b *testing.B) {
 	samples := make(dynparquet.Samples, 0, 1000)
 	for i := 0; i < 1000; i++ {
 		samples = append(samples, dynparquet.Sample{
-			Labels: []dynparquet.Label{
-				{Name: "label1", Value: "value1"},
-				{Name: "label2", Value: "value2"},
+			Labels: map[string]string{
+				"label1": "value1",
+				"label2": "value2",
 			},
 			Stacktrace: []uuid.UUID{
 				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -411,9 +411,9 @@ func TestDistinctBinaryExprOptimization(t *testing.T) {
 	dynSchema := dynparquet.NewSampleSchema()
 
 	samples := dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -422,9 +422,9 @@ func TestDistinctBinaryExprOptimization(t *testing.T) {
 		Timestamp: 1,
 		Value:     1,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -485,9 +485,9 @@ func TestDistinctBinaryExprOptimizationMixed(t *testing.T) {
 	dynSchema := dynparquet.NewSampleSchema()
 
 	samples := dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -496,9 +496,9 @@ func TestDistinctBinaryExprOptimizationMixed(t *testing.T) {
 		Timestamp: 1,
 		Value:     1,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -507,9 +507,9 @@ func TestDistinctBinaryExprOptimizationMixed(t *testing.T) {
 		Timestamp: 2,
 		Value:     2,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -518,9 +518,9 @@ func TestDistinctBinaryExprOptimizationMixed(t *testing.T) {
 		Timestamp: 3,
 		Value:     0,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -817,9 +817,9 @@ func Test_ParquetToArrow(t *testing.T) {
 	samples := make(dynparquet.Samples, 0, 1000)
 	for i := 0; i < 1000; i++ {
 		samples = append(samples, dynparquet.Sample{
-			Labels: []dynparquet.Label{
-				{Name: "label1", Value: "value1"},
-				{Name: "label2", Value: "value2"},
+			Labels: map[string]string{
+				"label1": "value1",
+				"label2": "value2",
 			},
 			Stacktrace: []uuid.UUID{
 				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
diff --git a/pqarrow/arrowutils/sort_test.go b/pqarrow/arrowutils/sort_test.go
new file mode 100644
index 0000000..8f94586
--- /dev/null
+++ b/pqarrow/arrowutils/sort_test.go
@@ -0,0 +1,83 @@
+package arrowutils
+
+import (
+	"context"
+	"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/stretchr/testify/require"
+)
+
+func TestSortRecord(t *testing.T) {
+	ctx := context.Background()
+	schema := arrow.NewSchema(
+		[]arrow.Field{
+			{Name: "int", Type: arrow.PrimitiveTypes.Int64},
+			{Name: "string", Type: arrow.BinaryTypes.String},
+		},
+		nil,
+	)
+
+	mem := memory.DefaultAllocator
+	ib := array.NewInt64Builder(mem)
+	ib.Append(0)
+	ib.AppendNull()
+	ib.Append(3)
+	ib.Append(5)
+	ib.Append(1)
+
+	sb := array.NewStringBuilder(mem)
+	sb.Append("d")
+	sb.Append("c")
+	sb.Append("b")
+	sb.AppendNull()
+	sb.Append("a")
+
+	record := array.NewRecord(schema, []arrow.Array{ib.NewArray(), sb.NewArray()}, int64(5))
+
+	// Sort the record by the first column - int64
+	{
+		sortedIndices, err := SortRecord(mem, record, []int{record.Schema().FieldIndices("int")[0]})
+		require.NoError(t, err)
+		require.Equal(t, []int64{0, 4, 2, 3, 1}, sortedIndices.Int64Values())
+
+		sortedByInts, err := ReorderRecord(ctx, record, sortedIndices)
+		require.NoError(t, err)
+
+		// check that the column got sortedIndices
+		intCol := sortedByInts.Column(0).(*array.Int64)
+		require.Equal(t, []int64{0, 1, 3, 5, 0}, intCol.Int64Values())
+		require.True(t, intCol.IsNull(intCol.Len()-1)) // last is NULL
+		// make sure the other column got updated too
+		strings := make([]string, sortedByInts.NumRows())
+		stringCol := sortedByInts.Column(1).(*array.String)
+		for i := 0; i < int(sortedByInts.NumRows()); i++ {
+			strings[i] = stringCol.Value(i)
+		}
+		require.Equal(t, []string{"d", "a", "b", "", "c"}, strings)
+	}
+
+	// Sort the record by the second column - string
+	{
+		sortedIndices, err := SortRecord(mem, record, []int{record.Schema().FieldIndices("string")[0]})
+		require.NoError(t, err)
+		require.Equal(t, []int64{4, 2, 1, 0, 3}, sortedIndices.Int64Values())
+
+		sortedByStrings, err := ReorderRecord(ctx, record, sortedIndices)
+		require.NoError(t, err)
+
+		// check that the column got sortedByInts
+		intCol := sortedByStrings.Column(0).(*array.Int64)
+		require.Equal(t, []int64{1, 3, 0, 0, 5}, intCol.Int64Values())
+		// make sure the other column got updated too
+		strings := make([]string, sortedByStrings.NumRows())
+		stringCol := sortedByStrings.Column(1).(*array.String)
+		for i := 0; i < int(sortedByStrings.NumRows()); i++ {
+			strings[i] = stringCol.Value(i)
+		}
+		require.Equal(t, []string{"a", "b", "c", "d", ""}, strings)
+		require.True(t, stringCol.IsNull(stringCol.Len()-1)) // last is NULL
+	}
+}
diff --git a/snapshot_test.go b/snapshot_test.go
index 465c92c..3f8bf54 100644
--- a/snapshot_test.go
+++ b/snapshot_test.go
@@ -28,8 +28,8 @@ func insertSampleRecords(ctx context.Context, t *testing.T, table *Table, timest
 	for _, ts := range timestamps {
 		samples = append(samples, dynparquet.Sample{
 			ExampleType: "ex",
-			Labels: []dynparquet.Label{
-				{Name: "label1", Value: "value1"},
+			Labels: map[string]string{
+				"label1": "value1",
 			},
 			Stacktrace: []uuid.UUID{
 				{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
diff --git a/table_test.go b/table_test.go
index cce1dc9..c8ca3f9 100644
--- a/table_test.go
+++ b/table_test.go
@@ -92,9 +92,9 @@ func Test_Table_Concurrency(t *testing.T) {
 				rows := make(dynparquet.Samples, 0, n)
 				for i := 0; i < n; i++ {
 					rows = append(rows, dynparquet.Sample{
-						Labels: []dynparquet.Label{ // TODO would be nice to not have all the same column
-							{Name: "label1", Value: "value1"},
-							{Name: "label2", Value: "value2"},
+						Labels: map[string]string{ // TODO would be nice to not have all the same column
+							"label1": "value1",
+							"label2": "value2",
 						},
 						Stacktrace: []uuid.UUID{
 							{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -199,9 +199,9 @@ func benchmarkTableInserts(b *testing.B, rows, iterations, writers int) {
 		rows := make(dynparquet.Samples, 0, n)
 		for i := 0; i < n; i++ {
 			rows = append(rows, dynparquet.Sample{
-				Labels: []dynparquet.Label{ // TODO would be nice to not have all the same column
-					{Name: "label1", Value: id},
-					{Name: "label2", Value: "value2"},
+				Labels: map[string]string{ // TODO would be nice to not have all the same column
+					"label1": id,
+					"label2": "value2",
 				},
 				Stacktrace: []uuid.UUID{
 					{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -286,9 +286,9 @@ func Test_Table_ReadIsolation(t *testing.T) {
 	defer c.Close()
 
 	samples := dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -297,10 +297,10 @@ func Test_Table_ReadIsolation(t *testing.T) {
 		Timestamp: 1,
 		Value:     1,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -309,10 +309,10 @@ func Test_Table_ReadIsolation(t *testing.T) {
 		Timestamp: 2,
 		Value:     2,
 	}, {
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -332,9 +332,9 @@ func Test_Table_ReadIsolation(t *testing.T) {
 
 	// Perform a new insert that will have a higher tx id
 	samples = dynparquet.Samples{{
-		Labels: []dynparquet.Label{
-			{Name: "blarg", Value: "blarg"},
-			{Name: "blah", Value: "blah"},
+		Labels: map[string]string{
+			"blarg": "blarg",
+			"blah":  "blah",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -460,9 +460,9 @@ func Test_Table_Bloomfilter(t *testing.T) {
 
 	samples := dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -472,10 +472,10 @@ func Test_Table_Bloomfilter(t *testing.T) {
 		Value:     1,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -485,10 +485,10 @@ func Test_Table_Bloomfilter(t *testing.T) {
 		Value:     2,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -580,9 +580,9 @@ func Test_L0Query(t *testing.T) {
 
 	samples := dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -592,10 +592,10 @@ func Test_L0Query(t *testing.T) {
 		Value:     1,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -605,10 +605,10 @@ func Test_L0Query(t *testing.T) {
 		Value:     2,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -653,9 +653,9 @@ func Test_Serialize_DisparateDynamicColumns(t *testing.T) {
 
 	samples := dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value1"},
-			{Name: "label2", Value: "value2"},
+		Labels: map[string]string{
+			"label1": "value1",
+			"label2": "value2",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -665,10 +665,10 @@ func Test_Serialize_DisparateDynamicColumns(t *testing.T) {
 		Value:     1,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value2"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label3", Value: "value3"},
+		Labels: map[string]string{
+			"label1": "value2",
+			"label2": "value2",
+			"label3": "value3",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -678,10 +678,10 @@ func Test_Serialize_DisparateDynamicColumns(t *testing.T) {
 		Value:     2,
 	}, {
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label1", Value: "value3"},
-			{Name: "label2", Value: "value2"},
-			{Name: "label4", Value: "value4"},
+		Labels: map[string]string{
+			"label1": "value3",
+			"label2": "value2",
+			"label4": "value4",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},
@@ -700,8 +700,8 @@ func Test_Serialize_DisparateDynamicColumns(t *testing.T) {
 
 	samples = dynparquet.Samples{{
 		ExampleType: "test",
-		Labels: []dynparquet.Label{
-			{Name: "label100", Value: "a"},
+		Labels: map[string]string{
+			"label100": "a",
 		},
 		Stacktrace: []uuid.UUID{
 			{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1},