mover 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2009 Winton Welsh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,95 @@
1
+ Mover
2
+ =====
3
+
4
+ Move ActiveRecord records across tables like it ain't no thang.
5
+
6
+ Requirements
7
+ ------------
8
+
9
+ <pre>
10
+ sudo gem install mover
11
+ </pre>
12
+
13
+ <a name="create_the_movable_table"></a>
14
+
15
+ Create the movable table
16
+ ------------------------
17
+
18
+ Migration:
19
+
20
+ <pre>
21
+ class CreateArchivedArticles < ActiveRecord::Migration
22
+ def self.up
23
+ Article.create_movable_table(
24
+ :archived,
25
+ :columns => %w(id title body created_at),
26
+ :indexes => %w(id created_at)
27
+ )
28
+ add_column :archived_articles, :move_id, :string
29
+ add_column :archived_articles, :moved_at, :datetime
30
+ end
31
+
32
+ def self.down
33
+ Article.drop_movable_table(:archived)
34
+ end
35
+ end
36
+ </pre>
37
+
38
+ The first parameter names your movable table. In this example, the table is named <code>archived_articles</code>.
39
+
40
+ Options:
41
+
42
+ * <code>:columns</code> - Only use certain columns from the original table. Defaults to all.
43
+ * <code>:indexes</code> - Only create certain indexes. Defaults to all.
44
+
45
+ We also added two columns, <code>move\_id</code> and <code>moved\_at</code>. These are <a href="#magic_columns">magic columns</a>.
46
+
47
+ <a name="define_the_model"></a>
48
+
49
+ Define the model
50
+ ----------------
51
+
52
+ <pre>
53
+ class Article < ActiveRecord::Base
54
+ is_movable :archived
55
+ end
56
+ </pre>
57
+
58
+ The <code>is_movable</code> method takes any number of parameters for multiple movable tables.
59
+
60
+ Moving records
61
+ --------------
62
+
63
+ <pre>
64
+ Article.last.move_to(:archived)
65
+ Article.move_to(:archived, [ "created_at > ?", Date.today ])
66
+ </pre>
67
+
68
+ Associations move if they are movable and if all movable tables have a <code>move_id</code> column (see <a href="#magic_columns">magic columns</a>).
69
+
70
+ Restoring records
71
+ -----------------
72
+
73
+ <pre>
74
+ Article.move_from(:archived, [ "created_at > ?", Date.today ])
75
+ ArchivedArticle.last.move_from
76
+ </pre>
77
+
78
+ You can access the movable table by prepending its name to the original class name. In this example, you would use <code>ArchivedArticle</code>.
79
+
80
+ <a name="magic_columns"></a>
81
+
82
+ Magic columns
83
+ -------------
84
+
85
+ ### move_id
86
+
87
+ By default, restoring a record will only restore itself and not its movable relationships.
88
+
89
+ To restore the relationships automatically, add the <code>move_id</code> column to all movable tables involved.
90
+
91
+ ### moved_at
92
+
93
+ If you need to know when the record was moved, add the <code>moved\_at</code> column to your movable table.
94
+
95
+ See the <a href="#create_the_movable_table">create the movable table</a> section for an example of how to add the magic columns.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "#{File.dirname(__FILE__)}/require"
2
+ Require.rakefile!
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,43 @@
1
+ module Mover
2
+ module Migrator
3
+
4
+ def method_missing_with_mover(method, *arguments, &block)
5
+ args = Marshal.load(Marshal.dump(arguments))
6
+ method_missing_without_mover(method, *arguments, &block)
7
+ supported = [
8
+ :add_column, :add_timestamps, :change_column,
9
+ :change_column_default, :change_table,
10
+ :drop_table, :remove_column, :remove_columns,
11
+ :remove_timestamps, :rename_column, :rename_table
12
+ ]
13
+
14
+ # Don't change these columns
15
+ %w(moved_at move_id).each do |column|
16
+ return if args.include?(column) || args.include?(column.intern)
17
+ end
18
+
19
+ if !args.empty? && supported.include?(method)
20
+ connection = ActiveRecord::Base.connection
21
+ table_name = ActiveRecord::Migrator.proper_table_name(args[0])
22
+ # Find model
23
+ klass = Object.subclasses_of(ActiveRecord::Base).detect do |klass|
24
+ if klass.respond_to?(:movable_types)
25
+ klass.table_name.to_s == table_name
26
+ end
27
+ end
28
+ # Run migration on movable table
29
+ if klass
30
+ klass.movable_types.each do |type|
31
+ args[0] = [ type, table_name ].join('_')
32
+ if method == :rename_table
33
+ args[1] = [ type, args[1].to_s ].join('_')
34
+ end
35
+ if connection.table_exists?(args[0])
36
+ connection.send(method, *args, &block)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,114 @@
1
+ module Mover
2
+ module Base
3
+ module Record
4
+ module ClassMethods
5
+
6
+ def move_from(type, conditions)
7
+ klass = movable_class(type)
8
+ if klass
9
+ if klass.column_names.include?('move_id')
10
+ klass.find_each(:conditions => conditions) do |record|
11
+ record.move_from
12
+ end
13
+ else
14
+ execute_move(klass, self, conditions)
15
+ end
16
+ end
17
+ end
18
+
19
+ def move_to(type, conditions)
20
+ if movable_class(type).column_names.include?('move_id')
21
+ self.find_each(:conditions => conditions) do |record|
22
+ record.move_to(type)
23
+ end
24
+ else
25
+ execute_move(self, movable_class(type), conditions)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def execute_move(from_class, to_class, conditions, &block)
32
+ add_conditions! where = '', conditions
33
+ insert = from_class.column_names & to_class.column_names
34
+ insert.collect! { |col| connection.quote_column_name(col) }
35
+ select = insert.clone
36
+ yield(insert, select) if block_given?
37
+ if to_class.column_names.include?('moved_at')
38
+ insert << connection.quote_column_name('moved_at')
39
+ select << connection.quote(Time.now)
40
+ end
41
+ connection.execute(<<-SQL)
42
+ INSERT INTO #{to_class.table_name} (#{insert.join(', ')})
43
+ SELECT #{select.join(', ')}
44
+ FROM #{from_class.table_name}
45
+ #{where}
46
+ SQL
47
+ connection.execute("DELETE FROM #{from_class.table_name} #{where}")
48
+ end
49
+
50
+ def movable_class(type)
51
+ eval(type.to_s.classify + self.table_name.classify)
52
+ rescue
53
+ raise "#{self.table_name.classify} needs an `is_movable :#{type}` definition"
54
+ end
55
+ end
56
+
57
+ module InstanceMethods
58
+
59
+ def move_from
60
+ return unless self.respond_to?(:moved_from_class)
61
+ # Move associations
62
+ moved_from_class.reflect_on_all_associations.each do |association|
63
+ if move_association?(association)
64
+ klass = association.klass.send(:movable_class, self.class.movable_type)
65
+ klass.find_each(:conditions => [ 'move_id = ?', self.move_id ]) do |record|
66
+ record.move_from
67
+ end
68
+ end
69
+ end
70
+ # Move record
71
+ conditions = "#{self.class.primary_key} = #{id}"
72
+ moved_from_class.send(:execute_move, self.class, moved_from_class, conditions)
73
+ end
74
+
75
+ def move_to(type)
76
+ return if self.respond_to?(:moved_from_class)
77
+ klass = self.class.send :movable_class, type
78
+ if klass
79
+ # Create movable_id
80
+ if !self.movable_id && klass.column_names.include?('move_id')
81
+ self.movable_id = Digest::MD5.hexdigest("#{self.class.name}#{self.id}")
82
+ end
83
+ # Move associations
84
+ self.class.reflect_on_all_associations.each do |association|
85
+ if move_association?(association)
86
+ self.send(association.name).each do |record|
87
+ record.movable_id = self.movable_id
88
+ record.move_to(type)
89
+ end
90
+ end
91
+ end
92
+ # Move record
93
+ me = self
94
+ conditions = "#{self.class.primary_key} = #{id}"
95
+ self.class.send(:execute_move, self.class, klass, conditions) do |insert, select|
96
+ if me.movable_id
97
+ insert << connection.quote_column_name('move_id')
98
+ select << connection.quote(self.movable_id)
99
+ end
100
+ end
101
+ self.movable_id = nil
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def move_association?(association)
108
+ association.klass.respond_to?(:movable_types) &&
109
+ association.macro.to_s =~ /^has/
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,82 @@
1
+ module Mover
2
+ module Base
3
+ module Table
4
+
5
+ def create_movable_table(type, options={})
6
+ movable_table = [ type, table_name ].join('_')
7
+ columns =
8
+ if options[:columns]
9
+ options[:columns].collect { |c| "`#{c}`" }.join(', ')
10
+ else
11
+ '*'
12
+ end
13
+ engine = options[:engine]
14
+ engine ||=
15
+ if connection.class.to_s.include?('Mysql')
16
+ "ENGINE=InnoDB"
17
+ end
18
+ if table_exists? and !connection.table_exists?(movable_table)
19
+ # Create table
20
+ connection.execute(<<-SQL)
21
+ CREATE TABLE #{movable_table} #{engine}
22
+ AS SELECT #{columns}
23
+ FROM #{table_name}
24
+ WHERE false;
25
+ SQL
26
+ # Create indexes
27
+ options[:indexes] ||= indexed_columns(table_name)
28
+ options[:indexes].each do |column|
29
+ connection.add_index(movable_table, column)
30
+ end
31
+ end
32
+ end
33
+
34
+ def drop_movable_table(*types)
35
+ types.each do |type|
36
+ connection.execute("DROP TABLE IF EXISTS #{[ type, table_name ].join('_')}")
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def indexed_columns(table_name)
43
+ # MySQL
44
+ if connection.class.to_s.include?('Mysql')
45
+ index_query = "SHOW INDEX FROM #{table_name}"
46
+ indexes = connection.select_all(index_query).collect do |r|
47
+ r["Column_name"]
48
+ end
49
+ # PostgreSQL
50
+ # http://stackoverflow.com/questions/2204058/show-which-columns-an-index-is-on-in-postgresql/2213199
51
+ elsif connection.class.to_s.include?('PostgreSQL')
52
+ index_query = <<-SQL
53
+ select
54
+ t.relname as table_name,
55
+ i.relname as index_name,
56
+ a.attname as column_name
57
+ from
58
+ pg_class t,
59
+ pg_class i,
60
+ pg_index ix,
61
+ pg_attribute a
62
+ where
63
+ t.oid = ix.indrelid
64
+ and i.oid = ix.indexrelid
65
+ and a.attrelid = t.oid
66
+ and a.attnum = ANY(ix.indkey)
67
+ and t.relkind = 'r'
68
+ and t.relname = '#{table_name}'
69
+ order by
70
+ t.relname,
71
+ i.relname
72
+ SQL
73
+ indexes = connection.select_all(index_query).collect do |r|
74
+ r["column_name"]
75
+ end
76
+ else
77
+ raise 'Mover does not support this database adapter'
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
data/lib/mover.rb ADDED
@@ -0,0 +1,69 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
+ Require.lib!
3
+
4
+ module Mover
5
+ module Base
6
+ def self.included(base)
7
+ unless base.included_modules.include?(Included)
8
+ base.extend ClassMethods
9
+ base.send :include, Included
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def is_movable(*types)
15
+ @movable_types = types
16
+
17
+ self.class_eval do
18
+ attr_accessor :movable_id
19
+ class <<self
20
+ attr_reader :movable_types
21
+ end
22
+ end
23
+
24
+ types.each do |type|
25
+ eval <<-RUBY
26
+ class ::#{type.to_s.classify}#{self.table_name.classify} < ActiveRecord::Base
27
+ include Mover::Base::Record::InstanceMethods
28
+
29
+ self.table_name = "#{type}_#{self.table_name}"
30
+
31
+ def self.movable_type
32
+ #{type.inspect}
33
+ end
34
+
35
+ def moved_from_class
36
+ #{self.table_name.classify}
37
+ end
38
+ end
39
+ RUBY
40
+ end
41
+
42
+ extend Table
43
+ extend Record::ClassMethods
44
+ include Record::InstanceMethods
45
+ end
46
+ end
47
+ end
48
+
49
+ module Migration
50
+ def self.included(base)
51
+ unless base.included_modules.include?(Included)
52
+ base.extend Migrator
53
+ base.send :include, Included
54
+ base.class_eval do
55
+ class <<self
56
+ alias_method :method_missing_without_mover, :method_missing
57
+ alias_method :method_missing, :method_missing_with_mover
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ module Included
65
+ end
66
+ end
67
+
68
+ ActiveRecord::Base.send(:include, Mover::Base)
69
+ ActiveRecord::Migration.send(:include, Mover::Migration)
@@ -0,0 +1,6 @@
1
+ SQL (0.2ms) SET SQL_AUTO_IS_NULL=0
2
+ User Load (59.6ms) SELECT * FROM `users` ORDER BY users.id DESC LIMIT 1
3
+ User Columns (21.6ms) SHOW FIELDS FROM `users`
4
+ SQL (0.3ms) SET SQL_AUTO_IS_NULL=0
5
+ SQL (0.3ms) SHOW TABLES
6
+ User Columns (13.0ms) SHOW FIELDS FROM `users`
data/rails/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
+ Require.rails_init!
data/require.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ gem 'require'
3
+ require 'require'
4
+
5
+ Require do
6
+ gem(:active_wrapper, '=0.2.3') { require 'active_wrapper' }
7
+ gem :require, '=0.2.6'
8
+ gem(:rake, '=0.8.7') { require 'rake' }
9
+ gem :rspec, '=1.3.0'
10
+
11
+ gemspec do
12
+ author 'Winton Welsh'
13
+ dependencies do
14
+ gem :require
15
+ end
16
+ email 'mail@wintoni.us'
17
+ name 'mover'
18
+ homepage "http://github.com/winton/#{name}"
19
+ summary "Move ActiveRecord records across tables like it ain't no thang"
20
+ version '0.1.0'
21
+ end
22
+
23
+ bin { require 'lib/mover' }
24
+
25
+ lib do
26
+ require 'digest/md5'
27
+ require 'lib/mover/migrator'
28
+ require 'lib/mover/record'
29
+ require 'lib/mover/table'
30
+ end
31
+
32
+ rakefile do
33
+ gem(:active_wrapper)
34
+ gem(:rake) { require 'rake/gempackagetask' }
35
+ gem(:rspec) { require 'spec/rake/spectask' }
36
+ require 'require/tasks'
37
+ end
38
+
39
+ rails_init { require 'lib/mover' }
40
+
41
+ spec_helper do
42
+ gem(:active_wrapper)
43
+ require 'require/spec_helper'
44
+ require 'rails/init'
45
+ require 'pp'
46
+ require 'spec/fixtures/article'
47
+ require 'spec/fixtures/comment'
48
+ end
49
+ end
data/spec/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'active_wrapper/tasks'
6
+
7
+ ActiveWrapper::Tasks.new(
8
+ :base => File.dirname(__FILE__),
9
+ :env => ENV['ENV']
10
+ )
11
+ rescue Exception
12
+ end
@@ -0,0 +1,6 @@
1
+ test:
2
+ adapter: mysql
3
+ database: mover
4
+ username: root
5
+ password:
6
+ host: localhost
@@ -0,0 +1,34 @@
1
+ class CreateArticles < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :articles do |t|
4
+ t.string :title
5
+ t.string :body
6
+ t.boolean :read
7
+ end
8
+ add_index :articles, :title
9
+
10
+ Article.create_movable_table(:archived)
11
+ add_column :archived_articles, :move_id, :string
12
+ add_column :archived_articles, :moved_at, :datetime
13
+
14
+ Article.create_movable_table(:drafted)
15
+
16
+ create_table :comments do |t|
17
+ t.string :title
18
+ t.string :body
19
+ t.boolean :read
20
+ t.integer :article_id
21
+ end
22
+
23
+ Comment.create_movable_table(:archived)
24
+ add_column :archived_comments, :move_id, :string
25
+ add_column :archived_comments, :moved_at, :datetime
26
+ end
27
+
28
+ def self.down
29
+ drop_table :articles
30
+ drop_table :comments
31
+ Article.drop_movable_table(:archived)
32
+ Comment.drop_movable_table(:archived)
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ class AddPermalink < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :articles, :permalink, :string
4
+ end
5
+
6
+ def self.down
7
+ remove_column :articles, :permalink
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class RemoveMagicColumns < ActiveRecord::Migration
2
+ def self.up
3
+ remove_column :articles, :move_id
4
+ remove_column :articles, :moved_at
5
+ end
6
+
7
+ def self.down
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ class Article < ActiveRecord::Base
2
+ has_many :comments
3
+ is_movable :archived, :drafted
4
+ end
@@ -0,0 +1,4 @@
1
+ class Comment < ActiveRecord::Base
2
+ belongs_to :article
3
+ is_movable :archived
4
+ end