fix(migration): 재생성된 유산 저장소 자연키를 보존한다
This commit is contained in:
@@ -92,19 +92,26 @@ apply. It also refuses a changed host/database/user identity or a source table
|
|||||||
whose maximum ID moved behind its checkpoint. Password rotation does not change
|
whose maximum ID moved behind its checkpoint. Password rotation does not change
|
||||||
the source fingerprint.
|
the source fingerprint.
|
||||||
|
|
||||||
| Source data | Incremental policy |
|
| Source data | Incremental policy |
|
||||||
| --------------------------------------------- | ----------------------------------------------------------------------- |
|
| ------------------------------------------ | ----------------------------------------------------------------------- |
|
||||||
| `member_log` | Read only IDs after the committed high-water mark. |
|
| `member_log` | Read only IDs after the committed high-water mark. |
|
||||||
| game archive/event-history tables | Read only IDs after the profile checkpoint. |
|
| game archive/event-history tables | Read only IDs after the profile checkpoint. |
|
||||||
| `member`, root/game `storage`, `system`, bans | Rescan and idempotently upsert because old rows are mutable. |
|
| `member`, root `storage`, `system`, bans | Rescan and idempotently upsert because old rows are mutable. |
|
||||||
| `ng_games` | Rescan because a season row can gain its final winner after creation. |
|
| game `storage` | Rescan by `(namespace, key)` and refresh a recreated row's source ID. |
|
||||||
| preserved `batres<general_no>.txt` seasons | Hash each season; import new immutable seasons after a full checkpoint. |
|
| `ng_games` | Rescan because a season row can gain its final winner after creation. |
|
||||||
|
| preserved `batres<general_no>.txt` seasons | Hash each season; import new immutable seasons after a full checkpoint. |
|
||||||
|
|
||||||
The append policy assumes Ref primary keys are never reused and completed
|
The append policy assumes Ref primary keys are never reused and completed
|
||||||
archive rows are immutable. Incremental mode does not mirror source deletions.
|
archive rows are immutable. Incremental mode does not mirror source deletions.
|
||||||
If either assumption is false, take a new reviewed backup and run full mode;
|
If either assumption is false, take a new reviewed backup and run full mode;
|
||||||
do not edit checkpoint rows by hand.
|
do not edit checkpoint rows by hand.
|
||||||
|
|
||||||
|
Ref may delete and recreate a mutable game-storage tuple with the same
|
||||||
|
`(namespace, key)` and a new auto-increment ID. That tuple is the durable
|
||||||
|
identity; the latest source ID is retained only as recovery metadata. Rows for
|
||||||
|
deleted tuples remain archived because incremental mode does not infer
|
||||||
|
tombstones.
|
||||||
|
|
||||||
The optional file importer reads only immediate
|
The optional file importer reads only immediate
|
||||||
`logs/preserved/<profile>_*/batres<general_no>.txt` regular files. It maps the
|
`logs/preserved/<profile>_*/batres<general_no>.txt` regular files. It maps the
|
||||||
profile and season directory to the archived general's `(source_profile,
|
profile and season directory to the archived general's `(source_profile,
|
||||||
|
|||||||
@@ -504,7 +504,9 @@ const migrateStorage = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (target) {
|
if (target) {
|
||||||
await upsertRows(target, 'legacy_game_storage', archives, ['source_id']);
|
// Ref storage는 같은 namespace/key를 삭제 후 새 auto-increment ID로 다시 만들 수 있다.
|
||||||
|
// 장기 상태의 권위 identity로 자연키를 사용하고 최신 source ID까지 함께 갱신한다.
|
||||||
|
await upsertRows(target, 'legacy_game_storage', archives, ['namespace', 'key']);
|
||||||
await upsertRows(target, 'inheritance_point', points, ['user_id', 'key']);
|
await upsertRows(target, 'inheritance_point', points, ['user_id', 'key']);
|
||||||
await upsertRows(target, 'inheritance_user_state', userStates, ['user_id']);
|
await upsertRows(target, 'inheritance_user_state', userStates, ['user_id']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ const sourceRows = {
|
|||||||
data: JSON.stringify({ leader: 80, power: 70, intel: 60, history: 'first<br>second<br>' }),
|
data: JSON.stringify({ leader: 80, power: 70, intel: 60, history: 'first<br>second<br>' }),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
storage: [
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
namespace: 'inheritance_42',
|
||||||
|
key: 'point',
|
||||||
|
value: '[30,null]',
|
||||||
|
},
|
||||||
|
],
|
||||||
} satisfies Record<string, Array<Record<string, unknown>>>;
|
} satisfies Record<string, Array<Record<string, unknown>>>;
|
||||||
|
|
||||||
const sourcePool = (): MariaPool => {
|
const sourcePool = (): MariaPool => {
|
||||||
@@ -148,6 +156,8 @@ describe('legacy archive game migration', () => {
|
|||||||
expect(summary.importRunId).toBe('77');
|
expect(summary.importRunId).toBe('77');
|
||||||
expect(sql).toContain('INSERT INTO "legacy_archive"."game_history"');
|
expect(sql).toContain('INSERT INTO "legacy_archive"."game_history"');
|
||||||
expect(sql).toContain('INSERT INTO "legacy_archive"."general"');
|
expect(sql).toContain('INSERT INTO "legacy_archive"."general"');
|
||||||
|
expect(sql).toContain('INSERT INTO "legacy_game_storage"');
|
||||||
|
expect(sql).toContain('ON CONFLICT ("namespace", "key")');
|
||||||
expect(sql).not.toContain('INSERT INTO "ng_games"');
|
expect(sql).not.toContain('INSERT INTO "ng_games"');
|
||||||
expect(sql).not.toContain('INSERT INTO "ng_old_generals"');
|
expect(sql).not.toContain('INSERT INTO "ng_old_generals"');
|
||||||
expect(sql).toContain(`SET "status" = 'COMPLETED'`);
|
expect(sql).toContain(`SET "status" = 'COMPLETED'`);
|
||||||
|
|||||||
Reference in New Issue
Block a user