Skip to content

Commit

Permalink
Merge pull request #962 from go-kivik/moreStats
Browse files Browse the repository at this point in the history
More _stats support
  • Loading branch information
flimzy committed May 10, 2024
2 parents 472fa53 + 4e5a006 commit a680e92
Show file tree
Hide file tree
Showing 7 changed files with 499 additions and 261 deletions.
1 change: 1 addition & 0 deletions x/sqlite/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/dop251/goja v0.0.0-20240220182346-e401ed450204
github.com/go-kivik/kivik/v4 v4.2.2
github.com/google/go-cmp v0.6.0
github.com/mitchellh/mapstructure v1.5.0
gitlab.com/flimzy/testy v0.14.0
golang.org/x/text v0.15.0
modernc.org/sqlite v1.29.8
Expand Down
2 changes: 2 additions & 0 deletions x/sqlite/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand Down
90 changes: 0 additions & 90 deletions x/sqlite/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
package sqlite

import (
"math"
"net/http"
"strconv"

Expand Down Expand Up @@ -163,95 +162,6 @@ func toUint64(in interface{}, msg string) (uint64, error) {
}
}

// toInt64 converts the input to a int64. If the input is malformed, it
// returns an error with msg as the message, and 400 as the status code.
func toInt64(in interface{}, msg string) (int64, error) {
switch t := in.(type) {
case int:
return int64(t), nil
case int64:
return t, nil
case int8:
return int64(t), nil
case int16:
return int64(t), nil
case int32:
return int64(t), nil
case uint:
return int64(t), nil
case uint8:
return int64(t), nil
case uint16:
return int64(t), nil
case uint32:
return int64(t), nil
case uint64:
if t > math.MaxInt64 {
return 0, &internal.Error{Status: http.StatusBadRequest, Message: msg}
}
return int64(t), nil
case string:
i, err := strconv.ParseInt(t, 10, 64)
if err != nil {
return 0, &internal.Error{Status: http.StatusBadRequest, Message: msg}
}
return i, nil
case float32:
i := int64(t)
if float32(i) != t {
return 0, &internal.Error{Status: http.StatusBadRequest, Message: msg}
}
return i, nil
case float64:
i := int64(t)
if float64(i) != t {
return 0, &internal.Error{Status: http.StatusBadRequest, Message: msg}
}
return i, nil
default:
return 0, &internal.Error{Status: http.StatusBadRequest, Message: msg}
}
}

// toFloat64 converts the input to a float64. If the input is malformed, it
// returns an error with msg as the message, and 400 as the status code.
func toFloat64(in interface{}) (float64, bool) {
switch t := in.(type) {
case int:
return float64(t), true
case int64:
return float64(t), true
case int8:
return float64(t), true
case int16:
return float64(t), true
case int32:
return float64(t), true
case uint:
return float64(t), true
case uint8:
return float64(t), true
case uint16:
return float64(t), true
case uint32:
return float64(t), true
case uint64:
return float64(t), true
case string:
i, err := strconv.ParseFloat(t, 64)
if err != nil {
return 0, false
}
return i, true
case float32:
return float64(t), true
case float64:
return t, true
default:
return 0, false
}
}

func toBool(in interface{}) (value bool, ok bool) {
switch t := in.(type) {
case bool:
Expand Down
62 changes: 0 additions & 62 deletions x/sqlite/options_test.go

This file was deleted.

99 changes: 0 additions & 99 deletions x/sqlite/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"slices"
"strings"

"github.com/dop251/goja"
Expand Down Expand Up @@ -604,100 +602,3 @@ func (d *db) writeMapIndexBatch(ctx context.Context, seq int, rev revision, ddoc

return tx.Commit()
}

type reduceFunc func(keys [][2]interface{}, values []interface{}, rereduce bool) (interface{}, error)

func (d *db) reduceFunc(reduceFuncJS *string, logger *log.Logger) (reduceFunc, error) {
if reduceFuncJS == nil {
return nil, nil
}
switch *reduceFuncJS {
case "_count":
return func(_ [][2]interface{}, values []interface{}, rereduce bool) (interface{}, error) {
if !rereduce {
return len(values), nil
}
var total uint64
for _, value := range values {
v, _ := toUint64(value, "")
total += v
}
return total, nil
}, nil
case "_sum":
return func(_ [][2]interface{}, values []interface{}, _ bool) (interface{}, error) {
var total uint64
for _, value := range values {
v, _ := toUint64(value, "")
total += v
}
return total, nil
}, nil
case "_stats":
return func(_ [][2]interface{}, values []interface{}, rereduce bool) (interface{}, error) {
type stats struct {
Sum float64 `json:"sum"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Count float64 `json:"count"`
SumSqr float64 `json:"sumsqr"`
}
var result stats
if rereduce {
mins := make([]float64, 0, len(values))
maxs := make([]float64, 0, len(values))
for _, v := range values {
value := v.(stats)
mins = append(mins, value.Min)
maxs = append(maxs, value.Max)
result.Sum += value.Sum
result.Count += value.Count
result.SumSqr += value.SumSqr
}
result.Min = slices.Min(mins)
result.Max = slices.Max(maxs)
return result, nil
}
result.Count = float64(len(values))
nvals := make([]float64, 0, len(values))
for _, v := range values {
value, ok := toFloat64(v)
if !ok {
return nil, &internal.Error{
Status: http.StatusInternalServerError,
Message: fmt.Sprintf("the _stats function requires that map values be numbers or arrays of numbers, not '%s'", v),
}
}
nvals = append(nvals, value)
result.Sum += value
result.SumSqr += value * value
}
result.Min = slices.Min(nvals)
result.Max = slices.Max(nvals)
return result, nil
}, nil
default:
vm := goja.New()

if _, err := vm.RunString("const reduce = " + *reduceFuncJS); err != nil {
return nil, err
}
reduceFunc, ok := goja.AssertFunction(vm.Get("reduce"))
if !ok {
return nil, fmt.Errorf("expected reduce to be a function, got %T", vm.Get("map"))
}

return func(keys [][2]interface{}, values []interface{}, rereduce bool) (interface{}, error) {
reduceValue, err := reduceFunc(goja.Undefined(), vm.ToValue(keys), vm.ToValue(values), vm.ToValue(rereduce))
// According to CouchDB reference implementation, when a user-defined
// reduce function throws an exception, the error is logged and the
// return value is set to null.
if err != nil {
logger.Printf("reduce function threw exception: %s", err.Error())
return nil, nil
}

return reduceValue.Export(), nil
}, nil
}
}
Loading

0 comments on commit a680e92

Please sign in to comment.