mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
477 lines
10 KiB
Go
477 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strconv"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/idk"
|
|
"github.com/pkg/errors"
|
|
|
|
pilosaclient "github.com/featurebasedb/featurebase/v3/client"
|
|
"github.com/featurebasedb/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
|
|
}
|