Repository: modin-project/modin

Issue:
OmniSci raise assertion error when performing `DataFrame.merge`
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Any
- **Modin version** (`modin.__version__`): 521b62e
- **Python version**: 3.7.5
- **Code we can use to reproduce**:
```python
import modin.pandas as pd

df1 = pd.DataFrame(
    {"l_a": [1, 2, 3, 4], "l_b": [10, 20, 30, 40], "l_c": [11, 12, 13, 14]}
)
df2 = pd.DataFrame(
    {"r_a": [1, 2, 3, 4], "r_b": [10, 20, 30, 40], "r_d": [100, 200, 300, 400]}
)

res = df1.merge(df2, left_on=["l_a", "l_b"], right_on=["r_a", "r_b"])
print(res)
```
Output:
```
2021-08-04T18:04:08.483131 F 954314 1 RelAlgExecutor.cpp:3480 Check failed: bin_oper->size() >= size_t(2) (0 >= 2)
Aborted (core dumped)
```

<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->




Discussion:
left_on and right_on are unsupported now. We don't default to pandas which looks wrong anyway

Resolving patch:
diff --git a/modin/experimental/backends/omnisci/query_compiler.py b/modin/experimental/backends/omnisci/query_compiler.py
--- a/modin/experimental/backends/omnisci/query_compiler.py
+++ b/modin/experimental/backends/omnisci/query_compiler.py
@@ -239,23 +239,32 @@ def getitem_array(self, key):
 
     def merge(self, right, **kwargs):
         on = kwargs.get("on", None)
+        left_on = kwargs.get("left_on", None)
+        right_on = kwargs.get("right_on", None)
         left_index = kwargs.get("left_index", False)
         right_index = kwargs.get("right_index", False)
         """Only non-index joins with explicit 'on' are supported"""
         if left_index is False and right_index is False:
-            if on is None:
-                on = [c for c in self.columns if c in right.columns]
+            if left_on is None and right_on is None:
+                if on is None:
+                    on = [c for c in self.columns if c in right.columns]
+                left_on = on
+                right_on = on
+
+            if not isinstance(left_on, list):
+                left_on = [left_on]
+            if not isinstance(right_on, list):
+                right_on = [right_on]
 
             how = kwargs.get("how", "inner")
             sort = kwargs.get("sort", False)
             suffixes = kwargs.get("suffixes", None)
-            if not isinstance(on, list):
-                on = [on]
             return self.__constructor__(
                 self._modin_frame.join(
                     right._modin_frame,
                     how=how,
-                    on=on,
+                    left_on=left_on,
+                    right_on=right_on,
                     sort=sort,
                     suffixes=suffixes,
                 )
diff --git a/modin/experimental/engines/omnisci_on_ray/frame/data.py b/modin/experimental/engines/omnisci_on_ray/frame/data.py
--- a/modin/experimental/engines/omnisci_on_ray/frame/data.py
+++ b/modin/experimental/engines/omnisci_on_ray/frame/data.py
@@ -847,7 +847,15 @@ def astype(self, col_dtypes, **kwargs):
             force_execution_mode=self._force_execution_mode,
         )
 
-    def join(self, other, how="inner", on=None, sort=False, suffixes=("_x", "_y")):
+    def join(
+        self,
+        other,
+        how="inner",
+        left_on=None,
+        right_on=None,
+        sort=False,
+        suffixes=("_x", "_y"),
+    ):
         """
         Join operation.
 
@@ -857,8 +865,10 @@ def join(self, other, how="inner", on=None, sort=False, suffixes=("_x", "_y")):
             A frame to join with.
         how : str, default: "inner"
             A type of join.
-        on : list of str, optional
-            A list of columns to join on.
+        left_on : list of str, optional
+            A list of columns for the left frame to join on.
+        right_on : list of str, optional
+            A list of columns for the right frame to join on.
         sort : bool, default: False
             Sort the result by join keys.
         suffixes : list-like of str, default: ("_x", "_y")
@@ -871,32 +881,37 @@ def join(self, other, how="inner", on=None, sort=False, suffixes=("_x", "_y")):
             The new frame.
         """
         assert (
-            on is not None
-        ), "Merge with unspecified 'on' parameter is not supported in the engine"
+            left_on is not None and right_on is not None
+        ), "Merge with unspecified 'left_on' or 'right_on' parameter is not supported in the engine"
+        assert len(left_on) == len(
+            right_on
+        ), "'left_on' and 'right_on' lengths don't match"
 
-        for col in on:
-            assert (
-                col in self.columns and col in other.columns
-            ), "Only cases when both frames contain key column are supported"
+        for col in left_on:
+            assert col in self.columns, f"'left_on' references unknown column {col}"
+        for col in right_on:
+            assert col in other.columns, f"'right_on' references unknown column {col}"
 
         new_columns = []
         new_dtypes = []
         exprs = OrderedDict()
 
-        conflicting_cols = set(self.columns) & set(other.columns) - set(on)
+        left_conflicts = set(self.columns) & (set(other.columns) - set(right_on))
+        right_conflicts = set(other.columns) & (set(self.columns) - set(left_on))
+        conflicting_cols = left_conflicts | right_conflicts
         for c in self.columns:
             new_name = f"{c}{suffixes[0]}" if c in conflicting_cols else c
             new_columns.append(new_name)
             new_dtypes.append(self._dtypes[c])
             exprs[new_name] = self.ref(c)
         for c in other.columns:
-            if c not in on:
+            if c not in left_on or c not in right_on:
                 new_name = f"{c}{suffixes[1]}" if c in conflicting_cols else c
                 new_columns.append(new_name)
                 new_dtypes.append(other._dtypes[c])
                 exprs[new_name] = other.ref(c)
 
-        condition = self._build_equi_join_condition(other, on, on)
+        condition = self._build_equi_join_condition(other, left_on, right_on)
 
         op = JoinNode(
             self,
@@ -916,7 +931,7 @@ def join(self, other, how="inner", on=None, sort=False, suffixes=("_x", "_y")):
 
         if sort:
             res = res.sort_rows(
-                on, ascending=True, ignore_index=True, na_position="last"
+                left_on, ascending=True, ignore_index=True, na_position="last"
             )
 
         return res


Test patch:
diff --git a/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py b/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
--- a/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
+++ b/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
@@ -1420,6 +1420,34 @@ def merge(df1, df2, **kwargs):
 
         run_and_compare(merge, data=self.dt_data1, data2=self.dt_data2)
 
+    left_data = {"a": [1, 2, 3, 4], "b": [10, 20, 30, 40], "c": [11, 12, 13, 14]}
+    right_data = {"c": [1, 2, 3, 4], "b": [10, 20, 30, 40], "d": [100, 200, 300, 400]}
+
+    @pytest.mark.parametrize("how", how_values)
+    @pytest.mark.parametrize(
+        "left_on, right_on", [["a", "c"], [["a", "b"], ["c", "b"]]]
+    )
+    def test_merge_left_right_on(self, how, left_on, right_on):
+        def merge(df1, df2, how, left_on, right_on, **kwargs):
+            return df1.merge(df2, how=how, left_on=left_on, right_on=right_on)
+
+        run_and_compare(
+            merge,
+            data=self.left_data,
+            data2=self.right_data,
+            how=how,
+            left_on=left_on,
+            right_on=right_on,
+        )
+        run_and_compare(
+            merge,
+            data=self.right_data,
+            data2=self.left_data,
+            how=how,
+            left_on=right_on,
+            right_on=left_on,
+        )
+
 
 class TestBinaryOp:
     data = {