featurebase/idk/api/schema.go
2022-12-12 09:01:20 -08:00

184 lines
6 KiB
Go

package api
import (
"encoding/json"
"io"
"time"
pilosa "github.com/featurebasedb/featurebase/v3"
pilosaclient "github.com/featurebasedb/featurebase/v3/client"
clienttypes "github.com/featurebasedb/featurebase/v3/client/types"
"github.com/pkg/errors"
)
type schema struct {
IndexName string `json:"index-name"`
IfNotExists bool `json:"if-not-exists,omitempty"`
PrimaryKeyType string `json:"primary-key-type"`
Fields []schemaField `json:"fields"`
}
type schemaField struct {
FieldName string `json:"field-name"`
FieldType string `json:"field-type"`
FieldOptions struct {
TimeQuantum string `json:"time-quantum,omitempty"`
CacheSize int `json:"cache-size,omitempty"`
CacheType string `json:"cache-type,omitempty"`
EnforceMutualExclusion bool `json:"enforce-mutual-exclusion,omitempty"`
Scale int `json:"scale,omitempty"`
Epoch string `json:"epoch,omitempty"`
Unit string `json:"unit,omitempty"`
TTL string `json:"ttl,omitempty"`
} `json:"field-options,omitempty"`
}
// DecodeSchema takes JSON stream (io.Reader - mainly http request body)
// and tries to decode it into pilosa.Schema.
// The function returns also a map: index-name -> if-not-exists option,
// so we can swallow "conflict" error in case of creating an index with
// the name as existing one.
func DecodeSchema(r io.Reader) (*pilosaclient.Schema, map[string]bool, error) {
pilosaSchema := pilosaclient.NewSchema()
// map: index-name -> if-not-exists option
ifNotExists := make(map[string]bool)
// Create a JSON decoder.
d := json.NewDecoder(r)
// This is just a generic loop-approach where we can iterate over JSON objects
// (stream of indexes), but on daily-bases we should just decode one index.
for d.More() {
var apiSchema schema
if err := d.Decode(&apiSchema); err != nil {
return nil, nil, errors.Wrap(err, "decoding schema")
}
err := apiSchema.applyToPilosa(pilosaSchema)
if err != nil {
return nil, nil, errors.Wrapf(err, "applying schema of index %q", apiSchema.IndexName)
}
ifNotExists[apiSchema.IndexName] = apiSchema.IfNotExists
}
return pilosaSchema, ifNotExists, nil
}
// applyToPilosa validates an index schema and adds it to a Pilosa schema.
func (s schema) applyToPilosa(ps *pilosaclient.Schema) error {
if err := pilosa.ValidateName(s.IndexName); err != nil {
return errors.Wrap(err, "validating index name")
}
var keyed bool
switch s.PrimaryKeyType {
case "string":
keyed = true
case "uint":
keyed = false
case "auto":
return errors.New("auto primary keys not yet implemented")
default:
return errors.Errorf(`invalid primary-key-type %q, it must be "string", "uint" or "auto"`, s.PrimaryKeyType)
}
idx := ps.Index(s.IndexName,
pilosaclient.OptIndexTrackExistence(true),
pilosaclient.OptIndexKeys(keyed),
)
for _, f := range s.Fields {
err := f.applyToPilosa(idx)
if err != nil {
return errors.Wrapf(err, "applying field %q from schema", f.FieldName)
}
}
return nil
}
// applyToPilosa validates a field schema and adds it to an index schema.
func (f schemaField) applyToPilosa(idx *pilosaclient.Index) error {
if err := pilosa.ValidateName(f.FieldName); err != nil {
return errors.Wrap(err, "validating field name")
}
if idx.HasField(f.FieldName) {
return errors.New("field defined multiple times")
}
var opts []pilosaclient.FieldOption
switch f.FieldType {
case "id":
switch {
case f.FieldOptions.EnforceMutualExclusion:
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeMutex(pilosaclient.CacheType(f.FieldOptions.CacheType), f.FieldOptions.CacheSize)}
case f.FieldOptions.TimeQuantum != "":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeTime(clienttypes.TimeQuantum(f.FieldOptions.TimeQuantum))}
if f.FieldOptions.TTL != "" {
ttl, err := time.ParseDuration(f.FieldOptions.TTL)
if err != nil {
return errors.Wrapf(err, "unable to parse TTL from field %s", f.FieldName)
} else {
opts = append(opts, pilosaclient.OptFieldTTL(ttl))
}
}
default:
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeSet(pilosaclient.CacheType(f.FieldOptions.CacheType), f.FieldOptions.CacheSize)}
}
case "string":
switch {
case f.FieldOptions.EnforceMutualExclusion:
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeMutex(pilosaclient.CacheType(f.FieldOptions.CacheType), f.FieldOptions.CacheSize)}
case f.FieldOptions.TimeQuantum != "":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeTime(clienttypes.TimeQuantum(f.FieldOptions.TimeQuantum))}
if f.FieldOptions.TTL != "" {
ttl, err := time.ParseDuration(f.FieldOptions.TTL)
if err != nil {
return errors.Wrapf(err, "unable to parse TTL from field %s", f.FieldName)
} else {
opts = append(opts, pilosaclient.OptFieldTTL(ttl))
}
}
default:
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeSet(pilosaclient.CacheType(f.FieldOptions.CacheType), f.FieldOptions.CacheSize)}
}
case "bool":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeBool()}
case "int":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeInt()}
case "decimal":
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeDecimal(int64(f.FieldOptions.Scale))}
case "timestamp":
epoch := time.Unix(0, 0)
if f.FieldOptions.Epoch != "" {
var err error
epoch, err = time.Parse(time.RFC3339, f.FieldOptions.Epoch)
if err != nil {
return errors.Errorf("invalid epoch time %q for layout %q", f.FieldOptions.Epoch, time.RFC3339)
}
}
unit := f.FieldOptions.Unit
if !pilosa.IsValidTimeUnit(unit) {
return errors.Errorf("invalid time unit %q", f.FieldOptions.Unit)
}
opts = []pilosaclient.FieldOption{pilosaclient.OptFieldTypeTimestamp(epoch, unit)}
default:
return errors.Errorf(`invalid field-type %q, it must be "id", "string", "bool", "int", "decimal" or "timestamp"`, f.FieldType)
}
idx.Field(f.FieldName, opts...)
return nil
}