Skip to content

Commit a11a076

Browse files
authored
fix: group by counts for link row field (baserow#5048)
* Fix group by counts for link row field * Review feedback
1 parent a13578a commit a11a076

3 files changed

Lines changed: 80 additions & 21 deletions

File tree

backend/src/baserow/contrib/database/fields/registries.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,31 +2127,29 @@ def get_group_by_field_filters_and_annotations(
21272127
reversed_field = through_model._meta.get_fields()[1].name
21282128
related_field = through_model._meta.get_fields()[2].name
21292129

2130-
if field_name not in cte:
2131-
row_ids = [row.id for row in rows]
2132-
# Improve performance of the query by creating a CTE with all the
2133-
# relationships of the field to group by. This is significantly faster than
2134-
# doing this every row in a separate subquery.
2135-
aggregated_cte = (
2136-
through_model.objects.filter(**{f"{reversed_field}_id__in": row_ids})
2137-
.values(f"{reversed_field}_id")
2138-
.annotate(
2139-
res=ArrayAgg(
2140-
F(f"{related_field}_id"),
2141-
filter=Q(**{f"{related_field}_id__isnull": False}),
2142-
order_by=self.get_group_by_aggregated_order(related_field),
2143-
)
2144-
)
2145-
)
2146-
cte[field_name] = With(aggregated_cte, name=f"{field_name}_cte")
2147-
21482130
filters = {field_name: value}
2131+
# Use a correlated subquery instead of a CTE so that the database
2132+
# computes each row's linked-ID array via an index lookup on the
2133+
# through table, without materialising the entire relationship set
2134+
# up-front. This is correct for all rows in the base_queryset
2135+
# (including off-page ones) and avoids dragging enhance_by_fields()
2136+
# joins into a CTE filter subquery.
21492137
annotations = {
21502138
field_name: Coalesce(
21512139
Subquery(
2152-
cte[field_name]
2153-
.queryset()
2154-
.filter(**{f"{reversed_field}_id": OuterRef("id")})
2140+
through_model.objects.filter(
2141+
**{
2142+
f"{reversed_field}_id": OuterRef("id"),
2143+
f"{related_field}_id__isnull": False,
2144+
}
2145+
)
2146+
.values(f"{reversed_field}_id")
2147+
.annotate(
2148+
res=ArrayAgg(
2149+
F(f"{related_field}_id"),
2150+
order_by=self.get_group_by_aggregated_order(related_field),
2151+
)
2152+
)
21552153
.values("res")[:1]
21562154
),
21572155
Value([], output_field=ArrayField(IntegerField())),

backend/tests/baserow/contrib/database/field/test_link_row_field_type.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2722,6 +2722,58 @@ def test_get_group_by_metadata_in_rows_with_many_to_many_field(data_fixture):
27222722
}
27232723

27242724

2725+
@pytest.mark.django_db
2726+
def test_list_rows_group_by_link_row_counts_across_pages(api_client, data_fixture):
2727+
user, token = data_fixture.create_user_and_token(
2728+
email="test@test.nl", password="password", first_name="Test1"
2729+
)
2730+
table_a, table_b, link_a_to_b = data_fixture.create_two_linked_tables(user=user)
2731+
2732+
row_b1, row_b2 = (
2733+
RowHandler()
2734+
.force_create_rows(
2735+
user=user,
2736+
table=table_b,
2737+
rows_values=[{}, {}],
2738+
)
2739+
.created_rows
2740+
)
2741+
2742+
RowHandler().force_create_rows(
2743+
user=user,
2744+
table=table_a,
2745+
rows_values=[
2746+
{f"field_{link_a_to_b.id}": []},
2747+
{f"field_{link_a_to_b.id}": []},
2748+
{f"field_{link_a_to_b.id}": [row_b1.id]},
2749+
{f"field_{link_a_to_b.id}": [row_b1.id]},
2750+
{f"field_{link_a_to_b.id}": [row_b2.id]},
2751+
{f"field_{link_a_to_b.id}": [row_b2.id]},
2752+
],
2753+
)
2754+
2755+
grid = data_fixture.create_grid_view(table=table_a)
2756+
data_fixture.create_view_group_by(view=grid, field=link_a_to_b)
2757+
2758+
url = reverse("api:database:views:grid:list", kwargs={"view_id": grid.id})
2759+
response = api_client.get(f"{url}?size=3", **{"HTTP_AUTHORIZATION": f"JWT {token}"})
2760+
assert response.status_code == HTTP_200_OK
2761+
response_json = response.json()
2762+
2763+
# Only groups present on the current page are returned, but their counts
2764+
# must reflect ALL matching rows in the view. Before the fix, off-page
2765+
# rows were missing from the CTE so they were counted as "empty",
2766+
# producing count=5 for [] and count=1 for [row_b1] instead of 2 each.
2767+
assert response_json["group_by_metadata"] == {
2768+
f"field_{link_a_to_b.id}": unordered(
2769+
[
2770+
{f"field_{link_a_to_b.id}": [], "count": 2},
2771+
{f"field_{link_a_to_b.id}": [row_b1.id], "count": 2},
2772+
]
2773+
)
2774+
}
2775+
2776+
27252777
@pytest.mark.django_db
27262778
def test_list_rows_with_group_by_link_row_to_multiple_select_field(
27272779
api_client, data_fixture
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Fix group by counts for link row field",
4+
"issue_origin": "github",
5+
"issue_number": 5047,
6+
"domain": "database",
7+
"bullet_points": [],
8+
"created_at": "2026-03-25"
9+
}

0 commit comments

Comments
 (0)