Repository: mabel-dev/opteryx

Issue:
✨ reorder ORs for most TRUEs
And update the expression engine to not evaluate subsequent expressions when we already know the row is true. (Do account for cost of execution in the ordering)

This should decrease the cost of evaluating ORs.


Discussion:

Resolving patch:
diff --git a/opteryx/__version__.py b/opteryx/__version__.py
--- a/opteryx/__version__.py
+++ b/opteryx/__version__.py
@@ -1,4 +1,4 @@
-__build__ = 168
+__build__ = 172
 # 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
@@ -27,8 +27,8 @@ class VersionStatus(Enum):
 
 _major = 0
 _minor = 12
-_revision = 1
-_status = VersionStatus.RELEASE
+_revision = 2
+_status = VersionStatus.ALPHA
 
 __version__ = f"{_major}.{_minor}.{_revision}" + (
     f"-{_status.value}.{__build__}" if _status != VersionStatus.RELEASE else ""
diff --git a/opteryx/components/heuristic_optimizer/strategies/split_conjunctive_predicates.py b/opteryx/components/heuristic_optimizer/strategies/split_conjunctive_predicates.py
--- a/opteryx/components/heuristic_optimizer/strategies/split_conjunctive_predicates.py
+++ b/opteryx/components/heuristic_optimizer/strategies/split_conjunctive_predicates.py
@@ -17,6 +17,7 @@
 from opteryx.components.logical_planner import LogicalPlanStepType
 from opteryx.managers.expression import NodeType
 from opteryx.managers.expression import get_all_nodes_of_type
+from opteryx.models import Node
 
 from .optimization_strategy import HeuristicOptimizerContext
 from .optimization_strategy import OptimizationStrategy
@@ -52,6 +53,9 @@ def _tag_predicates(nodes):
     """
 
     for node in nodes:
+        while node.node_type == NodeType.NESTED and node.centre:
+            node = node.centre
+
         node.weight = 0
         node.simple = True
         node.relations = set()
@@ -87,6 +91,26 @@ def _tag_predicates(nodes):
 
 
 def _inner_split(node):
+    while node.node_type == NodeType.NESTED:
+        node = node.centre
+
+    if node.node_type == NodeType.OR:
+        sides = _tag_predicates(
+            [
+                Node(node.left.node_type, condition=node.left),
+                Node(node.right.node_type, condition=node.right),
+            ]
+        )
+        sides.reverse()
+        node = Node(
+            NodeType.OR,
+            left=sides[0].condition,
+            right=sides[1].condition,
+            schema_column=node.schema_column,
+        )
+
+        return [node]
+
     if node.node_type != NodeType.AND:
         return [node]
 
diff --git a/opteryx/managers/expression/__init__.py b/opteryx/managers/expression/__init__.py
--- a/opteryx/managers/expression/__init__.py
+++ b/opteryx/managers/expression/__init__.py
@@ -123,6 +123,32 @@ def identities(self):
         return list(self.results.keys())
 
 
+def short_cut_or(root, table, context):
+    # Assuming table.num_rows returns the number of rows in the table
+    false_indices = numpy.arange(table.num_rows)
+
+    # Evaluate left expression
+    left_result = numpy.array(evaluate(root.left, table, context), dtype=numpy.bool_)
+
+    # Filter out indices where left_result is TRUE
+    subset_indices = false_indices[~left_result]
+
+    if subset_indices.size == 0:
+        return left_result
+
+    # Create a subset table for evaluating the right expression
+    subset_table = table.take(subset_indices)
+
+    # Evaluate right expression on the subset table
+    right_result = numpy.array(evaluate(root.right, subset_table, context), dtype=numpy.bool_)
+
+    # Combine results
+    # Update left_result with the right_result where left_result was False
+    left_result[subset_indices] = left_result[subset_indices] | right_result
+
+    return left_result
+
+
 def prioritize_evaluation(expressions):
     non_dependent_expressions = []
     dependent_expressions = []
@@ -172,6 +198,9 @@ def _inner_evaluate(root: Node, table: Table, context: ExecutionContext):
 
     # BOOLEAN OPERATORS
     if node_type & LOGICAL_TYPE == LOGICAL_TYPE:
+        if node_type == NodeType.OR:
+            return short_cut_or(root, table, context)
+
         if node_type in LOGICAL_OPERATIONS:
             left = _inner_evaluate(root.left, table, context) if root.left else None
             right = _inner_evaluate(root.right, table, context) if root.right else None


Test patch:
diff --git a/tests/misc/test_expressions.py b/tests/misc/test_expressions.py
--- a/tests/misc/test_expressions.py
+++ b/tests/misc/test_expressions.py
@@ -126,7 +126,7 @@ def test_logical_expressions():
     result = evaluate(F_OR_T, table=planets)
     assert all(result)
     result = evaluate(F_OR_F, table=planets)
-    assert not any(c.as_py() for c in result)
+    assert not any(c for c in result)
 
     NOT_T = Node(NodeType.NOT, centre=true, schema_column=FunctionColumn(name="func", type=0))
     NOT_F = Node(NodeType.NOT, centre=false, schema_column=FunctionColumn(name="func", type=0))
diff --git a/tests/sql_battery/test_shapes_and_errors_battery.py b/tests/sql_battery/test_shapes_and_errors_battery.py
--- a/tests/sql_battery/test_shapes_and_errors_battery.py
+++ b/tests/sql_battery/test_shapes_and_errors_battery.py
@@ -359,6 +359,17 @@
         ("SELECT * FROM $satellites WHERE name ~* '^c.'", 12, 8, None),
         ("SELECT * FROM $satellites WHERE name !~* '^c.'", 165, 8, None),
 
+        ("SELECT * FROM $satellites WHERE (id = 5 OR name = 'Europa') AND TRUE;", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE (id = 5 OR name = 'Europa') OR FALSE;", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE id = 5 OR (name = 'Europa' AND TRUE);", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE (id = 5 AND TRUE) OR name = 'Europa';", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE (id = 5 OR FALSE) OR name = 'Europa';", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE id = 5 OR (name = 'Europa' OR FALSE);", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE (id = 5 OR name = 'Europa') AND (1=1);", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE (id = 5 AND (2>1)) OR name = 'Europa';", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE id = 5 OR (name = 'Europa' AND ('a'='a'));", 1, 8, None),
+        ("SELECT * FROM $satellites WHERE (id = 5 OR (3=4)) OR name = 'Europa';", 1, 8, None),
+
         ("SELECT COUNT(*) FROM $satellites", 1, 1, None),
         ("SELECT count(*) FROM $satellites", 1, 1, None),
         ("SELECT COUNT (*) FROM $satellites", 1, 1, None),
@@ -688,7 +699,7 @@
         ("SELECT * FROM $astronauts WHERE 'Apollo 11' = any(missions) AND True", 3, 19, None),
         ("SELECT * FROM $astronauts WHERE 'X' > any(alma_mater) OR 'Z' > any(alma_mater)", 3, 19, None),
         ("SELECT * FROM $astronauts WHERE name != 'Brian' AND 'B' < any(alma_mater)", 15, 19, None),
-        ("SELECT * FROM $astronauts WHERE name != 'Brian' OR 'Apollo 11' != any(missions)", 334, 19, None),
+        ("SELECT * FROM $astronauts WHERE name != 'Brian' OR 'Apollo 11' != any(missions)", 357, 19, None),
         ("SELECT * FROM $astronauts WHERE 'Apollo 11' != all(missions) AND name != 'Brian'", 331, 19, None),
         ("SELECT * FROM $astronauts WHERE name != 'Brian' AND 'Apollo 11' = all(missions)", 0, 19, None),