From 91d90bd2b632b2457f8c2f2897b564c9072d9e40 Mon Sep 17 00:00:00 2001 From: Deep Koluguri Date: Sun, 5 Apr 2026 16:01:36 -0400 Subject: [PATCH] fixed --- backend/scripts/migrate-pg-to-sqlite.js | 115 ++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/backend/scripts/migrate-pg-to-sqlite.js b/backend/scripts/migrate-pg-to-sqlite.js index ff029f3..b41ae3d 100644 --- a/backend/scripts/migrate-pg-to-sqlite.js +++ b/backend/scripts/migrate-pg-to-sqlite.js @@ -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) { try { return await pg.query(`SELECT * FROM "${table}"`, { type: QueryTypes.SELECT }); @@ -253,6 +350,24 @@ async function main() { 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'); for (const table of TABLES_ORDER) { const rows = await fetchPostgresTable(pg, table);