From b313a09abc8da9d49adfe3db35152e38b8887421 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Tue, 27 Oct 2026 02:15:12 -0700 Subject: [PATCH] perf: look script lineage up by key when copying drafts The lineage check compared a hex rendering of every script hash, which ruled out the primary key and scanned the target's script versions for each script draft, fork creation included. It now converts the draft's 16-digit hex to the bigint column, guarded so a malformed value never reaches the cast. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...91e2241721e34586dedf7bdacb83b732f2b7.json} | 4 ++-- backend/windmill-common/src/user_drafts.rs | 21 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) rename backend/.sqlx/{query-8b62d25c98a6b3db3a0b7039cacf79799e6d7c7c6c9532545844bec9ec314516.json => query-e9efda2d1ab6b111103cfcd626cd91e2241721e34586dedf7bdacb83b732f2b7.json} (55%) diff --git a/backend/.sqlx/query-8b62d25c98a6b3db3a0b7039cacf79799e6d7c7c6c9532545844bec9ec314516.json b/backend/.sqlx/query-e9efda2d1ab6b111103cfcd626cd91e2241721e34586dedf7bdacb83b732f2b7.json similarity index 55% rename from backend/.sqlx/query-8b62d25c98a6b3db3a0b7039cacf79799e6d7c7c6c9532545844bec9ec314516.json rename to backend/.sqlx/query-e9efda2d1ab6b111103cfcd626cd91e2241721e34586dedf7bdacb83b732f2b7.json index 1218b5b2ec..99a4c12462 100644 --- a/backend/.sqlx/query-8b62d25c98a6b3db3a0b7039cacf79799e6d7c7c6c9532545844bec9ec314516.json +++ b/backend/.sqlx/query-e9efda2d1ab6b111103cfcd626cd91e2241721e34586dedf7bdacb83b732f2b7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO draft (workspace_id, path, typ, value, created_at, email, base)\n SELECT $2::text, path, typ,\n to_json(\n CASE WHEN typ IN ('script', 'flow') THEN clean - 'on_behalf_of' ELSE clean END\n - CASE WHEN typ = 'flow' THEN 'version_id'\n WHEN typ IN ('app', 'raw_app') THEN 'parent_version'\n ELSE '' END\n - CASE WHEN typ = 'script' AND NOT EXISTS (\n SELECT 1 FROM script sc WHERE sc.workspace_id::text = $2::text\n AND lpad(to_hex(sc.hash), 16, '0') = clean->>'parent_hash')\n THEN 'parent_hash' ELSE '' END\n ),\n created_at, email,\n CASE WHEN typ = 'script' AND EXISTS (\n SELECT 1 FROM script sc WHERE sc.workspace_id::text = $2::text\n AND lpad(to_hex(sc.hash), 16, '0') = base)\n THEN base END\n FROM (\n SELECT d.path, d.typ, d.created_at, d.email, d.base,\n replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean\n FROM draft d\n WHERE d.workspace_id = $1\n AND CASE WHEN $4::text[] IS NULL THEN d.email = $3 OR d.email IS NULL\n ELSE d.email = $3\n AND (d.typ::text, d.path) IN (SELECT * FROM unnest($4::text[], $5::text[]))\n END\n ) s\n RETURNING typ AS \"typ: UserDraftItemKind\", path", + "query": "INSERT INTO draft (workspace_id, path, typ, value, created_at, email, base)\n SELECT $2::text, path, typ,\n to_json(\n CASE WHEN typ IN ('script', 'flow') THEN clean - 'on_behalf_of' ELSE clean END\n - CASE WHEN typ = 'flow' THEN 'version_id'\n WHEN typ IN ('app', 'raw_app') THEN 'parent_version'\n ELSE '' END\n - CASE WHEN typ <> 'script' THEN ''\n WHEN (clean->>'parent_hash' ~ '^[0-9a-f]{16}$') IS NOT TRUE THEN 'parent_hash'\n WHEN EXISTS (SELECT 1 FROM script sc WHERE sc.workspace_id = $2::text\n AND sc.hash = ('x' || (clean->>'parent_hash'))::bit(64)::bigint)\n THEN '' ELSE 'parent_hash' END\n ),\n created_at, email,\n CASE WHEN typ = 'script' AND base ~ '^[0-9a-f]{16}$' THEN\n CASE WHEN EXISTS (SELECT 1 FROM script sc WHERE sc.workspace_id = $2::text\n AND sc.hash = ('x' || base)::bit(64)::bigint)\n THEN base END\n END\n FROM (\n SELECT d.path, d.typ, d.created_at, d.email, d.base,\n replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean\n FROM draft d\n WHERE d.workspace_id = $1\n AND CASE WHEN $4::text[] IS NULL THEN d.email = $3 OR d.email IS NULL\n ELSE d.email = $3\n AND (d.typ::text, d.path) IN (SELECT * FROM unnest($4::text[], $5::text[]))\n END\n ) s\n RETURNING typ AS \"typ: UserDraftItemKind\", path", "describe": { "columns": [ { @@ -62,5 +62,5 @@ false ] }, - "hash": "8b62d25c98a6b3db3a0b7039cacf79799e6d7c7c6c9532545844bec9ec314516" + "hash": "e9efda2d1ab6b111103cfcd626cd91e2241721e34586dedf7bdacb83b732f2b7" } diff --git a/backend/windmill-common/src/user_drafts.rs b/backend/windmill-common/src/user_drafts.rs index 61756d1683..b99c0a26d4 100644 --- a/backend/windmill-common/src/user_drafts.rs +++ b/backend/windmill-common/src/user_drafts.rs @@ -695,7 +695,8 @@ pub async fn copy_drafts_to_workspace( // deploy time, which is the more accurate answer of the two. let rows = sqlx::query!( // A script hash is content-addressed and shared between a workspace and its forks, so - // a script draft keeps its base (and `parent_hash`) when the target has that version, + // a script draft keeps its base (and `parent_hash`, both `ScriptHash`'s 16 hex digits, + // matched on the column so the lookup uses the key) when the target has that version, // which a fork's clone does; one deployed in the source after the fork was made is not // there, and a deploy naming it as parent would be refused. Flow and app ids are minted // per workspace, so those drafts arrive with no base (staleness falls back to the @@ -713,16 +714,18 @@ pub async fn copy_drafts_to_workspace( - CASE WHEN typ = 'flow' THEN 'version_id' WHEN typ IN ('app', 'raw_app') THEN 'parent_version' ELSE '' END - - CASE WHEN typ = 'script' AND NOT EXISTS ( - SELECT 1 FROM script sc WHERE sc.workspace_id::text = $2::text - AND lpad(to_hex(sc.hash), 16, '0') = clean->>'parent_hash') - THEN 'parent_hash' ELSE '' END + - CASE WHEN typ <> 'script' THEN '' + WHEN (clean->>'parent_hash' ~ '^[0-9a-f]{16}$') IS NOT TRUE THEN 'parent_hash' + WHEN EXISTS (SELECT 1 FROM script sc WHERE sc.workspace_id = $2::text + AND sc.hash = ('x' || (clean->>'parent_hash'))::bit(64)::bigint) + THEN '' ELSE 'parent_hash' END ), created_at, email, - CASE WHEN typ = 'script' AND EXISTS ( - SELECT 1 FROM script sc WHERE sc.workspace_id::text = $2::text - AND lpad(to_hex(sc.hash), 16, '0') = base) - THEN base END + CASE WHEN typ = 'script' AND base ~ '^[0-9a-f]{16}$' THEN + CASE WHEN EXISTS (SELECT 1 FROM script sc WHERE sc.workspace_id = $2::text + AND sc.hash = ('x' || base)::bit(64)::bigint) + THEN base END + END FROM ( SELECT d.path, d.typ, d.created_at, d.email, d.base, replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean