mover 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Winton Welsh
1
+ Copyright (c) 2010 Winton Welsh
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the "Software"), to deal in
@@ -15,4 +15,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
15
  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
16
  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
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.
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
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
+ Move records
14
+ ------------
15
+
16
+ Move the last article:
17
+
18
+ <pre>
19
+ Article.last.move_to(ArticleArchive)
20
+ </pre>
21
+
22
+ Move today's articles:
23
+
24
+ <pre>
25
+ Article.move_to(
26
+ ArticleArchive,
27
+ :conditions => [ "created_at > ?", Date.today ]
28
+ )
29
+ </pre>
30
+
31
+ The two tables do not have to be identical. Only shared columns transfer.
32
+
33
+ If a primary key collision occurs, the destination record is updated.
34
+
35
+ Callbacks
36
+ ---------
37
+
38
+ In this example, we want an "archive" table for articles and comments.
39
+
40
+ We also want the article's comments to be archived when the article is.
41
+
42
+ <pre>
43
+ class Article &lt; ActiveRecord::Base
44
+ has_many :comments
45
+ before_move :ArticleArchive do
46
+ comments.each { |c| c.move_to(CommentArchive) }
47
+ end
48
+ end
49
+
50
+ class ArticleArchive &lt; ActiveRecord::Base
51
+ has_many :comments, :class_name => 'CommentArchive', :foreign_key => 'article_id'
52
+ before_move :Article do
53
+ comments.each { |c| c.move_to(Comment) }
54
+ end
55
+ end
56
+
57
+ class Comment &lt; ActiveRecord::Base
58
+ belongs_to :article
59
+ end
60
+
61
+ class CommentArchive &lt; ActiveRecord::Base
62
+ belongs_to :article, :class_name => 'ArticleArchive', :foreign_key => 'article_id'
63
+ end
64
+ </pre>
65
+
66
+ The <code>after\_move</code> callback is also available.
67
+
68
+ Magic column
69
+ ------------
70
+
71
+ If a table contains a <code>moved_at</code> column, it will magically populate with the date and time it was moved.
72
+
73
+ Options
74
+ -------
75
+
76
+ There are other options, in addition to <code>conditions</code>:
77
+
78
+ <pre>
79
+ Article.move_to(
80
+ ArticleArchive,
81
+ :copy => true, # Do not delete Article after move
82
+ :generic => true, # UPDATE using a JOIN instead of ON DUPLICATE KEY UPDATE (default on non-MySQL engines)
83
+ :magic => 'updated_at', # Custom magic column
84
+ :quick => true # You are certain only INSERTs are necessary, no primary key collisions possible
85
+ # May only be a little faster on MySQL, but dramatically faster on other engines
86
+ )
87
+ </pre>
88
+
89
+ You can access these options from callbacks using <code>move_options</code>.
90
+
91
+ Reserve a spot
92
+ --------------
93
+
94
+ Before you create a record, you can "reserve a spot" on a table that you will move the record to later.
95
+
96
+ <pre>
97
+ archive = ArticleArchive.new
98
+ archive.id = Article.reserve_id
99
+ archive.save
100
+ </pre>
data/lib/mover/gems.rb ADDED
@@ -0,0 +1,43 @@
1
+ unless defined?(Mover::Gems)
2
+
3
+ require 'rubygems'
4
+
5
+ module Mover
6
+ class Gems
7
+
8
+ VERSIONS = {
9
+ :active_wrapper => '=0.3.4',
10
+ :rake => '=0.8.7',
11
+ :rspec => '=1.3.1'
12
+ }
13
+
14
+ TYPES = {
15
+ :gemspec => [],
16
+ :gemspec_dev => [ :active_wrapper, :rspec ],
17
+ :lib => [],
18
+ :rake => [ :rake, :rspec ],
19
+ :spec => [ :active_wrapper, :rspec ]
20
+ }
21
+
22
+ class <<self
23
+
24
+ def lockfile
25
+ file = File.expand_path('../../../gems', __FILE__)
26
+ unless File.exists?(file)
27
+ File.open(file, 'w') do |f|
28
+ Gem.loaded_specs.each do |key, value|
29
+ f.puts "#{key} #{value.version.version}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def require(type=nil)
36
+ (TYPES[type] || TYPES.values.flatten.compact).each do |name|
37
+ gem name.to_s, VERSIONS[name]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module Mover
2
+ VERSION = "0.3.0" unless defined?(::Mover::VERSION)
3
+ end
data/lib/mover.rb CHANGED
@@ -1,11 +1,18 @@
1
- require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
- Require.lib!
1
+ require File.dirname(__FILE__) + '/mover/gems'
2
+
3
+ Mover::Gems.require(:lib)
4
+
5
+ $:.unshift File.dirname(__FILE__) + '/mover'
6
+
7
+ require 'version'
3
8
 
4
9
  module Mover
10
+
5
11
  def self.included(base)
6
12
  unless base.included_modules.include?(InstanceMethods)
7
13
  base.extend ClassMethods
8
14
  base.send :include, InstanceMethods
15
+ base.send :attr_accessor, :move_options
9
16
  end
10
17
  end
11
18
 
@@ -20,117 +27,121 @@ module Mover
20
27
  @before_move ||= []
21
28
  @before_move << [ to_class, block ]
22
29
  end
23
-
24
- def after_copy(*to_class, &block)
25
- @after_copy ||= []
26
- @after_copy << [ to_class, block ]
27
- end
28
-
29
- def before_copy(*to_class, &block)
30
- @before_copy ||= []
31
- @before_copy << [ to_class, block ]
32
- end
33
30
 
34
- def move_to(to_class, conditions, instance=nil, copy = false)
31
+ def move_to(to_class, options={})
35
32
  from_class = self
33
+
36
34
  # Conditions
37
- add_conditions! where = '', conditions
35
+ conditions = options[:conditions] || '1'
36
+ conditions = self.sanitize_sql(conditions)
37
+ where = "WHERE #{conditions}"
38
+
38
39
  # Columns
40
+ magic = options[:magic] || 'moved_at'
39
41
  insert = from_class.column_names & to_class.column_names
40
- insert -= [ 'moved_at' ]
42
+ insert -= [ magic ]
41
43
  insert.collect! { |col| connection.quote_column_name(col) }
42
44
  select = insert.clone
45
+
43
46
  # Magic columns
44
- if to_class.column_names.include?('moved_at')
45
- insert << connection.quote_column_name('moved_at')
47
+ if to_class.column_names.include?(magic)
48
+ insert << connection.quote_column_name(magic)
46
49
  select << connection.quote(Time.now.utc)
47
50
  end
51
+
48
52
  # Callbacks
49
53
  collector = lambda do |(classes, block)|
50
54
  classes.collect! { |c| eval(c.to_s) }
51
55
  block if classes.include?(to_class) || classes.empty?
52
56
  end
53
- if copy
54
- before = (@before_copy || []).collect(&collector).compact
55
- after = (@after_copy || []).collect(&collector).compact
56
- else # move
57
- before = (@before_move || []).collect(&collector).compact
58
- after = (@after_move || []).collect(&collector).compact
59
- end
57
+ before = (@before_move || []).collect(&collector).compact
58
+ after = (@after_move || []).collect(&collector).compact
59
+
60
60
  # Instances
61
61
  instances =
62
- if instance
63
- [ instance ]
62
+ if options[:instance]
63
+ [ options[:instance] ]
64
64
  elsif before.empty? && after.empty?
65
65
  []
66
66
  else
67
- self.find(:all, :conditions => where[5..-1])
67
+ self.find(:all, :conditions => conditions)
68
68
  end
69
+ options.delete(:instance)
70
+
69
71
  # Callback executor
70
72
  exec_callbacks = lambda do |callbacks|
71
73
  callbacks.each do |block|
72
- instances.each { |instance| instance.instance_eval(&block) }
74
+ instances.each do |instance|
75
+ instance.move_options = options
76
+ instance.instance_eval(&block)
77
+ instance.move_options = nil
78
+ end
73
79
  end
74
80
  end
81
+
75
82
  # Execute
76
83
  transaction do
77
84
  exec_callbacks.call before
78
- if copy
79
- # for copy existing rows require an UPDATE statment, new rows require an INSERT statement
80
- from_ids = from_class.find(:all, :select => "id", :conditions => where[5..-1]).collect(&:id)
81
- to_ids = to_class.find(:all, :select => "id", :conditions => where[5..-1]).collect(&:id)
82
- trash_ids = to_ids - from_ids
83
- # rid of the 'extra' to_ids to ensure to_ids is always be a subset of from_ids
84
- insert_ids = from_ids - to_ids - trash_ids
85
- update_ids = to_ids - insert_ids - trash_ids
86
-
87
- unless update_ids.empty?
88
- # add table scope to columns for the UPDATE statement
89
- update = []
90
- insert.each_with_index{|col, i|
91
- to = insert[i].include?('`') ? "#{to_class.table_name}.#{insert[i]}" : insert[i]
92
- from = select[i].include?('`') ? "#{from_class.table_name}.#{select[i]}" : select[i]
93
- update << "#{to} = #{from}"
94
- }
95
- where_ids = update_ids.collect{|x| "#{from_class.table_name}.id = #{x}"}
96
- connection.execute(<<-SQL)
97
- UPDATE #{to_class.table_name}
98
- INNER JOIN #{from_class.table_name}
99
- ON #{to_class.table_name}.id = #{from_class.table_name}.id
100
- AND (#{where_ids.join(' AND ')})
101
- SET #{update.join(', ')}
102
- SQL
103
- end
104
-
105
- unless insert_ids.empty?
106
- # insert ids, same as move except using a different where clause
107
- where_ids = insert_ids.collect{|x| "#{from_class.table_name}.id = #{x}"}
108
- sql =<<-SQL
109
- INSERT INTO #{to_class.table_name} (#{insert.join(', ')})
110
- SELECT #{select.join(', ')}
111
- FROM #{from_class.table_name}
112
- WHERE (#{where_ids.join(' OR ')})
113
- SQL
114
- connection.execute(sql)
85
+
86
+ if options[:quick]
87
+ connection.execute(<<-SQL)
88
+ INSERT INTO #{to_class.table_name} (#{insert.join(', ')})
89
+ SELECT #{select.join(', ')}
90
+ FROM #{from_class.table_name}
91
+ #{where}
92
+ SQL
93
+ elsif !options[:generic] && connection.class.to_s.include?('Mysql')
94
+ update = insert.collect do |i|
95
+ "#{to_class.table_name}.#{i} = #{from_class.table_name}.#{i}"
115
96
  end
116
- else
117
- # moves
97
+
118
98
  connection.execute(<<-SQL)
119
99
  INSERT INTO #{to_class.table_name} (#{insert.join(', ')})
120
100
  SELECT #{select.join(', ')}
121
101
  FROM #{from_class.table_name}
122
102
  #{where}
103
+ ON DUPLICATE KEY
104
+ UPDATE #{update.join(', ')};
105
+ SQL
106
+ else
107
+ conditions.gsub!(to_class.table_name, 't')
108
+ conditions.gsub!(from_class.table_name, 'f')
109
+ select.collect! { |s| s.include?('`') ? "f.#{s}" : s }
110
+ set = insert.collect { |i| "t.#{i} = f.#{i}" }
111
+
112
+ connection.execute(<<-SQL)
113
+ UPDATE #{to_class.table_name}
114
+ AS t
115
+ INNER JOIN #{from_class.table_name}
116
+ AS f
117
+ ON f.id = t.id
118
+ AND #{conditions}
119
+ SET #{set.join(', ')}
120
+ SQL
121
+
122
+ connection.execute(<<-SQL)
123
+ INSERT INTO #{to_class.table_name} (#{insert.join(', ')})
124
+ SELECT #{select.join(', ')}
125
+ FROM #{from_class.table_name}
126
+ AS f
127
+ LEFT OUTER JOIN #{to_class.table_name}
128
+ AS t
129
+ ON f.id = t.id
130
+ WHERE (
131
+ t.id IS NULL
132
+ AND #{conditions}
133
+ )
123
134
  SQL
135
+ end
136
+
137
+ unless options[:copy]
124
138
  connection.execute("DELETE FROM #{from_class.table_name} #{where}")
125
139
  end
140
+
126
141
  exec_callbacks.call after
127
142
  end
128
143
  end
129
144
 
130
- def copy_to(to_class, conditions, instance=nil)
131
- move_to(to_class, conditions, instance, true)
132
- end
133
-
134
145
  def reserve_id
135
146
  id = nil
136
147
  transaction do
@@ -142,12 +153,11 @@ module Mover
142
153
  end
143
154
 
144
155
  module InstanceMethods
145
- def move_to(to_class)
146
- self.class.move_to(to_class, "#{self.class.primary_key} = #{id}", self)
147
- end
148
-
149
- def copy_to(to_class)
150
- self.class.copy_to(to_class, "#{self.class.primary_key} = #{id}", self)
156
+
157
+ def move_to(to_class, options={})
158
+ options[:conditions] = "#{self.class.table_name}.#{self.class.primary_key} = #{id}"
159
+ options[:instance] = self
160
+ self.class.move_to(to_class, options)
151
161
  end
152
162
  end
153
163
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 2
8
- - 2
9
- version: 0.2.2
7
+ - 3
8
+ - 0
9
+ version: 0.3.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Winton Welsh
@@ -14,49 +14,54 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-04 00:00:00 -07:00
17
+ date: 2010-12-04 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- name: require
21
+ name: active_wrapper
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
24
25
  requirements:
25
26
  - - "="
26
27
  - !ruby/object:Gem::Version
27
28
  segments:
28
29
  - 0
29
- - 2
30
- - 6
31
- version: 0.2.6
32
- type: :runtime
30
+ - 3
31
+ - 4
32
+ version: 0.3.4
33
+ type: :development
33
34
  version_requirements: *id001
34
- description:
35
- email: mail@wintoni.us
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - "="
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 1
45
+ - 3
46
+ - 1
47
+ version: 1.3.1
48
+ type: :development
49
+ version_requirements: *id002
50
+ description: Move ActiveRecord records across tables like it ain't no thang
51
+ email:
52
+ - mail@wintoni.us
36
53
  executables: []
37
54
 
38
55
  extensions: []
39
56
 
40
- extra_rdoc_files:
41
- - README.markdown
57
+ extra_rdoc_files: []
58
+
42
59
  files:
43
- - init.rb
60
+ - lib/mover/gems.rb
61
+ - lib/mover/version.rb
44
62
  - lib/mover.rb
45
- - MIT-LICENSE
46
- - rails/init.rb
47
- - Rakefile
48
- - README.markdown
49
- - require.rb
50
- - spec/config/database.yml.example
51
- - spec/db/migrate/001_create_fixtures.rb
52
- - spec/fixtures/article.rb
53
- - spec/fixtures/article_archive.rb
54
- - spec/fixtures/comment.rb
55
- - spec/fixtures/comment_archive.rb
56
- - spec/log/test.log
57
- - spec/mover_spec.rb
58
- - spec/Rakefile
59
- - spec/spec_helper.rb
63
+ - LICENSE
64
+ - README.md
60
65
  has_rdoc: true
61
66
  homepage: http://github.com/winton/mover
62
67
  licenses: []
@@ -67,6 +72,7 @@ rdoc_options: []
67
72
  require_paths:
68
73
  - lib
69
74
  required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
70
76
  requirements:
71
77
  - - ">="
72
78
  - !ruby/object:Gem::Version
@@ -74,6 +80,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
74
80
  - 0
75
81
  version: "0"
76
82
  required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
77
84
  requirements:
78
85
  - - ">="
79
86
  - !ruby/object:Gem::Version
@@ -83,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
90
  requirements: []
84
91
 
85
92
  rubyforge_project:
86
- rubygems_version: 1.3.6
93
+ rubygems_version: 1.3.7
87
94
  signing_key:
88
95
  specification_version: 3
89
96
  summary: Move ActiveRecord records across tables like it ain't no thang
data/README.markdown DELETED
@@ -1,72 +0,0 @@
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
- Move records
14
- ------------
15
-
16
- <pre>
17
- Article.last.move_to(ArticleArchive)
18
- Article.move_to(ArticleArchive, [ "created_at > ?", Date.today ])
19
- </pre>
20
-
21
- The <code>move_to</code> method is available to all models.
22
-
23
- The two tables do not have to be identical. Only shared columns transfer.
24
-
25
- Callbacks
26
- ---------
27
-
28
- In this example, we want an "archive" table for articles and comments.
29
-
30
- We also want the article's comments to be archived when the article is.
31
-
32
- <pre>
33
- class Article < ActiveRecord::Base
34
- has_many :comments
35
- before_move :ArticleArchive do
36
- comments.each { |c| c.move_to(CommentArchive) }
37
- end
38
- end
39
-
40
- class ArticleArchive < ActiveRecord::Base
41
- has_many :comments, :class_name => 'CommentArchive', :foreign_key => 'article_id'
42
- before_move :Article do
43
- comments.each { |c| c.move_to(Comment) }
44
- end
45
- end
46
-
47
- class Comment < ActiveRecord::Base
48
- belongs_to :article
49
- end
50
-
51
- class CommentArchive < ActiveRecord::Base
52
- belongs_to :article, :class_name => 'ArticleArchive', :foreign_key => 'article_id'
53
- end
54
- </pre>
55
-
56
- Reserve a spot
57
- --------------
58
-
59
- Before you create a record, you can "reserve a spot" on a table that you will move the record to later.
60
-
61
- <pre>
62
- archive = ArticleArchive.new
63
- archive.id = Article.reserve_id
64
- archive.save
65
- </pre>
66
-
67
- Magic columns
68
- -------------
69
-
70
- ### moved_at
71
-
72
- If a table contains the column <code>moved_at</code>, it will automatically be populated with the date and time it was moved.
data/Rakefile DELETED
@@ -1,2 +0,0 @@
1
- require "#{File.dirname(__FILE__)}/require"
2
- Require.rakefile!
data/init.rb DELETED
@@ -1 +0,0 @@
1
- require File.dirname(__FILE__) + "/rails/init"
data/rails/init.rb DELETED
@@ -1,2 +0,0 @@
1
- require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
- Require.rails_init!
data/require.rb DELETED
@@ -1,51 +0,0 @@
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.2.2'
21
- end
22
-
23
- bin { require 'lib/mover' }
24
- lib {}
25
-
26
- rakefile do
27
- gem(:active_wrapper)
28
- gem(:rake) { require 'rake/gempackagetask' }
29
- gem(:rspec) { require 'spec/rake/spectask' }
30
- require 'require/tasks'
31
- end
32
-
33
- rails_init { require 'lib/mover' }
34
-
35
- spec_helper do
36
- require 'fileutils'
37
- gem(:active_wrapper)
38
- require 'require/spec_helper'
39
- require 'rails/init'
40
- require 'pp'
41
- require 'spec/fixtures/article'
42
- require 'spec/fixtures/article_archive'
43
- require 'spec/fixtures/comment'
44
- require 'spec/fixtures/comment_archive'
45
- end
46
-
47
- spec_rakefile do
48
- gem(:rake)
49
- gem(:active_wrapper) { require 'active_wrapper/tasks' }
50
- end
51
- end
data/spec/Rakefile DELETED
@@ -1,10 +0,0 @@
1
- require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
- Require.spec_rakefile!
3
-
4
- begin
5
- ActiveWrapper::Tasks.new(
6
- :base => File.dirname(__FILE__),
7
- :env => ENV['ENV']
8
- )
9
- rescue Exception
10
- end
@@ -1,6 +0,0 @@
1
- test:
2
- adapter: mysql
3
- database: mover
4
- username: root
5
- password:
6
- host: localhost
@@ -1,28 +0,0 @@
1
- class CreateFixtures < ActiveRecord::Migration
2
- def self.up
3
- [ :articles, :article_archives ].each do |table|
4
- create_table table do |t|
5
- t.string :title
6
- t.string :body
7
- t.boolean :read
8
- t.datetime :moved_at
9
- end
10
- end
11
-
12
- [ :comments, :comment_archives ].each do |table|
13
- create_table table do |t|
14
- t.string :title
15
- t.string :body
16
- t.boolean :read
17
- t.integer :article_id
18
- t.datetime :moved_at
19
- end
20
- end
21
- end
22
-
23
- def self.down
24
- [ :articles, :article_archives, :comments, :comment_archives ].each do |table|
25
- drop_table table
26
- end
27
- end
28
- end