| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 |
- // Selective release. SQL adds derived structures only; authoritative definitions are pinned.
- import fs from 'node:fs';import {createHash} from 'node:crypto';import {parse,withGateway} from './server-util.mjs';import {localCandidates,patchFunctions,MOBILE_HELPERS,MONTHLY_HELPERS,OPERATIONS_HELPERS} from '../../scripts/speed-round3-patch.mjs';
- const root='/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3',action=process.argv[2];
- const files=['payroll-source-index.sql','payroll-read-snapshot.sql','payroll-cutoff-bulk.sql','teacher-earned-months.sql'];
- const sql=files.map(name=>fs.readFileSync('scripts/sql/'+name,'utf8').replace(/^BEGIN;\s*$/gm,'').replace(/^COMMIT;\s*$/gm,'')).join('\n');
- const originals=JSON.parse(fs.readFileSync(root+'/original-definitions.json')).map(({proname,definition})=>({proname,definition})).sort((a,b)=>(a.proname+a.definition).localeCompare(b.proname+b.definition));
- const names=[...new Set(originals.map(x=>x.proname))],newNames=[...new Set([...sql.matchAll(/CREATE OR REPLACE FUNCTION\s+(?:public\.)?(\w+)/g)].map(x=>x[1])), 'xs_payroll_read_snapshot_authority_ready'];
- const sha=x=>createHash('sha256').update(x).digest('hex');
- const query=`SELECT proname,pg_get_functiondef(p.oid) AS definition FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[])`;
- const rollback=`DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON "CommonModel";DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON "CommonModel";
- DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON "CourseAppointment";DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON "CourseAppointment";
- DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON "LessonRecord";DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON "LessonRecord";
- DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON "LessonRecordFact";DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON "LessonRecordFact";
- DROP TABLE IF EXISTS "PayrollSourceIndex","PayrollSourceIndexMeta","TeacherEarnedMonth";
- DROP INDEX IF EXISTS payroll_source_index_common59_digest;`;
- async function run(p){
- const canonical=rows=>rows.sort((a,b)=>(a.proname+a.definition).localeCompare(b.proname+b.definition));
- return Psql.tx(async tx=>{
- await tx.none("SET LOCAL lock_timeout='3s';SET LOCAL statement_timeout='120s';");
- if(JSON.stringify(canonical(await tx.query(config.query,[config.names])))!==JSON.stringify(config.originals))throw Error('Original SQL definitions drifted');
- if(!(await tx.one('SELECT xs_payroll_digest_ready() AND xs_payroll_authority_ready() AS ready')).ready)throw Error('Existing digest/authority guard is not ready');
- if(p.action==='install'){
- if((await tx.one('SELECT EXISTS(SELECT 1 FROM "LegacyDirectRun" WHERE status=$1) active',['running'])).active)throw Error('Synchronization must be idle');
- if((await tx.one(`SELECT to_regclass('"PayrollSourceIndex"') IS NOT NULL OR to_regclass('"PayrollSourceIndexMeta"') IS NOT NULL OR to_regclass('"TeacherEarnedMonth"') IS NOT NULL OR to_regclass('payroll_source_index_common59_digest') IS NOT NULL AS present`)).present)throw Error('Derived infrastructure already present');
- if((await tx.one("SELECT EXISTS(SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[])) AS present",[config.newNames])).present)throw Error('A proposed helper already exists');
- if((await tx.one("SELECT EXISTS(SELECT 1 FROM pg_trigger WHERE tgrelid=ANY(ARRAY['\"CommonModel\"'::regclass,'\"CourseAppointment\"'::regclass,'\"LessonRecord\"'::regclass,'\"LessonRecordFact\"'::regclass]) AND tgname IN ('xs_payroll_source_index_row','xs_payroll_source_index_truncate')) AS present")).present)throw Error('A proposed trigger already exists');
- await tx.any(config.sql);
- }else if(p.action==='rehearse'||p.action==='rollback'){
- if(p.action==='rehearse'&&!(await tx.one(`SELECT to_regclass('\"PayrollSourceIndex\"') IS NOT NULL AND to_regclass('\"PayrollSourceIndexMeta\"') IS NOT NULL AND to_regclass('\"TeacherEarnedMonth\"') IS NOT NULL AND to_regprocedure('xs_payroll_read_snapshot(text,text,boolean,boolean)') IS NOT NULL AS present`)).present)throw Error('Install first; an empty rollback is not a rehearsal');
- await tx.none(config.rollback);
- const functions=await tx.query("SELECT p.oid::regprocedure::text AS identity FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[])",[config.newNames]);
- for(const fn of functions)await tx.none('DROP FUNCTION IF EXISTS '+fn.identity);
- if(JSON.stringify(canonical(await tx.query(config.query,[config.names])))!==JSON.stringify(config.originals))throw Error('Rollback changed authoritative SQL');
- if(p.action==='rehearse')throw Error('ROLLBACK_REHEARSAL_OK');return{rolledBack:true};
- }
- if(JSON.stringify(canonical(await tx.query(config.query,[config.names])))!==JSON.stringify(config.originals))throw Error('Migration changed authoritative SQL');
- const ready=await tx.one('SELECT xs_payroll_digest_ready() digest,xs_payroll_source_index_ready() source,xs_payroll_read_snapshot_authority_ready() snapshot');
- if(!ready.digest||!ready.source||!ready.snapshot)throw Error('Installed guard not ready');
- const definitions=await tx.query("SELECT proname,pg_get_function_identity_arguments(p.oid) args,pg_get_functiondef(p.oid) definition FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[]) ORDER BY proname,args",[config.newNames]);
- return{ready,definitions,originalFunctionsUnchanged:true};
- });
- }
- const source='const config='+JSON.stringify({sql,originals,names,newNames,query,rollback})+';'+run.toString();
- if(action==='prepare'){
- const candidates=localCandidates(),manifest={sqlHash:sha(sql),rollbackHash:sha(rollback),scopes:{}};
- for(const [scope,baseFile,helpers]of[['mobile','mobile-gateway.json',[...MOBILE_HELPERS,...MONTHLY_HELPERS]],['operations','admin-gateway.json',OPERATIONS_HELPERS]]){
- const baseline=JSON.parse(fs.readFileSync(root+'/'+baseFile)),result=patchFunctions(baseline.code,candidates[scope],helpers);
- fs.writeFileSync(root+'/'+scope+'-candidate.json',JSON.stringify({...baseline,code:result.code}),{mode:0o600});manifest.scopes[scope]={id:baseline.objectId,...result.audit};
- }
- fs.writeFileSync(root+'/infrastructure.sql',sql,{mode:0o600});fs.writeFileSync(root+'/rollback.sql',rollback,{mode:0o600});fs.writeFileSync(root+'/manifest.json',JSON.stringify(manifest,null,2),{mode:0o600});console.log(JSON.stringify({prepared:true,sqlHash:manifest.sqlHash,scopes:Object.keys(manifest.scopes)}));
- }else if(['install','check','rehearse','rollback'].includes(action)){
- const manifest=JSON.parse(fs.readFileSync(root+'/manifest.json'));if(manifest.sqlHash!==sha(sql)||manifest.rollbackHash!==sha(rollback))throw Error('Reviewed migration changed');
- const result=await withGateway(source,async call=>{try{return await call({action});}catch(e){if(action==='rehearse'&&e.message==='ROLLBACK_REHEARSAL_OK')return{rehearsed:true};throw e;}});
- fs.writeFileSync(root+'/'+action+'-receipt.json',JSON.stringify(result,null,2),{mode:0o600});console.log(JSON.stringify({...result,definitions:result.definitions?.length}));
- }else if(action==='publish'){
- const manifest=JSON.parse(fs.readFileSync(root+'/manifest.json'));
- for(const scope of ['operations','mobile']){
- const saved=manifest.scopes[scope],candidate=JSON.parse(fs.readFileSync(root+'/'+scope+'-candidate.json')),live=await parse('/classes/Function/'+saved.id);
- if(sha(live.code)!==saved.baselineHash||sha(candidate.code)!==saved.candidateHash)throw Error('Gateway drift or candidate changed: '+scope);
- }
- const receipts=[];for(const scope of ['operations','mobile']){const saved=manifest.scopes[scope],candidate=JSON.parse(fs.readFileSync(root+'/'+scope+'-candidate.json'));await parse('/classes/Function/'+saved.id,'PUT',{code:candidate.code});const readback=await parse('/classes/Function/'+saved.id);if(sha(readback.code)!==saved.candidateHash)throw Error('Gateway readback mismatch: '+scope);receipts.push({scope,hash:saved.candidateHash});}
- fs.writeFileSync(root+'/publish-receipt.json',JSON.stringify(receipts,null,2),{mode:0o600});console.log(JSON.stringify(receipts));
- }else throw Error('Expected prepare/install/check/rehearse/publish/rollback');
|