Skip to content

Commit 111885b

Browse files
Refactor tie-breakers in sortfilter
1 parent 54a007d commit 111885b

File tree

9 files changed

+99
-208
lines changed

9 files changed

+99
-208
lines changed

internal/graph/sortfilter/sortfilter.go

+65-33
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"slices"
7+
"strings"
78

89
"github.com/nais/api/internal/graph/model"
910
"github.com/sirupsen/logrus"
@@ -23,27 +24,27 @@ type ConcurrentSortFunc[T any] func(ctx context.Context, a T) int
2324
// Filter is a function that returns true if the given value should be included in the result.
2425
type Filter[T any, FilterObj any] func(ctx context.Context, v T, filter FilterObj) bool
2526

26-
// TieBreaker is a combination of a SortField and a direction that might be able to resolve equal fields during sorting.
27+
// tieBreaker is a combination of a SortField and a direction that might be able to resolve equal fields during sorting.
2728
// If the direction is not supplied, the direction used for the original sort will be used. The referenced field must be
2829
// registered with RegisterSort (concurrent tie-break sorters are not supported).
29-
type TieBreaker[SortField comparable] struct {
30-
Field SortField
31-
Direction *model.OrderDirection
30+
type tieBreaker[SortField ~string] struct {
31+
field SortField
32+
direction model.OrderDirection
3233
}
3334

34-
type funcs[T any, SortField comparable] struct {
35+
type funcs[T any, SortField ~string] struct {
3536
concurrentSort ConcurrentSortFunc[T]
3637
sort SortFunc[T]
37-
tieBreakers []TieBreaker[SortField]
38+
tieBreakers []tieBreaker[SortField]
3839
}
3940

40-
type SortFilter[T any, SortField comparable, FilterObj comparable] struct {
41+
type SortFilter[T any, SortField ~string, FilterObj comparable] struct {
4142
sorters map[SortField]funcs[T, SortField]
4243
filters []Filter[T, FilterObj]
4344
}
4445

4546
// New creates a new SortFilter
46-
func New[T any, SortField comparable, FilterObj comparable]() *SortFilter[T, SortField, FilterObj] {
47+
func New[T any, SortField ~string, FilterObj comparable]() *SortFilter[T, SortField, FilterObj] {
4748
return &SortFilter[T, SortField, FilterObj]{
4849
sorters: make(map[SortField]funcs[T, SortField]),
4950
}
@@ -55,29 +56,59 @@ func (s *SortFilter[T, SortField, FilterObj]) SupportsSort(field SortField) bool
5556
return exists
5657
}
5758

59+
// sortFieldsToTieBreakers takes a slice of SortField values and returns a list of tie-breakers. The values can be
60+
// suffixed with :ASC or :DESC to specify the direction. If no direction is supplied, ASC is assumed.
61+
func sortFieldsToTieBreakers[SortField ~string](fields []SortField) []tieBreaker[SortField] {
62+
ret := make([]tieBreaker[SortField], len(fields))
63+
for i, field := range fields {
64+
direction := model.OrderDirectionAsc
65+
if parts := strings.Split(string(field), ":"); len(parts) == 2 {
66+
if parts[1] != "ASC" && parts[1] != "DESC" {
67+
panic(fmt.Sprintf("invalid direction in sort field: %q", field))
68+
}
69+
70+
direction = model.OrderDirection(parts[1])
71+
field = SortField(parts[0])
72+
} else if len(parts) > 2 {
73+
panic(fmt.Sprintf("invalid sort field: %q", field))
74+
}
75+
76+
ret[i] = tieBreaker[SortField]{
77+
field: field,
78+
direction: direction,
79+
}
80+
}
81+
82+
return ret
83+
}
84+
85+
func tieBreakerToSortField[SortField ~string](tb tieBreaker[SortField]) SortField {
86+
return tb.field + ":" + SortField(tb.direction)
87+
}
88+
5889
// RegisterSort will add support for sorting on a specific field. Optional tie-breakers can be supplied to resolve equal
5990
// values, and will be executed in the given order.
60-
func (s *SortFilter[T, SortField, FilterObj]) RegisterSort(field SortField, sort SortFunc[T], tieBreakers ...TieBreaker[SortField]) {
91+
func (s *SortFilter[T, SortField, FilterObj]) RegisterSort(field SortField, sort SortFunc[T], tieBreakers ...SortField) {
6192
if _, ok := s.sorters[field]; ok {
6293
panic(fmt.Sprintf("sort field is already registered: %v", field))
6394
}
6495

6596
s.sorters[field] = funcs[T, SortField]{
6697
sort: sort,
67-
tieBreakers: tieBreakers,
98+
tieBreakers: sortFieldsToTieBreakers(tieBreakers),
6899
}
69100
}
70101

71102
// RegisterConcurrentSort will add support for doing concurrent sorting on a specific field. Optional tie-breakers can
72103
// be supplied to resolve equal values, and will be executed in the given order.
73-
func (s *SortFilter[T, SortField, FilterObj]) RegisterConcurrentSort(field SortField, sort ConcurrentSortFunc[T], tieBreakers ...TieBreaker[SortField]) {
104+
func (s *SortFilter[T, SortField, FilterObj]) RegisterConcurrentSort(field SortField, sort ConcurrentSortFunc[T], tieBreakers ...SortField) {
74105
if _, ok := s.sorters[field]; ok {
75106
panic(fmt.Sprintf("sort field is already registered: %v", field))
76107
}
77108

78109
s.sorters[field] = funcs[T, SortField]{
79110
concurrentSort: sort,
80-
tieBreakers: tieBreakers,
111+
tieBreakers: sortFieldsToTieBreakers(tieBreakers),
81112
}
82113
}
83114

@@ -139,14 +170,14 @@ func (s *SortFilter[T, SortField, FilterObj]) Sort(ctx context.Context, items []
139170
}
140171

141172
if sorter.concurrentSort != nil {
142-
s.sortConcurrent(ctx, items, sorter.concurrentSort, field, direction, sorter.tieBreakers...)
173+
s.sortConcurrent(ctx, items, sorter.concurrentSort, field, direction, sorter.tieBreakers)
143174
return
144175
}
145176

146-
s.sort(ctx, items, sorter.sort, field, direction, sorter.tieBreakers...)
177+
s.sort(ctx, items, sorter.sort, field, direction, sorter.tieBreakers)
147178
}
148179

149-
func (s *SortFilter[T, SortField, FilterObj]) sortConcurrent(ctx context.Context, items []T, sort ConcurrentSortFunc[T], field SortField, direction model.OrderDirection, tieBreakers ...TieBreaker[SortField]) {
180+
func (s *SortFilter[T, SortField, FilterObj]) sortConcurrent(ctx context.Context, items []T, sort ConcurrentSortFunc[T], field SortField, direction model.OrderDirection, tieBreakers []tieBreaker[SortField]) {
150181
type sortable struct {
151182
item T
152183
key int
@@ -170,7 +201,7 @@ func (s *SortFilter[T, SortField, FilterObj]) sortConcurrent(ctx context.Context
170201

171202
slices.SortStableFunc(res, func(a, b sortable) int {
172203
if b.key == a.key {
173-
return s.tieBreak(ctx, a.item, b.item, field, direction, tieBreakers...)
204+
return s.tieBreak(ctx, a.item, b.item, field, tieBreakers)
174205
}
175206

176207
if direction == model.OrderDirectionDesc {
@@ -184,7 +215,7 @@ func (s *SortFilter[T, SortField, FilterObj]) sortConcurrent(ctx context.Context
184215
}
185216
}
186217

187-
func (s *SortFilter[T, SortField, FilterObj]) sort(ctx context.Context, items []T, sort SortFunc[T], field SortField, direction model.OrderDirection, tieBreakers ...TieBreaker[SortField]) {
218+
func (s *SortFilter[T, SortField, FilterObj]) sort(ctx context.Context, items []T, sort SortFunc[T], field SortField, direction model.OrderDirection, tieBreakers []tieBreaker[SortField]) {
188219
slices.SortStableFunc(items, func(a, b T) int {
189220
var ret int
190221
if direction == model.OrderDirectionDesc {
@@ -194,38 +225,33 @@ func (s *SortFilter[T, SortField, FilterObj]) sort(ctx context.Context, items []
194225
}
195226

196227
if ret == 0 {
197-
return s.tieBreak(ctx, a, b, field, direction, tieBreakers...)
228+
return s.tieBreak(ctx, a, b, field, tieBreakers)
198229
}
199230
return ret
200231
})
201232
}
202233

203234
// tieBreak will resolve equal fields after the initial sort by using the supplied tie-breakers. The function will
204235
// return as soon as a tie-breaker returns a non-zero value.
205-
func (s *SortFilter[T, SortField, FilterObj]) tieBreak(ctx context.Context, a, b T, field SortField, direction model.OrderDirection, tieBreakers ...TieBreaker[SortField]) int {
236+
func (s *SortFilter[T, SortField, FilterObj]) tieBreak(ctx context.Context, a, b T, originalSortField SortField, tieBreakers []tieBreaker[SortField]) int {
206237
for _, tb := range tieBreakers {
207-
dir := direction
208-
if tb.Direction != nil {
209-
dir = *tb.Direction
210-
}
211-
212-
sorter, ok := s.sorters[tb.Field]
238+
sorter, ok := s.sorters[tb.field]
213239
if !ok {
214240
logrus.WithFields(logrus.Fields{
215-
"field_type": fmt.Sprintf("%T", field),
216-
"tie_breaker": tb.Field,
241+
"field_type": fmt.Sprintf("%T", originalSortField),
242+
"tie_breaker": tieBreakerToSortField(tb),
217243
}).Errorf("no sort registered for tie-breaker")
218244
continue
219245
} else if sorter.sort == nil {
220246
logrus.WithFields(logrus.Fields{
221-
"field_type": fmt.Sprintf("%T", field),
222-
"tie_breaker": tb.Field,
247+
"field_type": fmt.Sprintf("%T", originalSortField),
248+
"tie_breaker": tieBreakerToSortField(tb),
223249
}).Errorf("tie-breaker can not be a concurrent sort")
224250
continue
225251
}
226252

227253
var v int
228-
if dir == model.OrderDirectionDesc {
254+
if tb.direction == model.OrderDirectionDesc {
229255
v = sorter.sort(ctx, b, a)
230256
} else {
231257
v = sorter.sort(ctx, a, b)
@@ -238,9 +264,15 @@ func (s *SortFilter[T, SortField, FilterObj]) tieBreak(ctx context.Context, a, b
238264

239265
logrus.
240266
WithFields(logrus.Fields{
241-
"field_type": fmt.Sprintf("%T", field),
242-
"sort_field": field,
243-
"tie_breakers": tieBreakers,
267+
"field_type": fmt.Sprintf("%T", originalSortField),
268+
"sort_field": originalSortField,
269+
"tie_breakers": func(tbs []tieBreaker[SortField]) []SortField {
270+
ret := make([]SortField, len(tbs))
271+
for i, tb := range tbs {
272+
ret[i] = tieBreakerToSortField(tb)
273+
}
274+
return ret
275+
}(tieBreakers),
244276
}).
245277
Errorf("unable to tie-break sort, gotta have more tie-breakers")
246278
return 0

internal/persistence/bigquery/sortfilter.go

+2-14
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,21 @@ import (
44
"context"
55
"strings"
66

7-
"github.com/nais/api/internal/graph/model"
87
"github.com/nais/api/internal/graph/sortfilter"
9-
"k8s.io/utils/ptr"
108
)
119

1210
var (
1311
SortFilter = sortfilter.New[*BigQueryDataset, BigQueryDatasetOrderField, struct{}]()
1412
SortFilterAccess = sortfilter.New[*BigQueryDatasetAccess, BigQueryDatasetAccessOrderField, struct{}]()
1513
)
1614

17-
type (
18-
SortFilterTieBreaker = sortfilter.TieBreaker[BigQueryDatasetOrderField]
19-
)
20-
2115
func init() {
2216
SortFilter.RegisterSort("NAME", func(ctx context.Context, a, b *BigQueryDataset) int {
2317
return strings.Compare(a.GetName(), b.GetName())
24-
}, SortFilterTieBreaker{
25-
Field: "ENVIRONMENT",
26-
Direction: ptr.To(model.OrderDirectionAsc),
27-
})
18+
}, "ENVIRONMENT")
2819
SortFilter.RegisterSort("ENVIRONMENT", func(ctx context.Context, a, b *BigQueryDataset) int {
2920
return strings.Compare(a.EnvironmentName, b.EnvironmentName)
30-
}, SortFilterTieBreaker{
31-
Field: "NAME",
32-
Direction: ptr.To(model.OrderDirectionAsc),
33-
})
21+
}, "NAME")
3422

3523
SortFilterAccess.RegisterSort("EMAIL", func(ctx context.Context, a, b *BigQueryDatasetAccess) int {
3624
return strings.Compare(a.Email, b.Email)

internal/persistence/sqlinstance/sortfilter.go

+3-19
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,21 @@ import (
55
"strings"
66

77
"github.com/nais/api/internal/cost"
8-
"github.com/nais/api/internal/graph/model"
98
"github.com/nais/api/internal/graph/sortfilter"
10-
"k8s.io/utils/ptr"
119
)
1210

1311
var (
1412
SortFilterSQLInstance = sortfilter.New[*SQLInstance, SQLInstanceOrderField, struct{}]()
1513
SortFilterSQLInstanceUser = sortfilter.New[*SQLInstanceUser, SQLInstanceUserOrderField, struct{}]()
1614
)
1715

18-
type SortFilterTieBreaker = sortfilter.TieBreaker[SQLInstanceOrderField]
19-
2016
func init() {
2117
SortFilterSQLInstance.RegisterSort("NAME", func(ctx context.Context, a, b *SQLInstance) int {
2218
return strings.Compare(a.GetName(), b.GetName())
23-
}, SortFilterTieBreaker{
24-
Field: "ENVIRONMENT",
25-
Direction: ptr.To(model.OrderDirectionAsc),
26-
})
19+
}, "ENVIRONMENT")
2720
SortFilterSQLInstance.RegisterSort("ENVIRONMENT", func(ctx context.Context, a, b *SQLInstance) int {
2821
return strings.Compare(a.EnvironmentName, b.EnvironmentName)
29-
}, SortFilterTieBreaker{
30-
Field: "NAME",
31-
Direction: ptr.To(model.OrderDirectionAsc),
32-
})
22+
}, "NAME")
3323
SortFilterSQLInstance.RegisterSort("VERSION", func(ctx context.Context, a, b *SQLInstance) int {
3424
if a.Version == nil && b.Version == nil {
3525
return 0
@@ -39,13 +29,7 @@ func init() {
3929
return -1
4030
}
4131
return strings.Compare(*a.Version, *b.Version)
42-
}, SortFilterTieBreaker{
43-
Field: "NAME",
44-
Direction: ptr.To(model.OrderDirectionAsc),
45-
}, SortFilterTieBreaker{
46-
Field: "ENVIRONMENT",
47-
Direction: ptr.To(model.OrderDirectionAsc),
48-
})
32+
}, "NAME", "ENVIRONMENT")
4933
SortFilterSQLInstance.RegisterConcurrentSort("STATUS", func(ctx context.Context, a *SQLInstance) int {
5034
stateOrder := map[string]int{
5135
"UNSPECIFIED": 0,

internal/status/sortfilter.go

+3-23
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,21 @@ package status
33
import (
44
"context"
55

6-
"github.com/nais/api/internal/graph/model"
76
"github.com/nais/api/internal/workload"
87
"github.com/nais/api/internal/workload/application"
98
"github.com/nais/api/internal/workload/job"
10-
"k8s.io/utils/ptr"
119
)
1210

1311
func init() {
1412
application.SortFilter.RegisterConcurrentSort("STATUS", func(ctx context.Context, a *application.Application) int {
1513
return int(ForWorkload(ctx, a).State)
16-
}, application.SortFilterTieBreaker{
17-
Field: "NAME",
18-
Direction: ptr.To(model.OrderDirectionAsc),
19-
}, application.SortFilterTieBreaker{
20-
Field: "ENVIRONMENT",
21-
Direction: ptr.To(model.OrderDirectionAsc),
22-
})
14+
}, "NAME", "ENVIRONMENT")
2315

2416
job.SortFilter.RegisterConcurrentSort("STATUS", func(ctx context.Context, a *job.Job) int {
2517
return int(ForWorkload(ctx, a).State)
26-
}, job.SortFilterTieBreaker{
27-
Field: "NAME",
28-
Direction: ptr.To(model.OrderDirectionAsc),
29-
}, job.SortFilterTieBreaker{
30-
Field: "ENVIRONMENT",
31-
Direction: ptr.To(model.OrderDirectionAsc),
32-
})
18+
}, "NAME", "ENVIRONMENT")
3319

3420
workload.SortFilter.RegisterConcurrentSort("STATUS", func(ctx context.Context, a workload.Workload) int {
3521
return int(ForWorkload(ctx, a).State)
36-
}, workload.SortFilterTieBreaker{
37-
Field: "NAME",
38-
Direction: ptr.To(model.OrderDirectionAsc),
39-
}, workload.SortFilterTieBreaker{
40-
Field: "ENVIRONMENT",
41-
Direction: ptr.To(model.OrderDirectionAsc),
42-
})
22+
}, "NAME", "ENVIRONMENT")
4323
}

0 commit comments

Comments
 (0)