This commit is contained in:
Deep Koluguri 2026-04-05 16:01:36 -04:00
parent 08cf18537b
commit 91d90bd2b6
1 changed files with 115 additions and 0 deletions

View File

@ -157,6 +157,103 @@ async function recreateGroupMembersTable(sqlite) {
); );
} }
/** Sorted column-set keys for each non-PK unique index (matches `PRAGMA index_list` / `index_info`). */
async function sqliteUniqueIndexColKeys(sqlite, tableName) {
if (!/^[a-z_]+$/.test(tableName)) {
throw new Error(`sqliteUniqueIndexColKeys: invalid table name "${tableName}"`);
}
const list = await sqlite.query(`PRAGMA index_list('${tableName}')`, { type: QueryTypes.SELECT });
const keys = [];
for (const row of list) {
const unique = row.unique === 1 || row.unique === true;
if (!unique) continue;
const info = await sqlite.query(`PRAGMA index_info('${String(row.name).replace(/'/g, "''")}')`, {
type: QueryTypes.SELECT,
});
const cols = info
.slice()
.sort((a, b) => (a.seqno ?? 0) - (b.seqno ?? 0))
.map((i) => i.name)
.filter(Boolean);
if (row.origin === 'pk') continue;
if (cols.length === 1 && cols[0] === 'id') continue;
keys.push(colSetKey(cols));
}
return keys;
}
async function monthlyDrawsSqliteNeedsRepair(sqlite) {
const keys = await sqliteUniqueIndexColKeys(sqlite, 'monthly_draws');
const hasComposite = keys.includes('group_id,month,year');
const badSingleton = keys.some((k) => k === 'year' || k === 'month' || k === 'group_id');
return !hasComposite || badSingleton;
}
/** Must match `src/models/MonthlyDraw.js` + Sequelize SQLite DDL (table is empty). */
async function recreateMonthlyDrawsTable(sqlite) {
await sqlite.query('DROP TABLE IF EXISTS `monthly_draws`');
await sqlite.query(`
CREATE TABLE \`monthly_draws\` (
\`id\` UUID PRIMARY KEY,
\`group_id\` UUID NOT NULL REFERENCES \`chit_groups\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE,
\`month\` INTEGER NOT NULL,
\`year\` INTEGER NOT NULL,
\`draw_date\` DATETIME NOT NULL,
\`eligible_members\` JSON NOT NULL DEFAULT '[]',
\`winner_id\` UUID REFERENCES \`users\` (\`id\`) ON DELETE SET NULL ON UPDATE CASCADE,
\`prize_amount\` DECIMAL(15,2),
\`server_seed\` VARCHAR(255) NOT NULL,
\`server_seed_hash\` VARCHAR(255) NOT NULL,
\`client_seed\` VARCHAR(255) NOT NULL,
\`nonce\` BIGINT NOT NULL,
\`result_hash\` VARCHAR(255) NOT NULL,
\`status\` TEXT DEFAULT 'pending',
\`notes\` TEXT,
\`created_at\` DATETIME NOT NULL,
\`updated_at\` DATETIME NOT NULL
)
`);
await sqlite.query(
'CREATE UNIQUE INDEX `monthly_draws_group_id_month_year` ON `monthly_draws` (`group_id`, `month`, `year`)'
);
}
async function paymentsSqliteNeedsRepair(sqlite) {
const keys = await sqliteUniqueIndexColKeys(sqlite, 'payments');
const hasComposite = keys.includes('group_id,month,year,user_id');
const badSingleton = keys.some((k) => ['year', 'month', 'group_id', 'user_id'].includes(k));
return !hasComposite || badSingleton;
}
/** Must match `src/models/Payment.js` + Sequelize SQLite DDL (table is empty). */
async function recreatePaymentsTable(sqlite) {
await sqlite.query('DROP TABLE IF EXISTS `payments`');
await sqlite.query(`
CREATE TABLE \`payments\` (
\`id\` UUID PRIMARY KEY,
\`group_id\` UUID NOT NULL REFERENCES \`chit_groups\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE,
\`user_id\` UUID NOT NULL REFERENCES \`users\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE,
\`month\` INTEGER NOT NULL,
\`year\` INTEGER NOT NULL,
\`amount\` DECIMAL(15,2) NOT NULL,
\`payment_method\` TEXT DEFAULT 'upi',
\`transaction_id\` VARCHAR(255),
\`source\` TEXT NOT NULL DEFAULT 'manual_manager_entry',
\`entered_by\` UUID REFERENCES \`users\` (\`id\`) ON DELETE SET NULL ON UPDATE CASCADE,
\`idempotency_key\` VARCHAR(128),
\`status\` TEXT DEFAULT 'pending',
\`paid_at\` DATETIME,
\`notes\` TEXT,
\`created_at\` DATETIME NOT NULL,
\`updated_at\` DATETIME NOT NULL
)
`);
await sqlite.query(
'CREATE UNIQUE INDEX `payments_group_id_user_id_month_year` ON `payments` (`group_id`, `user_id`, `month`, `year`)'
);
await sqlite.query('CREATE UNIQUE INDEX `payments_idempotency_key` ON `payments` (`idempotency_key`)');
}
async function fetchPostgresTable(pg, table) { async function fetchPostgresTable(pg, table) {
try { try {
return await pg.query(`SELECT * FROM "${table}"`, { type: QueryTypes.SELECT }); return await pg.query(`SELECT * FROM "${table}"`, { type: QueryTypes.SELECT });
@ -253,6 +350,24 @@ async function main() {
console.log(' Done.\n'); console.log(' Done.\n');
} }
if (await monthlyDrawsSqliteNeedsRepair(sqlite)) {
console.log(
'🔧 Fixing `monthly_draws` SQLite schema (wrong UNIQUE on `year` / missing composite on group+month+year).\n' +
' Recreating empty table to match current Sequelize model…\n'
);
await recreateMonthlyDrawsTable(sqlite);
console.log(' Done.\n');
}
if (await paymentsSqliteNeedsRepair(sqlite)) {
console.log(
'🔧 Fixing `payments` SQLite schema (legacy UNIQUE columns vs composite + idempotency_key).\n' +
' Recreating empty table to match current Sequelize model…\n'
);
await recreatePaymentsTable(sqlite);
console.log(' Done.\n');
}
console.log('📥 Copying from PostgreSQL → SQLite…\n'); console.log('📥 Copying from PostgreSQL → SQLite…\n');
for (const table of TABLES_ORDER) { for (const table of TABLES_ORDER) {
const rows = await fetchPostgresTable(pg, table); const rows = await fetchPostgresTable(pg, table);