Repository: dask/dask

Issue:
Incorrect divisions when doing an inner join between 2 1-partition dataframes
<!-- Please include a self-contained copy-pastable example that generates the issue if possible.

Please be concise with code posted. See guidelines below on how to provide a good bug report:

- Craft Minimal Bug Reports http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports
- Minimal Complete Verifiable Examples https://stackoverflow.com/help/mcve

Bug reports that follow these guidelines are easier to diagnose, and so are often handled much more quickly.
-->

**What happened**:

When doing an inner join between 2 Dask DataFrames that both have 1 partition, the joined Dask Dataframe inherits the divisions from the right DF. This appears to be incorrect behavior when `on` is specified.

This problem was discovered when calling repartition() later on yielded an empty dataframe.

**What you expected to happen**:

The divisions field should be inherited from the left dataframe instead.

**Minimal Complete Verifiable Example**:

```python
import dask
import dask.dataframe as dd
import pandas as pd


df = pd.DataFrame({'a': ['a','b','c']}, index=[0,1,2])
ddf = dd.from_pandas(df, npartitions=1)

right_df = pd.DataFrame({'a': ['a','b','c'], 'b':[1.0, 2.0, 3.0]}).set_index('a')

print("Divisions before join", ddf.divisions)
ddf = ddf.join(right_df, how='inner', on='a')
print("Divisions after join", ddf.divisions)

print("Without repartition", dask.compute(ddf)[0])
print("With no-op repartition", dask.compute(ddf.repartition(divisions=ddf.divisions))[0])
```

The first & second print statements should print the same thing, and so should the third and fourth.

**Anything else we need to know?**:

Some initial investigation notes:
https://github.com/dask/dask/blob/main/dask/dataframe/multi.py#L391 is definitely the problematic area.
The most trivial fix for my particular case would be to swap the left and right if blocks, but that doesn't seem like the full solution.

Perhaps a better solution would be factoring in the `on` parameter(s) in the if statements

**Environment**:

- Dask version: 2021.8.1 (discovered on 2021.4.1)
- Python version: 3.8.8
- Operating System: Ubuntu Bionic
- Install method (conda, pip, source): pip



Discussion:
My current workaround is something like this:
```
ddf = left_ddf.join(...)
if left_ddf.npartitions == 1:
	ddf.divisions = left_ddf.divisions
```
Thank you for the excellent bug report! Are you interested in opening a pull request to resolve this issue? It seems like you have a good sense of what is going on. 
Thanks! As mentioned above, I know enough to fix my particular case, but am not familiar enough with all the other possibile combinations of parameters to know whether or not I'll break other cases
That's totally fair and that is what tests are for if you want to give it a go :)

Resolving patch:
diff --git a/dask/dataframe/multi.py b/dask/dataframe/multi.py
--- a/dask/dataframe/multi.py
+++ b/dask/dataframe/multi.py
@@ -393,35 +393,54 @@ def single_partition_join(left, right, **kwargs):
     # new index will not necessarily correspond with the current divisions
 
     meta = left._meta_nonempty.merge(right._meta_nonempty, **kwargs)
+
+    use_left = kwargs.get("right_index") or right._contains_index_name(
+        kwargs.get("right_on")
+    )
+    use_right = kwargs.get("left_index") or left._contains_index_name(
+        kwargs.get("left_on")
+    )
+
+    if len(meta) == 0:
+        if use_left:
+            meta.index = meta.index.astype(left.index.dtype)
+        elif use_right:
+            meta.index = meta.index.astype(right.index.dtype)
+        else:
+            meta.index = meta.index.astype("int64")
+
     kwargs["empty_index_dtype"] = meta.index.dtype
     kwargs["categorical_columns"] = meta.select_dtypes(include="category").columns
 
     name = "merge-" + tokenize(left, right, **kwargs)
-    if left.npartitions == 1 and kwargs["how"] in allowed_right:
-        left_key = first(left.__dask_keys__())
+
+    if right.npartitions == 1 and kwargs["how"] in allowed_left:
+        right_key = first(right.__dask_keys__())
         dsk = {
             (name, i): (apply, merge_chunk, [left_key, right_key], kwargs)
-            for i, right_key in enumerate(right.__dask_keys__())
+            for i, left_key in enumerate(left.__dask_keys__())
         }
-
-        if kwargs.get("right_index") or right._contains_index_name(
-            kwargs.get("right_on")
-        ):
+        if use_left:
+            divisions = left.divisions
+        elif use_right and len(right.divisions) == len(left.divisions):
             divisions = right.divisions
         else:
-            divisions = [None for _ in right.divisions]
+            divisions = [None for _ in left.divisions]
 
-    elif right.npartitions == 1 and kwargs["how"] in allowed_left:
-        right_key = first(right.__dask_keys__())
+    elif left.npartitions == 1 and kwargs["how"] in allowed_right:
+        left_key = first(left.__dask_keys__())
         dsk = {
             (name, i): (apply, merge_chunk, [left_key, right_key], kwargs)
-            for i, left_key in enumerate(left.__dask_keys__())
+            for i, right_key in enumerate(right.__dask_keys__())
         }
 
-        if kwargs.get("left_index") or left._contains_index_name(kwargs.get("left_on")):
+        if use_right:
+            divisions = right.divisions
+        elif use_left and len(left.divisions) == len(right.divisions):
             divisions = left.divisions
         else:
-            divisions = [None for _ in left.divisions]
+            divisions = [None for _ in right.divisions]
+
     else:
         raise NotImplementedError(
             "single_partition_join has no fallback for invalid calls"
@@ -513,14 +532,14 @@ def merge(
     if not is_dask_collection(left):
         if right_index and left_on:  # change to join on index
             left = left.set_index(left[left_on])
-            left_on = False
+            left_on = None
             left_index = True
         left = from_pandas(left, npartitions=1)  # turn into DataFrame
 
     if not is_dask_collection(right):
         if left_index and right_on:  # change to join on index
             right = right.set_index(right[right_on])
-            right_on = False
+            right_on = None
             right_index = True
         right = from_pandas(right, npartitions=1)  # turn into DataFrame
 


Test patch:
diff --git a/dask/dataframe/tests/test_merge_column_and_index.py b/dask/dataframe/tests/test_merge_column_and_index.py
--- a/dask/dataframe/tests/test_merge_column_and_index.py
+++ b/dask/dataframe/tests/test_merge_column_and_index.py
@@ -111,7 +111,7 @@ def test_merge_known_to_single(df_left, df_right, ddf_left, ddf_right_single, on
 
     # Assertions
     assert_eq(result, expected)
-    assert_eq(result.divisions, ddf_left.divisions)
+    assert result.divisions == ddf_left.divisions
     assert len(result.__dask_graph__()) < 30
 
 
@@ -125,7 +125,7 @@ def test_merge_single_to_known(df_left, df_right, ddf_left_single, ddf_right, on
 
     # Assertions
     assert_eq(result, expected)
-    assert_eq(result.divisions, ddf_right.divisions)
+    assert result.divisions == ddf_right.divisions
     assert len(result.__dask_graph__()) < 30
 
 
diff --git a/dask/dataframe/tests/test_multi.py b/dask/dataframe/tests/test_multi.py
--- a/dask/dataframe/tests/test_multi.py
+++ b/dask/dataframe/tests/test_multi.py
@@ -1197,6 +1197,20 @@ def test_join_by_index_patterns(how, shuffle):
             """
 
 
+def test_join_gives_proper_divisions():
+    # https://github.com/dask/dask/issues/8113
+    df = pd.DataFrame({"a": ["a", "b", "c"]}, index=[0, 1, 2])
+    ddf = dd.from_pandas(df, npartitions=1)
+
+    right_df = pd.DataFrame({"b": [1.0, 2.0, 3.0]}, index=["a", "b", "c"])
+
+    expected = df.join(right_df, how="inner", on="a")
+    actual = ddf.join(right_df, how="inner", on="a")
+    assert actual.divisions == ddf.divisions
+
+    assert_eq(expected, actual)
+
+
 @pytest.mark.parametrize("how", ["inner", "outer", "left", "right"])
 @pytest.mark.parametrize("shuffle", ["disk", "tasks"])
 def test_merge_by_multiple_columns(how, shuffle):
@@ -1434,14 +1448,14 @@ def test_cheap_single_partition_merge_on_index():
     # for empty joins.
     expected.index = expected.index.astype("int64")
 
-    assert actual.known_divisions
+    assert not actual.known_divisions
     assert_eq(actual, expected)
 
     actual = bb.merge(aa, right_index=True, left_on="x", how="inner")
     expected = b.merge(a, right_index=True, left_on="x", how="inner")
     expected.index = expected.index.astype("int64")
 
-    assert actual.known_divisions
+    assert not actual.known_divisions
     assert_eq(actual, expected)