Skip to content

Commit 4883984

Browse files
authored
Gracefully handle missing specific instance of DataSource service (baserow#4390)
* Optimistically batch fetch specific services, fall back to fetching individually. * Add changelog * Add comment * Simplify * Add docstring * Simplify the fix by allow specific_iterator() to skip missing specific objects.
1 parent 624f00b commit 4883984

File tree

5 files changed

+118
-4
lines changed

5 files changed

+118
-4
lines changed

backend/src/baserow/core/db.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def specific_iterator(
9191
| None = None,
9292
base_model: T | None = None,
9393
select_related: List[str] = None,
94+
skip_missing_specific_objects: bool = False,
9495
) -> List[T]:
9596
"""
9697
Iterates over the given queryset or list of model instances, and finds the specific
@@ -116,6 +117,8 @@ def specific_iterator(
116117
is provided in the `queryset_or_list` argument. This should be used if the
117118
instances provided in the list have select related objects.
118119
:return: A list of specific objects in the right order.
120+
:param skip_missing_specific_objects: When True, missing specific instances
121+
are logged without raising an error.
119122
"""
120123

121124
if isinstance(queryset_or_list, QuerySet):
@@ -183,9 +186,13 @@ def specific_iterator(
183186
try:
184187
specific_object = specific_objects[item.id]
185188
except KeyError:
186-
raise base_model.DoesNotExist(
187-
f"The specific object with id {item.id} does not exist."
188-
)
189+
error = f"The specific object with id {item.id} does not exist."
190+
191+
if skip_missing_specific_objects:
192+
logger.error(error)
193+
continue
194+
195+
raise base_model.DoesNotExist(error)
189196

190197
# If there are annotation keys, we must extract them from the original item
191198
# because they should exist there and set them on the specific object so they

backend/src/baserow/core/services/handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def per_content_type_queryset_hook(model, queryset):
112112
specific_iterator(
113113
queryset,
114114
per_content_type_queryset_hook=per_content_type_queryset_hook,
115+
skip_missing_specific_objects=True,
115116
)
116117
)
117118

backend/tests/baserow/contrib/builder/data_sources/test_data_source_handler.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from decimal import Decimal
2+
from unittest import mock
23
from unittest.mock import patch
34

5+
from django.db import connection
46
from django.http import HttpRequest
57
from django.shortcuts import reverse
68

@@ -654,3 +656,52 @@ def test_query_data_sources_excludes_trashed_service(data_fixture):
654656
DataSource.objects.filter(page=page), specific=True
655657
)
656658
assert len(data_sources) == 1
659+
660+
661+
@pytest.mark.django_db
662+
def test_query_data_sources_with_missing_specific_service(data_fixture):
663+
"""
664+
Test that a missing specific instance of a local baserow service
665+
doesn't cause an exception.
666+
"""
667+
668+
user = data_fixture.create_user()
669+
page = data_fixture.create_builder_page()
670+
integration = data_fixture.create_local_baserow_integration(
671+
user=user, application=page.builder
672+
)
673+
674+
service_1 = data_fixture.create_local_baserow_get_row_service(
675+
integration=integration
676+
)
677+
data_source = data_fixture.create_builder_local_baserow_get_row_data_source(
678+
page=page, service=service_1
679+
)
680+
service_2 = data_fixture.create_local_baserow_get_row_service(
681+
integration=integration
682+
)
683+
data_fixture.create_builder_local_baserow_get_row_data_source(
684+
page=page, service=service_2
685+
)
686+
687+
missing_service_id = service_2.id
688+
689+
# Simulate a data integrity issue
690+
specific_table_name = LocalBaserowGetRow._meta.db_table
691+
with connection.cursor() as cursor:
692+
# Delete from the specific table (LocalBaserowGetRow) instead of using ORM
693+
# to better simulate the data integrity issue.
694+
cursor.execute(
695+
f"DELETE FROM {specific_table_name} WHERE service_ptr_id = %s",
696+
[missing_service_id],
697+
)
698+
699+
with mock.patch("baserow.core.db.logger") as mock_logger:
700+
data_sources = DataSourceHandler()._query_data_sources(
701+
DataSource.objects.filter(page=page), specific=True
702+
)
703+
704+
mock_logger.error.assert_called_once_with(
705+
f"The specific object with id {missing_service_id} does not exist."
706+
)
707+
assert data_sources == [data_source]

backend/tests/baserow/core/test_core_db.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest.mock import MagicMock
1+
from unittest.mock import MagicMock, patch
22

33
from django.contrib.contenttypes.models import ContentType
44
from django.db import connection
@@ -655,3 +655,49 @@ def test_multiple_field_prefetch__many_to_many_missing_source(data_fixture):
655655
)
656656
row = rows[0]
657657
assert len(row.field.all()) == 1
658+
659+
660+
@pytest.mark.django_db
661+
def test_specific_iterator_skip_missing_specific_objects(data_fixture):
662+
"""
663+
Tests the optional skip_missing_specific_objects argument.
664+
665+
When True, a missing specific instance shouldn't raise an exception.
666+
"""
667+
668+
user = data_fixture.create_user()
669+
table = data_fixture.create_database_table(user=user)
670+
field = data_fixture.create_text_field(table=table)
671+
672+
# Create a field without a specific type (simulating data integrity issue)
673+
field_without_specific = Field.objects.create(
674+
table=table,
675+
order=1,
676+
name="Test",
677+
primary=False,
678+
content_type=field.content_type,
679+
)
680+
681+
base_queryset = Field.objects.filter(
682+
id__in=[field.id, field_without_specific.id]
683+
).order_by("id")
684+
685+
# Without skip_missing_specific_objects, the 2nd field should
686+
# cause an exception
687+
with pytest.raises(Field.DoesNotExist):
688+
list(specific_iterator(base_queryset))
689+
690+
# With skip_missing_specific_objects, the 2nd field should be
691+
# skipped gracefully
692+
with patch("baserow.core.db.logger") as mock_logger:
693+
specific_objects = list(
694+
specific_iterator(base_queryset, skip_missing_specific_objects=True)
695+
)
696+
697+
# Should only return the 1 specific field, skipping the one without
698+
# a specific instance
699+
assert len(specific_objects) == 1
700+
assert specific_objects[0].id == field.id
701+
mock_logger.error.assert_called_once_with(
702+
f"The specific object with id {field_without_specific.id} does not exist."
703+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Gracefully handle fetching data source services when the specific instance is missing.",
4+
"issue_origin": "github",
5+
"issue_number": 4389,
6+
"domain": "builder",
7+
"bullet_points": [],
8+
"created_at": "2025-12-04"
9+
}

0 commit comments

Comments
 (0)