Repository: polarsignals/frostdb (go)

Issue:
Add `Or` expression
While we already have the `And` expression, we cannot express `Or` expressions today.

Pull request:
*: Add support for `Or` expressions
This change adds support for `Or` expressions, make changes to:
- `logicalplan`
- `physicalplan`

Also modifies the example to demonstrate it.

TODO:
- [x] Add test coverage

Fixes #146 

Patch:
diff --git a/filter.go b/filter.go
index d250bb6..1b5c669 100644
--- a/filter.go
+++ b/filter.go
@@ -86,6 +86,21 @@ func binaryBooleanExpr(expr *logicalplan.BinaryExpr) (TrueNegativeFilter, error)
 			Left:  left,
 			Right: right,
 		}, nil
+	case logicalplan.OpOr:
+		left, err := booleanExpr(expr.Left)
+		if err != nil {
+			return nil, err
+		}
+
+		right, err := booleanExpr(expr.Right)
+		if err != nil {
+			return nil, err
+		}
+
+		return &OrExpr{
+			Left:  left,
+			Right: right,
+		}, nil
 	default:
 		return &AlwaysTrueFilter{}, nil
 	}
@@ -114,6 +129,28 @@ func (a *AndExpr) Eval(rg dynparquet.DynamicRowGroup) (bool, error) {
 	return left && right, nil
 }
 
+type OrExpr struct {
+	Left  TrueNegativeFilter
+	Right TrueNegativeFilter
+}
+
+func (a *OrExpr) Eval(rg dynparquet.DynamicRowGroup) (bool, error) {
+	left, err := a.Left.Eval(rg)
+	if err != nil {
+		return false, err
+	}
+	if left {
+		return true, nil
+	}
+
+	right, err := a.Right.Eval(rg)
+	if err != nil {
+		return false, err
+	}
+
+	return right, nil
+}
+
 func booleanExpr(expr logicalplan.Expr) (TrueNegativeFilter, error) {
 	if expr == nil {
 		return &AlwaysTrueFilter{}, nil
diff --git a/query/logicalplan/expr.go b/query/logicalplan/expr.go
index 76fafdb..f0f0079 100644
--- a/query/logicalplan/expr.go
+++ b/query/logicalplan/expr.go
@@ -24,6 +24,7 @@ const (
 	OpRegexMatch
 	OpRegexNotMatch
 	OpAnd
+	OpOr
 )
 
 func (o Op) String() string {
@@ -46,6 +47,8 @@ func (o Op) String() string {
 		return "!~"
 	case OpAnd:
 		return "&&"
+	case OpOr:
+		return "||"
 	default:
 		panic("unknown operator")
 	}
@@ -216,6 +219,18 @@ func And(exprs ...Expr) Expr {
 }
 
 func and(exprs []Expr) Expr {
+	return computeBinaryExpr(exprs, OpAnd)
+}
+
+func Or(exprs ...Expr) Expr {
+	return or(exprs)
+}
+
+func or(exprs []Expr) Expr {
+	return computeBinaryExpr(exprs, OpOr)
+}
+
+func computeBinaryExpr(exprs []Expr, op Op) Expr {
 	nonNilExprs := make([]Expr, 0, len(exprs))
 	for _, expr := range exprs {
 		if expr != nil {
@@ -232,15 +247,15 @@ func and(exprs []Expr) Expr {
 	if len(nonNilExprs) == 2 {
 		return &BinaryExpr{
 			Left:  nonNilExprs[0],
-			Op:    OpAnd,
+			Op:    op,
 			Right: nonNilExprs[1],
 		}
 	}
 
 	return &BinaryExpr{
 		Left:  nonNilExprs[0],
-		Op:    OpAnd,
-		Right: and(nonNilExprs[1:]),
+		Op:    op,
+		Right: computeBinaryExpr(nonNilExprs[1:], op),
 	}
 }
 
diff --git a/query/logicalplan/validate.go b/query/logicalplan/validate.go
index 6f0a042..6d55520 100644
--- a/query/logicalplan/validate.go
+++ b/query/logicalplan/validate.go
@@ -251,7 +251,7 @@ func ValidateFilterExpr(plan *LogicalPlan, e Expr) *ExprValidationError {
 
 // ValidateFilterBinaryExpr validates the filter's binary expression.
 func ValidateFilterBinaryExpr(plan *LogicalPlan, expr *BinaryExpr) *ExprValidationError {
-	if expr.Op == OpAnd {
+	if expr.Op == OpAnd || expr.Op == OpOr {
 		return ValidateFilterAndBinaryExpr(plan, expr)
 	}
 
diff --git a/query/physicalplan/filter.go b/query/physicalplan/filter.go
index 5f2e949..b126351 100644
--- a/query/physicalplan/filter.go
+++ b/query/physicalplan/filter.go
@@ -114,6 +114,21 @@ func binaryBooleanExpr(expr *logicalplan.BinaryExpr) (BooleanExpression, error)
 			Left:  left,
 			Right: right,
 		}, nil
+	case logicalplan.OpOr:
+		left, err := booleanExpr(expr.Left)
+		if err != nil {
+			return nil, err
+		}
+
+		right, err := booleanExpr(expr.Right)
+		if err != nil {
+			return nil, err
+		}
+
+		return &OrExpr{
+			Left:  left,
+			Right: right,
+		}, nil
 	default:
 		panic("unsupported binary boolean expression")
 	}
@@ -144,6 +159,31 @@ func (a *AndExpr) String() string {
 	return "(" + a.Left.String() + " AND " + a.Right.String() + ")"
 }
 
+type OrExpr struct {
+	Left  BooleanExpression
+	Right BooleanExpression
+}
+
+func (a *OrExpr) Eval(r arrow.Record) (*Bitmap, error) {
+	left, err := a.Left.Eval(r)
+	if err != nil {
+		return nil, err
+	}
+
+	right, err := a.Right.Eval(r)
+	if err != nil {
+		return nil, err
+	}
+
+	// This stores the result in place to avoid allocations.
+	left.Or(right)
+	return left, nil
+}
+
+func (a *OrExpr) String() string {
+	return "(" + a.Left.String() + " OR " + a.Right.String() + ")"
+}
+
 func booleanExpr(expr logicalplan.Expr) (BooleanExpression, error) {
 	switch e := expr.(type) {
 	case *logicalplan.BinaryExpr:


Test patch:
diff --git a/filter_test.go b/filter_test.go
index 869d65d..932cfa1 100644
--- a/filter_test.go
+++ b/filter_test.go
@@ -157,6 +157,44 @@ func TestFilter(t *testing.T) {
 			filterExpr: logicalplan.Col("labels.label1").RegexMatch("values."),
 			rows:       0,
 		},
+		"== string or == string": {
+			filterExpr: logicalplan.Or(
+				logicalplan.Col("labels.label1").Eq(logicalplan.Literal("value1")),
+				logicalplan.Col("labels.label1").Eq(logicalplan.Literal("value2")),
+			),
+			cols: 6,
+			rows: 2,
+		},
+		"regexp or == missing column": {
+			filterExpr: logicalplan.Or(
+				logicalplan.Col("labels.label1").RegexMatch("value."),
+				logicalplan.Col("labels.label5").Eq(logicalplan.Literal("")),
+			),
+			cols: 7,
+			rows: 3,
+		},
+		"regexp and (== string or == string)": {
+			filterExpr: logicalplan.And(
+				logicalplan.Col("labels.label3").RegexMatch("value."),
+				logicalplan.Or(
+					logicalplan.Col("labels.label1").Eq(logicalplan.Literal("value1")),
+					logicalplan.Col("labels.label1").Eq(logicalplan.Literal("value2")),
+				),
+			),
+			cols: 6,
+			rows: 1,
+		},
+		"== string or (regexp and == string)": {
+			filterExpr: logicalplan.Or(
+				logicalplan.Col("labels.label4").Eq(logicalplan.Literal("value4")),
+				logicalplan.And(
+					logicalplan.Col("labels.label2").RegexMatch("value."),
+					logicalplan.Col("labels.label1").Eq(logicalplan.Literal("value2")),
+				),
+			),
+			cols: 7,
+			rows: 2,
+		},
 	}
 
 	engine := query.NewEngine(