extracts journal migrator

pull/374/head
Jens Ulferts 11 years ago
parent 279f20e697
commit efe8f85f3d
  1. 298
      db/migrate/20130919132104_awesome_legacy_journals.rb
  2. 271
      db/migrate/migration_utils/legacy_journal_migrator.rb

@ -70,7 +70,6 @@ class AwesomeLegacyJournals < ActiveRecord::Migration
private
def ignored
@ignored ||= Hash.new do |k, v|
0
@ -97,7 +96,7 @@ class AwesomeLegacyJournals < ActiveRecord::Migration
end
def attachment_migrator
LegacyJournalMigrator.new("AttachmentJournal", "attachment_journals") do
Migration::LegacyJournalMigrator.new("AttachmentJournal", "attachment_journals") do
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
@ -116,16 +115,16 @@ class AwesomeLegacyJournals < ActiveRecord::Migration
end
def changesets_migrator
LegacyJournalMigrator.new("ChangesetJournal", "changeset_journals")
Migration::LegacyJournalMigrator.new("ChangesetJournal", "changeset_journals")
end
def news_migrator
LegacyJournalMigrator.new("NewsJournal", "news_journals")
Migration::LegacyJournalMigrator.new("NewsJournal", "news_journals")
end
def message_migrator
LegacyJournalMigrator.new("MessageJournal", "message_journals") do
extend Migrator::JournalMigratorConcern::Attachable
Migration::LegacyJournalMigrator.new("MessageJournal", "message_journals") do
extend Migraton::JournalMigratorConcern::Attachable
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
@ -136,7 +135,22 @@ class AwesomeLegacyJournals < ActiveRecord::Migration
end
def work_package_migrator
LegacyJournalMigrator.new "WorkPackageJournal", "work_package_journals" do
Migration::LegacyJournalMigrator.new "WorkPackageJournal", "work_package_journals" do
extend Migration::JournalMigratorConcern::Attachable
extend Migration::JournalMigratorConcern::Customizable
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
migrate_attachments(to_insert, legacy_journal, journal_id)
migrate_custom_values(to_insert, legacy_journal, journal_id)
end
end
end
def planning_element_migrator
Migration::LegacyJournalMigrator.new "Timelines_PlanningElementJournal", "work_package_journals" do
extend Migration::JournalMigratorConcern::Attachable
extend Migration::JournalMigratorConcern::Customizable
@ -147,12 +161,34 @@ class AwesomeLegacyJournals < ActiveRecord::Migration
migrate_custom_values(to_insert, legacy_journal, journal_id)
end
private
def new_journaled_id_for_old(old_journaled_id)
# We should be able to keep that in memory
@new_journaled_ids ||= begin
old_new = select_all <<-SQL
SELECT journaled_id AS old_id, new_id
FROM legacy_journals
LEFT JOIN legacy_planning_elements
ON legacy_journals.journaled_id = legacy_planning_elements.id
WHERE type = 'Timelines_PlanningElementJournal'
SQL
old_new.inject({}) do |mem, entry|
mem[entry['old_id']] = entry['new_id']
mem
end
end
@new_journaled_ids[old_journaled_id]
end
end
end
def time_entry_migrator
LegacyJournalMigrator.new("TimeEntryJournal", "time_entry_journals") do
extend Migrator::JournalMigratorConcern::Customizable
Migration::LegacyJournalMigrator.new("TimeEntryJournal", "time_entry_journals") do
extend Migraton::JournalMigratorConcern::Customizable
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
@ -164,7 +200,7 @@ class AwesomeLegacyJournals < ActiveRecord::Migration
def wiki_content_migrator
LegacyJournalMigrator.new("WikiContentJournal", "wiki_content_journals") do
Migration::LegacyJournalMigrator.new("WikiContentJournal", "wiki_content_journals") do
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
@ -275,246 +311,4 @@ class AwesomeLegacyJournals < ActiveRecord::Migration
end
include Migration::DbWorker
class LegacyJournalMigrator
include Migration::DbWorker
attr_accessor :table_name,
:type,
:journable_class
def initialize(type, table_name, &block)
self.table_name = table_name
self.type = type
instance_eval &block if block_given?
if table_name.nil? || type.nil?
raise ArgumentError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
table_name and type have to be provided. Either as parameters or set within the block.
MESSAGE
end
self.journable_class = self.type.gsub(/Journal$/, "")
end
def migrate(legacy_journal)
journal = set_journal(legacy_journal)
journal_id = journal["id"]
set_journal_data(journal_id, legacy_journal)
end
protected
def combine_journal(journaled_id, legacy_journal)
# compute the combined journal from current and all previous changesets.
combined_journal = legacy_journal["changed_data"]
if previous.journaled_id == journaled_id
combined_journal = previous.journal.merge(combined_journal)
end
# remember the combined journal as the previous one for the next iteration.
previous.set(combined_journal, journaled_id)
combined_journal
end
def previous
@previous ||= PreviousState.new({}, 0)
end
# here to be overwritten by instances
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id) end
# fetches specific journal data row. might be empty.
def fetch_existing_data_journal(journal_id)
ActiveRecord::Base.connection.select_all <<-SQL
SELECT *
FROM #{journal_table_name} AS d
WHERE d.journal_id = #{quote_value(journal_id)};
SQL
end
# gets a journal row, and makes sure it has a valid id in the database.
# if the journal does not exist, it creates it
def set_journal(legacy_journal)
journal = fetch_journal(legacy_journal)
if journal.size > 1
raise AmbiguousJournalsError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are ambiguous journals. Please make sure
journals are consistent and that the unique constraint on id,
type and version is met.
MESSAGE
elsif journal.size == 0
journal = create_journal(legacy_journal)
end
journal.first
end
# fetches specific journal row. might be empty.
def fetch_journal(legacy_journal)
id, version = legacy_journal["journaled_id"], legacy_journal["version"]
db_select_all <<-SQL
SELECT *
FROM #{quoted_journals_table_name} AS j
WHERE j.journable_id = #{quote_value(id)}
AND j.journable_type = #{quote_value(journable_class)}
AND j.version = #{quote_value(version)};
SQL
end
# creates a valid journal.
# But might be not what is desired as an end result, yet. It is e.g.
# created with created_at set to now. This will need to be set to an actual
# date
def create_journal(legacy_journal)
db_execute <<-SQL
INSERT INTO #{quoted_journals_table_name} (
id,
journable_id,
version,
user_id,
notes,
activity_type,
created_at,
journable_type
)
VALUES (
#{quote_value(legacy_journal["id"])},
#{quote_value(legacy_journal["journaled_id"])},
#{quote_value(legacy_journal["version"])},
#{quote_value(legacy_journal["user_id"])},
#{quote_value(legacy_journal["notes"])},
#{quote_value(legacy_journal["activity_type"])},
#{quote_value(legacy_journal["created_at"])},
#{quote_value(journable_class)}
);
SQL
fetch_journal(legacy_journal)
end
def set_journal_data(journal_id, legacy_journal)
deserialize_journal(legacy_journal)
journaled_id = legacy_journal["journaled_id"]
combined_journal = combine_journal(journaled_id, legacy_journal)
migrate_key_value_pairs!(combined_journal, legacy_journal, journal_id)
to_insert = insertable_data_journal(combined_journal)
existing_data_journal = fetch_existing_data_journal(journal_id)
if existing_data_journal.size > 1
raise AmbiguousJournalsError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are ambiguous journal data. Please make sure
journal data are consistent and that the unique constraint on
journal_id is met.
MESSAGE
elsif existing_data_journal.size == 0
existing_data_journal = create_data_journal(journal_id, to_insert)
end
existing_data_journal = existing_data_journal.first
update_data_journal(existing_data_journal["id"], to_insert)
end
def create_data_journal(journal_id, to_insert)
keys = to_insert.keys
values = to_insert.values
db_execute <<-SQL
INSERT INTO #{journal_table_name} (journal_id#{", " + keys.join(", ") unless keys.empty? })
VALUES (#{quote_value(journal_id)}#{", " + values.map{|d| quote_value(d)}.join(", ") unless values.empty?});
SQL
fetch_existing_data_journal(journal_id)
end
def update_data_journal(id, to_insert)
db_execute <<-SQL unless to_insert.empty?
UPDATE #{journal_table_name}
SET #{(to_insert.each.map { |key,value| "#{key} = #{quote_value(value)}"}).join(", ") }
WHERE id = #{id};
SQL
end
def deserialize_journal(journal)
integerize_ids(journal)
journal["changed_data"] = YAML.load(journal["changed_data"])
end
def insertable_data_journal(journal)
journal.inject({}) do |mem, (key, value)|
current_key = map_key(key)
if column_names.include?(current_key)
# The old journal's values attribute was structured like
# [old_value, new_value]
# We only need the new_value
mem[current_key] = value.last
end
mem
end
end
def map_key(key)
case key
when "issue_id"
"work_package_id"
when "tracker_id"
"type_id"
when "end_date"
"due_date"
else
key
end
end
def integerize_ids(journal)
# turn id fields into integers.
["id", "journaled_id", "user_id", "version"].each do |f|
journal[f] = journal[f].to_i
end
end
def journal_table_name
quoted_table_name(table_name)
end
def quoted_journals_table_name
@quoted_journals_table_name ||= quoted_table_name 'journals'
end
def column_names
@column_names ||= db_columns(table_name).map(&:name)
end
end
class PreviousState < Struct.new(:journal, :journaled_id)
def set(journal, journaled_id)
self.journal = journal
self.journaled_id = journaled_id
end
end
end

@ -0,0 +1,271 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module Migration
class LegacyJournalMigrator
include Migration::DbWorker
attr_accessor :table_name,
:type,
:journable_class
def initialize(type, table_name, &block)
self.table_name = table_name
self.type = type
instance_eval &block if block_given?
if table_name.nil? || type.nil?
raise ArgumentError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
table_name and type have to be provided. Either as parameters or set within the block.
MESSAGE
end
self.journable_class = self.type.gsub(/Journal$/, "")
end
def migrate(legacy_journal)
journal = set_journal(legacy_journal)
journal_id = journal["id"]
set_journal_data(journal_id, legacy_journal)
end
protected
def combine_journal(journaled_id, legacy_journal)
# compute the combined journal from current and all previous changesets.
combined_journal = legacy_journal["changed_data"]
if previous.journaled_id == journaled_id
combined_journal = previous.journal.merge(combined_journal)
end
# remember the combined journal as the previous one for the next iteration.
previous.set(combined_journal, journaled_id)
combined_journal
end
def previous
@previous ||= PreviousState.new({}, 0)
end
# here to be overwritten by instances
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id) end
# fetches specific journal data row. might be empty.
def fetch_existing_data_journal(journal_id)
ActiveRecord::Base.connection.select_all <<-SQL
SELECT *
FROM #{journal_table_name} AS d
WHERE d.journal_id = #{quote_value(journal_id)};
SQL
end
# gets a journal row, and makes sure it has a valid id in the database.
# if the journal does not exist, it creates it
def set_journal(legacy_journal)
journal = fetch_journal(legacy_journal)
if journal.size > 1
raise AmbiguousJournalsError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are ambiguous journals. Please make sure
journals are consistent and that the unique constraint on id,
type and version is met.
MESSAGE
elsif journal.size == 0
journal = create_journal(legacy_journal)
end
journal.first
end
# fetches specific journal row. might be empty.
def fetch_journal(legacy_journal)
id, version = legacy_journal["journaled_id"], legacy_journal["version"]
db_select_all <<-SQL
SELECT *
FROM #{quoted_journals_table_name} AS j
WHERE j.journable_id = #{quote_value(id)}
AND j.journable_type = #{quote_value(journable_class)}
AND j.version = #{quote_value(version)};
SQL
end
# creates a valid journal.
# But might be not what is desired as an end result, yet. It is e.g.
# created with created_at set to now. This will need to be set to an actual
# date
def create_journal(legacy_journal)
db_execute <<-SQL
INSERT INTO #{quoted_journals_table_name} (
id,
journable_id,
version,
user_id,
notes,
activity_type,
created_at,
journable_type
)
VALUES (
#{quote_value(legacy_journal["id"])},
#{quote_value(legacy_journal["journaled_id"])},
#{quote_value(legacy_journal["version"])},
#{quote_value(legacy_journal["user_id"])},
#{quote_value(legacy_journal["notes"])},
#{quote_value(legacy_journal["activity_type"])},
#{quote_value(legacy_journal["created_at"])},
#{quote_value(journable_class)}
);
SQL
fetch_journal(legacy_journal)
end
def set_journal_data(journal_id, legacy_journal)
deserialize_journal(legacy_journal)
journaled_id = legacy_journal["journaled_id"]
combined_journal = combine_journal(journaled_id, legacy_journal)
migrate_key_value_pairs!(combined_journal, legacy_journal, journal_id)
to_insert = insertable_data_journal(combined_journal)
existing_data_journal = fetch_existing_data_journal(journal_id)
if existing_data_journal.size > 1
raise AmbiguousJournalsError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are ambiguous journal data. Please make sure
journal data are consistent and that the unique constraint on
journal_id is met.
MESSAGE
elsif existing_data_journal.size == 0
existing_data_journal = create_data_journal(journal_id, to_insert)
end
existing_data_journal = existing_data_journal.first
update_data_journal(existing_data_journal["id"], to_insert)
end
def create_data_journal(journal_id, to_insert)
keys = to_insert.keys
values = to_insert.values
db_execute <<-SQL
INSERT INTO #{journal_table_name} (journal_id#{", " + keys.join(", ") unless keys.empty? })
VALUES (#{quote_value(journal_id)}#{", " + values.map{|d| quote_value(d)}.join(", ") unless values.empty?});
SQL
fetch_existing_data_journal(journal_id)
end
def update_data_journal(id, to_insert)
db_execute <<-SQL unless to_insert.empty?
UPDATE #{journal_table_name}
SET #{(to_insert.each.map { |key,value| "#{key} = #{quote_value(value)}"}).join(", ") }
WHERE id = #{id};
SQL
end
def deserialize_journal(journal)
integerize_ids(journal)
journal["changed_data"] = YAML.load(journal["changed_data"])
end
def insertable_data_journal(journal)
journal.inject({}) do |mem, (key, value)|
current_key = map_key(key)
if column_names.include?(current_key)
# The old journal's values attribute was structured like
# [old_value, new_value]
# We only need the new_value
mem[current_key] = value.last
end
mem
end
end
def map_key(key)
case key
when "issue_id"
"work_package_id"
when "tracker_id"
"type_id"
when "end_date"
"due_date"
else
key
end
end
def integerize_ids(journal)
# turn id fields into integers.
["id", "journaled_id", "user_id", "version"].each do |f|
journal[f] = journal[f].to_i
end
end
def journal_table_name
quoted_table_name(table_name)
end
def quoted_journals_table_name
@quoted_journals_table_name ||= quoted_table_name 'journals'
end
def column_names
@column_names ||= db_columns(table_name).map(&:name)
end
end
class PreviousState < Struct.new(:journal, :journaled_id)
def set(journal, journaled_id)
self.journal = journal
self.journaled_id = journaled_id
end
end
end
Loading…
Cancel
Save