class BackfillInitialMaterialVersions < ActiveRecord::Migration[8.0] class MigrationMaterial < ActiveRecord::Base self.table_name = 'materials' end class MigrationMaterialExportItem < ActiveRecord::Base self.table_name = 'material_export_items' end class MigrationMaterialVersion < ActiveRecord::Base self.table_name = 'material_versions' end class MigrationStorageAttachment < ActiveRecord::Base self.table_name = 'active_storage_attachments' end class MigrationStorageBlob < ActiveRecord::Base self.table_name = 'active_storage_blobs' end class MigrationTag < ActiveRecord::Base self.table_name = 'tags' end class MigrationTagName < ActiveRecord::Base self.table_name = 'tag_names' end def up say_with_time 'Backfilling initial material versions' do materials_without_versions.find_in_batches(batch_size: 500) do |materials| material_ids = materials.map(&:id) tags_by_id = load_tags(materials.map(&:tag_id).compact.uniq) export_paths_by_material_id = load_export_paths(material_ids) file_snapshots_by_material_id = load_file_snapshots(material_ids) rows = materials.map do |material| tag = tags_by_id[material.tag_id] file_snapshot = file_snapshots_by_material_id[material.id] || empty_file_snapshot { material_id: material.id, version_no: 1, event_type: 'create', url: material.url, parent_id: material.parent_id, tag_id: material.tag_id, tag_name: tag&.fetch(:name, nil), tag_category: tag&.fetch(:category, nil), source_kind: material.source_kind, source_uri: material.source_uri, source_path: material.source_path, source_file_id: material.source_file_id, normalized_source_key: material.normalized_source_key, export_paths_json: export_paths_by_material_id[material.id] || { }, discarded_at: material.discarded_at, file_blob_id: file_snapshot[:file_blob_id], file_filename: file_snapshot[:file_filename], file_content_type: file_snapshot[:file_content_type], file_byte_size: file_snapshot[:file_byte_size], file_checksum: file_snapshot[:file_checksum], file_sha256: file_snapshot[:file_sha256], created_by_user_id: material.created_by_user_id, updated_by_user_id: material.updated_by_user_id, created_at: material.created_at, updated_at: material.created_at } end MigrationMaterialVersion.insert_all!(rows) if rows.present? end end execute <<~SQL.squish UPDATE materials SET version_no = COALESCE( ( SELECT MAX(material_versions.version_no) FROM material_versions WHERE material_versions.material_id = materials.id ), 1 ) SQL end def down raise ActiveRecord::IrreversibleMigration end private def materials_without_versions MigrationMaterial.where.not(id: MigrationMaterialVersion.select(:material_id)) end def load_tags tag_ids return { } if tag_ids.empty? tag_name_ids_by_tag_id = MigrationTag.where(id: tag_ids).pluck(:id, :tag_name_id).to_h tag_names_by_id = MigrationTagName .where(id: tag_name_ids_by_tag_id.values.uniq) .pluck(:id, :name) .to_h categories_by_tag_id = MigrationTag.where(id: tag_ids).pluck(:id, :category).to_h tag_ids.each_with_object({ }) do |tag_id, hash| hash[tag_id] = { name: tag_names_by_id[tag_name_ids_by_tag_id[tag_id]], category: categories_by_tag_id[tag_id] } end end def load_export_paths material_ids MigrationMaterialExportItem .where(material_id: material_ids) .order(:material_id, :profile) .pluck(:material_id, :profile, :export_path) .each_with_object({ }) do |(material_id, profile, export_path), hash| hash[material_id] ||= { } hash[material_id][profile] = export_path end end def load_file_snapshots material_ids attachments_by_material_id = MigrationStorageAttachment .where(record_type: 'Material', name: 'file', record_id: material_ids) .pluck(:record_id, :blob_id) .to_h blobs_by_id = MigrationStorageBlob .where(id: attachments_by_material_id.values.uniq) .index_by(&:id) attachments_by_material_id.each_with_object({ }) do |(material_id, blob_id), hash| blob = blobs_by_id[blob_id] next unless blob hash[material_id] = { file_blob_id: blob.id, file_filename: blob.filename.to_s, file_content_type: blob.content_type, file_byte_size: blob.byte_size, file_checksum: blob.checksum, file_sha256: blob.metadata['sha256'] } end end def empty_file_snapshot { file_blob_id: nil, file_filename: nil, file_content_type: nil, file_byte_size: nil, file_checksum: nil, file_sha256: nil } end end