갱신 행의 ID를 보존하고 예약턴의 불필요한 시퀀스를 제거한다

This commit is contained in:
Hide_D committed 2026-11-14 18:44:22 +09:00
1 parent bc54b157c1
commit 81e101ffef
43 files changed
+587 -113

No files matched your search

@@ -53,6 +53,11 @@ const readIndexes = async (table) =>
).rows;
try {
await fs.cp(path.join(packageRoot, 'prisma'), path.join(temp, 'prisma'), { recursive: true });
for (const name of await fs.readdir(path.join(temp, 'prisma', 'migrations'))) {
if (/^\d/.test(name) && name > targets.at(-1)) {
await fs.rm(path.join(temp, 'prisma', 'migrations', name), { recursive: true });
}
}
for (const target of targets) await fs.rm(path.join(temp, 'prisma', 'migrations', target), { recursive: true });
await fs.writeFile(
config,
@@ -0,0 +1,147 @@
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import pg from 'pg';
const sourceUrl = process.env.RESERVED_TURN_MIGRATION_TEST_DATABASE_URL;
if (!sourceUrl) throw new Error('RESERVED_TURN_MIGRATION_TEST_DATABASE_URL is required (isolated development DB only)');
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const target = '20261114020000_reserved_turn_slot_primary_keys';
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'reserved-slot-migration-'));
const schema = `reserved_slot_${randomUUID().replaceAll('-', '')}`;
const fresh = `${schema}_fresh`;
const config = path.join(packageRoot, `.reserved-slot-${process.pid}.config.mjs`);
const client = new pg.Client({ connectionString: sourceUrl });
const url = new URL(sourceUrl);
const deploy = (name) => {
url.searchParams.set('schema', name);
const result = spawnSync('pnpm', ['exec', 'prisma', 'migrate', 'deploy', '--config', config], {
cwd: packageRoot,
env: { ...process.env, DATABASE_URL: url.href },
encoding: 'utf8',
});
if (result.status !== 0) throw new Error(`Migration failed (exit ${result.status}); datasource output suppressed`);
};
const snapshot = async () => ({
general: (
await client.query(
'SELECT general_id,turn_idx,action_code,arg,created_at FROM general_turn ORDER BY general_id,turn_idx'
)
).rows,
nation: (
await client.query(
'SELECT nation_id,officer_level,turn_idx,action_code,arg,created_at FROM nation_turn ORDER BY nation_id,officer_level,turn_idx'
)
).rows,
generalRevision: (await client.query('SELECT * FROM general_turn_revision ORDER BY general_id')).rows,
nationRevision: (await client.query('SELECT * FROM nation_turn_revision ORDER BY nation_id,officer_level')).rows,
});
const assertKeys = async (name) => {
assert.equal(
(
await client.query(
"SELECT count(*)::int AS n FROM information_schema.columns WHERE table_schema=$1 AND table_name IN ('general_turn','nation_turn') AND column_name='id'",
[name]
)
).rows[0].n,
0
);
assert.equal(
(
await client.query(
"SELECT count(*)::int AS n FROM pg_sequences WHERE schemaname=$1 AND sequencename IN ('general_turn_id_seq','nation_turn_id_seq')",
[name]
)
).rows[0].n,
0
);
assert.deepEqual(
(
await client.query(
"SELECT t.relname,pg_get_constraintdef(c.oid) AS definition FROM pg_constraint c JOIN pg_class t ON c.conrelid=t.oid JOIN pg_namespace n ON t.relnamespace=n.oid WHERE n.nspname=$1 AND c.contype='p' AND t.relname IN ('general_turn','nation_turn') ORDER BY t.relname",
[name]
)
).rows,
[
{ relname: 'general_turn', definition: 'PRIMARY KEY (general_id, turn_idx)' },
{ relname: 'nation_turn', definition: 'PRIMARY KEY (nation_id, officer_level, turn_idx)' },
]
);
};
try {
await fs.cp(path.join(packageRoot, 'prisma'), path.join(temp, 'prisma'), { recursive: true });
await fs.rm(path.join(temp, 'prisma', 'migrations', target), { recursive: true });
await fs.writeFile(
config,
`export default { schema: ${JSON.stringify(path.join(temp, 'prisma/game.prisma'))}, datasource: { url: process.env.DATABASE_URL } };\n`
);
await client.connect();
await client.query(`CREATE SCHEMA "${schema}"`);
await client.query(`CREATE SCHEMA "${fresh}"`);
deploy(schema);
await client.query(`SET search_path TO "${schema}", public`);
await client.query(
`INSERT INTO general_turn(id,general_id,turn_idx,action_code,arg,created_at) VALUES(3000000001,7,0,'che_훈련','{"x":1}','2026-11-14'),(3000000002,7,1,'휴식','{}','2026-11-14')`
);
await client.query(
`INSERT INTO nation_turn(id,nation_id,officer_level,turn_idx,action_code,arg,created_at) VALUES(3000000003,3,12,0,'che_포상','{"amount":10}','2026-11-14'),(3000000004,3,10,0,'휴식','{}','2026-11-14')`
);
await client.query(
`INSERT INTO general_turn_revision(general_id,revision,lease_owner,lease_expires_at,updated_at) VALUES(7,29,'fixture-daemon','2026-11-15','2026-11-14')`
);
await client.query(
`INSERT INTO nation_turn_revision(nation_id,officer_level,revision,lease_owner,lease_expires_at,updated_at) VALUES(3,12,41,'fixture-daemon','2026-11-15','2026-11-14')`
);
const before = await snapshot();
await fs.cp(
path.join(packageRoot, 'prisma', 'migrations', target),
path.join(temp, 'prisma', 'migrations', target),
{ recursive: true }
);
deploy(schema);
assert.deepEqual(await snapshot(), before);
await assertKeys(schema);
await assert.rejects(
client.query("INSERT INTO general_turn(general_id,turn_idx,action_code) VALUES(7,0,'duplicate')"),
{ code: '23505' }
);
await assert.rejects(
client.query(
"INSERT INTO nation_turn(nation_id,officer_level,turn_idx,action_code) VALUES(3,12,0,'duplicate')"
),
{ code: '23505' }
);
await client.query('BEGIN');
await client.query('DELETE FROM general_turn WHERE general_id=7');
await client.query("INSERT INTO general_turn(general_id,turn_idx,action_code) VALUES(7,0,'replacement')");
await client.query('ROLLBACK');
assert.deepEqual(await snapshot(), before);
const migrations = (
await client.query('SELECT migration_name,checksum FROM _prisma_migrations ORDER BY migration_name')
).rows;
deploy(schema);
assert.deepEqual(
(await client.query('SELECT migration_name,checksum FROM _prisma_migrations ORDER BY migration_name')).rows,
migrations
);
deploy(fresh);
await client.query(`SET search_path TO "${fresh}", public`);
await assertKeys(fresh);
await client.query("INSERT INTO general_turn(general_id,turn_idx,action_code) VALUES(7,0,'휴식')");
await client.query("INSERT INTO nation_turn(nation_id,officer_level,turn_idx,action_code) VALUES(3,12,0,'휴식')");
console.log(
'PASS: populated predecessor and fresh migration, command/arg/timestamp/revision/lease preservation, natural PK uniqueness, queue replacement rollback, removed sequences, deploy no-op'
);
} finally {
await client.query('SET search_path TO public').catch(() => {});
await client.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`).catch(() => {});
await client.query(`DROP SCHEMA IF EXISTS "${fresh}" CASCADE`).catch(() => {});
await client.end();
await fs.rm(config, { force: true });
await fs.rm(temp, { recursive: true });
}