blob: 4bdb8c148b11deba2f3313f2990ed1b001c4b719 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 iceberg
import (
"fmt"
"reflect"
"github.com/google/uuid"
)
//go:generate stringer -type=Operation -linecomment
// Operation is an enum used for constants to define what operation a given
// expression or predicate is going to execute.
type Operation int
const (
// do not change the order of these enum constants.
// they are grouped for quick validation of operation type by
// using <= and >= of the first/last operation in a group
OpTrue Operation = iota // True
OpFalse // False
// unary ops
OpIsNull // IsNull
OpNotNull // NotNull
OpIsNan // IsNaN
OpNotNan // NotNaN
// literal ops
OpLT // LessThan
OpLTEQ // LessThanEqual
OpGT // GreaterThan
OpGTEQ // GreaterThanEqual
OpEQ // Equal
OpNEQ // NotEqual
OpStartsWith // StartsWith
OpNotStartsWith // NotStartsWith
// set ops
OpIn // In
OpNotIn // NotIn
// boolean ops
OpNot // Not
OpAnd // And
OpOr // Or
)
// Negate returns the inverse operation for a given op
func (op Operation) Negate() Operation {
switch op {
case OpIsNull:
return OpNotNull
case OpNotNull:
return OpIsNull
case OpIsNan:
return OpNotNan
case OpNotNan:
return OpIsNan
case OpLT:
return OpGTEQ
case OpLTEQ:
return OpGT
case OpGT:
return OpLTEQ
case OpGTEQ:
return OpLT
case OpEQ:
return OpNEQ
case OpNEQ:
return OpEQ
case OpIn:
return OpNotIn
case OpNotIn:
return OpIn
case OpStartsWith:
return OpNotStartsWith
case OpNotStartsWith:
return OpStartsWith
default:
panic("no negation for operation " + op.String())
}
}
// FlipLR returns the correct operation to use if the left and right operands
// are flipped.
func (op Operation) FlipLR() Operation {
switch op {
case OpLT:
return OpGT
case OpLTEQ:
return OpGTEQ
case OpGT:
return OpLT
case OpGTEQ:
return OpLTEQ
case OpAnd:
return OpAnd
case OpOr:
return OpOr
default:
panic("no left-right flip for operation: " + op.String())
}
}
// BooleanExpression represents a full expression which will evaluate to a
// boolean value such as GreaterThan or StartsWith, etc.
type BooleanExpression interface {
fmt.Stringer
Op() Operation
Negate() BooleanExpression
Equals(BooleanExpression) bool
}
// AlwaysTrue is the boolean expression "True"
type AlwaysTrue struct{}
func (AlwaysTrue) String() string { return "AlwaysTrue()" }
func (AlwaysTrue) Op() Operation { return OpTrue }
func (AlwaysTrue) Negate() BooleanExpression { return AlwaysFalse{} }
func (AlwaysTrue) Equals(other BooleanExpression) bool {
_, ok := other.(AlwaysTrue)
return ok
}
// AlwaysFalse is the boolean expression "False"
type AlwaysFalse struct{}
func (AlwaysFalse) String() string { return "AlwaysFalse()" }
func (AlwaysFalse) Op() Operation { return OpFalse }
func (AlwaysFalse) Negate() BooleanExpression { return AlwaysTrue{} }
func (AlwaysFalse) Equals(other BooleanExpression) bool {
_, ok := other.(AlwaysFalse)
return ok
}
type NotExpr struct {
child BooleanExpression
}
// NewNot creates a BooleanExpression representing a "Not" operation on the given
// argument. It will optimize slightly though:
//
// If the argument is AlwaysTrue or AlwaysFalse, the appropriate inverse expression
// will be returned directly. If the argument is itself a NotExpr, then the child
// will be returned rather than NotExpr(NotExpr(child)).
func NewNot(child BooleanExpression) BooleanExpression {
if child == nil {
panic(fmt.Errorf("%w: cannot create NotExpr with nil child",
ErrInvalidArgument))
}
switch t := child.(type) {
case NotExpr:
return t.child
case AlwaysTrue:
return AlwaysFalse{}
case AlwaysFalse:
return AlwaysTrue{}
}
return NotExpr{child: child}
}
func (n NotExpr) String() string { return "Not(child=" + n.child.String() + ")" }
func (NotExpr) Op() Operation { return OpNot }
func (n NotExpr) Negate() BooleanExpression { return n.child }
func (n NotExpr) Equals(other BooleanExpression) bool {
rhs, ok := other.(NotExpr)
if !ok {
return false
}
return n.child.Equals(rhs.child)
}
type AndExpr struct {
left, right BooleanExpression
}
func newAnd(left, right BooleanExpression) BooleanExpression {
if left == nil || right == nil {
panic(fmt.Errorf("%w: cannot construct AndExpr with nil arguments",
ErrInvalidArgument))
}
switch {
case left == AlwaysFalse{} || right == AlwaysFalse{}:
return AlwaysFalse{}
case left == AlwaysTrue{}:
return right
case right == AlwaysTrue{}:
return left
}
return AndExpr{left: left, right: right}
}
// NewAnd will construct a new AndExpr, allowing the caller to provide potentially
// more than just two arguments which will be folded to create an appropriate expression
// tree. i.e. NewAnd(a, b, c, d) becomes AndExpr(a, AndExpr(b, AndExpr(c, d)))
//
// Slight optimizations are performed on creation if either argument is AlwaysFalse
// or AlwaysTrue by performing reductions. If any argument is AlwaysFalse, then everything
// will get folded to a return of AlwaysFalse. If an argument is AlwaysTrue, then the other
// argument will be returned directly rather than creating an AndExpr.
//
// Will panic if any argument is nil
func NewAnd(left, right BooleanExpression, addl ...BooleanExpression) BooleanExpression {
folded := newAnd(left, right)
for _, a := range addl {
folded = newAnd(folded, a)
}
return folded
}
func (a AndExpr) String() string {
return "And(left=" + a.left.String() + ", right=" + a.right.String() + ")"
}
func (AndExpr) Op() Operation { return OpAnd }
func (a AndExpr) Equals(other BooleanExpression) bool {
rhs, ok := other.(AndExpr)
if !ok {
return false
}
return (a.left.Equals(rhs.left) && a.right.Equals(rhs.right)) ||
(a.left.Equals(rhs.right) && a.right.Equals(rhs.left))
}
func (a AndExpr) Negate() BooleanExpression {
return NewOr(a.left.Negate(), a.right.Negate())
}
type OrExpr struct {
left, right BooleanExpression
}
func newOr(left, right BooleanExpression) BooleanExpression {
if left == nil || right == nil {
panic(fmt.Errorf("%w: cannot construct OrExpr with nil arguments",
ErrInvalidArgument))
}
switch {
case left == AlwaysTrue{} || right == AlwaysTrue{}:
return AlwaysTrue{}
case left == AlwaysFalse{}:
return right
case right == AlwaysFalse{}:
return left
}
return OrExpr{left: left, right: right}
}
// NewOr will construct a new OrExpr, allowing the caller to provide potentially
// more than just two arguments which will be folded to create an appropriate expression
// tree. i.e. NewOr(a, b, c, d) becomes OrExpr(a, OrExpr(b, OrExpr(c, d)))
//
// Slight optimizations are performed on creation if either argument is AlwaysFalse
// or AlwaysTrue by performing reductions. If any argument is AlwaysTrue, then everything
// will get folded to a return of AlwaysTrue. If an argument is AlwaysFalse, then the other
// argument will be returned directly rather than creating an OrExpr.
//
// Will panic if any argument is nil
func NewOr(left, right BooleanExpression, addl ...BooleanExpression) BooleanExpression {
folded := newOr(left, right)
for _, a := range addl {
folded = newOr(folded, a)
}
return folded
}
func (o OrExpr) String() string {
return "Or(left=" + o.left.String() + ", right=" + o.right.String() + ")"
}
func (OrExpr) Op() Operation { return OpOr }
func (o OrExpr) Equals(other BooleanExpression) bool {
rhs, ok := other.(OrExpr)
if !ok {
return false
}
return (o.left.Equals(rhs.left) && o.right.Equals(rhs.right)) ||
(o.left.Equals(rhs.right) && o.right.Equals(rhs.left))
}
func (o OrExpr) Negate() BooleanExpression {
return NewAnd(o.left.Negate(), o.right.Negate())
}
// A Term is a simple expression that evaluates to a value
type Term interface {
fmt.Stringer
// requiring this method ensures that only types we define can be used
// as a term.
isTerm()
}
// UnboundTerm is an expression that evaluates to a value that isn't yet bound
// to a schema, thus it isn't yet known what the type will be.
type UnboundTerm interface {
Term
Equals(UnboundTerm) bool
Bind(schema *Schema, caseSensitive bool) (BoundTerm, error)
}
// BoundTerm is a simple expression (typically a reference) that evaluates to a
// value and has been bound to a schema.
type BoundTerm interface {
Term
Equals(BoundTerm) bool
Ref() BoundReference
Type() Type
evalToLiteral(structLike) Optional[Literal]
evalIsNull(structLike) bool
}
// unbound is a generic interface representing something that is not yet bound
// to a particular type.
type unbound[B any] interface {
Bind(schema *Schema, caseSensitive bool) (B, error)
}
// An UnboundPredicate represents a boolean predicate expression which has not
// yet been bound to a schema. Binding it will produce a BooleanExpression.
//
// BooleanExpression is used for the binding result because we may optimize and
// return AlwaysTrue / AlwaysFalse in some scenarios during binding which are
// not considered to be "Bound" as they do not have a bound Term or Reference.
type UnboundPredicate interface {
BooleanExpression
unbound[BooleanExpression]
Term() UnboundTerm
}
// BoundPredicate is a boolean predicate expression which has been bound to a schema.
// The underlying reference and term can be retrieved from it.
type BoundPredicate interface {
BooleanExpression
Ref() BoundReference
Term() BoundTerm
}
// Reference is a field name not yet bound to a particular field in a schema
type Reference string
func (r Reference) String() string {
return "Reference(name='" + string(r) + "')"
}
func (Reference) isTerm() {}
func (r Reference) Equals(other UnboundTerm) bool {
rhs, ok := other.(Reference)
if !ok {
return false
}
return r == rhs
}
func (r Reference) Bind(s *Schema, caseSensitive bool) (BoundTerm, error) {
var (
field NestedField
found bool
)
if caseSensitive {
field, found = s.FindFieldByName(string(r))
} else {
field, found = s.FindFieldByNameCaseInsensitive(string(r))
}
if !found {
return nil, fmt.Errorf("%w: could not bind reference '%s', caseSensitive=%t",
ErrInvalidSchema, string(r), caseSensitive)
}
acc, ok := s.accessorForField(field.ID)
if !ok {
return nil, ErrInvalidSchema
}
return createBoundRef(field, acc), nil
}
// BoundReference is a named reference that has been bound to a particular field
// in a given schema.
type BoundReference interface {
BoundTerm
Field() NestedField
Pos() int
}
type boundRef[T LiteralType] struct {
field NestedField
acc accessor
}
func createBoundRef(field NestedField, acc accessor) BoundReference {
switch field.Type.(type) {
case BooleanType:
return &boundRef[bool]{field: field, acc: acc}
case Int32Type:
return &boundRef[int32]{field: field, acc: acc}
case Int64Type:
return &boundRef[int64]{field: field, acc: acc}
case Float32Type:
return &boundRef[float32]{field: field, acc: acc}
case Float64Type:
return &boundRef[float64]{field: field, acc: acc}
case DateType:
return &boundRef[Date]{field: field, acc: acc}
case TimeType:
return &boundRef[Time]{field: field, acc: acc}
case TimestampType, TimestampTzType:
return &boundRef[Timestamp]{field: field, acc: acc}
case StringType:
return &boundRef[string]{field: field, acc: acc}
case FixedType, BinaryType:
return &boundRef[[]byte]{field: field, acc: acc}
case DecimalType:
return &boundRef[Decimal]{field: field, acc: acc}
case UUIDType:
return &boundRef[uuid.UUID]{field: field, acc: acc}
}
panic("unhandled bound reference type: " + field.Type.String())
}
func (b *boundRef[T]) Pos() int { return b.acc.pos }
func (*boundRef[T]) isTerm() {}
func (b *boundRef[T]) String() string {
return fmt.Sprintf("BoundReference(field=%s, accessor=%s)", b.field, &b.acc)
}
func (b *boundRef[T]) Equals(other BoundTerm) bool {
rhs, ok := other.(*boundRef[T])
if !ok {
return false
}
return b.field.Equals(rhs.field)
}
func (b *boundRef[T]) Ref() BoundReference { return b }
func (b *boundRef[T]) Field() NestedField { return b.field }
func (b *boundRef[T]) Type() Type { return b.field.Type }
func (b *boundRef[T]) eval(st structLike) Optional[T] {
switch v := b.acc.Get(st).(type) {
case nil:
return Optional[T]{}
case T:
return Optional[T]{Valid: true, Val: v}
default:
var z T
typ, val := reflect.TypeOf(z), reflect.ValueOf(v)
if !val.CanConvert(typ) {
panic(fmt.Errorf("%w: cannot convert value '%+v' to expected type %s",
ErrInvalidSchema, val.Interface(), typ.String()))
}
return Optional[T]{
Valid: true,
Val: val.Convert(typ).Interface().(T),
}
}
}
func (b *boundRef[T]) evalToLiteral(st structLike) Optional[Literal] {
v := b.eval(st)
if !v.Valid {
return Optional[Literal]{}
}
lit := NewLiteral[T](v.Val)
if !lit.Type().Equals(b.field.Type) {
lit, _ = lit.To(b.field.Type)
}
return Optional[Literal]{Val: lit, Valid: true}
}
func (b *boundRef[T]) evalIsNull(st structLike) bool {
v := b.eval(st)
return !v.Valid
}
// UnaryPredicate creates and returns an unbound predicate for the provided unary operation.
// Will panic if op is not a unary operation.
func UnaryPredicate(op Operation, t UnboundTerm) UnboundPredicate {
if op < OpIsNull || op > OpNotNan {
panic(fmt.Errorf("%w: invalid operation for unary predicate: %s",
ErrInvalidArgument, op))
}
if t == nil {
panic(fmt.Errorf("%w: cannot create unary predicate with nil term",
ErrInvalidArgument))
}
return &unboundUnaryPredicate{op: op, term: t}
}
type unboundUnaryPredicate struct {
op Operation
term UnboundTerm
}
func (up *unboundUnaryPredicate) String() string {
return fmt.Sprintf("%s(term=%s)", up.op, up.term)
}
func (up *unboundUnaryPredicate) Equals(other BooleanExpression) bool {
rhs, ok := other.(*unboundUnaryPredicate)
if !ok {
return false
}
return up.op == rhs.op && up.term.Equals(rhs.term)
}
func (up *unboundUnaryPredicate) Op() Operation { return up.op }
func (up *unboundUnaryPredicate) Negate() BooleanExpression {
return &unboundUnaryPredicate{op: up.op.Negate(), term: up.term}
}
func (up *unboundUnaryPredicate) Term() UnboundTerm { return up.term }
func (up *unboundUnaryPredicate) Bind(schema *Schema, caseSensitive bool) (BooleanExpression, error) {
bound, err := up.term.Bind(schema, caseSensitive)
if err != nil {
return nil, err
}
// fast case optimizations
switch up.op {
case OpIsNull:
if bound.Ref().Field().Required && !schema.FieldHasOptionalParent(bound.Ref().Field().ID) {
return AlwaysFalse{}, nil
}
case OpNotNull:
if bound.Ref().Field().Required && !schema.FieldHasOptionalParent(bound.Ref().Field().ID) {
return AlwaysTrue{}, nil
}
case OpIsNan:
if !bound.Type().Equals(PrimitiveTypes.Float32) && !bound.Type().Equals(PrimitiveTypes.Float64) {
return AlwaysFalse{}, nil
}
case OpNotNan:
if !bound.Type().Equals(PrimitiveTypes.Float32) && !bound.Type().Equals(PrimitiveTypes.Float64) {
return AlwaysTrue{}, nil
}
}
return createBoundUnaryPredicate(up.op, bound), nil
}
// BoundUnaryPredicate is a bound predicate expression that has no arguments
type BoundUnaryPredicate interface {
BoundPredicate
AsUnbound(Reference) UnboundPredicate
}
type bound[T LiteralType] interface {
BoundTerm
eval(structLike) Optional[T]
}
func newBoundUnaryPred[T LiteralType](op Operation, term BoundTerm) BoundUnaryPredicate {
return &boundUnaryPredicate[T]{op: op, term: term.(bound[T])}
}
func createBoundUnaryPredicate(op Operation, term BoundTerm) BoundUnaryPredicate {
switch term.Type().(type) {
case BooleanType:
return newBoundUnaryPred[bool](op, term)
case Int32Type:
return newBoundUnaryPred[int32](op, term)
case Int64Type:
return newBoundUnaryPred[int64](op, term)
case Float32Type:
return newBoundUnaryPred[float32](op, term)
case Float64Type:
return newBoundUnaryPred[float64](op, term)
case DateType:
return newBoundUnaryPred[Date](op, term)
case TimeType:
return newBoundUnaryPred[Time](op, term)
case TimestampType, TimestampTzType:
return newBoundUnaryPred[Timestamp](op, term)
case StringType:
return newBoundUnaryPred[string](op, term)
case FixedType, BinaryType:
return newBoundUnaryPred[[]byte](op, term)
case DecimalType:
return newBoundUnaryPred[Decimal](op, term)
case UUIDType:
return newBoundUnaryPred[uuid.UUID](op, term)
}
panic("unhandled bound reference type: " + term.Type().String())
}
type boundUnaryPredicate[T LiteralType] struct {
op Operation
term bound[T]
}
func (bp *boundUnaryPredicate[T]) AsUnbound(r Reference) UnboundPredicate {
return &unboundUnaryPredicate{op: bp.op, term: r}
}
func (bp *boundUnaryPredicate[T]) Equals(other BooleanExpression) bool {
rhs, ok := other.(*boundUnaryPredicate[T])
if !ok {
return false
}
return bp.op == rhs.op && bp.term.Equals(rhs.term)
}
func (bp *boundUnaryPredicate[T]) Op() Operation { return bp.op }
func (bp *boundUnaryPredicate[T]) Negate() BooleanExpression {
return &boundUnaryPredicate[T]{op: bp.op.Negate(), term: bp.term}
}
func (bp *boundUnaryPredicate[T]) Term() BoundTerm { return bp.term }
func (bp *boundUnaryPredicate[T]) Ref() BoundReference { return bp.term.Ref() }
func (bp *boundUnaryPredicate[T]) String() string {
return fmt.Sprintf("Bound%s(term=%s)", bp.op, bp.term)
}
// LiteralPredicate constructs an unbound predicate for an operation that requires
// a single literal argument, such as LessThan or StartsWith.
//
// Panics if the operation provided is not a valid Literal operation,
// if the term is nil or if the literal is nil.
func LiteralPredicate(op Operation, t UnboundTerm, lit Literal) UnboundPredicate {
switch {
case op < OpLT || op > OpNotStartsWith:
panic(fmt.Errorf("%w: invalid operation for LiteralPredicate: %s",
ErrInvalidArgument, op))
case t == nil:
panic(fmt.Errorf("%w: cannot create literal predicate with nil term",
ErrInvalidArgument))
case lit == nil:
panic(fmt.Errorf("%w: cannot create literal predicate with nil literal",
ErrInvalidArgument))
}
return &unboundLiteralPredicate{op: op, term: t, lit: lit}
}
type unboundLiteralPredicate struct {
op Operation
term UnboundTerm
lit Literal
}
func (ul *unboundLiteralPredicate) String() string {
return fmt.Sprintf("%s(term=%s, literal=%s)", ul.op, ul.term, ul.lit)
}
func (ul *unboundLiteralPredicate) Equals(other BooleanExpression) bool {
rhs, ok := other.(*unboundLiteralPredicate)
if !ok {
return false
}
return ul.op == rhs.op && ul.term.Equals(rhs.term) && ul.lit.Equals(rhs.lit)
}
func (ul *unboundLiteralPredicate) Op() Operation { return ul.op }
func (ul *unboundLiteralPredicate) Negate() BooleanExpression {
return &unboundLiteralPredicate{op: ul.op.Negate(), term: ul.term, lit: ul.lit}
}
func (ul *unboundLiteralPredicate) Term() UnboundTerm { return ul.term }
func (ul *unboundLiteralPredicate) Bind(schema *Schema, caseSensitive bool) (BooleanExpression, error) {
bound, err := ul.term.Bind(schema, caseSensitive)
if err != nil {
return nil, err
}
if (ul.op == OpStartsWith || ul.op == OpNotStartsWith) &&
!(bound.Type().Equals(PrimitiveTypes.String) || bound.Type().Equals(PrimitiveTypes.Binary)) {
return nil, fmt.Errorf("%w: StartsWith and NotStartsWith must bind to String type, not %s",
ErrType, bound.Type())
}
lit, err := ul.lit.To(bound.Type())
if err != nil {
return nil, err
}
switch lit.(type) {
case AboveMaxLiteral:
switch ul.op {
case OpLT, OpLTEQ, OpNEQ:
return AlwaysTrue{}, nil
case OpGT, OpGTEQ, OpEQ:
return AlwaysFalse{}, nil
}
case BelowMinLiteral:
switch ul.op {
case OpLT, OpLTEQ, OpEQ:
return AlwaysFalse{}, nil
case OpGT, OpGTEQ, OpNEQ:
return AlwaysTrue{}, nil
}
}
return createBoundLiteralPredicate(ul.op, bound, lit)
}
// BoundLiteralPredicate represents a bound boolean expression that utilizes a single
// literal as an argument, such as Equals or StartsWith.
type BoundLiteralPredicate interface {
BoundPredicate
Literal() Literal
AsUnbound(Reference, Literal) UnboundPredicate
}
func newBoundLiteralPredicate[T LiteralType](op Operation, term BoundTerm, lit Literal) BoundPredicate {
return &boundLiteralPredicate[T]{op: op, term: term.(bound[T]),
lit: lit.(TypedLiteral[T])}
}
func createBoundLiteralPredicate(op Operation, term BoundTerm, lit Literal) (BoundPredicate, error) {
finalLit, err := lit.To(term.Type())
if err != nil {
return nil, err
}
switch term.Type().(type) {
case BooleanType:
return newBoundLiteralPredicate[bool](op, term, finalLit), nil
case Int32Type:
return newBoundLiteralPredicate[int32](op, term, finalLit), nil
case Int64Type:
return newBoundLiteralPredicate[int64](op, term, finalLit), nil
case Float32Type:
return newBoundLiteralPredicate[float32](op, term, finalLit), nil
case Float64Type:
return newBoundLiteralPredicate[float64](op, term, finalLit), nil
case DateType:
return newBoundLiteralPredicate[Date](op, term, finalLit), nil
case TimeType:
return newBoundLiteralPredicate[Time](op, term, finalLit), nil
case TimestampType, TimestampTzType:
return newBoundLiteralPredicate[Timestamp](op, term, finalLit), nil
case StringType:
return newBoundLiteralPredicate[string](op, term, finalLit), nil
case FixedType, BinaryType:
return newBoundLiteralPredicate[[]byte](op, term, finalLit), nil
case DecimalType:
return newBoundLiteralPredicate[Decimal](op, term, finalLit), nil
case UUIDType:
return newBoundLiteralPredicate[uuid.UUID](op, term, finalLit), nil
}
return nil, fmt.Errorf("%w: could not create bound literal predicate for term type %s",
ErrInvalidArgument, term.Type())
}
type boundLiteralPredicate[T LiteralType] struct {
op Operation
term bound[T]
lit TypedLiteral[T]
}
func (blp *boundLiteralPredicate[T]) Equals(other BooleanExpression) bool {
rhs, ok := other.(*boundLiteralPredicate[T])
if !ok {
return false
}
return blp.op == rhs.op && blp.term.Equals(rhs.term) && blp.lit.Equals(rhs.lit)
}
func (blp *boundLiteralPredicate[T]) Op() Operation { return blp.op }
func (blp *boundLiteralPredicate[T]) Negate() BooleanExpression {
return &boundLiteralPredicate[T]{op: blp.op.Negate(), term: blp.term, lit: blp.lit}
}
func (blp *boundLiteralPredicate[T]) Term() BoundTerm { return blp.term }
func (blp *boundLiteralPredicate[T]) Ref() BoundReference { return blp.term.Ref() }
func (blp *boundLiteralPredicate[T]) String() string {
return fmt.Sprintf("Bound%s(term=%s, literal=%s)", blp.op, blp.term, blp.lit)
}
func (blp *boundLiteralPredicate[T]) Literal() Literal { return blp.lit }
func (blp *boundLiteralPredicate[T]) AsUnbound(r Reference, l Literal) UnboundPredicate {
return &unboundLiteralPredicate{op: blp.op, term: r, lit: l}
}
// SetPredicate creates a boolean expression representing a predicate that uses a set
// of literals as the argument, like In or NotIn. Duplicate literals will be folded
// into a set, only maintaining the unique literals.
//
// Will panic if op is not a valid Set operation
func SetPredicate(op Operation, t UnboundTerm, lits []Literal) BooleanExpression {
if op < OpIn || op > OpNotIn {
panic(fmt.Errorf("%w: invalid operation for SetPredicate: %s",
ErrInvalidArgument, op))
}
if t == nil {
panic(fmt.Errorf("%w: cannot create set predicate with nil term",
ErrInvalidArgument))
}
switch len(lits) {
case 0:
if op == OpIn {
return AlwaysFalse{}
} else if op == OpNotIn {
return AlwaysTrue{}
}
case 1:
if op == OpIn {
return LiteralPredicate(OpEQ, t, lits[0])
} else if op == OpNotIn {
return LiteralPredicate(OpNEQ, t, lits[0])
}
}
return &unboundSetPredicate{op: op, term: t, lits: newLiteralSet(lits...)}
}
type unboundSetPredicate struct {
op Operation
term UnboundTerm
lits Set[Literal]
}
func (usp *unboundSetPredicate) String() string {
return fmt.Sprintf("%s(term=%s, {%v})", usp.op, usp.term, usp.lits.Members())
}
func (usp *unboundSetPredicate) Equals(other BooleanExpression) bool {
rhs, ok := other.(*unboundSetPredicate)
if !ok {
return false
}
return usp.op == rhs.op && usp.term.Equals(rhs.term) &&
usp.lits.Equals(rhs.lits)
}
func (usp *unboundSetPredicate) Op() Operation { return usp.op }
func (usp *unboundSetPredicate) Negate() BooleanExpression {
return &unboundSetPredicate{op: usp.op.Negate(), term: usp.term, lits: usp.lits}
}
func (usp *unboundSetPredicate) Term() UnboundTerm { return usp.term }
func (usp *unboundSetPredicate) Bind(schema *Schema, caseSensitive bool) (BooleanExpression, error) {
bound, err := usp.term.Bind(schema, caseSensitive)
if err != nil {
return nil, err
}
return createBoundSetPredicate(usp.op, bound, usp.lits)
}
// BoundSetPredicate is a bound expression that utilizes a set of literals such as In or NotIn
type BoundSetPredicate interface {
BoundPredicate
Literals() Set[Literal]
AsUnbound(Reference, []Literal) UnboundPredicate
}
func createBoundSetPredicate(op Operation, term BoundTerm, lits Set[Literal]) (BooleanExpression, error) {
boundType := term.Type()
typedSet := newLiteralSet()
for _, v := range lits.Members() {
casted, err := v.To(boundType)
if err != nil {
return nil, err
}
typedSet.Add(casted)
}
switch typedSet.Len() {
case 0:
if op == OpIn {
return AlwaysFalse{}, nil
} else if op == OpNotIn {
return AlwaysTrue{}, nil
}
case 1:
if op == OpIn {
return createBoundLiteralPredicate(OpEQ, term, typedSet.Members()[0])
} else if op == OpNotIn {
return createBoundLiteralPredicate(OpNEQ, term, typedSet.Members()[0])
}
}
switch term.Type().(type) {
case BooleanType:
return newBoundSetPredicate[bool](op, term, typedSet), nil
case Int32Type:
return newBoundSetPredicate[int32](op, term, typedSet), nil
case Int64Type:
return newBoundSetPredicate[int64](op, term, typedSet), nil
case Float32Type:
return newBoundSetPredicate[float32](op, term, typedSet), nil
case Float64Type:
return newBoundSetPredicate[float64](op, term, typedSet), nil
case DateType:
return newBoundSetPredicate[Date](op, term, typedSet), nil
case TimeType:
return newBoundSetPredicate[Time](op, term, typedSet), nil
case TimestampType, TimestampTzType:
return newBoundSetPredicate[Timestamp](op, term, typedSet), nil
case StringType:
return newBoundSetPredicate[string](op, term, typedSet), nil
case BinaryType, FixedType:
return newBoundSetPredicate[[]byte](op, term, typedSet), nil
case DecimalType:
return newBoundSetPredicate[Decimal](op, term, typedSet), nil
case UUIDType:
return newBoundSetPredicate[uuid.UUID](op, term, typedSet), nil
}
return nil, fmt.Errorf("%w: invalid bound type for set predicate - %s",
ErrType, term.Type())
}
func newBoundSetPredicate[T LiteralType](op Operation, term BoundTerm, lits Set[Literal]) *boundSetPredicate[T] {
return &boundSetPredicate[T]{op: op, term: term.(bound[T]), lits: lits}
}
type boundSetPredicate[T LiteralType] struct {
op Operation
term bound[T]
lits Set[Literal]
}
func (bsp *boundSetPredicate[T]) Equals(other BooleanExpression) bool {
rhs, ok := other.(*boundSetPredicate[T])
if !ok {
return false
}
return bsp.op == rhs.op && bsp.term.Equals(rhs.term) &&
bsp.lits.Equals(rhs.lits)
}
func (bsp *boundSetPredicate[T]) Op() Operation { return bsp.op }
func (bsp *boundSetPredicate[T]) Negate() BooleanExpression {
return &boundSetPredicate[T]{op: bsp.op.Negate(), term: bsp.term,
lits: bsp.lits}
}
func (bsp *boundSetPredicate[T]) Term() BoundTerm { return bsp.term }
func (bsp *boundSetPredicate[T]) Ref() BoundReference { return bsp.term.Ref() }
func (bsp *boundSetPredicate[T]) String() string {
return fmt.Sprintf("Bound%s(term=%s, {%v})", bsp.op, bsp.term, bsp.lits.Members())
}
func (bsp *boundSetPredicate[T]) AsUnbound(r Reference, lits []Literal) UnboundPredicate {
litSet := newLiteralSet(lits...)
if litSet.Len() == 1 {
switch bsp.op {
case OpIn:
return LiteralPredicate(OpEQ, r, lits[0])
case OpNotIn:
return LiteralPredicate(OpNEQ, r, lits[0])
}
}
return &unboundSetPredicate{op: bsp.op, term: r, lits: litSet}
}
func (bsp *boundSetPredicate[T]) Literals() Set[Literal] {
return bsp.lits
}
type BoundTransform struct {
transform Transform
term BoundTerm
}
func (*BoundTransform) isTerm() {}
func (b *BoundTransform) String() string {
return fmt.Sprintf("BoundTransform(transform=%s, term=%s)",
b.transform, b.term)
}
func (b *BoundTransform) Ref() BoundReference { return b.term.Ref() }
func (b *BoundTransform) Type() Type { return b.transform.ResultType(b.term.Type()) }
func (b *BoundTransform) Equals(other BoundTerm) bool {
rhs, ok := other.(*BoundTransform)
if !ok {
return false
}
return b.transform.Equals(rhs.transform) && b.term.Equals(rhs.term)
}
func (b *BoundTransform) evalToLiteral(st structLike) Optional[Literal] {
return b.transform.Apply(b.term.evalToLiteral(st))
}
func (b *BoundTransform) evalIsNull(st structLike) bool {
return !b.evalToLiteral(st).Valid
}