mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
1306 lines
33 KiB
Go
1306 lines
33 KiB
Go
package idk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
pilosacore "github.com/featurebasedb/featurebase/v3"
|
|
pilosaclient "github.com/featurebasedb/featurebase/v3/client"
|
|
"github.com/featurebasedb/featurebase/v3/pql"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var (
|
|
// ErrSchemaChange is returned from Source.Record when the returned
|
|
// record has a different schema from the previous record.
|
|
ErrSchemaChange = errors.New("this record has a different schema from the previous record (or is the first one delivered). Please call Source.Schema() to fetch the schema in order to properly decode this record")
|
|
|
|
// ErrFlush is returned from Source.Record when the Source wants to
|
|
// signal that there may not be data for a while, so it's a good time
|
|
// to make sure all data which has been received is ingested. The
|
|
// record must be nil when ErrFlush is returned.
|
|
ErrFlush = errors.New("the Source is requesting the batch be flushed")
|
|
|
|
ErrFmtUnknownUnit = "unknown unit %q, please choose from d/h/m/s/ms/us/ns"
|
|
ErrIntOutOfRange = errors.New("value provided for int field is out of range")
|
|
ErrDecimalOutOfRange = errors.New("value provided for decimal field is out of range")
|
|
ErrTimestampOutOfRange = errors.New("value provided for timestamp field is out of range")
|
|
)
|
|
|
|
type (
|
|
// Source is an interface implemented by sources of data which can be
|
|
// ingested into Pilosa. Each Record returned from Record is described
|
|
// by the slice of Fields returned from Source.Schema directly after
|
|
// the call to Source.Record. If the error returned from Source.Record
|
|
// is nil, then the call to Schema which applied to the previous
|
|
// Record also applies to this Record. Source implementations are
|
|
// fundamentally not threadsafe (due to the interplay between Record
|
|
// and Schema).
|
|
Source interface {
|
|
// Record returns a data record, and an optional error. If the
|
|
// error is ErrSchemaChange, then the record is valid, but one
|
|
// should call Source.Schema to understand how each of its fields
|
|
// should be interpreted.
|
|
Record() (Record, error)
|
|
|
|
// Schema returns a slice of Fields which applies to the most
|
|
// recent Record returned from Source.Record. Every Field has a
|
|
// name and a type, and depending on the concrete type of the
|
|
// Field, may have other information which is relevant to how it
|
|
// should be indexed.
|
|
Schema() []Field
|
|
|
|
Close() error
|
|
}
|
|
|
|
Record interface {
|
|
// Commit notifies the Source which produced this record that it
|
|
// and any record which came before it have been completely
|
|
// processed. The Source can then take any necessary action to
|
|
// record which records have been processed, and restart from the
|
|
// earliest unprocessed record in the event of a failure.
|
|
Commit(ctx context.Context) error
|
|
|
|
Data() []interface{}
|
|
}
|
|
|
|
// OffsetStreamRecord is an extension of the record type which also tracks offsets within streams.
|
|
OffsetStreamRecord interface {
|
|
Record
|
|
|
|
// StreamOffset returns the stream from which the record originated, and the offset of the record within that stream.
|
|
StreamOffset() (key string, offset uint64)
|
|
}
|
|
|
|
Metadata interface {
|
|
// SchemaMetadata returns a string representation of source-specific details
|
|
// about the schema.
|
|
SchemaMetadata() string
|
|
SchemaSubject() string
|
|
SchemaSchema() string
|
|
SchemaVersion() int
|
|
SchemaID() int
|
|
}
|
|
|
|
// Field knows how to interpret values of different types and tells
|
|
// how they get indexed in Pilosa. Every field implementation should
|
|
// be a struct named like <something>Field, and have as members
|
|
// `NameVal string` and `DestNameVal string`, where NameVal contains
|
|
// the name of the field at the source, and DestNameVal contains the
|
|
// name of the field at the destination (pilosa)
|
|
//
|
|
// Many Field implementations have a Quantum field which can be any
|
|
// valid Pilosa time quantum, e.g. "Y", "YMDH", "DH", etc. If Quantum
|
|
// is set to a valid quantum, the Pilosa field created for this field
|
|
// will be of type "time". Other fields which control field type will
|
|
// be ignored until/if Pilosa supports time+(othertype) fields.
|
|
Field interface {
|
|
Name() string
|
|
DestName() string
|
|
PilosafyVal(val interface{}) (interface{}, error) // TODO rename this
|
|
}
|
|
)
|
|
|
|
// FieldsEqual is used in testing to compare Fields. The pointers make it a bit tricky for IntField.
|
|
func FieldsEqual(f1, f2 Field) bool {
|
|
if reflect.TypeOf(f1) != reflect.TypeOf(f2) {
|
|
return false
|
|
}
|
|
switch f1t := f1.(type) {
|
|
case IgnoreField, IDField, BoolField, RecordTimeField, StringField, LookupTextField, DecimalField, SignedIntBoolKeyField, StringArrayField, IDArrayField, TimestampField, DateIntField:
|
|
return f1 == f2
|
|
case IntField:
|
|
f2t := f2.(IntField)
|
|
if f1t.NameVal == f2t.NameVal && f1t.DestNameVal == f2t.DestNameVal && f1t.ForeignIndex == f2t.ForeignIndex {
|
|
if !(f1t.Min == nil && f2t.Min == nil) {
|
|
if f1t.Min == nil || f2t.Min == nil {
|
|
return false
|
|
}
|
|
if *f1t.Min != *f2t.Min {
|
|
return false
|
|
}
|
|
}
|
|
if !(f1t.Max == nil && f2t.Max == nil) {
|
|
if f1t.Max == nil || f2t.Max == nil {
|
|
return false
|
|
}
|
|
if *f1t.Max != *f2t.Max {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
default:
|
|
panic(fmt.Sprintf("unknown field type %T when comparing fields", f1))
|
|
}
|
|
}
|
|
|
|
// CacheConfigOf returns CacheConfig of the Field.
|
|
func CacheConfigOf(f Field) CacheConfig {
|
|
switch f := f.(type) {
|
|
case StringField:
|
|
if f.CacheConfig == nil {
|
|
if f.Quantum != "" {
|
|
return noneCacheConfig
|
|
}
|
|
return defaultCacheConfig
|
|
}
|
|
return *f.CacheConfig
|
|
case StringArrayField:
|
|
if f.CacheConfig == nil {
|
|
if f.Quantum != "" {
|
|
return noneCacheConfig
|
|
}
|
|
return defaultCacheConfig
|
|
}
|
|
return *f.CacheConfig
|
|
case IDField:
|
|
if f.CacheConfig == nil {
|
|
if f.Quantum != "" {
|
|
return noneCacheConfig
|
|
}
|
|
return defaultCacheConfig
|
|
}
|
|
return *f.CacheConfig
|
|
case IDArrayField:
|
|
if f.CacheConfig == nil {
|
|
if f.Quantum != "" {
|
|
return noneCacheConfig
|
|
}
|
|
return defaultCacheConfig
|
|
}
|
|
return *f.CacheConfig
|
|
default:
|
|
return defaultCacheConfig
|
|
}
|
|
}
|
|
|
|
// QuantumOf returns Quantum of the Field.
|
|
func QuantumOf(fld Field) string {
|
|
switch ft := fld.(type) {
|
|
case IDField:
|
|
return ft.Quantum
|
|
case StringField:
|
|
return ft.Quantum
|
|
case StringArrayField:
|
|
return ft.Quantum
|
|
case IDArrayField:
|
|
return ft.Quantum
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func TTLOf(fld Field) (time.Duration, error) {
|
|
var ttl time.Duration
|
|
var err error
|
|
|
|
switch ft := fld.(type) {
|
|
case IDField:
|
|
if ft.TTL != "" {
|
|
ttl, err = time.ParseDuration(ft.TTL)
|
|
} else {
|
|
return 0, nil
|
|
}
|
|
case StringField:
|
|
if ft.TTL != "" {
|
|
ttl, err = time.ParseDuration(ft.TTL)
|
|
} else {
|
|
return 0, nil
|
|
}
|
|
case StringArrayField:
|
|
if ft.TTL != "" {
|
|
ttl, err = time.ParseDuration(ft.TTL)
|
|
} else {
|
|
return 0, nil
|
|
}
|
|
case IDArrayField:
|
|
if ft.TTL != "" {
|
|
ttl, err = time.ParseDuration(ft.TTL)
|
|
} else {
|
|
return 0, nil
|
|
}
|
|
default:
|
|
return 0, nil
|
|
}
|
|
if err != nil {
|
|
return ttl, errors.Wrapf(err, "unable to parse TTL from field %s", fld.Name())
|
|
} else {
|
|
return ttl, nil
|
|
}
|
|
}
|
|
|
|
// HasMutex returns Mutex value of StringField or IDField, otherwise false
|
|
func HasMutex(fld Field) bool {
|
|
if sfld, ok := fld.(StringField); ok && sfld.Quantum == "" {
|
|
return sfld.Mutex
|
|
}
|
|
if sfld, ok := fld.(IDField); ok && sfld.Quantum == "" {
|
|
return sfld.Mutex
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IgnoreField can be used when you wish not to process one of the
|
|
// input fields, but it is inconvenient to remove it ahead of time.
|
|
type IgnoreField struct{}
|
|
|
|
func (IgnoreField) Name() string { return "" }
|
|
func (IgnoreField) DestName() string { return "" }
|
|
func (IgnoreField) PilosafyVal(interface{}) (interface{}, error) { return nil, nil }
|
|
|
|
type IDField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
|
|
// Mutex denotes whether we need to enforce that each record only
|
|
// has a single value for this field. Put another way, says
|
|
// whether a new value for this field be treated as adding an
|
|
// additional value, or replacing the existing value (if there is
|
|
// one).
|
|
Mutex bool
|
|
|
|
// Quantum — see note about Quantum on "Field" interface.
|
|
Quantum string
|
|
|
|
TTL string
|
|
|
|
*CacheConfig
|
|
}
|
|
|
|
func (id IDField) Name() string { return id.NameVal }
|
|
func (id IDField) DestName() string {
|
|
if id.DestNameVal == "" {
|
|
return id.NameVal
|
|
}
|
|
|
|
return id.DestNameVal
|
|
}
|
|
func (id IDField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
} else if vs, ok := val.(string); ok && vs == "" {
|
|
return nil, nil
|
|
}
|
|
return toUint64(val)
|
|
}
|
|
|
|
type BoolField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
}
|
|
|
|
func (b BoolField) Name() string { return b.NameVal }
|
|
func (b BoolField) DestName() string {
|
|
if b.DestNameVal == "" {
|
|
return b.NameVal
|
|
}
|
|
|
|
return b.DestNameVal
|
|
}
|
|
func (b BoolField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
} else if vs, ok := val.(string); ok && vs == "" {
|
|
return nil, nil
|
|
}
|
|
return toBool(val)
|
|
}
|
|
|
|
var (
|
|
MinTimestampNano = time.Unix(-1<<32, 0).UTC() // 1833-11-24T17:31:44Z
|
|
MaxTimestampNano = time.Unix(1<<32, 0).UTC() // 2106-02-07T06:28:16Z
|
|
MinTimestamp = time.Unix(-62135596799, 0).UTC() // 0001-01-01T00:00:01Z
|
|
MaxTimestamp = time.Unix(253402300799, 0).UTC() // 9999-12-31T23:59:59Z
|
|
)
|
|
|
|
const (
|
|
Custom = Unit("c")
|
|
Day = Unit("d")
|
|
Hour = Unit("h")
|
|
Minute = Unit("m")
|
|
Second = Unit("s")
|
|
Millisecond = Unit("ms")
|
|
Microsecond = Unit("us")
|
|
Nanosecond = Unit("ns")
|
|
|
|
DefaultUnit = Second
|
|
)
|
|
|
|
type Unit string
|
|
|
|
func (u Unit) unit() Unit {
|
|
s := strings.ToLower(string(u))
|
|
if s == "" {
|
|
return DefaultUnit
|
|
}
|
|
return Unit(s)
|
|
}
|
|
|
|
func (u Unit) IsCustom() bool {
|
|
return u.unit() == Custom
|
|
}
|
|
|
|
func (u Unit) Duration() (time.Duration, error) {
|
|
duration := time.Duration(1)
|
|
switch u.unit() {
|
|
case Day:
|
|
duration *= 24
|
|
fallthrough
|
|
case Hour:
|
|
duration *= 60
|
|
fallthrough
|
|
case Minute:
|
|
duration *= 60
|
|
fallthrough
|
|
case Second:
|
|
duration *= 1000
|
|
fallthrough
|
|
case Millisecond:
|
|
duration *= 1000
|
|
fallthrough
|
|
case Microsecond:
|
|
duration *= 1000
|
|
fallthrough
|
|
case Nanosecond:
|
|
return duration, nil
|
|
}
|
|
return 0, errors.Errorf(ErrFmtUnknownUnit, u)
|
|
}
|
|
|
|
// ToNanos returns the number of Nanoseconds per given Unit
|
|
func (u Unit) ToNanos() (int64, error) {
|
|
duration := int64(1)
|
|
switch u.unit() {
|
|
case Day:
|
|
duration *= 24
|
|
fallthrough
|
|
case Hour:
|
|
duration *= 60
|
|
fallthrough
|
|
case Minute:
|
|
duration *= 60
|
|
fallthrough
|
|
case Second:
|
|
duration *= 1000
|
|
fallthrough
|
|
case Millisecond:
|
|
duration *= 1000
|
|
fallthrough
|
|
case Microsecond:
|
|
duration *= 1000
|
|
fallthrough
|
|
case Nanosecond:
|
|
return duration, nil
|
|
}
|
|
return 0, errors.Errorf(ErrFmtUnknownUnit, u)
|
|
}
|
|
|
|
func (u Unit) DurationFromValue(val int64) (time.Duration, error) {
|
|
scale, err := u.Duration()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if max, min := int64(math.MaxInt64/scale), int64(math.MinInt64/scale); val > max || val < min {
|
|
return 0, errors.Errorf("%d%s is outside representable time scales, must be between %d and %d", val, u, min, max)
|
|
}
|
|
return time.Duration(val) * scale, nil
|
|
}
|
|
|
|
func (r RecordTimeField) epoch() time.Time {
|
|
if r.Epoch.IsZero() {
|
|
return time.Unix(0, 0)
|
|
}
|
|
return r.Epoch
|
|
}
|
|
|
|
// RecordTimeField applies to whole record, but doesn't have a name
|
|
// (or quantum) of its own since it applies to any other time fields
|
|
// in the record.
|
|
type RecordTimeField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
Layout string // Layout tells how the time should be parsed. Defaults to RFC3339.
|
|
// need a way to create other time fields in the record (add time/quantum to String, StringArray, ID, IDArray?)
|
|
// do we need a way to have timefields in a record with independent times/values
|
|
Epoch time.Time
|
|
Unit Unit
|
|
}
|
|
|
|
func (r RecordTimeField) Name() string { return r.NameVal }
|
|
func (r RecordTimeField) DestName() string {
|
|
if r.DestNameVal == "" {
|
|
return r.NameVal
|
|
}
|
|
|
|
return r.DestNameVal
|
|
}
|
|
|
|
// PilosafyVal for RecordTimeField always returns a time.Time or nil.
|
|
func (r RecordTimeField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if valt, ok := val.(time.Time); ok {
|
|
return valt, nil
|
|
}
|
|
if !r.Epoch.IsZero() || r.Unit != "" {
|
|
result, err := timeFromEpoch(val, r.epoch(), r.Unit)
|
|
if err != nil {
|
|
err = errors.Wrap(err, "converting RecordTimeField from epoch")
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
result, err := timeFromTimestring(val, r.layout())
|
|
if err != nil {
|
|
err = errors.Wrap(err, "converting RecordTimeField from layout")
|
|
}
|
|
if result.IsZero() {
|
|
return nil, err
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
func (r RecordTimeField) layout() string {
|
|
if r.Layout == "" {
|
|
return time.RFC3339
|
|
}
|
|
return r.Layout
|
|
}
|
|
|
|
// CacheConfig - type (ranked, lru, none) and size.
|
|
type CacheConfig struct {
|
|
CacheType pilosaclient.CacheType
|
|
CacheSize int
|
|
}
|
|
|
|
var defaultCacheConfig = CacheConfig{CacheType: pilosaclient.CacheTypeRanked, CacheSize: pilosacore.DefaultCacheSize}
|
|
var noneCacheConfig = CacheConfig{CacheType: "", CacheSize: 0}
|
|
|
|
func (cfg CacheConfig) setOption() pilosaclient.FieldOption {
|
|
if cfg == (CacheConfig{}) {
|
|
cfg = defaultCacheConfig
|
|
}
|
|
|
|
return pilosaclient.OptFieldTypeSet(pilosaclient.CacheType(cfg.CacheType), cfg.CacheSize)
|
|
}
|
|
|
|
func (cfg CacheConfig) mutexOption() pilosaclient.FieldOption {
|
|
if cfg == (CacheConfig{}) {
|
|
cfg = defaultCacheConfig
|
|
}
|
|
|
|
return pilosaclient.OptFieldTypeMutex(pilosaclient.CacheType(cfg.CacheType), cfg.CacheSize)
|
|
}
|
|
|
|
type StringField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
|
|
// Mutex denotes whether we need to enforce that each record only
|
|
// has a single value for this field. Put another way, says
|
|
// whether a new value for this field be treated as adding an
|
|
// additional value, or replacing the existing value (if there is
|
|
// one).
|
|
Mutex bool
|
|
|
|
// Quantum — see note about Quantum on "Field" interface.
|
|
Quantum string
|
|
|
|
TTL string
|
|
|
|
*CacheConfig
|
|
}
|
|
|
|
func (s StringField) Name() string { return s.NameVal }
|
|
func (s StringField) DestName() string {
|
|
if s.DestNameVal == "" {
|
|
return s.NameVal
|
|
}
|
|
|
|
return s.DestNameVal
|
|
}
|
|
func (s StringField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
}
|
|
return toString(val)
|
|
}
|
|
|
|
type LookupTextField struct {
|
|
// NOTE this implements the Field interface for simplicity of implementation/API, but that interface is intended for data going into pilosa, while this is not.
|
|
NameVal string
|
|
DestNameVal string
|
|
// TODO this might should reference the lookupDB
|
|
}
|
|
|
|
func (s LookupTextField) Name() string { return s.NameVal }
|
|
func (s LookupTextField) DestName() string {
|
|
if s.DestNameVal == "" {
|
|
return s.NameVal
|
|
}
|
|
|
|
return s.DestNameVal
|
|
}
|
|
func (s LookupTextField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
}
|
|
return toString(val)
|
|
}
|
|
|
|
// IntField - if you add any new fields to this struct, please update the FieldsEqual function to accomodate.
|
|
type IntField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
Min *int64
|
|
Max *int64
|
|
ForeignIndex string
|
|
}
|
|
|
|
func (i IntField) Name() string { return i.NameVal }
|
|
func (i IntField) DestName() string {
|
|
if i.DestNameVal == "" {
|
|
return i.NameVal
|
|
}
|
|
|
|
return i.DestNameVal
|
|
}
|
|
func (i IntField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
}
|
|
if valS, ok := val.(string); ok && i.ForeignIndex != "" {
|
|
return valS, nil
|
|
} else if ok && valS == "" {
|
|
return nil, nil
|
|
}
|
|
asInt, err := toInt64(val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if i.Min != nil && asInt < *i.Min {
|
|
return nil, errors.Wrapf(ErrIntOutOfRange, "field = %v, value %v is smaller than min allowed %v", i.Name(), asInt, *i.Min)
|
|
}
|
|
if i.Max != nil && asInt > *i.Max {
|
|
return nil, errors.Wrapf(ErrIntOutOfRange, "field = %v, value %v is bigger than max allowed %v", i.Name(), asInt, *i.Max)
|
|
}
|
|
return asInt, nil
|
|
}
|
|
|
|
const DecimalPrecision = 18
|
|
|
|
type DecimalField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
Scale int64
|
|
}
|
|
|
|
func (d DecimalField) Name() string { return d.NameVal }
|
|
func (d DecimalField) DestName() string {
|
|
if d.DestNameVal == "" {
|
|
return d.NameVal
|
|
}
|
|
|
|
return d.DestNameVal
|
|
}
|
|
|
|
// PilosafyVal for DecimalField always returns an int64. If the
|
|
// incoming value is anything but a float or string we attempt to
|
|
// convert to int64 and then scale it. Strings are attempted to be
|
|
// parsed into floats, and all values are scaled by the 10^scale
|
|
// before being returned. Byte slices are assumed to represent the
|
|
// already scaled value and are interpreted as int64.
|
|
func (d DecimalField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
// Make sure the provided scale is supported before proceeding.
|
|
// Note: the "1" value below doesn't matter; we're just
|
|
// validating the scale.
|
|
// Also, the existing tests don't exercise this check because
|
|
// they error creating the field prior to getting here.
|
|
// TODO: let's move scale validation to the pql package so
|
|
// that it can control the specific error message.
|
|
if !pql.NewDecimal(1, d.Scale).IsValid() {
|
|
return nil, errors.Errorf("scale values outside the range [0,19] are not supported: %d", d.Scale)
|
|
}
|
|
|
|
if val == nil {
|
|
return nil, nil
|
|
}
|
|
if vs, ok := val.(string); ok {
|
|
if vs == "" {
|
|
return nil, nil
|
|
}
|
|
asInt, err := scaledStringToInt(d.Scale, vs)
|
|
if err != nil {
|
|
// scaledStringToInt returns 0 on error. If there is a problem parsing the string,
|
|
// we want to import nil.
|
|
return nil, errors.Wrap(err, ErrDecimalOutOfRange.Error())
|
|
}
|
|
return asInt, nil
|
|
}
|
|
switch vt := val.(type) {
|
|
case pql.Decimal:
|
|
return vt.ToInt64(d.Scale), nil
|
|
|
|
case float32:
|
|
v := vt * float32(math.Pow10(int(d.Scale)))
|
|
return int64(v), nil
|
|
case float64:
|
|
vt = vt * math.Pow10(int(d.Scale))
|
|
return int64(vt), nil
|
|
case []byte:
|
|
// 16: int64(value)+int64(scale)
|
|
// 8: int64(value)
|
|
var tmp [8]byte
|
|
if len(vt) == 16 {
|
|
value := int64(binary.BigEndian.Uint64(vt[0:8]))
|
|
scale := int64(binary.BigEndian.Uint64(vt[8:16]))
|
|
return pql.NewDecimal(value, scale).ToInt64(d.Scale), nil
|
|
} else if len(vt) == 8 {
|
|
return int64(binary.BigEndian.Uint64(vt)), nil
|
|
} else if len(vt) < 8 {
|
|
copy(tmp[8-len(vt):], vt)
|
|
return int64(binary.BigEndian.Uint64(tmp[:])), nil
|
|
} else {
|
|
return nil, errors.Errorf("can only support decimal value up to 8 bytes, or 16 bytes containing value and scale, got %d for %s", len(vt), d.Name())
|
|
}
|
|
default:
|
|
v, err := toInt64(val)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "couldn't convert %v to int64 for decimal field", val)
|
|
}
|
|
return pql.NewDecimal(v, 0).ToInt64(d.Scale), nil
|
|
}
|
|
}
|
|
|
|
// SignedIntBoolKeyField translates a signed integer value to a (rowID, bool)
|
|
// pair corresponding to the magnitude and sign of the original value. This
|
|
// may be used to specify whether a bool value is to be set (positive/true)
|
|
// or cleared (negative/false).
|
|
type SignedIntBoolKeyField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
}
|
|
|
|
func (b SignedIntBoolKeyField) Name() string { return b.NameVal }
|
|
func (b SignedIntBoolKeyField) DestName() string {
|
|
if b.DestNameVal == "" {
|
|
return b.NameVal
|
|
}
|
|
|
|
return b.DestNameVal
|
|
}
|
|
|
|
func (SignedIntBoolKeyField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
} else if vs, ok := val.(string); ok && vs == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
return toInt64(val)
|
|
}
|
|
|
|
type StringArrayField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
|
|
// Quantum — see note about Quantum on "Field" interface.
|
|
Quantum string
|
|
|
|
TTL string
|
|
|
|
*CacheConfig
|
|
}
|
|
|
|
func (s StringArrayField) Name() string { return s.NameVal }
|
|
func (s StringArrayField) DestName() string {
|
|
if s.DestNameVal == "" {
|
|
return s.NameVal
|
|
}
|
|
|
|
return s.DestNameVal
|
|
}
|
|
func (StringArrayField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
}
|
|
return toStringArray(val)
|
|
}
|
|
|
|
type IDArrayField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
|
|
// Quantum — see note about Quantum on "Field" interface.
|
|
Quantum string
|
|
|
|
TTL string
|
|
|
|
*CacheConfig
|
|
}
|
|
|
|
func (i IDArrayField) Name() string { return i.NameVal }
|
|
func (i IDArrayField) DestName() string {
|
|
if i.DestNameVal == "" {
|
|
return i.NameVal
|
|
}
|
|
|
|
return i.DestNameVal
|
|
}
|
|
func (IDArrayField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
}
|
|
return toUint64Array(val)
|
|
}
|
|
|
|
type TimestampField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
Layout string
|
|
Epoch time.Time
|
|
Unit Unit
|
|
Granularity string
|
|
}
|
|
|
|
func (d TimestampField) Name() string { return d.NameVal }
|
|
|
|
func (d TimestampField) DestName() string {
|
|
if d.DestNameVal == "" {
|
|
return d.NameVal
|
|
}
|
|
return d.DestNameVal
|
|
}
|
|
|
|
// ValToTimestamp takes a timeunit and an integer value and converts it to time.Time
|
|
func ValToTimestamp(unit string, val int64) (time.Time, error) {
|
|
switch unit {
|
|
case string(Second):
|
|
return time.Unix(val, 0).UTC(), nil
|
|
case string(Millisecond):
|
|
return time.UnixMilli(val).UTC(), nil
|
|
case string(Microsecond):
|
|
return time.UnixMicro(val).UTC(), nil
|
|
case string(Nanosecond):
|
|
return time.Unix(0, val).UTC(), nil
|
|
default:
|
|
return time.Time{}, errors.Errorf("Unknown time unit: '%v'", unit)
|
|
}
|
|
}
|
|
|
|
// TimestampToVal takes a time unit and a time.Time and converts it to an integer value
|
|
func TimestampToVal(unit Unit, ts time.Time) int64 {
|
|
switch unit {
|
|
case Second:
|
|
return ts.Unix()
|
|
case Millisecond:
|
|
return ts.UnixMilli()
|
|
case Microsecond:
|
|
return ts.UnixMicro()
|
|
case Nanosecond:
|
|
return ts.UnixNano()
|
|
}
|
|
return 0
|
|
|
|
}
|
|
|
|
// PilosafyVal for TimestampField always returns an int or nil.
|
|
func (t TimestampField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
if val == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var dur int64
|
|
// Check if the epoch alone is out-of-range. If so, ingest should halt, regardless
|
|
// of state of the timestamp out-of-range CLI option.
|
|
if err := validateTimestamp(t.granularity(), t.epoch()); err != nil {
|
|
return nil, errors.Wrap(err, "validating epoch")
|
|
}
|
|
|
|
epochAsVal := TimestampToVal(t.granularity(), t.epoch())
|
|
if _, ok := val.(time.Time); ok || (t.Epoch.IsZero() && t.Unit == "") {
|
|
ts, err := timeFromTimestring(val, t.layout())
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "out of range") {
|
|
return nil, errors.Wrap(err, ErrTimestampOutOfRange.Error())
|
|
}
|
|
return nil, errors.Wrap(err, "converting TimestampField from layout")
|
|
}
|
|
if err := validateTimestamp(t.granularity(), ts); err != nil {
|
|
return nil, errors.Wrap(ErrTimestampOutOfRange, "validating timestamp")
|
|
}
|
|
|
|
tsAsVal := TimestampToVal(t.granularity(), ts)
|
|
|
|
dur = tsAsVal - epochAsVal
|
|
} else {
|
|
valAsInt, err := toInt64(val)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "out of range") {
|
|
return nil, errors.Wrap(err, ErrTimestampOutOfRange.Error())
|
|
}
|
|
return nil, errors.Wrap(err, "converting value to int64")
|
|
}
|
|
|
|
// Conversion ratio to scale incoming Units to Granularity
|
|
granNanos, err := Unit(t.granularity()).ToNanos()
|
|
if err != nil || granNanos == 0 {
|
|
return nil, errors.Wrap(err, "granularity not supported")
|
|
}
|
|
unitNanos, err := Unit(t.Unit).ToNanos()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unit not supported")
|
|
}
|
|
scale := float64(unitNanos) / float64(granNanos)
|
|
|
|
dur = int64(float64(valAsInt) * scale)
|
|
if (dur >= 0 && valAsInt < 0) || (dur < 0 && valAsInt > 0) {
|
|
return nil, errors.Wrap(ErrTimestampOutOfRange, "timestamp value out of range at specified granularity")
|
|
}
|
|
|
|
if err := validateDuration(dur, epochAsVal, Unit(t.granularity())); err != nil {
|
|
return nil, errors.Wrap(err, "validating duration")
|
|
}
|
|
}
|
|
|
|
return dur, nil
|
|
}
|
|
|
|
// validateTimestamp checks if the timestamp is within the range of what FB accepts.
|
|
func validateTimestamp(unit Unit, ts time.Time) error {
|
|
// Min and Max timestamps that Featurebase accepts
|
|
var minStamp, maxStamp time.Time
|
|
switch unit {
|
|
case Nanosecond:
|
|
minStamp = MinTimestampNano
|
|
maxStamp = MaxTimestampNano
|
|
default:
|
|
minStamp = MinTimestamp
|
|
maxStamp = MaxTimestamp
|
|
}
|
|
|
|
if ts.Before(minStamp) || ts.After(maxStamp) {
|
|
return errors.New(fmt.Sprintf("timestamp value must be within min: %v and max: %v", minStamp, maxStamp))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateDuration checks if the duration will overflow. Users can provide a custom epoch but
|
|
// Featurebase will ultimately convert this to some duration relative to the Unix epoch.
|
|
// So if the custom epoch + the provided value in the desired units is too far from
|
|
// Unix epoch such that it causes an interger overflow, this will return an error.
|
|
func validateDuration(dur int64, offset int64, granularity Unit) error {
|
|
var minInt, maxInt int64
|
|
switch granularity {
|
|
case Second:
|
|
minInt = MinTimestamp.Unix()
|
|
maxInt = MaxTimestamp.Unix()
|
|
case Millisecond:
|
|
minInt = MinTimestamp.UnixMilli()
|
|
maxInt = MaxTimestamp.UnixMilli()
|
|
case Microsecond:
|
|
minInt = MinTimestamp.UnixMicro()
|
|
maxInt = MaxTimestamp.UnixMicro()
|
|
case Nanosecond:
|
|
minInt = MinTimestampNano.UnixNano()
|
|
maxInt = MaxTimestampNano.UnixNano()
|
|
}
|
|
|
|
if offset > 0 {
|
|
if dur > maxInt-offset {
|
|
return errors.Wrap(ErrTimestampOutOfRange, "value + epoch is too far from Unix epoch")
|
|
}
|
|
} else if dur < minInt-offset {
|
|
return errors.Wrap(ErrTimestampOutOfRange, "value + epoch is too far from Unix epoch")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Return default granularity if not set
|
|
func (t TimestampField) granularity() Unit {
|
|
if t.Granularity == "" {
|
|
return "s"
|
|
}
|
|
return Unit(t.Granularity)
|
|
}
|
|
|
|
// Return default layout if not set
|
|
func (t TimestampField) layout() string {
|
|
if t.Layout == "" {
|
|
return time.RFC3339Nano
|
|
}
|
|
return t.Layout
|
|
}
|
|
|
|
// Return default epoch if not set
|
|
func (t TimestampField) epoch() time.Time {
|
|
if t.Epoch.IsZero() {
|
|
return time.Unix(0, 0)
|
|
}
|
|
return t.Epoch
|
|
}
|
|
|
|
type DateIntField struct {
|
|
NameVal string
|
|
DestNameVal string
|
|
Layout string
|
|
Epoch time.Time
|
|
Unit Unit
|
|
CustomUnit string
|
|
}
|
|
|
|
func (d DateIntField) Name() string { return d.NameVal }
|
|
func (d DateIntField) DestName() string {
|
|
if d.DestNameVal == "" {
|
|
return d.NameVal
|
|
}
|
|
|
|
return d.DestNameVal
|
|
}
|
|
|
|
// PilosafyVal for a DateIntField takes a time.Time and int64 which
|
|
// represents the number units from the epoch.
|
|
func (d DateIntField) PilosafyVal(val interface{}) (interface{}, error) {
|
|
var vt time.Time
|
|
var err error
|
|
switch valt := val.(type) {
|
|
case nil:
|
|
return nil, nil
|
|
case []byte:
|
|
if len(valt) == 0 {
|
|
return nil, nil
|
|
}
|
|
vt, err = parseTimeWithLayout(d.layout(), string(valt))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting DateIntField []byte")
|
|
}
|
|
case string:
|
|
vt, err = parseTimeWithLayout(d.layout(), valt)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting DateIntField string")
|
|
}
|
|
case time.Time:
|
|
vt = valt
|
|
case uint, int, uint8, uint16, uint32, uint64, int8, int16, int32, int64:
|
|
return toInt64(valt)
|
|
default:
|
|
return nil, errors.Errorf("didn't know how to handle %v of type %[1]T in DateIntField", valt)
|
|
}
|
|
|
|
dur := vt.Sub(d.epoch())
|
|
|
|
var unit time.Duration
|
|
if d.Unit.IsCustom() {
|
|
if unit, err = time.ParseDuration(d.CustomUnit); err != nil {
|
|
return nil, errors.Wrapf(err, "parsing custom unit %s", d.CustomUnit)
|
|
}
|
|
} else {
|
|
if unit, err = d.Unit.Duration(); err != nil {
|
|
return nil, errors.Wrapf(err, "parsing unit %s", d.Unit)
|
|
}
|
|
}
|
|
|
|
return int64(dur / unit), nil
|
|
}
|
|
|
|
func (d DateIntField) epoch() time.Time {
|
|
if d.Epoch.IsZero() {
|
|
return time.Unix(0, 0)
|
|
}
|
|
return d.Epoch
|
|
}
|
|
|
|
func (d DateIntField) layout() string {
|
|
if d.Layout == "" {
|
|
// this is kind of a ridiculous default for layout
|
|
return "2006-01-02"
|
|
}
|
|
return d.Layout
|
|
}
|
|
|
|
func parseTimeWithLayout(layout string, val string) (time.Time, error) {
|
|
if val == "0000-00-00" { // TODO this is kind of a special hack
|
|
// that we should remove - was added
|
|
// for a particular case of unparsable
|
|
// data that we wanted to ignore.
|
|
return time.Time{}, nil
|
|
}
|
|
tim, err := time.Parse(layout, val)
|
|
if err != nil {
|
|
return time.Time{}, errors.Wrapf(err, "parsing time string %s", val)
|
|
}
|
|
return tim, nil
|
|
}
|
|
|
|
func timeFromEpoch(val interface{}, epoch time.Time, unit Unit) (interface{}, error) {
|
|
valAsInt, err := toInt64(val)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting value to int64")
|
|
}
|
|
|
|
dur, err := unit.DurationFromValue(valAsInt)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting duration from value")
|
|
}
|
|
|
|
return epoch.Add(dur), err
|
|
}
|
|
|
|
func timeFromTimestring(val interface{}, layout string) (time.Time, error) {
|
|
if val == nil {
|
|
return time.Time{}, nil
|
|
}
|
|
switch valt := val.(type) {
|
|
case nil:
|
|
return time.Time{}, nil
|
|
case []byte:
|
|
if len(valt) == 0 {
|
|
return time.Time{}, nil
|
|
}
|
|
vt, err := parseTimeWithLayout(layout, string(valt))
|
|
if err != nil {
|
|
return time.Time{}, errors.Wrap(err, "parsing []byte")
|
|
}
|
|
return vt, nil
|
|
case string:
|
|
if valt == "" {
|
|
return time.Time{}, nil
|
|
}
|
|
vt, err := parseTimeWithLayout(layout, valt)
|
|
if err != nil {
|
|
return time.Time{}, errors.Wrapf(err, "parsing time string %s", valt)
|
|
}
|
|
return vt, nil
|
|
case time.Time:
|
|
return valt, nil
|
|
default:
|
|
return time.Time{}, errors.Errorf("didn't know how to interpret %v of %[1]T as time", valt)
|
|
}
|
|
}
|
|
|
|
func toUint64(val interface{}) (uint64, error) {
|
|
switch vt := val.(type) {
|
|
case uint:
|
|
return uint64(vt), nil
|
|
case uint8:
|
|
return uint64(vt), nil
|
|
case uint16:
|
|
return uint64(vt), nil
|
|
case uint32:
|
|
return uint64(vt), nil
|
|
case uint64:
|
|
return vt, nil
|
|
case int:
|
|
return uint64(vt), nil
|
|
case int8:
|
|
return uint64(vt), nil
|
|
case int16:
|
|
return uint64(vt), nil
|
|
case int32:
|
|
return uint64(vt), nil
|
|
case int64:
|
|
return uint64(vt), nil
|
|
case float64:
|
|
return uint64(vt), nil
|
|
case string:
|
|
v, err := strconv.ParseUint(strings.TrimSpace(vt), 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return v, nil
|
|
default:
|
|
return 0, errors.Errorf("couldn't convert %v of %[1]T to uint64", vt)
|
|
}
|
|
}
|
|
|
|
func toBool(val interface{}) (bool, error) {
|
|
switch vt := val.(type) {
|
|
case bool:
|
|
return vt, nil
|
|
case byte:
|
|
if vt == '0' || vt == 'f' || vt == 'F' {
|
|
return false, nil
|
|
}
|
|
return vt != 0, nil
|
|
case string:
|
|
switch strings.ToLower(vt) {
|
|
case "", "0", "f", "false":
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
default:
|
|
if vint, err := toInt64(val); err == nil {
|
|
return vint != 0, nil
|
|
}
|
|
return false, errors.Errorf("couldn't convert %v of %[1]T to bool", vt)
|
|
}
|
|
}
|
|
|
|
func toString(val interface{}) (string, error) {
|
|
switch vt := val.(type) {
|
|
case string:
|
|
return vt, nil
|
|
case []byte:
|
|
return string(vt), nil
|
|
default:
|
|
if vt == nil {
|
|
return "", nil
|
|
}
|
|
return fmt.Sprintf("%v", val), nil
|
|
}
|
|
}
|
|
|
|
func toInt64(val interface{}) (int64, error) {
|
|
switch vt := val.(type) {
|
|
case uint:
|
|
return int64(vt), nil
|
|
case uint8:
|
|
return int64(vt), nil
|
|
case uint16:
|
|
return int64(vt), nil
|
|
case uint32:
|
|
return int64(vt), nil
|
|
case uint64:
|
|
return int64(vt), nil
|
|
case int:
|
|
return int64(vt), nil
|
|
case int8:
|
|
return int64(vt), nil
|
|
case int16:
|
|
return int64(vt), nil
|
|
case int32:
|
|
return int64(vt), nil
|
|
case int64:
|
|
return vt, nil
|
|
case float32:
|
|
return int64(vt), nil
|
|
case float64:
|
|
return int64(vt), nil
|
|
case string: // added this case because of mysql driver sending the ids as strings
|
|
v, err := strconv.ParseInt(strings.TrimSpace(vt), 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return v, nil
|
|
default:
|
|
return 0, errors.Errorf("couldn't convert %v of %[1]T to int64", vt)
|
|
}
|
|
}
|
|
|
|
func toStringArray(val interface{}) ([]string, error) {
|
|
switch vt := val.(type) {
|
|
case []string:
|
|
return vt, nil
|
|
case []uint64:
|
|
ret := make([]string, len(vt))
|
|
for i, v := range vt {
|
|
ret[i] = strconv.FormatUint(v, 10)
|
|
}
|
|
return ret, nil
|
|
case map[uint64]struct{}:
|
|
arr, err := toUint64Array(val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return toStringArray(arr)
|
|
case string:
|
|
if vt == "" {
|
|
return nil, nil
|
|
}
|
|
if strings.HasPrefix(vt, "[") && strings.HasSuffix(vt, "]") {
|
|
vt = vt[1 : len(vt)-1]
|
|
}
|
|
vals := strings.Split(vt, ",")
|
|
return vals, nil
|
|
case []interface{}:
|
|
ret := make([]string, len(vt))
|
|
for i, v := range vt {
|
|
vs, ok := v.(string)
|
|
if !ok {
|
|
return nil, errors.Errorf("couldn't convert []interface{} to []string, value %v of type %[1]T at %d", v, i)
|
|
}
|
|
ret[i] = vs
|
|
}
|
|
return ret, nil
|
|
default:
|
|
return nil, errors.Errorf("couldn't convert %v of %[1]T to []string", vt)
|
|
}
|
|
}
|
|
|
|
func toUint64Array(val interface{}) ([]uint64, error) {
|
|
switch vt := val.(type) {
|
|
case []interface{}:
|
|
if len(vt) == 0 {
|
|
// Empty/nil set.
|
|
return nil, nil
|
|
}
|
|
arr := make([]uint64, len(vt))
|
|
for i := range vt {
|
|
vv, err := toUint64(vt[i])
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "non uint64 value in []interface{}: %v (%[1]T)", vt[i])
|
|
}
|
|
arr[i] = vv
|
|
}
|
|
return arr, nil
|
|
case map[uint64]struct{}:
|
|
if len(vt) == 0 {
|
|
// Empty/nil set.
|
|
return nil, nil
|
|
}
|
|
arr := make([]uint64, len(vt))
|
|
i := 0
|
|
for v := range vt {
|
|
arr[i] = v
|
|
i++
|
|
}
|
|
|
|
// Move the elements into a deterministic order.
|
|
sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })
|
|
|
|
return arr, nil
|
|
case []uint64:
|
|
return vt, nil
|
|
case string:
|
|
if vt == "" {
|
|
return nil, nil
|
|
}
|
|
if strings.HasPrefix(vt, "[") && strings.HasSuffix(vt, "]") {
|
|
vt = vt[1 : len(vt)-1]
|
|
}
|
|
parts := strings.Split(vt, ",")
|
|
ret := make([]uint64, len(parts))
|
|
for i := range parts {
|
|
v, err := strconv.ParseUint(strings.TrimSpace(parts[i]), 10, 64)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "parsing uint64 from string: %s", vt)
|
|
}
|
|
ret[i] = v
|
|
}
|
|
return ret, nil
|
|
default:
|
|
return nil, errors.Errorf("couldn't convert %v of %[1]T to []uint64", vt)
|
|
}
|
|
}
|
|
|
|
// Fields is a list of Field, representing a schema.
|
|
type Fields []Field
|
|
|
|
// ContainsBool returns true if at least one field
|
|
// in the list is a BoolField.
|
|
func (f Fields) ContainsBool() bool {
|
|
for i := range f {
|
|
if _, ok := f[i].(BoolField); ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|