ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

204 lines
6.2 KiB

  1. require 'set'
  2. class CreatePostVersions < ActiveRecord::Migration[8.0]
  3. class Post < ApplicationRecord
  4. self.table_name = 'posts'
  5. end
  6. class PostTag < ApplicationRecord
  7. self.table_name = 'post_tags'
  8. end
  9. class PostVersion < ApplicationRecord
  10. self.table_name = 'post_versions'
  11. end
  12. def up
  13. create_table :post_versions do |t|
  14. t.references :post, null: false, foreign_key: true
  15. t.integer :version_no, null: false
  16. t.string :event_type, null: false
  17. t.string :title
  18. t.string :url, limit: 768, null: false
  19. t.string :thumbnail_base, limit: 2000
  20. t.text :tags, null: false
  21. t.references :parent, foreign_key: { to_table: :posts }
  22. t.datetime :original_created_from
  23. t.datetime :original_created_before
  24. t.datetime :created_at, null: false
  25. t.references :created_by_user, foreign_key: { to_table: :users }
  26. t.index [:post_id, :version_no], unique: true
  27. t.check_constraint 'version_no > 0',
  28. name: 'post_versions_version_no_positive'
  29. t.check_constraint "event_type IN ('create', 'update', 'discard', 'restore')",
  30. name: 'post_versions_event_type_valid'
  31. end
  32. PostVersion.reset_column_information
  33. say_with_time 'Backfilling post_versions' do
  34. Post.find_in_batches(batch_size: 500) do |posts|
  35. post_ids = posts.map(&:id)
  36. post_tag_rows_by_post_id =
  37. PostTag
  38. .joins('INNER JOIN tags ON tags.id = post_tags.tag_id')
  39. .joins('INNER JOIN tag_names ON tag_names.id = tags.tag_name_id')
  40. .where(post_id: post_ids)
  41. .pluck('post_tags.post_id',
  42. 'post_tags.created_at',
  43. 'post_tags.discarded_at',
  44. 'post_tags.created_user_id',
  45. 'post_tags.deleted_user_id',
  46. 'tag_names.name')
  47. .each_with_object(Hash.new { |h, k| h[k] = [] }) do |row, h|
  48. post_id, created_at, discarded_at, created_user_id, deleted_user_id, tag_name = row
  49. h[post_id] << { created_at:,
  50. discarded_at:,
  51. created_user_id:,
  52. deleted_user_id:,
  53. tag_name: }
  54. end
  55. rows = []
  56. posts.each do |post|
  57. post_tag_rows = post_tag_rows_by_post_id[post.id]
  58. events = post_tag_rows.flat_map do |post_tag_row|
  59. ary = [[post_tag_row[:created_at],
  60. post_tag_row[:created_user_id],
  61. :add,
  62. post_tag_row[:tag_name]]]
  63. if post_tag_row[:discarded_at]
  64. ary << [post_tag_row[:discarded_at],
  65. post_tag_row[:deleted_user_id],
  66. :remove,
  67. post_tag_row[:tag_name]]
  68. end
  69. ary
  70. end
  71. kind_order = { add: 0, remove: 1 }
  72. events.sort_by! do |event_at, user_id, kind, tag_name|
  73. [event_at, user_id || 0, kind_order.fetch(kind), tag_name]
  74. end
  75. event_buckets = bucket_events(events)
  76. active_tags = Set.new
  77. version_no = 0
  78. if event_buckets.empty?
  79. version_no += 1
  80. rows << build_row(post:,
  81. version_no:,
  82. event_type: 'create',
  83. created_at: post.created_at,
  84. created_by_user_id: post.uploaded_user_id,
  85. tags: [])
  86. next
  87. end
  88. first_bucket = event_buckets.first
  89. merge_first_bucket_into_create = first_bucket[:first_at] <= post.created_at + 1.second
  90. if merge_first_bucket_into_create
  91. event_buckets.shift
  92. apply_bucket!(active_tags, first_bucket)
  93. version_no += 1
  94. rows << build_row(
  95. post:,
  96. version_no:,
  97. event_type: 'create',
  98. created_at: post.created_at,
  99. created_by_user_id: post.uploaded_user_id || first_bucket[:user_ids].compact.first,
  100. tags: active_tags.to_a.sort)
  101. else
  102. version_no += 1
  103. rows << build_row(
  104. post:,
  105. version_no:,
  106. event_type: 'create',
  107. created_at: post.created_at,
  108. created_by_user_id: post.uploaded_user_id,
  109. tags: [])
  110. end
  111. event_buckets.each do |bucket|
  112. apply_bucket!(active_tags, bucket)
  113. version_no += 1
  114. rows << build_row(
  115. post:,
  116. version_no:,
  117. event_type: 'update',
  118. created_at: bucket[:first_at],
  119. created_by_user_id: bucket[:user_ids].compact.first,
  120. tags: active_tags.to_a.sort)
  121. end
  122. end
  123. PostVersion.insert_all!(rows) if rows.any?
  124. end
  125. end
  126. end
  127. def down
  128. drop_table :post_versions
  129. end
  130. private
  131. def bucket_events events
  132. buckets = []
  133. events.each do |event_at, user_id, kind, tag_name|
  134. if buckets.empty? || event_at - buckets.last[:last_at] > 1.second
  135. buckets << { first_at: event_at,
  136. last_at: event_at,
  137. user_ids: [user_id],
  138. events: [[kind, tag_name]] }
  139. else
  140. bucket = buckets.last
  141. bucket[:last_at] = event_at
  142. bucket[:user_ids] << user_id
  143. bucket[:events] << [kind, tag_name]
  144. end
  145. end
  146. buckets
  147. end
  148. def apply_bucket! active_tags, bucket
  149. bucket[:events].each do |kind, tag_name|
  150. if kind == :add
  151. active_tags.add(tag_name)
  152. else
  153. active_tags.delete(tag_name)
  154. end
  155. end
  156. end
  157. def build_row post:, version_no:, event_type:, created_at:, created_by_user_id:, tags:
  158. { post_id: post.id,
  159. version_no:,
  160. event_type:,
  161. title: post.title,
  162. url: post.url,
  163. thumbnail_base: post.thumbnail_base,
  164. tags: tags.join(' '),
  165. parent_id: post.parent_id,
  166. original_created_from: post.original_created_from,
  167. original_created_before: post.original_created_before,
  168. created_at:,
  169. created_by_user_id: }
  170. end
  171. end