// Copyright 2022 Molecula Corp. All rights reserved. package planner import ( "context" "strconv" "strings" "github.com/molecula/featurebase/v3/pql" "github.com/molecula/featurebase/v3/sql3" "github.com/molecula/featurebase/v3/sql3/parser" "github.com/molecula/featurebase/v3/sql3/planner/types" ) // generatePQLCallFromExpr returns a *pql.Call tree for a given plan expression func (p *ExecutionPlanner) generatePQLCallFromExpr(ctx context.Context, expr types.PlanExpression) (_ *pql.Call, err error) { if expr == nil { return nil, nil } switch expr := expr.(type) { case *binOpPlanExpression: return p.generatePQLCallFromBinaryExpr(ctx, expr) case *callPlanExpression: switch strings.ToUpper(expr.name) { case "SETCONTAINS": col := expr.args[0].(*qualifiedRefPlanExpression) pqlValue, err := planExprToValue(expr.args[1]) if err != nil { return nil, err } return &pql.Call{ Name: "Row", Args: map[string]interface{}{ col.columnName: pqlValue, }, }, nil case "SETCONTAINSALL": col := expr.args[0].(*qualifiedRefPlanExpression) set, ok := expr.args[1].(*exprSetLiteralPlanExpression) if !ok { return nil, sql3.NewErrInternalf("unexpected argument type '%T'", expr.args[1]) } call := &pql.Call{ Name: "Intersect", Children: []*pql.Call{}, } for _, m := range set.members { pqlValue, err := planExprToValue(m) if err != nil { return nil, err } rc := &pql.Call{ Name: "Row", Args: map[string]interface{}{ col.columnName: pqlValue, }, } call.Children = append(call.Children, rc) } return call, nil case "SETCONTAINSANY": col := expr.args[0].(*qualifiedRefPlanExpression) set, ok := expr.args[1].(*exprSetLiteralPlanExpression) if !ok { return nil, sql3.NewErrInternalf("unexpected argument type '%T'", expr.args[1]) } call := &pql.Call{ Name: "Union", Children: []*pql.Call{}, } for _, m := range set.members { pqlValue, err := planExprToValue(m) if err != nil { return nil, err } rc := &pql.Call{ Name: "Row", Args: map[string]interface{}{ col.columnName: pqlValue, }, } call.Children = append(call.Children, rc) } return call, nil default: return nil, sql3.NewErrInternalf("unsupported scalar function '%s'", expr.name) } case *inOpPlanExpression: // lhs will be qualified ref lhs, ok := expr.lhs.(*qualifiedRefPlanExpression) if !ok { return nil, sql3.NewErrInternalf("unexpected lhs %T", expr.lhs) } // rhs is expression list - need to convert to a big OR list, ok := expr.rhs.(*exprListPlanExpression) if !ok { return nil, sql3.NewErrInternalf("unexpected argument type '%T'", expr.rhs) } // if it is the _id column, we can use ConstRow with a list if strings.EqualFold(lhs.columnName, "_id") { values := make([]interface{}, len(list.exprs)) for i, m := range list.exprs { pqlValue, err := planExprToValue(m) if err != nil { return nil, err } values[i] = pqlValue } call := &pql.Call{ Name: "ConstRow", Args: map[string]interface{}{ "columns": values, }, Type: pql.PrecallGlobal, } return call, nil } // otherwise, OR them all call := &pql.Call{ Name: "Union", Children: []*pql.Call{}, } for _, m := range list.exprs { pqlValue, err := planExprToValue(m) if err != nil { return nil, err } rc := &pql.Call{ Name: "Row", Args: map[string]interface{}{ lhs.columnName: pqlValue, }, } call.Children = append(call.Children, rc) } return call, nil default: return nil, sql3.NewErrInternalf("unexpected expression type: %T", expr) } } func (p *ExecutionPlanner) generatePQLCallFromBinaryExpr(ctx context.Context, expr *binOpPlanExpression) (_ *pql.Call, err error) { switch op := expr.op; op { case parser.AND, parser.OR: name := "Intersect" if op == parser.OR { name = "Union" } x, err := p.generatePQLCallFromExpr(ctx, expr.lhs) if err != nil { return nil, err } y, err := p.generatePQLCallFromExpr(ctx, expr.rhs) if err != nil { return nil, err } return &pql.Call{ Name: name, Children: []*pql.Call{x, y}, }, nil case parser.EQ, parser.NE, parser.LT, parser.LE, parser.GT, parser.GE: lhs, ok := expr.lhs.(*qualifiedRefPlanExpression) if !ok { return nil, sql3.NewErrInternalf("unexpected lhs %T", expr.lhs) } pqlValue, err := planExprToValue(expr.rhs) if err != nil { return nil, err } switch typ := expr.lhs.Type().(type) { case *parser.DataTypeInt: pqlOp, err := sqlToPQLOp(op) if err != nil { return nil, err } return &pql.Call{ Name: "Row", Args: map[string]interface{}{ lhs.columnName: &pql.Condition{ Op: pqlOp, Value: pqlValue, }, }, }, nil case *parser.DataTypeID: // TODO (pok) range queries on _id are not supported if strings.EqualFold(lhs.columnName, "_id") { cr := &pql.Call{ Name: "ConstRow", Args: map[string]interface{}{ "columns": []interface{}{pqlValue}, }, Type: pql.PrecallGlobal, } // TODO (pok) when we fix FB-1828 (https://molecula.atlassian.net/browse/FB-1828) // we can remove this - ConstRow returns a ghost record, thus to eliminate // we interset with All return &pql.Call{ Name: "Intersect", Children: []*pql.Call{ { Name: "All", }, cr, }, }, nil } return &pql.Call{ Name: "Row", Args: map[string]interface{}{ lhs.columnName: pqlValue, }, }, nil case *parser.DataTypeString: // TODO (pok) range queries on _id are not supported if strings.EqualFold(lhs.columnName, "_id") { cr := &pql.Call{ Name: "ConstRow", Args: map[string]interface{}{ "columns": []interface{}{pqlValue}, }, Type: pql.PrecallGlobal, } // TODO (pok) when we fix FB-1828 (https://molecula.atlassian.net/browse/FB-1828) // we can remove this - ConstRow returns a ghost record, thus to eliminate // we interset with All return &pql.Call{ Name: "Intersect", Children: []*pql.Call{ { Name: "All", }, cr, }, }, nil } return &pql.Call{ Name: "Row", Args: map[string]interface{}{ lhs.columnName: pqlValue, }, }, nil case *parser.DataTypeTimestamp: return &pql.Call{ Name: "Row", Args: map[string]interface{}{ lhs.columnName: pqlValue, }, }, nil case *parser.DataTypeBool: return &pql.Call{ Name: "Row", Args: map[string]interface{}{ lhs.columnName: pqlValue, }, }, nil case *parser.DataTypeDecimal: val, ok := pqlValue.(float64) if !ok { return nil, sql3.NewErrInternalf("unexpected type '%T", pqlValue) } d := pql.FromFloat64(val) return &pql.Call{ Name: "Row", Args: map[string]interface{}{ lhs.columnName: d, }, }, nil default: return nil, sql3.NewErrInternalf("unsupported type for binary expression: %v (%T)", typ, typ) } case parser.BITAND, parser.BITOR, parser.BITNOT, parser.LSHIFT, parser.RSHIFT: return nil, sql3.NewErrInternal("bitwise operators are not supported here") case parser.PLUS, parser.MINUS, parser.STAR, parser.SLASH, parser.REM: // + return nil, sql3.NewErrInternal("aritmetic operators are not supported here") case parser.CONCAT: return nil, sql3.NewErrInternal("concatenation operator is not supported here") case parser.IN, parser.NOTIN: return nil, sql3.NewErrInternal("IN operator is not supported") case parser.IS, parser.ISNOT: lhs, ok := expr.lhs.(*qualifiedRefPlanExpression) if !ok { return nil, sql3.NewErrInternalf("unexpected lhs %T", expr.lhs) } pqlOp := pql.EQ if op == parser.ISNOT { pqlOp = pql.NEQ } switch typ := expr.lhs.Type().(type) { case *parser.DataTypeID: if strings.EqualFold(lhs.columnName, "_id") { return nil, sql3.NewErrInvalidColumnInFilterExpression(0, 0, "_id", "is/is not null") } return nil, sql3.NewErrInvalidTypeInFilterExpression(0, 0, typ.TypeDescription(), "is/is not null") case *parser.DataTypeInt, *parser.DataTypeDecimal, *parser.DataTypeTimestamp: return &pql.Call{ Name: "Row", Args: map[string]interface{}{ lhs.columnName: &pql.Condition{ Op: pqlOp, Value: nil, }, }, }, nil default: return nil, sql3.NewErrInvalidTypeInFilterExpression(0, 0, typ.TypeDescription(), "is/is not null") } case parser.BETWEEN, parser.NOTBETWEEN: return nil, sql3.NewErrInternal("BETWEEN operator is not supported") default: return nil, sql3.NewErrInternalf("unexpected binary expression operator: %s", expr.op) } } // sqlToPQLOp converts a parser operation token to PQL. func sqlToPQLOp(op parser.Token) (pql.Token, error) { switch op { case parser.EQ: return pql.EQ, nil case parser.NE: return pql.NEQ, nil case parser.LT: return pql.LT, nil case parser.LE: return pql.LTE, nil case parser.GT: return pql.GT, nil case parser.GE: return pql.GTE, nil default: return pql.ILLEGAL, sql3.NewErrInternalf("cannot convert SQL op %q to PQL", op) } } // planExprToValue converts a literal parser expression node to a value. func planExprToValue(expr types.PlanExpression) (interface{}, error) { switch expr := expr.(type) { case *intLiteralPlanExpression: return expr.value, nil case *stringLiteralPlanExpression: return expr.value, nil case *dateLiteralPlanExpression: return expr.value, nil case *boolLiteralPlanExpression: return expr.value, nil case *floatLiteralPlanExpression: f, err := strconv.ParseFloat(expr.value, 64) if err != nil { return nil, err } return f, nil default: return nil, sql3.NewErrInternalf("cannot convert SQL expression %T to a literal value", expr) } }