fix: the move record alone excuses a carried draft at the destination, whoever owns it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 23:16:49 +02:00
co-authored by Claude Opus 5
parent df2973ffff
commit baafd1e002
5 changed files with 13 additions and 18 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT d.email IS NULL as \"legacy!\", COALESCE(u.username, p.username) as username\n FROM draft d\n LEFT JOIN usr u ON u.workspace_id = d.workspace_id AND u.email = d.email\n LEFT JOIN password p ON p.email = d.email AND p.super_admin = true\n WHERE d.workspace_id = $1 AND d.path = $2 AND d.typ::text = ANY($3::text[])\n -- A row of the consuming owner's that a move brought here from the very path\n -- being renamed is the draft this deploy carries, not an item in its way. Any\n -- other row of theirs is a second item and still collides.\n AND ($4::text IS NULL OR d.email IS DISTINCT FROM $4 OR NOT EXISTS (\n SELECT 1 FROM draft_move m\n WHERE m.workspace_id = $1 AND m.typ::text = ANY($3::text[])\n AND m.old_path = $5 AND m.new_path = $2\n AND (m.email IS NULL OR m.email = $4)\n ))\n ORDER BY 2",
"query": "SELECT d.email IS NULL as \"legacy!\", COALESCE(u.username, p.username) as username\n FROM draft d\n LEFT JOIN usr u ON u.workspace_id = d.workspace_id AND u.email = d.email\n LEFT JOIN password p ON p.email = d.email AND p.super_admin = true\n WHERE d.workspace_id = $1 AND d.path = $2 AND d.typ::text = ANY($3::text[])\n -- A row a move already brought here from the very path being renamed is this\n -- rename's own draft, not an item in its way: the record says which owners\n -- it moved (all of them for an item move, one for a draft-only move). Any\n -- other row is a second item and still collides.\n AND NOT EXISTS (\n SELECT 1 FROM draft_move m\n WHERE m.workspace_id = $1 AND m.typ::text = ANY($3::text[])\n AND m.old_path = $4 AND m.new_path = $2\n AND (m.email IS NULL OR m.email IS NOT DISTINCT FROM d.email)\n )\n ORDER BY 2",
"describe": {
"columns": [
{
@@ -19,7 +19,6 @@
"Text",
"Text",
"TextArray",
"Text",
"Text"
]
},
@@ -28,5 +27,5 @@
null
]
},
"hash": "e34f4d6a151cc791960c61197a4b00ea9db2f6ee47dc154faefe4c06c41cf1cf"
"hash": "fc2081d9077c743bc3978ae1905db558549b2356fc6ac8f0557aa40e1345ff46"
}
-1
View File
@@ -1426,7 +1426,6 @@ async fn update_flow(
&[UserDraftItemKind::Flow],
flow_path,
&nf.path,
(!nf.skip_draft_deletion.unwrap_or(false)).then_some(authed.email.as_str()),
)
.await?;
}
@@ -2322,7 +2322,6 @@ async fn create_script_internal<'c>(
&[UserDraftItemKind::Script],
p_path,
&ns.path,
(!skip_draft_deletion).then_some(authed.email.as_str()),
)
.await?;
}
-1
View File
@@ -3770,7 +3770,6 @@ async fn update_app_internal<'a>(
&[UserDraftItemKind::App, UserDraftItemKind::RawApp],
path,
&npath,
(!ns.skip_draft_deletion.unwrap_or(false)).then_some(authed.email.as_str()),
)
.await?;
}
+11 -12
View File
@@ -662,16 +662,15 @@ pub async fn delete_own_draft_for_path(
///
/// A draft already at `new_path` occupies it the way a deployed item does, so the move is
/// refused with `BadRequest` inside the deploy's transaction, refusing the rename itself:
/// moving onto it would merge two items or strand the row that lost. `consumed_by` names
/// the owner whose row there this deploy consumes, which is the draft being deployed after
/// an earlier move carried it, and so cannot be in its own way.
/// moving onto it would merge two items or strand the row that lost. A row a `draft_move`
/// record already brought there from `old_path` is this rename's own draft and does not
/// count: deploying it would otherwise be refused against itself, with no way out.
pub async fn move_drafts_for_path(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
w_id: &str,
kinds: &[UserDraftItemKind],
old_path: &str,
new_path: &str,
consumed_by: Option<&str>,
) -> Result<()> {
let typs = kinds.iter().map(|k| k.as_str()).collect::<Vec<_>>();
// Named by workspace username, as the editors name other users' drafts: the
@@ -682,20 +681,20 @@ pub async fn move_drafts_for_path(
LEFT JOIN usr u ON u.workspace_id = d.workspace_id AND u.email = d.email
LEFT JOIN password p ON p.email = d.email AND p.super_admin = true
WHERE d.workspace_id = $1 AND d.path = $2 AND d.typ::text = ANY($3::text[])
-- A row of the consuming owner's that a move brought here from the very path
-- being renamed is the draft this deploy carries, not an item in its way. Any
-- other row of theirs is a second item and still collides.
AND ($4::text IS NULL OR d.email IS DISTINCT FROM $4 OR NOT EXISTS (
-- A row a move already brought here from the very path being renamed is this
-- rename's own draft, not an item in its way: the record says which owners
-- it moved (all of them for an item move, one for a draft-only move). Any
-- other row is a second item and still collides.
AND NOT EXISTS (
SELECT 1 FROM draft_move m
WHERE m.workspace_id = $1 AND m.typ::text = ANY($3::text[])
AND m.old_path = $5 AND m.new_path = $2
AND (m.email IS NULL OR m.email = $4)
))
AND m.old_path = $4 AND m.new_path = $2
AND (m.email IS NULL OR m.email IS NOT DISTINCT FROM d.email)
)
ORDER BY 2"#,
w_id,
new_path,
&typs as &[&str],
consumed_by,
old_path,
)
.fetch_all(&mut **tx)