mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* Fix formatting in CLI results with custom SQLResonse.UnmarshalJSON
When I started this, it was meant to be a quick fix to address the confusing
result formats we were seeing in the CLI. For example, all large integer values
were displayed in scientifc notation. This is because we were passing the result
types from JSON (in this case, float64) into pretty print. Similarly, `IDSets`
and `StringSets` where being printed using the default go Stringer for the types
[]int64 and []string respectively.
I started by writing a customer UnmarshalJSON() method for the `SQLResponse`
type. Part of this (the part which converts data types based on header types)
was already being used in dax tests, so this just formalizes that logic as part
of the `SQLResponse` type.
Then I realized that the sql3 tests (run against the `sql3` package) were
failing because sql3 is not actually returning the `IDSets` and `StringSets`
types. A future task is to formalize return types, define them, and modify sql3
to return them. Once that is done, we can remove the "typed" switch in the
`SQLResponse` json unmarshaller.
Another significant change is the modification to the `ExprDataType` interface:
```
type ExprDataType interface {
exprDataType()
TypeName() string
TypeDescription() string
TypeInfo() map[string]interface{}
}
```
I added two more methods in order to distinguish between a type (`DECIMAL`), its
description (`DECIMAL(2)`), and its type info (`"scale": int64(2)`). Currently,
the description can be used as the field definition in a CREATE TABLE statement,
but we may want to re-think that. Also, Decimal is the only type currently using
TypeInfo.
Finally, I tried to consilidate things around `dax.FieldType` instead of
comparing against parser types outside of sql3. We still have some sql3 parser
and planner types lurking about, but we can address those in future commits.
* Add some test coverage
* smoke test expected INT, now int
* minor fixes
* Introduce WireQueryResponse and related types
This also changes dax.FieldType to dax.BaseType.
* Populate WireQueryResponse correctly
Currently this is in the http handler, and in the queryer.
* Convert sql3 and dax tests to expect pilosa.WireQueryField in results
* fix PQL tests in the SQL defs
* Address a few of the skipped sql tests in dax
(cherry picked from commit f4385df2cf)
983 lines
30 KiB
Go
983 lines
30 KiB
Go
// Copyright 2021 Molecula Corp. All rights reserved.
|
|
package pilosa
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"sync"
|
|
|
|
"github.com/molecula/featurebase/v3/dax"
|
|
"github.com/molecula/featurebase/v3/dax/computer"
|
|
"github.com/molecula/featurebase/v3/disco"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// ApplyDirective applies a Directive received, from the Controller, at the
|
|
// /directive endpoint.
|
|
func (api *API) ApplyDirective(ctx context.Context, d *dax.Directive) error {
|
|
// Get the current directive for comparison.
|
|
previousDirective := api.holder.Directive()
|
|
|
|
// Check that incoming version is newer.
|
|
// Note: 0 is an invalid Directive version. This decision was made because
|
|
// previousDirective is not a pointer to a directive, but a concrete
|
|
// Directive. Which means we can't check for nil, and by default it has a
|
|
// version of 0. So in order to ensure the version has increased, we need to
|
|
// require that incoming directive versions are greater than 0.
|
|
if d.Version == 0 {
|
|
return errors.Errorf("directive version cannot be 0")
|
|
} else if previousDirective.Version >= d.Version {
|
|
return errors.Errorf("directive version mismatch, got %d, but already have %d", d.Version, previousDirective.Version)
|
|
}
|
|
|
|
// Handle the operations based on the directive method.
|
|
switch d.Method {
|
|
case dax.DirectiveMethodDiff:
|
|
// pass: normal operation
|
|
|
|
case dax.DirectiveMethodReset:
|
|
// Delete all tables.
|
|
if err := api.deleteAllIndexes(ctx); err != nil {
|
|
return errors.Wrap(err, "deleting all indexes")
|
|
}
|
|
|
|
// Set previousDirective to empty so the diff handles everything as new.
|
|
previousDirective = dax.Directive{}
|
|
|
|
case dax.DirectiveMethodSnapshot:
|
|
// TODO(tlt): this was the existing logic, but we should really diff the
|
|
// directive and ensure that overwriting the value in the cache doesn't
|
|
// have a negative effect.
|
|
api.holder.SetDirective(d)
|
|
return nil
|
|
|
|
default:
|
|
return errors.Errorf("invalid directive method: %s", d.Method)
|
|
}
|
|
|
|
// Cache this directive as the latest applied. There is functionality within
|
|
// the "enactDirective" stage of ApplyDirective which validates against this
|
|
// cached Directive, so it's important that it be set before calling
|
|
// enactDirective(). An example: when loading partition data from the
|
|
// WriteLogger, there are validations to ensure that the partition being
|
|
// loaded is meant to be handled by this node; that validation is done
|
|
// against the cached Directive.
|
|
// TODO(tlt): despite what this comment says, this logic is not sound; we
|
|
// shouldn't be setting the directive until enactiveDirective() succeeds.
|
|
api.holder.SetDirective(d)
|
|
|
|
return api.enactDirective(ctx, &previousDirective, d)
|
|
}
|
|
|
|
// deleteAllIndexes deletes all indexes handled by this node.
|
|
func (api *API) deleteAllIndexes(ctx context.Context) error {
|
|
indexes, err := api.Schema(ctx, false)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting schema")
|
|
}
|
|
|
|
for i := range indexes {
|
|
if err := api.DeleteIndex(ctx, indexes[i].Name); err != nil {
|
|
return errors.Wrapf(err, "deleting index: %s", indexes[i].Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// directiveJobType allows us to switch on jobType in the directiveWorker in
|
|
// order to use a single worker pool for all job types (as opposed to having a
|
|
// separate worker pool for each job type).
|
|
type directiveJobType interface {
|
|
// We have this method just to prevent *any* struct from implementing this
|
|
// interface automatically. But, interestingly enough, we don't actually
|
|
// have to have this method on the implementation because we embed the
|
|
// interface.
|
|
isJobType() bool
|
|
}
|
|
|
|
type directiveJobTableKeys struct {
|
|
directiveJobType
|
|
idx *Index
|
|
tkey dax.TableKey
|
|
partition dax.VersionedPartition
|
|
}
|
|
|
|
type directiveJobFieldKeys struct {
|
|
directiveJobType
|
|
tkey dax.TableKey
|
|
field dax.VersionedField
|
|
}
|
|
|
|
type directiveJobShards struct {
|
|
directiveJobType
|
|
tkey dax.TableKey
|
|
shard dax.VersionedShard
|
|
}
|
|
|
|
// directiveWorker is a worker in a worker pool which handles portions of a
|
|
// directive. Multiple instances of directiveWorker run in goroutines in order
|
|
// to load data from snapshotter and writelogger concurrently. Note: unlike the
|
|
// api.ingestWorkerPool, of which one pool is always running, the
|
|
// directiveWorker pool is only running during the life of the
|
|
// api.ApplyDirective call. Technically, this means that multiple
|
|
// directiveWorker pools could be active at the same time, but we should never
|
|
// be running more than once instance of ApplyDirective concurrently.
|
|
func (api *API) directiveWorker(ctx context.Context, jobs <-chan directiveJobType, errs chan<- error) {
|
|
for j := range jobs {
|
|
switch job := j.(type) {
|
|
case directiveJobTableKeys:
|
|
if err := api.loadTableKeys(ctx, job.idx, job.tkey, job.partition); err != nil {
|
|
errs <- errors.Wrapf(err, "loading table keys: %s, %s", job.tkey, job.partition)
|
|
}
|
|
case directiveJobFieldKeys:
|
|
if err := api.loadFieldKeys(ctx, job.tkey, job.field); err != nil {
|
|
errs <- errors.Wrapf(err, "loading field keys: %s, %s", job.tkey, job.field)
|
|
}
|
|
case directiveJobShards:
|
|
if err := api.loadShard(ctx, job.tkey, job.shard); err != nil {
|
|
errs <- errors.Wrapf(err, "loading shard: %s, %s", job.tkey, job.shard)
|
|
}
|
|
default:
|
|
errs <- errors.Errorf("unsupported job type: %T %[1]v", job)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
// continue pulling jobs off the channel
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) enactDirective(ctx context.Context, fromD, toD *dax.Directive) error {
|
|
// enactTables is called before the jobs that run in the worker pool because
|
|
// it probably makes sense to apply the schema before trying to load data
|
|
// concurrently.
|
|
if err := api.enactTables(ctx, fromD, toD); err != nil {
|
|
return errors.Wrap(err, "enactTables")
|
|
}
|
|
|
|
// The following types use a shared pool of workers to run each
|
|
// directiveJobType.
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// open job channel
|
|
jobs := make(chan directiveJobType, api.directiveWorkerPoolSize)
|
|
errs := make(chan error)
|
|
done := make(chan struct{})
|
|
|
|
// Spin up n workers in goroutines that pull jobs from the jobs channel.
|
|
for i := 0; i < api.directiveWorkerPoolSize; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
api.directiveWorker(ctx, jobs, errs)
|
|
defer wg.Done()
|
|
}()
|
|
}
|
|
|
|
// Wait for the WaitGroup counter to reach 0. When it has, indicate that
|
|
// we're done processing all jobs by closing the done channel.
|
|
go func() {
|
|
wg.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
// Run through all the "enact" methods. These push jobs onto the jobs
|
|
// channel. Once all the jobs have been queued to the channel, we close the
|
|
// jobs channel. This allows the directiveWorkers to exit out of the
|
|
// function, which will then decrement the WaitGroup counter.
|
|
go func() {
|
|
api.pushJobsTableKeys(ctx, jobs, fromD, toD)
|
|
api.pushJobsFieldKeys(ctx, jobs, fromD, toD)
|
|
api.pushJobsShards(ctx, jobs, fromD, toD)
|
|
close(jobs)
|
|
}()
|
|
|
|
// Keep running until we get an error or until the done channel is closed.
|
|
// Note: the code is written such that only non-nil errors are pushed to the
|
|
// errs channel.
|
|
for {
|
|
select {
|
|
case err := <-errs:
|
|
return err
|
|
case <-done:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) enactTables(ctx context.Context, fromD, toD *dax.Directive) error {
|
|
currentIndexes := api.holder.Indexes()
|
|
|
|
// Make a list of indexes that currently exist (from).
|
|
from := make(dax.TableKeys, 0, len(currentIndexes))
|
|
for _, idx := range currentIndexes {
|
|
qtid, err := dax.QualifiedTableIDFromKey(idx.Name())
|
|
if err != nil {
|
|
return errors.Wrap(err, "converting index name to qualified table id")
|
|
}
|
|
from = append(from, qtid.Key())
|
|
}
|
|
|
|
// TODO sanity check holder against fromD. We're getting existing
|
|
// indexes from holder, but in theory fromD should be
|
|
// identical. If we have an error in our directive-caching logic
|
|
// (it has happened before (just now, in fact!) and we'd be
|
|
// foolish to think it won't happen again), or we have schema
|
|
// mutations that are not going through the directive path, we
|
|
// could potentially catch them here.
|
|
|
|
// Make a list of tables that are in the directive (to) along with a map of
|
|
// tableKey to table (m).
|
|
m := make(map[dax.TableKey]*dax.QualifiedTable, len(toD.Tables))
|
|
to := make(dax.TableKeys, 0, len(toD.Tables))
|
|
for _, t := range toD.Tables {
|
|
m[t.Key()] = t
|
|
to = append(to, t.Key())
|
|
}
|
|
|
|
sc := newSliceComparer(from, to)
|
|
|
|
// Remove all indexes that are no longer part of the directive.
|
|
for _, tkey := range sc.removed() {
|
|
idx := string(tkey)
|
|
if err := api.holder.deleteIndex(idx); err != nil {
|
|
return errors.Wrapf(err, "deleting index: %s", tkey)
|
|
}
|
|
}
|
|
|
|
// Put partitions into a map by table.
|
|
partitionMap := toD.TranslatePartitionsMap()
|
|
|
|
// Add all indexes that weren't previously (but now are) a part of the
|
|
// directive.
|
|
for _, tkey := range sc.added() {
|
|
if qtbl, found := m[tkey]; !found {
|
|
return errors.Errorf("table '%s' was not in map", tkey)
|
|
} else if err := api.createTableAndFields(qtbl, partitionMap[tkey]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check fields on all indexes present in both from and to.
|
|
for _, tkey := range sc.same() {
|
|
if err := api.enactFieldsForTable(ctx, tkey, fromD, toD); err != nil {
|
|
return errors.Wrapf(err, "enacting fields for table: '%s'", tkey)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) enactFieldsForTable(ctx context.Context, tkey dax.TableKey, fromD, toD *dax.Directive) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
fromT, err := fromD.Table(qtid)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting from table")
|
|
}
|
|
toT, err := toD.Table(qtid)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting to table")
|
|
}
|
|
|
|
// Get the index for tkey.
|
|
idx := api.holder.Index(string(tkey))
|
|
if idx == nil {
|
|
return errors.Errorf("index not found: %s", tkey)
|
|
}
|
|
|
|
sc := newSliceComparer(fromT.FieldNames(), toT.FieldNames())
|
|
|
|
// Add fields new to toT.
|
|
for _, fldName := range sc.added() {
|
|
if field, found := toT.Field(fldName); !found {
|
|
return dax.NewErrFieldDoesNotExist(fldName)
|
|
} else if err := createField(idx, field); err != nil {
|
|
return errors.Wrapf(err, "creating field: %s/%s", tkey, fldName)
|
|
}
|
|
}
|
|
|
|
// Remove fields which don't exist in toT.
|
|
for _, fldName := range sc.removed() {
|
|
if err := api.DeleteField(ctx, string(tkey), string(fldName)); err != nil {
|
|
return errors.Wrapf(err, "deleting field: %s/%s", tkey, fldName)
|
|
}
|
|
}
|
|
|
|
// // Update any field options which have changed for existing fields.
|
|
// for _, fldName := range sc.same() {
|
|
// // handle changed field options??
|
|
// }
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) pushJobsTableKeys(ctx context.Context, jobs chan<- directiveJobType, fromD, toD *dax.Directive) {
|
|
toPartitionsMap := toD.TranslatePartitionsMap()
|
|
|
|
// Get the diff between from/to directive.partitions.
|
|
partComp := newPartitionsComparer(fromD.TranslatePartitionsMap(), toPartitionsMap)
|
|
|
|
// Loop over the partition map and load from WriteLogger.
|
|
for tkey, partitions := range partComp.added() {
|
|
// Get index in order to find the translate stores (by partition) for
|
|
// the table.
|
|
idx := api.holder.Index(string(tkey))
|
|
if idx == nil {
|
|
log.Printf("index not found in holder: %s", tkey)
|
|
continue
|
|
}
|
|
|
|
// Update the cached version of translate partitions that we keep on the
|
|
// Index.
|
|
idx.SetTranslatePartitions(toPartitionsMap[tkey])
|
|
|
|
for _, partition := range partitions {
|
|
jobs <- directiveJobTableKeys{
|
|
idx: idx,
|
|
tkey: tkey,
|
|
partition: partition,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) loadTableKeys(ctx context.Context, idx *Index, tkey dax.TableKey, partition dax.VersionedPartition) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
// Load the previous snapshot. Version 0 doesn't have a snapshot
|
|
// file; it only has log entries.
|
|
if partition.Version > 0 {
|
|
// Load partition snapshot: version - 1
|
|
previousVersion := partition.Version - 1
|
|
rc, err := api.snapshotReadWriter.ReadTableKeys(ctx, qtid, partition.Num, previousVersion)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading table keys snapshot")
|
|
}
|
|
defer rc.Close()
|
|
|
|
if err := api.TranslateIndexDB(ctx, string(tkey), int(partition.Num), rc); err != nil {
|
|
return errors.Wrap(err, "restoring table keys")
|
|
|
|
}
|
|
}
|
|
|
|
if err := func() error {
|
|
store := idx.TranslateStore(int(partition.Num))
|
|
|
|
reader := api.writeLogReader.TableKeyReader(ctx, qtid, partition.Num, partition.Version)
|
|
if err := reader.Open(); err != nil {
|
|
// TODO: this log can be confusing because on a create
|
|
// table, there is no log file yet, so an error is expected.
|
|
// Instead of swallowing this error, we need to check the
|
|
// error code and handle it differently. This means the
|
|
// writelogger will need to return an error indicating that
|
|
// the log file does not exist, but that that is expected.
|
|
// log.Printf("could not open log file for table: %s, partition: %d: version: %d, err: %s", table, partition.Num, partition.Version, err)
|
|
return nil
|
|
}
|
|
defer reader.Close()
|
|
|
|
for msg, err := reader.Read(); err != io.EOF; msg, err = reader.Read() {
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading from log reader")
|
|
}
|
|
for key, id := range msg.StringToID {
|
|
if err := store.ForceSet(id, key); err != nil {
|
|
return errors.Wrapf(err, "forcing set id, key: %d, %s", id, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the table/partition/version in the holder.
|
|
if err := api.holder.versionStore.AddPartitions(ctx, qtid, partition); err != nil {
|
|
return errors.Wrap(err, "adding partition to sharder")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) pushJobsFieldKeys(ctx context.Context, jobs chan<- directiveJobType, fromD, toD *dax.Directive) {
|
|
// Get the diff between from/to directive.fields.
|
|
fieldComp := newFieldsComparer(fromD.TranslateFieldsMap(), toD.TranslateFieldsMap())
|
|
|
|
// Loop over the field map and load from WriteLogger.
|
|
for tkey, fields := range fieldComp.added() {
|
|
for _, field := range fields {
|
|
jobs <- directiveJobFieldKeys{
|
|
tkey: tkey,
|
|
field: field,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) loadFieldKeys(ctx context.Context, tkey dax.TableKey, field dax.VersionedField) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
// Load the previous snapshot. Version 0 doesn't have a snapshot
|
|
// file; it only has log entries.
|
|
if field.Version > 0 {
|
|
// Load field snapshot: version - 1
|
|
previousVersion := field.Version - 1
|
|
rc, err := api.snapshotReadWriter.ReadFieldKeys(ctx, qtid, field.Name, previousVersion)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading field keys snapshot")
|
|
}
|
|
defer rc.Close()
|
|
|
|
if err := api.TranslateFieldDB(ctx, string(tkey), string(field.Name), rc); err != nil {
|
|
return errors.Wrap(err, "restoring field keys")
|
|
}
|
|
}
|
|
|
|
if err := func() error {
|
|
// Get field in order to find the translate store.
|
|
fld := api.holder.Field(string(tkey), string(field.Name))
|
|
if fld == nil {
|
|
log.Printf("field not found in holder: %s", field.Name)
|
|
return nil
|
|
}
|
|
store := fld.TranslateStore()
|
|
|
|
reader := api.writeLogReader.FieldKeyReader(ctx, qtid, field.Name, field.Version)
|
|
if err := reader.Open(); err != nil {
|
|
// TODO: this log can be confusing because on a create
|
|
// table, there is no log file yet, so an error is expected.
|
|
// Instead of swallowing this error, we need to check the
|
|
// error code and handle it differently. This means the
|
|
// writelogger will need to return an error indicating that
|
|
// the log file does not exist, but that that is expected.
|
|
// log.Printf("could not open log file for table: %s, field: %s: version: %d, err: %s", table, field.Name, field.Version, err)
|
|
return nil
|
|
}
|
|
defer reader.Close()
|
|
|
|
for msg, err := reader.Read(); err != io.EOF; msg, err = reader.Read() {
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading from log reader")
|
|
}
|
|
for key, id := range msg.StringToID {
|
|
if err := store.ForceSet(id, key); err != nil {
|
|
return errors.Wrapf(err, "forcing set id, key: %d, %s", id, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the table/field/version in the holder.
|
|
if err := api.holder.versionStore.AddFields(ctx, qtid, field); err != nil {
|
|
return errors.Wrap(err, "adding field to sharder")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) pushJobsShards(ctx context.Context, jobs chan<- directiveJobType, fromD, toD *dax.Directive) {
|
|
// Put shards into a map by table.
|
|
shardMap := toD.ComputeShardsMap()
|
|
|
|
// Get the diff between from/to directive shards.
|
|
shardComp := newShardsComparer(fromD.ComputeShardsMap(), shardMap)
|
|
|
|
// Loop over the shard map and load from WriteLogger.
|
|
for tkey, shards := range shardComp.added() {
|
|
for _, shard := range shards {
|
|
jobs <- directiveJobShards{
|
|
tkey: tkey,
|
|
shard: shard,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) loadShard(ctx context.Context, tkey dax.TableKey, shard dax.VersionedShard) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
partition := disco.ShardToShardPartition(string(tkey), uint64(shard.Num), disco.DefaultPartitionN)
|
|
partitionNum := dax.PartitionNum(partition)
|
|
|
|
// Load the previous snapshot. Version 0 doesn't have a snapshot
|
|
// file; it only has log entries.
|
|
if shard.Version > 0 {
|
|
// Load shard snapshot: version - 1
|
|
previousVersion := shard.Version - 1
|
|
rc, err := api.snapshotReadWriter.ReadShardData(ctx, qtid, partitionNum, shard.Num, previousVersion)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading shard data snapshot")
|
|
}
|
|
|
|
if err := api.RestoreShard(ctx, string(tkey), uint64(shard.Num), rc); err != nil {
|
|
return errors.Wrap(err, "restoring shard data")
|
|
}
|
|
}
|
|
|
|
// WriteLog reader.
|
|
if err := func() error {
|
|
reader := api.writeLogReader.ShardReader(ctx, qtid, partitionNum, shard.Num, shard.Version)
|
|
if err := reader.Open(); err != nil {
|
|
// TODO: this log can be confusing because on a create
|
|
// table, there is no log file yet, so an error is expected.
|
|
// Instead of swallowing this error, we need to check the
|
|
// error code and handle it differently. This means the
|
|
// writelogger will need to return an error indicating that
|
|
// the log file does not exist, but that that is expected.
|
|
// log.Printf("could not open log file for table: %s, partition: %d: version: %d, shard: %d, err: %s", table, partition, shard.Version, shard.Num, err)
|
|
return nil
|
|
}
|
|
defer reader.Close()
|
|
|
|
for logMsg, err := reader.Read(); err != io.EOF; logMsg, err = reader.Read() {
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading from log reader")
|
|
}
|
|
switch msg := logMsg.(type) {
|
|
case *computer.ImportRoaringMessage:
|
|
req := &ImportRoaringRequest{
|
|
Clear: msg.Clear,
|
|
Action: msg.Action,
|
|
Block: msg.Block,
|
|
Views: msg.Views,
|
|
UpdateExistence: msg.UpdateExistence,
|
|
SuppressLog: true,
|
|
}
|
|
if err := api.ImportRoaring(ctx, msg.Table, msg.Field, msg.Shard, true, req); err != nil {
|
|
return errors.Wrapf(err, "import roaring, table: %s, field: %s, shard: %d", msg.Table, msg.Field, msg.Shard)
|
|
}
|
|
|
|
case *computer.ImportMessage:
|
|
req := &ImportRequest{
|
|
Index: msg.Table,
|
|
Field: msg.Field,
|
|
Shard: msg.Shard,
|
|
RowIDs: msg.RowIDs,
|
|
ColumnIDs: msg.ColumnIDs,
|
|
RowKeys: msg.RowKeys,
|
|
ColumnKeys: msg.ColumnKeys,
|
|
Timestamps: msg.Timestamps,
|
|
Clear: msg.Clear,
|
|
}
|
|
|
|
qcx := api.Txf().NewQcx()
|
|
defer qcx.Abort()
|
|
|
|
opts := []ImportOption{
|
|
OptImportOptionsClear(msg.Clear),
|
|
OptImportOptionsIgnoreKeyCheck(msg.IgnoreKeyCheck),
|
|
OptImportOptionsPresorted(msg.Presorted),
|
|
OptImportOptionsSuppressLog(true),
|
|
}
|
|
if err := api.Import(ctx, qcx, req, opts...); err != nil {
|
|
return errors.Wrapf(err, "import, table: %s, field: %s, shard: %d", msg.Table, msg.Field, msg.Shard)
|
|
}
|
|
|
|
case *computer.ImportValueMessage:
|
|
req := &ImportValueRequest{
|
|
Index: msg.Table,
|
|
Field: msg.Field,
|
|
Shard: msg.Shard,
|
|
ColumnIDs: msg.ColumnIDs,
|
|
ColumnKeys: msg.ColumnKeys,
|
|
Values: msg.Values,
|
|
FloatValues: msg.FloatValues,
|
|
TimestampValues: msg.TimestampValues,
|
|
StringValues: msg.StringValues,
|
|
Clear: msg.Clear,
|
|
}
|
|
|
|
qcx := api.Txf().NewQcx()
|
|
defer qcx.Abort()
|
|
|
|
opts := []ImportOption{
|
|
OptImportOptionsClear(msg.Clear),
|
|
OptImportOptionsIgnoreKeyCheck(msg.IgnoreKeyCheck),
|
|
OptImportOptionsPresorted(msg.Presorted),
|
|
OptImportOptionsSuppressLog(true),
|
|
}
|
|
if err := api.ImportValue(ctx, qcx, req, opts...); err != nil {
|
|
return errors.Wrapf(err, "import value, table: %s, field: %s, shard: %d", msg.Table, msg.Field, msg.Shard)
|
|
}
|
|
case *computer.ImportRoaringShardMessage:
|
|
req := &ImportRoaringShardRequest{
|
|
Remote: true,
|
|
Views: make([]RoaringUpdate, len(msg.Views)),
|
|
SuppressLog: true,
|
|
}
|
|
for i, view := range msg.Views {
|
|
req.Views[i] = RoaringUpdate{
|
|
Field: view.Field,
|
|
View: view.View,
|
|
Clear: view.Clear,
|
|
Set: view.Set,
|
|
ClearRecords: view.ClearRecords,
|
|
}
|
|
}
|
|
if err := api.ImportRoaringShard(ctx, msg.Table, msg.Shard, req); err != nil {
|
|
return errors.Wrapf(err, "import roaring shard table: %s, shard: %d", msg.Table, msg.Shard)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the table/shard/version in the holder.
|
|
if err := api.holder.versionStore.AddShards(ctx, qtid, shard); err != nil {
|
|
return errors.Wrap(err, "adding shard to sharder")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
// sliceComparer is used to compare the differences between two slices of comparables.
|
|
type sliceComparer[K comparable] struct {
|
|
from []K
|
|
to []K
|
|
}
|
|
|
|
func newSliceComparer[K comparable](from []K, to []K) *sliceComparer[K] {
|
|
return &sliceComparer[K]{
|
|
from: from,
|
|
to: to,
|
|
}
|
|
}
|
|
|
|
// added returns the items which are present in `to` but not in `from`.
|
|
func (s *sliceComparer[K]) added() []K {
|
|
return thingsAdded(s.from, s.to)
|
|
}
|
|
|
|
// removed returns the items which are present in `from` but not in `to`.
|
|
func (s *sliceComparer[K]) removed() []K {
|
|
return thingsAdded(s.to, s.from)
|
|
}
|
|
|
|
// same returns the items which are in both `to` and `from`.
|
|
func (s *sliceComparer[K]) same() []K {
|
|
var same []K
|
|
for _, fromThing := range s.from {
|
|
for _, toThing := range s.to {
|
|
if fromThing == toThing {
|
|
same = append(same, fromThing)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return same
|
|
}
|
|
|
|
// thingsAdded returns the comparable things which are present in `to` but not
|
|
// in `from`.
|
|
func thingsAdded[K comparable](from []K, to []K) []K {
|
|
var added []K
|
|
for i := range to {
|
|
var found bool
|
|
for j := range from {
|
|
if from[j] == to[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
added = append(added, to[i])
|
|
}
|
|
}
|
|
return added
|
|
}
|
|
|
|
// partitionsComparer is used to compare the differences between two maps of
|
|
// table:[]partition.
|
|
type partitionsComparer struct {
|
|
from map[dax.TableKey]dax.VersionedPartitions
|
|
to map[dax.TableKey]dax.VersionedPartitions
|
|
}
|
|
|
|
func newPartitionsComparer(from map[dax.TableKey]dax.VersionedPartitions, to map[dax.TableKey]dax.VersionedPartitions) *partitionsComparer {
|
|
return &partitionsComparer{
|
|
from: from,
|
|
to: to,
|
|
}
|
|
}
|
|
|
|
// added returns the partitions which are present in `to` but not in `from`. The
|
|
// results remain in the format of a map of table:[]partition.
|
|
func (p *partitionsComparer) added() map[dax.TableKey]dax.VersionedPartitions {
|
|
return partitionsAdded(p.from, p.to)
|
|
}
|
|
|
|
// removed returns the partitions which are present in `from` but not in `to`.
|
|
// The results remain in the format of a map of table:[]partition.
|
|
func (p *partitionsComparer) removed() map[dax.TableKey]dax.VersionedPartitions {
|
|
return partitionsAdded(p.to, p.from)
|
|
}
|
|
|
|
// partitionsAdded returns the partitions which are present in `to` but not in `from`.
|
|
func partitionsAdded(from map[dax.TableKey]dax.VersionedPartitions, to map[dax.TableKey]dax.VersionedPartitions) map[dax.TableKey]dax.VersionedPartitions {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey]dax.VersionedPartitions)
|
|
for tt, tps := range to {
|
|
fps, found := from[tt]
|
|
if !found {
|
|
added[tt] = tps
|
|
continue
|
|
}
|
|
|
|
addedPartitions := dax.VersionedPartitions{}
|
|
for i := range tps {
|
|
var found bool
|
|
for j := range fps {
|
|
if fps[j] == tps[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
addedPartitions = append(addedPartitions, tps[i])
|
|
}
|
|
}
|
|
|
|
if len(addedPartitions) > 0 {
|
|
added[tt] = addedPartitions
|
|
}
|
|
}
|
|
return added
|
|
}
|
|
|
|
// fieldsComparer is used to compare the differences between two maps of
|
|
// table:[]fieldVersion.
|
|
type fieldsComparer struct {
|
|
from map[dax.TableKey]dax.VersionedFields
|
|
to map[dax.TableKey]dax.VersionedFields
|
|
}
|
|
|
|
func newFieldsComparer(from map[dax.TableKey]dax.VersionedFields, to map[dax.TableKey]dax.VersionedFields) *fieldsComparer {
|
|
return &fieldsComparer{
|
|
from: from,
|
|
to: to,
|
|
}
|
|
}
|
|
|
|
// added returns the fields which are present in `to` but not in `from`. The
|
|
// results remain in the format of a map of table:[]field.
|
|
func (f *fieldsComparer) added() map[dax.TableKey]dax.VersionedFields {
|
|
return fieldsAdded(f.from, f.to)
|
|
}
|
|
|
|
// removed returns the fields which are present in `from` but not in `to`.
|
|
// The results remain in the format of a map of table:[]field.
|
|
func (f *fieldsComparer) removed() map[dax.TableKey]dax.VersionedFields {
|
|
return fieldsAdded(f.to, f.from)
|
|
}
|
|
|
|
// fieldsAdded returns the fields which are present in `to` but not in `from`.
|
|
func fieldsAdded(from map[dax.TableKey]dax.VersionedFields, to map[dax.TableKey]dax.VersionedFields) map[dax.TableKey]dax.VersionedFields {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey]dax.VersionedFields)
|
|
for tt, tps := range to {
|
|
fps, found := from[tt]
|
|
if !found {
|
|
added[tt] = tps
|
|
continue
|
|
}
|
|
|
|
addedFieldVersions := dax.VersionedFields{}
|
|
for i := range tps {
|
|
var found bool
|
|
for j := range fps {
|
|
if fps[j] == tps[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
addedFieldVersions = append(addedFieldVersions, tps[i])
|
|
}
|
|
}
|
|
|
|
if len(addedFieldVersions) > 0 {
|
|
added[tt] = addedFieldVersions
|
|
}
|
|
}
|
|
return added
|
|
}
|
|
|
|
// shardsComparer is used to compare the differences between two maps of
|
|
// table:[]shardV.
|
|
type shardsComparer struct {
|
|
from map[dax.TableKey]dax.VersionedShards
|
|
to map[dax.TableKey]dax.VersionedShards
|
|
}
|
|
|
|
func newShardsComparer(from map[dax.TableKey]dax.VersionedShards, to map[dax.TableKey]dax.VersionedShards) *shardsComparer {
|
|
return &shardsComparer{
|
|
from: from,
|
|
to: to,
|
|
}
|
|
}
|
|
|
|
// added returns the shards which are present in `to` but not in `from`. The
|
|
// results remain in the format of a map of table:[]shard.
|
|
func (s *shardsComparer) added() map[dax.TableKey]dax.VersionedShards {
|
|
return shardsAdded(s.from, s.to)
|
|
}
|
|
|
|
// removed returns the shards which are present in `from` but not in `to`. The
|
|
// results remain in the format of a map of table:[]shard.
|
|
func (s *shardsComparer) removed() map[dax.TableKey]dax.VersionedShards {
|
|
return shardsAdded(s.to, s.from)
|
|
}
|
|
|
|
// shardsAdded returns the shards which are present in `to` but not in `from`.
|
|
func shardsAdded(from map[dax.TableKey]dax.VersionedShards, to map[dax.TableKey]dax.VersionedShards) map[dax.TableKey]dax.VersionedShards {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey]dax.VersionedShards)
|
|
for tt, tss := range to {
|
|
fss, found := from[tt]
|
|
if !found {
|
|
added[tt] = tss
|
|
continue
|
|
}
|
|
|
|
addedShards := dax.VersionedShards{}
|
|
for i := range tss {
|
|
var found bool
|
|
for j := range fss {
|
|
if fss[j] == tss[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
addedShards = append(addedShards, tss[i])
|
|
}
|
|
}
|
|
|
|
if len(addedShards) > 0 {
|
|
added[tt] = addedShards
|
|
}
|
|
}
|
|
return added
|
|
}
|
|
|
|
// createTableAndFields creates the FeatureBase Tables and Fields provided in
|
|
// the dax.Directive format.
|
|
func (api *API) createTableAndFields(tbl *dax.QualifiedTable, partitions dax.VersionedPartitions) error {
|
|
cim := &CreateIndexMessage{
|
|
Index: string(tbl.Key()),
|
|
CreatedAt: 0,
|
|
Meta: IndexOptions{
|
|
Keys: tbl.StringKeys(),
|
|
TrackExistence: true,
|
|
},
|
|
}
|
|
|
|
// Create the index in etcd as the system of record.
|
|
if err := api.holder.persistIndex(context.Background(), cim); err != nil {
|
|
return errors.Wrap(err, "persisting index")
|
|
}
|
|
|
|
idx, err := api.holder.createIndexWithPartitions(cim, partitions)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "adding index: %s", tbl.Name)
|
|
}
|
|
|
|
// Add the fields
|
|
for _, fld := range tbl.Fields {
|
|
if fld.IsPrimaryKey() {
|
|
continue
|
|
}
|
|
if err := createField(idx, fld); err != nil {
|
|
return errors.Wrapf(err, "creating field: %s", fld.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createField creates a FeatureBase Field in the provided FeatureBase Index
|
|
// based on the provided field's type.
|
|
//
|
|
// TODO: `time` fields
|
|
func createField(idx *Index, fld *dax.Field) 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),
|
|
)
|
|
case dax.BaseTypeID:
|
|
opts = append(opts,
|
|
OptFieldTypeMutex(cacheType, cacheSize),
|
|
)
|
|
case dax.BaseTypeIDSet:
|
|
opts = append(opts,
|
|
OptFieldTypeSet(cacheType, cacheSize),
|
|
)
|
|
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.BaseTypeTimestamp:
|
|
opts = append(opts,
|
|
OptFieldTypeTimestamp(fld.Options.Epoch, fld.Options.TimeUnit),
|
|
)
|
|
default:
|
|
return errors.Errorf("unsupport field type: %s", fld.Type)
|
|
}
|
|
|
|
if _, err := idx.CreateField(string(fld.Name), "", opts...); err != nil {
|
|
return errors.Wrapf(err, "creating field on index: %s", fld.Name)
|
|
}
|
|
return nil
|
|
}
|