Repository: geopandas/geopandas

Issue:
Spatial join loses index names of input
Small example:

```
In [15]: countries = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))

In [16]: cities = geopandas.read_file(geopandas.datasets.get_path('naturalearth_cities'))

In [17]: countries = countries.rename(columns={'name': 'country_name'}).set_index('country_name')

In [21]: cities.index.name = 'id'

In [22]: geopandas.sjoin(cities, countries, op='within').head()
Out[22]: 
             name                                     geometry index_right     pop_est continent iso_a3  gdp_md_est
0    Vatican City  POINT (12.45338654497177 41.90328217996012)       Italy  58126212.0    Europe    ITA   1823000.0
1      San Marino    POINT (12.44177015780014 43.936095834768)       Italy  58126212.0    Europe    ITA   1823000.0
192          Rome    POINT (12.481312562874 41.89790148509894)       Italy  58126212.0    Europe    ITA   1823000.0
2           Vaduz  POINT (9.516669472907267 47.13372377429357)     Austria   8210281.0    Europe    AUT    329500.0
184        Vienna  POINT (16.36469309674374 48.20196113681686)     Austria   8210281.0    Europe    AUT    329500.0
```

So both the index name of the left dataframe is lost, as the index name of the right dataframe (which has become the generic 'index_right' column)


Discussion:
I'd be interested in fixing this as I find this behavior pretty counterintuitive. But at the same time I can imagine this "fix" being a pretty big breaking change (I know my own code has `index_right` hard-coded here and there). @jorisvandenbossche or anyone else, do you think the next release is "big" enough for this behavior to change or should it wait until a really major version?
@bnaul I vote for fixing this in the next release. After all, it is a bug. The longer it will take to fix it the more issues it might cause.
would like to pick up this issue to work on
shall the desired output be like below?

```ipython
In [22]: geopandas.sjoin(cities, countries, op='within').head()
Out[22]: 
             name                                     geometry country_name     pop_est continent iso_a3  gdp_md_est
id
0    Vatican City  POINT (12.45338654497177 41.90328217996012)       Italy  58126212.0    Europe    ITA   1823000.0
1      San Marino    POINT (12.44177015780014 43.936095834768)       Italy  58126212.0    Europe    ITA   1823000.0
192          Rome    POINT (12.481312562874 41.89790148509894)       Italy  58126212.0    Europe    ITA   1823000.0
2           Vaduz  POINT (9.516669472907267 47.13372377429357)     Austria   8210281.0    Europe    AUT    329500.0
184        Vienna  POINT (16.36469309674374 48.20196113681686)     Austria   8210281.0    Europe    AUT    329500.0
```
It seems like part of this could be done without resulting in a breaking change.

Nonbreaking change:
In the case of `how="left"` and `how="inner"` the index of the left data frame is used as the index for the result.  In this case, instead of setting it as `None` it could be set to the original name of the left index.  Hopefully nobody is relying on the returned index having no name, so setting the name to the original shouldn't cause any issues.

Likewise, for `how="right"` the same could be done with the right index name.

It seems like this fix could / should be done at any time.


Potentially breaking change:
The name of the index on the opposite end of the join (e.g., the right index in the case of `how="left"` and `how="inner"`) may well be a breaking change, since folks are already specifically handling the column with the name as returned here (as stated above).  So either this should wait for a new release or left as is.

In #498 there is some similar discussion about whether to keep the indeces or not. And it is talked about consistancy with pandas.
I guess that geopandas.sjoin is something like short for "gdf.join(gdf2, on='geometry operation')". What does pandas really do in such cases? It looks like it always transhes the right index, no matter what even for right joins with keyword on=xxx as it seems, which is quite disturbing actually as you get Nans there:

``` python:
>>> df1=pd.DataFrame([[1,3,6],[3,4,1]],columns=['d1','b1','d1'],index=pd.Index([2,1],name='s'))
>>> df2=pd.DataFrame([[0,3,2],[1,5,2]],columns=['d2','b2','d2'],index=pd.Index([3,4],name='t'))
>>> df3=df1.join(df2,how='outer')
>>> print(df3)
    d1   b1   d1   d2   b2   d2
1  3.0  4.0  1.0  NaN  NaN  NaN
2  1.0  3.0  6.0  NaN  NaN  NaN
3  NaN  NaN  NaN  0.0  3.0  2.0
4  NaN  NaN  NaN  1.0  5.0  2.0

>>> df3=df1.join(df2.set_index('b2'),on='b1',how='outer')
>>> print(df3)
      d1  b1   d1   d2   d2
2.0  1.0   3  6.0  0.0  2.0
1.0  3.0   4  1.0  NaN  NaN
NaN  NaN   5  NaN  1.0  2.0

>>> df3=df1.join(df2.set_index('b2'),on='b1',how='left')
>>> print(df3)
   d1  b1  d1   d2   d2
s
2   1   3   6  0.0  2.0
1   3   4   1  NaN  NaN

>>> df3=df1.join(df2.set_index('b2'),on='b1',how='right')
>>> print(df3)
      d1  b1   d1  d2  d2
2.0  1.0   3  6.0   0   2
NaN  NaN   5  NaN   1   2

```
 

My question is now if there would be a possible workaround by assigning property ***setters or getters for properties index_right/index_left***, such that one could still use old codes without immideatly getting into trouble? 
Pandas also offers to name variables as non column variables for subclassing (***Define original properties*** in https://pandas.pydata.org/docs/development/extending.html). But I dont know if you need it

so what about property methods combined with some ***subclassing of [ ] or __getitem__/__setitem__ and calling pandas __getitem__/__setitem__after that*** ? Not sure though what happens with loc[ ] then. A real mess!

Even in pandas it does not always trash the name of the index, only when the right index is involved it vanishes. And .. in pandas there is never any column that refers to old left or right indexes... as far as I can see from above tests. They are just dropped

Man that is a complicated problem.

Now, as is, the name of the ***to be index is kept*** and just the ***other index name is not transferred into column name*** and pretty sure people use other.index_right/left . And changing it will break their code

Is this at all viable? Since again property decorators are also used for extending constructors somehow (again https://pandas.pydata.org/docs/development/extending.html , ***Override constructor properties*** ). I don't know.

But maybe you guys can get some idea from this about how to solve this issue???

Resolving patch:
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
--- a/geopandas/tools/sjoin.py
+++ b/geopandas/tools/sjoin.py
@@ -1,4 +1,6 @@
+from functools import partial
 from typing import Optional
+import warnings
 
 import numpy as np
 import pandas as pd
@@ -146,18 +148,6 @@ def _basic_checks(left_df, right_df, how, lsuffix, rsuffix):
     if not _check_crs(left_df, right_df):
         _crs_mismatch_warn(left_df, right_df, stacklevel=4)
 
-    index_left = "index_{}".format(lsuffix)
-    index_right = "index_{}".format(rsuffix)
-
-    # due to GH 352
-    if any(left_df.columns.isin([index_left, index_right])) or any(
-        right_df.columns.isin([index_left, index_right])
-    ):
-        raise ValueError(
-            "'{0}' and '{1}' cannot be names in the frames being"
-            " joined".format(index_left, index_right)
-        )
-
 
 def _geom_predicate_query(left_df, right_df, predicate, distance):
     """Compute geometric comparisons and get matching indices.
@@ -210,6 +200,101 @@ def _geom_predicate_query(left_df, right_df, predicate, distance):
     return indices
 
 
+def _reset_index_with_suffix(df, suffix, other):
+    """
+    Equivalent of df.reset_index(), but with adding 'suffix' to auto-generated
+    column names.
+    """
+    index_original = df.index.names
+    df_reset = df.reset_index()
+    column_names = df_reset.columns.to_numpy(copy=True)
+    for i, label in enumerate(index_original):
+        # if the original label was None, add suffix to auto-generated name
+        if label is None:
+            new_label = column_names[i]
+            if "level" in new_label:
+                # reset_index of MultiIndex gives "level_i" names, preserve the "i"
+                lev = new_label.split("_")[1]
+                new_label = f"index_{suffix}{lev}"
+            else:
+                new_label = f"index_{suffix}"
+            # check new label will not be in other dataframe
+            if new_label in df.columns or new_label in other.columns:
+                raise ValueError(
+                    "'{0}' cannot be a column name in the frames being"
+                    " joined".format(new_label)
+                )
+            column_names[i] = new_label
+    return df_reset, pd.Index(column_names)
+
+
+def _process_column_names_with_suffix(
+    left: pd.Index, right: pd.Index, suffixes, left_df, right_df
+):
+    """
+    Add suffixes to overlapping labels (ignoring the geometry column).
+
+    This is based on pandas' merge logic at https://github.com/pandas-dev/pandas/blob/
+    a0779adb183345a8eb4be58b3ad00c223da58768/pandas/core/reshape/merge.py#L2300-L2370
+    """
+    to_rename = left.intersection(right)
+    if len(to_rename) == 0:
+        return left, right
+
+    lsuffix, rsuffix = suffixes
+
+    if not lsuffix and not rsuffix:
+        raise ValueError(f"columns overlap but no suffix specified: {to_rename}")
+
+    def renamer(x, suffix, geometry):
+        if x in to_rename and x != geometry and suffix is not None:
+            return f"{x}_{suffix}"
+        return x
+
+    lrenamer = partial(renamer, suffix=lsuffix, geometry=left_df._geometry_column_name)
+    rrenamer = partial(renamer, suffix=rsuffix, geometry=right_df._geometry_column_name)
+
+    # TODO retain index name?
+    left_renamed = pd.Index([lrenamer(lab) for lab in left])
+    right_renamed = pd.Index([rrenamer(lab) for lab in right])
+
+    dups = []
+    if not left_renamed.is_unique:
+        # Only warn when duplicates are caused because of suffixes, already duplicated
+        # columns in origin should not warn
+        dups = left_renamed[(left_renamed.duplicated()) & (~left.duplicated())].tolist()
+    if not right_renamed.is_unique:
+        dups.extend(
+            right_renamed[(right_renamed.duplicated()) & (~right.duplicated())].tolist()
+        )
+    # TODO turn this into an error (pandas has done so as well)
+    if dups:
+        warnings.warn(
+            f"Passing 'suffixes' which cause duplicate columns {set(dups)} in the "
+            f"result is deprecated and will raise a MergeError in a future version.",
+            FutureWarning,
+            stacklevel=4,
+        )
+
+    return left_renamed, right_renamed
+
+
+def _restore_index(joined, index_names, index_names_original):
+    """
+    Set back the the original index columns, and restoring their name as `None`
+    if they didn't have a name originally.
+    """
+    joined = joined.set_index(list(index_names))
+
+    # restore the fact that the index didn't have a name
+    joined_index_names = list(joined.index.names)
+    for i, label in enumerate(index_names_original):
+        if label is None:
+            joined_index_names[i] = None
+    joined.index.names = joined_index_names
+    return joined
+
+
 def _frame_join(join_df, left_df, right_df, how, lsuffix, rsuffix):
     """Join the GeoDataFrames at the DataFrame level.
 
@@ -240,33 +325,29 @@ def _frame_join(join_df, left_df, right_df, how, lsuffix, rsuffix):
     # index in geopandas may be any arbitrary dtype. so reset both indices now
     # and store references to the original indices, to be reaffixed later.
     # GH 352
-    index_left = "index_{}".format(lsuffix)
     left_df = left_df.copy(deep=True)
-    try:
-        left_index_name = left_df.index.name
-        left_df.index = left_df.index.rename(index_left)
-    except TypeError:
-        index_left = [
-            "index_{}".format(lsuffix + str(pos))
-            for pos, ix in enumerate(left_df.index.names)
-        ]
-        left_index_name = left_df.index.names
-        left_df.index = left_df.index.rename(index_left)
-    left_df = left_df.reset_index()
-
-    index_right = "index_{}".format(rsuffix)
+    left_nlevels = left_df.index.nlevels
+    left_index_original = left_df.index.names
+    left_df, left_column_names = _reset_index_with_suffix(left_df, lsuffix, right_df)
+
     right_df = right_df.copy(deep=True)
-    try:
-        right_index_name = right_df.index.name
-        right_df.index = right_df.index.rename(index_right)
-    except TypeError:
-        index_right = [
-            "index_{}".format(rsuffix + str(pos))
-            for pos, ix in enumerate(right_df.index.names)
-        ]
-        right_index_name = right_df.index.names
-        right_df.index = right_df.index.rename(index_right)
-    right_df = right_df.reset_index()
+    right_nlevels = right_df.index.nlevels
+    right_index_original = right_df.index.names
+    right_df, right_column_names = _reset_index_with_suffix(right_df, rsuffix, left_df)
+
+    # if conflicting names in left and right, add suffix
+    left_column_names, right_column_names = _process_column_names_with_suffix(
+        left_column_names,
+        right_column_names,
+        (lsuffix, rsuffix),
+        left_df,
+        right_df,
+    )
+    left_df.columns = left_column_names
+    right_df.columns = right_column_names
+
+    left_index = left_df.columns[:left_nlevels]
+    right_index = right_df.columns[:right_nlevels]
 
     # perform join on the dataframes
     if how == "inner":
@@ -277,15 +358,11 @@ def _frame_join(join_df, left_df, right_df, how, lsuffix, rsuffix):
                 right_df.drop(right_df.geometry.name, axis=1),
                 left_on="_key_right",
                 right_index=True,
-                suffixes=("_{}".format(lsuffix), "_{}".format(rsuffix)),
+                suffixes=(None, None),
             )
-            .set_index(index_left)
             .drop(["_key_right"], axis=1)
         )
-        if isinstance(index_left, list):
-            joined.index.names = left_index_name
-        else:
-            joined.index.name = left_index_name
+        joined = _restore_index(joined, left_index, left_index_original)
 
     elif how == "left":
         join_df = join_df.set_index("_key_left")
@@ -296,15 +373,11 @@ def _frame_join(join_df, left_df, right_df, how, lsuffix, rsuffix):
                 how="left",
                 left_on="_key_right",
                 right_index=True,
-                suffixes=("_{}".format(lsuffix), "_{}".format(rsuffix)),
+                suffixes=(None, None),
             )
-            .set_index(index_left)
             .drop(["_key_right"], axis=1)
         )
-        if isinstance(index_left, list):
-            joined.index.names = left_index_name
-        else:
-            joined.index.name = left_index_name
+        joined = _restore_index(joined, left_index, left_index_original)
 
     else:  # how == 'right':
         joined = (
@@ -316,16 +389,12 @@ def _frame_join(join_df, left_df, right_df, how, lsuffix, rsuffix):
                 left_index=True,
                 right_on="_key_left",
                 how="right",
-                suffixes=("_{}".format(lsuffix), "_{}".format(rsuffix)),
+                suffixes=(None, None),
             )
-            .set_index(index_right)
             .drop(["_key_left", "_key_right"], axis=1)
             .set_geometry(right_df.geometry.name)
         )
-        if isinstance(index_right, list):
-            joined.index.names = right_index_name
-        else:
-            joined.index.name = right_index_name
+        joined = _restore_index(joined, right_index, right_index_original)
 
     return joined
 


Test patch:
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -149,7 +149,7 @@ def test_inner(self, predicate, dfs):
             ].astype("int64")
         if index == "named-index":
             exp[["df1_ix", "df2_ix"]] = exp[["df1_ix", "df2_ix"]].astype("int64")
-            exp = exp.set_index("df1_ix").rename(columns={"df2_ix": "index_right"})
+            exp = exp.set_index("df1_ix")
         if index in ["default-index", "string-index"]:
             exp = exp.set_index("index_left")
             exp.index.name = None
@@ -159,10 +159,7 @@ def test_inner(self, predicate, dfs):
             )
             exp.index.names = df1.index.names
         if index == "named-multi-index":
-            exp = exp.set_index(["df1_ix1", "df1_ix2"]).rename(
-                columns={"df2_ix1": "index_right0", "df2_ix2": "index_right1"}
-            )
-            exp.index.names = df1.index.names
+            exp = exp.set_index(["df1_ix1", "df1_ix2"])
 
         assert_frame_equal(res, exp)
 
@@ -199,7 +196,7 @@ def test_left(self, predicate, dfs):
             res["index_right"] = res["index_right"].astype(float)
         elif index == "named-index":
             exp[["df1_ix"]] = exp[["df1_ix"]].astype("int64")
-            exp = exp.set_index("df1_ix").rename(columns={"df2_ix": "index_right"})
+            exp = exp.set_index("df1_ix")
         if index in ["default-index", "string-index"]:
             exp = exp.set_index("index_left")
             exp.index.name = None
@@ -209,10 +206,7 @@ def test_left(self, predicate, dfs):
             )
             exp.index.names = df1.index.names
         if index == "named-multi-index":
-            exp = exp.set_index(["df1_ix1", "df1_ix2"]).rename(
-                columns={"df2_ix1": "index_right0", "df2_ix2": "index_right1"}
-            )
-            exp.index.names = df1.index.names
+            exp = exp.set_index(["df1_ix1", "df1_ix2"])
 
         assert_frame_equal(res, exp)
 
@@ -315,7 +309,7 @@ def test_right(self, predicate, dfs):
             res["index_left"] = res["index_left"].astype(float)
         elif index == "named-index":
             exp[["df2_ix"]] = exp[["df2_ix"]].astype("int64")
-            exp = exp.set_index("df2_ix").rename(columns={"df1_ix": "index_left"})
+            exp = exp.set_index("df2_ix")
         if index in ["default-index", "string-index"]:
             exp = exp.set_index("index_right")
             exp = exp.reindex(columns=res.columns)
@@ -326,10 +320,8 @@ def test_right(self, predicate, dfs):
             )
             exp.index.names = df2.index.names
         if index == "named-multi-index":
-            exp = exp.set_index(["df2_ix1", "df2_ix2"]).rename(
-                columns={"df1_ix1": "index_left0", "df1_ix2": "index_left1"}
-            )
-            exp.index.names = df2.index.names
+            exp = exp.set_index(["df2_ix1", "df2_ix2"])
+
         if predicate == "within":
             exp = exp.sort_index()
 
@@ -392,6 +384,232 @@ def test_sjoin_dwithin(
         assert_frame_equal(expected_gdf.sort_index(), joined.sort_index())
 
 
+class TestIndexNames:
+    @pytest.mark.parametrize("how", ["inner", "left", "right"])
+    def test_preserve_index_names(self, how):
+        # preserve names of both left and right index
+        geoms = [Point(1, 1), Point(2, 2)]
+        df1 = GeoDataFrame({"geometry": geoms}, index=pd.Index([1, 2], name="myidx1"))
+        df2 = GeoDataFrame(
+            {"geometry": geoms}, index=pd.Index(["a", "b"], name="myidx2")
+        )
+        result = sjoin(df1, df2, how=how)
+        if how in ("inner", "left"):
+            expected = GeoDataFrame(
+                {"myidx1": [1, 2], "geometry": geoms, "myidx2": ["a", "b"]}
+            ).set_index("myidx1")
+        else:
+            # right join
+            expected = GeoDataFrame(
+                {"myidx2": ["a", "b"], "myidx1": [1, 2], "geometry": geoms},
+            ).set_index("myidx2")
+        assert_geodataframe_equal(result, expected)
+
+        # but also add suffixes if both left and right have the same index
+        df1.index.name = "myidx"
+        df2.index.name = "myidx"
+        result = sjoin(df1, df2, how=how)
+        if how in ("inner", "left"):
+            expected = GeoDataFrame(
+                {"myidx_left": [1, 2], "geometry": geoms, "myidx_right": ["a", "b"]}
+            ).set_index("myidx_left")
+        else:
+            # right join
+            expected = GeoDataFrame(
+                {"myidx_right": ["a", "b"], "myidx_left": [1, 2], "geometry": geoms},
+            ).set_index("myidx_right")
+        assert_geodataframe_equal(result, expected)
+
+    @pytest.mark.parametrize("how", ["inner", "left", "right"])
+    def test_preserve_index_names_multiindex(self, how):
+        # preserve names of both left and right index
+        geoms = [Point(1, 1), Point(2, 2)]
+        df1 = GeoDataFrame(
+            {"geometry": geoms},
+            index=pd.MultiIndex.from_tuples(
+                [("a", 1), ("b", 2)], names=["myidx1", "level2"]
+            ),
+        )
+        df2 = GeoDataFrame(
+            {"geometry": geoms},
+            index=pd.MultiIndex.from_tuples(
+                [("c", 3), ("d", 4)], names=["myidx2", None]
+            ),
+        )
+        result = sjoin(df1, df2, how=how)
+        expected_base = GeoDataFrame(
+            {
+                "myidx1": ["a", "b"],
+                "level2": [1, 2],
+                "geometry": geoms,
+                "myidx2": ["c", "d"],
+                "index_right1": [3, 4],
+            }
+        )
+        if how in ("inner", "left"):
+            expected = expected_base.set_index(["myidx1", "level2"])
+        else:
+            # right join
+            expected = expected_base.set_index(["myidx2", "index_right1"])
+            # if it was originally None, that is preserved
+            expected.index.names = ["myidx2", None]
+        assert_geodataframe_equal(result, expected)
+
+        # but also add suffixes if both left and right have the same index
+        df1.index.names = ["myidx", "level2"]
+        df2.index.names = ["myidx", None]
+        result = sjoin(df1, df2, how=how)
+        expected_base = GeoDataFrame(
+            {
+                "myidx_left": ["a", "b"],
+                "level2": [1, 2],
+                "geometry": geoms,
+                "myidx_right": ["c", "d"],
+                "index_right1": [3, 4],
+            }
+        )
+        if how in ("inner", "left"):
+            expected = expected_base.set_index(["myidx_left", "level2"])
+        else:
+            # right join
+            expected = expected_base.set_index(["myidx_right", "index_right1"])
+            # if it was originally None, that is preserved
+            expected.index.names = ["myidx_right", None]
+        assert_geodataframe_equal(result, expected)
+
+    @pytest.mark.parametrize("how", ["inner", "left", "right"])
+    def test_duplicate_column_index_name(self, how):
+        # case where a left column and the right index have the same name or the
+        # other way around -> correctly add suffix or preserve index name
+        geoms = [Point(1, 1), Point(2, 2)]
+        df1 = GeoDataFrame({"myidx": [1, 2], "geometry": geoms})
+        df2 = GeoDataFrame(
+            {"geometry": geoms}, index=pd.Index(["a", "b"], name="myidx")
+        )
+        result = sjoin(df1, df2, how=how)
+        if how in ("inner", "left"):
+            expected = GeoDataFrame(
+                {"myidx_left": [1, 2], "geometry": geoms, "myidx_right": ["a", "b"]}
+            )
+        else:
+            # right join
+            expected = GeoDataFrame(
+                {"index_left": [0, 1], "myidx_left": [1, 2], "geometry": geoms},
+                index=pd.Index(["a", "b"], name="myidx_right"),
+            )
+        assert_geodataframe_equal(result, expected)
+
+        result = sjoin(df2, df1, how=how)
+        if how in ("inner", "left"):
+            expected = GeoDataFrame(
+                {"geometry": geoms, "index_right": [0, 1], "myidx_right": [1, 2]},
+                index=pd.Index(["a", "b"], name="myidx_left"),
+            )
+        else:
+            # right join
+            expected = GeoDataFrame(
+                {"myidx_left": ["a", "b"], "myidx_right": [1, 2], "geometry": geoms},
+            )
+        assert_geodataframe_equal(result, expected)
+
+    @pytest.mark.parametrize("how", ["inner", "left", "right"])
+    def test_duplicate_column_index_name_multiindex(self, how):
+        # case where a left column and the right index have the same name or the
+        # other way around -> correctly add suffix or preserve index name
+        geoms = [Point(1, 1), Point(2, 2)]
+        df1 = GeoDataFrame({"myidx": [1, 2], "geometry": geoms})
+        df2 = GeoDataFrame(
+            {"geometry": geoms},
+            index=pd.MultiIndex.from_tuples(
+                [("a", 1), ("b", 2)], names=["myidx", "level2"]
+            ),
+        )
+        result = sjoin(df1, df2, how=how)
+        if how in ("inner", "left"):
+            expected = GeoDataFrame(
+                {
+                    "myidx_left": [1, 2],
+                    "geometry": geoms,
+                    "myidx_right": ["a", "b"],
+                    "level2": [1, 2],
+                }
+            )
+        else:
+            # right join
+            expected = GeoDataFrame(
+                {"index_left": [0, 1], "myidx_left": [1, 2], "geometry": geoms},
+                index=pd.MultiIndex.from_tuples(
+                    [("a", 1), ("b", 2)], names=["myidx_right", "level2"]
+                ),
+            )
+        assert_geodataframe_equal(result, expected)
+
+        result = sjoin(df2, df1, how=how)
+        if how in ("inner", "left"):
+            expected = GeoDataFrame(
+                {"geometry": geoms, "index_right": [0, 1], "myidx_right": [1, 2]},
+                index=pd.MultiIndex.from_tuples(
+                    [("a", 1), ("b", 2)], names=["myidx_left", "level2"]
+                ),
+            )
+        else:
+            # right join
+            expected = GeoDataFrame(
+                {
+                    "myidx_left": ["a", "b"],
+                    "level2": [1, 2],
+                    "myidx_right": [1, 2],
+                    "geometry": geoms,
+                },
+            )
+        assert_geodataframe_equal(result, expected)
+
+    @pytest.mark.parametrize("how", ["inner", "left", "right"])
+    def test_conflicting_column_index_name(self, how):
+        # test case where the auto-generated index name conflicts
+        geoms = [Point(1, 1), Point(2, 2)]
+        df1 = GeoDataFrame({"index_right": [1, 2], "geometry": geoms})
+        df2 = GeoDataFrame({"geometry": geoms})
+        with pytest.raises(ValueError, match="'index_right' cannot be a column name"):
+            sjoin(df1, df2, how=how)
+
+    @pytest.mark.xfail
+    @pytest.mark.parametrize("how", ["inner", "left", "right"])
+    def test_conflicting_column_with_suffix(self, how):
+        # test case where the auto-generated index name conflicts
+        geoms = [Point(1, 1), Point(2, 2)]
+        df1 = GeoDataFrame(
+            {"column": [1, 2], "column_right": ["a", "b"], "geometry": geoms}
+        )
+        df2 = GeoDataFrame({"column": [0.1, 0.2], "geometry": geoms})
+
+        result = sjoin(df1, df2, how=how)
+        if how in ("inner", "left"):
+            expected = GeoDataFrame(
+                {1: [1, 2], 2: ["a", "b"], 3: geoms, 4: [0, 1], 5: [0.1, 0.2]}
+            )
+            expected.columns = [
+                "column_left",
+                "column_right",
+                "geometry",
+                "index_right",
+                "column_right",
+            ]
+        else:
+            # right join
+            expected = GeoDataFrame(
+                {1: [0, 1], 2: [1, 2], 3: ["a", "b"], 4: [0.1, 0.2], 5: geoms}
+            )
+            expected.columns = [
+                "index_left",
+                "column_left",
+                "column_right",
+                "column_right",
+                "geometry",
+            ]
+        assert_geodataframe_equal(result, expected)
+
+
 @pytest.mark.usefixtures("_setup_class_nybb_filename")
 class TestSpatialJoinNYBB:
     def setup_method(self):