-
Notifications
You must be signed in to change notification settings - Fork 22
Hotfix admin deletion qf project middle table #2217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
fd00e0a
efab150
aba6757
6595f6d
bcde46f
498acb4
1189ae0
f1d0096
066fa45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,32 +1,300 @@ | ||
| import { MigrationInterface, QueryRunner } from 'typeorm'; | ||
| import { logger } from '../src/utils/logger'; | ||
|
|
||
| export class AddIdToProjectQfRound1779182511001 implements MigrationInterface { | ||
| name = 'AddIdToProjectQfRound1779182511001'; | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| // Add the new id column as auto-incrementing column (not primary key) | ||
| // First, check if the table exists and get the current primary key constraint name | ||
| const tableExists = await queryRunner.hasTable( | ||
| 'project_qf_rounds_qf_round', | ||
| ); | ||
| if (!tableExists) { | ||
| throw new Error('Table project_qf_rounds_qf_round does not exist'); | ||
| } | ||
|
|
||
| // Check if the id column already exists (migration already ran) | ||
| const idColumnExists = await queryRunner.hasColumn( | ||
| 'project_qf_rounds_qf_round', | ||
| 'id', | ||
| ); | ||
|
|
||
| if (idColumnExists) { | ||
| return; | ||
| } | ||
|
|
||
| // Get all primary key constraints for this table | ||
| const allConstraintsQuery = await queryRunner.query(` | ||
| SELECT conname, contype | ||
| FROM pg_constraint | ||
| WHERE conrelid = ( | ||
| SELECT oid | ||
| FROM pg_class | ||
| WHERE relname = 'project_qf_rounds_qf_round' | ||
| ) AND contype = 'p' | ||
| `); | ||
|
|
||
| // Drop all primary key constraints | ||
| for (const constraint of allConstraintsQuery) { | ||
| try { | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| DROP CONSTRAINT IF EXISTS "${constraint.conname}" | ||
| `); | ||
| } catch (error) { | ||
| // Continue with other constraints | ||
| } | ||
| } | ||
|
|
||
| // Also try to drop the standard constraint names that might exist | ||
| // Use a more robust approach to handle constraint dropping | ||
| const possibleConstraints = [ | ||
| 'PK_046d515dee2988817725ec75ebf', | ||
| 'project_qf_rounds_qf_round_pkey', | ||
| 'PK_project_qf_rounds_qf_round', | ||
| ]; | ||
|
|
||
| for (const constraintName of possibleConstraints) { | ||
| try { | ||
| // First check if constraint exists before trying to drop it | ||
| const constraintExists = await queryRunner.query(` | ||
| SELECT 1 FROM pg_constraint | ||
| WHERE conname = '${constraintName}' | ||
| AND conrelid = ( | ||
| SELECT oid FROM pg_class WHERE relname = 'project_qf_rounds_qf_round' | ||
| ) | ||
| `); | ||
|
|
||
| if (constraintExists && constraintExists.length > 0) { | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| DROP CONSTRAINT "${constraintName}" | ||
| `); | ||
| } | ||
| } catch (error) { | ||
| // Ignore errors for constraints that don't exist | ||
| logger.error(`Error handling constraint ${constraintName}:`, error); | ||
| } | ||
| } | ||
|
|
||
| // PostgreSQL doesn't support column positioning, so we need to recreate the table | ||
| // to have id as the first column | ||
| await this.resetTableStructure(queryRunner); | ||
| } | ||
|
|
||
| // Emergency method to completely reset the table if needed | ||
| private async resetTableStructure(queryRunner: QueryRunner): Promise<void> { | ||
| // Step 1: Backup existing data | ||
| await queryRunner.query(` | ||
| CREATE TEMP TABLE project_qf_rounds_backup AS | ||
| SELECT "projectId", "qfRoundId", "sumDonationValueUsd", "countUniqueDonors", "createdAt", "updatedAt" | ||
| FROM "project_qf_rounds_qf_round" | ||
| `); | ||
|
|
||
| // Step 2: Drop the problematic table | ||
| await queryRunner.query(` | ||
| DROP TABLE IF EXISTS "project_qf_rounds_qf_round" CASCADE | ||
| `); | ||
|
|
||
| // Step 3: Recreate the table with proper structure | ||
| await queryRunner.query(` | ||
| CREATE TABLE "project_qf_rounds_qf_round" ( | ||
| "id" SERIAL PRIMARY KEY, | ||
| "projectId" INTEGER NOT NULL, | ||
| "qfRoundId" INTEGER NOT NULL, | ||
| "sumDonationValueUsd" REAL DEFAULT 0, | ||
| "countUniqueDonors" INTEGER DEFAULT 0, | ||
| "createdAt" TIMESTAMP DEFAULT NOW(), | ||
| "updatedAt" TIMESTAMP DEFAULT NOW(), | ||
| CONSTRAINT "UQ_project_qf_rounds_composite" UNIQUE ("projectId", "qfRoundId") | ||
| ) | ||
| `); | ||
|
|
||
| // Step 4: Create indexes | ||
| await queryRunner.query(` | ||
| CREATE INDEX "IDX_project_qf_rounds_projectId" ON "project_qf_rounds_qf_round" ("projectId") | ||
| `); | ||
|
|
||
| await queryRunner.query(` | ||
| CREATE INDEX "IDX_project_qf_rounds_qfRoundId" ON "project_qf_rounds_qf_round" ("qfRoundId") | ||
| `); | ||
|
|
||
| // Step 5: Restore data with new auto-incrementing IDs | ||
| await queryRunner.query(` | ||
| INSERT INTO "project_qf_rounds_qf_round" ("projectId", "qfRoundId", "sumDonationValueUsd", "countUniqueDonors", "createdAt", "updatedAt") | ||
| SELECT "projectId", "qfRoundId", "sumDonationValueUsd", "countUniqueDonors", "createdAt", "updatedAt" | ||
| FROM project_qf_rounds_backup | ||
| `); | ||
|
|
||
| // Clean up | ||
| await queryRunner.query(`DROP TABLE project_qf_rounds_backup`); | ||
| } | ||
|
|
||
| // Alternative method using the constraint fix approach | ||
| private async fixConstraintsOnly(queryRunner: QueryRunner): Promise<void> { | ||
| // Drop all possible primary key constraints | ||
| const allConstraints = await queryRunner.query(` | ||
| SELECT conname | ||
| FROM pg_constraint | ||
| WHERE conrelid = ( | ||
| SELECT oid | ||
| FROM pg_class | ||
| WHERE relname = 'project_qf_rounds_qf_round' | ||
| ) AND contype = 'p' | ||
| `); | ||
|
|
||
| for (const constraint of allConstraints) { | ||
| try { | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| DROP CONSTRAINT IF EXISTS "${constraint.conname}" | ||
| `); | ||
| } catch (error) { | ||
| logger.error(`Error dropping constraint ${constraint.conname}:`, error); | ||
| } | ||
| } | ||
|
|
||
| // Also try common constraint names | ||
| const commonConstraints = [ | ||
| 'PK_046d515dee2988817725ec75ebf', | ||
| 'project_qf_rounds_qf_round_pkey', | ||
| 'PK_project_qf_rounds_qf_round', | ||
| ]; | ||
|
|
||
| for (const constraintName of commonConstraints) { | ||
| try { | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| DROP CONSTRAINT IF EXISTS "${constraintName}" | ||
| `); | ||
| } catch (error) { | ||
| // Ignore errors for constraints that don't exist | ||
| } | ||
| } | ||
|
|
||
| // Check if id column exists, if not add it | ||
| const idColumnExists = await queryRunner.hasColumn( | ||
| 'project_qf_rounds_qf_round', | ||
| 'id', | ||
| ); | ||
|
|
||
| if (!idColumnExists) { | ||
| // First add the column as SERIAL (auto-incrementing) | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" ADD COLUMN id SERIAL | ||
| `); | ||
|
|
||
| // Then add the primary key constraint | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| ADD CONSTRAINT "PK_project_qf_rounds_qf_round_id" PRIMARY KEY (id) | ||
| `); | ||
| } else { | ||
| // Check if there are records without IDs (shouldn't happen with SERIAL) | ||
| const recordsWithoutIdResult = await queryRunner.query(` | ||
| SELECT COUNT(*) as count | ||
| FROM "project_qf_rounds_qf_round" | ||
| WHERE id IS NULL | ||
| `); | ||
| const recordsWithoutId = recordsWithoutIdResult[0]?.count || 0; | ||
|
|
||
| if (recordsWithoutId > 0) { | ||
| // Fix records without ID | ||
| await queryRunner.query(` | ||
| UPDATE "project_qf_rounds_qf_round" | ||
| SET id = nextval(pg_get_serial_sequence('project_qf_rounds_qf_round', 'id')) | ||
| WHERE id IS NULL | ||
| `); | ||
| } | ||
| } | ||
|
|
||
| // Add unique constraint if it doesn't exist | ||
| const uniqueConstraintExists = await queryRunner.query(` | ||
| SELECT 1 | ||
| FROM information_schema.table_constraints | ||
| WHERE table_name = 'project_qf_rounds_qf_round' | ||
| AND constraint_name = 'UQ_project_qf_rounds_composite' | ||
| `); | ||
|
|
||
| if (!uniqueConstraintExists || uniqueConstraintExists.length === 0) { | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| ADD CONSTRAINT "UQ_project_qf_rounds_composite" | ||
| UNIQUE ("projectId", "qfRoundId") | ||
| `); | ||
| } | ||
|
|
||
| // Add indexes if they don't exist | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| ADD COLUMN "id" SERIAL | ||
| CREATE INDEX IF NOT EXISTS "IDX_project_qf_rounds_projectId" | ||
| ON "project_qf_rounds_qf_round" ("projectId") | ||
| `); | ||
|
|
||
| // Add index on the id column for performance | ||
| await queryRunner.query(` | ||
| CREATE INDEX "IDX_project_qf_rounds_id" | ||
| ON "project_qf_rounds_qf_round" ("id") | ||
| CREATE INDEX IF NOT EXISTS "IDX_project_qf_rounds_qfRoundId" | ||
| ON "project_qf_rounds_qf_round" ("qfRoundId") | ||
| `); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| // Drop the index first | ||
| // Drop the indexes first | ||
| await queryRunner.query(` | ||
| DROP INDEX IF EXISTS "IDX_project_qf_rounds_projectId" | ||
| `); | ||
|
|
||
| await queryRunner.query(` | ||
| DROP INDEX IF EXISTS "IDX_project_qf_rounds_qfRoundId" | ||
| `); | ||
|
|
||
| // Drop the unique constraint | ||
| await queryRunner.query(` | ||
| DROP INDEX IF EXISTS "IDX_project_qf_rounds_id" | ||
| ALTER TABLE IF EXISTS "project_qf_rounds_qf_round" | ||
| DROP CONSTRAINT IF EXISTS "UQ_project_qf_rounds_composite" | ||
| `); | ||
|
|
||
| // Get the current primary key constraint name for the id column | ||
| const primaryKeyQuery = await queryRunner.query(` | ||
| SELECT constraint_name | ||
| FROM information_schema.table_constraints | ||
| WHERE table_name = 'project_qf_rounds_qf_round' | ||
| AND constraint_type = 'PRIMARY KEY' | ||
| `); | ||
|
|
||
| const primaryKeyName = primaryKeyQuery[0]?.constraint_name; | ||
|
|
||
| if (primaryKeyName) { | ||
| try { | ||
| // Drop the id primary key constraint | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| DROP CONSTRAINT "${primaryKeyName}" | ||
| `); | ||
| } catch (error) { | ||
| // Fallback with IF EXISTS | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| DROP CONSTRAINT IF EXISTS "${primaryKeyName}" | ||
| `); | ||
| } | ||
| } else { | ||
| // Fallback: drop all possible primary key constraints | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| DROP CONSTRAINT IF EXISTS "UQ_project_qf_rounds_qf_round" | ||
| `); | ||
| } | ||
|
|
||
| // Drop the id column | ||
| await queryRunner.query(` | ||
| ALTER TABLE "project_qf_rounds_qf_round" | ||
| ALTER TABLE IF EXISTS "project_qf_rounds_qf_round" | ||
| DROP COLUMN IF EXISTS "id" | ||
| `); | ||
|
|
||
| // Restore the composite primary key | ||
| await queryRunner.query(` | ||
| ALTER TABLE IF EXISTS "project_qf_rounds_qf_round" | ||
| ADD CONSTRAINT "PK_project_qf_rounds_qf_round" | ||
| PRIMARY KEY ("projectId", "qfRoundId") | ||
| `); | ||
|
Comment on lines
+293
to
+298
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hard-coded constraint name may differ from original. The composite primary key is restored with the hard-coded name This was previously flagged and marked as addressed in commit 6595f6d, but the hard-coded name remains. While functionally equivalent, this creates a naming inconsistency. Consider dynamically determining the original name if perfect reversibility is required. However, since constraint names don't affect behavior, this may be acceptable if you're standardizing on the new name going forward. If standardization is not the goal, you could store the original PK name before dropping it in // In up(), before dropping PKs:
const originalPK = await queryRunner.query(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'project_qf_rounds_qf_round'::regclass
AND contype = 'p'
LIMIT 1
`);
// Store originalPK[0]?.conname somewhere (e.g., migration metadata table)
// In down():
// Retrieve stored name and use it instead of hard-coded value🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CASCADE will drop dependent objects without recreating them.
DROP TABLE ... CASCADEon line 96 will silently drop all dependent database objects:These dependencies are not recreated after the table is rebuilt, which will break referential integrity and potentially cause application failures.
Either:
Option 1: Remove CASCADE and handle dependencies explicitly:
// Step 2: Drop the problematic table await queryRunner.query(` - DROP TABLE IF EXISTS "project_qf_rounds_qf_round" CASCADE + DROP TABLE IF EXISTS "project_qf_rounds_qf_round" `);Then query
pg_constraintfor foreign keys and recreate them manually after table creation.Option 2: Document that this migration requires manual restoration of dependent objects, and provide a script to identify what will be dropped: