mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* serverless sqldb use same env for test config as normal * rip boltdb implementation of controller backend out it was replaced by postgres and no longer works properly. This involved migrating a number of tests which only worked with boltdb, which exposed several ways in which the postgres implementation had slightly different behavior from the bolt one: 1. ordering of results in some cases, and 2. (more importantly) erroring when a record to delete was not found. The bolt implementation silently ignored it when things to delete weren't found, so we make some changes to match that behavior. Also stopped propagating CreatedAt and UpdatedAt from DB tables into dax types. These were breaking existing tests. Perhaps it would be better to actually use them, but for now they will only exist at the DB level. This change set also moves the insertion of the directive_versions record out of migrations and into the startup/connection code. Having this in the migrations was a bit ugly because you couldn't just truncate all the tables and have everything work from scratch. Inserting it during startup is fairly innocuous, and will just continue on if it already exists. * update directive_version test I changed the initial value to 0 so that the first version that gets sent out is 1
490 lines
13 KiB
Go
490 lines
13 KiB
Go
package pilosa
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
"github.com/featurebasedb/featurebase/v3/errors"
|
|
"github.com/featurebasedb/featurebase/v3/pql"
|
|
)
|
|
|
|
// Ensure type implements interface.
|
|
var _ SchemaAPI = (*onPremSchema)(nil)
|
|
|
|
type onPremSchema struct {
|
|
api *API
|
|
}
|
|
|
|
func NewOnPremSchema(api *API) *onPremSchema {
|
|
return &onPremSchema{
|
|
api: api,
|
|
}
|
|
}
|
|
|
|
func (s *onPremSchema) CreateDatabase(context.Context, *dax.Database) error {
|
|
return errors.Errorf("unimplemented: onPremSchema.CreateDatabase()")
|
|
}
|
|
func (s *onPremSchema) DropDatabase(context.Context, dax.DatabaseID) error {
|
|
return errors.Errorf("unimplemented: onPremSchema.DropDatabase()")
|
|
}
|
|
|
|
func (s *onPremSchema) DatabaseByName(ctx context.Context, dbname dax.DatabaseName) (*dax.Database, error) {
|
|
return nil, errors.Errorf("unimplemented: onPremSchema.DatabaseByName()")
|
|
}
|
|
func (s *onPremSchema) DatabaseByID(ctx context.Context, dbid dax.DatabaseID) (*dax.Database, error) {
|
|
return nil, errors.Errorf("unimplemented: onPremSchema.DatabaseByID()")
|
|
}
|
|
func (s *onPremSchema) SetDatabaseOption(ctx context.Context, dbid dax.DatabaseID, option string, value string) error {
|
|
return nil
|
|
}
|
|
func (s *onPremSchema) Databases(context.Context, ...dax.DatabaseID) ([]*dax.Database, error) {
|
|
return []*dax.Database{}, nil
|
|
}
|
|
|
|
func (s *onPremSchema) TableByName(ctx context.Context, tname dax.TableName) (*dax.Table, error) {
|
|
idx, err := s.api.IndexInfo(context.Background(), string(tname))
|
|
if err != nil {
|
|
if err == ErrIndexNotFound {
|
|
return nil, dax.NewErrTableNameDoesNotExist(tname)
|
|
}
|
|
return nil, errors.Wrapf(err, "getting index info for table name: %s", tname)
|
|
}
|
|
|
|
return IndexInfoToTable(idx), nil
|
|
}
|
|
|
|
func (s *onPremSchema) TableByID(ctx context.Context, tid dax.TableID) (*dax.Table, error) {
|
|
idx, err := s.api.IndexInfo(context.Background(), string(tid))
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "getting index info for table id: %s", tid)
|
|
}
|
|
|
|
return IndexInfoToTable(idx), nil
|
|
}
|
|
|
|
func (s *onPremSchema) Tables(ctx context.Context) ([]*dax.Table, error) {
|
|
idxs, err := s.api.Schema(ctx, false)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting schema")
|
|
}
|
|
|
|
return IndexInfosToTables(idxs), nil
|
|
}
|
|
|
|
func (s *onPremSchema) CreateTable(ctx context.Context, tbl *dax.Table) error {
|
|
// We make a slice of fields with the _id field removed. Also, while we're
|
|
// at it, we can use the type of the _id field to determine if the index
|
|
// should be keyed.
|
|
var keyed bool
|
|
flds := make([]*dax.Field, 0)
|
|
for _, fld := range tbl.Fields {
|
|
if fld.Name == "_id" {
|
|
if fld.Type == dax.BaseTypeString {
|
|
keyed = true
|
|
}
|
|
continue
|
|
}
|
|
flds = append(flds, fld)
|
|
}
|
|
|
|
iopts := IndexOptions{
|
|
Keys: keyed,
|
|
TrackExistence: true,
|
|
PartitionN: tbl.PartitionN,
|
|
Description: tbl.Description,
|
|
}
|
|
|
|
// Add the index.
|
|
if _, err := s.api.CreateIndex(ctx, string(tbl.Name), iopts); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Now add fields.
|
|
for _, fld := range flds {
|
|
if err := s.CreateField(ctx, tbl.Name, fld); err != nil {
|
|
return errors.Wrapf(err, "creating field: %s", fld.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *onPremSchema) CreateField(ctx context.Context, tname dax.TableName, fld *dax.Field) error {
|
|
opts, err := FieldOptionsFromField(fld)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "creating field options from field: %s", fld.Name)
|
|
}
|
|
|
|
_, err = s.api.CreateField(ctx, string(tname), string(fld.Name), opts...)
|
|
return err
|
|
}
|
|
|
|
func (s *onPremSchema) DeleteTable(ctx context.Context, tname dax.TableName) error {
|
|
return s.api.DeleteIndex(ctx, string(tname))
|
|
}
|
|
|
|
func (s *onPremSchema) DeleteField(ctx context.Context, tname dax.TableName, fname dax.FieldName) error {
|
|
return s.api.DeleteField(ctx, string(tname), string(fname))
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// The following are helper functions which convert between
|
|
// featurebase.IndexInfo and dax.Table, and between featurebase.FieldInfo and
|
|
// dax.Field.
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
//
|
|
// Functions to convert from featurebase to dax.
|
|
//
|
|
|
|
// IndexInfosToTables converts a slice of featurebase.IndexInfo to a slice of
|
|
// dax.Table.
|
|
func IndexInfosToTables(iis []*IndexInfo) []*dax.Table {
|
|
tbls := make([]*dax.Table, 0, len(iis))
|
|
for _, ii := range iis {
|
|
tbls = append(tbls, IndexInfoToTable(ii))
|
|
}
|
|
return tbls
|
|
}
|
|
|
|
// IndexInfoToTable converts a featurebase.IndexInfo to a dax.Table.
|
|
func IndexInfoToTable(ii *IndexInfo) *dax.Table {
|
|
tbl := &dax.Table{
|
|
// TODO(tlt): be careful here. This ID=Name logic only applies to "onPrem".
|
|
ID: dax.TableID(ii.Name),
|
|
Name: dax.TableName(ii.Name),
|
|
Fields: make([]*dax.Field, 0, len(ii.Fields)+1), // +1 to account for the _id field
|
|
PartitionN: dax.DefaultPartitionN,
|
|
|
|
Description: ii.Options.Description,
|
|
Owner: ii.Owner,
|
|
UpdatedBy: ii.LastUpdateUser,
|
|
}
|
|
|
|
// Sort ii.Fields by CreatedAt before adding them to sortedFields.
|
|
sort.Slice(ii.Fields, func(i, j int) bool {
|
|
return ii.Fields[i].CreatedAt < ii.Fields[j].CreatedAt
|
|
})
|
|
|
|
// Add the _id Field.
|
|
var idType dax.BaseType = dax.BaseTypeID
|
|
if ii.Options.Keys {
|
|
idType = dax.BaseTypeString
|
|
}
|
|
tbl.Fields = append(tbl.Fields, &dax.Field{
|
|
Name: "_id",
|
|
Type: idType,
|
|
})
|
|
|
|
// Populate the rest of the fields.
|
|
for _, fld := range ii.Fields {
|
|
tbl.Fields = append(tbl.Fields, FieldInfoToField(fld))
|
|
}
|
|
|
|
return tbl
|
|
}
|
|
|
|
// FieldInfoToField converts a featurebase.FieldInfo to a dax.Field.
|
|
func FieldInfoToField(fi *FieldInfo) *dax.Field {
|
|
// Initialize field options; to be overridden based on field type specific
|
|
// options.
|
|
var fieldType dax.BaseType
|
|
var min pql.Decimal
|
|
var max pql.Decimal
|
|
var scale int64
|
|
var cacheType string
|
|
var cacheSize uint32
|
|
var timeUnit string
|
|
var epoch time.Time
|
|
var foreignIndex string
|
|
var timeQuantum dax.TimeQuantum
|
|
|
|
fo := &fi.Options
|
|
|
|
switch fo.Type {
|
|
case FieldTypeMutex:
|
|
if fo.Keys {
|
|
fieldType = dax.BaseTypeString
|
|
} else {
|
|
fieldType = dax.BaseTypeID
|
|
}
|
|
cacheType = fo.CacheType
|
|
cacheSize = fo.CacheSize
|
|
case FieldTypeSet:
|
|
if fo.Keys {
|
|
fieldType = dax.BaseTypeStringSet
|
|
} else {
|
|
fieldType = dax.BaseTypeIDSet
|
|
}
|
|
cacheType = fo.CacheType
|
|
cacheSize = fo.CacheSize
|
|
case FieldTypeInt:
|
|
min = fo.Min
|
|
max = fo.Max
|
|
fieldType = dax.BaseTypeInt
|
|
foreignIndex = fo.ForeignIndex
|
|
case FieldTypeDecimal:
|
|
min = fo.Min
|
|
max = fo.Max
|
|
scale = fo.Scale
|
|
fieldType = dax.BaseTypeDecimal
|
|
case FieldTypeTimestamp:
|
|
epoch = featurebaseFieldOptionsToEpoch(fo)
|
|
timeUnit = fo.TimeUnit
|
|
fieldType = dax.BaseTypeTimestamp
|
|
case FieldTypeBool:
|
|
fieldType = dax.BaseTypeBool
|
|
case FieldTypeTime:
|
|
if fo.Keys {
|
|
fieldType = dax.BaseTypeStringSetQ
|
|
} else {
|
|
fieldType = dax.BaseTypeIDSetQ
|
|
}
|
|
timeQuantum = dax.TimeQuantum(fo.TimeQuantum)
|
|
default:
|
|
panic(fmt.Sprintf("unhandled featurebase field type: %s", fo.Type))
|
|
}
|
|
|
|
return &dax.Field{
|
|
Name: dax.FieldName(fi.Name),
|
|
Type: fieldType,
|
|
Options: dax.FieldOptions{
|
|
Min: min,
|
|
Max: max,
|
|
Scale: scale,
|
|
NoStandardView: fo.NoStandardView,
|
|
CacheType: cacheType,
|
|
CacheSize: cacheSize,
|
|
TimeUnit: timeUnit,
|
|
Epoch: epoch,
|
|
TimeQuantum: timeQuantum,
|
|
TTL: fo.TTL,
|
|
ForeignIndex: foreignIndex,
|
|
},
|
|
}
|
|
}
|
|
|
|
// featurebaseFieldOptionsToEpoch produces an Epoch (time.Time) value based on
|
|
// the given featurebase FieldOptions.
|
|
func featurebaseFieldOptionsToEpoch(fo *FieldOptions) time.Time {
|
|
epochNano := fo.Base * TimeUnitNanos(fo.TimeUnit)
|
|
return time.Unix(0, epochNano)
|
|
}
|
|
|
|
// FieldInfosToFields converts a []*featurebase.FieldInfo to a []*dax.Field.
|
|
func FieldInfosToFields(fis []*FieldInfo) []*dax.Field {
|
|
fs := make([]*dax.Field, 0, len(fis))
|
|
for i := range fis {
|
|
fs = append(fs, FieldInfoToField(fis[i]))
|
|
}
|
|
return fs
|
|
}
|
|
|
|
//
|
|
// Functions to convert from dax to featurebase.
|
|
//
|
|
|
|
// TablesToIndexInfos converts a slice of dax.Table to a slice of
|
|
// featurease.IndexInfo.
|
|
func TablesToIndexInfos(tbls []*dax.Table) []*IndexInfo {
|
|
iis := make([]*IndexInfo, 0, len(tbls))
|
|
for _, tbl := range tbls {
|
|
iis = append(iis, TableToIndexInfo(tbl))
|
|
}
|
|
return iis
|
|
}
|
|
|
|
// TableToIndexInfo converts a dax.Table to a featurease.IndexInfo.
|
|
func TableToIndexInfo(tbl *dax.Table) *IndexInfo {
|
|
ii := &IndexInfo{
|
|
Name: string(tbl.Name),
|
|
Owner: tbl.Owner,
|
|
LastUpdateUser: tbl.UpdatedBy,
|
|
Options: IndexOptions{
|
|
Keys: tbl.StringKeys(),
|
|
TrackExistence: true,
|
|
Description: tbl.Description,
|
|
},
|
|
ShardWidth: ShardWidth,
|
|
}
|
|
|
|
// fields
|
|
fields := make([]*FieldInfo, 0, len(tbl.Fields)-1)
|
|
for i := range tbl.Fields {
|
|
if tbl.Fields[i].Name == "_id" {
|
|
continue
|
|
}
|
|
fields = append(fields, FieldToFieldInfo(tbl.Fields[i]))
|
|
}
|
|
ii.Fields = fields
|
|
|
|
return ii
|
|
}
|
|
|
|
// FieldToFieldInfo converts a dax.Field to a featurebase.FieldInfo. Note: it
|
|
// does not return errors; there is one scenario where a timestamp epoch could
|
|
// be out of range. In that case, this function will only log the error, and the
|
|
// proceed with timestamp option values which are likely incorrect. We are going
|
|
// to leave this as is for now because, since this is used for internal
|
|
// conversions of types which already exist and have been validated, we assume
|
|
// the option values are valid.
|
|
// TODO(tlt): add error handling to this function; worst case: panic.
|
|
func FieldToFieldInfo(fld *dax.Field) *FieldInfo {
|
|
var timeUnit string
|
|
var base int64
|
|
min := fld.Options.Min
|
|
max := fld.Options.Max
|
|
|
|
switch fld.Type {
|
|
case dax.BaseTypeTimestamp:
|
|
timestampOptions, err := fieldOptionsForTimestamp(fld.Options)
|
|
if err != nil {
|
|
log.Printf("ERROR: converting timestamp options: %v", err)
|
|
}
|
|
timeUnit = timestampOptions.TimeUnit
|
|
base = timestampOptions.Base
|
|
min = timestampOptions.Min
|
|
max = timestampOptions.Max
|
|
}
|
|
|
|
return &FieldInfo{
|
|
Name: string(fld.Name),
|
|
Options: FieldOptions{
|
|
Type: fieldToFieldType(fld),
|
|
Base: base,
|
|
Min: min,
|
|
Max: max,
|
|
Scale: fld.Options.Scale,
|
|
Keys: fld.StringKeys(),
|
|
NoStandardView: fld.Options.NoStandardView,
|
|
CacheType: fld.Options.CacheType,
|
|
CacheSize: fld.Options.CacheSize,
|
|
TimeUnit: timeUnit,
|
|
TimeQuantum: TimeQuantum(fld.Options.TimeQuantum),
|
|
TTL: fld.Options.TTL,
|
|
ForeignIndex: fld.Options.ForeignIndex,
|
|
},
|
|
Views: nil, // TODO(tlt): do we need views populated?
|
|
}
|
|
}
|
|
|
|
// fieldOptionsForTimestamp produces a featurebase.FieldOptions value with the
|
|
// timestamp-related options populated.
|
|
func fieldOptionsForTimestamp(fo dax.FieldOptions) (*FieldOptions, error) {
|
|
out := &FieldOptions{}
|
|
|
|
// Check if the epoch will overflow when converted to nano.
|
|
if err := CheckEpochOutOfRange(fo.Epoch, MinTimestampNano, MaxTimestampNano); err != nil {
|
|
return out, errors.Wrap(err, "checking overflow")
|
|
}
|
|
|
|
out.TimeUnit = fo.TimeUnit
|
|
out.Base = fo.Epoch.UnixNano() / TimeUnitNanos(fo.TimeUnit)
|
|
out.Min = pql.NewDecimal(MinTimestamp.UnixNano()/TimeUnitNanos(fo.TimeUnit), 0)
|
|
out.Max = pql.NewDecimal(MaxTimestamp.UnixNano()/TimeUnitNanos(fo.TimeUnit), 0)
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// fieldToFieldType returns the featurebase.FieldType for the given dax.Field.
|
|
func fieldToFieldType(f *dax.Field) string {
|
|
switch f.Type {
|
|
case dax.BaseTypeID, dax.BaseTypeString:
|
|
if f.Name == dax.PrimaryKeyFieldName {
|
|
return string(f.Type)
|
|
}
|
|
return "mutex"
|
|
|
|
case dax.BaseTypeIDSet, dax.BaseTypeStringSet:
|
|
return "set"
|
|
|
|
case dax.BaseTypeIDSetQ, dax.BaseTypeStringSetQ:
|
|
return "time"
|
|
|
|
default:
|
|
return string(f.Type)
|
|
}
|
|
}
|
|
|
|
func FieldFromFieldOptions(fname dax.FieldName, opts ...FieldOption) (*dax.Field, error) {
|
|
fo, err := newFieldOptions(opts...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "creating new field options")
|
|
}
|
|
|
|
fi := &FieldInfo{
|
|
Name: string(fname),
|
|
Options: *fo,
|
|
}
|
|
|
|
return FieldInfoToField(fi), nil
|
|
}
|
|
|
|
// FieldOptionsFromField returns a slice of featurebase.FieldOption based on the
|
|
// given dax.Field.
|
|
func FieldOptionsFromField(fld *dax.Field) ([]FieldOption, error) {
|
|
// Set the cache type and size (or use default) for those fields which
|
|
// require them.
|
|
cacheType := DefaultCacheType
|
|
cacheSize := uint32(DefaultCacheSize)
|
|
if fld.Options.CacheType != "" {
|
|
cacheType = fld.Options.CacheType
|
|
cacheSize = fld.Options.CacheSize
|
|
}
|
|
|
|
opts := []FieldOption{}
|
|
|
|
switch fld.Type {
|
|
case dax.BaseTypeBool:
|
|
opts = append(opts,
|
|
OptFieldTypeBool(),
|
|
)
|
|
case dax.BaseTypeDecimal:
|
|
opts = append(opts,
|
|
OptFieldTypeDecimal(fld.Options.Scale, fld.Options.Min, fld.Options.Max),
|
|
)
|
|
case dax.BaseTypeID:
|
|
opts = append(opts,
|
|
OptFieldTypeMutex(cacheType, cacheSize),
|
|
)
|
|
case dax.BaseTypeIDSet:
|
|
opts = append(opts,
|
|
OptFieldTypeSet(cacheType, cacheSize),
|
|
)
|
|
case dax.BaseTypeIDSetQ:
|
|
opts = append(opts,
|
|
OptFieldTypeTime(TimeQuantum(fld.Options.TimeQuantum), fld.Options.TTL.String()),
|
|
)
|
|
case dax.BaseTypeInt:
|
|
opts = append(opts,
|
|
OptFieldTypeInt(fld.Options.Min.ToInt64(0), fld.Options.Max.ToInt64(0)),
|
|
)
|
|
case dax.BaseTypeString:
|
|
opts = append(opts,
|
|
OptFieldTypeMutex(cacheType, cacheSize),
|
|
OptFieldKeys(),
|
|
)
|
|
case dax.BaseTypeStringSet:
|
|
opts = append(opts,
|
|
OptFieldTypeSet(cacheType, cacheSize),
|
|
OptFieldKeys(),
|
|
)
|
|
case dax.BaseTypeStringSetQ:
|
|
opts = append(opts,
|
|
OptFieldTypeTime(TimeQuantum(fld.Options.TimeQuantum), fld.Options.TTL.String()),
|
|
OptFieldKeys(),
|
|
)
|
|
case dax.BaseTypeTimestamp:
|
|
opts = append(opts,
|
|
OptFieldTypeTimestamp(fld.Options.Epoch, fld.Options.TimeUnit),
|
|
)
|
|
default:
|
|
return nil, errors.Errorf("unsupport field type: %s", fld.Type)
|
|
}
|
|
|
|
return opts, nil
|
|
}
|