Repository: polarsignals/frostdb
Pull request #682 (2024-01-10 17:46:11)

query/physicalplan: Use arrow compute package for binary scalar expressions
Instead of implementing individual functions for each BinaryScalar operation, we'll now use the arrow `compute` package. It already implements most of the heavy lifting for us. 

Once we pass in the array and the scalar, we can convert the data back to a `*array.Boolean`. Walking this array, we construct the bitmap like we did before. 
It cleans up a lot of duplicate code and removes the need to add new functions for new Arrow types. 

Given the quality of live improvement, the performance impact is fine:
```
name                         old time/op    new time/op    delta
BinaryScalarOperation/==-24    3.39ms ± 2%    3.76ms ± 7%  +11.00%  (p=0.008 n=5+5)
BinaryScalarOperation/!=-24    11.2ms ± 3%    11.4ms ± 1%   +1.64%  (p=0.032 n=5+4)
BinaryScalarOperation/<-24     6.00ms ± 6%    6.25ms ± 3%     ~     (p=0.056 n=5+5)
BinaryScalarOperation/<=-24    7.07ms ± 2%    7.04ms ± 1%     ~     (p=0.841 n=5+5)
BinaryScalarOperation/>-24     7.02ms ± 6%    6.92ms ± 5%     ~     (p=0.690 n=5+5)
BinaryScalarOperation/>=-24    7.78ms ± 3%    7.95ms ± 5%     ~     (p=0.310 n=5+5)

name                         old alloc/op   new alloc/op   delta
BinaryScalarOperation/==-24     509kB ± 0%     643kB ± 0%  +26.23%  (p=0.008 n=5+5)
BinaryScalarOperation/!=-24     532kB ± 0%     665kB ± 0%  +25.12%  (p=0.008 n=5+5)
BinaryScalarOperation/<-24      532kB ± 0%     532kB ± 0%     ~     (all equal)
BinaryScalarOperation/<=-24     532kB ± 0%     532kB ± 0%     ~     (all equal)
BinaryScalarOperation/>-24      532kB ± 0%     532kB ± 0%     ~     (all equal)
BinaryScalarOperation/>=-24     532kB ± 0%     532kB ± 0%     ~     (all equal)

name                         old allocs/op  new allocs/op  delta
BinaryScalarOperation/==-24       263 ± 0%       309 ± 0%  +17.49%  (p=0.008 n=5+5)
BinaryScalarOperation/!=-24       267 ± 0%       313 ± 0%  +17.23%  (p=0.008 n=5+5)
BinaryScalarOperation/<-24        267 ± 0%       267 ± 0%     ~     (all equal)
BinaryScalarOperation/<=-24       267 ± 0%       267 ± 0%     ~     (all equal)
BinaryScalarOperation/>-24        267 ± 0%       267 ± 0%     ~     (all equal)
BinaryScalarOperation/>=-24       267 ± 0%       267 ± 0%     ~     (all equal)
```


asubiotto: Interesting. I guess you can even get rid of the type switch in this case and just delegate that to `compute.CallFunction`? I wonder what else in our execution engine we could replace with these compute kernels.
metalmatze: I'm working on an update with these exact changes. It's going to delete _a lot_ of code. :joy: 
metalmatze: Pushed and updated the title and description of the PR. 
gernest: @metalmatze @asubiotto @thorfour  I take this opportunity to thank you for being awesome and giving me a chance to hack on `frostdb` uninterrupted.

I have a minor surgery on Monday that will take me a couple of weeks to recover. So my silence won't mean I jumped ship, but rather I won't be able to work on my laptop.

I will pickup the slack the moment I get back, I'm quite experienced with `compute` and whole `arrow/**`  stuff, so I will probably focus there and our parquet converter ro see how much improvement we can get.

I have some pending local branches, that I will be glad to share the details with anyone touching these topics maybe they can be helpful.

- Building direct arrow records in logictest
- Integrating `GenericTable` in `parca` Ingester
- Benchmarking `arrowutils.ReorderRecord`  


 shameless plug 😃 **_Please don't forget about about me If you ever need to expand your team, our collaboration has been awesome._**

metalmatze: @gernest, thank you for all your contributions in the last weeks. They've been totally awesome!
Good luck with the surgery! I wish you a fast recovery! 

We'll definitely let you know if we hire someone (you'll probably see it quickly anyway).

Patch:
diff --git a/query/logicalplan/expr.go b/query/logicalplan/expr.go
index 45e53ff..8f73391 100644
--- a/query/logicalplan/expr.go
+++ b/query/logicalplan/expr.go
@@ -69,6 +69,37 @@ func (o Op) String() string {
 	}
 }
 
+func (o Op) ArrowString() string {
+	switch o {
+	case OpEq:
+		return "equal"
+	case OpNotEq:
+		return "not_equal"
+	case OpLt:
+		return "less"
+	case OpLtEq:
+		return "less_equal"
+	case OpGt:
+		return "greater"
+	case OpGtEq:
+		return "greater_equal"
+	case OpAnd:
+		return "and"
+	case OpOr:
+		return "or"
+	case OpAdd:
+		return "add"
+	case OpSub:
+		return "subtract"
+	case OpMul:
+		return "multiply"
+	case OpDiv:
+		return "divide"
+	default:
+		panic("unknown operator")
+	}
+}
+
 type BinaryExpr struct {
 	Left  Expr
 	Op    Op
diff --git a/query/physicalplan/binaryscalarexpr.go b/query/physicalplan/binaryscalarexpr.go
index 768a520..710156f 100644
--- a/query/physicalplan/binaryscalarexpr.go
+++ b/query/physicalplan/binaryscalarexpr.go
@@ -2,11 +2,13 @@ package physicalplan
 
 import (
 	"bytes"
+	"context"
 	"errors"
 	"fmt"
 
 	"github.com/apache/arrow/go/v14/arrow"
 	"github.com/apache/arrow/go/v14/arrow/array"
+	"github.com/apache/arrow/go/v14/arrow/compute"
 	"github.com/apache/arrow/go/v14/arrow/scalar"
 
 	"github.com/polarsignals/frostdb/query/logicalplan"
@@ -79,111 +81,8 @@ func (e BinaryScalarExpr) String() string {
 var ErrUnsupportedBinaryOperation = errors.New("unsupported binary operation")
 
 func BinaryScalarOperation(left arrow.Array, right scalar.Scalar, operator logicalplan.Op) (*Bitmap, error) {
+	// TODO: Figure out dictionary arrays and lists with compute next
 	leftType := left.DataType()
-	switch leftType {
-	case arrow.FixedWidthTypes.Boolean:
-		switch operator {
-		case logicalplan.OpEq:
-			return BooleanArrayScalarEqual(left.(*array.Boolean), right.(*scalar.Boolean))
-		case logicalplan.OpNotEq:
-			return BooleanArrayScalarNotEqual(left.(*array.Boolean), right.(*scalar.Boolean))
-		default:
-			panic("something terrible has happened, this should have errored previously during validation")
-		}
-	case &arrow.FixedSizeBinaryType{ByteWidth: 16}:
-		switch operator {
-		case logicalplan.OpEq:
-			return FixedSizeBinaryArrayScalarEqual(left.(*array.FixedSizeBinary), right.(*scalar.FixedSizeBinary))
-		case logicalplan.OpNotEq:
-			return FixedSizeBinaryArrayScalarNotEqual(left.(*array.FixedSizeBinary), right.(*scalar.FixedSizeBinary))
-		default:
-			panic("something terrible has happened, this should have errored previously during validation")
-		}
-	case arrow.BinaryTypes.String:
-		switch operator {
-		case logicalplan.OpEq:
-			return StringArrayScalarEqual(left.(*array.String), right.(*scalar.String))
-		case logicalplan.OpNotEq:
-			return StringArrayScalarNotEqual(left.(*array.String), right.(*scalar.String))
-		default:
-			panic("something terrible has happened, this should have errored previously during validation")
-		}
-	case arrow.BinaryTypes.Binary:
-		switch operator {
-		case logicalplan.OpEq:
-			switch r := right.(type) {
-			case *scalar.Binary:
-				return BinaryArrayScalarEqual(left.(*array.Binary), r)
-			case *scalar.String:
-				return BinaryArrayScalarEqual(left.(*array.Binary), r.Binary)
-			default:
-				panic("something terrible has happened, this should have errored previously during validation")
-			}
-		case logicalplan.OpNotEq:
-			switch r := right.(type) {
-			case *scalar.Binary:
-				return BinaryArrayScalarNotEqual(left.(*array.Binary), r)
-			case *scalar.String:
-				return BinaryArrayScalarNotEqual(left.(*array.Binary), r.Binary)
-			default:
-				panic("something terrible has happened, this should have errored previously during validation")
-			}
-		default:
-			panic("something terrible has happened, this should have errored previously during validation")
-		}
-	case arrow.PrimitiveTypes.Int64:
-		switch operator {
-		case logicalplan.OpEq:
-			return Int64ArrayScalarEqual(left.(*array.Int64), right.(*scalar.Int64))
-		case logicalplan.OpNotEq:
-			return Int64ArrayScalarNotEqual(left.(*array.Int64), right.(*scalar.Int64))
-		case logicalplan.OpLt:
-			return Int64ArrayScalarLessThan(left.(*array.Int64), right.(*scalar.Int64))
-		case logicalplan.OpLtEq:
-			return Int64ArrayScalarLessThanOrEqual(left.(*array.Int64), right.(*scalar.Int64))
-		case logicalplan.OpGt:
-			return Int64ArrayScalarGreaterThan(left.(*array.Int64), right.(*scalar.Int64))
-		case logicalplan.OpGtEq:
-			return Int64ArrayScalarGreaterThanOrEqual(left.(*array.Int64), right.(*scalar.Int64))
-		default:
-			panic("something terrible has happened, this should have errored previously during validation")
-		}
-	case arrow.PrimitiveTypes.Int32:
-		switch operator {
-		case logicalplan.OpEq:
-			return Int32ArrayScalarEqual(left.(*array.Int32), right.(*scalar.Int32))
-		case logicalplan.OpNotEq:
-			return Int32ArrayScalarNotEqual(left.(*array.Int32), right.(*scalar.Int32))
-		case logicalplan.OpLt:
-			return Int32ArrayScalarLessThan(left.(*array.Int32), right.(*scalar.Int32))
-		case logicalplan.OpLtEq:
-			return Int32ArrayScalarLessThanOrEqual(left.(*array.Int32), right.(*scalar.Int32))
-		case logicalplan.OpGt:
-			return Int32ArrayScalarGreaterThan(left.(*array.Int32), right.(*scalar.Int32))
-		case logicalplan.OpGtEq:
-			return Int32ArrayScalarGreaterThanOrEqual(left.(*array.Int32), right.(*scalar.Int32))
-		default:
-			panic("something terrible has happened, this should have errored previously during validation")
-		}
-	case arrow.PrimitiveTypes.Uint64:
-		switch operator {
-		case logicalplan.OpEq:
-			return Uint64ArrayScalarEqual(left.(*array.Uint64), right.(*scalar.Uint64))
-		case logicalplan.OpNotEq:
-			return Uint64ArrayScalarNotEqual(left.(*array.Uint64), right.(*scalar.Uint64))
-		case logicalplan.OpLt:
-			return Uint64ArrayScalarLessThan(left.(*array.Uint64), right.(*scalar.Uint64))
-		case logicalplan.OpLtEq:
-			return Uint64ArrayScalarLessThanOrEqual(left.(*array.Uint64), right.(*scalar.Uint64))
-		case logicalplan.OpGt:
-			return Uint64ArrayScalarGreaterThan(left.(*array.Uint64), right.(*scalar.Uint64))
-		case logicalplan.OpGtEq:
-			return Uint64ArrayScalarGreaterThanOrEqual(left.(*array.Uint64), right.(*scalar.Uint64))
-		default:
-			panic("something terrible has happened, this should have errored previously during validation")
-		}
-	}
-
 	switch arr := left.(type) {
 	case *array.Dictionary:
 		switch operator {
@@ -201,7 +100,38 @@ func BinaryScalarOperation(left arrow.Array, right scalar.Scalar, operator logic
 		panic("TODO: list comparisons unimplemented")
 	}
 
-	return nil, ErrUnsupportedBinaryOperation
+	return ArrayScalarCompute(operator.ArrowString(), left, right)
+}
+
+func ArrayScalarCompute(funcName string, left arrow.Array, right scalar.Scalar) (*Bitmap, error) {
+	equalsResult, err := compute.CallFunction(context.TODO(), funcName, nil, compute.NewDatum(left), compute.NewDatum(right))
+	if err != nil {
+		if errors.Unwrap(err).Error() == "not implemented" {
+			return nil, ErrUnsupportedBinaryOperation
+		}
+		return nil, fmt.Errorf("error calling equal function: %w", err)
+	}
+	defer equalsResult.Release()
+	equalsDatum, ok := equalsResult.(*compute.ArrayDatum)
+	if !ok {
+		return nil, fmt.Errorf("expected *compute.ArrayDatum, got %T", equalsResult)
+	}
+	equalsArray, ok := equalsDatum.MakeArray().(*array.Boolean)
+	if !ok {
+		return nil, fmt.Errorf("expected *array.Boolean, got %T", equalsDatum.MakeArray())
+	}
+	defer equalsArray.Release()
+
+	res := NewBitmap()
+	for i := 0; i < equalsArray.Len(); i++ {
+		if equalsArray.IsNull(i) {
+			continue
+		}
+		if equalsArray.Value(i) {
+			res.AddInt(i)
+		}
+	}
+	return res, nil
 }
 
 func DictionaryArrayScalarNotEqual(left *array.Dictionary, right scalar.Scalar) (*Bitmap, error) {
@@ -283,393 +213,3 @@ func DictionaryArrayScalarEqual(left *array.Dictionary, right scalar.Scalar) (*B
 
 	return res, nil
 }
-
-func FixedSizeBinaryArrayScalarEqual(left *array.FixedSizeBinary, right *scalar.FixedSizeBinary) (*Bitmap, error) {
-	res := NewBitmap()
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if bytes.Equal(left.Value(i), right.Data()) {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func FixedSizeBinaryArrayScalarNotEqual(left *array.FixedSizeBinary, right *scalar.FixedSizeBinary) (*Bitmap, error) {
-	res := NewBitmap()
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			res.Add(uint32(i))
-			continue
-		}
-		if !bytes.Equal(left.Value(i), right.Data()) {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func StringArrayScalarEqual(left *array.String, right *scalar.String) (*Bitmap, error) {
-	res := NewBitmap()
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) == string(right.Data()) {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func StringArrayScalarNotEqual(left *array.String, right *scalar.String) (*Bitmap, error) {
-	res := NewBitmap()
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			res.Add(uint32(i))
-			continue
-		}
-		if left.Value(i) != string(right.Data()) {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func BinaryArrayScalarEqual(left *array.Binary, right *scalar.Binary) (*Bitmap, error) {
-	res := NewBitmap()
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if bytes.Equal(left.Value(i), right.Data()) {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func BinaryArrayScalarNotEqual(left *array.Binary, right *scalar.Binary) (*Bitmap, error) {
-	res := NewBitmap()
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			res.Add(uint32(i))
-			continue
-		}
-		if !bytes.Equal(left.Value(i), right.Data()) {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int64ArrayScalarEqual(left *array.Int64, right *scalar.Int64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) == right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int64ArrayScalarNotEqual(left *array.Int64, right *scalar.Int64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			res.Add(uint32(i))
-			continue
-		}
-		if left.Value(i) != right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int64ArrayScalarLessThan(left *array.Int64, right *scalar.Int64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) < right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int64ArrayScalarLessThanOrEqual(left *array.Int64, right *scalar.Int64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) <= right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int64ArrayScalarGreaterThan(left *array.Int64, right *scalar.Int64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) > right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int64ArrayScalarGreaterThanOrEqual(left *array.Int64, right *scalar.Int64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) >= right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Uint64ArrayScalarEqual(left *array.Uint64, right *scalar.Uint64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) == right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Uint64ArrayScalarNotEqual(left *array.Uint64, right *scalar.Uint64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			res.Add(uint32(i))
-			continue
-		}
-		if left.Value(i) != right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Uint64ArrayScalarLessThan(left *array.Uint64, right *scalar.Uint64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) < right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Uint64ArrayScalarLessThanOrEqual(left *array.Uint64, right *scalar.Uint64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) <= right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Uint64ArrayScalarGreaterThan(left *array.Uint64, right *scalar.Uint64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) > right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Uint64ArrayScalarGreaterThanOrEqual(left *array.Uint64, right *scalar.Uint64) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) >= right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func BooleanArrayScalarEqual(left *array.Boolean, right *scalar.Boolean) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) == right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func BooleanArrayScalarNotEqual(left *array.Boolean, right *scalar.Boolean) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) != right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int32ArrayScalarEqual(left *array.Int32, right *scalar.Int32) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) == right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int32ArrayScalarNotEqual(left *array.Int32, right *scalar.Int32) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			res.Add(uint32(i))
-			continue
-		}
-		if left.Value(i) != right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int32ArrayScalarLessThan(left *array.Int32, right *scalar.Int32) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) < right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int32ArrayScalarLessThanOrEqual(left *array.Int32, right *scalar.Int32) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) <= right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int32ArrayScalarGreaterThan(left *array.Int32, right *scalar.Int32) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) > right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}
-
-func Int32ArrayScalarGreaterThanOrEqual(left *array.Int32, right *scalar.Int32) (*Bitmap, error) {
-	res := NewBitmap()
-
-	for i := 0; i < left.Len(); i++ {
-		if left.IsNull(i) {
-			continue
-		}
-		if left.Value(i) >= right.Value {
-			res.Add(uint32(i))
-		}
-	}
-
-	return res, nil
-}


Test patch:
diff --git a/logictest/testdata/exec/filter/filter b/logictest/testdata/exec/filter/filter
index a9f597b..212cf3c 100644
--- a/logictest/testdata/exec/filter/filter
+++ b/logictest/testdata/exec/filter/filter
@@ -7,6 +7,33 @@ value2  value2  value3  null    stack1  2   2
 value3  value2  null    value4  stack1  3   3
 ----
 
+exec
+select labels, stacktrace, timestamp, value where timestamp = 2
+----
+value2  value2  value3  null    stack1  2       2
+
+exec
+select labels, stacktrace, timestamp, value where timestamp != 2
+----
+value1  value2  null    null    stack1  1       1
+value3  value2  null    value4  stack1  3       3
+
+exec
+select labels, stacktrace, timestamp, value where timestamp < 2
+----
+value1  value2  null    null    stack1  1       1
+
+exec
+select labels, stacktrace, timestamp, value where timestamp <= 2
+----
+value1  value2  null    null    stack1  1       1
+value2  value2  value3  null    stack1  2       2
+
+exec
+select labels, stacktrace, timestamp, value where timestamp > 2
+----
+value3  value2  null    value4  stack1  3       3
+
 exec
 select labels, stacktrace, timestamp, value where timestamp >= 2
 ----
diff --git a/query/physicalplan/binaryscalarexpr_test.go b/query/physicalplan/binaryscalarexpr_test.go
new file mode 100644
index 0000000..96f22d2
--- /dev/null
+++ b/query/physicalplan/binaryscalarexpr_test.go
@@ -0,0 +1,53 @@
+package physicalplan
+
+import (
+	"testing"
+
+	"github.com/apache/arrow/go/v14/arrow/array"
+	"github.com/apache/arrow/go/v14/arrow/memory"
+	"github.com/apache/arrow/go/v14/arrow/scalar"
+	"github.com/stretchr/testify/require"
+
+	"github.com/polarsignals/frostdb/query/logicalplan"
+)
+
+func BenchmarkBinaryScalarOperation(b *testing.B) {
+	ab := array.NewInt64Builder(memory.DefaultAllocator)
+	for i := int64(0); i < 1_000_000; i++ {
+		ab.Append(i % 10)
+	}
+
+	arr := ab.NewInt64Array()
+	ab.Release()
+
+	s := scalar.NewInt64Scalar(4) // chosen by fair dice roll. guaranteed to be random.
+
+	operators := []logicalplan.Op{
+		logicalplan.OpAnd,
+		logicalplan.OpEq,
+		logicalplan.OpNotEq,
+		logicalplan.OpLt,
+		logicalplan.OpLtEq,
+		logicalplan.OpGt,
+		logicalplan.OpGtEq,
+	}
+
+	for _, op := range operators {
+		b.Run(op.String(), func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				_, _ = BinaryScalarOperation(arr, s, op)
+			}
+		})
+	}
+}
+
+func TestBinaryScalarOperationNotImplemented(t *testing.T) {
+	ab := array.NewInt64Builder(memory.DefaultAllocator)
+	arr := ab.NewInt64Array()
+	ab.Release()
+
+	s := scalar.NewInt64Scalar(4) // chosen by fair dice roll. guaranteed to be random.
+
+	_, err := BinaryScalarOperation(arr, s, logicalplan.OpAnd)
+	require.Equal(t, err, ErrUnsupportedBinaryOperation)
+}