fix(datatables): let a retried clone reclaim its own leftover database

A clone creates its target database one request before it copies into it, and
the fork that would name it is written a request after that. Any failure in
between — a pg_dump error, a bad restore, a dropped connection, the source's
roles changing mid-flow — left a registered `wm_fork_*` that no entry names,
and every retry then failed on its name. This predates data table roles.

`create_pg_database` now reclaims such a leftover before creating: only a
`wm_fork_*` database Windmill registered as a data table database and that no
data table or ducklake entry names, in any workspace, archived ones included.
The drop never terminates connections, so a clone still copying into it makes
the reclaim fail instead of being cut off. It is limited to callers who
administer the source — reaching it is not enough, since on a data table
without roles every member reaches it — and anyone else gets the refusal an
existing database always got.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-09-17 10:01:15 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 432ccc0fbd
commit 29fccb0978
4 changed files with 190 additions and 4 deletions
@@ -3312,10 +3312,12 @@ async fn create_pg_database(
// The copy this database is for is refused a call later, and nothing collects an instance
// database that no data table entry names. Refuse here too, so the clone stops before one
// exists rather than leaving an empty registered `wm_fork_…` behind.
if let Some(reference) = req.source.strip_prefix("datatable://") {
let name = datatable_ref_name(reference);
ensure_datatable_is_clonable(&db, &w_id, name).await?;
}
let governing = match req.source.strip_prefix("datatable://") {
Some(reference) => {
Some(ensure_datatable_is_clonable(&db, &w_id, datatable_ref_name(reference)).await?)
}
None => None,
};
// Non-superadmin: restrict dbname to wm_fork_ prefix
if !windmill_api_auth::is_super_admin_authed(&db, &authed).await? {
@@ -3328,6 +3330,21 @@ async fn create_pg_database(
}
if is_instance_datatable_source(&db, &w_id, &req.source).await? {
// A retry after a clone that failed past this point finds its own leftover here. Reclaiming
// it is a `DROP DATABASE`, so it is for whoever administers the source — not merely whoever
// reaches it, which on a data table without roles is every member. Anyone else gets the
// refusal an existing database always got.
let may_reclaim = match &governing {
Some(governing) => crate::datatable_permissions::ensure_governs_datatable(
&db, &authed, &w_id, governing,
)
.await
.is_ok(),
None => false,
};
if may_reclaim {
windmill_common::reclaim_orphaned_fork_database(&db, &req.target_dbname).await?;
}
windmill_common::create_custom_instance_database(&db, &req.target_dbname, "datatable")
.await?;
} else {