Skip to content

Commit bcd8d94

Browse files
authored
fix: ensure primary field always exists on sync table (baserow#5089)
* Ensure primary field always exists on sync table * Review changes * Remove elidable from migration
1 parent e506790 commit bcd8d94

4 files changed

Lines changed: 123 additions & 1 deletion

File tree

backend/src/baserow/contrib/database/data_sync/handler.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,8 +622,24 @@ def set_data_sync_synced_properties(
622622

623623
handler = FieldHandler()
624624

625+
unique_primary_field = next(
626+
(ep.field for ep in enabled_properties if ep.unique_primary), None
627+
)
628+
625629
for data_sync_property_instance in properties_to_be_removed:
626630
field = data_sync_property_instance.field
631+
# If we're about to delete the primary field, first move primary to the
632+
# unique_primary field so the table is never left without one.
633+
if (
634+
field.primary
635+
and unique_primary_field
636+
and unique_primary_field.id != field.id
637+
):
638+
handler.change_primary_field(
639+
user=user,
640+
table=data_sync.table,
641+
new_primary_field=unique_primary_field,
642+
)
627643
data_sync_property_instance.delete()
628644
handler.delete_field(
629645
user=user,
@@ -655,7 +671,6 @@ def set_data_sync_synced_properties(
655671
data_sync.table,
656672
[field_kwargs.pop("name")],
657673
)
658-
print(new_name)
659674
field = handler.create_field(
660675
user=user,
661676
table=data_sync.table,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from django.db import migrations
2+
3+
4+
def fix_data_sync_missing_primary_field(apps, schema_editor):
5+
"""
6+
Finds data sync tables that have no primary field and restores primary=True
7+
on the field associated with their unique_primary synced property.
8+
9+
This fixes a bug where the primary field could be lost when a user changed
10+
the primary to a non-unique_primary field, and that field was later removed
11+
during a data sync.
12+
"""
13+
14+
DataSyncSyncedProperty = apps.get_model(
15+
"database", "DataSyncSyncedProperty"
16+
)
17+
Field = apps.get_model("database", "Field")
18+
19+
tables_with_primary = Field.objects.filter(primary=True).values("table_id")
20+
should_be_primary_field_ids = (
21+
DataSyncSyncedProperty.objects.filter(unique_primary=True)
22+
.exclude(data_sync__table_id__in=tables_with_primary)
23+
.values("field_id")
24+
)
25+
Field.objects.filter(id__in=should_be_primary_field_ids).update(primary=True)
26+
27+
28+
class Migration(migrations.Migration):
29+
30+
dependencies = [
31+
("database", "0207_add_view_default_values"),
32+
]
33+
34+
operations = [
35+
migrations.RunPython(
36+
fix_data_sync_missing_primary_field,
37+
migrations.RunPython.noop,
38+
),
39+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Ensure primary field always exists on sync table",
4+
"issue_origin": "github",
5+
"issue_number": 5088,
6+
"domain": "database",
7+
"bullet_points": [],
8+
"created_at": "2026-03-31"
9+
}

enterprise/backend/tests/baserow_enterprise_tests/data_sync/test_local_baserow_table_data_sync_type.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,3 +1683,62 @@ def test_table_model_is_data_synced_table(enterprise_data_fixture):
16831683

16841684
assert data_sync.table.is_data_synced_table
16851685
assert not table_with_no_synced_table.is_data_synced_table
1686+
1687+
1688+
@pytest.mark.django_db
1689+
@pytest.mark.data_sync
1690+
@override_settings(DEBUG=True)
1691+
def test_sync_ensure_primary_field_always_exists(enterprise_data_fixture):
1692+
enterprise_data_fixture.enable_enterprise()
1693+
user = enterprise_data_fixture.create_user()
1694+
1695+
source_table = enterprise_data_fixture.create_database_table(
1696+
user=user, name="Source"
1697+
)
1698+
source_text_field = enterprise_data_fixture.create_text_field(
1699+
table=source_table, name="Name", primary=True
1700+
)
1701+
source_model = source_table.get_model()
1702+
source_model.objects.create(**{f"field_{source_text_field.id}": "Row 1"})
1703+
1704+
database = enterprise_data_fixture.create_database_application(user=user)
1705+
handler = DataSyncHandler()
1706+
data_sync = handler.create_data_sync_table(
1707+
user=user,
1708+
database=database,
1709+
table_name="Synced",
1710+
type_name="local_baserow_table",
1711+
synced_properties=["id", f"field_{source_text_field.id}"],
1712+
source_table_id=source_table.id,
1713+
)
1714+
handler.sync_data_sync_table(user=user, data_sync=data_sync)
1715+
1716+
synced_fields = specific_iterator(data_sync.table.field_set.all().order_by("id"))
1717+
row_id_field = synced_fields[0]
1718+
name_field = synced_fields[1]
1719+
1720+
assert row_id_field.primary is True
1721+
assert name_field.primary is False
1722+
1723+
FieldHandler().change_primary_field(
1724+
user=user, table=data_sync.table, new_primary_field=name_field
1725+
)
1726+
row_id_field.refresh_from_db()
1727+
name_field.refresh_from_db()
1728+
assert row_id_field.primary is False
1729+
assert name_field.primary is True
1730+
1731+
FieldHandler().update_field(
1732+
user=user,
1733+
field=source_text_field,
1734+
new_type_name="formula",
1735+
formula="'constant'",
1736+
)
1737+
1738+
handler.sync_data_sync_table(user=user, data_sync=data_sync)
1739+
1740+
assert not data_sync.table.field_set.filter(id=name_field.id).exists()
1741+
1742+
row_id_field.refresh_from_db()
1743+
assert row_id_field.primary is True
1744+
assert data_sync.table.field_set.filter(primary=True).count() == 1

0 commit comments

Comments
 (0)