Skip to content

Commit ecc358a

Browse files
authored
Fix formula recursion detected when the same data source is used twice in the one formula (baserow#4196)
1 parent 8d25a43 commit ecc358a

File tree

5 files changed

+86
-25
lines changed

5 files changed

+86
-25
lines changed

backend/src/baserow/contrib/builder/data_providers/data_provider_types.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,6 @@ def get_data_chunk(self, dispatch_context: BuilderDispatchContext, path: List[st
170170
# The data source has probably been deleted
171171
raise InvalidRuntimeFormula() from exc
172172

173-
# Declare the call and check for recursion
174-
dispatch_context.add_call(data_source.id)
175-
176173
dispatch_result = DataSourceHandler().dispatch_data_source(
177174
data_source, dispatch_context
178175
)

backend/src/baserow/contrib/builder/data_sources/handler.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -508,16 +508,12 @@ def dispatch_data_sources(
508508
data_sources_dispatch[data_source.id] = {}
509509
continue
510510

511-
# Add the initial call to the call stack
512-
dispatch_context.add_call(data_source.id)
513511
try:
514512
data_sources_dispatch[data_source.id] = self.dispatch_data_source(
515513
data_source, dispatch_context
516514
)
517515
except Exception as e:
518516
data_sources_dispatch[data_source.id] = e
519-
# Reset the stack as we are starting a new dispatch
520-
dispatch_context.reset_call_stack()
521517

522518
return data_sources_dispatch
523519

@@ -538,21 +534,12 @@ def dispatch_data_source(
538534
raise ServiceImproperlyConfiguredDispatchException(
539535
"The service type is missing."
540536
)
541-
542537
cache = dispatch_context.cache
543-
call_stack = dispatch_context.call_stack
544-
538+
page = dispatch_context.page
545539
current_data_source_dispatched = dispatch_context.data_source or data_source
546540

547-
dispatch_context = dispatch_context.clone(
548-
data_source=current_data_source_dispatched,
549-
)
550-
551-
# keep the call stack
552-
dispatch_context.call_stack = call_stack
553-
554541
if current_data_source_dispatched != data_source:
555-
data_sources = self.get_data_sources_with_cache(dispatch_context.page)
542+
data_sources = self.get_data_sources_with_cache(page)
556543
ordered_ids = [d.id for d in data_sources]
557544
if ordered_ids.index(current_data_source_dispatched.id) < ordered_ids.index(
558545
data_source.id
@@ -561,9 +548,16 @@ def dispatch_data_source(
561548
"You can't reference a data source after the current data source"
562549
)
563550

551+
# Clone the dispatch context to keep the call stack as it is
552+
cloned_dispatch_context = dispatch_context.clone(
553+
data_source=current_data_source_dispatched
554+
)
555+
# Declare the call and check for recursion
556+
cloned_dispatch_context.add_call(data_source.id)
557+
564558
if data_source.id not in cache.setdefault("data_source_contents", {}):
565559
service_dispatch = self.service_handler.dispatch_service(
566-
data_source.service.specific, dispatch_context
560+
data_source.service.specific, cloned_dispatch_context
567561
)
568562

569563
# Cache the dispatch in the formula cache if we have formulas that need

backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,26 +240,20 @@ def test_data_source_data_provider_get_data_chunk_with_list_data_source(data_fix
240240
== "Blue"
241241
)
242242

243-
dispatch_context.reset_call_stack()
244-
245243
assert (
246244
data_source_provider.get_data_chunk(
247245
dispatch_context, [data_source.id, "2", fields[1].db_column]
248246
)
249247
== "White"
250248
)
251249

252-
dispatch_context.reset_call_stack()
253-
254250
assert (
255251
data_source_provider.get_data_chunk(
256252
dispatch_context, [data_source.id, "0", "id"]
257253
)
258254
== rows[0].id
259255
)
260256

261-
dispatch_context.reset_call_stack()
262-
263257
assert data_source_provider.get_data_chunk(
264258
dispatch_context, [data_source.id, "*", fields[1].db_column]
265259
) == ["Blue", "Orange", "White", "Green"]

backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_actions_handler.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import json
2+
from unittest.mock import MagicMock
3+
14
import pytest
25

6+
from baserow.contrib.builder.data_sources.builder_dispatch_context import (
7+
BuilderDispatchContext,
8+
)
39
from baserow.contrib.builder.workflow_actions.exceptions import (
410
WorkflowActionNotInElement,
511
)
@@ -15,6 +21,7 @@
1521
OpenPageWorkflowActionType,
1622
)
1723
from baserow.core.services.models import Service
24+
from baserow.test_utils.helpers import AnyInt, AnyStr
1825

1926

2027
@pytest.mark.django_db
@@ -229,3 +236,64 @@ def test_order_workflow_actions_different_scopes(data_fixture):
229236
)
230237

231238
assert page_workflow_action.order == element_workflow_action.order
239+
240+
241+
@pytest.mark.django_db
242+
def test_dispatch_workflow_action_doesnt_trigger_formula_recursion(data_fixture):
243+
user, token = data_fixture.create_user_and_token()
244+
workspace = data_fixture.create_workspace(user=user)
245+
database = data_fixture.create_database_application(workspace=workspace)
246+
builder = data_fixture.create_builder_application(workspace=workspace)
247+
table, fields, rows = data_fixture.build_table(
248+
user=user,
249+
columns=[
250+
("Name", "text"),
251+
("My Color", "text"),
252+
],
253+
rows=[
254+
["BMW", "Blue"],
255+
["Audi", "Orange"],
256+
["Volkswagen", "White"],
257+
["Volkswagen", "Green"],
258+
],
259+
)
260+
page = data_fixture.create_builder_page(builder=builder)
261+
element = data_fixture.create_builder_button_element(page=page)
262+
integration = data_fixture.create_local_baserow_integration(
263+
application=builder, user=user, authorized_user=user
264+
)
265+
data_source = data_fixture.create_builder_local_baserow_list_rows_data_source(
266+
integration=integration,
267+
page=page,
268+
table=table,
269+
)
270+
service = data_fixture.create_local_baserow_upsert_row_service(
271+
table=table,
272+
integration=integration,
273+
)
274+
service.field_mappings.create(
275+
field=fields[0],
276+
value=f'concat(get("data_source.{data_source.id}.0.{fields[0].db_column}"), '
277+
f'get("data_source.{data_source.id}.0.{fields[1].db_column}"))',
278+
)
279+
workflow_action = data_fixture.create_local_baserow_create_row_workflow_action(
280+
page=page, service=service, element=element, event=EventTypes.CLICK
281+
)
282+
283+
fake_request = MagicMock()
284+
fake_request.data = {"metadata": json.dumps({})}
285+
286+
dispatch_context = BuilderDispatchContext(
287+
fake_request, page, only_expose_public_allowed_properties=False
288+
)
289+
290+
result = BuilderWorkflowActionHandler().dispatch_workflow_action(
291+
workflow_action, dispatch_context
292+
)
293+
294+
assert result.data == {
295+
"id": AnyInt(),
296+
"order": AnyStr(),
297+
"Name": "AudiOrange",
298+
"My Color": None,
299+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"type": "bug",
3+
"message": "Fix formula recursion error when the same data source is used twice in one formula of workflow action",
4+
"domain": "builder",
5+
"issue_number": 4195,
6+
"bullet_points": [],
7+
"created_at": "2025-11-10"
8+
}

0 commit comments

Comments
 (0)