fix_duplicate_versions.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. #!/usr/bin/env node
  2. 'use strict'
  3. const commandLineArgs = require('command-line-args')
  4. const { chunkStore } = require('..')
  5. main()
  6. .then(() => {
  7. process.exit(0)
  8. })
  9. .catch(err => {
  10. console.error(err)
  11. process.exit(1)
  12. })
  13. async function main() {
  14. const opts = commandLineArgs([
  15. { name: 'project-ids', type: String, multiple: true, defaultOption: true },
  16. { name: 'save', type: Boolean, defaultValue: false },
  17. { name: 'help', type: Boolean, defaultValue: false },
  18. ])
  19. if (opts.help || opts['project-ids'] == null) {
  20. console.log('Usage: fix_duplicate_versions [--save] PROJECT_ID...')
  21. process.exit()
  22. }
  23. for (const projectId of opts['project-ids']) {
  24. await processProject(projectId, opts.save)
  25. }
  26. if (!opts.save) {
  27. console.log('\nThis was a dry run. Re-run with --save to persist changes.')
  28. }
  29. }
  30. async function processProject(projectId, save) {
  31. console.log(`Project ${projectId}:`)
  32. const chunk = await chunkStore.loadLatest(projectId, { persistedOnly: true })
  33. let numChanges = 0
  34. numChanges += removeDuplicateProjectVersions(chunk)
  35. numChanges += removeDuplicateDocVersions(chunk)
  36. console.log(` ${numChanges > 0 ? numChanges : 'no'} changes`)
  37. if (save && numChanges > 0) {
  38. await replaceChunk(projectId, chunk)
  39. }
  40. }
  41. function removeDuplicateProjectVersions(chunk) {
  42. let numChanges = 0
  43. let lastVersion = null
  44. const { snapshot, changes } = chunk.history
  45. if (snapshot.projectVersion != null) {
  46. lastVersion = snapshot.projectVersion
  47. }
  48. for (const change of changes) {
  49. if (change.projectVersion == null) {
  50. // Not a project structure change. Ignore.
  51. continue
  52. }
  53. if (
  54. lastVersion != null &&
  55. !areProjectVersionsIncreasing(lastVersion, change.projectVersion)
  56. ) {
  57. // Duplicate. Remove all ops
  58. console.log(
  59. ` Removing out-of-order project structure change: ${change.projectVersion} <= ${lastVersion}`
  60. )
  61. change.setOperations([])
  62. delete change.projectVersion
  63. numChanges++
  64. } else {
  65. lastVersion = change.projectVersion
  66. }
  67. }
  68. return numChanges
  69. }
  70. function removeDuplicateDocVersions(chunk) {
  71. let numChanges = 0
  72. const lastVersions = new Map()
  73. const { snapshot, changes } = chunk.history
  74. if (snapshot.v2DocVersions != null) {
  75. for (const { pathname, v } of Object.values(snapshot.v2DocVersions.data)) {
  76. lastVersions.set(pathname, v)
  77. }
  78. }
  79. for (const change of changes) {
  80. if (change.v2DocVersions == null) {
  81. continue
  82. }
  83. // Collect all docs that have problematic versions
  84. const badPaths = []
  85. const badDocIds = []
  86. for (const [docId, { pathname, v }] of Object.entries(
  87. change.v2DocVersions.data
  88. )) {
  89. const lastVersion = lastVersions.get(docId)
  90. if (lastVersion != null && v <= lastVersion) {
  91. // Duplicate. Remove ops related to that doc
  92. console.log(
  93. ` Removing out-of-order change for doc ${docId} (${pathname}): ${v} <= ${lastVersion}`
  94. )
  95. badPaths.push(pathname)
  96. badDocIds.push(docId)
  97. numChanges++
  98. } else {
  99. lastVersions.set(docId, v)
  100. }
  101. }
  102. // Remove bad operations
  103. if (badPaths.length > 0) {
  104. change.setOperations(
  105. change.operations.filter(
  106. op => op.pathname == null || !badPaths.includes(op.pathname)
  107. )
  108. )
  109. }
  110. // Remove bad v2 doc versions
  111. for (const docId of badDocIds) {
  112. delete change.v2DocVersions.data[docId]
  113. }
  114. }
  115. return numChanges
  116. }
  117. function areProjectVersionsIncreasing(v1Str, v2Str) {
  118. const v1 = parseProjectVersion(v1Str)
  119. const v2 = parseProjectVersion(v2Str)
  120. return v2.major > v1.major || (v2.major === v1.major && v2.minor > v1.minor)
  121. }
  122. function parseProjectVersion(version) {
  123. const [major, minor] = version.split('.').map(x => parseInt(x, 10))
  124. if (isNaN(major) || isNaN(minor)) {
  125. throw new Error(`Invalid project version: ${version}`)
  126. }
  127. return { major, minor }
  128. }
  129. async function replaceChunk(projectId, chunk) {
  130. const endVersion = chunk.getEndVersion()
  131. const oldChunkId = await chunkStore.getChunkIdForVersion(
  132. projectId,
  133. endVersion
  134. )
  135. console.log(` Replacing chunk ${oldChunkId}`)
  136. // The chunks table has a unique constraint on doc_id and end_version. Because
  137. // we're replacing a chunk with the same end version, we need to destroy the
  138. // chunk first.
  139. await chunkStore.destroy(projectId, oldChunkId)
  140. await chunkStore.create(projectId, chunk)
  141. }