Skip to content

postgres: Support purge_on_delete on postgres_projects#5414

Open
pietern wants to merge 1 commit into
mainfrom
postgres-purge
Open

postgres: Support purge_on_delete on postgres_projects#5414
pietern wants to merge 1 commit into
mainfrom
postgres-purge

Conversation

@pietern
Copy link
Copy Markdown
Contributor

@pietern pietern commented Jun 2, 2026

Summary

Adds support for purge_on_delete on Lakebase postgres_projects so bundles can hard-delete a project on destroy. The flag is passed to the DeleteProject API call as ?purge=true; when unset, the backend performs a soft delete that can be undone via databricks postgres undelete-project within the project's retention window.

The field is input-only — it is not modeled by the backend resource for projects, and the GET API never returns it. We store it in state purely so DoDelete can apply it on destroy: by that point the configuration for the resource is gone, so state is the only place it can live.

PrepareState preserves input.ForceSendFields so the structdiff comparison correctly distinguishes "explicit false" from the fictional remote zero — otherwise toggling true → false would be classified as no change, state would stay true, and the next destroy would still emit ?purge=true. DoUpdate strips purge_on_delete from the API field mask so a state-only flip doesn't fire an unnecessary remote write.

Acceptance tests under acceptance/bundle/resources/postgres_projects/:

  • purge_on_delete/: deploys a hard_delete and a soft_delete project side by side and asserts the destroy emits ?purge=true and a plain DELETE respectively, on both engines.
  • purge_on_delete_transitions/: direct-engine only. Walks purge_on_delete through unset → true → false → unset and records the persisted value at each step; final destroy is a plain DELETE. Regression coverage for the FSF-preservation fix.

Test plan

  • Manually verified against dogfood with ephemeral projects on both engines: deploy → flip → destroy; observed GET on project/branches/endpoints in soft vs hard cases.
  • databricks postgres undelete-project restoration semantics confirmed end-to-end (project + implicit production branch + primary endpoint return; user-created sub-resources are cascade-deleted and not restored — captured in DECO-27233).

This pull request and its description were written by Isaac.

@pietern pietern temporarily deployed to test-trigger-is June 2, 2026 14:45 — with GitHub Actions Inactive
@pietern pietern temporarily deployed to test-trigger-is June 2, 2026 14:45 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Approval status: pending

/acceptance/bundle/ - needs approval

15 files changed
Suggested: @denik
Also eligible: @andrewnester, @shreyas-goenka, @janniklasrose, @anton-107, @lennartkats-db

/bundle/ - needs approval

9 files changed
Suggested: @denik
Also eligible: @andrewnester, @shreyas-goenka, @janniklasrose, @anton-107, @lennartkats-db

General files (require maintainer)

Files: NEXT_CHANGELOG.md, libs/testserver/postgres.go
Based on git history:

  • @simonfaltum -- recent work in ./, bundle/direct/dresources/, bundle/schema/

Any maintainer (@andrewnester, @anton-107, @denik, @shreyas-goenka, @simonfaltum, @renaudhartert-db) can approve all areas.
See OWNERS for ownership rules.

@pietern pietern requested a review from janniklasrose June 2, 2026 15:00
@eng-dev-ecosystem-bot
Copy link
Copy Markdown
Collaborator

eng-dev-ecosystem-bot commented Jun 2, 2026

Commit: 36bd5cc

Run: 26830530549

Adds support for `purge_on_delete` on Lakebase `postgres_projects` so
bundles can hard-delete a project on destroy. The flag is passed to the
DeleteProject API call as `?purge=true`; when unset, the backend
performs a soft delete that can be undone via `databricks postgres
undelete-project` within the project's retention window.

The field is input-only — it is not modeled by the backend resource for
projects, and the GET API never returns it. We store it in state purely
so DoDelete can apply it on destroy: by that point the configuration
for the resource is gone, so state is the only place it can live.

PrepareState preserves `input.ForceSendFields` so the structdiff
comparison correctly distinguishes "explicit false" from the fictional
remote zero — otherwise toggling `true -> false` would be classified as
no change, state would stay `true`, and the next destroy would still
emit `?purge=true`. DoUpdate strips `purge_on_delete` from the API
field mask so a state-only flip doesn't fire an unnecessary remote
write.

Acceptance tests under `acceptance/bundle/resources/postgres_projects/`:

- `purge_on_delete/`: deploys a `hard_delete` and a `soft_delete`
  project side by side and asserts the destroy emits `?purge=true` and
  a plain DELETE respectively, on both engines.

- `purge_on_delete_transitions/`: direct-engine only. Walks
  `purge_on_delete` through unset -> true -> false -> unset and
  records the persisted value at each step; final destroy is a plain
  DELETE. Regression coverage for the FSF-preservation fix.

Manually verified against dogfood with ephemeral projects on both
engines (deploy -> flip -> destroy; GET on project/branches/endpoints
in soft vs hard cases; native `postgres undelete-project` restoration
semantics — captured in DECO-27233).

Co-authored-by: Isaac
@pietern pietern temporarily deployed to test-trigger-is June 2, 2026 15:35 — with GitHub Actions Inactive
@pietern pietern temporarily deployed to test-trigger-is June 2, 2026 15:35 — with GitHub Actions Inactive
This action will result in the deletion of the following Lakebase projects along with
all their branches, databases, and endpoints. All data stored in them will be permanently lost:
delete resources.postgres_projects.hard_delete
delete resources.postgres_projects.soft_delete
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated, but why do we have duplicate message here?

cleanup() {
# Belt-and-braces in case bundle destroy was skipped or partially failed.
# The soft-delete case leaves a record in the trash; --purge clears it.
$CLI postgres delete-project --purge "projects/test-pg-proj-hard-${UNIQUE_NAME}" 2>/dev/null || true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is 2>/dev/null for? is there noise? you can also sent to LOG.delete-project, it'll show up in go test -v output.

title "bundle destroy"
$CLI bundle destroy --auto-approve > out.destroy.txt 2>&1 || true

trace print_requests.py --keep --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.destroy.json
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--keep is unnecessary here?

trace $CLI postgres get-project "projects/test-pg-proj-hard-${UNIQUE_NAME}" | jq 'del(.create_time, .update_time)'
trace $CLI postgres get-project "projects/test-pg-proj-soft-${UNIQUE_NAME}" | jq 'del(.create_time, .update_time)'

trace print_requests.py --keep --sort --get '//postgres' '^//workspace-files/' '^//workspace/' '^//telemetry-ext' '^//operations/' > out.requests.deploy.json
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why --keep? it'll be double printed by next print_requests.py

# resources.json. The field is omitted from JSON when unset (omitempty +
# ForceSendFields tracking), so absent => null, explicit true/false => bool.
get_purge() {
jq -r '.state["resources.postgres_projects.proj"].state.purge_on_delete' .databricks/bundle/default/resources.json
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: gron.py purge_on_delete < .databricks/bundle/default/resources.json could be more self-descriptive.

@@ -0,0 +1,4 @@
# Direct engine only: this test exercises the FSF preservation in
# PrepareState and direct's plan/diff classification when the user flips
# purge_on_delete. Terraform has its own provider-managed state lifecycle.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but destroy requests should match between TF and direct, right? so could still make sense to run part of of these tests on TF to record destroy requests and compare.

// not a spec field. Strip it from the mask so toggling it between deploys
// becomes a state-only refresh (the framework saves newState when this
// returns nil error).
fieldPaths = slices.DeleteFunc(fieldPaths, func(p string) bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we instead do a static list of the paths above? We should not use entry.Changes to populate update-mask.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants