mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* Add kafka support to CLI (fbsql) This commit adds the ability to provide a `--kafka-config` command line argument referncing a toml file to configure kafka. * Move "Molecula Consumer" message to the logger; hide it in basic mode * Fold decimal(scale) into kafka.source-type * Build fbsql with cgo in docker for CI * Re-organize the fbsql kafka config and setup. Allow field config to use the table schema if no fields provided. * Display timestamp fields with format RFC3339Nano * remove kafkaRunner (no longer used) * Fix cli/batch test (and make sure it's not excluded from CI) The logic in our Makefile was exluding from tests any package with `/batch` in the package name. This excluded `/cli/batch`, which is not good. This commit changes the exclusion logic to include the `/v3` portion of the package name, so `/v3/batch`. * Rename Basic() to SetBasic()
215 lines
5.2 KiB
Go
215 lines
5.2 KiB
Go
package batch
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
fbbatch "github.com/featurebasedb/featurebase/v3/batch"
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
"github.com/featurebasedb/featurebase/v3/errors"
|
|
"github.com/featurebasedb/featurebase/v3/pql"
|
|
)
|
|
|
|
// Ensure type implements interface.
|
|
var _ fbbatch.Batcher = (*sqlBatcher)(nil)
|
|
|
|
type sqlBatcher struct {
|
|
inserter Inserter
|
|
fields []*dax.Field
|
|
}
|
|
|
|
func NewSQLBatcher(i Inserter, flds []*dax.Field) *sqlBatcher {
|
|
return &sqlBatcher{
|
|
inserter: i,
|
|
fields: flds,
|
|
}
|
|
}
|
|
|
|
func (b *sqlBatcher) NewBatch(cfg fbbatch.Config, tbl *dax.Table, flds []*dax.Field) (fbbatch.RecordBatch, error) {
|
|
fields := flds
|
|
if b.fields != nil {
|
|
fields = b.fields
|
|
}
|
|
return &sqlBatch{
|
|
table: tbl,
|
|
fields: fields,
|
|
size: cfg.Size,
|
|
maxStaleness: cfg.MaxStaleness,
|
|
ids: make([]interface{}, 0, cfg.Size),
|
|
rows: make([][]interface{}, 0, cfg.Size),
|
|
inserter: b.inserter,
|
|
}, nil
|
|
}
|
|
|
|
// Ensure type implements interface.
|
|
var _ fbbatch.RecordBatch = (*sqlBatch)(nil)
|
|
|
|
type sqlBatch struct {
|
|
table *dax.Table
|
|
fields []*dax.Field
|
|
size int
|
|
|
|
ids []interface{}
|
|
rows [][]interface{}
|
|
|
|
// staleTime tracks the time the first record of the batch was inserted
|
|
// plus the maxStaleness, in order to raise ErrBatchNowStale if the
|
|
// maxStaleness has elapsed
|
|
staleTime time.Time
|
|
maxStaleness time.Duration
|
|
|
|
// inserter handles SQL INSERT statements generated for each batch.
|
|
inserter Inserter
|
|
}
|
|
|
|
func (b *sqlBatch) Add(rec fbbatch.Row) error {
|
|
// Clear rec.Values and rec.Clears upon return.
|
|
defer func() {
|
|
for i := range rec.Values {
|
|
rec.Values[i] = nil
|
|
}
|
|
for k := range rec.Clears {
|
|
delete(rec.Clears, k)
|
|
}
|
|
}()
|
|
|
|
if len(b.ids) == cap(b.ids) {
|
|
return fbbatch.ErrBatchAlreadyFull
|
|
}
|
|
if len(rec.Values) != len(b.fields) {
|
|
return errors.Errorf("record needs to match up with batch fields, got %d fields and %d record", len(b.fields), len(rec.Values))
|
|
}
|
|
|
|
// Append the ID to b.ids.
|
|
b.ids = append(b.ids, rec.ID)
|
|
|
|
// Convert decimal fields (which come in as int64, along with the scale in
|
|
// field) to pql.Decimal.
|
|
for i, fld := range b.fields {
|
|
switch b.fields[i].Type {
|
|
case dax.BaseTypeDecimal:
|
|
if val, ok := rec.Values[i].(int64); ok {
|
|
rec.Values[i] = pql.NewDecimal(val, fld.Options.Scale)
|
|
}
|
|
case dax.BaseTypeTimestamp:
|
|
if val, ok := rec.Values[i].(int64); ok {
|
|
ts := time.Unix(val, 0)
|
|
rec.Values[i] = ts.Format(time.RFC3339)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Append the record to b.rows.
|
|
vals := make([]interface{}, 0, len(rec.Values))
|
|
vals = append(vals, rec.Values...)
|
|
b.rows = append(b.rows, vals)
|
|
|
|
// Check for batch full or stale.
|
|
if len(b.ids) == cap(b.ids) {
|
|
return fbbatch.ErrBatchNowFull
|
|
}
|
|
if b.maxStaleness != time.Duration(0) { // set maxStaleness to 0 to disable staleness checking
|
|
if len(b.ids) == 1 {
|
|
b.staleTime = time.Now().Add(b.maxStaleness)
|
|
} else if time.Now().After(b.staleTime) {
|
|
return fbbatch.ErrBatchNowStale
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *sqlBatch) Import() error {
|
|
if len(b.rows) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Construct the BULK INSERT statement based on the table and fields.
|
|
sql, err := buildBulkInsert(b.table, b.fields, b.ids, b.rows)
|
|
if err != nil {
|
|
return errors.Wrap(err, "building bulk insert statement")
|
|
}
|
|
|
|
// Reset batch data.
|
|
b.reset()
|
|
|
|
// Submit the SQL statement.
|
|
return b.inserter.Insert(sql)
|
|
}
|
|
|
|
func (b *sqlBatch) reset() {
|
|
b.ids = b.ids[:0]
|
|
b.rows = b.rows[:0]
|
|
}
|
|
|
|
func (b *sqlBatch) Len() int {
|
|
return len(b.rows)
|
|
}
|
|
|
|
func (b *sqlBatch) Flush() error {
|
|
return nil
|
|
}
|
|
|
|
func buildBulkInsert(tbl *dax.Table, fields []*dax.Field, ids []interface{}, rows [][]interface{}) (string, error) {
|
|
// Validation.
|
|
if tbl.Name == "" {
|
|
return "", errors.New(errors.ErrUncoded, "table name is required")
|
|
} else if len(fields) == 0 {
|
|
return "", errors.New(errors.ErrUncoded, "at least one field is required")
|
|
}
|
|
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(`BULK INSERT INTO `)
|
|
sb.WriteString(string(tbl.Name))
|
|
sb.WriteString(` (_id,`)
|
|
|
|
flds := make([]string, 0, len(fields))
|
|
maps := make([]string, 0, len(fields))
|
|
for i := range fields {
|
|
flds = append(flds, string(fields[i].Name))
|
|
maps = append(maps, fmt.Sprintf("'$.col_%d' %s", i, fields[i].FullType()))
|
|
}
|
|
// Fields
|
|
sb.WriteString(strings.Join(flds, ","))
|
|
|
|
// MAP
|
|
keyType := dax.BaseTypeID
|
|
if tbl.StringKeys() {
|
|
keyType = dax.BaseTypeString
|
|
}
|
|
sb.WriteString(`) MAP ('$._id' `)
|
|
sb.WriteString(keyType)
|
|
sb.WriteString(`,`)
|
|
sb.WriteString(strings.Join(maps, ","))
|
|
sb.WriteString(`) FROM x'`)
|
|
|
|
// Row values.
|
|
|
|
// m is a map representing a single row to be marshalled and added to the
|
|
// bulk insert as one line in the NDJSON payload. We re-use the map for each
|
|
// row.
|
|
m := make(map[string]interface{})
|
|
for i := range rows {
|
|
// Write the ID value.
|
|
m[string(dax.PrimaryKeyFieldName)] = ids[i]
|
|
// Write the rest of the data values.
|
|
for col := range rows[i] {
|
|
m[fmt.Sprintf("col_%d", col)] = rows[i][col]
|
|
}
|
|
|
|
// Marshal the map to json and add to the sql statement.
|
|
if j, err := json.Marshal(m); err != nil {
|
|
return "", errors.Wrap(err, "marshalling row to json")
|
|
} else {
|
|
sb.Write(j)
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
// WITH
|
|
sb.WriteString(fmt.Sprintf(`' WITH BATCHSIZE %d FORMAT 'NDJSON' INPUT 'STREAM'`, len(rows)))
|
|
|
|
return sb.String(), nil
|
|
}
|