Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This workflow automatically updates the Database Team's GitHub Project board bas

The workflow triggers on PR events (opened, review requested, review submitted, merged, etc.) and automatically:

1. **Checks domain labels** - Only processes PRs with labels starting with `domain::database` or `domain::core`
1. **Checks domain labels** - Only processes PRs with labels starting with `database 🗄`
2. **Adds PR to project** - Ensures the PR is added to the Database Team project board
3. **Updates Status field** - Sets the PR status based on its state:
- `In Progress` - PR is a draft
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/database-projects-issues-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Update Database Team Project Fields

env:
PROJECT_NUMBER: '3'
DOMAIN_LABELS: '["domain::database"]'
DOMAIN_LABELS: '["database 🗄️"]'
STATUS_FIELD_NAME: 'Status'
STATUS_TODO: 'Todo'

Expand Down Expand Up @@ -91,7 +91,7 @@ jobs:
return;
}

console.log(`Issue has domain label: ${labels.filter(l => l.startsWith('domain::')).join(', ')}`);
console.log(`Issue has domain label: ${labels.filter(l => domainLabels.some(d => l.startsWith(d))).join(', ')}`);

// ============================================================
// CHECK IF ALREADY IN PROJECT
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/database-projects-pr-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Database Team PR Automation

env:
PROJECT_NUMBER: '3'
DOMAIN_LABELS: '["domain::database"]'
DOMAIN_LABELS: '["database 🗄️"]'
STATUS_FIELD_NAME: 'Status'
REVIEW_STATUS_FIELD_NAME: 'Review Status'
STATUS_IN_PROGRESS: 'In Progress'
Expand All @@ -26,6 +26,7 @@ on:

jobs:
update-status:
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Update PR Project Status
Expand Down Expand Up @@ -114,7 +115,7 @@ jobs:
return;
}

console.log(`PR has domain label: ${labels.filter(l => l.startsWith('domain::')).join(', ')}`);
console.log(`PR has domain label: ${labels.filter(l => domainLabels.some(d => l.startsWith(d))).join(', ')}`);

const statusField = project.fields.nodes.find(f => f.name === '${{ env.STATUS_FIELD_NAME }}');
const reviewStatusField = project.fields.nodes.find(f => f.name === '${{ env.REVIEW_STATUS_FIELD_NAME }}');
Expand Down
40 changes: 19 additions & 21 deletions backend/src/baserow/contrib/database/fields/registries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2127,31 +2127,29 @@ def get_group_by_field_filters_and_annotations(
reversed_field = through_model._meta.get_fields()[1].name
related_field = through_model._meta.get_fields()[2].name

if field_name not in cte:
row_ids = [row.id for row in rows]
# Improve performance of the query by creating a CTE with all the
# relationships of the field to group by. This is significantly faster than
# doing this every row in a separate subquery.
aggregated_cte = (
through_model.objects.filter(**{f"{reversed_field}_id__in": row_ids})
.values(f"{reversed_field}_id")
.annotate(
res=ArrayAgg(
F(f"{related_field}_id"),
filter=Q(**{f"{related_field}_id__isnull": False}),
order_by=self.get_group_by_aggregated_order(related_field),
)
)
)
cte[field_name] = With(aggregated_cte, name=f"{field_name}_cte")

filters = {field_name: value}
# Use a correlated subquery instead of a CTE so that the database
# computes each row's linked-ID array via an index lookup on the
# through table, without materialising the entire relationship set
# up-front. This is correct for all rows in the base_queryset
# (including off-page ones) and avoids dragging enhance_by_fields()
# joins into a CTE filter subquery.
annotations = {
field_name: Coalesce(
Subquery(
cte[field_name]
.queryset()
.filter(**{f"{reversed_field}_id": OuterRef("id")})
through_model.objects.filter(
**{
f"{reversed_field}_id": OuterRef("id"),
f"{related_field}_id__isnull": False,
}
)
.values(f"{reversed_field}_id")
.annotate(
res=ArrayAgg(
F(f"{related_field}_id"),
order_by=self.get_group_by_aggregated_order(related_field),
)
)
.values("res")[:1]
),
Value([], output_field=ArrayField(IntegerField())),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2722,6 +2722,58 @@ def test_get_group_by_metadata_in_rows_with_many_to_many_field(data_fixture):
}


@pytest.mark.django_db
def test_list_rows_group_by_link_row_counts_across_pages(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
table_a, table_b, link_a_to_b = data_fixture.create_two_linked_tables(user=user)

row_b1, row_b2 = (
RowHandler()
.force_create_rows(
user=user,
table=table_b,
rows_values=[{}, {}],
)
.created_rows
)

RowHandler().force_create_rows(
user=user,
table=table_a,
rows_values=[
{f"field_{link_a_to_b.id}": []},
{f"field_{link_a_to_b.id}": []},
{f"field_{link_a_to_b.id}": [row_b1.id]},
{f"field_{link_a_to_b.id}": [row_b1.id]},
{f"field_{link_a_to_b.id}": [row_b2.id]},
{f"field_{link_a_to_b.id}": [row_b2.id]},
],
)

grid = data_fixture.create_grid_view(table=table_a)
data_fixture.create_view_group_by(view=grid, field=link_a_to_b)

url = reverse("api:database:views:grid:list", kwargs={"view_id": grid.id})
response = api_client.get(f"{url}?size=3", **{"HTTP_AUTHORIZATION": f"JWT {token}"})
assert response.status_code == HTTP_200_OK
response_json = response.json()

# Only groups present on the current page are returned, but their counts
# must reflect ALL matching rows in the view. Before the fix, off-page
# rows were missing from the CTE so they were counted as "empty",
# producing count=5 for [] and count=1 for [row_b1] instead of 2 each.
assert response_json["group_by_metadata"] == {
f"field_{link_a_to_b.id}": unordered(
[
{f"field_{link_a_to_b.id}": [], "count": 2},
{f"field_{link_a_to_b.id}": [row_b1.id], "count": 2},
]
)
}


@pytest.mark.django_db
def test_list_rows_with_group_by_link_row_to_multiple_select_field(
api_client, data_fixture
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Fix group by counts for link row field",
"issue_origin": "github",
"issue_number": 5047,
"domain": "database",
"bullet_points": [],
"created_at": "2026-03-25"
}
10 changes: 5 additions & 5 deletions web-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
},
"dependencies": {
"@nuxtjs/i18n": "10.2.4",
"@sentry/node": "10.40.0",
"@sentry/nuxt": "10.40.0",
"@sentry/vue": "10.40.0",
"@sentry/node": "10.46.0",
"@sentry/nuxt": "10.46.0",
"@sentry/vue": "10.46.0",
"@tiptap/core": "^3.13.0",
"@tiptap/extension-blockquote": "^3.13.0",
"@tiptap/extension-bold": "^3.13.0",
Expand Down Expand Up @@ -103,12 +103,12 @@
"normalize.css": "^8.0.1",
"nuxt": "^3.21.2",
"papaparse": "5.4.1",
"path-to-regexp": "8.2.0",
"path-to-regexp": "8.4.0",
"posthog-js": "^1.232.4",
"sass": "1.98.0",
"thenby": "^1.3.4",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.9",
"tiptap-markdown": "0.9.0",
"tldjs": "^2.3.1",
"ulid": "^3.0.1",
"vite-plugin-node-polyfills": "^0.24.0",
Expand Down
Loading
Loading