diff --git a/internal/planner/average.go b/internal/planner/average.go index 76bbfc107d..022ff014b6 100644 --- a/internal/planner/average.go +++ b/internal/planner/average.go @@ -75,38 +75,47 @@ func (n *averageNode) Close() error { return n.plan.Close() func (n *averageNode) Source() planNode { return n.plan } func (n *averageNode) Next() (bool, error) { - n.execInfo.iterations++ + for { + n.execInfo.iterations++ - hasNext, err := n.plan.Next() - if err != nil || !hasNext { - return hasNext, err - } + hasNext, err := n.plan.Next() + if err != nil || !hasNext { + return hasNext, err + } - n.currentValue = n.plan.Value() + n.currentValue = n.plan.Value() - countProp := n.currentValue.Fields[n.countFieldIndex] - typedCount, isInt := countProp.(int) - if !isInt { - return false, client.NewErrUnexpectedType[int]("count", countProp) - } - count := typedCount + countProp := n.currentValue.Fields[n.countFieldIndex] + typedCount, isInt := countProp.(int) + if !isInt { + return false, client.NewErrUnexpectedType[int]("count", countProp) + } + count := typedCount - if count == 0 { - n.currentValue.Fields[n.virtualFieldIndex] = float64(0) - return true, nil - } + if count == 0 { + n.currentValue.Fields[n.virtualFieldIndex] = float64(0) + return true, nil + } - sumProp := n.currentValue.Fields[n.sumFieldIndex] - switch sum := sumProp.(type) { - case float64: - n.currentValue.Fields[n.virtualFieldIndex] = sum / float64(count) - case int64: - n.currentValue.Fields[n.virtualFieldIndex] = float64(sum) / float64(count) - default: - return false, client.NewErrUnhandledType("sum", sumProp) - } + sumProp := n.currentValue.Fields[n.sumFieldIndex] + switch sum := sumProp.(type) { + case float64: + n.currentValue.Fields[n.virtualFieldIndex] = sum / float64(count) + case int64: + n.currentValue.Fields[n.virtualFieldIndex] = float64(sum) / float64(count) + default: + return false, client.NewErrUnhandledType("sum", sumProp) + } - return mapper.RunFilter(n.currentValue, n.aggregateFilter) + passes, err := mapper.RunFilter(n.currentValue, n.aggregateFilter) + if err != nil { + return false, err + } + if !passes { + continue + } + return true, nil + } } func (n *averageNode) SetPlan(p planNode) { n.plan = p } diff --git a/internal/planner/count.go b/internal/planner/count.go index 1b58109749..a7b243bb8d 100644 --- a/internal/planner/count.go +++ b/internal/planner/count.go @@ -125,65 +125,74 @@ func (n *countNode) Explain(explainType request.ExplainType) (map[string]any, er } func (n *countNode) Next() (bool, error) { - n.execInfo.iterations++ + for { + n.execInfo.iterations++ - hasValue, err := n.plan.Next() - if err != nil || !hasValue { - return hasValue, err - } - - n.currentValue = n.plan.Value() - // Can just scan for now, can be replaced later by something fancier if needed - var count int - for _, source := range n.aggregateMapping { - property := n.currentValue.Fields[source.Index] - v := reflect.ValueOf(property) - switch v.Kind() { - // v.Len will panic if v is not one of these types, we don't want it to panic - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: - if source.Filter == nil && source.Limit == nil { - count = count + v.Len() - } else { - var arrayCount int - var err error - switch array := property.(type) { - case []core.Doc: - arrayCount = countDocs(array) - - case []bool: - arrayCount, err = countItems(array, source.Filter, source.Limit) - - case []immutable.Option[bool]: - arrayCount, err = countItems(array, source.Filter, source.Limit) - - case []int64: - arrayCount, err = countItems(array, source.Filter, source.Limit) - - case []immutable.Option[int64]: - arrayCount, err = countItems(array, source.Filter, source.Limit) - - case []float64: - arrayCount, err = countItems(array, source.Filter, source.Limit) - - case []immutable.Option[float64]: - arrayCount, err = countItems(array, source.Filter, source.Limit) - - case []string: - arrayCount, err = countItems(array, source.Filter, source.Limit) + hasValue, err := n.plan.Next() + if err != nil || !hasValue { + return hasValue, err + } - case []immutable.Option[string]: - arrayCount, err = countItems(array, source.Filter, source.Limit) - } - if err != nil { - return false, err + n.currentValue = n.plan.Value() + // Can just scan for now, can be replaced later by something fancier if needed + var count int + for _, source := range n.aggregateMapping { + property := n.currentValue.Fields[source.Index] + v := reflect.ValueOf(property) + switch v.Kind() { + // v.Len will panic if v is not one of these types, we don't want it to panic + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: + if source.Filter == nil && source.Limit == nil { + count = count + v.Len() + } else { + var arrayCount int + var err error + switch array := property.(type) { + case []core.Doc: + arrayCount = countDocs(array) + + case []bool: + arrayCount, err = countItems(array, source.Filter, source.Limit) + + case []immutable.Option[bool]: + arrayCount, err = countItems(array, source.Filter, source.Limit) + + case []int64: + arrayCount, err = countItems(array, source.Filter, source.Limit) + + case []immutable.Option[int64]: + arrayCount, err = countItems(array, source.Filter, source.Limit) + + case []float64: + arrayCount, err = countItems(array, source.Filter, source.Limit) + + case []immutable.Option[float64]: + arrayCount, err = countItems(array, source.Filter, source.Limit) + + case []string: + arrayCount, err = countItems(array, source.Filter, source.Limit) + + case []immutable.Option[string]: + arrayCount, err = countItems(array, source.Filter, source.Limit) + } + if err != nil { + return false, err + } + count += arrayCount } - count += arrayCount } } - } + n.currentValue.Fields[n.virtualFieldIndex] = count - n.currentValue.Fields[n.virtualFieldIndex] = count - return mapper.RunFilter(n.currentValue, n.aggregateFilter) + passes, err := mapper.RunFilter(n.currentValue, n.aggregateFilter) + if err != nil { + return false, err + } + if !passes { + continue + } + return true, nil + } } // countDocs counts the number of documents in a slice, skipping over hidden items diff --git a/internal/planner/max.go b/internal/planner/max.go index 530e60e25e..502b401d8f 100644 --- a/internal/planner/max.go +++ b/internal/planner/max.go @@ -124,136 +124,146 @@ func (n *maxNode) Explain(explainType request.ExplainType) (map[string]any, erro } func (n *maxNode) Next() (bool, error) { - n.execInfo.iterations++ + for { + n.execInfo.iterations++ - hasNext, err := n.plan.Next() - if err != nil || !hasNext { - return hasNext, err - } - n.currentValue = n.plan.Value() + hasNext, err := n.plan.Next() + if err != nil || !hasNext { + return hasNext, err + } + n.currentValue = n.plan.Value() - var max *big.Float - isFloat := false + var max *big.Float + isFloat := false - for _, source := range n.aggregateMapping { - child := n.currentValue.Fields[source.Index] - var collectionMax *big.Float - var err error - switch childCollection := child.(type) { - case []core.Doc: - collectionMax = reduceDocs( - childCollection, - nil, - func(childItem core.Doc, value *big.Float) *big.Float { - childProperty := childItem.Fields[source.ChildTarget.Index] - res := &big.Float{} - switch v := childProperty.(type) { - case int: - res = res.SetInt64(int64(v)) - case int64: - res = res.SetInt64(v) - case uint64: - res = res.SetUint64(v) - case float64: - res = res.SetFloat64(v) - default: - return nil - } - if value == nil || res.Cmp(value) > 0 { - return res - } - return value - }, - ) + for _, source := range n.aggregateMapping { + child := n.currentValue.Fields[source.Index] + var collectionMax *big.Float + var err error + switch childCollection := child.(type) { + case []core.Doc: + collectionMax = reduceDocs( + childCollection, + nil, + func(childItem core.Doc, value *big.Float) *big.Float { + childProperty := childItem.Fields[source.ChildTarget.Index] + res := &big.Float{} + switch v := childProperty.(type) { + case int: + res = res.SetInt64(int64(v)) + case int64: + res = res.SetInt64(v) + case uint64: + res = res.SetUint64(v) + case float64: + res = res.SetFloat64(v) + default: + return nil + } + if value == nil || res.Cmp(value) > 0 { + return res + } + return value + }, + ) - case []int64: - collectionMax, err = reduceItems( - childCollection, - &source, - lessN[int64], - nil, - func(childItem int64, value *big.Float) *big.Float { - res := (&big.Float{}).SetInt64(childItem) - if value == nil || res.Cmp(value) > 0 { - return res - } - return value - }, - ) + case []int64: + collectionMax, err = reduceItems( + childCollection, + &source, + lessN[int64], + nil, + func(childItem int64, value *big.Float) *big.Float { + res := (&big.Float{}).SetInt64(childItem) + if value == nil || res.Cmp(value) > 0 { + return res + } + return value + }, + ) - case []immutable.Option[int64]: - collectionMax, err = reduceItems( - childCollection, - &source, - lessO[int64], - nil, - func(childItem immutable.Option[int64], value *big.Float) *big.Float { - if !childItem.HasValue() { + case []immutable.Option[int64]: + collectionMax, err = reduceItems( + childCollection, + &source, + lessO[int64], + nil, + func(childItem immutable.Option[int64], value *big.Float) *big.Float { + if !childItem.HasValue() { + return value + } + res := (&big.Float{}).SetInt64(childItem.Value()) + if value == nil || res.Cmp(value) > 0 { + return res + } return value - } - res := (&big.Float{}).SetInt64(childItem.Value()) - if value == nil || res.Cmp(value) > 0 { - return res - } - return value - }, - ) + }, + ) - case []float64: - collectionMax, err = reduceItems( - childCollection, - &source, - lessN[float64], - nil, - func(childItem float64, value *big.Float) *big.Float { - res := big.NewFloat(childItem) - if value == nil || res.Cmp(value) > 0 { - return res - } - return value - }, - ) + case []float64: + collectionMax, err = reduceItems( + childCollection, + &source, + lessN[float64], + nil, + func(childItem float64, value *big.Float) *big.Float { + res := big.NewFloat(childItem) + if value == nil || res.Cmp(value) > 0 { + return res + } + return value + }, + ) - case []immutable.Option[float64]: - collectionMax, err = reduceItems( - childCollection, - &source, - lessO[float64], - nil, - func(childItem immutable.Option[float64], value *big.Float) *big.Float { - if !childItem.HasValue() { + case []immutable.Option[float64]: + collectionMax, err = reduceItems( + childCollection, + &source, + lessO[float64], + nil, + func(childItem immutable.Option[float64], value *big.Float) *big.Float { + if !childItem.HasValue() { + return value + } + res := big.NewFloat(childItem.Value()) + if value == nil || res.Cmp(value) > 0 { + return res + } return value - } - res := big.NewFloat(childItem.Value()) - if value == nil || res.Cmp(value) > 0 { - return res - } - return value - }, - ) + }, + ) + } + if err != nil { + return false, err + } + if collectionMax == nil || (max != nil && collectionMax.Cmp(max) <= 0) { + continue + } + isTargetFloat, err := n.p.isValueFloat(n.parent, &source) + if err != nil { + return false, err + } + isFloat = isTargetFloat + max = collectionMax } + + if max == nil { + n.currentValue.Fields[n.virtualFieldIndex] = nil + } else if isFloat { + res, _ := max.Float64() + n.currentValue.Fields[n.virtualFieldIndex] = res + } else { + res, _ := max.Int64() + n.currentValue.Fields[n.virtualFieldIndex] = res + } + + passes, err := mapper.RunFilter(n.currentValue, n.aggregateFilter) if err != nil { return false, err } - if collectionMax == nil || (max != nil && collectionMax.Cmp(max) <= 0) { + if !passes { continue } - isTargetFloat, err := n.p.isValueFloat(n.parent, &source) - if err != nil { - return false, err - } - isFloat = isTargetFloat - max = collectionMax - } - - if max == nil { - n.currentValue.Fields[n.virtualFieldIndex] = nil - } else if isFloat { - res, _ := max.Float64() - n.currentValue.Fields[n.virtualFieldIndex] = res - } else { - res, _ := max.Int64() - n.currentValue.Fields[n.virtualFieldIndex] = res + return true, nil } - return mapper.RunFilter(n.currentValue, n.aggregateFilter) } diff --git a/internal/planner/min.go b/internal/planner/min.go index be70a8ccb9..ca67d8d553 100644 --- a/internal/planner/min.go +++ b/internal/planner/min.go @@ -124,136 +124,146 @@ func (n *minNode) Explain(explainType request.ExplainType) (map[string]any, erro } func (n *minNode) Next() (bool, error) { - n.execInfo.iterations++ + for { + n.execInfo.iterations++ - hasNext, err := n.plan.Next() - if err != nil || !hasNext { - return hasNext, err - } - n.currentValue = n.plan.Value() + hasNext, err := n.plan.Next() + if err != nil || !hasNext { + return hasNext, err + } + n.currentValue = n.plan.Value() - var min *big.Float - isFloat := false + var min *big.Float + isFloat := false - for _, source := range n.aggregateMapping { - child := n.currentValue.Fields[source.Index] - var collectionMin *big.Float - var err error - switch childCollection := child.(type) { - case []core.Doc: - collectionMin = reduceDocs( - childCollection, - nil, - func(childItem core.Doc, value *big.Float) *big.Float { - childProperty := childItem.Fields[source.ChildTarget.Index] - res := &big.Float{} - switch v := childProperty.(type) { - case int: - res = res.SetInt64(int64(v)) - case int64: - res = res.SetInt64(v) - case uint64: - res = res.SetUint64(v) - case float64: - res = res.SetFloat64(v) - default: - return nil - } - if value == nil || res.Cmp(value) < 0 { - return res - } - return value - }, - ) + for _, source := range n.aggregateMapping { + child := n.currentValue.Fields[source.Index] + var collectionMin *big.Float + var err error + switch childCollection := child.(type) { + case []core.Doc: + collectionMin = reduceDocs( + childCollection, + nil, + func(childItem core.Doc, value *big.Float) *big.Float { + childProperty := childItem.Fields[source.ChildTarget.Index] + res := &big.Float{} + switch v := childProperty.(type) { + case int: + res = res.SetInt64(int64(v)) + case int64: + res = res.SetInt64(v) + case uint64: + res = res.SetUint64(v) + case float64: + res = res.SetFloat64(v) + default: + return nil + } + if value == nil || res.Cmp(value) < 0 { + return res + } + return value + }, + ) - case []int64: - collectionMin, err = reduceItems( - childCollection, - &source, - lessN[int64], - nil, - func(childItem int64, value *big.Float) *big.Float { - res := (&big.Float{}).SetInt64(childItem) - if value == nil || res.Cmp(value) < 0 { - return res - } - return value - }, - ) + case []int64: + collectionMin, err = reduceItems( + childCollection, + &source, + lessN[int64], + nil, + func(childItem int64, value *big.Float) *big.Float { + res := (&big.Float{}).SetInt64(childItem) + if value == nil || res.Cmp(value) < 0 { + return res + } + return value + }, + ) - case []immutable.Option[int64]: - collectionMin, err = reduceItems( - childCollection, - &source, - lessO[int64], - nil, - func(childItem immutable.Option[int64], value *big.Float) *big.Float { - if !childItem.HasValue() { + case []immutable.Option[int64]: + collectionMin, err = reduceItems( + childCollection, + &source, + lessO[int64], + nil, + func(childItem immutable.Option[int64], value *big.Float) *big.Float { + if !childItem.HasValue() { + return value + } + res := (&big.Float{}).SetInt64(childItem.Value()) + if value == nil || res.Cmp(value) < 0 { + return res + } return value - } - res := (&big.Float{}).SetInt64(childItem.Value()) - if value == nil || res.Cmp(value) < 0 { - return res - } - return value - }, - ) + }, + ) - case []float64: - collectionMin, err = reduceItems( - childCollection, - &source, - lessN[float64], - nil, - func(childItem float64, value *big.Float) *big.Float { - res := big.NewFloat(childItem) - if value == nil || res.Cmp(value) < 0 { - return res - } - return value - }, - ) + case []float64: + collectionMin, err = reduceItems( + childCollection, + &source, + lessN[float64], + nil, + func(childItem float64, value *big.Float) *big.Float { + res := big.NewFloat(childItem) + if value == nil || res.Cmp(value) < 0 { + return res + } + return value + }, + ) - case []immutable.Option[float64]: - collectionMin, err = reduceItems( - childCollection, - &source, - lessO[float64], - nil, - func(childItem immutable.Option[float64], value *big.Float) *big.Float { - if !childItem.HasValue() { + case []immutable.Option[float64]: + collectionMin, err = reduceItems( + childCollection, + &source, + lessO[float64], + nil, + func(childItem immutable.Option[float64], value *big.Float) *big.Float { + if !childItem.HasValue() { + return value + } + res := big.NewFloat(childItem.Value()) + if value == nil || res.Cmp(value) < 0 { + return res + } return value - } - res := big.NewFloat(childItem.Value()) - if value == nil || res.Cmp(value) < 0 { - return res - } - return value - }, - ) + }, + ) + } + if err != nil { + return false, err + } + if collectionMin == nil || (min != nil && collectionMin.Cmp(min) >= 0) { + continue + } + isTargetFloat, err := n.p.isValueFloat(n.parent, &source) + if err != nil { + return false, err + } + isFloat = isTargetFloat + min = collectionMin } + + if min == nil { + n.currentValue.Fields[n.virtualFieldIndex] = nil + } else if isFloat { + res, _ := min.Float64() + n.currentValue.Fields[n.virtualFieldIndex] = res + } else { + res, _ := min.Int64() + n.currentValue.Fields[n.virtualFieldIndex] = res + } + + passes, err := mapper.RunFilter(n.currentValue, n.aggregateFilter) if err != nil { return false, err } - if collectionMin == nil || (min != nil && collectionMin.Cmp(min) >= 0) { + if !passes { continue } - isTargetFloat, err := n.p.isValueFloat(n.parent, &source) - if err != nil { - return false, err - } - isFloat = isTargetFloat - min = collectionMin - } - - if min == nil { - n.currentValue.Fields[n.virtualFieldIndex] = nil - } else if isFloat { - res, _ := min.Float64() - n.currentValue.Fields[n.virtualFieldIndex] = res - } else { - res, _ := min.Int64() - n.currentValue.Fields[n.virtualFieldIndex] = res + return true, nil } - return mapper.RunFilter(n.currentValue, n.aggregateFilter) } diff --git a/internal/planner/sum.go b/internal/planner/sum.go index a77e56da3d..26f49d9ab3 100644 --- a/internal/planner/sum.go +++ b/internal/planner/sum.go @@ -217,103 +217,112 @@ func (n *sumNode) Explain(explainType request.ExplainType) (map[string]any, erro } func (n *sumNode) Next() (bool, error) { - n.execInfo.iterations++ + for { + n.execInfo.iterations++ - hasNext, err := n.plan.Next() - if err != nil || !hasNext { - return hasNext, err - } - - n.currentValue = n.plan.Value() - - sum := float64(0) - - for _, source := range n.aggregateMapping { - child := n.currentValue.Fields[source.Index] - var collectionSum float64 - var err error - switch childCollection := child.(type) { - case []core.Doc: - collectionSum = reduceDocs(childCollection, 0, func(childItem core.Doc, value float64) float64 { - childProperty := childItem.Fields[source.ChildTarget.Index] - switch v := childProperty.(type) { - case int: - return value + float64(v) - case int64: - return value + float64(v) - case uint64: - return value + float64(v) - case float64: - return value + v - default: - // return nothing, cannot be summed - return value + 0 - } - }) - case []int64: - collectionSum, err = reduceItems( - childCollection, - &source, - lessN[int64], - 0, - func(childItem int64, value float64) float64 { - return value + float64(childItem) - }, - ) + hasNext, err := n.plan.Next() + if err != nil || !hasNext { + return hasNext, err + } - case []immutable.Option[int64]: - collectionSum, err = reduceItems( - childCollection, - &source, - lessO[int64], - 0, - func(childItem immutable.Option[int64], value float64) float64 { - if !childItem.HasValue() { + n.currentValue = n.plan.Value() + + sum := float64(0) + + for _, source := range n.aggregateMapping { + child := n.currentValue.Fields[source.Index] + var collectionSum float64 + var err error + switch childCollection := child.(type) { + case []core.Doc: + collectionSum = reduceDocs(childCollection, 0, func(childItem core.Doc, value float64) float64 { + childProperty := childItem.Fields[source.ChildTarget.Index] + switch v := childProperty.(type) { + case int: + return value + float64(v) + case int64: + return value + float64(v) + case uint64: + return value + float64(v) + case float64: + return value + v + default: + // return nothing, cannot be summed return value + 0 } - return value + float64(childItem.Value()) - }, - ) - - case []float64: - collectionSum, err = reduceItems( - childCollection, - &source, - lessN[float64], - 0, - func(childItem float64, value float64) float64 { - return value + childItem - }, - ) + }) + case []int64: + collectionSum, err = reduceItems( + childCollection, + &source, + lessN[int64], + 0, + func(childItem int64, value float64) float64 { + return value + float64(childItem) + }, + ) + + case []immutable.Option[int64]: + collectionSum, err = reduceItems( + childCollection, + &source, + lessO[int64], + 0, + func(childItem immutable.Option[int64], value float64) float64 { + if !childItem.HasValue() { + return value + 0 + } + return value + float64(childItem.Value()) + }, + ) + + case []float64: + collectionSum, err = reduceItems( + childCollection, + &source, + lessN[float64], + 0, + func(childItem float64, value float64) float64 { + return value + childItem + }, + ) + + case []immutable.Option[float64]: + collectionSum, err = reduceItems( + childCollection, + &source, + lessO[float64], + 0, + func(childItem immutable.Option[float64], value float64) float64 { + if !childItem.HasValue() { + return value + 0 + } + return value + childItem.Value() + }, + ) + } + if err != nil { + return false, err + } + sum += collectionSum + } - case []immutable.Option[float64]: - collectionSum, err = reduceItems( - childCollection, - &source, - lessO[float64], - 0, - func(childItem immutable.Option[float64], value float64) float64 { - if !childItem.HasValue() { - return value + 0 - } - return value + childItem.Value() - }, - ) + var typedSum any + if n.isFloat { + typedSum = sum + } else { + typedSum = int64(sum) } + n.currentValue.Fields[n.virtualFieldIndex] = typedSum + passes, err := mapper.RunFilter(n.currentValue, n.aggregateFilter) if err != nil { return false, err } - sum += collectionSum - } - - var typedSum any - if n.isFloat { - typedSum = sum - } else { - typedSum = int64(sum) + if !passes { + continue + } + return true, nil } - n.currentValue.Fields[n.virtualFieldIndex] = typedSum - return mapper.RunFilter(n.currentValue, n.aggregateFilter) } func (n *sumNode) SetPlan(p planNode) { n.plan = p } diff --git a/tests/integration/query/one_to_many/with_average_test.go b/tests/integration/query/one_to_many/with_average_test.go new file mode 100644 index 0000000000..40306a33b1 --- /dev/null +++ b/tests/integration/query/one_to_many/with_average_test.go @@ -0,0 +1,153 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package one_to_many + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryOneToMany_WithAverageAliasFilter_ShouldMatchAll(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with average alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {averageRating: {_gt: 0}}}) { + name + averageRating: _avg(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "Cornelia Funke", + "averageRating": 4.8, + }, + { + "name": "John Grisham", + "averageRating": 4.7, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryOneToMany_WithAverageAliasFilter_ShouldMatchOne(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with average alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {averageRating: {_lt: 4.8}}}) { + name + averageRating: _avg(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "John Grisham", + "averageRating": 4.7, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/one_to_many/with_count_test.go b/tests/integration/query/one_to_many/with_count_test.go index 77905ed748..567642a067 100644 --- a/tests/integration/query/one_to_many/with_count_test.go +++ b/tests/integration/query/one_to_many/with_count_test.go @@ -188,3 +188,69 @@ func TestQueryOneToMany_WithCountAliasFilter_ShouldMatchAll(t *testing.T) { executeTestCase(t, test) } + +func TestQueryOneToMany_WithCountAliasFilter_ShouldMatchOne(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with count alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {publishedCount: {_gt: 1}}}) { + name + publishedCount: _count(published: {}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "John Grisham", + "publishedCount": 2, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/one_to_many/with_max_test.go b/tests/integration/query/one_to_many/with_max_test.go new file mode 100644 index 0000000000..2cc3031ea4 --- /dev/null +++ b/tests/integration/query/one_to_many/with_max_test.go @@ -0,0 +1,153 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package one_to_many + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryOneToMany_WithMaxAliasFilter_ShouldMatchAll(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with max alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {maxRating: {_gt: 0}}}) { + name + maxRating: _max(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "Cornelia Funke", + "maxRating": 4.8, + }, + { + "name": "John Grisham", + "maxRating": 4.9, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryOneToMany_WithMaxAliasFilter_ShouldMatchOne(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with max alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {maxRating: {_gt: 4.8}}}) { + name + maxRating: _max(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "John Grisham", + "maxRating": 4.9, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/one_to_many/with_min_test.go b/tests/integration/query/one_to_many/with_min_test.go new file mode 100644 index 0000000000..3325ccbe5a --- /dev/null +++ b/tests/integration/query/one_to_many/with_min_test.go @@ -0,0 +1,153 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package one_to_many + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryOneToMany_WithMinAliasFilter_ShouldMatchAll(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with min alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {minRating: {_gt: 0}}}) { + name + minRating: _min(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "Cornelia Funke", + "minRating": 4.8, + }, + { + "name": "John Grisham", + "minRating": 4.5, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryOneToMany_WithMinAliasFilter_ShouldMatchOne(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with min alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {minRating: {_lt: 4.7}}}) { + name + minRating: _min(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "John Grisham", + "minRating": 4.5, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/one_to_many/with_sum_test.go b/tests/integration/query/one_to_many/with_sum_test.go new file mode 100644 index 0000000000..fb383d9a2f --- /dev/null +++ b/tests/integration/query/one_to_many/with_sum_test.go @@ -0,0 +1,153 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package one_to_many + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryOneToMany_WithSumAliasFilter_ShouldMatchAll(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with sum alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {totalRating: {_gt: 0}}}) { + name + totalRating: _sum(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "Cornelia Funke", + "totalRating": 4.8, + }, + { + "name": "John Grisham", + "totalRating": 9.4, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQueryOneToMany_WithSumAliasFilter_ShouldMatchOne(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with sum alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(filter: {_alias: {totalRating: {_gt: 5}}}) { + name + totalRating: _sum(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "John Grisham", + "totalRating": 9.4, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +}