mover 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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