We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
🪲 joins aren't pushing predicates when predicates are on each relation in the join
**Sample Code/Statement** _If you can, please submit the SQL statement or Python code snippet, or a representative example using the sample datasets._

~~~sql
SELECT * 
  FROM $planets AS p 
 INNER JOIN $satellites AS s 
    ON p.id = s.planet_id 
 WHERE p.name = 'Jupiter' 
   AND s.radius = 1.0
~~~

Returns the wrong result

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py`
Content:
```
1 # Licensed under the Apache License, Version 2.0 (the "License");
2 # you may not use this file except in compliance with the License.
3 # You may obtain a copy of the License at
4 #
5 #     http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12 
13 from orso.tools import random_string
14 
15 from opteryx.components.logical_planner import LogicalPlan
16 from opteryx.components.logical_planner import LogicalPlanNode
17 from opteryx.components.logical_planner import LogicalPlanStepType
18 from opteryx.managers.expression import NodeType
19 from opteryx.managers.expression import get_all_nodes_of_type
20 
21 from .optimization_strategy import OptimizationStrategy
22 from .optimization_strategy import OptimizerContext
23 
24 
25 def _inner_split(node):
26     while node.node_type == NodeType.NESTED:
27         node = node.centre
28 
29     if node.node_type != NodeType.AND:
30         return [node]
31 
32     # get the left and right filters
33     left_nodes = _inner_split(node.left)
34     right_nodes = _inner_split(node.right)
35 
36     return left_nodes + right_nodes
37 
38 
39 class SplitConjunctivePredicatesStrategy(OptimizationStrategy):
40     def visit(self, node: LogicalPlanNode, context: OptimizerContext) -> OptimizerContext:
41         """
42         Conjunctive Predicates (ANDs) can be split and executed in any order to get the
43         same result. This means we can split them into separate steps in the plan.
44 
45         The reason for splitting is two-fold:
46 
47         1)  Smaller expressions are easier to move around the query plan as they have fewer
48             dependencies.
49         2)  Executing predicates like this means each runs in turn, filtering out some of
50             the records meaning susequent predicates will be operating on fewer records,
51             which is generally faster. We can also order these predicates to get a faster
52             result, balancing the selectivity (get rid of more records faster) vs cost of
53             the check (a numeric check is faster than a string check)
54         """
55         if node.node_type == LogicalPlanStepType.Filter:
56             split_predicates = _inner_split(node.condition)
57             new_nodes = []
58             for predicate in split_predicates:
59                 new_node = LogicalPlanNode(
60                     node_type=LogicalPlanStepType.Filter, condition=predicate
61                 )
62                 new_node.columns = get_all_nodes_of_type(
63                     node.condition, select_nodes=(NodeType.IDENTIFIER,)
64                 )
65                 new_node.relations = node.relations
66                 new_nodes.append(new_node)
67         else:
68             new_nodes = [node]
69 
70         for i, new_node in enumerate(new_nodes):
71             nid = random_string() if (i + 1) < len(new_nodes) else context.node_id
72             context.optimized_plan.add_node(nid, LogicalPlanNode(**new_node.properties))
73             if context.parent_nid:
74                 context.optimized_plan.add_edge(nid, context.parent_nid)
75             context.parent_nid = nid
76 
77         return context
78 
79     def complete(self, plan: LogicalPlan, context: OptimizerContext) -> LogicalPlan:
80         # No finalization needed for this strategy
81         return plan
82
```

Path: `opteryx/__version__.py`
Content:
```
1 __build__ = 329
2 
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 """
16 Store the version here so:
17 1) we don't load dependencies by storing it in __init__.py
18 2) we can import it in setup.py for the same reason
19 """
20 from enum import Enum  # isort: skip
21 
22 
23 class VersionStatus(Enum):
24     ALPHA = "alpha"
25     BETA = "beta"
26     RELEASE = "release"
27 
28 
29 _major = 0
30 _minor = 13
31 _revision = 4
32 _status = VersionStatus.ALPHA
33 
34 __author__ = "@joocer"
35 __version__ = f"{_major}.{_minor}.{_revision}" + (
36     f"-{_status.value}.{__build__}" if _status != VersionStatus.RELEASE else ""
37 )
38
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/opteryx/__version__.py b/opteryx/__version__.py
--- a/opteryx/__version__.py
+++ b/opteryx/__version__.py
@@ -1,4 +1,4 @@
-__build__ = 329
+__build__ = 330
 
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py b/opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py
--- a/opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py
+++ b/opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py
@@ -56,13 +56,12 @@
             split_predicates = _inner_split(node.condition)
             new_nodes = []
             for predicate in split_predicates:
-                new_node = LogicalPlanNode(
-                    node_type=LogicalPlanStepType.Filter, condition=predicate
-                )
+                new_node = LogicalPlanNode(node_type=LogicalPlanStepType.Filter)
+                new_node.condition = predicate
                 new_node.columns = get_all_nodes_of_type(
-                    node.condition, select_nodes=(NodeType.IDENTIFIER,)
+                    predicate, select_nodes=(NodeType.IDENTIFIER,)
                 )
-                new_node.relations = node.relations
+                new_node.relations = {c.source for c in new_node.columns}
                 new_nodes.append(new_node)
         else:
             new_nodes = [node]

{"golden_diff": "diff --git a/opteryx/__version__.py b/opteryx/__version__.py\n--- a/opteryx/__version__.py\n+++ b/opteryx/__version__.py\n@@ -1,4 +1,4 @@\n-__build__ = 329\n+__build__ = 330\n \n # Licensed under the Apache License, Version 2.0 (the \"License\");\n # you may not use this file except in compliance with the License.\ndiff --git a/opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py b/opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py\n--- a/opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py\n+++ b/opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py\n@@ -56,13 +56,12 @@\n             split_predicates = _inner_split(node.condition)\n             new_nodes = []\n             for predicate in split_predicates:\n-                new_node = LogicalPlanNode(\n-                    node_type=LogicalPlanStepType.Filter, condition=predicate\n-                )\n+                new_node = LogicalPlanNode(node_type=LogicalPlanStepType.Filter)\n+                new_node.condition = predicate\n                 new_node.columns = get_all_nodes_of_type(\n-                    node.condition, select_nodes=(NodeType.IDENTIFIER,)\n+                    predicate, select_nodes=(NodeType.IDENTIFIER,)\n                 )\n-                new_node.relations = node.relations\n+                new_node.relations = {c.source for c in new_node.columns}\n                 new_nodes.append(new_node)\n         else:\n             new_nodes = [node]\n", "issue": "\ud83e\udeb2 joins aren't pushing predicates when predicates are on each relation in the join\n**Sample Code/Statement** _If you can, please submit the SQL statement or Python code snippet, or a representative example using the sample datasets._\r\n\r\n~~~sql\r\nSELECT * \r\n  FROM $planets AS p \r\n INNER JOIN $satellites AS s \r\n    ON p.id = s.planet_id \r\n WHERE p.name = 'Jupiter' \r\n   AND s.radius = 1.0\r\n~~~\r\n\r\nReturns the wrong result\n", "before_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom orso.tools import random_string\n\nfrom opteryx.components.logical_planner import LogicalPlan\nfrom opteryx.components.logical_planner import LogicalPlanNode\nfrom opteryx.components.logical_planner import LogicalPlanStepType\nfrom opteryx.managers.expression import NodeType\nfrom opteryx.managers.expression import get_all_nodes_of_type\n\nfrom .optimization_strategy import OptimizationStrategy\nfrom .optimization_strategy import OptimizerContext\n\n\ndef _inner_split(node):\n    while node.node_type == NodeType.NESTED:\n        node = node.centre\n\n    if node.node_type != NodeType.AND:\n        return [node]\n\n    # get the left and right filters\n    left_nodes = _inner_split(node.left)\n    right_nodes = _inner_split(node.right)\n\n    return left_nodes + right_nodes\n\n\nclass SplitConjunctivePredicatesStrategy(OptimizationStrategy):\n    def visit(self, node: LogicalPlanNode, context: OptimizerContext) -> OptimizerContext:\n        \"\"\"\n        Conjunctive Predicates (ANDs) can be split and executed in any order to get the\n        same result. This means we can split them into separate steps in the plan.\n\n        The reason for splitting is two-fold:\n\n        1)  Smaller expressions are easier to move around the query plan as they have fewer\n            dependencies.\n        2)  Executing predicates like this means each runs in turn, filtering out some of\n            the records meaning susequent predicates will be operating on fewer records,\n            which is generally faster. We can also order these predicates to get a faster\n            result, balancing the selectivity (get rid of more records faster) vs cost of\n            the check (a numeric check is faster than a string check)\n        \"\"\"\n        if node.node_type == LogicalPlanStepType.Filter:\n            split_predicates = _inner_split(node.condition)\n            new_nodes = []\n            for predicate in split_predicates:\n                new_node = LogicalPlanNode(\n                    node_type=LogicalPlanStepType.Filter, condition=predicate\n                )\n                new_node.columns = get_all_nodes_of_type(\n                    node.condition, select_nodes=(NodeType.IDENTIFIER,)\n                )\n                new_node.relations = node.relations\n                new_nodes.append(new_node)\n        else:\n            new_nodes = [node]\n\n        for i, new_node in enumerate(new_nodes):\n            nid = random_string() if (i + 1) < len(new_nodes) else context.node_id\n            context.optimized_plan.add_node(nid, LogicalPlanNode(**new_node.properties))\n            if context.parent_nid:\n                context.optimized_plan.add_edge(nid, context.parent_nid)\n            context.parent_nid = nid\n\n        return context\n\n    def complete(self, plan: LogicalPlan, context: OptimizerContext) -> LogicalPlan:\n        # No finalization needed for this strategy\n        return plan\n", "path": "opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py"}, {"content": "__build__ = 329\n\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nStore the version here so:\n1) we don't load dependencies by storing it in __init__.py\n2) we can import it in setup.py for the same reason\n\"\"\"\nfrom enum import Enum  # isort: skip\n\n\nclass VersionStatus(Enum):\n    ALPHA = \"alpha\"\n    BETA = \"beta\"\n    RELEASE = \"release\"\n\n\n_major = 0\n_minor = 13\n_revision = 4\n_status = VersionStatus.ALPHA\n\n__author__ = \"@joocer\"\n__version__ = f\"{_major}.{_minor}.{_revision}\" + (\n    f\"-{_status.value}.{__build__}\" if _status != VersionStatus.RELEASE else \"\"\n)\n", "path": "opteryx/__version__.py"}], "after_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom orso.tools import random_string\n\nfrom opteryx.components.logical_planner import LogicalPlan\nfrom opteryx.components.logical_planner import LogicalPlanNode\nfrom opteryx.components.logical_planner import LogicalPlanStepType\nfrom opteryx.managers.expression import NodeType\nfrom opteryx.managers.expression import get_all_nodes_of_type\n\nfrom .optimization_strategy import OptimizationStrategy\nfrom .optimization_strategy import OptimizerContext\n\n\ndef _inner_split(node):\n    while node.node_type == NodeType.NESTED:\n        node = node.centre\n\n    if node.node_type != NodeType.AND:\n        return [node]\n\n    # get the left and right filters\n    left_nodes = _inner_split(node.left)\n    right_nodes = _inner_split(node.right)\n\n    return left_nodes + right_nodes\n\n\nclass SplitConjunctivePredicatesStrategy(OptimizationStrategy):\n    def visit(self, node: LogicalPlanNode, context: OptimizerContext) -> OptimizerContext:\n        \"\"\"\n        Conjunctive Predicates (ANDs) can be split and executed in any order to get the\n        same result. This means we can split them into separate steps in the plan.\n\n        The reason for splitting is two-fold:\n\n        1)  Smaller expressions are easier to move around the query plan as they have fewer\n            dependencies.\n        2)  Executing predicates like this means each runs in turn, filtering out some of\n            the records meaning susequent predicates will be operating on fewer records,\n            which is generally faster. We can also order these predicates to get a faster\n            result, balancing the selectivity (get rid of more records faster) vs cost of\n            the check (a numeric check is faster than a string check)\n        \"\"\"\n        if node.node_type == LogicalPlanStepType.Filter:\n            split_predicates = _inner_split(node.condition)\n            new_nodes = []\n            for predicate in split_predicates:\n                new_node = LogicalPlanNode(node_type=LogicalPlanStepType.Filter)\n                new_node.condition = predicate\n                new_node.columns = get_all_nodes_of_type(\n                    predicate, select_nodes=(NodeType.IDENTIFIER,)\n                )\n                new_node.relations = {c.source for c in new_node.columns}\n                new_nodes.append(new_node)\n        else:\n            new_nodes = [node]\n\n        for i, new_node in enumerate(new_nodes):\n            nid = random_string() if (i + 1) < len(new_nodes) else context.node_id\n            context.optimized_plan.add_node(nid, LogicalPlanNode(**new_node.properties))\n            if context.parent_nid:\n                context.optimized_plan.add_edge(nid, context.parent_nid)\n            context.parent_nid = nid\n\n        return context\n\n    def complete(self, plan: LogicalPlan, context: OptimizerContext) -> LogicalPlan:\n        # No finalization needed for this strategy\n        return plan\n", "path": "opteryx/components/cost_based_optimizer/strategies/split_conjunctive_predicates.py"}, {"content": "__build__ = 330\n\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nStore the version here so:\n1) we don't load dependencies by storing it in __init__.py\n2) we can import it in setup.py for the same reason\n\"\"\"\nfrom enum import Enum  # isort: skip\n\n\nclass VersionStatus(Enum):\n    ALPHA = \"alpha\"\n    BETA = \"beta\"\n    RELEASE = \"release\"\n\n\n_major = 0\n_minor = 13\n_revision = 4\n_status = VersionStatus.ALPHA\n\n__author__ = \"@joocer\"\n__version__ = f\"{_major}.{_minor}.{_revision}\" + (\n    f\"-{_status.value}.{__build__}\" if _status != VersionStatus.RELEASE else \"\"\n)\n", "path": "opteryx/__version__.py"}]}