Skip to content

fix: left join with no matching rows crashed queries on labels - #1266

Merged
LucaMarconato merged 2 commits into
scverse:mainfrom
FarisHrvat:fix-left-join-no-match
Nov 11, 2026
Merged

LucaMarconato merged 2 commits into
scverse:mainfrom
FarisHrvat:fix-left-join-no-match

Conversation

@FarisHrvat

@FarisHrvat FarisHrvat commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Querying a labels element that is annotated by a table, but whose cropped area contains none of the annotated instances, crashes polygon_query and bounding_box_query with IndexError: list index out of range in _region_as_str_if_list_of_len_one, which reads the first element of an empty region list.

The helper now only unwraps a list of length one. A left join with no matching rows returns an empty table with region == [], as the docstring and the storage format spec describe, and the query callers already drop empty tables, so the query keeps the labels and drops the table. The exclusive and right joins use the same helper, so they're covered too. Thanks to @jan-glx for the suggestion.

It's also the random failure of test_attributes_are_copied[labels2d-*] in the pre-release job on #1265. The labels there come from the shared RNG in conftest.py, so whether the small query box contains an annotated instance depends on which tests ran before on the same worker. Added a test that builds that case on purpose.

Also addresses the left join part of #1226.

@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.43%. Comparing base (ce40138) to head (fa156d8).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1266   +/-   ##
=======================================
  Coverage   91.43%   91.43%           
=======================================
  Files          53       53           
  Lines        8395     8395           
=======================================
  Hits         7676     7676           
  Misses        719      719           
Files with missing lines Coverage Δ
src/spatialdata/_core/query/relational_query.py 93.34% <100.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

When a queried labels element is annotated by a table but none of its
remaining instances are in the table, the left join built a 0-row table and
_region_as_str_if_list_of_len_one crashed on the empty region list. Return
no table in that case, like the inner and exclusive joins already do.

This is the random IndexError in test_attributes_are_copied[labels2d]: the
labels come from the shared RNG in conftest, so whether the tiny query box
contains an annotated instance depends on test order.
@jan-glx
jan-glx force-pushed the fix-left-join-no-match branch from 5377b7f to d426a63 Compare November 4, 2026 13:37
@jan-glx

jan-glx commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

(sorry for the rebase)

Thanks for tracking this down, and for the test that builds the case on purpose.

I'd fix it one level down, though. The IndexError comes from _region_as_str_if_list_of_len_one, which returns region[0] for an empty list. With

def _region_as_str_if_list_of_len_one(region: list[str]) -> str | list[str]:
    return region[0] if len(region) == 1 else region

a single region is still stored as a string (the released behaviour #1160 restored), and the left join returns a 0-row table with region == [] instead of None:

  • That's what the docstring of join_spatialelement_table describes for 'left' (the table "is filtered to only include matching rows"; None is documented for 'left_exclusive').
  • It's what the storage format spec requires: "a table without rows cannot name elements in region (it sets region to [])" (annotation rules). TableModel.validate accepts it.
  • The query callers (_filter_table_by_elements, SpatialData._filter_tables) already drop empty tables, so your new test passes unchanged.
  • It also covers the two other call sites of the helper (exclusive and right joins).

I tried this together with your test on a branch, and the query and operations tests seemed happy with it. Feel free to take it over into this PR if it makes sense to you, or I'm happy to push it here myself.

This is also the bug in #1226 (left join part), and the cause of the flaky test_attributes_are_copied[labels2d-*].

A left join with no matching rows now returns an empty table with region set to [], as the docstring and the storage spec describe, rather than None. This also covers the exclusive and right joins, which use the same helper.

Co-authored-by: Jan Gleixner <[email protected]>
@FarisHrvat

Copy link
Copy Markdown
Contributor Author

Thanks, that makes sense, the helper is the better place for it. I've taken your change over on top and kept the test. Query and operations tests pass locally. I also updated the description and linked #1226.

@jan-glx jan-glx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@jan-glx jan-glx left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LucaMarconato could you have a look? This also fixes the flaky test_attributes_are_copied[labels2d-*] in the pre-release job, so it is on the release list.

@LucaMarconato LucaMarconato left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@LucaMarconato
LucaMarconato merged commit bd410fa into scverse:main Nov 11, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants