activerecord-import 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -41,6 +41,8 @@ ADAPTERS.each do |adapter|
41
41
  namespace :test do
42
42
  desc "Runs #{adapter} database tests."
43
43
  Rake::TestTask.new(adapter) do |t|
44
+ # FactoryGirl has an issue with warnings, so turn off, so noisy
45
+ # t.warning = true
44
46
  t.test_files = FileList["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/active_record/*_test.rb", "test/#{adapter}/**/*_test.rb"]
45
47
  end
46
48
  task adapter
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.5
1
+ 0.2.6
@@ -25,3 +25,4 @@ end
25
25
  this_dir = Pathname.new File.dirname(__FILE__)
26
26
  require this_dir.join("import")
27
27
  require this_dir.join("active_record/adapters/abstract_adapter")
28
+ require this_dir.join("synchronize")
@@ -129,6 +129,11 @@ class ActiveRecord::Base
129
129
  # BlogPost.import posts, :synchronize=>[ post ]
130
130
  # puts post.author_name # => 'yoda'
131
131
  #
132
+ # # Example synchronizing unsaved/new instances in memory by using a uniqued imported field
133
+ # posts = [BlogPost.new(:title => "Foo"), BlogPost.new(:title => "Bar")]
134
+ # BlogPost.import posts, :synchronize => posts
135
+ # puts posts.first.new_record? # => false
136
+ #
132
137
  # == On Duplicate Key Update (MySQL only)
133
138
  #
134
139
  # The :on_duplicate_key_update option can be either an Array or a Hash.
@@ -178,6 +183,9 @@ class ActiveRecord::Base
178
183
  end
179
184
  # end
180
185
  end
186
+ # supports empty array
187
+ elsif args.last.is_a?( Array ) and args.last.empty?
188
+ return ActiveRecord::Import::Result.new([], 0) if args.last.empty?
181
189
  # supports 2-element array and array
182
190
  elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
183
191
  column_names, array_of_attributes = args
@@ -209,7 +217,8 @@ class ActiveRecord::Base
209
217
  end
210
218
 
211
219
  if options[:synchronize]
212
- synchronize( options[:synchronize] )
220
+ sync_keys = options[:synchronize_keys] || [self.primary_key]
221
+ synchronize( options[:synchronize], sync_keys)
213
222
  end
214
223
 
215
224
  return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
@@ -0,0 +1,55 @@
1
+ module ActiveRecord # :nodoc:
2
+ class Base # :nodoc:
3
+
4
+ # Synchronizes the passed in ActiveRecord instances with data
5
+ # from the database. This is like calling reload on an individual
6
+ # ActiveRecord instance but it is intended for use on multiple instances.
7
+ #
8
+ # This uses one query for all instance updates and then updates existing
9
+ # instances rather sending one query for each instance
10
+ #
11
+ # == Examples
12
+ # # Synchronizing existing models by matching on the primary key field
13
+ # posts = Post.find_by_author("Zach")
14
+ # <.. out of system changes occur to change author name from Zach to Zachary..>
15
+ # Post.synchronize posts
16
+ # posts.first.author # => "Zachary" instead of Zach
17
+ #
18
+ # # Synchronizing using custom key fields
19
+ # posts = Post.find_by_author("Zach")
20
+ # <.. out of system changes occur to change the address of author 'Zach' to 1245 Foo Ln ..>
21
+ # Post.synchronize posts, [:name] # queries on the :name column and not the :id column
22
+ # posts.first.address # => "1245 Foo Ln" instead of whatever it was
23
+ #
24
+ def self.synchronize(instances, keys=[self.primary_key])
25
+ return if instances.empty?
26
+
27
+ conditions = {}
28
+ order = ""
29
+
30
+ key_values = keys.map { |key| instances.map(&"#{key}".to_sym) }
31
+ keys.zip(key_values).each { |key, values| conditions[key] = values }
32
+ order = keys.map{ |key| "#{key} ASC" }.join(",")
33
+
34
+ klass = instances.first.class
35
+
36
+ fresh_instances = klass.find( :all, :conditions=>conditions, :order=>order )
37
+ instances.each do |instance|
38
+ matched_instance = fresh_instances.detect do |fresh_instance|
39
+ keys.all?{ |key| fresh_instance.send(key) == instance.send(key) }
40
+ end
41
+
42
+ if matched_instance
43
+ instance.clear_aggregation_cache
44
+ instance.clear_association_cache
45
+ instance.instance_variable_set '@attributes', matched_instance.attributes
46
+ end
47
+ end
48
+ end
49
+
50
+ # See ActiveRecord::ConnectionAdapters::AbstractAdapter.synchronize
51
+ def synchronize(instances, key=[ActiveRecord::Base.primary_key])
52
+ self.class.synchronize(instances, key)
53
+ end
54
+ end
55
+ end
data/test/import_test.rb CHANGED
@@ -12,6 +12,13 @@ describe "#import" do
12
12
  end
13
13
  end
14
14
 
15
+ it "should not produce an error when importing empty arrays" do
16
+ assert_nothing_raised do
17
+ Topic.import []
18
+ Topic.import %w(title author_name), []
19
+ end
20
+ end
21
+
15
22
  context "with :validation option" do
16
23
  let(:columns) { %w(title author_name) }
17
24
  let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
@@ -59,6 +66,26 @@ describe "#import" do
59
66
  end
60
67
  end
61
68
 
69
+ context "with :synchronize option" do
70
+ context "synchronizing on new records" do
71
+ let(:new_topics) { Build(3, :topics) }
72
+
73
+ it "doesn't reload any data (doesn't work)" do
74
+ Topic.import new_topics, :synchronize => new_topics
75
+ assert new_topics.all?(&:new_record?), "No record should have been reloaded"
76
+ end
77
+ end
78
+
79
+ context "synchronizing on new records with explicit conditions" do
80
+ let(:new_topics) { Build(3, :topics) }
81
+
82
+ it "reloads data for existing in-memory instances" do
83
+ Topic.import(new_topics, :synchronize => new_topics, :synchronize_key => [:title] )
84
+ assert new_topics.all?(&:new_record?), "Records should have been reloaded"
85
+ end
86
+ end
87
+ end
88
+
62
89
  context "with an array of unsaved model instances" do
63
90
  let(:topic) { Build(:topic, :title => "The RSpec Book", :author_name => "David Chelimsky")}
64
91
  let(:topics) { Build(9, :topics) }
@@ -135,6 +162,13 @@ describe "#import" do
135
162
  end
136
163
  end
137
164
 
165
+ context "with an array of columns and an array of values" do
166
+ it "should import ids when specified" do
167
+ Topic.import [:id, :author_name, :title], [[99, "Bob Jones", "Topic 99"]]
168
+ assert_equal 99, Topic.last.id
169
+ end
170
+ end
171
+
138
172
  context "ActiveRecord timestamps" do
139
173
  context "when the timestamps columns are present" do
140
174
  setup do
@@ -210,5 +244,4 @@ describe "#import" do
210
244
  assert_equal "05/14/2010".to_date, Topic.last.last_read.to_date
211
245
  end
212
246
  end
213
-
214
247
  end
@@ -142,4 +142,30 @@ def should_support_mysql_import_functionality
142
142
  end
143
143
 
144
144
  end
145
+
146
+ describe "#import with :synchronization option" do
147
+ let(:topics){ Array.new }
148
+ let(:values){ [ [topics.first.id, "Jerry Carter"], [topics.last.id, "Chad Fowler"] ]}
149
+ let(:columns){ %W(id author_name) }
150
+
151
+ setup do
152
+ topics << Topic.create!(:title=>"LDAP", :author_name=>"Big Bird")
153
+ topics << Topic.create!(:title=>"Rails Recipes", :author_name=>"Elmo")
154
+ end
155
+
156
+ it "synchronizes passed in ActiveRecord model instances with the data just imported" do
157
+ columns2update = [ 'author_name' ]
158
+
159
+ expected_count = Topic.count
160
+ Topic.import( columns, values,
161
+ :validate=>false,
162
+ :on_duplicate_key_update=>columns2update,
163
+ :synchronize=>topics )
164
+
165
+ assert_equal expected_count, Topic.count, "no new records should have been created!"
166
+ assert_equal "Jerry Carter", topics.first.author_name, "wrong author!"
167
+ assert_equal "Chad Fowler", topics.last.author_name, "wrong author!"
168
+ end
169
+ end
170
+
145
171
  end
@@ -0,0 +1,22 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe ".synchronize" do
4
+ let(:topics){ Generate(3, :topics) }
5
+ let(:titles){ %w(one two three) }
6
+
7
+ setup do
8
+ # update records outside of ActiveRecord knowing about it
9
+ Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[0]}_haha' WHERE id=#{topics[0].id}", "Updating record 1 without ActiveRecord" )
10
+ Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[1]}_haha' WHERE id=#{topics[1].id}", "Updating record 2 without ActiveRecord" )
11
+ Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[2]}_haha' WHERE id=#{topics[2].id}", "Updating record 3 without ActiveRecord" )
12
+ end
13
+
14
+ it "reloads data for the specified records" do
15
+ Book.synchronize topics
16
+
17
+ actual_titles = topics.map(&:title)
18
+ assert_equal "#{titles[0]}_haha", actual_titles[0], "the first record was not correctly updated"
19
+ assert_equal "#{titles[1]}_haha", actual_titles[1], "the second record was not correctly updated"
20
+ assert_equal "#{titles[2]}_haha", actual_titles[2], "the third record was not correctly updated"
21
+ end
22
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-import
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 5
10
- version: 0.2.5
9
+ - 6
10
+ version: 0.2.6
11
11
  platform: ruby
12
12
  authors:
13
13
  - Zach Dennis
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-11 00:00:00 -05:00
18
+ date: 2011-04-06 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -42,10 +42,8 @@ extensions: []
42
42
 
43
43
  extra_rdoc_files:
44
44
  - README.markdown
45
- - README.textile
46
45
  files:
47
46
  - README.markdown
48
- - README.textile
49
47
  - Rakefile
50
48
  - VERSION
51
49
  - lib/activerecord-import.rb
@@ -64,6 +62,7 @@ files:
64
62
  - lib/activerecord-import/mysql2.rb
65
63
  - lib/activerecord-import/postgresql.rb
66
64
  - lib/activerecord-import/sqlite3.rb
65
+ - lib/activerecord-import/synchronize.rb
67
66
  - test/active_record/connection_adapter_test.rb
68
67
  - test/adapters/mysql.rb
69
68
  - test/adapters/mysql2.rb
@@ -84,6 +83,7 @@ files:
84
83
  - test/support/generate.rb
85
84
  - test/support/mysql/assertions.rb
86
85
  - test/support/mysql/import_examples.rb
86
+ - test/synchronize_test.rb
87
87
  - test/test_helper.rb
88
88
  has_rdoc: true
89
89
  homepage: http://github.com/zdennis/activerecord-import
@@ -140,4 +140,5 @@ test_files:
140
140
  - test/support/generate.rb
141
141
  - test/support/mysql/assertions.rb
142
142
  - test/support/mysql/import_examples.rb
143
+ - test/synchronize_test.rb
143
144
  - test/test_helper.rb
data/README.textile DELETED
@@ -1,62 +0,0 @@
1
- h1. activerecord-import
2
-
3
- activerecord-import is a library for bulk inserting data using ActiveRecord.
4
-
5
- h2. Why activerecord-import?
6
-
7
- Because plain-vanilla, out-of-the-box ActiveRecord doesn't provide support for inserting large amounts of data efficiently. With vanilla ActiveRecord you would have to perform individual save operations on each model:
8
-
9
- <pre>
10
- 10.times do |i|
11
- Book.create! :name => "book #{i}"
12
- end
13
- </pre>
14
-
15
- This may work fine if all you have is 10 records, but if you have hundreds, thousands, or millions of records it can turn into a nightmare. This is where activerecord-import comes into play.
16
-
17
- h2. An Introductory Example
18
-
19
- Here's an example with equivalent behaviour using the @#import@ method:
20
-
21
- <pre>
22
- books = []
23
- 10.times do |i|
24
- books << Book.new(:name => "book #{i}")
25
- end
26
- Book.import books
27
- </pre>
28
-
29
- This call to import does whatever is most efficient for the underlying database adapter. Pretty slick, eh?
30
-
31
- h2. Features
32
-
33
- Here's a list of some of the high-level features that activerecord-import provides:
34
-
35
- * activerecord-import can work with raw columns and arrays of values (fastest)
36
- * activerecord-import works with model objects (faster)
37
- * activerecord-import can perform validations (fast)
38
- * activerecord-import can perform on duplicate key updates (requires mysql)
39
-
40
-
41
- h1. Upgrading from ar-extensions
42
-
43
- activerecord-import replaces the ar-extensions library and is compatible with Rails 3. It provides the exact same API for importing data, but it does not include any additional ar-extensions functionality.
44
-
45
- h1. License
46
-
47
- This is licensed under the ruby license.
48
-
49
- h1. Author
50
-
51
- Zach Dennis (zach.dennis@gmail.com)
52
-
53
- h1. Contributors
54
-
55
- * Blythe Dunham
56
- * Gabe da Silveira
57
- * Henry Work
58
- * James Herdman
59
- * Marcus Crafter
60
- * Thibaud Guillaume-Gentil
61
- * Mark Van Holstyn
62
- * Victor Costan