From 9d19d96832d8f5c920c1223b1cedcadb023d255a Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 23 Jun 2026 14:12:13 -0700 Subject: [PATCH] fix(data-warehouse): make duckgres table_suffix read-only in admin table_suffix names a team's warehouse tables and data-import schema, so changing it after data is written moves the target and silently orphans the old tables. The product enable flow is already write-once, but the Django admin left the field editable. Make it read-only so the suffix is only ever set through the validated enable flow. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../admin/admins/ducklake_backfill_admin.py | 5 ++++- posthog/admin/test_ducklake_backfill_admin.py | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 posthog/admin/test_ducklake_backfill_admin.py 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)