mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
porting sqlmapper from vdsm
This commit is contained in:
parent
039780a85e
commit
2b34976c22
19 changed files with 5134 additions and 61 deletions
123
executor.go
123
executor.go
|
|
@ -2820,6 +2820,119 @@ type ExtractedTable struct {
|
|||
Columns []ExtractedTableColumn `json:"columns"`
|
||||
}
|
||||
|
||||
// ToRows implements the ToRowser interface.
|
||||
func (t ExtractedTable) ToRows(callback func(*pb.RowResponse) error) error {
|
||||
if len(t.Columns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
headers := make([]*pb.ColumnInfo, len(t.Fields)+1)
|
||||
colType := "uint64"
|
||||
if t.Columns[0].Column.Keyed {
|
||||
colType = "string"
|
||||
}
|
||||
headers[0] = &pb.ColumnInfo{
|
||||
Name: "_id",
|
||||
Datatype: colType,
|
||||
}
|
||||
dataHeaders := headers[1:]
|
||||
for i, f := range t.Fields {
|
||||
dataHeaders[i] = &pb.ColumnInfo{
|
||||
Name: f.Name,
|
||||
Datatype: f.Type,
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range t.Columns {
|
||||
cols := make([]*pb.ColumnResponse, len(c.Rows)+1)
|
||||
if c.Column.Keyed {
|
||||
cols[0] = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_StringVal{
|
||||
StringVal: c.Column.Key,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
cols[0] = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_Uint64Val{
|
||||
Uint64Val: c.Column.ID,
|
||||
},
|
||||
}
|
||||
}
|
||||
valCols := cols[1:]
|
||||
for i, r := range c.Rows {
|
||||
var col *pb.ColumnResponse
|
||||
switch r := r.(type) {
|
||||
case bool:
|
||||
col = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_BoolVal{
|
||||
BoolVal: r,
|
||||
},
|
||||
}
|
||||
case int64:
|
||||
col = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_Int64Val{
|
||||
Int64Val: r,
|
||||
},
|
||||
}
|
||||
case uint64:
|
||||
col = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_Uint64Val{
|
||||
Uint64Val: r,
|
||||
},
|
||||
}
|
||||
case string:
|
||||
col = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_StringVal{
|
||||
StringVal: r,
|
||||
},
|
||||
}
|
||||
case []uint64:
|
||||
col = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_Uint64ArrayVal{
|
||||
Uint64ArrayVal: &pb.Uint64Array{
|
||||
Vals: r,
|
||||
},
|
||||
},
|
||||
}
|
||||
case []string:
|
||||
col = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_StringArrayVal{
|
||||
StringArrayVal: &pb.StringArray{
|
||||
Vals: r,
|
||||
},
|
||||
},
|
||||
}
|
||||
case pql.Decimal:
|
||||
col = &pb.ColumnResponse{
|
||||
ColumnVal: &pb.ColumnResponse_DecimalVal{
|
||||
DecimalVal: &pb.Decimal{
|
||||
Value: r.Value,
|
||||
Scale: r.Scale,
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("unsupported field value: %v (type: %T)", r, r)
|
||||
}
|
||||
valCols[i] = col
|
||||
}
|
||||
err := callback(&pb.RowResponse{
|
||||
Headers: headers,
|
||||
Columns: cols,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToTable converts the table to protobuf format.
|
||||
func (t ExtractedTable) ToTable() (*pb.TableResponse, error) {
|
||||
return pb.RowsToTable(t, len(t.Columns))
|
||||
}
|
||||
|
||||
type ExtractedIDColumn struct {
|
||||
ColumnID uint64
|
||||
Rows [][]uint64
|
||||
|
|
@ -5019,15 +5132,17 @@ func (e *executor) translateResult(ctx context.Context, index string, idx *Index
|
|||
return nil, ErrFieldNotFound
|
||||
}
|
||||
|
||||
typ := field.Type()
|
||||
|
||||
datatype, err := field.Datatype()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "field %s", v)
|
||||
}
|
||||
fields[i] = ExtractedTableField{
|
||||
Name: v,
|
||||
Type: typ,
|
||||
Type: datatype,
|
||||
}
|
||||
|
||||
var mapper fieldMapper
|
||||
switch typ {
|
||||
switch typ := field.Type(); typ {
|
||||
case FieldTypeBool:
|
||||
mapper = func(ids []uint64) (interface{}, error) {
|
||||
switch len(ids) {
|
||||
|
|
|
|||
|
|
@ -4344,7 +4344,11 @@ func TestExecutor_Execute_Extract(t *testing.T) {
|
|||
c := test.MustRunCluster(t, 3)
|
||||
defer c.Close()
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "set")
|
||||
set := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "set")
|
||||
dtSet, err := set.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.ImportBits(t, "i", "set", [][2]uint64{
|
||||
{0, 1},
|
||||
{0, 2},
|
||||
|
|
@ -4355,56 +4359,88 @@ func TestExecutor_Execute_Extract(t *testing.T) {
|
|||
})
|
||||
c.Query(t, "i", fmt.Sprintf("Clear(%d, set=5)", ShardWidth))
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "keyset", pilosa.OptFieldKeys())
|
||||
keyset := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "keyset", pilosa.OptFieldKeys())
|
||||
dtKeyset, err := keyset.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Query(t, "i", `
|
||||
Set(0, keyset="h")
|
||||
Set(1, keyset="xyzzy")
|
||||
Set(0, keyset="plugh")
|
||||
`)
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "mutex", pilosa.OptFieldTypeMutex(pilosa.CacheTypeRanked, 5000))
|
||||
mutex := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "mutex", pilosa.OptFieldTypeMutex(pilosa.CacheTypeRanked, 5000))
|
||||
dtMutex, err := mutex.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.ImportBits(t, "i", "mutex", [][2]uint64{
|
||||
{0, 1},
|
||||
{0, 2},
|
||||
{4, 4 * ShardWidth},
|
||||
})
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "keymutex", pilosa.OptFieldKeys(), pilosa.OptFieldTypeMutex(pilosa.CacheTypeRanked, 5000))
|
||||
keymutex := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "keymutex", pilosa.OptFieldKeys(), pilosa.OptFieldTypeMutex(pilosa.CacheTypeRanked, 5000))
|
||||
dtKeyMutex, err := keymutex.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Query(t, "i", `
|
||||
Set(0, keymutex="h")
|
||||
Set(1, keymutex="xyzzy")
|
||||
Set(3, keymutex="plugh")
|
||||
`)
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "time", pilosa.OptFieldTypeTime("YMDH"))
|
||||
tm := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "time", pilosa.OptFieldTypeTime("YMDH"))
|
||||
dtTm, err := tm.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Query(t, "i", `
|
||||
Set(0, time=1, 2016-01-01T00:00)
|
||||
Set(1, time=2, 2017-01-01T00:00)
|
||||
Set(3, time=3, 2018-01-01T00:00)
|
||||
`)
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "keytime", pilosa.OptFieldKeys(), pilosa.OptFieldTypeTime("YMDH"))
|
||||
keytm := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "keytime", pilosa.OptFieldKeys(), pilosa.OptFieldTypeTime("YMDH"))
|
||||
dtKeyTm, err := keytm.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Query(t, "i", `
|
||||
Set(0, keytime="h", 2016-01-01T00:00)
|
||||
Set(1, keytime="xyzzy", 2017-01-01T00:00)
|
||||
Set(0, keytime="plugh", 2018-01-01T00:00)
|
||||
`)
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "bsint", pilosa.OptFieldTypeInt(-100, 100))
|
||||
bsiInt := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "bsint", pilosa.OptFieldTypeInt(-100, 100))
|
||||
dtBsiInt, err := bsiInt.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Query(t, "i", `
|
||||
Set(0, bsint=1)
|
||||
Set(1, bsint=-1)
|
||||
Set(3, bsint=2)
|
||||
`)
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "bsidecimal", pilosa.OptFieldTypeDecimal(2))
|
||||
bsidecimal := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "bsidecimal", pilosa.OptFieldTypeDecimal(2))
|
||||
dtBsiDecimal, err := bsidecimal.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Query(t, "i", `
|
||||
Set(0, bsidecimal=0.01)
|
||||
Set(1, bsidecimal=1.00)
|
||||
Set(3, bsidecimal=-1.01)
|
||||
`)
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "bool", pilosa.OptFieldTypeBool())
|
||||
boolean := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true}, "bool", pilosa.OptFieldTypeBool())
|
||||
dtBoolean, err := boolean.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Query(t, "i", `
|
||||
Set(0, bool=true)
|
||||
Set(1, bool=false)
|
||||
|
|
@ -4417,39 +4453,39 @@ func TestExecutor_Execute_Extract(t *testing.T) {
|
|||
Fields: []pilosa.ExtractedTableField{
|
||||
{
|
||||
Name: "set",
|
||||
Type: pilosa.FieldTypeSet,
|
||||
Type: dtSet,
|
||||
},
|
||||
{
|
||||
Name: "keyset",
|
||||
Type: pilosa.FieldTypeSet,
|
||||
Type: dtKeyset,
|
||||
},
|
||||
{
|
||||
Name: "mutex",
|
||||
Type: pilosa.FieldTypeMutex,
|
||||
Type: dtMutex,
|
||||
},
|
||||
{
|
||||
Name: "keymutex",
|
||||
Type: pilosa.FieldTypeMutex,
|
||||
Type: dtKeyMutex,
|
||||
},
|
||||
{
|
||||
Name: "time",
|
||||
Type: pilosa.FieldTypeTime,
|
||||
Type: dtTm,
|
||||
},
|
||||
{
|
||||
Name: "keytime",
|
||||
Type: pilosa.FieldTypeTime,
|
||||
Type: dtKeyTm,
|
||||
},
|
||||
{
|
||||
Name: "bsint",
|
||||
Type: pilosa.FieldTypeInt,
|
||||
Type: dtBsiInt,
|
||||
},
|
||||
{
|
||||
Name: "bsidecimal",
|
||||
Type: pilosa.FieldTypeDecimal,
|
||||
Type: dtBsiDecimal,
|
||||
},
|
||||
{
|
||||
Name: "bool",
|
||||
Type: pilosa.FieldTypeBool,
|
||||
Type: dtBoolean,
|
||||
},
|
||||
},
|
||||
Columns: []pilosa.ExtractedTableColumn{
|
||||
|
|
@ -4574,7 +4610,11 @@ func TestExecutor_Execute_Extract_Keyed(t *testing.T) {
|
|||
c := test.MustRunCluster(t, 3)
|
||||
defer c.Close()
|
||||
|
||||
c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true, Keys: true}, "set")
|
||||
set := c.CreateField(t, "i", pilosa.IndexOptions{TrackExistence: true, Keys: true}, "set")
|
||||
dtSet, err := set.Datatype()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Query(t, "i", `
|
||||
Set("h", set=1)
|
||||
Set("h", set=2)
|
||||
|
|
@ -4589,7 +4629,7 @@ func TestExecutor_Execute_Extract_Keyed(t *testing.T) {
|
|||
Fields: []pilosa.ExtractedTableField{
|
||||
{
|
||||
Name: "set",
|
||||
Type: "set",
|
||||
Type: dtSet,
|
||||
},
|
||||
},
|
||||
Columns: []pilosa.ExtractedTableColumn{
|
||||
|
|
|
|||
30
field.go
30
field.go
|
|
@ -1321,6 +1321,36 @@ func (f *Field) ClearBit(tx Tx, rowID, colID uint64) (changed bool, err error) {
|
|||
return changed, nil
|
||||
}
|
||||
|
||||
// Datatype returns a useful data type (string,
|
||||
// uint64, bool, etc.) based on the field type.
|
||||
func (f *Field) Datatype() (string, error) {
|
||||
switch t := f.Type(); t {
|
||||
case "set":
|
||||
if f.Keys() {
|
||||
return "[]string", nil
|
||||
}
|
||||
return "[]uint64", nil
|
||||
case "mutex":
|
||||
if f.Keys() {
|
||||
return "string", nil
|
||||
}
|
||||
return "uint64", nil
|
||||
case "int":
|
||||
if f.Keys() {
|
||||
return "string", nil
|
||||
}
|
||||
return "int64", nil
|
||||
case "decimal":
|
||||
return "decimal", nil
|
||||
case "bool":
|
||||
return "bool", nil
|
||||
case "time":
|
||||
return "int64", nil // TODO: this is a placeholder
|
||||
default:
|
||||
return "", fmt.Errorf("unimplemented field Datatype: %s", t)
|
||||
}
|
||||
}
|
||||
|
||||
func groupCompare(a, b string, offset int) (lt, eq bool) {
|
||||
if len(a) > offset {
|
||||
a = a[:offset]
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -43,6 +43,7 @@ require (
|
|||
google.golang.org/grpc v1.28.0
|
||||
modernc.org/mathutil v1.0.0
|
||||
modernc.org/strutil v1.0.0
|
||||
vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -288,3 +288,5 @@ modernc.org/mathutil v1.0.0 h1:93vKjrJopTPrtTNpZ8XIovER7iCIH1QU7wNbOQXC60I=
|
|||
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
|
||||
modernc.org/strutil v1.0.0 h1:XVFtQwFVwc02Wk+0L/Z/zDDXO81r5Lhe6iMKmGX3KhE=
|
||||
modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||
vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible h1:GWnLrAdetgJM0Co5bwwczO49iFZBSInpyGAT77BP9Y0=
|
||||
vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible/go.mod h1:h4qvkyNYTOC0xI+vcidSWoka0gQAZc9ZPHbkHo48gP0=
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ package pilosa
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -30,6 +31,36 @@ type StreamClient interface {
|
|||
Recv() (*RowResponse, error)
|
||||
}
|
||||
|
||||
// ReadIntoTable reads from a StreamClient and stores the result into a table response.
|
||||
func ReadIntoTable(cli StreamClient) (*TableResponse, error) {
|
||||
var headers []*ColumnInfo
|
||||
rows := []*Row{}
|
||||
|
||||
rx:
|
||||
for {
|
||||
row, err := cli.Recv()
|
||||
switch err {
|
||||
case nil:
|
||||
case io.EOF:
|
||||
break rx
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if headers == nil {
|
||||
headers = row.Headers
|
||||
}
|
||||
rows = append(rows, &Row{
|
||||
Columns: row.Columns,
|
||||
})
|
||||
}
|
||||
|
||||
return &TableResponse{
|
||||
Headers: headers,
|
||||
Rows: rows,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StreamServer is an interface for a stream
|
||||
// which can accept a RowResponse to be later
|
||||
// returned by the stream via Recv().
|
||||
|
|
@ -84,6 +115,52 @@ func RowsToTable(tr ToRowser, n int) (*TableResponse, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// RowBuffer acts as a Sender/Receiver of RowResponses.
|
||||
// Note that sending a nil value will cause the Recv
|
||||
// method to return an io.EOF error.
|
||||
type RowBuffer struct {
|
||||
ch chan *RowResponse
|
||||
}
|
||||
|
||||
// NewRowBuffer returns a new instance of RowBuffer.
|
||||
// sz is the size of the buffer.
|
||||
func NewRowBuffer(sz int) *RowBuffer {
|
||||
var chSz int
|
||||
if sz > 0 {
|
||||
// Add one to allow for the EOF record.
|
||||
chSz = sz + 1
|
||||
}
|
||||
return &RowBuffer{
|
||||
ch: make(chan *RowResponse, chSz),
|
||||
}
|
||||
}
|
||||
|
||||
// Recv returns a RowResponse. When the buffer is empty,
|
||||
// calling Recv will return an io.EOF error.
|
||||
func (rb *RowBuffer) Recv() (*RowResponse, error) {
|
||||
r := <-rb.ch
|
||||
|
||||
// If the StatusError contains a message then return
|
||||
// with the approprate error.
|
||||
se := r.GetStatusError()
|
||||
code := codes.Code(se.GetCode())
|
||||
msg := se.GetMessage()
|
||||
if code != codes.OK {
|
||||
return nil, status.Error(code, msg)
|
||||
} else if msg == "EOF" {
|
||||
return nil, io.EOF
|
||||
} else if msg != "" {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (rb *RowBuffer) Send(rr *RowResponse) error {
|
||||
rb.ch <- rr
|
||||
return nil
|
||||
}
|
||||
|
||||
// EOF acts as an io.EOF encoded into a RowResponse.
|
||||
var EOF *RowResponse = &RowResponse{
|
||||
StatusError: &StatusError{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -25,6 +26,7 @@ import (
|
|||
"github.com/pilosa/pilosa/v2"
|
||||
"github.com/pilosa/pilosa/v2/logger"
|
||||
pb "github.com/pilosa/pilosa/v2/proto"
|
||||
"github.com/pilosa/pilosa/v2/sql"
|
||||
"github.com/pilosa/pilosa/v2/stats"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc"
|
||||
|
|
@ -117,9 +119,49 @@ func (h *GRPCHandler) DeleteVDS(ctx context.Context, req *pb.DeleteVDSRequest) (
|
|||
return &pb.DeleteVDSResponse{}, nil
|
||||
}
|
||||
|
||||
func (h *GRPCHandler) execSQL(ctx context.Context, queryStr string) (pb.StreamClient, error) {
|
||||
mapper := sql.NewMapper()
|
||||
mapper.Logger = h.logger
|
||||
query, err := mapper.MapSQL(queryStr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to map SQL")
|
||||
}
|
||||
var results pb.StreamClient
|
||||
switch query.SQLType {
|
||||
case sql.SQLTypeSelect:
|
||||
handler := sql.NewSelectHandler(h.api)
|
||||
results, err = handler.Handle(ctx, query)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to start SQL query")
|
||||
}
|
||||
default:
|
||||
return nil, status.Errorf(codes.Unimplemented, "query type not supported")
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// QuerySQL handles the SQL request and sends RowResponses to the stream.
|
||||
func (*GRPCHandler) QuerySQL(req *pb.QuerySQLRequest, stream pb.Pilosa_QuerySQLServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method QuerySQL not implemented")
|
||||
func (h *GRPCHandler) QuerySQL(req *pb.QuerySQLRequest, stream pb.Pilosa_QuerySQLServer) error {
|
||||
results, err := h.execSQL(stream.Context(), req.Sql)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
row, err := results.Recv()
|
||||
switch err {
|
||||
case nil:
|
||||
case io.EOF:
|
||||
return nil
|
||||
default:
|
||||
return errors.Wrap(err, "failed to load next row")
|
||||
}
|
||||
|
||||
err = stream.Send(row)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to send row")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// QuerySQLUnary is a unary-response (non-streaming) version of QuerySQL, returning a TableResponse.
|
||||
|
|
@ -134,8 +176,12 @@ func (*GRPCHandler) QuerySQL(req *pb.QuerySQLRequest, stream pb.Pilosa_QuerySQLS
|
|||
// Futures, which are used by python-molecula to perform multiple queries
|
||||
// concurrently. There is additional discussion and historical context here:
|
||||
// https://github.com/molecula/pilosa/pull/644
|
||||
func (*GRPCHandler) QuerySQLUnary(ctx context.Context, req *pb.QuerySQLRequest) (*pb.TableResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method QuerySQLUnary not implemented")
|
||||
func (h *GRPCHandler) QuerySQLUnary(ctx context.Context, req *pb.QuerySQLRequest) (*pb.TableResponse, error) {
|
||||
results, err := h.execSQL(ctx, req.Sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pb.ReadIntoTable(results)
|
||||
}
|
||||
|
||||
// QueryPQL handles the PQL request and sends RowResponses to the stream.
|
||||
|
|
@ -298,36 +344,6 @@ func ToRowserWrapper(result interface{}) (pb.ToRowser, error) {
|
|||
return toRowser, nil
|
||||
}
|
||||
|
||||
// fieldDataType returns a useful data type (string,
|
||||
// uint64, bool, etc.) based on the Pilosa field type.
|
||||
func fieldDataType(f *pilosa.Field) string {
|
||||
switch f.Type() {
|
||||
case "set":
|
||||
if f.Keys() {
|
||||
return "[]string"
|
||||
}
|
||||
return "[]uint64"
|
||||
case "mutex":
|
||||
if f.Keys() {
|
||||
return "string"
|
||||
}
|
||||
return "uint64"
|
||||
case "int":
|
||||
if f.Keys() {
|
||||
return "string"
|
||||
}
|
||||
return "int64"
|
||||
case "decimal":
|
||||
return "decimal"
|
||||
case "bool":
|
||||
return "bool"
|
||||
case "time":
|
||||
return "int64" // TODO: this is a placeholder
|
||||
default:
|
||||
panic(fmt.Sprintf("unimplemented fieldDataType: %s", f.Type()))
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect handles the inspect request and sends an InspectResponse to the stream.
|
||||
func (h *GRPCHandler) Inspect(req *pb.InspectRequest, stream pb.Pilosa_InspectServer) error {
|
||||
const defaultLimit = 100000
|
||||
|
|
@ -422,7 +438,11 @@ func (h *GRPCHandler) Inspect(req *pb.InspectRequest, stream pb.Pilosa_InspectSe
|
|||
{Name: "_id", Datatype: "uint64"},
|
||||
}
|
||||
for _, field := range fields {
|
||||
ci = append(ci, &pb.ColumnInfo{Name: field.Name(), Datatype: fieldDataType(field)})
|
||||
fdt, err := field.Datatype()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "field %s", field.Name())
|
||||
}
|
||||
ci = append(ci, &pb.ColumnInfo{Name: field.Name(), Datatype: fdt})
|
||||
}
|
||||
|
||||
// If Columns is empty, then get the _exists list (via All()),
|
||||
|
|
@ -666,7 +686,11 @@ func (h *GRPCHandler) Inspect(req *pb.InspectRequest, stream pb.Pilosa_InspectSe
|
|||
{Name: "_id", Datatype: "string"},
|
||||
}
|
||||
for _, field := range fields {
|
||||
ci = append(ci, &pb.ColumnInfo{Name: field.Name(), Datatype: fieldDataType(field)})
|
||||
fdt, err := field.Datatype()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "field %s", field.Name())
|
||||
}
|
||||
ci = append(ci, &pb.ColumnInfo{Name: field.Name(), Datatype: fdt})
|
||||
}
|
||||
|
||||
// If Columns is empty, then get the _exists list (via All()),
|
||||
|
|
|
|||
|
|
@ -16,9 +16,13 @@ package server_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/pilosa/pilosa/v2"
|
||||
"github.com/pilosa/pilosa/v2/pql"
|
||||
pb "github.com/pilosa/pilosa/v2/proto"
|
||||
"github.com/pilosa/pilosa/v2/server"
|
||||
"github.com/pilosa/pilosa/v2/test"
|
||||
|
|
@ -338,7 +342,6 @@ func TestQueryPQLUnary(t *testing.T) {
|
|||
|
||||
i := m.MustCreateIndex(t, "i", pilosa.IndexOptions{})
|
||||
m.MustCreateField(t, i.Name(), "f", pilosa.OptFieldKeys())
|
||||
|
||||
ctx := context.Background()
|
||||
gh := server.NewGRPCHandler(m.API)
|
||||
|
||||
|
|
@ -361,3 +364,579 @@ func TestQueryPQLUnary(t *testing.T) {
|
|||
t.Fatalf("expected error: InvalidArgument, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
tableResponse struct {
|
||||
headers []columnInfo
|
||||
rows []row
|
||||
}
|
||||
columnInfo struct {
|
||||
name string
|
||||
datatype string
|
||||
}
|
||||
row struct {
|
||||
columns []columnResponse
|
||||
}
|
||||
columnResponse interface{}
|
||||
)
|
||||
|
||||
func TestQuerySQLUnary(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
gh, tearDownFunc := setUpTestQuerySQLUnary(ctx, t)
|
||||
defer tearDownFunc()
|
||||
|
||||
tests := []struct {
|
||||
sql string
|
||||
exp tableResponse
|
||||
eq func(tableResponse, tableResponse) error
|
||||
}{
|
||||
{
|
||||
// Extract(Limit(All(), limit=100, offset=0),Rows(age))
|
||||
sql: "select age from grouper",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"age", "int64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(27)}},
|
||||
{[]columnResponse{int64(16)}},
|
||||
{[]columnResponse{int64(19)}},
|
||||
{[]columnResponse{int64(27)}},
|
||||
{[]columnResponse{int64(16)}},
|
||||
{[]columnResponse{int64(34)}},
|
||||
{[]columnResponse{int64(27)}},
|
||||
{[]columnResponse{int64(16)}},
|
||||
{[]columnResponse{int64(16)}},
|
||||
{[]columnResponse{int64(31)}},
|
||||
},
|
||||
},
|
||||
eq: equal,
|
||||
},
|
||||
{
|
||||
// Extract(Limit(ConstRow(columns=[2]), limit=100, offset=0),Rows(age),Rows(color),Rows(height),Rows(score))
|
||||
sql: "select * from grouper where _id=2",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"_id", "uint64"},
|
||||
{"age", "int64"},
|
||||
{"color", "[]string"},
|
||||
{"height", "int64"},
|
||||
{"score", "int64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{uint64(2), int64(16), []string{"blue"}, int64(30), int64(-8)}},
|
||||
},
|
||||
},
|
||||
eq: equal,
|
||||
},
|
||||
// join
|
||||
{
|
||||
// Count(Intersect(All(),Distinct(Row(grouperid!=null),index='joiner',field='grouperid')))
|
||||
sql: "select count(*) from grouper g INNER JOIN joiner j ON g._id = j.grouperid",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"count(*)", "uint64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{uint64(8)}},
|
||||
},
|
||||
},
|
||||
eq: equal,
|
||||
},
|
||||
{
|
||||
// Intersect(All(),Distinct(Row(grouperid!=null),index='joiner',field='grouperid'))
|
||||
sql: "select _id from grouper g INNER JOIN joiner j ON g._id = j.grouperid",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"_id", "uint64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{uint64(1)}},
|
||||
{[]columnResponse{uint64(2)}},
|
||||
{[]columnResponse{uint64(3)}},
|
||||
{[]columnResponse{uint64(5)}},
|
||||
{[]columnResponse{uint64(6)}},
|
||||
{[]columnResponse{uint64(7)}},
|
||||
{[]columnResponse{uint64(8)}},
|
||||
{[]columnResponse{uint64(9)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
{
|
||||
// Intersect(Row(color='red'),Distinct(Row(grouperid!=null),index='joiner',field='grouperid'))
|
||||
sql: "select _id from grouper g INNER JOIN joiner j ON g._id = j.grouperid where g.color = 'red'",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"_id", "uint64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{uint64(3)}},
|
||||
{[]columnResponse{uint64(8)}},
|
||||
{[]columnResponse{uint64(9)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
{
|
||||
// Intersect(Row(color='red'),Distinct(Row(jointype=2),index='joiner',field='grouperid'))
|
||||
sql: "select _id from grouper g INNER JOIN joiner j ON g._id = j.grouperid where g.color = 'red' and j.jointype = 2",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"_id", "uint64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{uint64(3)}},
|
||||
{[]columnResponse{uint64(8)}},
|
||||
{[]columnResponse{uint64(9)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
// order by
|
||||
{
|
||||
// Distinct(Row(score!=null),index='grouper',field='score')
|
||||
sql: "select distinct score from grouper order by score asc",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"score", "int64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(-13)}},
|
||||
{[]columnResponse{int64(-10)}},
|
||||
{[]columnResponse{int64(-8)}},
|
||||
{[]columnResponse{int64(-2)}},
|
||||
{[]columnResponse{int64(0)}},
|
||||
{[]columnResponse{int64(6)}},
|
||||
{[]columnResponse{int64(80)}},
|
||||
{[]columnResponse{int64(100)}},
|
||||
},
|
||||
},
|
||||
eq: equal,
|
||||
},
|
||||
{
|
||||
// Distinct(Row(score!=null),index='grouper',field='score')
|
||||
sql: "select distinct score from grouper order by score desc",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"score", "int64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(100)}},
|
||||
{[]columnResponse{int64(80)}},
|
||||
{[]columnResponse{int64(6)}},
|
||||
{[]columnResponse{int64(0)}},
|
||||
{[]columnResponse{int64(-2)}},
|
||||
{[]columnResponse{int64(-8)}},
|
||||
{[]columnResponse{int64(-10)}},
|
||||
{[]columnResponse{int64(-13)}},
|
||||
},
|
||||
},
|
||||
eq: equal,
|
||||
},
|
||||
{
|
||||
// Distinct(Row(score!=null),index='grouper',field='score')
|
||||
sql: "select distinct score from grouper order by score asc limit 5",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"score", "int64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(-13)}},
|
||||
{[]columnResponse{int64(-10)}},
|
||||
{[]columnResponse{int64(-8)}},
|
||||
{[]columnResponse{int64(-2)}},
|
||||
{[]columnResponse{int64(0)}},
|
||||
},
|
||||
},
|
||||
eq: equal,
|
||||
},
|
||||
|
||||
{
|
||||
// Distinct(Row(score!=null),index='grouper',field='score')
|
||||
sql: "select distinct score from grouper order by score desc limit 5",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"score", "int64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(100)}},
|
||||
{[]columnResponse{int64(80)}},
|
||||
{[]columnResponse{int64(6)}},
|
||||
{[]columnResponse{int64(0)}},
|
||||
{[]columnResponse{int64(-2)}},
|
||||
},
|
||||
},
|
||||
eq: equal,
|
||||
},
|
||||
|
||||
// distinct
|
||||
{
|
||||
// Distinct(Row(score!=null),index='grouper',field='score')
|
||||
sql: "select distinct score from grouper",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"score", "int64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(-13)}},
|
||||
{[]columnResponse{int64(-10)}},
|
||||
{[]columnResponse{int64(-8)}},
|
||||
{[]columnResponse{int64(-2)}},
|
||||
{[]columnResponse{int64(0)}},
|
||||
{[]columnResponse{int64(6)}},
|
||||
{[]columnResponse{int64(80)}},
|
||||
{[]columnResponse{int64(100)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
{
|
||||
|
||||
// Distinct(Row(height!=null),index='grouper',field='height')
|
||||
sql: "select distinct height from grouper",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{{"height", "int64"}},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(20)}},
|
||||
{[]columnResponse{int64(30)}},
|
||||
{[]columnResponse{int64(40)}},
|
||||
{[]columnResponse{int64(50)}},
|
||||
{[]columnResponse{int64(60)}},
|
||||
{[]columnResponse{int64(70)}},
|
||||
{[]columnResponse{int64(80)}},
|
||||
{[]columnResponse{int64(90)}},
|
||||
{[]columnResponse{int64(100)}},
|
||||
{[]columnResponse{int64(110)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
|
||||
// groupby
|
||||
{
|
||||
// GroupBy(Rows(field='age'),limit=100)
|
||||
sql: "select age as yrs, count(*) as cnt from grouper group by age",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"yrs", "int64"},
|
||||
{"cnt", "uint64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(16), uint64(4)}},
|
||||
{[]columnResponse{int64(19), uint64(1)}},
|
||||
{[]columnResponse{int64(27), uint64(3)}},
|
||||
{[]columnResponse{int64(31), uint64(1)}},
|
||||
{[]columnResponse{int64(34), uint64(1)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
{
|
||||
// GroupBy(Rows(field='age'),Rows(field='color'),limit=100)
|
||||
sql: "select age, color, count(*) from grouper group by age, color",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"age", "int64"},
|
||||
{"color", "string"},
|
||||
{"count(*)", "uint64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(16), "blue", uint64(2)}},
|
||||
{[]columnResponse{int64(16), "red", uint64(2)}},
|
||||
{[]columnResponse{int64(19), "red", uint64(1)}},
|
||||
{[]columnResponse{int64(27), "blue", uint64(2)}},
|
||||
{[]columnResponse{int64(27), "green", uint64(1)}},
|
||||
{[]columnResponse{int64(31), "red", uint64(1)}},
|
||||
{[]columnResponse{int64(34), "blue", uint64(1)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
{
|
||||
// GroupBy(Rows(field='age'),Rows(field='color'),limit=100,filter=Row(age=27),aggregate=Sum(field='height'))
|
||||
sql: "select age, color, sum(height) from grouper where age = 27 group by age, color",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"age", "int64"},
|
||||
{"color", "string"},
|
||||
{"sum(height)", "int64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(27), "blue", int64(100)}},
|
||||
{[]columnResponse{int64(27), "green", int64(50)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
{
|
||||
// GroupBy(Rows(field='age'),limit=100,having=Condition(count>1))
|
||||
sql: "select age, count(*) from grouper group by age having count > 1",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"age", "int64"},
|
||||
{"count(*)", "uint64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(16), uint64(4)}},
|
||||
{[]columnResponse{int64(27), uint64(3)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
{
|
||||
// GroupBy(Rows(field='age'),limit=100,having=Condition(1<=count<=3))
|
||||
sql: "select age, count(*) from grouper group by age having count between 1 and 3",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"age", "int64"},
|
||||
{"count(*)", "uint64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(19), uint64(1)}},
|
||||
{[]columnResponse{int64(27), uint64(3)}},
|
||||
{[]columnResponse{int64(31), uint64(1)}},
|
||||
{[]columnResponse{int64(34), uint64(1)}},
|
||||
},
|
||||
},
|
||||
eq: equalUnordered,
|
||||
},
|
||||
|
||||
{
|
||||
// GroupBy(Rows(field='age'),limit=3)
|
||||
sql: "select age, count(*) as cnt from grouper group by age order by cnt desc, age desc limit 3",
|
||||
exp: tableResponse{
|
||||
headers: []columnInfo{
|
||||
{"age", "int64"},
|
||||
{"cnt", "uint64"},
|
||||
},
|
||||
rows: []row{
|
||||
{[]columnResponse{int64(16), uint64(4)}},
|
||||
{[]columnResponse{int64(27), uint64(3)}},
|
||||
{[]columnResponse{int64(19), uint64(1)}},
|
||||
},
|
||||
},
|
||||
eq: equal,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run("test-"+strconv.Itoa(i), func(t *testing.T) {
|
||||
resp, err := gh.QuerySQLUnary(ctx, &pb.QuerySQLRequest{Sql: test.sql})
|
||||
if err != nil {
|
||||
t.Fatalf("sql: %s, error: %v", test.sql, err)
|
||||
} else {
|
||||
tr := toTableResponse(resp)
|
||||
if err := test.eq(test.exp, tr); err != nil {
|
||||
t.Fatalf("sql: %s, error: %+v", test.sql, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setUpTestQuerySQLUnary(ctx context.Context, t *testing.T) (gh *server.GRPCHandler, tearDownFunc func()) {
|
||||
t.Helper()
|
||||
|
||||
m := test.RunCommand(t)
|
||||
gh = server.NewGRPCHandler(m.API)
|
||||
|
||||
// grouper
|
||||
grouper := m.MustCreateIndex(t, "grouper", pilosa.IndexOptions{Keys: false, TrackExistence: true})
|
||||
m.MustCreateField(t, grouper.Name(), "color", pilosa.OptFieldKeys())
|
||||
for id, color := range map[int]string{
|
||||
1: "blue",
|
||||
2: "blue",
|
||||
5: "blue",
|
||||
6: "blue",
|
||||
7: "blue",
|
||||
3: "red",
|
||||
8: "red",
|
||||
9: "red",
|
||||
10: "red",
|
||||
4: "green",
|
||||
} {
|
||||
if _, err := gh.QueryPQLUnary(ctx, &pb.QueryPQLRequest{
|
||||
Index: grouper.Name(),
|
||||
Pql: fmt.Sprintf(`Set(%d, color="%s")`, id, color),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
m.MustCreateField(t, grouper.Name(), "score", pilosa.OptFieldTypeInt(-1000, 1000))
|
||||
for id, score := range map[int]int{
|
||||
1: -10,
|
||||
2: -8,
|
||||
3: 6,
|
||||
4: 0,
|
||||
5: -2,
|
||||
6: 100,
|
||||
7: 0,
|
||||
8: -13,
|
||||
9: 80,
|
||||
10: -2,
|
||||
} {
|
||||
if _, err := gh.QueryPQLUnary(ctx, &pb.QueryPQLRequest{
|
||||
Index: grouper.Name(),
|
||||
Pql: fmt.Sprintf(`Set(%d, score=%d)`, id, score),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
m.MustCreateField(t, grouper.Name(), "age", pilosa.OptFieldTypeInt(0, 100))
|
||||
for id, age := range map[int]int{
|
||||
2: 16,
|
||||
5: 16,
|
||||
8: 16,
|
||||
9: 16,
|
||||
3: 19,
|
||||
1: 27,
|
||||
4: 27,
|
||||
7: 27,
|
||||
10: 31,
|
||||
6: 34,
|
||||
} {
|
||||
if _, err := gh.QueryPQLUnary(ctx, &pb.QueryPQLRequest{
|
||||
Index: grouper.Name(),
|
||||
Pql: fmt.Sprintf(`Set(%d, age=%d)`, id, age),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
m.MustCreateField(t, grouper.Name(), "height", pilosa.OptFieldTypeInt(0, 1000))
|
||||
for id, height := range map[int]int{
|
||||
1: 20,
|
||||
2: 30,
|
||||
3: 40,
|
||||
4: 50,
|
||||
5: 60,
|
||||
6: 70,
|
||||
7: 80,
|
||||
8: 90,
|
||||
9: 100,
|
||||
10: 110,
|
||||
} {
|
||||
if _, err := gh.QueryPQLUnary(ctx, &pb.QueryPQLRequest{
|
||||
Index: grouper.Name(),
|
||||
Pql: fmt.Sprintf(`Set(%d, height=%d)`, id, height),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// joiner
|
||||
joiner := m.MustCreateIndex(t, "joiner", pilosa.IndexOptions{TrackExistence: true})
|
||||
m.MustCreateField(t, joiner.Name(), "grouperid", pilosa.OptFieldTypeInt(0, 1000), pilosa.OptFieldForeignIndex(grouper.Name()))
|
||||
m.MustCreateField(t, joiner.Name(), "jointype", pilosa.OptFieldTypeInt(-1000, 1000))
|
||||
for id, grouperid := range map[int]int{
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 5,
|
||||
4: 6,
|
||||
5: 7,
|
||||
6: 3,
|
||||
7: 8,
|
||||
8: 9,
|
||||
9: 1,
|
||||
10: 2,
|
||||
} {
|
||||
if _, err := gh.QueryPQLUnary(ctx, &pb.QueryPQLRequest{
|
||||
Index: joiner.Name(),
|
||||
Pql: fmt.Sprintf(`Set(%d, grouperid=%d)`, id, grouperid),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for id, jointype := range map[int]int{
|
||||
1: 1,
|
||||
2: 1,
|
||||
3: 1,
|
||||
4: 1,
|
||||
5: 1,
|
||||
6: 2,
|
||||
7: 2,
|
||||
8: 2,
|
||||
9: 3,
|
||||
10: 3,
|
||||
} {
|
||||
if _, err := gh.QueryPQLUnary(ctx, &pb.QueryPQLRequest{
|
||||
Index: joiner.Name(),
|
||||
Pql: fmt.Sprintf(`Set(%d, jointype=%d)`, id, jointype),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return gh, func() {
|
||||
if err := m.API.DeleteIndex(ctx, joiner.Name()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := m.API.DeleteIndex(ctx, grouper.Name()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := m.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toTableResponse(resp *pb.TableResponse) tableResponse {
|
||||
tr := tableResponse{
|
||||
headers: make([]columnInfo, len(resp.Headers)),
|
||||
rows: make([]row, len(resp.Rows)),
|
||||
}
|
||||
|
||||
for i, h := range resp.Headers {
|
||||
tr.headers[i] = columnInfo{
|
||||
name: h.Name,
|
||||
datatype: h.Datatype,
|
||||
}
|
||||
}
|
||||
|
||||
for i, r := range resp.Rows {
|
||||
tr.rows[i].columns = make([]columnResponse, len(r.Columns))
|
||||
for j, c := range r.Columns {
|
||||
|
||||
switch v := c.GetColumnVal().(type) {
|
||||
case *pb.ColumnResponse_StringVal:
|
||||
tr.rows[i].columns[j] = v.StringVal
|
||||
case *pb.ColumnResponse_Uint64Val:
|
||||
tr.rows[i].columns[j] = v.Uint64Val
|
||||
case *pb.ColumnResponse_Int64Val:
|
||||
tr.rows[i].columns[j] = v.Int64Val
|
||||
case *pb.ColumnResponse_BoolVal:
|
||||
tr.rows[i].columns[j] = v.BoolVal
|
||||
case *pb.ColumnResponse_BlobVal:
|
||||
tr.rows[i].columns[j] = v.BlobVal
|
||||
case *pb.ColumnResponse_Uint64ArrayVal:
|
||||
tr.rows[i].columns[j] = v.Uint64ArrayVal.Vals
|
||||
case *pb.ColumnResponse_StringArrayVal:
|
||||
tr.rows[i].columns[j] = v.StringArrayVal.Vals
|
||||
case *pb.ColumnResponse_Float64Val:
|
||||
tr.rows[i].columns[j] = v.Float64Val
|
||||
case *pb.ColumnResponse_DecimalVal:
|
||||
tr.rows[i].columns[j] = pql.NewDecimal(v.DecimalVal.Value, v.DecimalVal.Scale)
|
||||
default:
|
||||
tr.rows[i].columns[j] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
func equal(exp tableResponse, got tableResponse) error {
|
||||
if !reflect.DeepEqual(exp, got) {
|
||||
return fmt.Errorf("got: %+v %[1]T, but expected: %+v", got, exp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func equalUnordered(exp tableResponse, got tableResponse) error {
|
||||
if len(exp.headers) != len(got.headers) || !reflect.DeepEqual(exp.headers, got.headers) {
|
||||
return fmt.Errorf("header does not match: got %+v, but expected %+v", got.headers, exp.headers)
|
||||
}
|
||||
|
||||
if len(exp.rows) != len(got.rows) {
|
||||
return fmt.Errorf("rows count does not match: got %+v, but expected %+v", len(got.rows), len(exp.rows))
|
||||
}
|
||||
for _, er := range exp.rows {
|
||||
for j, gr := range got.rows {
|
||||
if reflect.DeepEqual(er.columns, gr.columns) {
|
||||
got.rows[j] = got.rows[len(got.rows)-1]
|
||||
got.rows = got.rows[:len(got.rows)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(got.rows) > 0 {
|
||||
return fmt.Errorf("got incorrect rows: %+v", got.rows)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
112
sql/column.go
Normal file
112
sql/column.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// 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 (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const ColID = "_id"
|
||||
|
||||
type FuncName string
|
||||
|
||||
const (
|
||||
FuncCount FuncName = "count"
|
||||
FuncMin FuncName = "min"
|
||||
FuncMax FuncName = "max"
|
||||
FuncSum FuncName = "sum"
|
||||
FuncAvg FuncName = "avg"
|
||||
)
|
||||
|
||||
// Column is an interface which supports mapping to
|
||||
// source headers, and aliasing column names.
|
||||
// Alias() should always return a value;
|
||||
// either a unique alias, or the same value
|
||||
// return by Name(), but never an empty string.
|
||||
type Column interface {
|
||||
Source() string
|
||||
Name() string
|
||||
Alias() string
|
||||
}
|
||||
|
||||
type BasicColumn struct {
|
||||
source string
|
||||
name string
|
||||
alias string
|
||||
}
|
||||
|
||||
func NewBasicColumn(s, n, a string) *BasicColumn {
|
||||
return &BasicColumn{
|
||||
source: s,
|
||||
name: n,
|
||||
alias: a,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BasicColumn) Source() string {
|
||||
return b.source
|
||||
}
|
||||
func (b *BasicColumn) Name() string {
|
||||
return b.name
|
||||
}
|
||||
func (b *BasicColumn) Alias() string {
|
||||
if b.alias != "" {
|
||||
return b.alias
|
||||
}
|
||||
return b.name
|
||||
}
|
||||
|
||||
type StarColumn struct{}
|
||||
|
||||
func NewStarColumn() *StarColumn {
|
||||
return &StarColumn{}
|
||||
}
|
||||
|
||||
func (s *StarColumn) Source() string {
|
||||
return ""
|
||||
}
|
||||
func (s *StarColumn) Name() string {
|
||||
return ""
|
||||
}
|
||||
func (s *StarColumn) Alias() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func ConvertToTime(text string) (time.Time, bool) {
|
||||
timeFormats := []string{
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02",
|
||||
}
|
||||
for _, timeFormat := range timeFormats {
|
||||
if t, err := time.Parse(timeFormat, text); err == nil {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return time.Now(), false
|
||||
}
|
||||
|
||||
func ExtractFieldName(columName string) (fieldName string, isSpecial bool) {
|
||||
isSpecial = false
|
||||
fieldName = columName
|
||||
if strings.HasPrefix(columName, "_") {
|
||||
isSpecial = true
|
||||
if strings.HasSuffix(columName, "_time") {
|
||||
fieldName = columName[1 : len(columName)-5]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
1177
sql/extract.go
Normal file
1177
sql/extract.go
Normal file
File diff suppressed because it is too large
Load diff
113
sql/mapper.go
Normal file
113
sql/mapper.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// 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 (
|
||||
"strings"
|
||||
|
||||
"github.com/pilosa/pilosa/v2/logger"
|
||||
"github.com/pkg/errors"
|
||||
"vitess.io/vitess/go/vt/sqlparser"
|
||||
)
|
||||
|
||||
const (
|
||||
SQLTypeSelect = "select"
|
||||
SQLTypeShow = "show"
|
||||
SQLTypeEmpty = ""
|
||||
)
|
||||
|
||||
// System errors.
|
||||
var (
|
||||
ErrMultipleSQLStatements = errors.New("statement contains multiple sql queries")
|
||||
)
|
||||
|
||||
type Attributes map[string]interface{}
|
||||
|
||||
type MappedSQL struct {
|
||||
SQLType string
|
||||
Statement sqlparser.Statement
|
||||
Mask QueryMask
|
||||
Tables []string
|
||||
}
|
||||
|
||||
// Mapper is responsible for mapping a SQL query to structure representation
|
||||
type Mapper struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
func NewMapper() *Mapper {
|
||||
return &Mapper{
|
||||
Logger: logger.NopLogger,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses SQL query
|
||||
func (m *Mapper) Parse(sql string) (sqlparser.Statement, QueryMask, error) {
|
||||
parsed, err := sqlparser.Parse(sql)
|
||||
if err != nil {
|
||||
return nil, QueryMask{}, errors.Wrap(err, "parsing sql")
|
||||
}
|
||||
|
||||
qm := GenerateMask(parsed)
|
||||
|
||||
return parsed, qm, nil
|
||||
}
|
||||
|
||||
// MapSQL converts a sql string into a MappedSQL object,
|
||||
// which includes the parsed query and the query mask,
|
||||
// among other information about the query.
|
||||
func (m *Mapper) MapSQL(sql string) (*MappedSQL, error) {
|
||||
// In the case where `sql` contains more than one query—since
|
||||
// we don't support multiple return sets—we're going to just
|
||||
// ignore everything and return a specific error type. This
|
||||
// will allow the caller to handle it as needed (i.e. it can
|
||||
// return the error, or return an empty result set).
|
||||
if parts := strings.Split(sql, ";"); len(parts) > 1 {
|
||||
var partCount int
|
||||
for _, part := range parts {
|
||||
if trimmed := strings.TrimSpace(part); trimmed != "" && trimmed != "\x00" {
|
||||
partCount++
|
||||
}
|
||||
}
|
||||
if partCount != 1 {
|
||||
return nil, ErrMultipleSQLStatements
|
||||
}
|
||||
}
|
||||
|
||||
stmt, qm, err := m.Parse(sql)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing sql")
|
||||
}
|
||||
|
||||
var sqlType string
|
||||
var tableNames []string
|
||||
switch slct := stmt.(type) {
|
||||
case *sqlparser.Select:
|
||||
sqlType = SQLTypeSelect
|
||||
tableNames, err = extractTableNames(slct)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting table names")
|
||||
}
|
||||
case *sqlparser.Show:
|
||||
sqlType = SQLTypeShow
|
||||
}
|
||||
|
||||
return &MappedSQL{
|
||||
SQLType: sqlType,
|
||||
Statement: stmt,
|
||||
Mask: qm,
|
||||
Tables: tableNames,
|
||||
}, nil
|
||||
}
|
||||
394
sql/mapper_test.go
Normal file
394
sql/mapper_test.go
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"vitess.io/vitess/go/vt/sqlparser"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
sql string
|
||||
expMask QueryMask
|
||||
}{
|
||||
{
|
||||
sql: "select _id from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartID,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select * from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartStar,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartField,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld1, fld2 from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartFields,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select count(*) from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select min(fld) from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartMinField,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select max(fld) from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartMaxField,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select sum(fld) from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartSumField,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select avg(fld) from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartAvgField,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select _id, count(*) from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartID | SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select _id from tbl1, tbl2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartID,
|
||||
FromMask: FromPartTables,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select _id from tbl1 INNER JOIN tbl2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartID,
|
||||
FromMask: FromPartJoin,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select * from tbl where _id = 1",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartStar,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartIDCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select * from tbl where fld = 1",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartStar,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartFieldCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select * from tbl group by fld",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartStar,
|
||||
FromMask: FromPartTable,
|
||||
GroupByMask: GroupByPartField,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select * from tbl group by fld1, fld2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartStar,
|
||||
FromMask: FromPartTable,
|
||||
GroupByMask: GroupByPartFields,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld, sum(fld) from tbl group by fld",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartField | SelectPartSumField,
|
||||
FromMask: FromPartTable,
|
||||
GroupByMask: GroupByPartField,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld, sum(fld) from tbl group by fld having sum > 10",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartField | SelectPartSumField,
|
||||
FromMask: FromPartTable,
|
||||
GroupByMask: GroupByPartField,
|
||||
HavingMask: HavingPartCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld from tbl order by fld",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartField,
|
||||
FromMask: FromPartTable,
|
||||
OrderByMask: OrderByPartField,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld from tbl order by fld1, fld2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartField,
|
||||
FromMask: FromPartTable,
|
||||
OrderByMask: OrderByPartFields,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld from tbl limit 10",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartField,
|
||||
FromMask: FromPartTable,
|
||||
LimitMask: LimitPartLimit,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld from tbl limit 10, 5",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartField,
|
||||
FromMask: FromPartTable,
|
||||
LimitMask: LimitPartLimit | LimitPartOffset,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select distinct fld from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartDistinct | SelectPartField,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select count(*) from tbl where fld = 1",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartFieldCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select count(*) from tbl where fld1 = 1 and fld2 = 2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartMultiFieldCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select count(*) from tbl where fld1 = 1 or fld2 = 2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartMultiFieldCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select _id from tbl where not fld = 1 limit 10",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartID,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartFieldCondition,
|
||||
LimitMask: LimitPartLimit,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select _id from tbl where fld between 1 and 3",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartID,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartFieldCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select _id from tbl where fld1 between 1 and 3 and fld2 = 2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartID,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartMultiFieldCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select count(*) from tbl where fld is not null",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartFieldCondition,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld, count(*) from tbl group by fld",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartField | SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
GroupByMask: GroupByPartField,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld1, fld2, count(*) from grouper group by fld1, fld2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartFields | SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
GroupByMask: GroupByPartFields,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select fld1, fld2, count(*) from tbl where fld1 = 1 group by fld1, fld2 limit 1",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartFields | SelectPartCountStar,
|
||||
FromMask: FromPartTable,
|
||||
WhereMask: WherePartFieldCondition,
|
||||
GroupByMask: GroupByPartFields,
|
||||
LimitMask: LimitPartLimit,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select count(distinct fld) from tbl",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartCountDistinctField,
|
||||
FromMask: FromPartTable,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select count(*) from tbl1 INNER JOIN tbl2 ON tbl1._id = tbl2.bsi",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartCountStar,
|
||||
FromMask: FromPartJoin,
|
||||
},
|
||||
},
|
||||
{
|
||||
sql: "select count(*) from tbl1 INNER JOIN tbl2 ON tbl1._id = tbl2.bsi where tbl1.fld1 = 1 and tbl2.fld2 = 2",
|
||||
expMask: QueryMask{
|
||||
SelectMask: SelectPartCountStar,
|
||||
FromMask: FromPartJoin,
|
||||
WhereMask: WherePartMultiFieldCondition,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mapper := NewMapper()
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
||||
_, mask, err := mapper.Parse(test.sql)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if mask != test.expMask {
|
||||
t.Fatalf("expected mask: %v, but got: %v", test.expMask, mask)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This thing passes even if join is not implemented.
|
||||
// TODO: make this actually a test
|
||||
func TestSelectJoin(t *testing.T) {
|
||||
tests := []struct {
|
||||
sql string
|
||||
}{
|
||||
{
|
||||
// Count(Intersect(All(),Distinct(Row(grouperid!=null),index='joiner',field='grouperid')))
|
||||
sql: "select count(*) from grouper g INNER JOIN joiner j ON g._id = j.grouperid",
|
||||
},
|
||||
{
|
||||
// Intersect(All(),Distinct(Row(grouperid!=null),index='joiner',field='grouperid'))
|
||||
sql: "select _id from grouper g INNER JOIN joiner j ON g._id = j.grouperid",
|
||||
},
|
||||
{
|
||||
// Intersect(Row(color='red'),Distinct(Row(grouperid!=null),index='joiner',field='grouperid'))
|
||||
sql: "select _id from grouper g INNER JOIN joiner j ON g._id = j.grouperid where g.color = 'red'",
|
||||
},
|
||||
{
|
||||
// Intersect(All(),Distinct(Row(grouperid!=null),index='joiner',field='grouperid'))
|
||||
sql: "select _id from grouper g INNER JOIN joiner j ON g._id = j.grouperid where g.color = 'red' and j.jointype = 2",
|
||||
},
|
||||
}
|
||||
|
||||
mapper := NewMapper()
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
||||
if m, err := mapper.MapSQL(test.sql); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if stmt, ok := m.Statement.(*sqlparser.Select); !ok {
|
||||
t.Fatalf("%s: expected Statement: sqlparser.Select, got %T", test.sql, m.Statement)
|
||||
} else {
|
||||
t.Logf("%+v\n", stmt)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderBy(t *testing.T) {
|
||||
tests := []struct {
|
||||
sql string
|
||||
}{
|
||||
{
|
||||
sql: "select distinct score from grouper order by score asc",
|
||||
},
|
||||
{
|
||||
sql: "select distinct score from grouper order by score desc",
|
||||
},
|
||||
{
|
||||
sql: "select distinct score from grouper order by score asc limit 5",
|
||||
},
|
||||
{
|
||||
sql: "select distinct score from grouper order by score desc limit 5",
|
||||
},
|
||||
}
|
||||
mapper := NewMapper()
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
||||
if m, err := mapper.MapSQL(test.sql); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if stmt, ok := m.Statement.(*sqlparser.Select); !ok {
|
||||
t.Fatalf("%s: expected Statement: sqlparser.Select, got %T", test.sql, m.Statement)
|
||||
} else {
|
||||
t.Logf("%+v\n", stmt)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
399
sql/mask.go
Normal file
399
sql/mask.go
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
// 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 (
|
||||
"strings"
|
||||
|
||||
"vitess.io/vitess/go/vt/sqlparser"
|
||||
)
|
||||
|
||||
type selectPart int
|
||||
|
||||
const (
|
||||
SelectPartDistinct selectPart = 1 << iota
|
||||
SelectPartID
|
||||
SelectPartStar
|
||||
SelectPartField
|
||||
SelectPartFields
|
||||
SelectPartCountStar
|
||||
SelectPartCountField
|
||||
SelectPartCountDistinctField
|
||||
SelectPartMinField
|
||||
SelectPartMaxField
|
||||
SelectPartSumField
|
||||
SelectPartAvgField
|
||||
)
|
||||
|
||||
type fromPart int
|
||||
|
||||
const (
|
||||
FromPartTable fromPart = 1 << iota
|
||||
FromPartTables
|
||||
FromPartJoin
|
||||
)
|
||||
|
||||
type wherePart int
|
||||
|
||||
const (
|
||||
WherePartIDCondition wherePart = 1 << iota
|
||||
WherePartFieldCondition
|
||||
WherePartMultiFieldCondition
|
||||
)
|
||||
|
||||
type groupByPart int
|
||||
|
||||
const (
|
||||
GroupByPartField groupByPart = 1 << iota
|
||||
GroupByPartFields
|
||||
)
|
||||
|
||||
type havingPart int
|
||||
|
||||
const (
|
||||
HavingPartCondition havingPart = 1 << iota
|
||||
)
|
||||
|
||||
type orderByPart int
|
||||
|
||||
const (
|
||||
OrderByPartField orderByPart = 1 << iota
|
||||
OrderByPartFields
|
||||
)
|
||||
|
||||
type limitPart int
|
||||
|
||||
const (
|
||||
LimitPartLimit limitPart = 1 << iota
|
||||
LimitPartOffset
|
||||
)
|
||||
|
||||
type QueryMask struct {
|
||||
SelectMask selectPart
|
||||
FromMask fromPart
|
||||
WhereMask wherePart
|
||||
GroupByMask groupByPart
|
||||
HavingMask havingPart
|
||||
OrderByMask orderByPart
|
||||
LimitMask limitPart
|
||||
}
|
||||
|
||||
func NewQueryMask(sp selectPart, fp fromPart, wp wherePart, gp groupByPart, hp havingPart) QueryMask {
|
||||
qm := QueryMask{}
|
||||
qm.orSelect(sp)
|
||||
qm.orFrom(fp)
|
||||
qm.orWhere(wp)
|
||||
qm.orGroupBy(gp)
|
||||
qm.orHaving(hp)
|
||||
return qm
|
||||
}
|
||||
|
||||
// ApplyFilter returns true if m passes the filter f.
|
||||
// Note: only certain query parts are included; namely,
|
||||
// the orderBy and limit masks are not applied to the
|
||||
// filter.
|
||||
func (m *QueryMask) ApplyFilter(f QueryMask) bool {
|
||||
if m.SelectMask&f.SelectMask != m.SelectMask {
|
||||
return false
|
||||
}
|
||||
if m.FromMask&f.FromMask != m.FromMask {
|
||||
return false
|
||||
}
|
||||
if m.WhereMask&f.WhereMask != m.WhereMask {
|
||||
return false
|
||||
}
|
||||
if m.GroupByMask&f.GroupByMask != m.GroupByMask {
|
||||
return false
|
||||
}
|
||||
if m.HavingMask&f.HavingMask != m.HavingMask {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// orSelect applies the bitwise-OR operation to the selectMask.
|
||||
func (m *QueryMask) orSelect(o selectPart) {
|
||||
m.SelectMask |= o
|
||||
}
|
||||
|
||||
// orFrom applies the bitwise-OR operation to the fromMask.
|
||||
func (m *QueryMask) orFrom(o fromPart) {
|
||||
m.FromMask |= o
|
||||
}
|
||||
|
||||
// orWhere applies the bitwise-OR operation to the whereMask.
|
||||
func (m *QueryMask) orWhere(parts ...wherePart) {
|
||||
for _, o := range parts {
|
||||
m.WhereMask |= o
|
||||
}
|
||||
}
|
||||
|
||||
// orGroupBy applies the bitwise-OR operation to the groupByMask.
|
||||
func (m *QueryMask) orGroupBy(o groupByPart) {
|
||||
m.GroupByMask |= o
|
||||
}
|
||||
|
||||
// orHaving applies the bitwise-OR operation to the havingMask.
|
||||
func (m *QueryMask) orHaving(o havingPart) {
|
||||
m.HavingMask |= o
|
||||
}
|
||||
|
||||
// orOrderBy applies the bitwise-OR operation to the orderByMask.
|
||||
func (m *QueryMask) orOrderBy(o orderByPart) {
|
||||
m.OrderByMask |= o
|
||||
}
|
||||
|
||||
// orLimit applies the bitwise-OR operation to the limitMask.
|
||||
func (m *QueryMask) orLimit(o limitPart) {
|
||||
m.LimitMask |= o
|
||||
}
|
||||
|
||||
// HasSelect returns true if the mask contains a supported select clause.
|
||||
func (m *QueryMask) HasSelect() bool {
|
||||
return m.SelectMask > 0
|
||||
}
|
||||
|
||||
// HasSelectPart returns true if the mask contains the provided select part.
|
||||
func (m *QueryMask) HasSelectPart(p selectPart) bool {
|
||||
return (m.SelectMask & p) > 0
|
||||
}
|
||||
|
||||
// HasFrom returns true if the mask contains a supported from clause.
|
||||
func (m *QueryMask) HasFrom() bool {
|
||||
return m.FromMask > 0
|
||||
}
|
||||
|
||||
// HasWhere returns true if the mask contains a supported where clause.
|
||||
func (m *QueryMask) HasWhere() bool {
|
||||
return m.WhereMask > 0
|
||||
}
|
||||
|
||||
// HasGroupBy returns true if the mask contains a supported group by clause.
|
||||
func (m *QueryMask) HasGroupBy() bool {
|
||||
return m.GroupByMask > 0
|
||||
}
|
||||
|
||||
// HasHaving returns true if the mask contains a supported having clause.
|
||||
func (m *QueryMask) HasHaving() bool {
|
||||
return m.HavingMask > 0
|
||||
}
|
||||
|
||||
// HasOrderBy returns true if the mask contains a supported order by clause.
|
||||
func (m *QueryMask) HasOrderBy() bool {
|
||||
return m.OrderByMask > 0
|
||||
}
|
||||
|
||||
// HasLimit returns true if the mask contains a supported limit clause.
|
||||
func (m *QueryMask) HasLimit() bool {
|
||||
return m.LimitMask > 0
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func MustGenerateMask(sql string) QueryMask {
|
||||
parsed, err := sqlparser.Parse(sql)
|
||||
if err != nil {
|
||||
return QueryMask{}
|
||||
}
|
||||
return GenerateMask(parsed)
|
||||
}
|
||||
|
||||
func GenerateMask(parsed sqlparser.Statement) QueryMask {
|
||||
qm := QueryMask{}
|
||||
|
||||
switch stmt := parsed.(type) {
|
||||
case *sqlparser.Select:
|
||||
if strings.ToLower(strings.TrimSpace(stmt.Distinct)) == "distinct" {
|
||||
qm.orSelect(SelectPartDistinct)
|
||||
}
|
||||
// select parts
|
||||
var fldCount int
|
||||
for _, item := range stmt.SelectExprs {
|
||||
switch expr := item.(type) {
|
||||
case *sqlparser.AliasedExpr:
|
||||
switch colExpr := expr.Expr.(type) {
|
||||
case *sqlparser.ColName:
|
||||
name := colExpr.Name.String()
|
||||
if name == ColID {
|
||||
qm.orSelect(SelectPartID)
|
||||
} else {
|
||||
fldCount++
|
||||
}
|
||||
case *sqlparser.FuncExpr:
|
||||
funcName := FuncName(strings.ToLower(colExpr.Name.String()))
|
||||
switch len(colExpr.Exprs) {
|
||||
case 1:
|
||||
var isStar bool
|
||||
var isField bool
|
||||
var isDistinctField bool
|
||||
switch exp := colExpr.Exprs[0].(type) {
|
||||
case *sqlparser.AliasedExpr:
|
||||
switch exp.Expr.(type) {
|
||||
case *sqlparser.ColName:
|
||||
isField = true
|
||||
isDistinctField = colExpr.Distinct
|
||||
}
|
||||
case *sqlparser.StarExpr:
|
||||
isStar = true
|
||||
}
|
||||
|
||||
switch funcName {
|
||||
case FuncCount:
|
||||
if isStar {
|
||||
qm.orSelect(SelectPartCountStar)
|
||||
} else if isDistinctField {
|
||||
qm.orSelect(SelectPartCountDistinctField)
|
||||
} else if isField {
|
||||
qm.orSelect(SelectPartCountField)
|
||||
}
|
||||
case FuncMin:
|
||||
if isField {
|
||||
qm.orSelect(SelectPartMinField)
|
||||
}
|
||||
case FuncMax:
|
||||
if isField {
|
||||
qm.orSelect(SelectPartMaxField)
|
||||
}
|
||||
case FuncSum:
|
||||
if isField {
|
||||
qm.orSelect(SelectPartSumField)
|
||||
}
|
||||
case FuncAvg:
|
||||
if isField {
|
||||
qm.orSelect(SelectPartAvgField)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *sqlparser.StarExpr:
|
||||
qm.orSelect(SelectPartStar)
|
||||
}
|
||||
}
|
||||
if fldCount == 1 {
|
||||
qm.orSelect(SelectPartField)
|
||||
} else if fldCount > 1 {
|
||||
qm.orSelect(SelectPartFields)
|
||||
}
|
||||
|
||||
// from parts
|
||||
switch len(stmt.From) {
|
||||
case 1:
|
||||
switch stmt.From[0].(type) {
|
||||
case *sqlparser.AliasedTableExpr:
|
||||
qm.orFrom(FromPartTable)
|
||||
case *sqlparser.JoinTableExpr:
|
||||
qm.orFrom(FromPartJoin)
|
||||
}
|
||||
case 2:
|
||||
switch stmt.From[0].(type) {
|
||||
case *sqlparser.AliasedTableExpr:
|
||||
switch stmt.From[1].(type) {
|
||||
case *sqlparser.AliasedTableExpr:
|
||||
qm.orFrom(FromPartTables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// where parts
|
||||
where := stmt.Where
|
||||
if where != nil {
|
||||
switch where.Type {
|
||||
case "where":
|
||||
qm.orWhere(generateWhereMask(where.Expr)...)
|
||||
}
|
||||
}
|
||||
|
||||
// group by parts
|
||||
var groupByFieldCount int
|
||||
for _, item := range stmt.GroupBy {
|
||||
switch item.(type) {
|
||||
case *sqlparser.ColName:
|
||||
groupByFieldCount++
|
||||
}
|
||||
}
|
||||
if groupByFieldCount == 1 {
|
||||
qm.orGroupBy(GroupByPartField)
|
||||
} else if groupByFieldCount > 1 {
|
||||
qm.orGroupBy(GroupByPartFields)
|
||||
}
|
||||
|
||||
// having parts
|
||||
if stmt.Having != nil {
|
||||
qm.orHaving(HavingPartCondition)
|
||||
}
|
||||
|
||||
// order by parts
|
||||
var orderByFieldCount int
|
||||
for _, item := range stmt.OrderBy {
|
||||
switch item.Expr.(type) {
|
||||
case *sqlparser.ColName:
|
||||
orderByFieldCount++
|
||||
}
|
||||
}
|
||||
if orderByFieldCount == 1 {
|
||||
qm.orOrderBy(OrderByPartField)
|
||||
} else if orderByFieldCount > 1 {
|
||||
qm.orOrderBy(OrderByPartFields)
|
||||
}
|
||||
|
||||
// limit parts
|
||||
if stmt.Limit != nil {
|
||||
switch stmt.Limit.Rowcount.(type) {
|
||||
case *sqlparser.SQLVal:
|
||||
qm.orLimit(LimitPartLimit)
|
||||
}
|
||||
switch stmt.Limit.Offset.(type) {
|
||||
case *sqlparser.SQLVal:
|
||||
qm.orLimit(LimitPartOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
return qm
|
||||
}
|
||||
|
||||
// TODO: add more recursion within the comparison operators (left/right parts)
|
||||
func generateWhereMask(e sqlparser.Expr) []wherePart {
|
||||
var wp []wherePart
|
||||
|
||||
switch expr := e.(type) {
|
||||
case *sqlparser.ComparisonExpr:
|
||||
var leftName string
|
||||
if colExpr, ok := expr.Left.(*sqlparser.ColName); ok {
|
||||
leftName = colExpr.Name.String()
|
||||
}
|
||||
switch leftName {
|
||||
case ColID:
|
||||
wp = append(wp, WherePartIDCondition)
|
||||
case "":
|
||||
//
|
||||
default:
|
||||
wp = append(wp, WherePartFieldCondition)
|
||||
}
|
||||
case *sqlparser.RangeCond:
|
||||
wp = append(wp, WherePartFieldCondition)
|
||||
case *sqlparser.AndExpr, *sqlparser.OrExpr:
|
||||
// TODO: we need to recursively ensure that the left/right
|
||||
// sides of these and/or expressions are field-op-val, and
|
||||
// that none of the fields are "_id"
|
||||
// TODO: could we use extractComparison or something like it?
|
||||
wp = append(wp, WherePartMultiFieldCondition)
|
||||
case *sqlparser.NotExpr:
|
||||
wp = append(wp, generateWhereMask(expr.Expr)...)
|
||||
case *sqlparser.IsExpr:
|
||||
wp = append(wp, WherePartFieldCondition)
|
||||
}
|
||||
|
||||
return wp
|
||||
}
|
||||
141
sql/model.go
Normal file
141
sql/model.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/pilosa/pilosa/v2"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedQuery = errors.New("unsupported query")
|
||||
)
|
||||
|
||||
// TODO: what the difference between this and IDIndexColumn?
|
||||
type KeyIndexColumn struct {
|
||||
Index *pilosa.Index
|
||||
text string
|
||||
alias string
|
||||
}
|
||||
|
||||
func NewKeyIndexColumn(index *pilosa.Index, alias string) *KeyIndexColumn {
|
||||
return &KeyIndexColumn{
|
||||
Index: index,
|
||||
text: ColID,
|
||||
alias: alias,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *KeyIndexColumn) Source() string {
|
||||
return "" // TODO
|
||||
}
|
||||
func (i *KeyIndexColumn) Name() string {
|
||||
return i.text
|
||||
}
|
||||
func (i *KeyIndexColumn) Alias() string {
|
||||
if i.alias != "" {
|
||||
return i.alias
|
||||
}
|
||||
return i.Name()
|
||||
}
|
||||
|
||||
type IDIndexColumn struct {
|
||||
Index *pilosa.Index
|
||||
text string
|
||||
alias string
|
||||
}
|
||||
|
||||
func NewIDIndexColumn(index *pilosa.Index, alias string) *IDIndexColumn {
|
||||
return &IDIndexColumn{
|
||||
Index: index,
|
||||
text: ColID,
|
||||
alias: alias,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IDIndexColumn) Source() string {
|
||||
return "" // TODO
|
||||
}
|
||||
func (i *IDIndexColumn) Name() string {
|
||||
return i.text
|
||||
}
|
||||
func (i *IDIndexColumn) Alias() string {
|
||||
if i.alias != "" {
|
||||
return i.alias
|
||||
}
|
||||
return i.Name()
|
||||
}
|
||||
|
||||
type FieldColumn struct {
|
||||
Field *pilosa.Field
|
||||
text string
|
||||
alias string
|
||||
}
|
||||
|
||||
func NewFieldColumn(field *pilosa.Field, alias string) *FieldColumn {
|
||||
return &FieldColumn{
|
||||
Field: field,
|
||||
text: field.Name(),
|
||||
alias: alias,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FieldColumn) Source() string {
|
||||
return ""
|
||||
}
|
||||
func (f *FieldColumn) Name() string {
|
||||
return f.text
|
||||
}
|
||||
func (f *FieldColumn) Alias() string {
|
||||
if f.alias != "" {
|
||||
return f.alias
|
||||
}
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
type FuncColumn struct {
|
||||
Field *pilosa.Field
|
||||
FuncName FuncName
|
||||
alias string
|
||||
}
|
||||
|
||||
func NewFuncColumn(funcName FuncName, field *pilosa.Field, alias string) *FuncColumn {
|
||||
return &FuncColumn{
|
||||
Field: field,
|
||||
FuncName: funcName,
|
||||
alias: alias,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FuncColumn) Source() string {
|
||||
return string(f.FuncName)
|
||||
}
|
||||
|
||||
func (f *FuncColumn) Name() string {
|
||||
fieldName := "*"
|
||||
if f.Field != nil {
|
||||
fieldName = f.Field.Name()
|
||||
}
|
||||
return fmt.Sprintf("%s(%s)", f.FuncName, fieldName)
|
||||
}
|
||||
|
||||
func (f *FuncColumn) Alias() string {
|
||||
if f.alias != "" {
|
||||
return f.alias
|
||||
}
|
||||
return f.Name()
|
||||
}
|
||||
295
sql/query.go
Normal file
295
sql/query.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const timeFormat = "2006-01-02T15:04"
|
||||
|
||||
// LT creates a less than query.
|
||||
func LT(fieldName string, value interface{}) string {
|
||||
return fmt.Sprintf("Row(%s<%s)", fieldName, intOrFloat(value))
|
||||
}
|
||||
|
||||
// LTE creates a less than or equal query.
|
||||
func LTE(fieldName string, value interface{}) string {
|
||||
return fmt.Sprintf("Row(%s<=%s)", fieldName, intOrFloat(value))
|
||||
}
|
||||
|
||||
// GT creates a greater than query.
|
||||
func GT(fieldName string, value interface{}) string {
|
||||
return fmt.Sprintf("Row(%s>%s)", fieldName, intOrFloat(value))
|
||||
}
|
||||
|
||||
// GTE creates a greater than or equal query.
|
||||
func GTE(fieldName string, value interface{}) string {
|
||||
return fmt.Sprintf("Row(%s>=%s)", fieldName, intOrFloat(value))
|
||||
}
|
||||
|
||||
// Equals creates an equals query.
|
||||
func Equals(fieldName string, value interface{}) string {
|
||||
return fmt.Sprintf("Row(%s=%s)", fieldName, intOrFloat(value))
|
||||
}
|
||||
|
||||
// NotEquals creates a not equals query.
|
||||
func NotEquals(fieldName string, value interface{}) string {
|
||||
return fmt.Sprintf("Row(%s!=%s)", fieldName, intOrFloat(value))
|
||||
}
|
||||
|
||||
// NotNull creates a not equal to null query.
|
||||
func NotNull(fieldName string) string {
|
||||
return fmt.Sprintf("Row(%s!=null)", fieldName)
|
||||
}
|
||||
|
||||
// Row query
|
||||
func Row(fieldName string, rowIDOrKey interface{}) (string, error) {
|
||||
rowStr, err := formatIDKeyBool(rowIDOrKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text := fmt.Sprintf("Row(%s=%s)", fieldName, rowStr)
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// RowRange is a Row query with from,to times
|
||||
func RowRange(fieldName string, rowIDOrKey interface{}, start time.Time, end time.Time) (string, error) {
|
||||
rowStr, err := formatIDKeyBool(rowIDOrKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text := fmt.Sprintf("Row(%s=%s,from='%s',to='%s')", fieldName, rowStr, start.Format(timeFormat), end.Format(timeFormat))
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// Union query - see rowOperation
|
||||
func Union(rows ...string) string {
|
||||
return rowOperation("Union", rows...)
|
||||
}
|
||||
|
||||
// Intersect query - see rowOperation
|
||||
func Intersect(rows ...string) string {
|
||||
return rowOperation("Intersect", rows...)
|
||||
}
|
||||
|
||||
// Not query
|
||||
func Not(rows ...string) string {
|
||||
return rowOperation("Not", rows...)
|
||||
}
|
||||
|
||||
// Like creates a Rows query filtered by a pattern.
|
||||
// An underscore ('_') can be used as a placeholder for a single UTF-8 codepoint or a percent sign ('%') can be used as a placeholder for 0 or more codepoints.
|
||||
// All other codepoints in the pattern are matched exactly.
|
||||
func Like(fieldName string, pattern string) string {
|
||||
pattern = strings.ReplaceAll(pattern, `\`, `\\`)
|
||||
pattern = strings.ReplaceAll(pattern, `'`, `\'`)
|
||||
return fmt.Sprintf("UnionRows(Rows(field='%s',like='%s'))", fieldName, pattern)
|
||||
}
|
||||
|
||||
// Between creates a between query.
|
||||
func Between(fieldName string, a interface{}, b interface{}) string {
|
||||
return fmt.Sprintf("Row(%s >< [%s,%s])", fieldName, intOrFloat(a), intOrFloat(b))
|
||||
}
|
||||
|
||||
// Distinct creates a Distinct query.
|
||||
func Distinct(indexName, fieldName string) string {
|
||||
return fmt.Sprintf("Distinct(Row(%s!=null),index='%s',field='%s')", fieldName, indexName, fieldName)
|
||||
}
|
||||
|
||||
// RowDistinct creates a Distinct query with the given row filter.
|
||||
func RowDistinct(indexName, fieldName string, row string) string {
|
||||
return fmt.Sprintf("Distinct(%s,index='%s',field='%s')", row, indexName, fieldName)
|
||||
}
|
||||
|
||||
// Rows creates a Rows query with defaults
|
||||
func Rows(fieldName string) string {
|
||||
return fmt.Sprintf("Rows(field='%s')", fieldName)
|
||||
}
|
||||
|
||||
// RowsLimit creates a Rows query with the given limit
|
||||
func RowsLimit(fieldName string, limit int64) (string, error) {
|
||||
if limit < 0 {
|
||||
return "", errors.New("rows limit must be non-negative")
|
||||
}
|
||||
text := fmt.Sprintf("Rows(field='%s',limit=%d)", fieldName, limit)
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// All creates an All query.
|
||||
// Returns the set columns with existence true.
|
||||
func All() string {
|
||||
return "All()"
|
||||
}
|
||||
|
||||
// Count creates a Count query.
|
||||
// Returns the number of set columns in the ROW_CALL passed in.
|
||||
func Count(rowCall string) string {
|
||||
return fmt.Sprintf("Count(%s)", rowCall)
|
||||
}
|
||||
|
||||
// Sum creates a sum query.
|
||||
func Sum(fieldName string, row string) string {
|
||||
return valQuery(fieldName, "Sum", row)
|
||||
}
|
||||
|
||||
// Min creates a min query.
|
||||
func Min(fieldName string, row string) string {
|
||||
return valQuery(fieldName, "Min", row)
|
||||
}
|
||||
|
||||
// Max creates a max query.
|
||||
func Max(fieldName string, row string) string {
|
||||
return valQuery(fieldName, "Max", row)
|
||||
}
|
||||
|
||||
// TopN creates a TopN query with the given item count.
|
||||
// Returns the id and count of the top n rows (by count of columns) in the field.
|
||||
func TopN(fieldName string, n uint64) string {
|
||||
return fmt.Sprintf("TopN(%s,n=%d)", fieldName, n)
|
||||
}
|
||||
|
||||
// RowTopN creates a TopN query with the given item count and row.
|
||||
// This variant supports customizing the row query.
|
||||
func RowTopN(fieldName string, n uint64, row string) string {
|
||||
return fmt.Sprintf("TopN(%s,%s,n=%d)", fieldName, row, n)
|
||||
}
|
||||
|
||||
// GroupByBase creates a GroupBy query with the given functional options.
|
||||
func GroupByBase(rows []string, limit int64, filter, aggregate, having string) (string, error) {
|
||||
if len(rows) == 0 {
|
||||
return "", errors.New("there should be at least one rows query")
|
||||
}
|
||||
if limit < 0 {
|
||||
return "", errors.New("limit must be non-negative")
|
||||
}
|
||||
|
||||
// rows
|
||||
text := fmt.Sprintf("GroupBy(%s", strings.Join(rows, ","))
|
||||
|
||||
// limit
|
||||
if limit > 0 {
|
||||
text += fmt.Sprintf(",limit=%d", limit)
|
||||
}
|
||||
|
||||
// filter
|
||||
if filter != "" {
|
||||
text += fmt.Sprintf(",filter=%s", filter)
|
||||
}
|
||||
|
||||
// aggregate
|
||||
if aggregate != "" {
|
||||
text += fmt.Sprintf(",aggregate=%s", aggregate)
|
||||
}
|
||||
|
||||
// having
|
||||
if having != "" {
|
||||
text += fmt.Sprintf(",having=%s", having)
|
||||
}
|
||||
|
||||
text += ")"
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// Limit creates a limit query.
|
||||
func Limit(row string, limit uint, offset uint) string {
|
||||
return fmt.Sprintf("Limit(%s, limit=%d, offset=%d)", row, limit, offset)
|
||||
}
|
||||
|
||||
// Offset creates a limit query but only with an offset.
|
||||
func Offset(row string, offset uint) string {
|
||||
return fmt.Sprintf("Limit(%s, offset=%d)", row, offset)
|
||||
}
|
||||
|
||||
// ConstRow creates a query value that uses a list of columns in place of a Row query.
|
||||
func ConstRow(ids ...interface{}) string {
|
||||
if ids == nil {
|
||||
ids = []interface{}{}
|
||||
}
|
||||
data, _ := json.Marshal(ids)
|
||||
return fmt.Sprintf("ConstRow(columns=%s)", data)
|
||||
}
|
||||
|
||||
// Extract creates an Extract query.
|
||||
// It accepts a bitmap query to select columns and a list of fields to select rows.
|
||||
func Extract(rowCall string, fields ...string) string {
|
||||
var rowsCall string
|
||||
for _, r := range fields {
|
||||
rowsCall += "," + fmt.Sprintf("Rows(%s)", r)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Extract(%s%s)", rowCall, rowsCall)
|
||||
}
|
||||
|
||||
func valQuery(fieldName string, op string, row string) string {
|
||||
if row != "" {
|
||||
row += ","
|
||||
}
|
||||
return fmt.Sprintf("%s(%sfield='%s')", op, row, fieldName)
|
||||
}
|
||||
|
||||
func rowOperation(name string, rows ...string) string {
|
||||
return fmt.Sprintf("%s(%s)", name, strings.Join(rows, ","))
|
||||
}
|
||||
|
||||
func formatIDKeyBool(idKeyBool interface{}) (string, error) {
|
||||
if b, ok := idKeyBool.(bool); ok {
|
||||
return strconv.FormatBool(b), nil
|
||||
}
|
||||
if flt, ok := idKeyBool.(float64); ok {
|
||||
return fmt.Sprintf("%f", flt), nil
|
||||
}
|
||||
return formatIDKey(idKeyBool)
|
||||
}
|
||||
|
||||
func formatIDKey(idKey interface{}) (string, error) {
|
||||
switch v := idKey.(type) {
|
||||
case uint:
|
||||
return strconv.FormatUint(uint64(v), 10), nil
|
||||
case uint32:
|
||||
return strconv.FormatUint(uint64(v), 10), nil
|
||||
case uint64:
|
||||
return strconv.FormatUint(v, 10), nil
|
||||
case int:
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10), nil
|
||||
case string:
|
||||
v = strings.ReplaceAll(v, `\`, `\\`)
|
||||
return fmt.Sprintf(`'%s'`, strings.ReplaceAll(v, `'`, `\'`)), nil
|
||||
default:
|
||||
return "", errors.Errorf("id/key is not a string or integer type: %#v", idKey)
|
||||
}
|
||||
}
|
||||
|
||||
func intOrFloat(value interface{}) string {
|
||||
switch value.(type) {
|
||||
case float64, float32:
|
||||
// In order to test expected values, we set the precision
|
||||
// to 8. TODO: It's likely we'll need to address this
|
||||
// at some point.
|
||||
return fmt.Sprintf("%.8f", value)
|
||||
default:
|
||||
return fmt.Sprintf("%d", value)
|
||||
}
|
||||
}
|
||||
500
sql/reduce.go
Normal file
500
sql/reduce.go
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
// 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
|
||||
}
|
||||
120
sql/reduce_test.go
Normal file
120
sql/reduce_test.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
pproto "github.com/pilosa/pilosa/v2/proto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestHeaderAssignment(t *testing.T) {
|
||||
abcdHdrs := []*pproto.ColumnInfo{
|
||||
{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "d"},
|
||||
}
|
||||
tests := []struct {
|
||||
cols []Column
|
||||
hdrs []*pproto.ColumnInfo
|
||||
expPlacement []uint
|
||||
expLabels []string
|
||||
expErr error
|
||||
}{
|
||||
{
|
||||
cols: []Column{
|
||||
NewBasicColumn("a", "namea", ""),
|
||||
},
|
||||
hdrs: abcdHdrs,
|
||||
expPlacement: []uint{0},
|
||||
expLabels: []string{"namea"},
|
||||
},
|
||||
{
|
||||
cols: []Column{
|
||||
NewBasicColumn("", "a", ""),
|
||||
},
|
||||
hdrs: abcdHdrs,
|
||||
expPlacement: []uint{0},
|
||||
expLabels: []string{"a"},
|
||||
},
|
||||
{
|
||||
cols: []Column{
|
||||
NewBasicColumn("", "a", "aliasa"),
|
||||
},
|
||||
hdrs: abcdHdrs,
|
||||
expPlacement: []uint{0},
|
||||
expLabels: []string{"aliasa"},
|
||||
},
|
||||
{
|
||||
cols: []Column{
|
||||
NewBasicColumn("", "a", "aliasa"),
|
||||
NewBasicColumn("c", "namec", ""),
|
||||
},
|
||||
hdrs: abcdHdrs,
|
||||
expPlacement: []uint{0, 2},
|
||||
expLabels: []string{"aliasa", "namec"},
|
||||
},
|
||||
{
|
||||
cols: []Column{
|
||||
NewBasicColumn("d", "c", "aliasd"),
|
||||
NewBasicColumn("b", "nameb", ""),
|
||||
},
|
||||
hdrs: abcdHdrs,
|
||||
expPlacement: []uint{3, 1},
|
||||
expLabels: []string{"aliasd", "nameb"},
|
||||
},
|
||||
// Errors
|
||||
{
|
||||
cols: []Column{
|
||||
NewBasicColumn("", "x", ""),
|
||||
},
|
||||
hdrs: abcdHdrs,
|
||||
expErr: ErrFieldNotInHeaders,
|
||||
},
|
||||
{
|
||||
cols: []Column{
|
||||
NewBasicColumn("", "a", ""),
|
||||
NewBasicColumn("", "b", ""),
|
||||
NewBasicColumn("", "c", ""),
|
||||
NewBasicColumn("", "d", ""),
|
||||
NewBasicColumn("", "e", ""),
|
||||
},
|
||||
hdrs: abcdHdrs,
|
||||
expErr: ErrIncompleteHeaders,
|
||||
},
|
||||
}
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
||||
placement, labels, err := headerAssignment(test.cols, test.hdrs)
|
||||
|
||||
if test.expErr == nil {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
if test.expErr != errors.Cause(err) {
|
||||
t.Fatalf("expected error: %v, but got: %v", test.expErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(placement, test.expPlacement) {
|
||||
t.Fatalf("expected placement: %v, but got: %v", test.expPlacement, placement)
|
||||
} else if !reflect.DeepEqual(labels, test.expLabels) {
|
||||
t.Fatalf("expected labels: %v, but got: %v", test.expLabels, labels)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
162
sql/router.go
Normal file
162
sql/router.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
// 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
|
||||
|
||||
type router struct {
|
||||
direct map[QueryMask]handler
|
||||
filters []maskFilter
|
||||
}
|
||||
|
||||
type maskFilter struct {
|
||||
optional QueryMask
|
||||
required []QueryMask
|
||||
handler handler
|
||||
}
|
||||
|
||||
func newRouter() *router {
|
||||
selectRouter := &router{
|
||||
direct: make(map[QueryMask]handler),
|
||||
}
|
||||
|
||||
selectRouter.addFilter(
|
||||
NewQueryMask(
|
||||
SelectPartID|SelectPartStar|SelectPartField|SelectPartFields,
|
||||
FromPartTable,
|
||||
WherePartFieldCondition|WherePartMultiFieldCondition|WherePartIDCondition,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
[]QueryMask{},
|
||||
handlerSelectFieldsFromTableWhere{},
|
||||
)
|
||||
////
|
||||
selectRouter.addRoute("select distinct fld from tbl", handlerSelectDistinctFromTable{})
|
||||
////
|
||||
selectRouter.addFilter(
|
||||
NewQueryMask(
|
||||
SelectPartCountStar|SelectPartCountField|SelectPartCountDistinctField,
|
||||
FromPartTable,
|
||||
WherePartFieldCondition|WherePartMultiFieldCondition|WherePartIDCondition,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
[]QueryMask{},
|
||||
handlerSelectCountFromTableWhere{},
|
||||
)
|
||||
////
|
||||
selectRouter.addFilter(
|
||||
NewQueryMask(
|
||||
SelectPartMinField|SelectPartMaxField|SelectPartSumField|SelectPartAvgField,
|
||||
FromPartTable,
|
||||
WherePartFieldCondition|WherePartMultiFieldCondition|WherePartIDCondition,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
[]QueryMask{},
|
||||
handlerSelectFuncFromTableWhere{},
|
||||
)
|
||||
////
|
||||
groupByOptional := NewQueryMask(
|
||||
SelectPartField|SelectPartFields|SelectPartCountStar|SelectPartSumField,
|
||||
FromPartTable,
|
||||
WherePartFieldCondition, // TODO: this can probably handle fields as well
|
||||
GroupByPartField|GroupByPartFields,
|
||||
HavingPartCondition,
|
||||
)
|
||||
selectRouter.addFilter(
|
||||
groupByOptional,
|
||||
[]QueryMask{NewQueryMask(0, 0, 0, GroupByPartField, 0)},
|
||||
handlerSelectGroupBy{},
|
||||
)
|
||||
selectRouter.addFilter(
|
||||
groupByOptional,
|
||||
[]QueryMask{NewQueryMask(0, 0, 0, GroupByPartFields, 0)},
|
||||
handlerSelectGroupBy{},
|
||||
)
|
||||
selectRouter.addRoute("select fld, count(fld) from tbl group by fld", handlerSelectGroupBy{})
|
||||
selectRouter.addRoute("select fld1, count(fld1) from tbl where fld2=1 group by fld1", handlerSelectGroupBy{})
|
||||
|
||||
selectRouter.addRoute("select count(*) from tbl1 INNER JOIN tbl2 ON tbl1._id = tbl2.bsi", handlerSelectJoin{})
|
||||
selectRouter.addRoute("select _id from tbl1 INNER JOIN tbl2 ON tbl1._id = tbl2.bsi", handlerSelectJoin{})
|
||||
selectRouter.addRoute("select _id from tbl1 INNER JOIN tbl2 ON tbl1._id = tbl2.bsi where fld1=1", handlerSelectJoin{})
|
||||
selectRouter.addRoute("select _id from tbl1 INNER JOIN tbl2 ON tbl1._id = tbl2.bsi where fld1=1 and fld2=2", handlerSelectJoin{})
|
||||
|
||||
return selectRouter
|
||||
}
|
||||
|
||||
func (r *router) addRoute(sql string, handler handler) {
|
||||
r.direct[MustGenerateMask(sql)] = handler
|
||||
}
|
||||
|
||||
func (r *router) addFilter(opt QueryMask, req []QueryMask, handler handler) {
|
||||
mf := maskFilter{
|
||||
optional: opt,
|
||||
required: req,
|
||||
handler: handler,
|
||||
}
|
||||
r.filters = append(r.filters, mf)
|
||||
}
|
||||
|
||||
func (r *router) handler(qm QueryMask) handler {
|
||||
// First, check for a direct mapping.
|
||||
// Zero out the orderBy and limit mask, because
|
||||
// those are not specific to the query processing.
|
||||
zm := QueryMask{
|
||||
SelectMask: qm.SelectMask,
|
||||
FromMask: qm.FromMask,
|
||||
WhereMask: qm.WhereMask,
|
||||
GroupByMask: qm.GroupByMask,
|
||||
HavingMask: qm.HavingMask,
|
||||
}
|
||||
if h, ok := r.direct[zm]; ok {
|
||||
return h
|
||||
}
|
||||
for _, mf := range r.filters {
|
||||
if applyMaskFilter(&qm, mf) {
|
||||
return mf.handler
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyMaskFilter returns true if m passes the filter mf.
|
||||
// Note: only certain query parts are included; namely,
|
||||
// the orderBy and limit masks are not applied to the
|
||||
// filter. A mask can satisfy any part of the optional
|
||||
// filter to pass through, but it MUST satisfy all parts
|
||||
// of the required filter.
|
||||
func applyMaskFilter(m *QueryMask, mf maskFilter) bool {
|
||||
if !m.ApplyFilter(mf.optional) {
|
||||
return false
|
||||
}
|
||||
for _, req := range mf.required {
|
||||
if m.SelectMask&req.SelectMask != req.SelectMask {
|
||||
return false
|
||||
}
|
||||
if m.FromMask&req.FromMask != req.FromMask {
|
||||
return false
|
||||
}
|
||||
if m.WhereMask&req.WhereMask != req.WhereMask {
|
||||
return false
|
||||
}
|
||||
if m.GroupByMask&req.GroupByMask != req.GroupByMask {
|
||||
return false
|
||||
}
|
||||
if m.HavingMask&req.HavingMask != req.HavingMask {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
792
sql/select.go
Normal file
792
sql/select.go
Normal file
|
|
@ -0,0 +1,792 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pilosa/pilosa/v2"
|
||||
"github.com/pilosa/pilosa/v2/pql"
|
||||
pproto "github.com/pilosa/pilosa/v2/proto"
|
||||
"github.com/pkg/errors"
|
||||
"vitess.io/vitess/go/vt/sqlparser"
|
||||
)
|
||||
|
||||
// SelectHandler executes SQL select statements
|
||||
type SelectHandler struct {
|
||||
api *pilosa.API
|
||||
router *router
|
||||
}
|
||||
|
||||
// NewSelectHandler constructor
|
||||
func NewSelectHandler(api *pilosa.API) *SelectHandler {
|
||||
return &SelectHandler{
|
||||
api: api,
|
||||
router: newRouter(),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle executes mapped SQL
|
||||
func (s *SelectHandler) Handle(ctx context.Context, mapped *MappedSQL) (pproto.StreamClient, error) {
|
||||
mr, err := s.mapSelect(ctx, mapped.Statement.(*sqlparser.Select), mapped.Mask)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "mapping select")
|
||||
}
|
||||
return s.execMappingResult(ctx, mr)
|
||||
}
|
||||
|
||||
func (s *SelectHandler) mapSelect(ctx context.Context, selectStmt *sqlparser.Select, qm QueryMask) (*MappingResult, error) {
|
||||
// Get the handler for this query mask.
|
||||
handler := s.router.handler(qm)
|
||||
if handler == nil {
|
||||
return nil, ErrUnsupportedQuery
|
||||
}
|
||||
indexFunc := func(indexName string) *pilosa.Index {
|
||||
idx, err := s.api.Index(ctx, indexName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
mr, err := handler.Apply(selectStmt, qm, indexFunc)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "handling")
|
||||
}
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
func (s *SelectHandler) execMappingResult(ctx context.Context, mr *MappingResult) (stream pproto.StreamClient, err error) {
|
||||
if mr.Query == "" {
|
||||
return nil, errors.New("no pql query created")
|
||||
}
|
||||
|
||||
fmt.Println("PQL:", mr.Query)
|
||||
resp, err := s.api.Query(ctx, &pilosa.QueryRequest{Index: mr.IndexName, Query: mr.Query})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "doing pql query")
|
||||
}
|
||||
res := resp.Results[0]
|
||||
|
||||
// TODO: synchronize this properly somehow.
|
||||
// It would probbably help to get rid of the streaming too.
|
||||
respRows := pproto.NewRowBuffer(0)
|
||||
switch res := res.(type) {
|
||||
case pproto.ToRowser:
|
||||
go func() {
|
||||
if err := res.ToRows(respRows.Send); err != nil {
|
||||
respRows.Send(pproto.Error(err)) //nolint:errcheck
|
||||
} else {
|
||||
_ = respRows.Send(pproto.EOF) //nolint:errcheck
|
||||
}
|
||||
}()
|
||||
case []pilosa.GroupCount:
|
||||
go func() {
|
||||
if err := pilosa.GroupCounts(res).ToRows(respRows.Send); err != nil {
|
||||
respRows.Send(pproto.Error(err)) //nolint:errcheck
|
||||
} else {
|
||||
respRows.Send(pproto.EOF) //nolint:errcheck
|
||||
}
|
||||
}()
|
||||
case uint64:
|
||||
go func() {
|
||||
respRows.Send(&pproto.RowResponse{ //nolint:errcheck
|
||||
Headers: []*pproto.ColumnInfo{
|
||||
{
|
||||
Name: "count",
|
||||
Datatype: "uint64",
|
||||
},
|
||||
},
|
||||
Columns: []*pproto.ColumnResponse{
|
||||
{
|
||||
ColumnVal: &pproto.ColumnResponse_Uint64Val{
|
||||
Uint64Val: res,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
respRows.Send(pproto.EOF) //nolint:errcheck
|
||||
}()
|
||||
case bool:
|
||||
go func() {
|
||||
respRows.Send(&pproto.RowResponse{ //nolint:errcheck
|
||||
Headers: []*pproto.ColumnInfo{
|
||||
{
|
||||
Name: "result",
|
||||
Datatype: "bool",
|
||||
},
|
||||
},
|
||||
Columns: []*pproto.ColumnResponse{
|
||||
{
|
||||
ColumnVal: &pproto.ColumnResponse_BoolVal{
|
||||
BoolVal: res,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
respRows.Send(pproto.EOF) //nolint:errcheck
|
||||
}()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported result type %T", res)
|
||||
}
|
||||
|
||||
// Apply Reducers
|
||||
result := respRows
|
||||
for _, red := range mr.Reducers {
|
||||
out := pproto.NewRowBuffer(0)
|
||||
|
||||
// Run Reducers asyncronously.
|
||||
// TODO: stop swallowing this error.
|
||||
// TODO: does this need an EOF as input?
|
||||
go red.Reduce(result, out) //nolint:errcheck
|
||||
|
||||
result = out
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type MappingResult struct {
|
||||
IndexName string
|
||||
ColumnIDs []uint64
|
||||
ColumnKeys []string
|
||||
FieldFilters []string
|
||||
Limit uint64
|
||||
Offset uint64
|
||||
Query string
|
||||
Header []Column
|
||||
Reducers []Reducer
|
||||
}
|
||||
|
||||
func (mr *MappingResult) addReducer(r Reducer) {
|
||||
mr.Reducers = append(mr.Reducers, r)
|
||||
}
|
||||
|
||||
type SelectProperties struct {
|
||||
Index *pilosa.Index
|
||||
Fields []Column
|
||||
Features selectFeatures
|
||||
WherePQL string
|
||||
WhereIDs []uint64
|
||||
WhereKeys []string
|
||||
Offset uint
|
||||
Limit uint
|
||||
GroupByFieldNames []string
|
||||
Having *HavingClause
|
||||
}
|
||||
|
||||
type selectFunc struct {
|
||||
funcName FuncName
|
||||
field *pilosa.Field
|
||||
}
|
||||
|
||||
type selectFeatures struct {
|
||||
HasRowAttrs bool
|
||||
HasColAttrs bool
|
||||
funcs []selectFunc
|
||||
}
|
||||
|
||||
type HavingClause struct {
|
||||
Subj string
|
||||
Cond pql.Condition
|
||||
}
|
||||
|
||||
type handler interface {
|
||||
Apply(*sqlparser.Select, QueryMask, func(string) *pilosa.Index) (*MappingResult, error)
|
||||
}
|
||||
|
||||
// handlerSelectFieldsFromTable: Inspect()
|
||||
type handlerSelectFieldsFromTableWhere struct{}
|
||||
|
||||
func (h handlerSelectFieldsFromTableWhere) Apply(stmt *sqlparser.Select, qm QueryMask, indexFunc func(string) *pilosa.Index) (*MappingResult, error) {
|
||||
indexName, err := extractIndexName(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "extracting index name")
|
||||
}
|
||||
index := indexFunc(indexName)
|
||||
if index == nil {
|
||||
return nil, errors.WithMessage(pilosa.ErrIndexNotFound, indexName)
|
||||
}
|
||||
|
||||
var whereQuery string
|
||||
if qm.HasWhere() {
|
||||
whereQuery, err = extractWhere(index, stmt.Where.Expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
whereQuery = "All()"
|
||||
}
|
||||
|
||||
selectFields, _, err := extractSelectFields(index, stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting select fields")
|
||||
}
|
||||
|
||||
var fields []string
|
||||
for _, fld := range selectFields {
|
||||
if _, ok := fld.(*StarColumn); ok {
|
||||
pflds := index.Fields()
|
||||
fields = []string{"_id"}
|
||||
for _, f := range pflds {
|
||||
name := f.Name()
|
||||
if strings.HasPrefix(name, "_") {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, name)
|
||||
}
|
||||
break
|
||||
}
|
||||
fields = append(fields, fld.Name())
|
||||
}
|
||||
for i, fld := range fields {
|
||||
if fld == "_id" && i != 0 {
|
||||
return nil, errors.New("_id can only be the first field in a select")
|
||||
}
|
||||
}
|
||||
|
||||
limit, offset, err := extractLimitOffset(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting limit")
|
||||
}
|
||||
|
||||
orderByFlds, orderByDirs, err := extractOrderBy(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting order by")
|
||||
}
|
||||
|
||||
mr := &MappingResult{
|
||||
IndexName: indexName,
|
||||
//FieldFilters: fields,
|
||||
Header: selectFields,
|
||||
}
|
||||
|
||||
// TODO: assign headers
|
||||
mr.addReducer(NewAssignHeadersReducer(selectFields))
|
||||
|
||||
// TODO: If both order and limit/offset are required, then
|
||||
// we can't supply limit/offset to the InspectRequest; we
|
||||
// have to get all records, which we don't want to do on
|
||||
// a large data set. We need to come up with a better
|
||||
// way to handle that situation.
|
||||
switch {
|
||||
case qm.HasOrderBy():
|
||||
mr.addReducer(NewOrderByReducer(orderByFlds, orderByDirs, limit, offset))
|
||||
case limit != 0:
|
||||
whereQuery = Limit(whereQuery, limit, offset)
|
||||
case offset != 0:
|
||||
whereQuery = Offset(whereQuery, offset)
|
||||
}
|
||||
|
||||
if len(fields) > 0 && fields[0] == "_id" {
|
||||
fields = fields[1:]
|
||||
}
|
||||
mr.Query = Extract(whereQuery, fields...)
|
||||
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// handlerSelectDistinctFromTable: Rows, Rows(limit): select distinct fld from tbl
|
||||
type handlerSelectDistinctFromTable struct{}
|
||||
|
||||
func (h handlerSelectDistinctFromTable) Apply(stmt *sqlparser.Select, qm QueryMask, indexFunc func(string) *pilosa.Index) (*MappingResult, error) {
|
||||
indexName, err := extractIndexName(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "extracting index name")
|
||||
}
|
||||
index := indexFunc(indexName)
|
||||
if index == nil {
|
||||
return nil, errors.WithMessage(pilosa.ErrIndexNotFound, indexName)
|
||||
}
|
||||
|
||||
selectFields, _, err := extractSelectFields(index, stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting select fields")
|
||||
}
|
||||
|
||||
fieldCol, ok := selectFields[0].(*FieldColumn)
|
||||
if !ok {
|
||||
return nil, errors.New("distinct requires a valid field column")
|
||||
}
|
||||
|
||||
limit, offset, err := extractLimitOffset(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting limit")
|
||||
}
|
||||
|
||||
orderByFlds, orderByDirs, err := extractOrderBy(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting order by")
|
||||
}
|
||||
|
||||
// Determine the type of the field needing distinct.
|
||||
// If the pilosa field is type int, handle it as a Distinct() query.
|
||||
// Otherwise, use Rows()
|
||||
// TODO: ensure this works for all field types (bool, time, etc).
|
||||
var qo string
|
||||
if fieldCol.Field.Type() == pilosa.FieldTypeInt {
|
||||
qo = Distinct(fieldCol.Field.Index(), fieldCol.Field.Name())
|
||||
} else {
|
||||
if !qm.HasOrderBy() && limit > 0 {
|
||||
if qo, err = RowsLimit(fieldCol.Field.Name(), int64(limit)); err != nil {
|
||||
return nil, errors.Wrap(err, "creating Rows query")
|
||||
}
|
||||
} else {
|
||||
qo = Rows(fieldCol.Field.Name())
|
||||
}
|
||||
}
|
||||
|
||||
mr := &MappingResult{
|
||||
IndexName: indexName,
|
||||
Header: selectFields,
|
||||
Query: qo,
|
||||
}
|
||||
|
||||
mr.addReducer(NewAssignHeadersReducer(selectFields))
|
||||
if qm.HasOrderBy() {
|
||||
mr.addReducer(NewOrderByReducer(orderByFlds, orderByDirs, limit, offset))
|
||||
} else {
|
||||
mr.addReducer(NewLimitReducer(limit, offset))
|
||||
}
|
||||
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// handlerSelectCountFromTableWhere: Count()
|
||||
type handlerSelectCountFromTableWhere struct{}
|
||||
|
||||
func (h handlerSelectCountFromTableWhere) Apply(stmt *sqlparser.Select, qm QueryMask, indexFunc func(string) *pilosa.Index) (*MappingResult, error) {
|
||||
var qo string
|
||||
var reducers []Reducer
|
||||
|
||||
indexName, err := extractIndexName(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "extracting index name")
|
||||
}
|
||||
index := indexFunc(indexName)
|
||||
if index == nil {
|
||||
return nil, errors.WithMessage(pilosa.ErrIndexNotFound, indexName)
|
||||
}
|
||||
|
||||
selectFields, features, err := extractSelectFields(index, stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting select fields")
|
||||
}
|
||||
|
||||
var wherePQL string
|
||||
if stmt.Where != nil {
|
||||
wherePQL, err = extractWhere(index, stmt.Where.Expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
wherePQL = All()
|
||||
}
|
||||
|
||||
funcs := features.funcs
|
||||
if len(funcs) != 1 {
|
||||
return nil, errors.New("handler does not support multiple functions")
|
||||
} else if funcs[0].funcName != FuncCount {
|
||||
return nil, errors.Errorf("handler expected func: %s", FuncCount)
|
||||
}
|
||||
|
||||
if funcs[0].field == nil {
|
||||
qo = Count(wherePQL)
|
||||
} else {
|
||||
// TODO: add the Distinct (for Int fields) here (like we do in handlerSelectDistinctFromTable)
|
||||
qo = Rows(funcs[0].field.Name())
|
||||
reducers = append(reducers, &CountIDReducer{})
|
||||
}
|
||||
mr := &MappingResult{
|
||||
IndexName: indexName,
|
||||
Header: selectFields,
|
||||
Query: qo,
|
||||
Reducers: reducers,
|
||||
}
|
||||
|
||||
mr.addReducer(NewAssignHeadersReducer(selectFields))
|
||||
// NOTE: limit and order by don't make sense in this handler
|
||||
// because it just returns a single row.
|
||||
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// handlerSelectFuncFromTableWhere: min(), max(), sum(), avg()
|
||||
type handlerSelectFuncFromTableWhere struct{}
|
||||
|
||||
func (h handlerSelectFuncFromTableWhere) Apply(stmt *sqlparser.Select, qm QueryMask, indexFunc func(string) *pilosa.Index) (*MappingResult, error) {
|
||||
var qo string
|
||||
|
||||
indexName, err := extractIndexName(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "extracting index name")
|
||||
}
|
||||
index := indexFunc(indexName)
|
||||
if index == nil {
|
||||
return nil, errors.WithMessage(pilosa.ErrIndexNotFound, indexName)
|
||||
}
|
||||
|
||||
selectFields, features, err := extractSelectFields(index, stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting select fields")
|
||||
}
|
||||
|
||||
funcs := features.funcs
|
||||
if len(funcs) != 1 {
|
||||
return nil, errors.New("handler does not support multiple functions")
|
||||
}
|
||||
|
||||
funcField := funcs[0].field
|
||||
if funcField == nil {
|
||||
return nil, errors.New("function contains no field")
|
||||
}
|
||||
|
||||
var wherePQL string
|
||||
if qm.HasWhere() {
|
||||
wherePQL, err = extractWhere(index, stmt.Where.Expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
limit, offset, err := extractLimitOffset(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting limit offset")
|
||||
}
|
||||
|
||||
orderByFlds, orderByDirs, err := extractOrderBy(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting order by")
|
||||
}
|
||||
|
||||
switch funcs[0].funcName {
|
||||
case FuncMin:
|
||||
qo = Min(funcField.Name(), wherePQL)
|
||||
case FuncMax:
|
||||
qo = Max(funcField.Name(), wherePQL)
|
||||
case FuncAvg:
|
||||
fallthrough
|
||||
case FuncSum:
|
||||
qo = Sum(funcField.Name(), wherePQL)
|
||||
}
|
||||
|
||||
mr := &MappingResult{
|
||||
IndexName: indexName,
|
||||
Header: selectFields,
|
||||
Query: qo,
|
||||
}
|
||||
|
||||
mr.addReducer(NewValCountFuncReducer(funcs[0].funcName))
|
||||
mr.addReducer(NewAssignHeadersReducer(selectFields))
|
||||
if qm.HasOrderBy() {
|
||||
mr.addReducer(NewOrderByReducer(orderByFlds, orderByDirs, limit, offset))
|
||||
} else {
|
||||
mr.addReducer(NewLimitReducer(limit, offset))
|
||||
}
|
||||
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// handlerSelectGroupBy: GroupBy
|
||||
type handlerSelectGroupBy struct{}
|
||||
|
||||
func (h handlerSelectGroupBy) Apply(stmt *sqlparser.Select, qm QueryMask, indexFunc func(string) *pilosa.Index) (*MappingResult, error) {
|
||||
var qo string
|
||||
|
||||
indexName, err := extractIndexName(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "extracting index name")
|
||||
}
|
||||
index := indexFunc(indexName)
|
||||
if index == nil {
|
||||
return nil, errors.WithMessage(pilosa.ErrIndexNotFound, indexName)
|
||||
}
|
||||
|
||||
selectFields, features, err := extractSelectFields(index, stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting select fields")
|
||||
}
|
||||
|
||||
orderByFlds, orderByDirs, err := extractOrderBy(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting order by")
|
||||
}
|
||||
|
||||
// If the query can be supported by TopN,
|
||||
// i.e. if it's of the form:
|
||||
// select fld, count(fld) as cnt from tbl group by fld order by cnt desc limit 1
|
||||
// select fld, count(fld) as cnt from tbl where fld2=1 group by fld order by cnt desc limit 1
|
||||
// then redirect it to handlerSelectIDCountFromTable.
|
||||
// Otherwise, handle it as a normal GroupBy query.
|
||||
// TODO: this level of inspection on the query needs to be built into
|
||||
// an official query planner. The existing solution, which uses very
|
||||
// broad masks to route the query to specific handlers, doesn't do
|
||||
// this kind of finer-grain inspection of, for example, the order by
|
||||
// fields themselves.
|
||||
if func() bool {
|
||||
if !qm.HasLimit() {
|
||||
return false
|
||||
}
|
||||
if len(orderByFlds) != 1 {
|
||||
return false
|
||||
}
|
||||
if orderByDirs[0] != "desc" {
|
||||
return false
|
||||
}
|
||||
if qm == MustGenerateMask("select fld, count(fld) from tbl group by fld order by cnt limit 1") ||
|
||||
qm == MustGenerateMask("select fld, count(fld) from tbl where fld=1 group by fld order by cnt limit 1") {
|
||||
// Check that the order-by field is the count field.
|
||||
for i := range selectFields {
|
||||
if s, ok := selectFields[i].(*FuncColumn); !ok {
|
||||
continue
|
||||
} else if s.FuncName == FuncCount && s.Alias() == orderByFlds[0] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}() {
|
||||
return handlerSelectIDCountFromTable{}.Apply(stmt, qm, indexFunc)
|
||||
}
|
||||
|
||||
groupByFieldNames, err := extractGroupByFieldNames(stmt.GroupBy)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting group by fields")
|
||||
}
|
||||
|
||||
having, err := extractHavingClause(stmt.Having)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting having clause")
|
||||
}
|
||||
|
||||
rowsQueries := []string{}
|
||||
for _, fieldName := range groupByFieldNames {
|
||||
field := index.Field(fieldName)
|
||||
rowsQueries = append(rowsQueries, Rows(field.Name()))
|
||||
}
|
||||
|
||||
var wherePQL string
|
||||
if stmt.Where != nil {
|
||||
wherePQL, err = extractWhere(index, stmt.Where.Expr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting where")
|
||||
}
|
||||
}
|
||||
|
||||
limit, offset, err := extractLimitOffset(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting limit offset")
|
||||
}
|
||||
|
||||
// Group by queries can any combination of count() and sum()
|
||||
// in the select fields.
|
||||
var idxSum int = -1
|
||||
funcs := features.funcs
|
||||
for i := range funcs {
|
||||
switch funcs[i].funcName {
|
||||
case FuncSum:
|
||||
idxSum = i
|
||||
}
|
||||
}
|
||||
|
||||
var sumQuery string
|
||||
if idxSum >= 0 {
|
||||
sumQuery = Sum(funcs[idxSum].field.Name(), "")
|
||||
}
|
||||
|
||||
var havingQuery string
|
||||
if having != nil {
|
||||
havingQuery = fmt.Sprintf("Condition(%s)", having.Cond.StringWithSubj(having.Subj))
|
||||
}
|
||||
|
||||
qo, err = GroupByBase(rowsQueries, int64(limit+offset), wherePQL, sumQuery, havingQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mr := &MappingResult{
|
||||
IndexName: indexName,
|
||||
Header: selectFields,
|
||||
Query: qo,
|
||||
}
|
||||
|
||||
mr.addReducer(NewAssignHeadersReducer(selectFields))
|
||||
if qm.HasOrderBy() {
|
||||
mr.addReducer(NewOrderByReducer(orderByFlds, orderByDirs, limit, offset))
|
||||
} else {
|
||||
mr.addReducer(NewLimitReducer(limit, offset))
|
||||
}
|
||||
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// handlerSelectIDCountFromTable: TopN
|
||||
type handlerSelectIDCountFromTable struct{}
|
||||
|
||||
func (f handlerSelectIDCountFromTable) Apply(stmt *sqlparser.Select, qm QueryMask, indexFunc func(string) *pilosa.Index) (*MappingResult, error) {
|
||||
var qo string
|
||||
|
||||
indexName, err := extractIndexName(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "extracting index name")
|
||||
}
|
||||
index := indexFunc(indexName)
|
||||
if index == nil {
|
||||
return nil, errors.WithMessage(pilosa.ErrIndexNotFound, indexName)
|
||||
}
|
||||
|
||||
selectFields, features, err := extractSelectFields(index, stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting select fields")
|
||||
}
|
||||
|
||||
var wherePQL string
|
||||
if stmt.Where != nil {
|
||||
wherePQL, err = extractWhere(index, stmt.Where.Expr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting where")
|
||||
}
|
||||
}
|
||||
|
||||
limit, offset, err := extractLimitOffset(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting limit offset")
|
||||
}
|
||||
|
||||
funcs := features.funcs
|
||||
if len(funcs) != 1 {
|
||||
return nil, errors.New("handler does not support multiple functions")
|
||||
} else if funcs[0].funcName != FuncCount {
|
||||
return nil, errors.Errorf("handler expected func: %s", FuncCount)
|
||||
}
|
||||
|
||||
if wherePQL == "" {
|
||||
qo = TopN(funcs[0].field.Name(), uint64(limit+offset))
|
||||
} else {
|
||||
qo = RowTopN(funcs[0].field.Name(), uint64(limit+offset), wherePQL)
|
||||
}
|
||||
|
||||
mr := &MappingResult{
|
||||
IndexName: indexName,
|
||||
Header: selectFields,
|
||||
Query: qo,
|
||||
}
|
||||
|
||||
mr.addReducer(NewAssignHeadersReducer(selectFields))
|
||||
mr.addReducer(NewLimitReducer(limit, offset))
|
||||
// TODO: order by is not implemented on this method because order desc
|
||||
// is handled in pilosa TopN. In order to support asc here, we would
|
||||
// have to return the entire TopN cache. Instead, we should consider
|
||||
// supported something like this in Pilosa itself.
|
||||
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// handlerSelectJoin: Join/Distinct()
|
||||
type handlerSelectJoin struct{}
|
||||
|
||||
func (h handlerSelectJoin) Apply(stmt *sqlparser.Select, qm QueryMask, indexFunc func(string) *pilosa.Index) (*MappingResult, error) {
|
||||
var qo string
|
||||
|
||||
pts, err := extractJoinTables(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting join tables")
|
||||
}
|
||||
|
||||
primary := pts.primary()
|
||||
secondary := pts.secondary()
|
||||
|
||||
primaryIndexName := primary.name
|
||||
primaryIndex := indexFunc(primaryIndexName)
|
||||
primaryField := primaryIndex.Field(primary.column.name)
|
||||
|
||||
secondaryIndexName := secondary.name
|
||||
secondaryIndex := indexFunc(secondaryIndexName)
|
||||
secondaryField := secondaryIndex.Field(secondary.column.name)
|
||||
|
||||
var wheres tableWheres
|
||||
if qm.HasWhere() {
|
||||
indexes := []*pilosa.Index{primaryIndex, secondaryIndex}
|
||||
wheres, err = extractWheres(indexes, pts, stmt.Where.Expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var primaryWhere string
|
||||
var secondaryWhere string
|
||||
for i, w := range wheres {
|
||||
switch w.table.index {
|
||||
case primaryIndex:
|
||||
primaryWhere = wheres[i].where
|
||||
case secondaryIndex:
|
||||
secondaryWhere = wheres[i].where
|
||||
}
|
||||
}
|
||||
|
||||
// Build the Distinct() portion of the query on the secondary.
|
||||
var distinctQry string
|
||||
if secondaryWhere == "" {
|
||||
distinctQry = Distinct(secondaryField.Index(), secondaryField.Name())
|
||||
} else {
|
||||
distinctQry = RowDistinct(secondaryField.Index(), secondaryField.Name(), secondaryWhere)
|
||||
}
|
||||
|
||||
var rowQry string
|
||||
if primaryWhere == "" {
|
||||
rowQry = Intersect(All(), distinctQry)
|
||||
} else {
|
||||
_ = primaryField
|
||||
rowQry = Intersect(primaryWhere, distinctQry)
|
||||
}
|
||||
|
||||
selectFields, _, err := extractSelectFields(primaryIndex, stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting select fields")
|
||||
}
|
||||
|
||||
orderByFlds, orderByDirs, err := extractOrderBy(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting order by")
|
||||
}
|
||||
|
||||
if qm.HasSelectPart(SelectPartCountStar) {
|
||||
qo = Count(rowQry)
|
||||
} else {
|
||||
qo = rowQry
|
||||
}
|
||||
|
||||
limit, offset, err := extractLimitOffset(stmt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extracting limit")
|
||||
}
|
||||
|
||||
mr := &MappingResult{
|
||||
IndexName: primaryIndex.Name(),
|
||||
Header: selectFields,
|
||||
Query: qo,
|
||||
}
|
||||
|
||||
mr.addReducer(NewAssignHeadersReducer(selectFields))
|
||||
if qm.HasOrderBy() {
|
||||
mr.addReducer(NewOrderByReducer(orderByFlds, orderByDirs, limit, offset))
|
||||
} else {
|
||||
mr.addReducer(NewLimitReducer(limit, offset))
|
||||
}
|
||||
|
||||
return mr, nil
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue