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{} Schema() 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 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 if _, ok := val.([]byte); ok { valAsString := string(val.([]byte)[:]) // try to convert as time string ts, err := timeFromTimestring(valAsString, t.layout()) if err != nil { // if that doesn't work, maybe it's an int represented as a string valAsInt, err := strconv.ParseInt(valAsString, 0, 64) if err == nil { return valAsInt - epochAsVal, nil } else { return nil, errors.Wrap(err, "converting TimestampField") } } 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: vt = strings.ToLower(vt) vt = strings.TrimSpace(vt) switch vt { case "", "0", "f", "false": return false, nil case "1", "t", "true": return true, nil } return false, errors.Errorf("couldn't convert %v of %[1]T to bool", vt) 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 case []byte: return toInt64(string(vt[:])) 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 { switch v := v.(type) { case []byte: ret[i] = string(v[:]) case string: ret[i] = v default: return nil, errors.Errorf("couldn't convert []interface{} to []string, value %v of type %[1]T at %d", v, i) } } 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 } // SchemaManager is meant to be an interface for managing schema information; // i.e. for interacting with a single source of truth for schema information, // like the Serverless Schemar. But... it currently contains methods which are // not related to schema because the first goal was just to introduce an // interface in ingest.go for any methods being called on *m.client. We don't // want a FeatureBase client directly called from ingest, rather, we want to // call these interface methods and allow for different implementations (such as // a Serverless implementation which uses the Schemar in Serverless as opposed // to a FeatureBase node or cluster). type SchemaManager interface { StartTransaction(id string, timeout time.Duration, exclusive bool, requestTimeout time.Duration) (*pilosacore.Transaction, error) FinishTransaction(id string) (*pilosacore.Transaction, error) Schema() (*pilosaclient.Schema, error) SyncIndex(index *pilosaclient.Index) error DeleteIndex(index *pilosaclient.Index) error Status() (pilosaclient.Status, error) SetAuthToken(string) } // Ensure type implements interface. var _ SchemaManager = &nopSchemaManager{} // NopSchemaManager is an implementation of the SchemaManager interface that // doesn't do anything. var NopSchemaManager SchemaManager = &nopSchemaManager{} type nopSchemaManager struct{} func (n *nopSchemaManager) StartTransaction(id string, timeout time.Duration, exclusive bool, requestTimeout time.Duration) (*pilosacore.Transaction, error) { return nil, nil } func (n *nopSchemaManager) FinishTransaction(id string) (*pilosacore.Transaction, error) { return nil, nil } func (n *nopSchemaManager) Schema() (*pilosaclient.Schema, error) { return pilosaclient.NewSchema(), nil } func (n *nopSchemaManager) SyncIndex(index *pilosaclient.Index) error { return nil } func (n *nopSchemaManager) DeleteIndex(index *pilosaclient.Index) error { return nil } func (n *nopSchemaManager) Status() (pilosaclient.Status, error) { return pilosaclient.Status{}, nil } func (n *nopSchemaManager) SetAuthToken(token string) {}