stagingbackend / src /db /migrate10.js
Antaram's picture
Upload 37 files
500ab10 verified
Raw
History Blame Contribute Delete
3.25 kB
/**
* migrate10 β€” prevent duplicate expense rows per transaction.
*
* The expense / patti_expense tables are 1:1 with their parent tx in practice
* but had no UNIQUE constraint. The list/detail SQL used to GROUP BY (t.id, e.id)
* which masked any accidental duplicate by row-multiplying items Γ— payments.
* We've moved to lateral subqueries that always pick the latest expense row,
* but a UNIQUE constraint stops the bad state arising in the first place.
*
* Idempotent β€” re-running is a no-op via migration_history guard.
*/
const pool = require('./config');
const KEY = 'migrate10_unique_expenses_per_tx';
async function dedupePerTx(client, table) {
// Keep the row with the latest created_at (ties broken by id desc); delete the rest.
const sql = `
DELETE FROM ${table} a
USING ${table} b
WHERE a.transaction_id = b.transaction_id
AND (a.created_at < b.created_at
OR (a.created_at = b.created_at AND a.id < b.id))
`;
const r = await client.query(sql);
return r.rowCount || 0;
}
async function addUnique(client, table, constraintName) {
// Check existing constraint first β€” re-runs after a failure should not error.
const { rows } = await client.query(
`SELECT 1 FROM pg_constraint WHERE conname = $1`,
[constraintName]
);
if (rows.length) return false;
await client.query(
`ALTER TABLE ${table} ADD CONSTRAINT ${constraintName} UNIQUE (transaction_id)`
);
return true;
}
(async () => {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
`CREATE TABLE IF NOT EXISTS migration_history (
key VARCHAR(120) PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`
);
const guard = await client.query('SELECT 1 FROM migration_history WHERE key = $1', [KEY]);
if (guard.rows.length) {
console.log(`[migrate10] already applied (${KEY}) β€” skipping`);
await client.query('ROLLBACK');
return;
}
const delExp = await dedupePerTx(client, 'expenses');
const delPattiExp = await dedupePerTx(client, 'patti_expenses');
console.log(`[migrate10] removed ${delExp} duplicate expenses rows`);
console.log(`[migrate10] removed ${delPattiExp} duplicate patti_expenses rows`);
const addedExp = await addUnique(client, 'expenses', 'expenses_tx_unique');
const addedPattiExp = await addUnique(client, 'patti_expenses', 'patti_expenses_tx_unique');
console.log(`[migrate10] expenses UNIQUE constraint: ${addedExp ? 'added' : 'already present'}`);
console.log(`[migrate10] patti_expenses UNIQUE constraint: ${addedPattiExp ? 'added' : 'already present'}`);
await client.query('INSERT INTO migration_history (key) VALUES ($1)', [KEY]);
await client.query('COMMIT');
console.log('[migrate10] done');
} catch (e) {
await client.query('ROLLBACK');
console.error('[migrate10] FAILED β€” rolled back:', e.message);
process.exit(1);
} finally {
client.release();
await pool.end();
}
})();