Files
2026-07-13 12:32:25 +08:00

287 lines
10 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// 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 expression
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/expression"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/server/functions/framework"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// InTuple represents a VALUE IN (<VALUES>) expression.
type InTuple struct {
leftExpr sql.Expression
rightExpr expression.Tuple
// These variables are used so that we can resolve the comparison functions once and reuse them as we iterate over rows.
// These are assigned in WithChildren, so refer there for more information.
staticLiteral *expression.Literal
arrayLiterals []*expression.Literal
compFuncs []framework.Function
}
var _ vitess.Injectable = (*InTuple)(nil)
var _ sql.Expression = (*InTuple)(nil)
var _ expression.BinaryExpression = (*InTuple)(nil)
var _ sql.IndexComparisonExpression = (*InTuple)(nil)
// NewInTuple returns a new *InTuple.
func NewInTuple() *InTuple {
return &InTuple{
leftExpr: nil,
rightExpr: nil,
}
}
// Children implements the sql.Expression interface.
func (it *InTuple) Children() []sql.Expression {
return []sql.Expression{it.leftExpr, it.rightExpr}
}
// Decay returns the expression as a series of OR expressions. The behavior is not the same, however it allows some
// paths to simplify their expression handling (such as filters).
func (it *InTuple) Decay() sql.Expression {
switch f := it.compFuncs[0].(type) {
case *framework.CompiledFunction:
f.Arguments = []sql.Expression{it.leftExpr, it.rightExpr[0]}
case *framework.QuickFunction2:
f.Arguments = [2]sql.Expression{it.leftExpr, it.rightExpr[0]}
}
var expr sql.Expression = &BinaryOperator{
operator: framework.Operator_BinaryEqual,
compiledFunc: it.compFuncs[0],
}
for i := 1; i < len(it.rightExpr); i++ {
switch f := it.compFuncs[i].(type) {
case *framework.CompiledFunction:
f.Arguments = []sql.Expression{it.leftExpr, it.rightExpr[i]}
case *framework.QuickFunction2:
f.Arguments = [2]sql.Expression{it.leftExpr, it.rightExpr[i]}
}
expr = expression.NewOr(expr, &BinaryOperator{
operator: framework.Operator_BinaryEqual,
compiledFunc: it.compFuncs[i],
})
}
return expr
}
// Eval implements the sql.Expression interface.
func (it *InTuple) Eval(ctx *sql.Context, row sql.Row) (any, error) {
if len(it.compFuncs) == 0 {
return nil, errors.Errorf("%T: cannot Eval as it has not been fully resolved", it)
}
// First we'll evaluate everything before we do the comparisons
left, err := it.leftExpr.Eval(ctx, row)
if err != nil {
return nil, err
}
if left == nil {
return nil, nil
}
rightInterface, err := it.rightExpr.Eval(ctx, row)
if err != nil {
return nil, err
}
rightValues, ok := rightInterface.([]any)
if !ok {
// Tuples will return the value directly if it has a length of one, so we'll check for that first
if len(it.rightExpr) == 1 {
rightValues = []any{rightInterface}
} else {
return nil, errors.Errorf("%T: expected right child to return `%T` but returned `%T`", it, []any{}, rightInterface)
}
}
// Next we'll assign our evaluated values to the expressions that the comparison functions reference
// Note that the compiled functions already have a reference to this literal, so we have to edit it in place
it.staticLiteral.Val = left
for i, rightValue := range rightValues {
it.arrayLiterals[i].Val = rightValue
}
// Now we can loop over all of the comparison functions, as they'll reference their respective values
// The rules for null comparisons are subtle: an IN expression that includes a NULL in the tuple will return null
// instead of false if a match is not found, but true otherwise.
sawNull := false
for _, compFunc := range it.compFuncs {
result, err := compFunc.Eval(ctx, row)
if err != nil {
return nil, err
}
if result == nil {
sawNull = true
} else if result.(bool) {
return true, nil
}
}
if sawNull {
return nil, nil
}
return false, nil
}
// IsNullable implements the sql.Expression interface.
func (it *InTuple) IsNullable(ctx *sql.Context) bool {
return true
}
// Resolved implements the sql.Expression interface.
func (it *InTuple) Resolved() bool {
if it.leftExpr == nil || !it.leftExpr.Resolved() || it.rightExpr == nil || !it.rightExpr.Resolved() || len(it.compFuncs) == 0 {
return false
}
for _, compFunc := range it.compFuncs {
if !compFunc.Resolved() {
return false
}
}
return true
}
// String implements the sql.Expression interface.
func (it *InTuple) String() string {
if it.leftExpr == nil || it.rightExpr == nil {
return "? IN ?"
}
return fmt.Sprintf("%s IN %s", it.leftExpr.String(), it.rightExpr.String())
}
// Type implements the sql.Expression interface.
func (it *InTuple) Type(ctx *sql.Context) sql.Type {
return pgtypes.Bool
}
// WithChildren implements the sql.Expression interface.
func (it *InTuple) WithChildren(ctx *sql.Context, children ...sql.Expression) (sql.Expression, error) {
if len(children) != 2 {
return nil, sql.ErrInvalidChildrenNumber.New(it, len(children), 2)
}
rightTuple, ok := children[1].(expression.Tuple)
if !ok {
return nil, errors.Errorf("%T: expected right child to be `%T` but has type `%T`", it, expression.Tuple{}, children[1])
}
if len(rightTuple) == 0 {
return nil, errors.Errorf("IN must contain at least 1 expression")
}
// We'll only resolve the comparison functions once we have all Doltgres types.
// We may see GMS types during some analyzer steps, so we should wait until those are done.
if leftType, ok := children[0].Type(ctx).(*pgtypes.DoltgresType); ok {
// Rather than finding and resolving a comparison function every time we call Eval, we resolve them once and
// reuse the functions. We also want to avoid re-assigning the parameters of the comparison functions since that
// will also cause the functions to resolve again. To do this, we store expressions within our struct that the
// functions reference, so we can freely switch the values within the literals without changing anything
// regarding the comparison functions. This is usually unsafe, but since we're verifying the types returned by
// the parameters, and assigning the values to our own literals, we do not have to worry. This offers a
// significant speedup as function resolution is very expensive, so we want to do it as few times as possible
// (preferably once).
staticLiteral := expression.NewLiteral(nil, leftType)
arrayLiterals := make([]*expression.Literal, len(rightTuple))
// Each expression may be a different type (which is valid), so we need a comparison function for each expression.
compFuncs := make([]framework.Function, len(rightTuple))
allValidChildren := true
for i, rightExpr := range rightTuple {
rightType, ok := rightExpr.Type(ctx).(*pgtypes.DoltgresType)
if !ok {
allValidChildren = false
break
}
arrayLiterals[i] = expression.NewLiteral(nil, rightType)
compFunc := framework.GetBinaryFunction(framework.Operator_BinaryEqual).Compile(ctx, "internal_in_comparison", staticLiteral, arrayLiterals[i])
if compFunc == nil {
return nil, errors.Errorf("operator does not exist: %s = %s", leftType.String(), rightType.String())
}
cid := compFunc.Type(ctx).(*pgtypes.DoltgresType).ID
if cid != pgtypes.Bool.ID {
// Prepared statement binding values will need explicit casting to appropriate type
ec := NewAssignmentCast(arrayLiterals[i], pgtypes.Unknown, staticLiteral.Type(ctx).(*pgtypes.DoltgresType))
compFunc = framework.GetBinaryFunction(framework.Operator_BinaryEqual).Compile(ctx, "internal_in_comparison", staticLiteral, ec)
if compFunc == nil || compFunc.StashedError() != nil {
return nil, errors.Errorf("operator does not exist: %s = %s", leftType.String(), rightType.String())
}
cid = compFunc.Type(ctx).(*pgtypes.DoltgresType).ID
if cid != pgtypes.Bool.ID {
// This should never happen, but this is just to be safe
return nil, errors.Errorf("%T: found equality comparison that does not return a bool", it)
}
}
compFuncs[i] = compFunc
}
if allValidChildren {
return &InTuple{
leftExpr: children[0],
rightExpr: rightTuple,
staticLiteral: staticLiteral,
arrayLiterals: arrayLiterals,
compFuncs: compFuncs,
}, nil
}
}
return &InTuple{
leftExpr: children[0],
rightExpr: rightTuple,
}, nil
}
// WithResolvedChildren implements the vitess.InjectableExpression interface.
func (it *InTuple) WithResolvedChildren(ctx context.Context, children []any) (any, error) {
if len(children) != 2 {
return nil, errors.Errorf("invalid vitess child count, expected `2` but got `%d`", len(children))
}
sqlCtx := ctx.(*sql.Context)
left, ok := children[0].(sql.Expression)
if !ok {
return nil, errors.Errorf("expected vitess child to be an expression but has type `%T`", children[0])
}
switch right := children[1].(type) {
case expression.Tuple:
return it.WithChildren(sqlCtx, left, right)
case *RecordExpr:
// TODO: For now, if we see a RecordExpr come in, we convert it to a vitess Tuple representation, so that
// the existing in_tuple code can work with it. Alternatively, we could change in_tuple to always
// work directly with a Record expression.
return it.WithChildren(sqlCtx, left, expression.Tuple(right.exprs))
default:
return nil, errors.Errorf("expected child to be a RecordExpr or vitess Tuple but has type `%T`", children[1])
}
}
// Left implements the expression.BinaryExpression interface.
func (it *InTuple) Left() sql.Expression {
return it.leftExpr
}
// Right implements the expression.BinaryExpression interface.
func (it *InTuple) Right() sql.Expression {
return it.rightExpr
}
// IndexScanOperation implements the sql.IndexComparisonExpression interface.
func (it *InTuple) IndexScanOperation() (sql.IndexScanOp, sql.Expression, sql.Expression, bool) {
return sql.IndexScanOpInSet, it.leftExpr, it.rightExpr, true
}