Files
2023-10-04 21:04:26 +01:00

250 lines
5.5 KiB
Go

package query
import (
"context"
"errors"
"fmt"
"github.com/bitmagnet-io/bitmagnet/internal/database/dao"
"github.com/bitmagnet-io/bitmagnet/internal/maps"
"github.com/bitmagnet-io/bitmagnet/internal/model"
"sort"
"sync"
)
type FacetConfig interface {
Key() string
Label() string
Logic() model.FacetLogic
Filter() FacetFilter
IsAggregated() bool
AggregationOption(b OptionBuilder) (OptionBuilder, error)
}
type Facet interface {
FacetConfig
Aggregate(ctx FacetContext) (AggregationItems, error)
Criteria() []Criteria
}
type FacetFilter map[string]struct{}
// Values allows iteration over deterministically sorted filter values, which helps with query caching.
func (f FacetFilter) Values() []string {
values := make([]string, 0, len(f))
for v := range f {
values = append(values, v)
}
sort.Strings(values)
return values
}
type facetConfig struct {
key string
label string
logic model.FacetLogic
filter FacetFilter
aggregate bool
aggregationOptions []Option
}
type FacetOption func(facetConfig) facetConfig
func NewFacetConfig(options ...FacetOption) FacetConfig {
c := facetConfig{}
for _, option := range options {
c = option(c)
}
return c
}
type AggregationItem struct {
Label string
Count uint
}
type AggregationItems = map[string]AggregationItem
type AggregationGroup struct {
Label string
Logic model.FacetLogic
Items AggregationItems
}
type Aggregations = maps.StringMap[AggregationGroup]
type FacetContext interface {
DbContext
Context() context.Context
NewAggregationQuery(options ...Option) (SubQuery, error)
}
type facetContext struct {
optionBuilder OptionBuilder
ctx context.Context
}
func (ctx facetContext) Query() *dao.Query {
return ctx.optionBuilder.Query()
}
func (ctx facetContext) TableName() string {
return ctx.optionBuilder.TableName()
}
func (ctx facetContext) NewSubQuery(c context.Context) SubQuery {
return ctx.optionBuilder.NewSubQuery(c)
}
func (ctx facetContext) Context() context.Context {
return ctx.ctx
}
func (ctx facetContext) NewAggregationQuery(options ...Option) (SubQuery, error) {
subCtx, subErr := Options(options...)(ctx.optionBuilder)
if subErr != nil {
return nil, subErr
}
sq := ctx.optionBuilder.NewSubQuery(ctx.Context())
applyErr := subCtx.applyPre(sq)
if applyErr != nil {
return nil, applyErr
}
return sq, nil
}
func FacetHasKey(key string) FacetOption {
return func(a facetConfig) facetConfig {
a.key = key
return a
}
}
func FacetHasLabel(label string) FacetOption {
return func(a facetConfig) facetConfig {
a.label = label
return a
}
}
func FacetUsesLogic(logic model.FacetLogic) FacetOption {
return func(a facetConfig) facetConfig {
a.logic = logic
return a
}
}
func FacetUsesAndLogic() FacetOption {
return FacetUsesLogic(model.FacetLogicAnd)
}
func FacetUsesOrLogic() FacetOption {
return FacetUsesLogic(model.FacetLogicOr)
}
func FacetIsAggregated() FacetOption {
return func(c facetConfig) facetConfig {
c.aggregate = true
return c
}
}
func FacetHasFilter(filter FacetFilter) FacetOption {
return func(c facetConfig) facetConfig {
c.filter = filter
return c
}
}
func FacetHasAggregationOption(options ...Option) FacetOption {
return func(c facetConfig) facetConfig {
c.aggregationOptions = append(c.aggregationOptions, options...)
return c
}
}
func (c facetConfig) Key() string {
return c.key
}
func (c facetConfig) Label() string {
return c.label
}
func (c facetConfig) Logic() model.FacetLogic {
return c.logic
}
func (c facetConfig) IsAggregated() bool {
return c.aggregate
}
func (c facetConfig) AggregationOption(b OptionBuilder) (OptionBuilder, error) {
return Options(c.aggregationOptions...)(b)
}
func (c facetConfig) Filter() FacetFilter {
return c.filter
}
func (b optionBuilder) createFacetsFilterCriteria() (c Criteria, err error) {
cs := make([]Criteria, 0, len(b.facets))
for _, facet := range b.facets {
cr := facet.Criteria()
switch facet.Logic() {
case model.FacetLogicAnd:
cs = append(cs, AndCriteria{cr})
case model.FacetLogicOr:
if b.currentFacet != facet.Key() {
cs = append(cs, OrCriteria{cr})
}
}
}
return AndCriteria{cs}, nil
}
// when aggregating with or logic the current facet's filter should be ignored
func withCurrentFacet(facetKey string) Option {
return func(b OptionBuilder) (OptionBuilder, error) {
return b.withCurrentFacet(facetKey), nil
}
}
func (b optionBuilder) calculateAggregations(ctx context.Context) (Aggregations, error) {
aggregations := make(Aggregations, len(b.facets))
wg := sync.WaitGroup{}
wg.Add(len(b.facets))
mtx := sync.Mutex{}
var errs []error
for _, facet := range b.facets {
go (func(facet Facet) {
defer wg.Done()
if !facet.IsAggregated() {
return
}
aggBuilder, aggBuilderErr := Options(facet.AggregationOption, withCurrentFacet(facet.Key()))(b)
if aggBuilderErr != nil {
errs = append(errs, fmt.Errorf("failed to create aggregation option for key '%s': %w", facet.Key(), aggBuilderErr))
return
}
aggCtx := facetContext{
optionBuilder: aggBuilder,
ctx: ctx,
}
aggregation, aggregateErr := facet.Aggregate(aggCtx)
mtx.Lock()
defer mtx.Unlock()
if aggregateErr != nil {
errs = append(errs, fmt.Errorf("failed to aggregate key '%s': %w", facet.Key(), aggregateErr))
} else {
aggregations[facet.Key()] = AggregationGroup{
Label: facet.Label(),
Logic: facet.Logic(),
Items: aggregation,
}
}
})(facet)
}
wg.Wait()
return aggregations, errors.Join(errs...)
}