featurebase/idk/api/source.go
souhailanoor 2bdf1f5081
FB-1597: unifying idk and featurebase (#2160)
* unifying idk and featurebase: first pass

* resolved conflict with master for gitignore & dockerignore

* deleted binaries that were accidentally pushed to git

* combined gitlab jobs for idk & featurebase

* run go fmt for idk

* updated ssh env variable, and made docker password variable in gitlab env variables

* fixed typo assigning variable name

* trying to fix docker login error

* trying a different solution for docker password

* pass registry

* fixed docker login

* updated paths for idk

* exclude idk tests from featurebase test run

* fix vendor error

* update certificates

* grpc needs to be in version 1.38
genproto, which is imported by big query updates the grpc version to 1.47.0
grpc 1.47.0 causes etcd to deadlock when calling etcd.Close()
the fix is to have a replace in go.mod to specify a specific grpc version

* run go mod tidy

* go mod

* run go mod tidy

* exclude bigquery since it is causing issues and undo grpc replace in go.mod

* fix grpc version

* fix formatting error

* update formatting

* attempt to fix formatting

* update path for code coverage

* update to use current branch binaries, not master

* fix for building idk - path updates

* udpate path for binaries

* update job dependecies

* update docker idk tests to use the current branch registry

* update stages for jobs

* updated job dependencies

* not allow idk s3 dump to fail since it is a dependency for integration tests

* update dependecy for idk tests

* update paths for idk build and code coverage

* download featurebase binary from s3

* pass branch name to all setup scripts

* change to current branch instead of master

* updated sonarcloud

* sonarcloud fix and branch name fix

* trying to speed up pipeline run time

* update stage

* branch name fix + sonar cloud

* sonarcloud
2022-07-28 17:23:16 -05:00

477 lines
10 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"github.com/molecula/featurebase/v3/idk"
"github.com/pkg/errors"
pilosaclient "github.com/molecula/featurebase/v3/client"
"github.com/molecula/featurebase/v3/pql"
)
func IngestJSON(index *pilosaclient.Index, m idk.Main, in io.Reader) error {
codec, err := newJSONCodec(in)
if err != nil {
return errors.Wrap(err, "reading start of JSON data")
}
fields := index.Fields()
idkSchema := make([]idk.Field, 1, len(index.Fields())+1)
fieldMappers := make(map[string]mapper, len(fields))
if index.Opts().Keys() {
idkSchema[0] = idk.StringField{
NameVal: "/",
}
m.PrimaryKeyFields = []string{"/"}
} else {
idkSchema[0] = idk.IDField{
NameVal: "/",
}
m.IDField = "/"
}
m.Index = index.Name()
// TODO: time quantums
for name, field := range fields {
i := len(idkSchema)
fopts := field.Opts()
switch typ := fopts.Type(); typ {
case pilosaclient.FieldTypeSet:
if fopts.Keys() {
idkSchema = append(idkSchema, idk.StringArrayField{
NameVal: name,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
var set []string
switch v := v.(type) {
case string:
set = []string{v}
case []interface{}:
set = make([]string, len(v))
for i, elem := range v {
str, ok := elem.(string)
if !ok {
return nil, errors.Wrapf(TypeError{
Expected: typeDescriptionString,
Value: elem,
}, "parsing element %d of set", i)
}
set[i] = str
}
if len(set) > 1 {
dups := make(map[string]struct{}, len(set))
for _, elem := range set {
if _, dup := dups[elem]; dup {
return nil, ErrDuplicateElement{
Elem: elem,
}
}
dups[elem] = struct{}{}
}
}
default:
return nil, TypeError{
Expected: typeDescriptionStringSet,
Value: v,
}
}
for _, v := range set {
if v == "" {
return nil, errors.New("empty string in set")
}
}
return set, nil
},
}
} else {
idkSchema = append(idkSchema, idk.IDArrayField{
NameVal: name,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
var set []uint64
switch v := v.(type) {
case json.Number:
id, err := strconv.ParseUint(string(v), 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "parsing ID")
}
set = []uint64{id}
case []interface{}:
set = make([]uint64, len(v))
for i, elem := range v {
num, ok := elem.(json.Number)
if !ok {
return nil, errors.Wrapf(TypeError{
Expected: typeDescriptionID,
Value: elem,
}, "parsing element %d of set", i)
}
id, err := strconv.ParseUint(string(num), 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "parsing element %d of set", i)
}
set[i] = id
}
if len(set) > 1 {
dups := make(map[uint64]struct{}, len(set))
for _, elem := range set {
if _, dup := dups[elem]; dup {
return nil, ErrDuplicateElement{
Elem: elem,
}
}
dups[elem] = struct{}{}
}
}
default:
return nil, TypeError{
Expected: typeDescriptionIDSet,
Value: v,
}
}
for _, v := range set {
if v == ^uint64(0) {
// The client package uses this as a nil sentinel.
return nil, errors.New("max uint64 is not a valid row ID")
}
}
return set, nil
},
}
}
case pilosaclient.FieldTypeMutex:
if fopts.Keys() {
idkSchema = append(idkSchema, idk.StringField{
NameVal: name,
Mutex: true,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
str, ok := v.(string)
if !ok {
return nil, TypeError{
Expected: typeDescriptionString,
Value: v,
}
}
return str, nil
},
}
} else {
idkSchema = append(idkSchema, idk.IDField{
NameVal: name,
Mutex: true,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
num, ok := v.(json.Number)
if !ok {
return nil, TypeError{
Expected: typeDescriptionID,
Value: v,
}
}
id, err := strconv.ParseUint(string(num), 10, 64)
if err != nil {
return nil, err
}
if id == ^uint64(0) {
// The client package uses this as a nil sentinel.
return nil, errors.New("max uint64 is not a valid row ID")
}
return id, nil
},
}
}
/*
case pilosaclient.FieldTypeBool:
// TODO: not supported by IDK :)
idkSchema = append(idkSchema, idk.BoolField{
NameVal: name,
})
*/
case pilosaclient.FieldTypeInt:
if fopts.Keys() {
idkSchema = append(idkSchema, idk.StringField{
NameVal: name,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
str, ok := v.(string)
if !ok {
return nil, TypeError{
Expected: typeDescriptionString,
Value: v,
}
}
return str, nil
},
}
break
}
idkSchema = append(idkSchema, idk.IntField{
NameVal: name,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
number, ok := v.(json.Number)
if !ok {
return nil, TypeError{
Expected: typeDescriptionInt,
Value: v,
}
}
return strconv.ParseInt(string(number), 10, 64)
},
}
case pilosaclient.FieldTypeDecimal:
scale := fopts.Scale()
idkSchema = append(idkSchema, idk.DecimalField{
NameVal: name,
Scale: scale,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
var raw string
switch v := v.(type) {
case json.Number:
raw = string(v)
case string:
raw = string(v)
default:
return nil, TypeError{
Expected: typeDescriptionDecimal,
Value: v,
}
}
dec, err := pql.ParseDecimal(raw)
if err != nil {
return nil, err
}
return dec, nil
},
}
/*
// Pilosa unfortunately does not return the epoch to us.
// As a result, this does not currently work.
// Also now we need to parse the timestamp here.
case pilosaclient.FieldTypeTimestamp:
idkSchema = append(idkSchema, idk.TimestampField{
NameVal: name,
Epoch: fopts.MinTimestamp(),
Granularity: fopts.TimeUnit(),
Layout: time.RFC3339Nano,
})
fieldMappers[name] = mapper{
idx: i,
mapper: func(v interface{}) (interface{}, error) {
str, ok := v.(string)
if !ok {
return nil, TypeError{
Expected: "time string",
Value: v,
}
}
return str, nil
},
}*/
default:
// Ignore the field for now.
}
}
src := source{
c: codec,
fields: fieldMappers,
schema: idkSchema,
pkstring: index.Opts().Keys(),
}
m.NewSource = func() (idk.Source, error) {
return &src, nil
}
return m.Run()
}
// TypeError is an error indicating that the type of a value was set incorrectly.
type TypeError struct {
// Expected is a human-readable name of the expected type.
Expected string
// Value is the incorrectly-typed value.
Value interface{}
}
const (
typeDescriptionID = "ID"
typeDescriptionIDSet = "set of " + typeDescriptionID + "s"
typeDescriptionString = "string"
typeDescriptionStringSet = "set of " + typeDescriptionString + "s"
typeDescriptionInt = "integer"
typeDescriptionDecimal = "decimal"
)
func (t TypeError) Error() string {
valType := reflect.TypeOf(t.Value)
valTypeName := valType.String()
if name, ok := friendlyTypeNames[valType]; ok {
valTypeName = name
}
return fmt.Sprintf("expected a %s but got %v (a %s)", t.Expected, t.Value, valTypeName)
}
// friendlyTypeNames contains more-easily understood names for some common types.
var friendlyTypeNames = map[reflect.Type]string{
reflect.TypeOf(json.Number("")): "number",
reflect.TypeOf([]interface{}{}): "array",
reflect.TypeOf(map[string]interface{}{}): "object",
}
// ErrDuplicateElement is an error indicating that an element was included in a set multiple times.
type ErrDuplicateElement struct {
// Elem is the duplicated element.
Elem interface{}
}
func (err ErrDuplicateElement) Error() string {
return fmt.Sprintf("found duplicate element %v in set", err.Elem)
}
type source struct {
c *jsonCodec
pkstring bool
fields map[string]mapper
schema []idk.Field
}
type mapper struct {
idx int
mapper func(interface{}) (interface{}, error)
}
func (s *source) Record() (idk.Record, error) {
// Decode the next record.
raw, err := s.c.Next()
if err != nil {
return nil, err
}
// Parse the record into IDK-format.
out := make(idkRec, len(s.schema))
switch pk := raw.PrimaryKey.(type) {
case string:
if !s.pkstring {
return nil, errors.Wrap(TypeError{
Expected: typeDescriptionID,
Value: pk,
}, "parsing primary key")
}
out[0] = pk
case json.Number:
if s.pkstring {
return nil, errors.Wrap(TypeError{
Expected: typeDescriptionString,
Value: pk,
}, "parsing primary key")
}
id, err := strconv.ParseUint(string(pk), 10, 64)
if err != nil {
return nil, errors.Wrap(err, "parsing primary key")
}
out[0] = id
default:
expect := typeDescriptionID
if s.pkstring {
expect = typeDescriptionString
}
return nil, errors.Wrap(TypeError{
Expected: expect,
Value: pk,
}, "parsing primary key")
}
for field, value := range raw.Values {
m, ok := s.fields[field]
if !ok {
return nil, errors.Errorf("field %q missing from schema", field)
}
v, err := m.mapper(value)
if err != nil {
return nil, errors.Wrapf(err, "parsing record field %q", field)
}
out[m.idx] = v
}
return out, nil
}
func (s *source) Schema() []idk.Field {
return s.schema
}
func (s *source) Close() error {
return nil
}
type idkRec []interface{}
func (r idkRec) Commit(ctx context.Context) error {
return nil
}
func (r idkRec) Data() []interface{} {
return r
}