exprs.go (800 lines of code) (raw):

// 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 PosPath() []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 (b *boundRef[T]) PosPath() []int { out, inner := []int{b.acc.pos}, &b.acc for inner.inner != nil { inner = inner.inner out = append(out, inner.pos) } return out } 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(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 }