diff --git a/posthog/admin/admins/ducklake_backfill_admin.py b/posthog/admin/admins/ducklake_backfill_admin.py index 9556561d0368..2bb23daf9073 100644 --- a/posthog/admin/admins/ducklake_backfill_admin.py +++ b/posthog/admin/admins/ducklake_backfill_admin.py @@ -18,7 +18,10 @@ class DuckLakeBackfillAdmin(admin.ModelAdmin): ) list_filter = ("enabled",) search_fields = ("=team__id", "table_suffix") - readonly_fields = ("id", "created_at", "updated_at") + # `table_suffix` is write-once: it names a team's warehouse tables/schema, so changing it after + # data is written moves the target and orphans the old tables. It's set only through the + # validated enable flow (`enable_team_backfill`), never edited here. + readonly_fields = ("id", "table_suffix", "created_at", "updated_at") raw_id_fields = ("team", "created_by") actions = ("make_enabled", "make_disabled") diff --git a/posthog/admin/test_ducklake_backfill_admin.py b/posthog/admin/test_ducklake_backfill_admin.py new file mode 100644 index 000000000000..1de52cd3e014 --- /dev/null +++ b/posthog/admin/test_ducklake_backfill_admin.py @@ -0,0 +1,21 @@ +from posthog.test.base import BaseTest + +from django.contrib.admin.sites import AdminSite +from django.test import RequestFactory + +from posthog.admin.admins.ducklake_backfill_admin import DuckLakeBackfillAdmin +from posthog.models import DuckLakeBackfill + + +class TestDuckLakeBackfillAdmin(BaseTest): + def setUp(self) -> None: + super().setUp() + self.admin = DuckLakeBackfillAdmin(DuckLakeBackfill, AdminSite()) + self.request = RequestFactory().get("/") + self.request.user = self.user + + def test_table_suffix_is_readonly(self) -> None: + # `table_suffix` is write-once — editing it in admin would move a team's warehouse + # schema/tables and orphan the old ones. It must never be editable here. + backfill = DuckLakeBackfill.objects.create(team=self.team, enabled=True, table_suffix="acme") + assert "table_suffix" in self.admin.get_readonly_fields(self.request, backfill)