feat(datastore): refuse deleting a container virtual upstream part 9/15 (S17 Phase 6 plan: 42/42)
Why
ADR-009 requires a 409 under either destructive value for any repository a virtual repository lists as an upstream. Parts 1 to 8 gave the container family a junction table with a registered writer, so that state is now reachable.
Without this arm the delete falls through to the foreign key, and mapRepositoryDeleteError collapses every 23503 to ErrRepositoryNotEmpty, the sentinel a destructive delete tombstones past.
The refusal's catalog scenarios, e2e.docker.upstreams.upstream-delete-refused and e2e.oci.upstreams.upstream-delete-refused, need an association that only the part 11 routes can create through the API, so they land in part 14, test(managementapi): cover the container upstre... (!2859 - merged) • Hayley Swimelar • 19.5.
No user doc changes here. docs/user/troubleshooting.md names only the Maven and npm refusal, which stays true while no route writes a container association, and part 11 makes the user-doc update with those routes, feat(managementapi): container virtual upstream... (!2856 - merged) • Hayley Swimelar • 19.5.
The new read's plan
containerVirtualUpstreamReferenceStmt on Postgres 17.10, after ANALYZE, over one namespace holding 2,000 junction rows. The scan reads one partition, container_virtual_repository_upstreams_p21, through container_virtual_repository_namespace_id_upstream_reposi_idx21, which is that partition's copy of index_cvru_on_ns_id_upstream_repository_id.
Limit (cost=0.28..8.30 rows=1 width=16) (actual time=0.009..0.010 rows=1 loops=1)
Buffers: shared hit=3
-> Index Scan using container_virtual_repository_namespace_id_upstream_reposi_idx21 on container_virtual_repository_upstreams_p21 container_virtual_repository_upstreams (cost=0.28..8.30 rows=1 width=16) (actual time=0.009..0.009 rows=1 loops=1)
Index Cond: ((namespace_id = '01a0d891-ee4a-7273-8a27-2526e400b3f7'::uuid) AND (upstream_repository_id = '01a0d891-f11a-702e-8948-e717687540f4'::uuid))
Buffers: shared hit=3
Planning:
Buffers: shared hit=338
Planning Time: 1.050 ms
Execution Time: 0.023 msThe stack
Fifteen parts, each stacked on the last, each green on its own, reviewed in this order. Each part stays under 700 changed lines, counted as insertions plus deletions against the part it targets.
The delete guard in part 9 must be deployed before part 11's associate route serves traffic. Merge order alone does not close the window: within one rollout, a pod without the guard can answer 202 on destructive=true for an upstream that a new pod has just associated, leaving a tombstone the reaper refuses.
Related to #314 (closed)