Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions migrations/20260205010629-cleanup-duplicate-folders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict';

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const MAX_ATTEMPTS = 10;
const BATCH_SIZE = 10;
const SLEEP_TIME_MS = 1000;

module.exports = {
up: async (queryInterface) => {
let totalRenamed = 0;
let batchCount = 0;
let attempts = 0;

console.info(`Batch size: ${BATCH_SIZE} duplicate folders per batch`);
console.info('Starting cleanup of duplicate folders...');

console.info('Creating supporting index...');
await queryInterface.sequelize.query(`
CREATE INDEX CONCURRENTLY IF NOT EXISTS folders_parentuuid_plainname_not_deleted_support_index
ON folders (parent_uuid, plain_name)
WHERE deleted = false AND removed = false;
`);

const renameQuery = `
WITH duplicate_groups AS (
SELECT parent_uuid, plain_name, MIN(id) as id_to_keep
FROM folders
WHERE deleted = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add removed = false from here also, as the indexes we should add and the issue we have, is with the existing folders in the same folder with the same name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

AND removed = false
AND parent_uuid IS NOT NULL
AND plain_name IS NOT NULL
GROUP BY parent_uuid, plain_name
HAVING COUNT(*) > 1
LIMIT ${BATCH_SIZE}
)
UPDATE folders f
SET
plain_name = f.plain_name || '_' || f.id::text,
updated_at = NOW()
FROM duplicate_groups dg
WHERE f.parent_uuid = dg.parent_uuid
AND f.plain_name = dg.plain_name
AND f.id != dg.id_to_keep
AND f.deleted = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

AND f.removed = false
RETURNING f.id;
`;

let hasMore = true;

while (hasMore) {
try {
const [results] = await queryInterface.sequelize.query(renameQuery);
const renamedInBatch = results.length;
batchCount++;
totalRenamed += renamedInBatch;
attempts = 0;

console.info(
`Batch ${batchCount}: Renamed ${renamedInBatch} folders (Total: ${totalRenamed})`,
);

hasMore = renamedInBatch > 0;

if (hasMore) {
await sleep(SLEEP_TIME_MS);
}
} catch (err) {
attempts++;
console.error(
`[ERROR]: Error in batch ${batchCount} (attempt ${attempts}/${MAX_ATTEMPTS}): ${err.message}`,
);

if (attempts >= MAX_ATTEMPTS) {
console.error(
'[ERROR]: Maximum retry attempts reached, exiting migration.',
);
throw err;
}

await sleep(SLEEP_TIME_MS);
}
}

console.info('\n=== Cleanup Complete ===');
console.info(`Total batches processed: ${batchCount}`);
console.info(`Total folders renamed: ${totalRenamed}`);

console.info('Dropping supporting index...');
await queryInterface.sequelize.query(`
DROP INDEX CONCURRENTLY IF EXISTS folders_parentuuid_plainname_not_deleted_support_index;
`);
},

down: async (queryInterface) => {
await queryInterface.sequelize.query(`
DROP INDEX CONCURRENTLY IF EXISTS folders_parentuuid_plainname_not_deleted_support_index;
`);
},
};
Loading