diff --git a/controlplane/admin/api.go b/controlplane/admin/api.go index 436306a7..263ee485 100644 --- a/controlplane/admin/api.go +++ b/controlplane/admin/api.go @@ -399,6 +399,7 @@ func managedWarehouseUpsertColumns() []string { // DB column name. Mismatching this against the actual column makes // the ON CONFLICT … DO UPDATE clause throw 42703. "duck_lake_version", + "duckling_name", "warehouse_database_endpoint", "warehouse_database_port", "metadata_store_kind", diff --git a/controlplane/admin/api_postgres_test.go b/controlplane/admin/api_postgres_test.go index f9ff29a1..e93d359c 100644 --- a/controlplane/admin/api_postgres_test.go +++ b/controlplane/admin/api_postgres_test.go @@ -106,10 +106,11 @@ func TestUpsertManagedWarehousePreservesCreatedAt(t *testing.T) { createdAt := time.Date(2024, time.January, 2, 3, 4, 5, 0, time.UTC) original := &configstore.ManagedWarehouse{ - OrgID: "analytics", - State: configstore.ManagedWarehouseStatePending, - CreatedAt: createdAt, - UpdatedAt: createdAt, + OrgID: "analytics", + DucklingName: "analytics", + State: configstore.ManagedWarehouseStatePending, + CreatedAt: createdAt, + UpdatedAt: createdAt, WarehouseDatabase: configstore.ManagedWarehouseDatabase{ Endpoint: "analytics-wh.cluster.example", }, @@ -124,8 +125,11 @@ func TestUpsertManagedWarehousePreservesCreatedAt(t *testing.T) { replacementCreatedAt := time.Date(2030, time.January, 2, 3, 4, 5, 0, time.UTC) stored, ok, err := apiStore.UpsertManagedWarehouse("analytics", &configstore.ManagedWarehouse{ - CreatedAt: replacementCreatedAt, - UpdatedAt: replacementCreatedAt, + CreatedAt: replacementCreatedAt, + UpdatedAt: replacementCreatedAt, + // Changed vs the seeded row: the ON CONFLICT assignment-column list + // must carry duckling_name or this change is silently dropped. + DucklingName: "analytics-renamed", State: configstore.ManagedWarehouseStateReady, StatusMessage: "ready", WarehouseDatabase: configstore.ManagedWarehouseDatabase{ @@ -152,6 +156,9 @@ func TestUpsertManagedWarehousePreservesCreatedAt(t *testing.T) { if stored.MetadataStore.DatabaseName != "ducklake_metadata" { t.Fatalf("expected updated metadata db name, got %q", stored.MetadataStore.DatabaseName) } + if stored.DucklingName != "analytics-renamed" { + t.Fatalf("expected updated duckling_name analytics-renamed, got %q", stored.DucklingName) + } } func TestMutateManagedWarehouseSerializesConcurrentWriters(t *testing.T) { diff --git a/controlplane/admin/ui/src/pages/OrgDetail.tsx b/controlplane/admin/ui/src/pages/OrgDetail.tsx index e2629771..89252faf 100644 --- a/controlplane/admin/ui/src/pages/OrgDetail.tsx +++ b/controlplane/admin/ui/src/pages/OrgDetail.tsx @@ -331,12 +331,21 @@ function WarehousePanel({ setMsg({ kind: "err", text: "Duckling name is required." }); return; } + // Send only fields that actually changed: the PUT is a merge-patch and + // the audit log records the body's keys as "changed", so carrying + // untouched fields would log phantom changes. + const body: Partial = {}; + if (image !== (data?.image ?? "")) body.image = image; + if (version !== (data?.ducklake_version ?? "")) body.ducklake_version = version; + if (ducklingNameInput !== (data?.duckling_name || ducklingName(orgId))) { + body.duckling_name = ducklingNameInput; + } + if (Object.keys(body).length === 0) { + setMsg({ kind: "ok", text: "No changes." }); + return; + } try { - await update.mutateAsync({ - image, - ducklake_version: version, - duckling_name: ducklingNameInput, - }); + await update.mutateAsync(body); setMsg({ kind: "ok", text: "Saved." }); } catch (e) { setMsg({ kind: "err", text: e instanceof Error ? e.message : "Save failed" });