From e3b15bceb5effc9c78b139c091633143658d3131 Mon Sep 17 00:00:00 2001
From: Keenan Nemetz <keenan.nemetz@gmail.com>
Date: Wed, 11 Dec 2024 09:36:18 -0800
Subject: [PATCH] fix and add tests

---
 internal/planner/average.go                   |  61 +++--
 internal/planner/count.go                     | 115 +++++----
 internal/planner/max.go                       | 242 +++++++++---------
 internal/planner/min.go                       | 242 +++++++++---------
 internal/planner/sum.go                       | 183 ++++++-------
 .../query/one_to_many/with_average_test.go    | 153 +++++++++++
 .../query/one_to_many/with_count_test.go      |  66 +++++
 .../query/one_to_many/with_max_test.go        | 153 +++++++++++
 .../query/one_to_many/with_min_test.go        | 153 +++++++++++
 .../query/one_to_many/with_sum_test.go        | 153 +++++++++++
 10 files changed, 1123 insertions(+), 398 deletions(-)
 create mode 100644 tests/integration/query/one_to_many/with_average_test.go
 create mode 100644 tests/integration/query/one_to_many/with_max_test.go
 create mode 100644 tests/integration/query/one_to_many/with_min_test.go
 create mode 100644 tests/integration/query/one_to_many/with_sum_test.go

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)
+}