Skip to content

Fix order-dependent slug collisions in deployment schedule replaces handling#20669

Open
chrisguidry wants to merge 4 commits intomainfrom
fix-replaces-slug-collisions
Open

Fix order-dependent slug collisions in deployment schedule replaces handling#20669
chrisguidry wants to merge 4 commits intomainfrom
fix-replaces-slug-collisions

Conversation

@chrisguidry
Copy link
Collaborator

@chrisguidry chrisguidry commented Feb 13, 2026

When updating deployment schedules with replaces, the server was patching
schedules one at a time in list order. Chain renames like a -> b and b -> c
would hit the unique index on (deployment_id, slug) because b still existed
when a -> b was applied. Slug swaps (x -> y, y -> x) had the same problem.

The previous workaround rejected chained replacements with a 422, but that's
unnecessarily restrictive. The fix uses a two-phase approach: first NULL out the
slugs being replaced (NULLs are distinct in the unique index), then apply all
updates by row ID. This handles chains, swaps, and simple renames uniformly.

Aligns the OSS server with the behavior we've shipped on Prefect Cloud
and supports these more complex rename patterns that
the replaces feature was meant to handle.

Session context

The replaces feature was initially added with a workaround that rejected
chained replacements entirely. We hit this in Cloud and fixed it there first
with a two-phase NULL-then-update approach that sidesteps the unique index
constraint. This ports the same fix to OSS, replacing the rejection with proper
handling. Both chain renames and slug swaps now work as expected.

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

chrisguidry and others added 2 commits February 13, 2026 12:14
… handling

When updating deployment schedules with `replaces`, the server was patching
schedules one at a time in list order. Chain renames like `a -> b` and `b -> c`
would hit the unique index on `(deployment_id, slug)` because `b` still existed
when `a -> b` was applied. Slug swaps (`x -> y`, `y -> x`) had the same problem.

The previous workaround rejected chained replacements with a 422, but that was
unnecessarily restrictive. The fix uses a two-phase approach: first NULL out the
slugs being replaced (NULLs are distinct in the unique index), then apply all
updates by row ID. This handles chains, swaps, and simple renames uniformly.

We addressed the same gap in Prefect Cloud (PrefectHQ/nebula#11081).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 13, 2026

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing fix-replaces-slug-collisions (052a0d1) with main (6b81d47)

Open in CodSpeed

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5188593a4e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +259 to +264
slugs_being_replaced = set(replaces_targets.keys())
for schedule in deployment.schedules:
if (
schedule.slug
and schedule.slug in current_slugs
and schedule.slug not in slugs_being_replaced

Choose a reason for hiding this comment

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

P2 Badge Validate replacement maps before applying chained renames

The new collision guard allows any rename whose destination slug is also in slugs_being_replaced, but this admits non-bijective chains like a -> b and b -> b that cannot satisfy the unique (deployment_id, slug) constraint. In update_deployment, both updates pass pre-validation, phase 1 nulls both rows, and phase 2 raises an IntegrityError on the second update (surfacing as a generic 409) instead of returning a deterministic 422 validation error. This regression was introduced when chained replacements stopped being rejected wholesale, so the replacement graph now needs an explicit uniqueness check on final destination slugs.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Considering now we're on an edge case to the edge case, I think we're okay here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also to me the 409 is the correct response code if that happens

Copy link
Member

Choose a reason for hiding this comment

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

Reverting the changes to this file should fix the CI failures on this PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

😡 thank you, definitely not intended

chrisguidry and others added 2 commits February 13, 2026 16:15
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This PR should only touch Python files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants