feat(migrate): migration_map idempotency ledger
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
11
lib/db/migrations/018_migration_map.sql
Normal file
11
lib/db/migrations/018_migration_map.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- 018_migration_map.sql — idempotency ledger for void-migrate. Maps a source row
|
||||
-- to the Void 2 entity it created, so re-running an import never duplicates.
|
||||
CREATE TABLE migration_map (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
source text NOT NULL, -- 'void1' | 'bookstack' | 'karakeep' | 'plans'
|
||||
source_id text NOT NULL, -- e.g. 'wiki_pages:42'
|
||||
entity_type text NOT NULL, -- 'page' | 'project' | 'task' | 'conversation' | 'message'
|
||||
entity_id uuid NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (source, source_id, entity_type)
|
||||
);
|
||||
15
lib/db/repos/migration_map.js
Normal file
15
lib/db/repos/migration_map.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { pool } from '../pool.js';
|
||||
|
||||
export async function seen(source, source_id, entity_type) {
|
||||
const { rows: [r] } = await pool.query(
|
||||
`SELECT entity_id FROM migration_map WHERE source=$1 AND source_id=$2 AND entity_type=$3`,
|
||||
[source, source_id, entity_type]);
|
||||
return r ? r.entity_id : null;
|
||||
}
|
||||
|
||||
export async function record(source, source_id, entity_type, entity_id) {
|
||||
await pool.query(
|
||||
`INSERT INTO migration_map(source, source_id, entity_type, entity_id) VALUES($1,$2,$3,$4)
|
||||
ON CONFLICT (source, source_id, entity_type) DO NOTHING`,
|
||||
[source, source_id, entity_type, entity_id]);
|
||||
}
|
||||
22
tests/db/migration_map.test.js
Normal file
22
tests/db/migration_map.test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { resetDb } from '../helpers/db.js';
|
||||
import { migrateUp } from '../../lib/db/migrate.js';
|
||||
import * as map from '../../lib/db/repos/migration_map.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
beforeAll(async () => { await resetDb(); await migrateUp(); });
|
||||
|
||||
describe('migration_map', () => {
|
||||
it('seen() is null until record(), then returns the entity id', async () => {
|
||||
const eid = randomUUID();
|
||||
expect(await map.seen('void1', 'wiki_pages:1', 'page')).toBeNull();
|
||||
await map.record('void1', 'wiki_pages:1', 'page', eid);
|
||||
expect(await map.seen('void1', 'wiki_pages:1', 'page')).toBe(eid);
|
||||
});
|
||||
it('record() is idempotent on the unique key', async () => {
|
||||
const eid = randomUUID();
|
||||
await map.record('plans', 'a.md', 'page', eid);
|
||||
await map.record('plans', 'a.md', 'page', randomUUID()); // ignored
|
||||
expect(await map.seen('plans', 'a.md', 'page')).toBe(eid);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user