From 5427df8ff33acb1b4d2e47a7951a5ac376098cc7 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Tue, 27 Oct 2026 15:56:28 +0800 Subject: [PATCH] Record the Studio schema migration atomically and support safe retries --- apps/waterfall-wiki/db/009_studio_designs.sql | 7 +++++-- apps/waterfall-wiki/tests/integration.test.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/waterfall-wiki/db/009_studio_designs.sql b/apps/waterfall-wiki/db/009_studio_designs.sql index 4372f64..b5b36f8 100644 --- a/apps/waterfall-wiki/db/009_studio_designs.sql +++ b/apps/waterfall-wiki/db/009_studio_designs.sql @@ -1,4 +1,5 @@ -CREATE TABLE studio_designs ( +BEGIN; +CREATE TABLE IF NOT EXISTS studio_designs ( id uuid PRIMARY KEY, layout_id text NOT NULL CHECK (layout_id ~ '^ref-(0[1-9]|1[0-9]|2[0-7])$'), name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 120), @@ -7,4 +8,6 @@ CREATE TABLE studio_designs ( created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); -CREATE INDEX studio_designs_layout_created ON studio_designs(layout_id,created_at DESC,id DESC); +CREATE INDEX IF NOT EXISTS studio_designs_layout_created ON studio_designs(layout_id,created_at DESC,id DESC); +INSERT INTO schema_migrations(version) VALUES('009_studio_designs'); +COMMIT; diff --git a/apps/waterfall-wiki/tests/integration.test.ts b/apps/waterfall-wiki/tests/integration.test.ts index fc6d581..5afa168 100644 --- a/apps/waterfall-wiki/tests/integration.test.ts +++ b/apps/waterfall-wiki/tests/integration.test.ts @@ -57,6 +57,7 @@ test('real PostgreSQL + S3 notes, flat tags, resource sharing and video processi assert.equal((await db().query("SELECT count(*)::integer AS n FROM assets WHERE kind='audio' AND derivation='audio_track'")).rows[0].n,1,'Video soundtracks are ordinary reusable assets'); const audio=join(dir,'audio.wav');await run('ffmpeg',['-y','-v','error','-f','lavfi','-i','sine=frequency=220','-t','0.2',audio]);await upload('audio',[{data:await readFile(audio),name:'audio.wav',mime:'audio/wav'}]);assert.equal((await request('/api/feed?section=audio')).posts.length,1); for(const removed of ['/library/assets','/library/tags'])assert.equal((await fetch(base+removed)).status,404); + assert.equal((await db().query("SELECT 1 FROM schema_migrations WHERE version='009_studio_designs'")).rowCount,1,'Studio migration must be recorded for repeat deployments'); // Studio owns no second database: designs use this same isolated PostgreSQL. for(let i=1;i<=27;i++){const layoutId='ref-'+String(i).padStart(2,'0'),saved=await request('/api/studio/designs',{layoutId,name:'布局 '+i,parameters:{}});assert.equal(saved.design.layoutId,layoutId);assert.equal(saved.design.revision,1);const read=await request('/api/studio/designs/'+saved.design.id);assert.deepEqual(read.design.parameters,{texts:{},images:{},decorations:{}});} const design=await request('/api/studio/designs',{layoutId:'ref-01',name:'可修改效果',parameters:{texts:{T01:{value:'新的标题'}},images:{I01:{assetId:first.assets[0],fit:'contain'}}}});