From b8077fb2de0a16b3071746d99da8f7f2a0a9dc52 Mon Sep 17 00:00:00 2001 From: Jan Gleixner Date: Fri, 28 Aug 2026 17:40:34 +0200 Subject: [PATCH 1/2] test: document join scoping for a region absent from the query Pins down current behavior for a table row whose region wasn't part of spatial_element_names, for both a real-but-unqueried element and one missing from the SpatialData object entirely (reachable e.g. via sdata.subset(..., filter_tables=False)). "left", "left_exclusive", "inner" and "right_exclusive" all scope themselves to the queried elements only; "right" always returns the table unfiltered. So "right_exclusive" isn't simply "right minus inner" here, and the two unqueried-region scenarios are currently indistinguishable to join_spatialelement_table. No behavior change; this documents the status quo ahead of a design decision on which of these two behaviors (if either) is intended. --- tests/core/query/test_relational_query.py | 63 +++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/core/query/test_relational_query.py b/tests/core/query/test_relational_query.py index 21e04a376..ae00b2db7 100644 --- a/tests/core/query/test_relational_query.py +++ b/tests/core/query/test_relational_query.py @@ -1127,6 +1127,69 @@ def test_join_preserves_row_order_multiple_interleaved_regions(how, match_rows): assert list(actual_element.index) == expected_index +def test_join_scoping_for_a_region_not_among_the_queried_elements(): + """ + A table can declare a region for which, at join time, no element is passed in `spatial_element_names`. This + can happen for two different reasons: + + - the element genuinely exists elsewhere in the `SpatialData` object, it's just not part of this + particular query (e.g. only some of the annotated elements are of interest right now); + - the element doesn't exist anywhere in the `SpatialData` object at all (e.g. after + `sdata.subset(..., filter_tables=False)`, which keeps the table whole while dropping elements; this + also triggers `SpatialData`'s own "table is annotating '{name}', which is not present" warning). + + This test documents that `join_spatialelement_table` currently can't tell these two cases apart -- and, + across the five join types, treats "not among the queried elements" inconsistently: + + - "left", "left_exclusive", "inner" and "right_exclusive" all scope themselves strictly to the elements + actually passed in `spatial_element_names`: rows for any other region never appear in their output, + matched or not. + - "right" instead always returns the table completely unfiltered (real SQL right-join semantics: every + right-hand row is kept, whether or not the left side covers its key at all), so rows for a region + outside the query leak into the result regardless. + + This means "right_exclusive" is *not* simply "right minus inner" here, unlike what its name may suggest. + Which of these two behaviors is actually intended is an open design question (see + https://github.com/scverse/spatialdata/issues/1162); until it's resolved, this test only pins down the + current, observed behavior so a future change is a deliberate, visible diff here. + """ + from geopandas import GeoDataFrame + from shapely.geometry import Point + + from spatialdata.models import ShapesModel + + def circle() -> GeoDataFrame: + return ShapesModel.parse(GeoDataFrame({"geometry": [Point(0, 0)], "radius": [1.0]}, index=pd.Index([0]))) + + obs = pd.DataFrame( + {"region": pd.Categorical(["a", "b", "c"]), "instance_id": [0, 0, 0], "label": ["a0", "b0", "c0"]}, + index=["0", "1", "2"], + ) + table = TableModel.parse( + AnnData(X=np.zeros((3, 1)), obs=obs), region=["a", "b", "c"], region_key="region", instance_key="instance_id" + ) + sdata_c_exists_unqueried = SpatialData(shapes={"a": circle(), "b": circle(), "c": circle()}, tables={"table": table}) + with pytest.warns(UserWarning, match="is annotating 'c'"): + sdata_c_missing_entirely = sdata_c_exists_unqueried.subset(["a", "b"], filter_tables=False) + + expected_labels_by_how = { + "left": ["a0", "b0"], + "left_exclusive": None, + "inner": ["a0", "b0"], + "right": ["a0", "b0", "c0"], + "right_exclusive": None, + } + for sdata in (sdata_c_exists_unqueried, sdata_c_missing_entirely): + for how, expected_labels in expected_labels_by_how.items(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + _, joined_table = join_spatialelement_table( + sdata=sdata, spatial_element_names=["a", "b"], table_name="table", how=how + ) + actual_labels = None if joined_table is None else joined_table.obs["label"].tolist() + assert actual_labels == expected_labels, f"how={how!r}" + + def test_filter_table_non_annotating(full_sdata): obs = pd.DataFrame({"test": ["a", "b", "c"]}, index=list(map(str, range(3)))) adata = AnnData(obs=obs) From 8e85e138bd4cc8b29e91e4abc0e0f5cd4fdcf8d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:44:28 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/core/query/test_relational_query.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/core/query/test_relational_query.py b/tests/core/query/test_relational_query.py index ae00b2db7..bccf2092c 100644 --- a/tests/core/query/test_relational_query.py +++ b/tests/core/query/test_relational_query.py @@ -1168,7 +1168,9 @@ def circle() -> GeoDataFrame: table = TableModel.parse( AnnData(X=np.zeros((3, 1)), obs=obs), region=["a", "b", "c"], region_key="region", instance_key="instance_id" ) - sdata_c_exists_unqueried = SpatialData(shapes={"a": circle(), "b": circle(), "c": circle()}, tables={"table": table}) + sdata_c_exists_unqueried = SpatialData( + shapes={"a": circle(), "b": circle(), "c": circle()}, tables={"table": table} + ) with pytest.warns(UserWarning, match="is annotating 'c'"): sdata_c_missing_entirely = sdata_c_exists_unqueried.subset(["a", "b"], filter_tables=False)