diff --git a/scripts/migrate_uapi_upappid.py b/scripts/migrate_uapi_upappid.py new file mode 100644 index 0000000..ec32de9 --- /dev/null +++ b/scripts/migrate_uapi_upappid.py @@ -0,0 +1,130 @@ +""" +Migration: uapi table apisetid -> upappid + +Removes uapiset intermediate layer. Each upapp now owns its own uapi records. +For shared apisetid: copies uapi records so each upapp has its own copy. + +Usage (in Sage virtual env): + ./py3/bin/python3 ~/repos/uapi/scripts/migrate_uapi_upappid.py --output /tmp/migrate_uapi.sql + +Review the output SQL, then execute on your database. +""" +from appPublic.getConfig import getConfig +import asyncio, json, sys, argparse +from sqlor.dbpools import DBPools +from appPublic.uniqueID import getID + +config = getConfig('.') +db = DBPools(config.databases) +dbname = list(config.databases.keys())[0] + + +async def generate_migration_sql(): + """Generate SQL to migrate uapi.apisetid -> uapi.upappid.""" + lines = [ + "-- Migration: uapi table apisetid -> upappid", + "-- Removes uapiset intermediate layer.", + "-- Each upapp owns its own uapi records after migration.", + "", + "-- Step 1: Add upappid column to uapi", + "ALTER TABLE uapi ADD COLUMN upappid VARCHAR(21) DEFAULT NULL COMMENT '上位系统ID' AFTER id;", + "" + ] + + # Load data + async with db.sqlorContext(dbname) as sor: + # Get all upapps + upapps = await sor.sqlExe('select id, name, apisetid from upapp', {}) + # Get all uapis + uapis = await sor.sqlExe('select id, apisetid, name, httpmethod, path, headers, ioid, auth_apiname, response, params, data, chunk_match from uapi', {}) + + # Build mapping: apisetid -> [upapp1, upapp2, ...] + apiset_to_upapps = {} + for u in upapps: + aid = u.get('apisetid') + if aid: + apiset_to_upapps.setdefault(aid, []).append(u) + + # Build mapping: apisetid -> [uapi_records] + apiset_to_uapis = {} + for a in uapis: + aid = a.get('apisetid') + if aid: + apiset_to_uapis.setdefault(aid, []).append(a) + + inserts = [] + for apisetid, upapp_list in apiset_to_upapps.items(): + uapi_records = apiset_to_uapis.get(apisetid, []) + + if len(upapp_list) == 1: + # Single upapp owns this apisetid -> just update + upapp = upapp_list[0] + for uapi in uapi_records: + inserts.append( + f"UPDATE uapi SET upappid = '{upapp['id']}' WHERE id = '{uapi['id']}';" + ) + else: + # Multiple upapps share this apisetid -> pick first as owner, copy for rest + owner = upapp_list[0] + + # Update existing records to point to owner + for uapi in uapi_records: + inserts.append( + f"UPDATE uapi SET upappid = '{owner['id']}' WHERE id = '{uapi['id']}';" + ) + + # Copy for other upapps + for other_upapp in upapp_list[1:]: + for uapi in uapi_records: + new_id = getID() + old_id = uapi['id'] + fields = ['id', 'upappid', 'name', 'httpmethod', 'path', 'headers', 'ioid', 'response', 'params', 'data', 'chunk_match'] + vals = [f"'{new_id}'", f"'{other_upapp['id']}'"] + for f in fields[2:]: + v = uapi.get(f) + if v is None: + vals.append('NULL') + elif isinstance(v, str): + escaped = v.replace("'", "\\'") + vals.append(f"'{escaped}'") + else: + vals.append(str(v)) + col_str = ', '.join(fields) + val_str = ', '.join(vals) + inserts.append(f"INSERT INTO uapi ({col_str}) VALUES ({val_str});") + + if inserts: + lines.append("-- Step 2: Migrate data (updates + copies for shared apisetid)") + lines.extend(inserts) + lines.append("") + + lines.extend([ + "-- Step 3: Drop apisetid column (verify upappid has no NULLs first)", + "-- UPDATE uapi SET upappid = (SELECT ownerid FROM organization LIMIT 1) WHERE upappid IS NULL;", + "-- ALTER TABLE uapi DROP COLUMN apisetid;", + "", + "-- Step 4: Add index", + "CREATE INDEX idx_uapi_upappid ON uapi (upappid);", + "" + ]) + + return '\n'.join(lines) + + +async def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--output', '-o', default='-') + args = parser.parse_args() + + sql = await generate_migration_sql() + + if args.output == '-': + print(sql) + else: + with open(args.output, 'w', encoding='utf-8') as f: + f.write(sql) + print(f"Generated migration SQL -> {args.output}") + + +if __name__ == '__main__': + asyncio.run(main())