20251030110238_update_subscription_v1_id_index.mjs 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import Helpers from './lib/helpers.mjs'
  2. const tags = ['saas']
  3. const originalIndexes = [
  4. {
  5. key: { v1_id: 1 },
  6. name: 'v1_id_1',
  7. sparse: true,
  8. },
  9. ]
  10. const newIndexes = [
  11. {
  12. key: { v1_id: 1 },
  13. name: 'v1_id_3',
  14. unique: true,
  15. sparse: true,
  16. },
  17. ]
  18. const tempIndex = [
  19. {
  20. key: { v1_id: 1 },
  21. name: 'v1_id_temp_migration',
  22. sparse: false, // Non-sparse so it includes null/missing values
  23. },
  24. ]
  25. async function removeNullV1Ids(collection) {
  26. // Remove the v1_id field from documents where it's null
  27. const result = await collection.updateMany(
  28. { v1_id: { $type: 'null' } },
  29. { $unset: { v1_id: 1 } }
  30. )
  31. console.log(
  32. `Removed \`{ v1_id: null }\` field from ${result.modifiedCount} documents`
  33. )
  34. }
  35. async function assertNoDuplicateV1Ids(collection) {
  36. const duplicates = await collection
  37. .aggregate([
  38. { $match: { v1_id: { $exists: true, $ne: null } } },
  39. {
  40. $group: {
  41. _id: '$v1_id',
  42. count: { $sum: 1 },
  43. docs: { $push: '$_id' },
  44. },
  45. },
  46. { $match: { count: { $gt: 1 } } },
  47. ])
  48. .toArray()
  49. if (duplicates.length > 0) {
  50. const duplicateDetails = duplicates.map(dup => ({
  51. v1_id: dup._id,
  52. count: dup.count,
  53. docs: dup.docs,
  54. }))
  55. throw new Error(
  56. `Duplicate v1_id values found. Migration aborted to prevent data loss. Details: ${JSON.stringify(
  57. duplicateDetails,
  58. null,
  59. 2
  60. )}`
  61. )
  62. }
  63. }
  64. const migrate = async client => {
  65. const { db } = client
  66. // Create temporary non-sparse index to allow queries with notablescan enabled
  67. await Helpers.addIndexesToCollection(db.subscriptions, tempIndex)
  68. // pre‑check (keep old index intact if failing)
  69. try {
  70. await assertNoDuplicateV1Ids(db.subscriptions)
  71. await removeNullV1Ids(db.subscriptions)
  72. } catch (error) {
  73. await Helpers.dropIndexesFromCollection(tempIndex)
  74. throw error
  75. }
  76. await Helpers.addIndexesToCollection(db.subscriptions, newIndexes)
  77. await Helpers.dropIndexesFromCollection(
  78. db.subscriptions,
  79. originalIndexes.concat({ name: 'v1_id_2' }).concat(tempIndex)
  80. )
  81. }
  82. const rollback = async client => {
  83. const { db } = client
  84. // recreate the original non-unique sparse index
  85. await Helpers.addIndexesToCollection(db.subscriptions, originalIndexes)
  86. await Helpers.dropIndexesFromCollection(db.subscriptions, newIndexes)
  87. }
  88. export default {
  89. tags,
  90. migrate,
  91. rollback,
  92. }