mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* Fix formatting in CLI results with custom SQLResonse.UnmarshalJSON
When I started this, it was meant to be a quick fix to address the confusing
result formats we were seeing in the CLI. For example, all large integer values
were displayed in scientifc notation. This is because we were passing the result
types from JSON (in this case, float64) into pretty print. Similarly, `IDSets`
and `StringSets` where being printed using the default go Stringer for the types
[]int64 and []string respectively.
I started by writing a customer UnmarshalJSON() method for the `SQLResponse`
type. Part of this (the part which converts data types based on header types)
was already being used in dax tests, so this just formalizes that logic as part
of the `SQLResponse` type.
Then I realized that the sql3 tests (run against the `sql3` package) were
failing because sql3 is not actually returning the `IDSets` and `StringSets`
types. A future task is to formalize return types, define them, and modify sql3
to return them. Once that is done, we can remove the "typed" switch in the
`SQLResponse` json unmarshaller.
Another significant change is the modification to the `ExprDataType` interface:
```
type ExprDataType interface {
exprDataType()
TypeName() string
TypeDescription() string
TypeInfo() map[string]interface{}
}
```
I added two more methods in order to distinguish between a type (`DECIMAL`), its
description (`DECIMAL(2)`), and its type info (`"scale": int64(2)`). Currently,
the description can be used as the field definition in a CREATE TABLE statement,
but we may want to re-think that. Also, Decimal is the only type currently using
TypeInfo.
Finally, I tried to consilidate things around `dax.FieldType` instead of
comparing against parser types outside of sql3. We still have some sql3 parser
and planner types lurking about, but we can address those in future commits.
* Add some test coverage
* smoke test expected INT, now int
* minor fixes
* Introduce WireQueryResponse and related types
This also changes dax.FieldType to dax.BaseType.
* Populate WireQueryResponse correctly
Currently this is in the http handler, and in the queryer.
* Convert sql3 and dax tests to expect pilosa.WireQueryField in results
* fix PQL tests in the SQL defs
* Address a few of the skipped sql tests in dax
743 lines
22 KiB
Go
743 lines
22 KiB
Go
// Copyright 2022 Molecula Corp. All rights reserved.
|
|
|
|
package planner
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/molecula/featurebase/v3/sql3"
|
|
"github.com/molecula/featurebase/v3/sql3/parser"
|
|
)
|
|
|
|
// analyze a parser.Expr. returns the analyzed parser.Expr
|
|
func (p *ExecutionPlanner) analyzeExpression(expr parser.Expr, scope parser.Statement) (parser.Expr, error) {
|
|
if expr == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
switch e := expr.(type) {
|
|
case *parser.BinaryExpr:
|
|
return p.analyzeBinaryExpression(e, scope)
|
|
|
|
case *parser.BoolLit:
|
|
return e, nil
|
|
|
|
case *parser.Call:
|
|
return p.analyzeCallExpression(e, scope)
|
|
|
|
case *parser.CastExpr:
|
|
analyzedExpr, err := p.analyzeExpression(e.X, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
targetType, err := dataTypeFromParserType(e.Type)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !typesCanBeCast(analyzedExpr.DataType(), targetType) {
|
|
return nil, sql3.NewErrInvalidCast(analyzedExpr.Pos().Line, analyzedExpr.Pos().Column, analyzedExpr.DataType().TypeDescription(), targetType.TypeDescription())
|
|
}
|
|
e.X = analyzedExpr
|
|
e.ResultDataType = targetType
|
|
return e, nil
|
|
|
|
case *parser.ExprList:
|
|
for i, ex := range e.Exprs {
|
|
listExpr, err := p.analyzeExpression(ex, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Exprs[i] = listExpr
|
|
}
|
|
return e, nil
|
|
|
|
case *parser.Ident:
|
|
switch sc := scope.(type) {
|
|
case *parser.SelectStatement:
|
|
// turn *parser.Ident into *parser.QualifiedRef
|
|
if sc.Source == nil {
|
|
return nil, sql3.NewErrColumnNotFound(e.NamePos.Line, e.NamePos.Column, e.Name)
|
|
}
|
|
|
|
// go find the first ident in the source that matches
|
|
oc, err := sc.Source.OutputColumnNamed(e.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if oc == nil {
|
|
return nil, sql3.NewErrColumnNotFound(e.NamePos.Line, e.NamePos.Column, e.Name)
|
|
}
|
|
|
|
ident := &parser.QualifiedRef{
|
|
Table: &parser.Ident{
|
|
Name: oc.TableName,
|
|
NamePos: e.NamePos,
|
|
},
|
|
Column: &parser.Ident{
|
|
Name: oc.ColumnName,
|
|
NamePos: e.NamePos,
|
|
},
|
|
ColumnIndex: oc.ColumnIndex,
|
|
}
|
|
return p.analyzeExpression(ident, scope)
|
|
|
|
case *parser.InsertStatement:
|
|
return nil, sql3.NewErrColumnNotFound(e.NamePos.Line, e.NamePos.Column, e.Name)
|
|
|
|
default:
|
|
return nil, sql3.NewErrInternalf("unhandled scope type '%T'", sc)
|
|
}
|
|
|
|
case *parser.VariableRef:
|
|
switch sc := scope.(type) {
|
|
case *parser.BulkInsertStatement:
|
|
// get the name of the variable without the @
|
|
varname := e.VarName()
|
|
|
|
for idx, mi := range sc.MapList {
|
|
if strings.EqualFold(varname, mi.Name.Name) {
|
|
e.VariableIndex = idx
|
|
|
|
dataType, err := dataTypeFromParserType(mi.Type)
|
|
if err != nil {
|
|
return nil, sql3.NewErrUnknownType(e.NamePos.Line, e.NamePos.Column, mi.Type.String())
|
|
}
|
|
e.VarDataType = dataType
|
|
return e, nil
|
|
}
|
|
}
|
|
return nil, sql3.NewErrUnknownIdentifier(e.NamePos.Line, e.NamePos.Column, varname)
|
|
default:
|
|
return nil, sql3.NewErrInternalf("unhandled scope type '%T'", sc)
|
|
}
|
|
|
|
case *parser.NullLit:
|
|
return e, nil
|
|
|
|
case *parser.IntegerLit:
|
|
return e, nil
|
|
|
|
case *parser.FloatLit:
|
|
return e, nil
|
|
|
|
case *parser.StringLit:
|
|
return e, nil
|
|
|
|
case *parser.DateLit:
|
|
return e, nil
|
|
|
|
case *parser.ParenExpr:
|
|
pexpr, err := p.analyzeExpression(e.X, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.X = pexpr
|
|
return e, nil
|
|
|
|
case *parser.SetLiteralExpr:
|
|
for i, ex := range e.Members {
|
|
listExpr, err := p.analyzeExpression(ex, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Members[i] = listExpr
|
|
}
|
|
|
|
if len(e.Members) == 0 {
|
|
return nil, sql3.NewErrLiteralEmptySetNotAllowed(e.Lbracket.Line, e.Lbracket.Column)
|
|
}
|
|
|
|
setDataType := e.Members[0].DataType()
|
|
switch setDataType.(type) {
|
|
case *parser.DataTypeID, *parser.DataTypeInt:
|
|
//make sure everything else is an int
|
|
for _, mbr := range e.Members {
|
|
if !typeIsInteger(mbr.DataType()) {
|
|
return nil, sql3.NewErrIntExpressionExpected(mbr.Pos().Line, mbr.Pos().Column)
|
|
}
|
|
}
|
|
e.ResultDataType = parser.NewDataTypeIDSet()
|
|
|
|
case *parser.DataTypeString:
|
|
//make sure everything else is a string
|
|
for _, mbr := range e.Members {
|
|
if !typeIsString(mbr.DataType()) {
|
|
return nil, sql3.NewErrStringExpressionExpected(mbr.Pos().Line, mbr.Pos().Column)
|
|
}
|
|
}
|
|
e.ResultDataType = parser.NewDataTypeStringSet()
|
|
|
|
default:
|
|
return nil, sql3.NewErrSetLiteralMustContainIntOrString(e.Members[0].Pos().Line, e.Members[0].Pos().Column)
|
|
}
|
|
|
|
return e, nil
|
|
|
|
case *parser.TupleLiteralExpr:
|
|
memberTypes := make([]parser.ExprDataType, 0)
|
|
for i, ex := range e.Members {
|
|
memberExpr, err := p.analyzeExpression(ex, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Members[i] = memberExpr
|
|
memberTypes = append(memberTypes, memberExpr.DataType())
|
|
}
|
|
|
|
if len(e.Members) == 0 {
|
|
return nil, sql3.NewErrLiteralEmptyTupleNotAllowed(e.Lbrace.Line, e.Lbrace.Column)
|
|
}
|
|
e.ResultDataType = parser.NewDataTypeTuple(memberTypes)
|
|
|
|
return e, nil
|
|
|
|
case *parser.QualifiedRef:
|
|
switch sc := scope.(type) {
|
|
case *parser.SelectStatement:
|
|
|
|
if e.Table.Name == "" {
|
|
// there is no table or alias name in the qualifier so go look for the first matching column from any of the sources
|
|
oc, err := sc.Source.OutputColumnNamed(e.Column.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if oc != nil {
|
|
e.RefDataType = oc.Datatype
|
|
e.ColumnIndex = oc.ColumnIndex
|
|
return e, nil
|
|
|
|
}
|
|
return nil, sql3.NewErrColumnNotFound(e.Column.NamePos.Line, e.Column.NamePos.Column, e.Column.Name)
|
|
|
|
} else {
|
|
oc, err := sc.Source.OutputColumnQualifierNamed(e.Table.Name, e.Column.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if oc != nil {
|
|
e.RefDataType = oc.Datatype
|
|
e.ColumnIndex = oc.ColumnIndex
|
|
return e, nil
|
|
|
|
}
|
|
return nil, sql3.NewErrColumnNotFound(e.Column.NamePos.Line, e.Column.NamePos.Column, e.Column.Name)
|
|
}
|
|
|
|
default:
|
|
return nil, sql3.NewErrInternalf("unhandled scope type '%T'", sc)
|
|
}
|
|
|
|
case *parser.Range:
|
|
return p.analyzeRangeExpression(e, scope)
|
|
|
|
case *parser.CaseExpr:
|
|
operand, err := p.analyzeExpression(e.Operand, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Operand = operand
|
|
|
|
for i, ex := range e.Blocks {
|
|
block, err := p.analyzeCaseBlockExpression(ex, e, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Blocks[i] = block
|
|
}
|
|
|
|
elseExpr, err := p.analyzeExpression(e.ElseExpr, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.ElseExpr = elseExpr
|
|
|
|
//type checking...
|
|
if e.Operand != nil {
|
|
//we are "case expr when" form, so need to make sure that 'expr' and all block conditions are equatable
|
|
for _, blk := range e.Blocks {
|
|
if !typesAreComparable(e.Operand.DataType(), blk.Condition.DataType()) {
|
|
return nil, sql3.NewErrTypesAreNotEquatable(blk.Condition.Pos().Line, blk.Condition.Pos().Column, e.Operand.DataType().TypeDescription(), blk.Condition.DataType().TypeDescription())
|
|
}
|
|
}
|
|
} else {
|
|
//we are "case when" form, so need to make sure that all block conditions are bool
|
|
for _, blk := range e.Blocks {
|
|
if !typeIsBool(blk.Condition.DataType()) {
|
|
return nil, sql3.NewErrBooleanExpressionExpected(blk.Condition.Pos().Line, blk.Condition.Pos().Column)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(e.Blocks) == 0 {
|
|
return nil, sql3.NewErrInternalf("unexpected case blocks length")
|
|
}
|
|
|
|
//set the result type for the case to the type of the first block
|
|
caseType := e.Blocks[0].DataType()
|
|
|
|
//now check all the other blocks to make sure that each body is assignment compatible with that type
|
|
for _, blk := range e.Blocks {
|
|
if !typesAreAssignmentCompatible(caseType, blk.Body.DataType()) {
|
|
return nil, sql3.NewErrTypeAssignmentIncompatible(blk.Body.Pos().Line, blk.Body.Pos().Column, caseType.TypeName(), blk.Body.DataType().TypeName())
|
|
}
|
|
}
|
|
|
|
//if there is an else check that too
|
|
if e.ElseExpr != nil {
|
|
if !typesAreAssignmentCompatible(caseType, e.ElseExpr.DataType()) {
|
|
return nil, sql3.NewErrTypeAssignmentIncompatible(e.ElseExpr.Pos().Line, e.ElseExpr.Pos().Column, caseType.TypeName(), e.ElseExpr.DataType().TypeName())
|
|
}
|
|
}
|
|
|
|
e.ResultDataType = caseType
|
|
|
|
return e, nil
|
|
|
|
case *parser.UnaryExpr:
|
|
return p.analyzeUnaryExpression(e, scope)
|
|
|
|
case *parser.SelectStatement:
|
|
err := p.analyzeSelectStatement(e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// if we return more than one column
|
|
if len(e.Columns) > 1 {
|
|
return nil, sql3.NewErrInternalf("subquery must return only one column")
|
|
}
|
|
return e, nil
|
|
|
|
default:
|
|
return nil, sql3.NewErrInternalf("unexpected SQL expression type: %T", expr)
|
|
}
|
|
}
|
|
|
|
func (p *ExecutionPlanner) analyzeUnaryExpression(expr *parser.UnaryExpr, scope parser.Statement) (parser.Expr, error) {
|
|
|
|
x, err := p.analyzeExpression(expr.X, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.X = x
|
|
|
|
switch op := expr.Op; op {
|
|
|
|
//bitwise operators
|
|
case parser.BITNOT:
|
|
if !typeIsCompatibleWithBitwiseOperator(x.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithBitwiseOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
expr.ResultDataType = x.DataType()
|
|
return expr, nil
|
|
|
|
//arithmetic operators
|
|
case parser.PLUS, parser.MINUS:
|
|
if !typeIsCompatibleWithArithmeticOperator(x.DataType(), op) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithArithmeticOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
if typeIsInteger(x.DataType()) {
|
|
expr.ResultDataType = parser.NewDataTypeInt()
|
|
} else if typeIsDecimal(x.DataType()) {
|
|
fd, ok := x.DataType().(*parser.DataTypeDecimal)
|
|
if !ok {
|
|
return nil, sql3.NewErrInternalf("unexpected data type")
|
|
}
|
|
expr.ResultDataType = fd
|
|
} else {
|
|
return nil, sql3.NewErrInternalf("unexpected unary expression type: %T", x.DataType())
|
|
}
|
|
return expr, nil
|
|
|
|
default:
|
|
return nil, sql3.NewErrInternalf("unexpected unary expression operator: %s", op)
|
|
}
|
|
}
|
|
|
|
func (p *ExecutionPlanner) analyzeBinaryExpression(expr *parser.BinaryExpr, scope parser.Statement) (parser.Expr, error) {
|
|
|
|
//analyze both sides first
|
|
x, err := p.analyzeExpression(expr.X, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.X = x
|
|
y, err := p.analyzeExpression(expr.Y, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.Y = y
|
|
|
|
//handle operator
|
|
switch op := expr.Op; op {
|
|
|
|
//logical operators
|
|
case parser.AND, parser.OR:
|
|
if !typeIsCompatibleWithLogicalOperator(x.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithLogicalOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
if !typeIsCompatibleWithLogicalOperator(y.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithLogicalOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeDescription())
|
|
}
|
|
//logical operator so type of expr is bool
|
|
expr.ResultDataType = parser.NewDataTypeBool()
|
|
return expr, nil
|
|
|
|
//equality operators
|
|
case parser.EQ, parser.NE:
|
|
if !typeIsCompatibleWithEqualityOperator(x.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithEqualityOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
if !typeIsCompatibleWithEqualityOperator(y.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithEqualityOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeDescription())
|
|
}
|
|
if !typesAreComparable(x.DataType(), y.DataType()) {
|
|
return nil, sql3.NewErrTypesAreNotEquatable(x.Pos().Line, x.Pos().Column, x.DataType().TypeDescription(), y.DataType().TypeDescription())
|
|
}
|
|
//equality operator so type of expr is bool
|
|
expr.ResultDataType = parser.NewDataTypeBool()
|
|
return expr, nil
|
|
|
|
//comparison operators
|
|
case parser.LT, parser.LE, parser.GT, parser.GE:
|
|
if !typeIsCompatibleWithComparisonOperator(x.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithComparisonOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
if typeIsTimestamp(x.DataType()) && y.IsLiteral() && typeIsString(y.DataType()) {
|
|
// we have a string literal on the rhs being compared to a date so
|
|
// try to convert to a date literal
|
|
rhs, ok := y.(*parser.StringLit)
|
|
if !ok {
|
|
return nil, sql3.NewErrInternalf("unexpected expression type '%T'", y)
|
|
}
|
|
newRhs := rhs.ConvertToTimestamp()
|
|
if newRhs != nil {
|
|
expr.Y = newRhs
|
|
y = newRhs
|
|
}
|
|
}
|
|
if !typeIsCompatibleWithComparisonOperator(y.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithComparisonOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeDescription())
|
|
}
|
|
if !typesAreComparable(x.DataType(), y.DataType()) {
|
|
return nil, sql3.NewErrTypesAreNotEquatable(x.Pos().Line, x.Pos().Column, x.DataType().TypeDescription(), y.DataType().TypeDescription())
|
|
}
|
|
//comparison operator so type of expr is bool
|
|
expr.ResultDataType = parser.NewDataTypeBool()
|
|
return expr, nil
|
|
|
|
//arithmetic operators
|
|
case parser.PLUS, parser.MINUS, parser.STAR, parser.SLASH, parser.REM:
|
|
if !typeIsCompatibleWithArithmeticOperator(x.DataType(), op) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithArithmeticOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
if !typeIsCompatibleWithArithmeticOperator(y.DataType(), op) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithArithmeticOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeDescription())
|
|
}
|
|
|
|
coercedType, err := typesCoercedForArithmeticOperator(x.DataType(), y.DataType(), x.Pos())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.ResultDataType = coercedType
|
|
return expr, nil
|
|
|
|
/*
|
|
opx, okx := x.(*NumLiteralPlanExpresssion)
|
|
opy, oky := y.(*NumLiteralPlanExpresssion)
|
|
if okx && oky {
|
|
//both literals so we can fold
|
|
numx, err := strconv.Atoi(opx.value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
numy, err := strconv.Atoi(opy.value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch op {
|
|
case parser.PLUS:
|
|
value := numx + numy
|
|
return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil
|
|
|
|
case parser.MINUS:
|
|
value := numx - numy
|
|
return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil
|
|
|
|
case parser.STAR:
|
|
value := numx * numy
|
|
return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil
|
|
|
|
case parser.SLASH:
|
|
value := numx / numy
|
|
return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil
|
|
|
|
case parser.REM:
|
|
value := numx % numy
|
|
return NewNumLiteralPlanExpresssion(p, strconv.Itoa(value)), nil
|
|
|
|
default:
|
|
//run home to momma
|
|
return NewBinOpPlanExpression(p, x, expr.Op, y), nil
|
|
}
|
|
} else {
|
|
return NewBinOpPlanExpression(p, x, expr.Op, y), nil
|
|
}*/
|
|
|
|
//bitwise operators
|
|
case parser.BITAND, parser.BITOR, parser.LSHIFT, parser.RSHIFT:
|
|
if !typeIsCompatibleWithBitwiseOperator(x.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithBitwiseOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
if !typeIsCompatibleWithBitwiseOperator(y.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithBitwiseOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeDescription())
|
|
}
|
|
coercedType, err := typesCoercedForBitwiseOperator(x.DataType(), y.DataType(), x.Pos())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.ResultDataType = coercedType
|
|
return expr, nil
|
|
|
|
/*
|
|
opx, okx := x.(*NumLiteralPlanExpression)
|
|
opy, oky := y.(*NumLiteralPlanExpression)
|
|
if okx && oky {
|
|
//both literals so we can fold
|
|
numx, err := strconv.Atoi(opx.value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
numy, err := strconv.Atoi(opy.value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch op {
|
|
case parser.PLUS:
|
|
value := numx + numy
|
|
return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil
|
|
|
|
case parser.MINUS:
|
|
value := numx - numy
|
|
return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil
|
|
|
|
case parser.STAR:
|
|
value := numx * numy
|
|
return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil
|
|
|
|
case parser.SLASH:
|
|
value := numx / numy
|
|
return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil
|
|
|
|
case parser.REM:
|
|
value := numx % numy
|
|
return NewNumLiteralPlanExpression(p, strconv.Itoa(value)), nil
|
|
|
|
default:
|
|
//run home to momma
|
|
return newBinOpPlanExpression(x, expr.Op, y), nil
|
|
}
|
|
} else {
|
|
return newBinOpPlanExpression(x, expr.Op, y), nil
|
|
}*/
|
|
|
|
//null test
|
|
case parser.IS, parser.ISNOT:
|
|
_, ok := expr.Y.(*parser.NullLit)
|
|
if !ok {
|
|
return nil, sql3.NewErrInternalf("NULL expected")
|
|
}
|
|
//no type check against null...logical operator so type of expr is bool
|
|
expr.ResultDataType = parser.NewDataTypeBool()
|
|
return expr, nil
|
|
|
|
case parser.IN, parser.NOTIN:
|
|
lst, ok := y.(*parser.ExprList)
|
|
if !ok {
|
|
return nil, sql3.NewErrExpressionListExpected(y.Pos().Line, y.Pos().Column)
|
|
}
|
|
|
|
for idx, ex := range lst.Exprs {
|
|
|
|
//check to see if our expression is a select statement
|
|
//if it is it needs special handling
|
|
sel, ok := ex.(*parser.SelectStatement)
|
|
if ok {
|
|
//we have a select in the expression list so make sure it is the only thing in the expression list
|
|
if len(lst.Exprs) > 1 {
|
|
return nil, sql3.NewErrInternalf("expresion list should only contain one select statement")
|
|
}
|
|
//make sure select only returns one column
|
|
if len(sel.Columns) > 1 {
|
|
return nil, sql3.NewErrInternalf("select used as part of IN expression should only return one column")
|
|
}
|
|
if !typesAreComparable(x.DataType(), sel.Columns[0].Expr.DataType()) {
|
|
return nil, sql3.NewErrTypesAreNotEquatable(x.Pos().Line, x.Pos().Column, x.DataType().TypeDescription(), ex.DataType().TypeDescription())
|
|
}
|
|
|
|
//need to turn this into an inner join
|
|
selStmt, ok := scope.(*parser.SelectStatement)
|
|
if !ok {
|
|
return nil, sql3.NewErrInternalf("unexpected scope type '%T'", scope)
|
|
}
|
|
|
|
operator := &parser.JoinOperator{
|
|
Inner: expr.OpPos,
|
|
}
|
|
|
|
constraint := &parser.OnConstraint{
|
|
X: &parser.BinaryExpr{
|
|
X: expr.X,
|
|
Op: parser.EQ,
|
|
Y: sel.Columns[0].Expr,
|
|
},
|
|
}
|
|
|
|
if lhs, ok := selStmt.Source.(*parser.JoinClause); ok {
|
|
selStmt.Source = &parser.JoinClause{
|
|
X: lhs.X,
|
|
Operator: lhs.Operator,
|
|
Y: &parser.JoinClause{
|
|
X: lhs.Y,
|
|
Operator: operator,
|
|
Y: sel,
|
|
Constraint: constraint,
|
|
},
|
|
Constraint: lhs.Constraint,
|
|
}
|
|
} else {
|
|
selStmt.Source = &parser.JoinClause{
|
|
X: selStmt.Source,
|
|
Operator: operator,
|
|
Y: sel,
|
|
Constraint: constraint,
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
//not a sql statement
|
|
|
|
//handle the case of of tthe LHS of the expression being a timestamp, the RHS being a string literal
|
|
//if so, try to coerce to a timestamp
|
|
if typeIsTimestamp(x.DataType()) && typeIsString(ex.DataType()) && ex.IsLiteral() {
|
|
litExpr, ok := ex.(*parser.StringLit)
|
|
if ok {
|
|
tsLit := litExpr.ConvertToTimestamp()
|
|
if tsLit != nil {
|
|
ex = tsLit
|
|
lst.Exprs[idx] = ex
|
|
}
|
|
}
|
|
}
|
|
|
|
//make sure LHS and RHS types are comparable
|
|
if !typesAreComparable(x.DataType(), ex.DataType()) {
|
|
return nil, sql3.NewErrTypesAreNotEquatable(x.Pos().Line, x.Pos().Column, x.DataType().TypeDescription(), ex.DataType().TypeDescription())
|
|
}
|
|
}
|
|
|
|
expr.ResultDataType = parser.NewDataTypeBool()
|
|
return expr, nil
|
|
|
|
case parser.BETWEEN, parser.NOTBETWEEN:
|
|
if !typeIsRange(y.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithBetweenOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
|
|
ok, err := typesAreRangeComparable(x.DataType(), y.DataType())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, sql3.NewErrTypeIncompatibleWithBetweenOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
expr.ResultDataType = parser.NewDataTypeBool()
|
|
return expr, nil
|
|
|
|
case parser.CONCAT:
|
|
if !typeIsCompatibleWithConcatOperator(x.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithConcatOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
if !typeIsCompatibleWithConcatOperator(y.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithConcatOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeDescription())
|
|
}
|
|
expr.ResultDataType = parser.NewDataTypeString()
|
|
return expr, nil
|
|
|
|
case parser.LIKE, parser.NOTLIKE:
|
|
if !typeIsCompatibleWithLikeOperator(x.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithLikeOperator(x.Pos().Line, x.Pos().Column, op.String(), x.DataType().TypeDescription())
|
|
}
|
|
if !typeIsCompatibleWithLikeOperator(y.DataType()) {
|
|
return nil, sql3.NewErrTypeIncompatibleWithLikeOperator(y.Pos().Line, y.Pos().Column, op.String(), y.DataType().TypeDescription())
|
|
}
|
|
//comparison operator so type of expr is bool
|
|
expr.ResultDataType = parser.NewDataTypeBool()
|
|
return expr, nil
|
|
|
|
default:
|
|
return nil, sql3.NewErrInternalf("unexpected binary expression operator: %s", op)
|
|
}
|
|
}
|
|
|
|
func (p *ExecutionPlanner) analyzeRangeExpression(expr *parser.Range, scope parser.Statement) (parser.Expr, error) {
|
|
//analyze subscripts
|
|
x, err := p.analyzeExpression(expr.X, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.X = x
|
|
y, err := p.analyzeExpression(expr.Y, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.Y = y
|
|
|
|
//check to see if we have string literals that are actually dates
|
|
xLiteral, ok := x.(*parser.StringLit)
|
|
if ok {
|
|
tsLiteral := xLiteral.ConvertToTimestamp()
|
|
if tsLiteral != nil {
|
|
expr.X = tsLiteral
|
|
}
|
|
}
|
|
|
|
yLiteral, ok := y.(*parser.StringLit)
|
|
if ok {
|
|
tsLiteral := yLiteral.ConvertToTimestamp()
|
|
if tsLiteral != nil {
|
|
expr.Y = tsLiteral
|
|
}
|
|
}
|
|
|
|
if !typeCanBeUsedInRange(expr.X.DataType()) {
|
|
return nil, sql3.NewErrTypeCannotBeUsedAsRangeSubscript(expr.X.Pos().Line, expr.X.Pos().Column, expr.X.DataType().TypeDescription())
|
|
}
|
|
if !typeCanBeUsedInRange(expr.Y.DataType()) {
|
|
return nil, sql3.NewErrTypeCannotBeUsedAsRangeSubscript(expr.Y.Pos().Line, expr.Y.Pos().Column, expr.Y.DataType().TypeDescription())
|
|
}
|
|
if !typesOfRangeBoundsAreTheSame(expr.X.DataType(), expr.Y.DataType()) {
|
|
return nil, sql3.NewErrIncompatibleTypesForRangeSubscripts(expr.Pos().Line, expr.Pos().Column, expr.X.DataType().TypeName(), expr.Y.DataType().TypeName())
|
|
}
|
|
|
|
expr.ResultDataType = parser.NewDataTypeRange(expr.X.DataType())
|
|
|
|
return expr, nil
|
|
}
|
|
|
|
func (p *ExecutionPlanner) analyzeCaseBlockExpression(expr *parser.CaseBlock, caseScope *parser.CaseExpr, scope parser.Statement) (*parser.CaseBlock, error) {
|
|
x, err := p.analyzeExpression(expr.Body, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.Body = x
|
|
y, err := p.analyzeExpression(expr.Condition, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expr.Condition = y
|
|
|
|
return expr, nil
|
|
}
|