parent
279f20e697
commit
efe8f85f3d
@ -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…
Reference in new issue