featurebase/sql/extract.go
2022-01-21 10:57:05 -07:00

1217 lines
34 KiB
Go

// Copyright 2021 Molecula Corp. All rights reserved.
package sql
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
"github.com/molecula/featurebase/v3"
"github.com/molecula/featurebase/v3/pql"
"github.com/pkg/errors"
"vitess.io/vitess/go/vt/sqlparser"
)
// parseColumn is a column parsed from a sql query. Its qualifier
// value should map to either the name or alias of a parseTable.
type parseColumn struct {
name string
qualifier string
}
// parseTable is a table parsed from a sql query. It includes
// its name and alias, along with a boolean indicating whether
// the table is the primary side of a join statement (i.e. it
// refers to the Pilosa column _id).
type parseTable struct {
name string
alias string
primary bool // indicates the side of the join representing the column _id
column *parseColumn // contains the related column from the ON clause
index *pilosa.Index // the pilosa index related to this table
}
// parseTables is a slice of parseTable parsed from a single sql query.
type parseTables []*parseTable
// byName returns the parseTable from the slice which matches on name.
// If there is no match it returns nil.
func (j parseTables) byName(n string) *parseTable {
for i := range j {
if j[i].name == n {
return j[i]
}
}
return nil
}
// byName returns the parseTable from the slice which matches on alias.
// If there is no match it returns nil.
func (j parseTables) byAlias(a string) *parseTable {
for i := range j {
if j[i].alias == a {
return j[i]
}
}
return nil
}
// primary returns the primary parseTable from the slice.
// In order for this to be useful, it is assumed that
// joinTables contains exactly two joinTable pointers
// (a primary and a secondary).
func (j parseTables) primary() *parseTable {
for i := range j {
if j[i].primary {
return j[i]
}
}
return nil
}
// secondary returns the secondary parseTable from the slice.
func (j parseTables) secondary() *parseTable {
for i := range j {
if !j[i].primary {
return j[i]
}
}
return nil
}
// tableWhere represents a parseTable from a sql query along
// with the portion of the where clause that relates to
// that table. For example, if a sql query had:
// from tbl1, tbl2
// where tbl1.field1=1 and tbl2.field2=2
// then each table would have a separate tableWhere object
// with the where made up of only the field with matching qualifier.
type tableWhere struct {
table *parseTable
where string
}
// tableWheres is a slice of tableWhere.
type tableWheres []*tableWhere
// extractParseTable returns a parseTable for the sqlparser.TableExpr.
func extractParseTable(tableExpr sqlparser.TableExpr) (*parseTable, error) {
switch tbl := tableExpr.(type) {
case *sqlparser.AliasedTableExpr:
tableName := tbl.Expr.(sqlparser.TableName).ToViewName().Name.String()
alias := tbl.As.String()
if alias == "" {
alias = tableName
}
return &parseTable{
name: tableName,
alias: alias,
}, nil
}
return nil, errors.New("unsupported table expression")
}
func extractSelectFields(index *pilosa.Index, stmt *sqlparser.Select) ([]Column, selectFeatures, error) {
columns := []Column{}
features := selectFeatures{}
for _, item := range stmt.SelectExprs {
switch expr := item.(type) {
case *sqlparser.AliasedExpr:
var column Column
var alias string = expr.As.String()
switch colExpr := expr.Expr.(type) {
case *sqlparser.ColName:
fieldName := colExpr.Name.String()
if fieldName == ColID {
if index.Options().Keys {
column = NewKeyIndexColumn(index, alias)
} else {
column = NewIDIndexColumn(index, alias)
}
} else {
field := index.Field(fieldName)
if field == nil {
return nil, features, errors.Wrapf(pilosa.ErrFieldNotFound, "field %s", fieldName)
}
column = NewFieldColumn(field, alias)
}
case *sqlparser.FuncExpr:
funcName := FuncName(strings.ToLower(colExpr.Name.String()))
var field *pilosa.Field
if len(colExpr.Exprs) != 1 {
return nil, features, errors.New("function should have a single argument")
}
switch expr := colExpr.Exprs[0].(type) {
case *sqlparser.AliasedExpr:
if colExpr, ok := expr.Expr.(*sqlparser.ColName); ok {
fieldName := colExpr.Name.String()
field = index.Field(fieldName)
if field == nil {
return nil, features, errors.Wrapf(pilosa.ErrFieldNotFound, "field %s", fieldName)
}
} else {
return nil, features, errors.New("table name is required")
}
case *sqlparser.StarExpr:
// We don't currently track this; it either has a field or doesn't.
default:
return nil, features, errors.New("table name is required")
}
switch funcName {
case FuncCount, FuncMin, FuncMax, FuncSum, FuncAvg:
column = NewFuncColumn(funcName, field, alias)
default:
return nil, features, fmt.Errorf("unknown function: %s", funcName)
}
features.funcs = append(features.funcs, selectFunc{
funcName: funcName,
field: field,
})
default:
return nil, features, errors.New("table name is required")
}
columns = append(columns, column)
case *sqlparser.StarExpr:
columns = append(columns, NewStarColumn())
default:
return nil, features, errors.New("only column names or * are supported in select")
}
}
return columns, features, nil
}
func extractIndexName(stmt *sqlparser.Select) (string, error) {
if len(stmt.From) != 1 {
return "", errors.New("selecting from multiple tables is not supported")
}
fromExpr := stmt.From[0]
switch from := fromExpr.(type) {
case *sqlparser.AliasedTableExpr:
indexName := from.Expr.(sqlparser.TableName).ToViewName().Name.String()
return indexName, nil
}
return "", errors.New("unsupported from clause")
}
// extractParseColumn returns a parseColumn for the sqlparser.ColName.
func extractParseColumn(col *sqlparser.ColName) (*parseColumn, error) {
colName := col.Name.String()
qualifier := col.Qualifier.ToViewName().Name.String()
return &parseColumn{
name: colName,
qualifier: qualifier,
}, nil
}
func extractWhere(index *pilosa.Index, expr sqlparser.Expr) (string, error) {
switch e := expr.(type) {
case *sqlparser.ComparisonExpr:
parseCol, op, val, err := extractComparison(e)
if err != nil {
return "", err
}
if parseCol.name == "_id" {
switch op {
case "=":
return ConstRow(val), nil
case "in":
switch valExpr := val.(type) {
case []interface{}:
return ConstRow(valExpr...), nil
}
}
}
field := index.Field(parseCol.name)
if field == nil {
return "", errors.Wrap(pilosa.ErrFieldNotFound, parseCol.name)
}
switch field.Type() {
case pilosa.FieldTypeInt, pilosa.FieldTypeDecimal, pilosa.FieldTypeTimestamp:
switch op {
case "=":
return Equals(field.Name(), val), nil
case "<":
return LT(field.Name(), val), nil
case "<=":
return LTE(field.Name(), val), nil
case ">":
return GT(field.Name(), val), nil
case ">=":
return GTE(field.Name(), val), nil
case "<>", "!=":
return NotEquals(field.Name(), val), nil
}
default:
switch op {
case "=":
return Row(field.Name(), val)
case "in":
var qs []string
switch valExpr := val.(type) {
case []interface{}:
for _, v := range valExpr {
q, err := Row(field.Name(), v)
if err != nil {
return "", err
}
qs = append(qs, q)
}
return Union(qs...), nil
default:
return "", fmt.Errorf("in operator expects `[]interface{}` but got: %T", valExpr)
}
case "like":
sval, ok := val.(string)
if !ok {
return "", fmt.Errorf("like operator expects `string` but got: %T", val)
}
return Like(field.Name(), sval), nil
}
}
case *sqlparser.AndExpr:
pql, err := extractWhereDateRange(index, e.Left, e.Right)
if err == nil {
return pql, err
}
left, err := extractWhere(index, e.Left)
if err != nil {
return "", err
}
right, err := extractWhere(index, e.Right)
if err != nil {
return "", err
}
return Intersect(left, right), nil
case *sqlparser.OrExpr:
left, err := extractWhere(index, e.Left)
if err != nil {
return "", err
}
right, err := extractWhere(index, e.Right)
if err != nil {
return "", err
}
return Union(left, right), nil
case *sqlparser.NotExpr:
expr, err := extractWhere(index, e.Expr)
if err != nil {
return "", err
}
return Not(expr), nil
case *sqlparser.ParenExpr:
expr, err := extractWhere(index, e.Expr)
if err != nil {
return "", err
}
return expr, nil
case *sqlparser.RangeCond:
if e.Operator != "between" {
return "", errors.New("only between is supported")
}
left, ok := e.Left.(*sqlparser.ColName)
if !ok {
return "", errors.New("left operand must be a column name")
}
columnName := left.Name.String()
fieldName, isSpecial := ExtractFieldName(columnName)
if isSpecial {
return "", errors.New("special fields are not allowed here")
}
field := index.Field(fieldName)
switch field.Type() {
case pilosa.FieldTypeInt:
fromNum, err := extractInt(e.From)
if err != nil {
return "", err
}
toNum, err := extractInt(e.To)
if err != nil {
return "", err
}
return Between(field.Name(), fromNum, toNum), nil
case pilosa.FieldTypeDecimal:
fromNum, err := extractFloat(e.From)
if err != nil {
return "", err
}
toNum, err := extractFloat(e.To)
if err != nil {
return "", err
}
return Between(field.Name(), fromNum, toNum), nil
case pilosa.FieldTypeTimestamp:
fromTime, err := extractTimestamp(e.From)
if err != nil {
return "", err
}
toTime, err := extractTimestamp(e.To)
if err != nil {
return "", err
}
return Between(field.Name(), fromTime, toTime), nil
default:
return "", errors.New("only int and float64 fields are supported")
}
case *sqlparser.IsExpr:
left, ok := e.Expr.(*sqlparser.ColName)
if !ok {
return "", errors.New("left operand must be a column name")
}
field := index.Field(left.Name.String())
if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
if e.Operator == "is not null" {
return NotNull(field.Name()), nil
}
return "", fmt.Errorf("only `is not null` is supported for %s fields", field.Type())
}
return "", fmt.Errorf("`is` expression is supported only for %s fields", field.Type())
}
return "", errors.New("cannot extract where")
}
func extractWhereDateRange(index *pilosa.Index, leftExpr sqlparser.Expr, rightExpr sqlparser.Expr) (string, error) {
var fieldName string
var fromExpr sqlparser.Expr
var toExpr sqlparser.Expr
var isSpecial bool
var idOrKey interface{}
// Where part is expected to be in one of the following forms:
// where FIELD=VALUE and FIELD between DATETIME_FORMAT and DATETIME_FORMAT
// Or:
// where FIELD between DATETIME_FORMAT and DATETIME_FORMAT and FIELD=VALUE
extract := func(compExpr *sqlparser.ComparisonExpr, rangeCond *sqlparser.RangeCond) error {
parseCol, op, val, err := extractComparison(compExpr)
if err != nil {
return err
}
idOrKey = val
if op != "=" {
// only = operator can exist here.
return errors.New("only = operator can exist here")
}
fieldName, isSpecial = ExtractFieldName(parseCol.name)
if isSpecial {
// column name cannot be special here.
return errors.New("column name cannot be special here")
}
if rangeCond.Operator != "between" {
// only between is accepted at this point.
return errors.New("only between is accepted at this point")
}
condFieldName, _ := ExtractFieldName(rangeCond.Left.(*sqlparser.ColName).Name.String())
if condFieldName != fieldName {
// the field names in both sides should be the same, otherwise reject.
return errors.New("the field names in both sides should be the same, otherwise reject")
}
fromExpr = rangeCond.From
toExpr = rangeCond.To
return nil
}
if left, ok := leftExpr.(*sqlparser.ComparisonExpr); ok {
// if FIELD=VALUE part is on the left, and range part is on the right
if right, ok := rightExpr.(*sqlparser.RangeCond); ok {
if err := extract(left, right); err != nil {
return "", err
}
} else {
return "", errors.New("right is not a range cond")
}
} else if right, ok := rightExpr.(*sqlparser.ComparisonExpr); ok {
// if FIELD=VALUE part is on the right, and left part is on the left
if left, ok := leftExpr.(*sqlparser.RangeCond); ok {
if err := extract(right, left); err != nil {
return "", err
}
} else {
return "", errors.New("left is not a range cond")
}
}
fromStr, err := extractStr(fromExpr)
if err != nil {
return "", err
}
toStr, err := extractStr(toExpr)
if err != nil {
return "", err
}
// try to convert `from` and `to` to time
fromTime, ok := ConvertToTime(fromStr)
if !ok {
return "", errors.New("from operand must be in the correct time format")
}
toTime, ok := ConvertToTime(toStr)
if !ok {
return "", errors.New("to operand must be in the correct time format")
}
field := index.Field(fieldName)
return RowRange(field.Name(), idOrKey, fromTime, toTime)
}
func extractVal(e sqlparser.Expr) (interface{}, error) {
val, ok := e.(*sqlparser.SQLVal)
if !ok {
return nil, errors.New("expression must be a value")
}
switch val.Type {
case sqlparser.StrVal:
return string(val.Val), nil
case sqlparser.IntVal:
value, err := strconv.Atoi(string(val.Val))
if err != nil {
return nil, err
}
return value, nil
case sqlparser.FloatVal:
value, err := strconv.ParseFloat(string(val.Val), 64)
if err != nil {
return nil, err
}
return value, nil
default:
return nil, fmt.Errorf("unknown type: %d", val.Type)
}
}
func extractInt(e sqlparser.Expr) (int, error) {
val, err := extractVal(e)
if err != nil {
return 0, err
}
num, ok := val.(int)
if !ok {
return 0, errors.New("value must be an integer")
}
return num, nil
}
func extractFloat(e sqlparser.Expr) (float64, error) {
val, err := extractVal(e)
if err != nil {
return 0, err
}
var num float64
switch v := val.(type) {
case int:
num = float64(v)
case float64:
num = v
default:
return 0, errors.New("value must be convertable to a float64")
}
return num, nil
}
func extractTimestamp(e sqlparser.Expr) (time.Time, error) {
val, err := extractVal(e)
if err != nil {
return time.Time{}, err
}
s, ok := val.(string)
if !ok {
return time.Time{}, errors.New("value must be an ISO 8601-formated timestamp string")
}
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return time.Time{}, errors.New("value must be an ISO 8601-formated timestamp string")
}
return t, nil
}
func extractStr(e sqlparser.Expr) (string, error) {
val, err := extractVal(e)
if err != nil {
return "", err
}
s, ok := val.(string)
if !ok {
return "", errors.New("value must be a string")
}
return s, nil
}
func extractTuple(tuple sqlparser.ValTuple) ([]interface{}, error) {
result := []interface{}{}
for _, item := range tuple {
if val, ok := item.(*sqlparser.SQLVal); ok {
v, err := extractVal(val)
if err != nil {
return nil, err
}
result = append(result, v)
} else {
return nil, errors.New("tuple should contain integers or strings")
}
}
return result, nil
}
func extractComparison(expr *sqlparser.ComparisonExpr) (col *parseColumn, op string, value interface{}, err error) {
op = expr.Operator
if colExpr, ok := expr.Left.(*sqlparser.ColName); ok {
col, err = extractParseColumn(colExpr)
if err != nil {
return
}
if op == "in" {
switch valExpr := expr.Right.(type) {
case sqlparser.ValTuple:
value, err = extractTuple(valExpr)
default:
err = fmt.Errorf("in operator excepts only a tuple or a query, received `%s`",
reflect.TypeOf(valExpr).String())
return
}
} else {
value, err = extractVal(expr.Right)
}
if err != nil {
return
}
} else {
if colExpr, ok := expr.Right.(*sqlparser.ColName); ok {
col, err = extractParseColumn(colExpr)
if err != nil {
return
}
if op == "in" {
value, err = extractTuple(expr.Left.(sqlparser.ValTuple))
} else {
value, err = extractVal(expr.Left)
}
if err != nil {
return
}
} else {
err = errors.New("either left or right operand should be a column name")
}
}
return
}
func extractLimitOffset(stmt *sqlparser.Select) (limit uint, offset uint, hasLimit bool, hasOffset bool, err error) {
if stmt.Limit == nil {
return 0, 0, false, false, nil
}
if offsetExpr, ok := stmt.Limit.Offset.(*sqlparser.SQLVal); ok {
val, err := extractVal(offsetExpr)
if err != nil {
return 0, 0, false, false, err
}
if offsetVal, ok := val.(int); ok {
offset = uint(offsetVal)
} else {
return 0, 0, false, false, errors.New("offset must be an integer")
}
hasOffset = true
}
if limitExpr, ok := stmt.Limit.Rowcount.(*sqlparser.SQLVal); ok {
val, err := extractVal(limitExpr)
if err != nil {
return 0, 0, false, false, err
}
if limitVal, ok := val.(int); ok {
limit = uint(limitVal)
} else {
return 0, 0, false, false, errors.New("limit must be an integer")
}
hasLimit = true
}
return limit, offset, hasLimit, hasOffset, nil
}
// extractOrderBy returns the order by fields and directions (asc/desc)
// as separate string slices.
func extractOrderBy(stmt *sqlparser.Select) ([]string, []string, error) {
var flds []string
var dirs []string
for _, item := range stmt.OrderBy {
switch colExpr := item.Expr.(type) {
case *sqlparser.ColName:
colName := colExpr.Name.String()
flds = append(flds, colName)
dirs = append(dirs, item.Direction)
}
}
return flds, dirs, nil
}
func extractTableNames(stmt *sqlparser.Select) ([]string, error) {
if len(stmt.From) == 0 {
return []string{}, nil
}
switch from := stmt.From[0].(type) {
case *sqlparser.AliasedTableExpr:
tableName := from.Expr.(sqlparser.TableName).ToViewName().Name.String()
return []string{tableName}, nil
case *sqlparser.JoinTableExpr:
ret := []string{}
switch left := from.LeftExpr.(type) {
case *sqlparser.AliasedTableExpr:
leftTableName := left.Expr.(sqlparser.TableName).ToViewName().Name.String()
ret = append(ret, leftTableName)
}
switch right := from.RightExpr.(type) {
case *sqlparser.AliasedTableExpr:
rightTableName := right.Expr.(sqlparser.TableName).ToViewName().Name.String()
ret = append(ret, rightTableName)
}
return ret, nil
}
return []string{}, nil
}
func extractGroupByFieldNames(stmt sqlparser.GroupBy) ([]string, error) {
fields := make([]string, len(stmt))
for i, item := range stmt {
col, ok := item.(*sqlparser.ColName)
if !ok {
return nil, errors.New("group by accepts columns")
}
fields[i] = col.Name.String()
}
return fields, nil
}
func extractHavingClause(stmt *sqlparser.Where) (*HavingClause, error) {
if stmt == nil {
return nil, nil
}
if stmt.Type != "having" {
return nil, fmt.Errorf("invalid having type: %s", stmt.Type)
}
hc := &HavingClause{}
switch having := stmt.Expr.(type) {
case *sqlparser.RangeCond:
if having.Operator != "between" {
return nil, errors.New("only between is supported")
}
hc.Subj = having.Left.(*sqlparser.ColName).Name.String()
hc.Cond.Op = pql.BETWEEN
fromPred, err := extractInt(having.From)
if err != nil {
return nil, err
}
toPred, err := extractInt(having.To)
if err != nil {
return nil, err
}
vals := make([]interface{}, 2)
switch hc.Subj {
case "count":
vals[0] = uint64(fromPred)
vals[1] = uint64(toPred)
case "sum":
vals[0] = int64(fromPred)
vals[1] = int64(toPred)
}
hc.Cond.Value = vals
return hc, nil
case *sqlparser.AndExpr:
left := having.Left.(*sqlparser.ComparisonExpr)
right := having.Right.(*sqlparser.ComparisonExpr)
leftName := left.Left.(*sqlparser.ColName).Name.String()
rightName := right.Left.(*sqlparser.ColName).Name.String()
if leftName != rightName {
return nil, fmt.Errorf("having comparitors do not match: %s, %s", leftName, rightName)
}
hc.Subj = leftName
leftOp := extractComparisonOp(left)
rightOp := extractComparisonOp(right)
leftPred, err := extractInt(left.Right)
if err != nil {
return nil, err
}
rightPred, err := extractInt(right.Right)
if err != nil {
return nil, err
}
intVals := make([]int, 2)
if leftOp == pql.GT && rightOp == pql.LT {
intVals[0] = leftPred
intVals[1] = rightPred
hc.Cond.Op = pql.BTWN_LT_LT
} else if leftOp == pql.GT && rightOp == pql.LTE {
intVals[0] = leftPred
intVals[1] = rightPred
hc.Cond.Op = pql.BTWN_LT_LTE
} else if leftOp == pql.GTE && rightOp == pql.LT {
intVals[0] = leftPred
intVals[1] = rightPred
hc.Cond.Op = pql.BTWN_LTE_LT
} else if leftOp == pql.GTE && rightOp == pql.LTE {
intVals[0] = leftPred
intVals[1] = rightPred
hc.Cond.Op = pql.BETWEEN
} else if leftOp == pql.LT && rightOp == pql.GT {
intVals[0] = rightPred
intVals[1] = leftPred
hc.Cond.Op = pql.BTWN_LT_LT
} else if leftOp == pql.LT && rightOp == pql.GTE {
intVals[0] = rightPred
intVals[1] = leftPred
hc.Cond.Op = pql.BTWN_LTE_LT
} else if leftOp == pql.LTE && rightOp == pql.GT {
intVals[0] = rightPred
intVals[1] = leftPred
hc.Cond.Op = pql.BTWN_LT_LTE
} else if leftOp == pql.LTE && rightOp == pql.GTE {
intVals[0] = rightPred
intVals[1] = leftPred
hc.Cond.Op = pql.BETWEEN
}
vals := make([]interface{}, 2)
switch hc.Subj {
case "count":
vals[0] = uint64(intVals[0])
vals[1] = uint64(intVals[1])
case "sum":
vals[0] = int64(intVals[0])
vals[1] = int64(intVals[1])
}
hc.Cond.Value = vals
return hc, nil
case *sqlparser.ComparisonExpr:
hc.Subj = having.Left.(*sqlparser.ColName).Name.String()
hc.Cond.Op = extractComparisonOp(having)
switch hc.Subj {
case "count":
pred, err := extractInt(having.Right)
if err != nil {
return nil, err
}
hc.Cond.Value = uint64(pred)
case "sum":
pred, err := extractInt(having.Right)
if err != nil {
return nil, err
}
hc.Cond.Value = int64(pred)
}
return hc, nil
}
return nil, errors.New("unsupported having clause")
}
func extractComparisonOp(expr *sqlparser.ComparisonExpr) pql.Token {
switch expr.Operator {
case "==":
return pql.EQ
case "!=":
return pql.NEQ
case "<":
return pql.LT
case "<=":
return pql.LTE
case ">":
return pql.GT
case ">=":
return pql.GTE
}
return pql.ILLEGAL
}
// extractJoinTables returns a slice of parseTable containing two
// items, the primary and secondary join tables. This function does
// not extract join tables of the form:
// from tbl1, tbl2
// The from clause must be of the form:
// from tbl1 INNER JOIN tbl2 ON ...
//
func extractJoinTables(stmt *sqlparser.Select) (parseTables, error) {
if len(stmt.From) != 1 {
return nil, errors.New("selecting from multiple tables is not supported")
}
tbls := make([]*parseTable, 2)
from, ok := stmt.From[0].(*sqlparser.JoinTableExpr)
if !ok {
return nil, errors.New("unsupported join clause")
}
leftTable, err := extractParseTable(from.LeftExpr)
if err != nil {
return nil, errors.Wrap(err, "extracting left join table")
}
rightTable, err := extractParseTable(from.RightExpr)
if err != nil {
return nil, errors.Wrap(err, "extracting right join table")
}
// It is not important which table goes in which tbls position;
// the primary/secondary table will be determined later.
tbls[0] = leftTable
tbls[1] = rightTable
// Get the ON condition and determine which table is primary.
switch onCond := from.Condition.On.(type) {
case *sqlparser.ComparisonExpr:
if onCond.Operator != "=" {
return nil, errors.Errorf("unsupported on condition comparison type: %s", onCond.Operator)
}
// get left ColName
left, ok := onCond.Left.(*sqlparser.ColName)
if !ok {
return nil, errors.New("left join operand must be a column name")
}
leftJoinCol, err := extractParseColumn(left)
if err != nil {
return nil, errors.Wrap(err, "extracting left join column")
}
// get right ColName
right, ok := onCond.Right.(*sqlparser.ColName)
if !ok {
return nil, errors.New("right join operand must be a column name")
}
rightJoinCol, err := extractParseColumn(right)
if err != nil {
return nil, errors.Wrap(err, "extracting right join column")
}
// The primary column is set as the column referencing the "_id" field.
var primaryColumn *parseColumn
if leftJoinCol.name == ColID && rightJoinCol.name != ColID {
primaryColumn = leftJoinCol
} else if leftJoinCol.name != ColID && rightJoinCol.name == ColID {
primaryColumn = rightJoinCol
} else {
return nil, errors.Errorf("exactly one join column must be %s, have: %s, %s", ColID, leftJoinCol.name, rightJoinCol.name)
}
// populate the joinTable column and primary fields
for _, jc := range []*parseColumn{leftJoinCol, rightJoinCol} {
var found bool
for i := range tbls {
if tbls[i].alias == jc.qualifier {
tbls[i].column = jc
if jc == primaryColumn {
tbls[i].primary = true
}
found = true
}
}
if !found {
return nil, errors.Errorf("no tables match qualifier: %s", jc.qualifier)
}
}
default:
return nil, errors.Errorf("unsupported on condition type: %T", onCond)
}
return tbls, nil
}
// extractWheres returns the slice of tableWhere for the sql query.
func extractWheres(indexes []*pilosa.Index, tbls parseTables, expr sqlparser.Expr) (tableWheres, error) {
wheres := make([]*tableWhere, 0)
// Set the index associated with each parseTable.
// TODO: may be able to move this to parseTables creation?
for _, idx := range indexes {
tbl := tbls.byName(idx.Name())
if tbl == nil {
return nil, errors.Errorf("index not in parseTables: %s", idx.Name())
}
tbl.index = idx
}
// make a map of tbls alias to slice index.
m := make(map[string]int)
for i, tbl := range tbls {
m[tbl.alias] = i
}
switch e := expr.(type) {
case *sqlparser.ComparisonExpr:
pCol, op, val, err := extractComparison(e)
if err != nil {
return nil, err
}
pTable := tbls.byAlias(pCol.qualifier)
if pTable == nil {
return nil, errors.Errorf("no index for qaulifier: %s", pCol.qualifier)
} else if pTable.index == nil {
return nil, errors.Errorf("parse table has no index: %s", pTable.name)
}
field := pTable.index.Field(pCol.name)
tw := &tableWhere{
table: pTable,
}
if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
if field.Type() == pilosa.FieldTypeInt {
if _, ok := val.(int); !ok {
return nil, errors.New("right operand must be a number")
}
} else { // timestamp
if _, ok := val.(time.Time); !ok {
return nil, errors.New("right operand must be a timestamp")
}
}
switch op {
case "=":
tw.where = Equals(field.Name(), val)
case "<":
tw.where = LT(field.Name(), val)
case "<=":
tw.where = LTE(field.Name(), val)
case ">":
tw.where = GT(field.Name(), val)
case ">=":
tw.where = GTE(field.Name(), val)
case "<>":
fallthrough
case "!=":
tw.where = NotEquals(field.Name(), val)
}
return append(wheres, tw), nil
}
if op == "=" {
if tw.where, err = Row(field.Name(), val); err != nil {
return nil, err
}
return append(wheres, tw), nil
}
if op == "in" {
var qs []string
switch valExpr := val.(type) {
case []interface{}:
for _, v := range valExpr {
r, e := Row(field.Name(), v)
if e != nil {
return nil, errors.Wrap(err, "extracting where statements")
}
qs = append(qs, r)
}
tw.where = Union(qs...)
return append(wheres, tw), nil
default:
return nil, fmt.Errorf("in operator expects `[]interface{}` but got: %T", valExpr)
}
}
case *sqlparser.AndExpr:
left, err := extractWheres(indexes, tbls, e.Left)
if err != nil {
return nil, err
}
right, err := extractWheres(indexes, tbls, e.Right)
if err != nil {
return nil, err
}
// The following logic is used to build the where portion of the query
// related to each table. The goal is to return one or two tableWhere
// objects (either 0 or 1 for each table in the join).
if len(left) == 1 && len(right) == 1 && left[0].table == right[0].table {
// if left(1) and right(1) are from the same alias,
// then intersect them into left and return left(1)
left[0].where = Intersect(left[0].where, right[0].where)
return left, nil
} else if len(left) == 1 && len(right) == 1 && left[0].table != right[0].table {
// if left(1) and right(1) are NOT from the same alias,
// then return final(2)
return []*tableWhere{left[0], right[0]}, nil
} else if len(left) == 1 && len(right) == 2 {
// if left(1) and right(2)
// then intersect the 1's and return final(2)
if left[0].table == right[0].table {
left[0].where = Intersect(left[0].where, right[0].where)
return []*tableWhere{left[0], right[1]}, nil
} else if left[0].table == right[1].table {
left[0].where = Intersect(left[0].where, right[1].where)
return []*tableWhere{left[0], right[0]}, nil
}
return nil, errors.Errorf("no matching table on right: %s", left[0].table.name)
} else if len(left) == 2 && len(right) == 1 {
// if left(2) and right(1),
// then intersect the 1's and return final(2)
if right[0].table == left[0].table {
right[0].where = Intersect(right[0].where, left[0].where)
return []*tableWhere{left[1], right[0]}, nil
} else if right[0].table == left[1].table {
right[0].where = Intersect(right[0].where, left[1].where)
return []*tableWhere{left[0], right[0]}, nil
}
return nil, errors.Errorf("no matching table on left: %s", right[0].table.name)
} else if len(left) == 2 && len(right) == 2 {
// if left(2) and right(2)
// then intsect both and return final(2)
if left[0].table == right[0].table && left[1].table == right[1].table {
left[0].where = Intersect(left[0].where, right[0].where)
left[1].where = Intersect(left[1].where, right[1].where)
return left, nil
} else if left[0].table == right[1].table && left[1].table == right[0].table {
left[0].where = Intersect(left[0].where, right[1].where)
left[1].where = Intersect(left[1].where, right[0].where)
return left, nil
}
return nil, errors.Errorf("non-matching tables: %s/%s, %s/%s", left[0].table.name, left[1].table.name, right[0].table.name, right[1].table.name)
}
return nil, errors.Errorf("invalid table count; expected 1 or 2, but got: %d, %d", len(left), len(right))
case *sqlparser.OrExpr:
left, err := extractWheres(indexes, tbls, e.Left)
if err != nil {
return nil, err
}
right, err := extractWheres(indexes, tbls, e.Right)
if err != nil {
return nil, err
}
if len(left) == 1 && len(right) == 1 && left[0].table == right[0].table {
// if left(1) and right(1) are from the same alias,
// then union them and return final(1)
left[0].where = Union(left[0].where, right[0].where)
return left, nil
}
return nil, errors.Errorf("invalid table count; expected 1/1, but got: %d/%d", len(left), len(right))
case *sqlparser.NotExpr:
expr, err := extractWheres(indexes, tbls, e.Expr)
if err != nil {
return nil, err
}
if len(expr) == 1 {
expr[0].where = Not(expr[0].where)
return expr, nil
}
return nil, errors.Errorf("not support a single expression. got: %d", len(expr))
case *sqlparser.ParenExpr:
expr, err := extractWheres(indexes, tbls, e.Expr)
if err != nil {
return nil, err
}
if len(expr) == 1 {
return expr, nil
}
return nil, errors.Errorf("not support a single expression. got: %d", len(expr))
case *sqlparser.RangeCond:
if e.Operator != "between" {
return nil, errors.New("only between is supported")
}
left, ok := e.Left.(*sqlparser.ColName)
if !ok {
return nil, errors.New("left operand must be a column name")
}
pCol, err := extractParseColumn(left)
if err != nil {
return nil, errors.Wrap(err, "extracting parse column")
}
pTable := tbls.byAlias(pCol.qualifier)
if pTable == nil {
return nil, errors.Errorf("no index for qaulifier: %s", pCol.qualifier)
} else if pTable.index == nil {
return nil, errors.Errorf("parse table has no index: %s", pTable.name)
}
fieldName, isSpecial := ExtractFieldName(pCol.name)
if isSpecial {
return nil, errors.New("special fields are not allowed here")
}
tw := &tableWhere{
table: pTable,
}
field := pTable.index.Field(fieldName)
if field.Type() == pilosa.FieldTypeInt {
fromNum, err := extractInt(e.From)
if err != nil {
return nil, err
}
toNum, err := extractInt(e.To)
if err != nil {
return nil, err
}
tw.where = Between(field.Name(), fromNum, toNum)
return append(wheres, tw), nil
}
if field.Type() == pilosa.FieldTypeTimestamp {
fromTime, err := extractTimestamp(e.From)
if err != nil {
return nil, err
}
toTime, err := extractTimestamp(e.To)
if err != nil {
return nil, err
}
tw.where = Between(field.Name(), fromTime, toTime)
return append(wheres, tw), nil
}
return nil, errors.New("only int or timestamp fields are supported")
case *sqlparser.IsExpr:
left, ok := e.Expr.(*sqlparser.ColName)
if !ok {
return nil, errors.New("left operand must be a column name")
}
pCol, err := extractParseColumn(left)
if err != nil {
return nil, errors.Wrap(err, "extracting parse column")
}
pTable := tbls.byAlias(pCol.qualifier)
if pTable == nil {
return nil, errors.Errorf("no index for qaulifier: %s", pCol.qualifier)
} else if pTable.index == nil {
return nil, errors.Errorf("parse table has no index: %s", pTable.name)
}
tw := &tableWhere{
table: pTable,
}
field := pTable.index.Field(pCol.name)
if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
if e.Operator == "is not null" {
tw.where = NotNull(field.Name())
return append(wheres, tw), nil
}
return nil, fmt.Errorf("only `is not null` is supported for %s fields", field.Type())
}
return nil, fmt.Errorf("`is` expression is supported only for %s fields", field.Type())
}
return nil, errors.New("cannot extract where")
}