Skip to content

Commit 443d04a

Browse files
authored
Fix importing a database with an AI field associated with an ai_auto_update_user_id (baserow#4656)
1 parent 608a4a5 commit 443d04a

File tree

3 files changed

+210
-5
lines changed

3 files changed

+210
-5
lines changed
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 importing a database with an AI field associated with an ai_auto_update_user",
4+
"issue_origin": "github",
5+
"issue_number": 4601,
6+
"domain": "database",
7+
"bullet_points": [],
8+
"created_at": "2026-02-05"
9+
}

premium/backend/src/baserow_premium/fields/field_types.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
ERROR_MODEL_DOES_NOT_BELONG_TO_TYPE,
1313
)
1414
from baserow.contrib.database.api.fields.errors import ERROR_FIELD_DOES_NOT_EXIST
15+
from baserow.contrib.database.fields.dependencies.handler import (
16+
FieldDependencyHandler,
17+
)
1518
from baserow.contrib.database.fields.dependencies.models import FieldDependency
1619
from baserow.contrib.database.fields.dependencies.types import FieldDependencies
1720
from baserow.contrib.database.fields.dependencies.update_collector import (
@@ -348,13 +351,17 @@ def get_field_dependencies(
348351
self, field_instance: AIField, field_cache: "FieldCache"
349352
) -> FieldDependencies:
350353
field_ids = extract_field_id_dependencies(field_instance.ai_prompt["formula"])
354+
existing_field_ids = set(
355+
Field.objects.filter(id__in=field_ids).values_list("id", flat=True)
356+
)
351357
return [
352358
FieldDependency(
353359
dependency_id=field_id,
354360
dependant=field_instance,
355361
via=None,
356362
)
357363
for field_id in field_ids
364+
if field_id in existing_field_ids
358365
]
359366

360367
def _handle_dependent_rows_change(
@@ -490,6 +497,26 @@ def before_update(self, from_field, to_field_values, user, field_kwargs):
490497

491498
return super().before_update(from_field, to_field_values, user, field_kwargs)
492499

500+
def import_serialized(
501+
self,
502+
table,
503+
serialized_values,
504+
import_export_config,
505+
id_mapping,
506+
deferred_fk_update_collector,
507+
):
508+
if not import_export_config.is_duplicate:
509+
serialized_values = serialized_values.copy()
510+
serialized_values.pop("ai_auto_update_user_id", None)
511+
serialized_values["ai_auto_update"] = False
512+
return super().import_serialized(
513+
table,
514+
serialized_values,
515+
import_export_config,
516+
id_mapping,
517+
deferred_fk_update_collector,
518+
)
519+
493520
def after_import_serialized(
494521
self,
495522
field: AIField,
@@ -518,6 +545,8 @@ def after_import_serialized(
518545
if save:
519546
field.save()
520547

548+
FieldDependencyHandler.rebuild_dependencies([field], field_cache)
549+
521550
def should_backup_field_data_for_same_type_update(
522551
self, old_field, new_field_attrs
523552
) -> bool:

premium/backend/tests/baserow_premium_tests/fields/test_ai_field_type.py

Lines changed: 172 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from unittest.mock import patch
2+
13
from django.shortcuts import reverse
4+
from django.test.utils import override_settings
25

36
import pytest
47
from pytest_unordered import unordered
@@ -7,10 +10,14 @@
710
from baserow.contrib.database.fields.dependencies.models import FieldDependency
811
from baserow.contrib.database.fields.handler import FieldHandler
912
from baserow.contrib.database.fields.registries import field_type_registry
13+
from baserow.contrib.database.fields.utils.deferred_foreign_key_updater import (
14+
DeferredForeignKeyUpdater,
15+
)
1016
from baserow.contrib.database.rows.handler import RowHandler
1117
from baserow.contrib.database.table.handler import TableHandler
1218
from baserow.core.cache import local_cache
1319
from baserow.core.db import specific_iterator
20+
from baserow.core.registries import ImportExportConfig
1421
from baserow_premium.fields.field_types import AIFieldType
1522
from baserow_premium.fields.models import AIField
1623

@@ -710,11 +717,16 @@ def test_update_ai_field_type_via_api_file_field_doesnt_exist(
710717
assert response_json["error"] == "ERROR_FIELD_DOES_NOT_EXIST"
711718

712719

713-
@pytest.mark.django_db
720+
@pytest.mark.django_db(transaction=True)
714721
@pytest.mark.field_ai
715-
def test_duplicate_table_with_ai_field(premium_data_fixture):
722+
@override_settings(DEBUG=True)
723+
@patch("baserow.core.jobs.handler.JobHandler.create_and_start_job")
724+
def test_duplicate_table_with_ai_field(patched_job_creation, premium_data_fixture):
725+
premium_data_fixture.register_fake_generate_ai_type()
716726
session_id = "session-id"
717-
user = premium_data_fixture.create_user(session_id=session_id)
727+
user = premium_data_fixture.create_user(
728+
session_id=session_id, has_active_premium_license=True
729+
)
718730
database = premium_data_fixture.create_database_application(
719731
user=user, name="Placeholder"
720732
)
@@ -735,6 +747,8 @@ def test_duplicate_table_with_ai_field(premium_data_fixture):
735747
ai_generative_ai_model="test_1",
736748
ai_file_field=file_field,
737749
ai_prompt=f"concat('test:',get('fields.field_{text_field.id}'))",
750+
ai_auto_update=True,
751+
ai_auto_update_user=user,
738752
)
739753

740754
table_handler = TableHandler()
@@ -754,6 +768,20 @@ def test_duplicate_table_with_ai_field(premium_data_fixture):
754768
duplicated_ai_field.ai_prompt["formula"]
755769
== f"concat('test:',get('fields.field_{duplicated_text_field.id}'))"
756770
)
771+
assert duplicated_ai_field.ai_auto_update is True
772+
assert duplicated_ai_field.ai_auto_update_user_id == user.id
773+
774+
# Verify auto-update triggers on the duplicated table's AI field.
775+
patched_job_creation.reset_mock()
776+
RowHandler().create_rows(
777+
user,
778+
duplicated_table,
779+
rows_values=[{duplicated_text_field.db_column: "test"}],
780+
send_webhook_events=False,
781+
send_realtime_update=False,
782+
)
783+
assert patched_job_creation.call_count == 1
784+
assert patched_job_creation.call_args.kwargs["field_id"] == duplicated_ai_field.id
757785

758786

759787
@pytest.mark.django_db
@@ -780,7 +808,7 @@ def test_duplicate_table_with_ai_field_broken_references(premium_data_fixture):
780808
ai_generative_ai_type="test_generative_ai",
781809
ai_generative_ai_model="test_1",
782810
ai_file_field=file_field,
783-
ai_prompt=f"concat('test:',get('fields.field_0'))",
811+
ai_prompt="concat('test:',get('fields.field_0'))",
784812
)
785813

786814
table_handler = TableHandler()
@@ -792,7 +820,7 @@ def test_duplicate_table_with_ai_field_broken_references(premium_data_fixture):
792820

793821
assert (
794822
duplicated_ai_field.ai_prompt["formula"]
795-
== f"concat('test:',get('fields.field_0'))"
823+
== "concat('test:',get('fields.field_0'))"
796824
)
797825

798826

@@ -1352,3 +1380,142 @@ def test_create_ai_field_auto_doesnt_update_user_if_set(premium_data_fixture):
13521380

13531381
assert ai_field.ai_auto_update is True
13541382
assert ai_field.ai_auto_update_user_id == user.id # not changed
1383+
1384+
1385+
@pytest.mark.django_db
1386+
@pytest.mark.field_ai
1387+
def test_import_serialized_ai_field_with_auto_update_user(premium_data_fixture):
1388+
user = premium_data_fixture.create_user()
1389+
table = premium_data_fixture.create_database_table(user=user)
1390+
premium_data_fixture.register_fake_generate_ai_type()
1391+
text_field = premium_data_fixture.create_text_field(
1392+
table=table, order=0, name="text"
1393+
)
1394+
1395+
ai_field = premium_data_fixture.create_ai_field(
1396+
table=table,
1397+
order=1,
1398+
name="ai",
1399+
ai_generative_ai_type="test_generative_ai",
1400+
ai_generative_ai_model="test_1",
1401+
ai_prompt=f"concat('test:',get('fields.field_{text_field.id}'))",
1402+
ai_auto_update=True,
1403+
ai_auto_update_user=user,
1404+
)
1405+
1406+
field_type = field_type_registry.get_by_model(ai_field)
1407+
serialized = field_type.export_serialized(ai_field)
1408+
1409+
serialized["ai_auto_update_user_id"] = 99999
1410+
1411+
imported_field = field_type.import_serialized(
1412+
table,
1413+
serialized,
1414+
ImportExportConfig(include_permission_data=False),
1415+
id_mapping={},
1416+
deferred_fk_update_collector=DeferredForeignKeyUpdater(),
1417+
)
1418+
1419+
imported_field = AIField.objects.get(id=imported_field.id)
1420+
assert imported_field.ai_auto_update is False
1421+
assert imported_field.ai_auto_update_user_id is None
1422+
1423+
1424+
@pytest.mark.django_db(transaction=True)
1425+
@pytest.mark.field_ai
1426+
@override_settings(DEBUG=True)
1427+
@patch("baserow.core.jobs.handler.JobHandler.create_and_start_job")
1428+
def test_duplicate_field_with_ai_auto_update_triggers_both(
1429+
patched_job_creation, premium_data_fixture
1430+
):
1431+
premium_data_fixture.register_fake_generate_ai_type()
1432+
user = premium_data_fixture.create_user(has_active_premium_license=True)
1433+
database = premium_data_fixture.create_database_application(
1434+
user=user, name="database"
1435+
)
1436+
table = premium_data_fixture.create_database_table(name="table", database=database)
1437+
text_field = premium_data_fixture.create_text_field(table=table, name="text")
1438+
ai_field = FieldHandler().create_field(
1439+
table=table,
1440+
user=user,
1441+
name="ai",
1442+
type_name="ai",
1443+
ai_generative_ai_type="test_generative_ai",
1444+
ai_generative_ai_model="test_1",
1445+
ai_prompt=f"get('fields.field_{text_field.id}')",
1446+
ai_auto_update=True,
1447+
)
1448+
1449+
assert ai_field.ai_auto_update is True
1450+
assert ai_field.ai_auto_update_user_id == user.id
1451+
1452+
RowHandler().create_rows(
1453+
user,
1454+
table,
1455+
rows_values=[{text_field.db_column: "test"}],
1456+
send_webhook_events=False,
1457+
send_realtime_update=False,
1458+
)
1459+
assert patched_job_creation.call_count == 1
1460+
assert patched_job_creation.call_args.kwargs["field_id"] == ai_field.id
1461+
1462+
duplicated_field, _ = FieldHandler().duplicate_field(user, ai_field)
1463+
duplicated_field = duplicated_field.specific
1464+
1465+
assert duplicated_field.ai_auto_update is True
1466+
assert duplicated_field.ai_auto_update_user_id == user.id
1467+
1468+
patched_job_creation.reset_mock()
1469+
RowHandler().create_rows(
1470+
user,
1471+
table,
1472+
rows_values=[{text_field.db_column: "test2"}],
1473+
send_webhook_events=False,
1474+
send_realtime_update=False,
1475+
)
1476+
assert patched_job_creation.call_count == 2
1477+
triggered_field_ids = {
1478+
call.kwargs["field_id"] for call in patched_job_creation.call_args_list
1479+
}
1480+
assert triggered_field_ids == {ai_field.id, duplicated_field.id}
1481+
1482+
1483+
@pytest.mark.django_db
1484+
@pytest.mark.field_ai
1485+
def test_import_ai_field_disables_auto_update(premium_data_fixture):
1486+
premium_data_fixture.register_fake_generate_ai_type()
1487+
user = premium_data_fixture.create_user()
1488+
database = premium_data_fixture.create_database_application(
1489+
user=user, name="database"
1490+
)
1491+
table = premium_data_fixture.create_database_table(name="table", database=database)
1492+
text_field = premium_data_fixture.create_text_field(table=table, name="text")
1493+
ai_field = FieldHandler().create_field(
1494+
table=table,
1495+
user=user,
1496+
name="ai",
1497+
type_name="ai",
1498+
ai_generative_ai_type="test_generative_ai",
1499+
ai_generative_ai_model="test_1",
1500+
ai_prompt=f"get('fields.field_{text_field.id}')",
1501+
ai_auto_update=True,
1502+
)
1503+
1504+
assert ai_field.ai_auto_update_user_id == user.id
1505+
1506+
field_type = field_type_registry.get_by_model(ai_field)
1507+
serialized = field_type.export_serialized(ai_field)
1508+
1509+
serialized["ai_auto_update_user_id"] = 99999
1510+
1511+
imported_field = field_type.import_serialized(
1512+
table,
1513+
serialized,
1514+
ImportExportConfig(include_permission_data=False),
1515+
id_mapping={},
1516+
deferred_fk_update_collector=DeferredForeignKeyUpdater(),
1517+
)
1518+
1519+
imported_field = AIField.objects.get(id=imported_field.id)
1520+
assert imported_field.ai_auto_update is False
1521+
assert imported_field.ai_auto_update_user_id is None

0 commit comments

Comments
 (0)