mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
500 lines
13 KiB
Go
500 lines
13 KiB
Go
// Copyright 2020 Pilosa Corp.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package sql
|
|
|
|
import (
|
|
"io"
|
|
"sort"
|
|
|
|
"github.com/pilosa/pilosa/v2/pql"
|
|
pproto "github.com/pilosa/pilosa/v2/proto"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc/codes"
|
|
)
|
|
|
|
// DataType contants describe the possible values
|
|
// for the Datatype value in the RowResponse header.
|
|
const (
|
|
DataTypeDecimal = "decimal"
|
|
DataTypeFloat64 = "float64"
|
|
DataTypeInt64 = "int64"
|
|
DataTypeString = "string"
|
|
DataTypeUint64Array = "[]uint64"
|
|
)
|
|
|
|
type Reducer interface {
|
|
Reduce(pproto.StreamClient, pproto.StreamServer) error
|
|
}
|
|
|
|
// LimitReducer limits the number of messages passed through.
|
|
type LimitReducer struct {
|
|
limit uint
|
|
offset uint
|
|
}
|
|
|
|
// NewLimitReducer returns a new instance of LimitReducer.
|
|
func NewLimitReducer(limit, offset uint) *LimitReducer {
|
|
return &LimitReducer{
|
|
limit: limit,
|
|
offset: offset,
|
|
}
|
|
}
|
|
|
|
// Reduce applies the limit reducer to the client stream and sends the results
|
|
// to the server stream.
|
|
func (l *LimitReducer) Reduce(c pproto.StreamClient, s pproto.StreamServer) error {
|
|
offsetCountdown := l.offset
|
|
|
|
// in the case of an offset, since we'll be skipping the first record
|
|
// which contains the headers, we need to pull the headers, save them,
|
|
// and apply them to the first record that we actually send through.
|
|
var headers []*pproto.ColumnInfo
|
|
|
|
for i := uint(0); i < l.limit+l.offset || l.limit == 0; i++ {
|
|
r, err := c.Recv()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return s.Send(pproto.ErrorWrap(err, "receiving on client stream"))
|
|
}
|
|
if offsetCountdown > 0 {
|
|
if headers == nil {
|
|
headers = r.Headers
|
|
}
|
|
offsetCountdown--
|
|
continue
|
|
}
|
|
if headers != nil {
|
|
r.Headers = headers
|
|
headers = nil
|
|
}
|
|
if err := s.Send(r); err != nil {
|
|
return s.Send(pproto.ErrorWrap(err, "sending on server stream"))
|
|
}
|
|
}
|
|
return s.Send(pproto.EOF)
|
|
}
|
|
|
|
// OrderByReducer orders the results based on the provide conditions.
|
|
// It also takes limit and offset to reduce the amount of items
|
|
// needing to be held in memory for sorting.
|
|
type OrderByReducer struct {
|
|
fields []string
|
|
isDescending []bool // direction[asc: false, desc: true]
|
|
limit uint
|
|
offset uint
|
|
}
|
|
|
|
// NewOrderByReducer returns a new instance of OrderByReducer.
|
|
func NewOrderByReducer(fields, dirs []string, limit, offset uint) *OrderByReducer {
|
|
descendings := make([]bool, len(fields))
|
|
for i := range dirs {
|
|
if dirs[i] == "desc" {
|
|
descendings[i] = true
|
|
}
|
|
}
|
|
return &OrderByReducer{
|
|
fields: fields,
|
|
isDescending: descendings,
|
|
limit: limit,
|
|
offset: offset,
|
|
}
|
|
}
|
|
|
|
// Reduce applies the order by reducer to the client stream and sends the results
|
|
// to the server stream.
|
|
func (o *OrderByReducer) Reduce(c pproto.StreamClient, s pproto.StreamServer) error {
|
|
// hold is a slice of row responses, to be sent to the output
|
|
// stream sorted by the sort conditions.
|
|
var hold []*pproto.RowResponse
|
|
|
|
// sortColNames contains the names of the columns to
|
|
// sort on.
|
|
sortColNames := o.fields
|
|
|
|
// sortColIdxs contains the positions of the sort columns
|
|
// in the result set.
|
|
sortColIdxs := make([]int, len(sortColNames))
|
|
|
|
// sortColTypes contains the data types of the columns
|
|
// to be sorted. (ex: "uint64", "string", etc.). This
|
|
// is used to determine how to convert it to a typed
|
|
// field for sorting.
|
|
sortColTypes := make([]string, len(sortColNames))
|
|
|
|
// holdHeaders is used to stash the headers (from
|
|
// the first row) so they can be applied later
|
|
// to what will eventually be the first row after
|
|
// sorting has occurred.
|
|
var holdHeaders []*pproto.ColumnInfo
|
|
|
|
ii := 0
|
|
for {
|
|
rr, err := c.Recv()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return s.Send(pproto.ErrorWrap(err, "receiving row response"))
|
|
}
|
|
|
|
// On the first row, get the sort column information
|
|
// from the headers. Also, stash the headers for
|
|
// later in the `holdHeaders` var.
|
|
if ii == 0 {
|
|
holdHeaders = rr.Headers
|
|
for i, rrHdr := range rr.Headers {
|
|
hdrName := rrHdr.GetName()
|
|
hdrType := rrHdr.GetDatatype()
|
|
for j := range sortColNames {
|
|
if sortColNames[j] == hdrName {
|
|
sortColIdxs[j] = i
|
|
sortColTypes[j] = hdrType
|
|
}
|
|
}
|
|
}
|
|
// Clear the headers in case this record is
|
|
// no longer first (we re-apply the headers
|
|
// to the first outgoing record later).
|
|
rr.Headers = nil
|
|
}
|
|
|
|
// Put each row in the hold.
|
|
hold = append(hold, rr)
|
|
|
|
ii++
|
|
|
|
// TODO: in the case where limit is provided and the number of possible
|
|
// rows is large, it might be more efficient to periodically sort/trim
|
|
// the hold so it doesn't become too large. For example, it could
|
|
// be constrained to size (limit + offset + buffer), where buffer is
|
|
// an amount that the hold can grow before being trimmed.
|
|
}
|
|
|
|
// Sort the hold.
|
|
sorter, err := pproto.NewRowResponseSorter(
|
|
sortColIdxs,
|
|
o.isDescending,
|
|
sortColTypes,
|
|
hold,
|
|
)
|
|
if err != nil {
|
|
return s.Send(pproto.ErrorWrap(err, "creating row response sorter"))
|
|
}
|
|
sort.Sort(sorter)
|
|
|
|
var rowsToConsider uint = uint(len(hold))
|
|
var offsetCountdown uint
|
|
if o.limit > 0 {
|
|
offsetCountdown = o.offset
|
|
if o.limit+o.offset < rowsToConsider {
|
|
rowsToConsider = o.limit + o.offset
|
|
}
|
|
}
|
|
|
|
// Loop over hold and send each row response.
|
|
// Apply the header to the first row that is sent.
|
|
var headerApplied bool
|
|
for i := uint(0); i < rowsToConsider; i++ {
|
|
if offsetCountdown > 0 {
|
|
offsetCountdown--
|
|
continue
|
|
}
|
|
// Re-apply the headers to the first record.
|
|
if !headerApplied {
|
|
hold[i].Headers = holdHeaders
|
|
headerApplied = true
|
|
}
|
|
err := s.Send(hold[i])
|
|
if err != nil {
|
|
return s.Send(pproto.ErrorWrap(err, "sending hold row"))
|
|
}
|
|
}
|
|
return s.Send(pproto.EOF)
|
|
}
|
|
|
|
// ValCountFuncReducer converts a ValCount result to the proper
|
|
// result for Func.
|
|
type ValCountFuncReducer struct {
|
|
fn FuncName
|
|
}
|
|
|
|
// NewValCountFuncReducer returns a new instance of ValCountFuncReducer.
|
|
func NewValCountFuncReducer(fn FuncName) *ValCountFuncReducer {
|
|
return &ValCountFuncReducer{
|
|
fn: fn,
|
|
}
|
|
}
|
|
|
|
// Reduce modifies the stream according to the function.
|
|
func (v *ValCountFuncReducer) Reduce(c pproto.StreamClient, s pproto.StreamServer) error {
|
|
r, err := c.Recv()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return s.Send(pproto.EOF)
|
|
}
|
|
return s.Send(pproto.Error(err))
|
|
}
|
|
|
|
// Get the index of the column with header of "value".
|
|
var idxVal int = -1
|
|
var idxCnt int = -1
|
|
headers := r.GetHeaders()
|
|
for i, hdr := range headers {
|
|
switch hdr.GetName() {
|
|
case "value":
|
|
idxVal = i
|
|
case "count":
|
|
idxCnt = i
|
|
}
|
|
}
|
|
|
|
var sourceDataType string
|
|
var returnDataType string
|
|
|
|
sourceDataType = headers[idxVal].GetDatatype()
|
|
returnDataType = sourceDataType
|
|
switch v.fn {
|
|
case FuncAvg:
|
|
returnDataType = DataTypeFloat64
|
|
}
|
|
|
|
rr := pproto.RowResponse{
|
|
Headers: []*pproto.ColumnInfo{
|
|
{Name: string(v.fn), Datatype: returnDataType},
|
|
},
|
|
Columns: make([]*pproto.ColumnResponse, 1),
|
|
}
|
|
|
|
cols := r.GetColumns()
|
|
if len(cols) == 0 {
|
|
return s.Send(pproto.ErrorCode(
|
|
errors.New("empty column set"),
|
|
codes.Unknown,
|
|
))
|
|
}
|
|
|
|
if idxVal == -1 {
|
|
return s.Send(pproto.ErrorCode(
|
|
errors.New("result set has no column: value"),
|
|
codes.Unknown,
|
|
))
|
|
}
|
|
if idxCnt == -1 {
|
|
return s.Send(pproto.ErrorCode(
|
|
errors.New("result set has no column: count"),
|
|
codes.Unknown,
|
|
))
|
|
}
|
|
|
|
switch v.fn {
|
|
case FuncAvg:
|
|
var avg float64
|
|
if sourceDataType == DataTypeDecimal {
|
|
val := cols[idxVal].GetDecimalVal()
|
|
dec := pql.NewDecimal(val.Value, val.Scale)
|
|
cnt := cols[idxCnt].GetInt64Val()
|
|
avg = dec.Float64() / float64(cnt)
|
|
} else {
|
|
val := cols[idxVal].GetInt64Val()
|
|
cnt := cols[idxCnt].GetInt64Val()
|
|
avg = float64(val) / float64(cnt)
|
|
}
|
|
rr.Columns[0] = &pproto.ColumnResponse{ColumnVal: &pproto.ColumnResponse_Float64Val{Float64Val: avg}}
|
|
default:
|
|
if sourceDataType == DataTypeDecimal {
|
|
val := cols[idxVal].GetDecimalVal()
|
|
rr.Columns[0] = &pproto.ColumnResponse{ColumnVal: &pproto.ColumnResponse_DecimalVal{DecimalVal: &pproto.Decimal{Value: val.Value, Scale: val.Scale}}}
|
|
} else {
|
|
val := cols[idxVal].GetInt64Val()
|
|
rr.Columns[0] = &pproto.ColumnResponse{ColumnVal: &pproto.ColumnResponse_Int64Val{Int64Val: val}}
|
|
}
|
|
}
|
|
|
|
if err := s.Send(&rr); err != nil {
|
|
return errors.Wrap(err, "sending row response")
|
|
}
|
|
return s.Send(pproto.EOF)
|
|
}
|
|
|
|
// CountIDReducer returns a stream of _id's as a count.
|
|
type CountIDReducer struct{}
|
|
|
|
// Reduce counts the stream of IDs and returns a single record.
|
|
func (r *CountIDReducer) Reduce(c pproto.StreamClient, s pproto.StreamServer) error {
|
|
var cnt uint64
|
|
|
|
for {
|
|
_, err := c.Recv()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return s.Send(pproto.ErrorWrap(err, "receiving on client stream"))
|
|
}
|
|
cnt++
|
|
}
|
|
|
|
rr := pproto.RowResponse{
|
|
Headers: []*pproto.ColumnInfo{
|
|
{Name: string(FuncCount), Datatype: "uint64"},
|
|
},
|
|
Columns: []*pproto.ColumnResponse{
|
|
&pproto.ColumnResponse{ColumnVal: &pproto.ColumnResponse_Uint64Val{Uint64Val: cnt}},
|
|
},
|
|
}
|
|
|
|
if err := s.Send(&rr); err != nil {
|
|
return errors.Wrap(err, "sending row response")
|
|
}
|
|
return s.Send(pproto.EOF)
|
|
}
|
|
|
|
// AssignHeadersReducer overwrites the headers on the first record
|
|
// according to field names and aliases from sql. It also reorders
|
|
// the columns in the result stream to match the sql select clause.
|
|
type AssignHeadersReducer struct {
|
|
cols []Column
|
|
}
|
|
|
|
// NewAssignHeadersReducer returns a new instance of AssignHeadersReducer.
|
|
func NewAssignHeadersReducer(cols []Column) *AssignHeadersReducer {
|
|
return &AssignHeadersReducer{
|
|
cols: cols,
|
|
}
|
|
}
|
|
|
|
// Reduce modifies the stream.
|
|
func (r *AssignHeadersReducer) Reduce(c pproto.StreamClient, s pproto.StreamServer) error {
|
|
var placement []uint
|
|
var labels []string
|
|
|
|
var cnt int
|
|
for {
|
|
rr, err := c.Recv()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return s.Send(pproto.ErrorWrap(err, "receiving on client stream"))
|
|
}
|
|
|
|
// If the placement slice is [0-n] where n == len(Headers)
|
|
// then we don't need to alter rr on records after cnt == 0.
|
|
// If we don't apply aliases, we don't have to alter Headers
|
|
// either, but that may not be worth messing with.
|
|
|
|
if cnt == 0 {
|
|
placement, labels, err = headerAssignment(r.cols, rr.Headers)
|
|
if err != nil {
|
|
return s.Send(pproto.ErrorWrap(err, "getting header assignment"))
|
|
}
|
|
|
|
// mod is the modified RowResponse object that gets populated
|
|
// according to placement and labels, then sent.
|
|
mod := &pproto.RowResponse{
|
|
Headers: make([]*pproto.ColumnInfo, len(placement)),
|
|
Columns: make([]*pproto.ColumnResponse, len(placement)),
|
|
}
|
|
|
|
// For now, we assume that the column count in each RowResponse
|
|
// is consistent (i.e. we can validate one time, here, on the
|
|
// first row, and not every time, in the `else` statement below).
|
|
if len(placement) > len(rr.Columns) {
|
|
return s.Send(pproto.ErrorCode(
|
|
errors.New("mismatched header placement and column count"),
|
|
codes.Unknown,
|
|
))
|
|
}
|
|
|
|
for i := 0; i < len(placement); i++ {
|
|
mod.Headers[i] = rr.Headers[placement[i]]
|
|
mod.Headers[i].Name = labels[i]
|
|
mod.Columns[i] = rr.Columns[placement[i]]
|
|
}
|
|
if err := s.Send(mod); err != nil {
|
|
return errors.Wrap(err, "sending mod")
|
|
}
|
|
} else {
|
|
// mod is the modified RowResponse object that gets populated
|
|
// according to placement and labels, then sent.
|
|
mod := &pproto.RowResponse{
|
|
Columns: make([]*pproto.ColumnResponse, len(placement)),
|
|
}
|
|
for i := 0; i < len(placement); i++ {
|
|
mod.Columns[i] = rr.Columns[placement[i]]
|
|
}
|
|
if err := s.Send(mod); err != nil {
|
|
return errors.Wrap(err, "sending mod")
|
|
}
|
|
}
|
|
cnt++
|
|
}
|
|
|
|
return s.Send(pproto.EOF)
|
|
}
|
|
|
|
var (
|
|
ErrIncompleteHeaders = errors.New("incomplete header assignment")
|
|
ErrFieldNotInHeaders = errors.New("field not found in source header")
|
|
)
|
|
|
|
func headerAssignment(cols []Column, hdrs []*pproto.ColumnInfo) ([]uint, []string, error) {
|
|
// If any of the columns are "*" (i.e. type StarColumn),
|
|
// then ignore everything else and just use all result
|
|
// headers.
|
|
var hasStar bool
|
|
for _, col := range cols {
|
|
if _, ok := col.(*StarColumn); ok {
|
|
hasStar = true
|
|
break
|
|
}
|
|
}
|
|
if hasStar {
|
|
placement := make([]uint, len(hdrs))
|
|
labels := make([]string, len(hdrs))
|
|
for i, hdr := range hdrs {
|
|
placement[i] = uint(i)
|
|
labels[i] = hdr.Name
|
|
}
|
|
return placement, labels, nil
|
|
}
|
|
|
|
if len(cols) > len(hdrs) {
|
|
return nil, nil, ErrIncompleteHeaders
|
|
}
|
|
placement := make([]uint, len(cols))
|
|
labels := make([]string, len(cols))
|
|
|
|
// Make a map of the RowResponse headers.
|
|
hdrMap := make(map[string]uint)
|
|
for i, hdr := range hdrs {
|
|
hdrMap[hdr.Name] = uint(i)
|
|
}
|
|
|
|
// Lookup each column in the hdrMap and determine the desired placement.
|
|
for i, col := range cols {
|
|
if srcHdrIdx, ok := hdrMap[col.Source()]; ok {
|
|
placement[i] = srcHdrIdx
|
|
labels[i] = col.Alias()
|
|
} else if nameHdrIdx, ok := hdrMap[col.Name()]; ok {
|
|
placement[i] = nameHdrIdx
|
|
labels[i] = col.Alias()
|
|
} else {
|
|
return nil, nil, errors.Wrapf(ErrFieldNotInHeaders, "field: %s", col.Name())
|
|
}
|
|
}
|
|
return placement, labels, nil
|
|
}
|