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
13 changes: 10 additions & 3 deletions backend/src/baserow/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def specific_iterator(
| None = None,
base_model: T | None = None,
select_related: List[str] = None,
skip_missing_specific_objects: bool = False,
) -> List[T]:
"""
Iterates over the given queryset or list of model instances, and finds the specific
Expand All @@ -116,6 +117,8 @@ def specific_iterator(
is provided in the `queryset_or_list` argument. This should be used if the
instances provided in the list have select related objects.
:return: A list of specific objects in the right order.
:param skip_missing_specific_objects: When True, missing specific instances
are logged without raising an error.
"""

if isinstance(queryset_or_list, QuerySet):
Expand Down Expand Up @@ -183,9 +186,13 @@ def specific_iterator(
try:
specific_object = specific_objects[item.id]
except KeyError:
raise base_model.DoesNotExist(
f"The specific object with id {item.id} does not exist."
)
error = f"The specific object with id {item.id} does not exist."

if skip_missing_specific_objects:
logger.error(error)
continue

raise base_model.DoesNotExist(error)

# If there are annotation keys, we must extract them from the original item
# because they should exist there and set them on the specific object so they
Expand Down
30 changes: 25 additions & 5 deletions backend/src/baserow/core/formula/field.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import logging
from typing import Dict, List, Union
from typing import Any, Dict, List, Optional, Union

from django.db import connection, models

Expand Down Expand Up @@ -41,8 +41,29 @@ def __init__(self, *args, **kwargs):
self.null = True
self.blank = True

def _value_is_serialized_object(self, value: FormulaFieldDatabaseValue) -> bool:
return isinstance(value, str) and value[:1] == "{" and value[-1:] == "}"
def _deserialize_baserow_object(
self, value: FormulaFieldDatabaseValue
) -> Optional[Dict[str, Any]]:
"""
Given a value from the database, attempts to deserialize it into a dictionary
representing a Baserow formula object. If the value is not a valid JSON string
representing a Baserow formula object, returns None.

:param value: The value from the database to deserialize.
:return: A dictionary representing the Baserow formula object, or None if
deserialization fails.
"""

if not isinstance(value, str):
return None

if not (value.startswith("{") and value.endswith("}")):
return None

try:
return json.loads(value)
except (TypeError, json.JSONDecodeError):
return None

def _transform_db_value_to_dict(
self, value: FormulaFieldDatabaseValue
Expand All @@ -65,9 +86,8 @@ def _transform_db_value_to_dict(
# receive an integer, we convert it to a string.
value = str(value)
# We could encounter a serialized object...
if self._value_is_serialized_object(value):
if context := self._deserialize_baserow_object(value):
# If we have, then we can parse it and return the `BaserowFormulaObject`
context = json.loads(value)
return BaserowFormulaObject(
mode=context["m"], version=context["v"], formula=context["f"]
)
Expand Down
1 change: 1 addition & 0 deletions backend/src/baserow/core/services/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def per_content_type_queryset_hook(model, queryset):
specific_iterator(
queryset,
per_content_type_queryset_hook=per_content_type_queryset_hook,
skip_missing_specific_objects=True,
)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from decimal import Decimal
from unittest import mock
from unittest.mock import patch

from django.db import connection
from django.http import HttpRequest
from django.shortcuts import reverse

Expand Down Expand Up @@ -654,3 +656,52 @@ def test_query_data_sources_excludes_trashed_service(data_fixture):
DataSource.objects.filter(page=page), specific=True
)
assert len(data_sources) == 1


@pytest.mark.django_db
def test_query_data_sources_with_missing_specific_service(data_fixture):
"""
Test that a missing specific instance of a local baserow service
doesn't cause an exception.
"""

user = data_fixture.create_user()
page = data_fixture.create_builder_page()
integration = data_fixture.create_local_baserow_integration(
user=user, application=page.builder
)

service_1 = data_fixture.create_local_baserow_get_row_service(
integration=integration
)
data_source = data_fixture.create_builder_local_baserow_get_row_data_source(
page=page, service=service_1
)
service_2 = data_fixture.create_local_baserow_get_row_service(
integration=integration
)
data_fixture.create_builder_local_baserow_get_row_data_source(
page=page, service=service_2
)

missing_service_id = service_2.id

# Simulate a data integrity issue
specific_table_name = LocalBaserowGetRow._meta.db_table
with connection.cursor() as cursor:
# Delete from the specific table (LocalBaserowGetRow) instead of using ORM
# to better simulate the data integrity issue.
cursor.execute(
f"DELETE FROM {specific_table_name} WHERE service_ptr_id = %s",
[missing_service_id],
)

with mock.patch("baserow.core.db.logger") as mock_logger:
data_sources = DataSourceHandler()._query_data_sources(
DataSource.objects.filter(page=page), specific=True
)

mock_logger.error.assert_called_once_with(
f"The specific object with id {missing_service_id} does not exist."
)
assert data_sources == [data_source]
19 changes: 19 additions & 0 deletions backend/tests/baserow/core/formula/test_formula_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from baserow.core.formula.field import FormulaField


def test_deserialize_baserow_object_valid():
field = FormulaField()

valid_json = '{"m": "simple", "v": "0.1", "f": "test formula"}'
result = field._deserialize_baserow_object(valid_json)

assert result == {"m": "simple", "v": "0.1", "f": "test formula"}


def test_deserialize_baserow_object_invalid():
field = FormulaField()

invalid_json = "{foo}"
result = field._deserialize_baserow_object(invalid_json)

assert result is None
48 changes: 47 additions & 1 deletion backend/tests/baserow/core/test_core_db.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

from django.contrib.contenttypes.models import ContentType
from django.db import connection
Expand Down Expand Up @@ -655,3 +655,49 @@ def test_multiple_field_prefetch__many_to_many_missing_source(data_fixture):
)
row = rows[0]
assert len(row.field.all()) == 1


@pytest.mark.django_db
def test_specific_iterator_skip_missing_specific_objects(data_fixture):
"""
Tests the optional skip_missing_specific_objects argument.

When True, a missing specific instance shouldn't raise an exception.
"""

user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field = data_fixture.create_text_field(table=table)

# Create a field without a specific type (simulating data integrity issue)
field_without_specific = Field.objects.create(
table=table,
order=1,
name="Test",
primary=False,
content_type=field.content_type,
)

base_queryset = Field.objects.filter(
id__in=[field.id, field_without_specific.id]
).order_by("id")

# Without skip_missing_specific_objects, the 2nd field should
# cause an exception
with pytest.raises(Field.DoesNotExist):
list(specific_iterator(base_queryset))

# With skip_missing_specific_objects, the 2nd field should be
# skipped gracefully
with patch("baserow.core.db.logger") as mock_logger:
specific_objects = list(
specific_iterator(base_queryset, skip_missing_specific_objects=True)
)

# Should only return the 1 specific field, skipping the one without
# a specific instance
assert len(specific_objects) == 1
assert specific_objects[0].id == field.id
mock_logger.error.assert_called_once_with(
f"The specific object with id {field_without_specific.id} does not exist."
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Gracefully handle fetching data source services when the specific instance is missing.",
"issue_origin": "github",
"issue_number": 4389,
"domain": "builder",
"bullet_points": [],
"created_at": "2025-12-04"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Improved error handling when FormulaField value is not JSON serializable.",
"issue_origin": "github",
"issue_number": 4402,
"domain": "core",
"bullet_points": [],
"created_at": "2025-12-05"
}
Loading