featurebase/pql/decimal.go
Jason Aten 97b530ca78 integration of Tx, RoaringTx and BadgerTx implementations.
- all tests green on RoaringTx
  - RoaringTx on by default
  - blueGreenTx testing framework available for A-vs-B comparison
    of Tx implementations
  - flag -tx added to server command line but not wired to
    change NewIndex() selection yet.
  - 918 green tests, 14 tests red on BadgerTx.

    A full list of the 14 red tests on BadgerTx follows.
    Note that these red tests represent not defects in BadgerDB
    or BadgerTx but rather failures of the pre-existing pilosa infrastructure to yet
    be fully adapted from files to using a transactional storage engine.

    As such these are tests that RBF should not be expected to
    pass yet either.

    Fixing the pilosa infrastructure to allow these tests
    to go green under Badger is the next and highest priority
    order of business, but RBF can get much testing benefit
    from the 918 green tests we do have, and hence we merge
    as much as we have today.

    The 14 red tests when NewIndex() is set to use
    BadgerTx are as follows. Note in particular
    that pilosa cluster resizing is not working yet under a
    transactional store.

     TestCluster_ResizeStates/Multiple_nodes,_with_data
     TestImportClearRestart/0MaxOpN10000
     TestImportClearRestart/1MaxOpN10000
     TestImportClearRestart/2MaxOpN10000
     TestImportClearRestart/3MaxOpN10000
     TestExecutor_Execute_Existence/Row
     TestExecutor_ForeignIndex
     TestExecutor_Execute_CountDistinct/Distinct
     TestExecutor_Execute_CountDistinct/Count(Distinct)
     TestExecutor_Execute_CountDistinct/GroupBy(Distinct)
     TestExecutor_BareDistinct
     TestExecutor_Execute_TopNDistinct/TopN
     TestHolderSyncer_IntField/BasicSync
     TestHolderSyncer_IntField/MultiShard
2020-07-20 15:50:08 -04:00

555 lines
13 KiB
Go

// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pql
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/pkg/errors"
)
// pow10 is a map used to avoid the float64 required by math.Pow10()
var pow10 = map[int64]int64{
0: 1,
1: 10,
2: 100,
3: 1000,
4: 10000,
5: 100000,
6: 1000000,
7: 10000000,
8: 100000000,
9: 1000000000,
10: 10000000000,
11: 100000000000,
12: 1000000000000,
13: 10000000000000,
14: 100000000000000,
15: 1000000000000000,
16: 10000000000000000,
17: 100000000000000000,
18: 1000000000000000000,
//19: 10000000000000000000,
}
// Pow10 is a function which can be used in place of math.Pow10()
// to avoid the float64 logic. Note that only powers 0-18 are
// currently supported; anything else will return 0, which is
// probably going to result in incorrect values.
func Pow10(p int64) int64 {
return pow10[p]
}
// Decimal represents a decimal value; the intention
// is to avoid relying on float64, and the primary
// purpose is to have a predictable way to encode such
// values used in query strings.
// Scale is the number of digits to the right of the
// decimal point.
// Precision is currently not considered; precision, for
// our purposes is implied to be the complete, known value.
type Decimal struct {
Value int64
Scale int64
}
func (d Decimal) Clone() (r *Decimal) {
r = &Decimal{
Value: d.Value,
Scale: d.Scale,
}
return
}
// NewDecimal returns a Decimal based on the provided arguments.
func NewDecimal(value, scale int64) Decimal {
return Decimal{
Value: value,
Scale: scale,
}
}
// MinMax returns the minimum and maximum values
// supported by the provided scale.
func MinMax(scale int64) (Decimal, Decimal) {
min := NewDecimal(math.MinInt64, scale)
max := NewDecimal(math.MaxInt64, scale)
return min, max
}
// LessThan returns true if d < d2.
func (d Decimal) LessThan(d2 Decimal) bool {
return d.lessThan(d2, false)
}
// LessThanOrEqualTo returns true if d <= d2.
func (d Decimal) LessThanOrEqualTo(d2 Decimal) bool {
return d.lessThan(d2, true)
}
// GreaterThan returns true if d > d2.
func (d Decimal) GreaterThan(d2 Decimal) bool {
return d.greaterThan(d2, false)
}
// GreaterThanOrEqualTo returns true if d >= d2.
func (d Decimal) GreaterThanOrEqualTo(d2 Decimal) bool {
return d.greaterThan(d2, true)
}
// EqualTo returns true if d == d2.
func (d Decimal) EqualTo(d2 Decimal) bool {
if d.Scale == d2.Scale {
return d.Value == d2.Value
}
quotientD := quotient(d)
quotientD2 := quotient(d2)
if quotientD != quotientD2 {
return false
}
remainderD, remainderD2 := remainder(d), remainder(d2)
if d.Scale < d2.Scale {
scaleDiff := d2.Scale - d.Scale
return (remainderD * pow10[scaleDiff]) == remainderD2
}
scaleDiff := d.Scale - d2.Scale
return remainderD == (remainderD2 * pow10[scaleDiff])
}
func (d Decimal) lessThan(d2 Decimal, eq bool) bool {
if d.Scale == d2.Scale {
if eq {
return d.Value <= d2.Value
}
return d.Value < d2.Value
}
quotientD, quotientD2 := quotient(d), quotient(d2)
if quotientD < quotientD2 {
return true
} else if quotientD == quotientD2 {
remainderD, remainderD2 := remainder(d), remainder(d2)
if d.Scale < d2.Scale {
scaleDiff := d2.Scale - d.Scale
if eq {
return (remainderD * pow10[scaleDiff]) <= remainderD2
}
return (remainderD * pow10[scaleDiff]) < remainderD2
}
scaleDiff := d.Scale - d2.Scale
if eq {
return remainderD <= (remainderD2 * pow10[scaleDiff])
}
return remainderD < (remainderD2 * pow10[scaleDiff])
}
return false
}
func (d Decimal) greaterThan(d2 Decimal, eq bool) bool {
if d.Scale == d2.Scale {
if eq {
return d.Value >= d2.Value
}
return d.Value > d2.Value
}
quotientD, quotientD2 := quotient(d), quotient(d2)
if quotientD > quotientD2 {
return true
} else if quotientD == quotientD2 {
remainderD, remainderD2 := remainder(d), remainder(d2)
if d.Scale < d2.Scale {
scaleDiff := d2.Scale - d.Scale
if eq {
return (remainderD * pow10[scaleDiff]) >= remainderD2
}
return (remainderD * pow10[scaleDiff]) > remainderD2
}
scaleDiff := d.Scale - d2.Scale
if eq {
return remainderD >= (remainderD2 * pow10[scaleDiff])
}
return remainderD > (remainderD2 * pow10[scaleDiff])
}
return false
}
// SupportedByScale returns true if d can be represented
// by a decimal based on scale.
// For example:
// scale = 2:
// min: -92233720368547758.08
// max: 92233720368547758.07
// would not support: NewDecimal(9223372036854775807, 0)
func (d Decimal) SupportedByScale(scale int64) bool {
min, max := MinMax(scale)
if d.GreaterThanOrEqualTo(min) && d.LessThanOrEqualTo(max) {
return true
}
return false
}
// IsValid returns true if the decimal does not break
// any assumption or resrictions on input.
func (d Decimal) IsValid() bool {
if d.Scale < -18 || d.Scale > 19 {
return false
}
return true
}
// ToInt64 returns d as an int64 adjusted to the
// provided scale.
func (d Decimal) ToInt64(scale int64) int64 {
var ret int64
scaleDiff := scale - d.Scale
if scaleDiff == 0 {
ret = d.Value
} else if scaleDiff < 0 {
ret = d.Value / Pow10(-1*scaleDiff)
} else {
ret = d.Value * Pow10(scaleDiff)
}
return ret
}
// Float64 returns d as a float64.
// TODO: this could potentially lose precision; we should audit
// its use and protect against unexpected results.
func (d Decimal) Float64() float64 {
var ret float64
if d.Scale == 0 {
ret = float64(d.Value)
} else {
ret = float64(d.Value) / math.Pow10(int(d.Scale))
}
return ret
}
// String returns the string representation of the decimal.
func (d Decimal) String() string {
var s string
var neg bool
sval := fmt.Sprintf("%d", d.Value)
// Strip the negative sign off for now, and
// re-apply it at the end.
if sval[0] == '-' {
neg = true
sval = sval[1:]
}
if d.Scale == 0 {
s = sval
} else if d.Scale < 0 {
s = sval + strings.Repeat("0", int(-1*d.Scale))
} else {
var bufLen int
if int(d.Scale) < len(sval) {
bufLen = len(sval) + 1
} else {
bufLen = int(d.Scale) + 2
}
buf := make([]byte, bufLen)
j := 0
for i := range buf {
z := len(buf) - 1 - i // index into buf from the end
if i == int(d.Scale) {
buf[z] = '.'
continue
}
if len(sval) > j {
buf[z] = sval[len(sval)-1-j]
j++
} else {
buf[z] = '0'
}
}
s = string(buf)
}
if neg {
return "-" + s
}
return s
}
const (
stateSign = "sign"
stateLeadingZeros = "zeros"
stateMantissa = "mantissa"
)
// ParseDecimal parses a string into a Decimal.
func ParseDecimal(s string) (Decimal, error) {
var sign bool
var value int64
var scale int64
var err error
// General steps:
// - Trim leading whitespace/zeros
// - Get the sign value
// - Trim leading zeros
// - Push characters into a buffer
// - Track position of decimal point
// - Trim trailing zeros of buffer
// - value = buffer -> int
// - scale = len(buffer) - tracked position
var decimalPos int = -1
var pos int
mantissa := make([]byte, len(s))
state := stateSign
var foundLeadingZero bool
for i := 0; i < len(s); i++ {
switch state {
case stateSign:
switch s[i] {
case ' ':
continue
case '-':
sign = true
fallthrough
case '+':
state = stateLeadingZeros
default:
state = stateLeadingZeros
i--
}
case stateLeadingZeros:
switch s[i] {
case '0':
foundLeadingZero = true
continue
default:
state = stateMantissa
i--
}
case stateMantissa:
switch s[i] {
case '.':
if decimalPos == -1 {
decimalPos = pos
} else {
return Decimal{}, errors.Errorf("invalid decimal string: %s", s)
}
continue
default:
mantissa[pos] = s[i]
pos++
}
}
}
// If we've gotten here and state is still in stateSign or
// it's in stateLeadingZeros without finding any zeros,
// it means no value was provided.
if state == stateSign || (state == stateLeadingZeros && !foundLeadingZero) {
return Decimal{}, errors.New("decimal string is empty")
}
// Trim trailing zeros/spaces of mantissa
// for any portion that would have been to the
// right of the decimal.
trimZeroCnt := 0
trimSpaceCnt := 0
for i := len(mantissa) - 1; i >= 0; i-- {
switch mantissa[i] {
case uint8(0), uint8(32): // nil/space
trimSpaceCnt++
continue
case uint8(48): // zero
trimZeroCnt++
continue
}
break
}
mantissa = mantissa[:len(mantissa)-trimSpaceCnt-trimZeroCnt]
// Based on where (or if) the decimal was found,
// calculate scale.
if decimalPos == -1 {
scale = -1 * int64(trimZeroCnt)
} else {
scale = int64(len(mantissa) - decimalPos)
}
// If mantissa is empty, treat it as "0".
if len(mantissa) == 0 {
mantissa = []byte{'0'}
sign = false
scale = 0
}
// If the mantissa can't be represented by an int64, but it contains
// enough decimal places such that we can sacrifice precision, then
// we do that. This is an attempt to be compatible with the way
// `strconv.ParseFloat` works.
if m, s, ok := reducePrecision(sign, mantissa, scale); ok {
mantissa = m
scale = s
} else {
return Decimal{}, errors.Errorf("value out of range: %s", mantissa)
}
// We have to use ParseUint here (as opposed to ParseInt) because
// math.MinInt64 is a valid value, but its absolute value is not.
// So this allows us to handle that one value without overflow, and
// then we check for the uint bounds in the next step.
uvalue, err := strconv.ParseUint(string(mantissa), 10, 64)
if err != nil {
return Decimal{}, errors.Wrap(err, "converting mantissa string to uint64")
}
if (sign && uvalue > -1*math.MinInt64) || (!sign && uvalue > math.MaxInt64) {
return Decimal{}, errors.New("value out of range")
}
value = int64(uvalue)
if sign {
value *= -1
}
return Decimal{
Value: value,
Scale: scale,
}, nil
}
// reducePrecision takes a []byte mantissa and scale, and if possible
// will adjust the mantissa (by reducing precision) until it can be
// represented by an int64. The returned bool indicates whether the
// reduction was successful.
func reducePrecision(sign bool, mantissa []byte, scale int64) ([]byte, int64, bool) {
// Trim leading zeros before considering length.
var zeroIdx int
for i := range mantissa {
if mantissa[i] == '0' {
zeroIdx++
} else {
break
}
}
mantissa = mantissa[zeroIdx:]
// If we zero out the mantissa to an empty
// string, that means it's value should be 0.
if len(mantissa) == 0 {
mantissa = []byte{'0'}
return mantissa, scale, true
}
lenMantissa := len(mantissa)
maxStr := "9223372036854775807"
if sign {
maxStr = "9223372036854775808"
}
if lenMantissa <= 18 || (lenMantissa == 19 && string(mantissa) <= maxStr) {
return mantissa, scale, true
}
// If we don't have any decimal places to sacrifice,
// we can't change anything.
if scale <= 0 {
return mantissa, scale, false
}
return reducePrecision(sign, mantissa[:len(mantissa)-1], scale-1)
}
func quotient(d Decimal) int64 {
if d.Scale == 0 {
return d.Value
} else if d.Scale > 0 && d.Scale < 19 {
return d.Value / pow10[d.Scale]
} else if d.Scale < 0 && d.Scale > -19 {
return d.Value * pow10[-1*d.Scale]
}
return 0
}
func remainder(d Decimal) int64 {
if d.Scale >= 0 && d.Scale < 19 {
return d.Value % pow10[d.Scale]
}
return 0
}
// UnmarshalJSON is a custom unmarshaller for the Decimal
// type. The intention is to avoid the use of float64
// anywhere, so this unmarhaller parses the decimal out
// of the byte string.
func (d *Decimal) UnmarshalJSON(data []byte) error {
o, err := ParseDecimal(string(data))
if err != nil {
return errors.Wrapf(err, "parsing decimal: %s", string(data))
}
d.Value = o.Value
d.Scale = o.Scale
return nil
}
// MarshalJSON is a custom marshaller for the Decimal type.
func (d Decimal) MarshalJSON() ([]byte, error) {
return []byte(d.String()), nil
}
// UnmarshalYAML is a custom unmarshaller for the Decimal
// type.
func (d *Decimal) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data string
if err := unmarshal(&data); err != nil {
return err
}
o, err := ParseDecimal(data)
if err != nil {
return errors.Wrapf(err, "parsing decimal: %s", data)
}
d.Value = o.Value
d.Scale = o.Scale
return nil
}
// MarshalYAML is a custom marshaller for the Decimal type.
func (d Decimal) MarshalYAML() (interface{}, error) {
// TODO: I don't love that this results in a quoted string
// in the yaml document:
//
// min: "-100.05"
//
// It would be nice if we could get that to result in:
//
// min: -100.05
//
// Note that we _can_ do that by casting the output as
// float64 (for certain cases), but the whole point of
// Decimal is to avoid using float64.
return d.String(), nil
}