activerecord-import-rails4 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +31 -0
  3. data/Appraisals +9 -0
  4. data/Gemfile +25 -0
  5. data/README.markdown +24 -0
  6. data/Rakefile +52 -0
  7. data/activerecord-import-rails4.gemspec +24 -0
  8. data/benchmarks/README +32 -0
  9. data/benchmarks/benchmark.rb +64 -0
  10. data/benchmarks/boot.rb +18 -0
  11. data/benchmarks/lib/base.rb +137 -0
  12. data/benchmarks/lib/cli_parser.rb +103 -0
  13. data/benchmarks/lib/float.rb +15 -0
  14. data/benchmarks/lib/mysql_benchmark.rb +22 -0
  15. data/benchmarks/lib/output_to_csv.rb +18 -0
  16. data/benchmarks/lib/output_to_html.rb +69 -0
  17. data/benchmarks/models/test_innodb.rb +3 -0
  18. data/benchmarks/models/test_memory.rb +3 -0
  19. data/benchmarks/models/test_myisam.rb +3 -0
  20. data/benchmarks/schema/mysql_schema.rb +16 -0
  21. data/gemfiles/rails3.gemfile +18 -0
  22. data/gemfiles/rails4.gemfile +18 -0
  23. data/lib/activerecord-import-rails4.rb +16 -0
  24. data/lib/activerecord-import-rails4/active_record/adapters/abstract_adapter.rb +10 -0
  25. data/lib/activerecord-import-rails4/active_record/adapters/jdbcmysql_adapter.rb +6 -0
  26. data/lib/activerecord-import-rails4/active_record/adapters/mysql2_adapter.rb +6 -0
  27. data/lib/activerecord-import-rails4/active_record/adapters/mysql_adapter.rb +6 -0
  28. data/lib/activerecord-import-rails4/active_record/adapters/postgresql_adapter.rb +7 -0
  29. data/lib/activerecord-import-rails4/active_record/adapters/seamless_database_pool_adapter.rb +7 -0
  30. data/lib/activerecord-import-rails4/active_record/adapters/sqlite3_adapter.rb +7 -0
  31. data/lib/activerecord-import-rails4/adapters/abstract_adapter.rb +119 -0
  32. data/lib/activerecord-import-rails4/adapters/mysql2_adapter.rb +5 -0
  33. data/lib/activerecord-import-rails4/adapters/mysql_adapter.rb +55 -0
  34. data/lib/activerecord-import-rails4/adapters/postgresql_adapter.rb +7 -0
  35. data/lib/activerecord-import-rails4/adapters/sqlite3_adapter.rb +5 -0
  36. data/lib/activerecord-import-rails4/base.rb +34 -0
  37. data/lib/activerecord-import-rails4/import.rb +387 -0
  38. data/lib/activerecord-import-rails4/mysql.rb +8 -0
  39. data/lib/activerecord-import-rails4/mysql2.rb +8 -0
  40. data/lib/activerecord-import-rails4/postgresql.rb +8 -0
  41. data/lib/activerecord-import-rails4/sqlite3.rb +8 -0
  42. data/lib/activerecord-import-rails4/synchronize.rb +60 -0
  43. data/lib/activerecord-import-rails4/version.rb +5 -0
  44. data/test/active_record/connection_adapter_test.rb +62 -0
  45. data/test/adapters/jdbcmysql.rb +1 -0
  46. data/test/adapters/mysql.rb +1 -0
  47. data/test/adapters/mysql2.rb +1 -0
  48. data/test/adapters/mysql2spatial.rb +1 -0
  49. data/test/adapters/mysqlspatial.rb +1 -0
  50. data/test/adapters/postgis.rb +1 -0
  51. data/test/adapters/postgresql.rb +1 -0
  52. data/test/adapters/seamless_database_pool.rb +1 -0
  53. data/test/adapters/spatialite.rb +1 -0
  54. data/test/adapters/sqlite3.rb +1 -0
  55. data/test/import_test.rb +321 -0
  56. data/test/jdbcmysql/import_test.rb +6 -0
  57. data/test/models/book.rb +3 -0
  58. data/test/models/group.rb +3 -0
  59. data/test/models/topic.rb +7 -0
  60. data/test/models/widget.rb +3 -0
  61. data/test/mysql/import_test.rb +6 -0
  62. data/test/mysql2/import_test.rb +6 -0
  63. data/test/mysqlspatial/import_test.rb +6 -0
  64. data/test/mysqlspatial2/import_test.rb +6 -0
  65. data/test/postgis/import_test.rb +4 -0
  66. data/test/postgresql/import_test.rb +4 -0
  67. data/test/schema/generic_schema.rb +102 -0
  68. data/test/schema/mysql_schema.rb +17 -0
  69. data/test/schema/version.rb +10 -0
  70. data/test/support/active_support/test_case_extensions.rb +67 -0
  71. data/test/support/factories.rb +19 -0
  72. data/test/support/generate.rb +29 -0
  73. data/test/support/mysql/assertions.rb +55 -0
  74. data/test/support/mysql/import_examples.rb +190 -0
  75. data/test/support/postgresql/import_examples.rb +21 -0
  76. data/test/synchronize_test.rb +22 -0
  77. data/test/test_helper.rb +48 -0
  78. metadata +197 -0
@@ -0,0 +1,7 @@
1
+ module ActiveRecord::Import::PostgreSQLAdapter
2
+ include ActiveRecord::Import::ImportSupport
3
+
4
+ def next_value_for_sequence(sequence_name)
5
+ %{nextval('#{sequence_name}')}
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord::Import::SQLite3Adapter
2
+ def next_value_for_sequence(sequence_name)
3
+ %{nextval('#{sequence_name}')}
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ require "pathname"
2
+ require "active_record"
3
+ require "active_record/version"
4
+
5
+ module ActiveRecord::Import
6
+ AdapterPath = File.join File.expand_path(File.dirname(__FILE__)), "/active_record/adapters"
7
+
8
+ def self.base_adapter(adapter)
9
+ case adapter
10
+ when 'mysqlspatial' then 'mysql'
11
+ when 'mysql2spatial' then 'mysql2'
12
+ when 'spatialite' then 'sqlite3'
13
+ when 'postgis' then 'postgresql'
14
+ else adapter
15
+ end
16
+ end
17
+
18
+ # Loads the import functionality for a specific database adapter
19
+ def self.require_adapter(adapter)
20
+ require File.join(AdapterPath,"/abstract_adapter")
21
+ require File.join(AdapterPath,"/#{base_adapter(adapter)}_adapter")
22
+ end
23
+
24
+ # Loads the import functionality for the passed in ActiveRecord connection
25
+ def self.load_from_connection_pool(connection_pool)
26
+ require_adapter connection_pool.spec.config[:adapter]
27
+ end
28
+ end
29
+
30
+
31
+ this_dir = Pathname.new File.dirname(__FILE__)
32
+ require this_dir.join("import").to_s
33
+ require this_dir.join("active_record/adapters/abstract_adapter").to_s
34
+ require this_dir.join("synchronize").to_s
@@ -0,0 +1,387 @@
1
+ require "ostruct"
2
+
3
+ module ActiveRecord::Import::ConnectionAdapters ; end
4
+
5
+ module ActiveRecord::Import #:nodoc:
6
+ class Result < Struct.new(:failed_instances, :num_inserts)
7
+ end
8
+
9
+ module ImportSupport #:nodoc:
10
+ def supports_import? #:nodoc:
11
+ true
12
+ end
13
+ end
14
+
15
+ module OnDuplicateKeyUpdateSupport #:nodoc:
16
+ def supports_on_duplicate_key_update? #:nodoc:
17
+ true
18
+ end
19
+ end
20
+
21
+ class MissingColumnError < StandardError
22
+ def initialize(name, index)
23
+ super "Missing column for value <#{name}> at index #{index}"
24
+ end
25
+ end
26
+ end
27
+
28
+ class ActiveRecord::Base
29
+ class << self
30
+
31
+ # use tz as set in ActiveRecord::Base
32
+ tproc = lambda do
33
+ ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
34
+ end
35
+
36
+ AREXT_RAILS_COLUMNS = {
37
+ :create => { "created_on" => tproc ,
38
+ "created_at" => tproc },
39
+ :update => { "updated_on" => tproc ,
40
+ "updated_at" => tproc }
41
+ }
42
+ AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
43
+
44
+ # Returns true if the current database connection adapter
45
+ # supports import functionality, otherwise returns false.
46
+ def supports_import?
47
+ connection.supports_import?
48
+ rescue NoMethodError
49
+ false
50
+ end
51
+
52
+ # Returns true if the current database connection adapter
53
+ # supports on duplicate key update functionality, otherwise
54
+ # returns false.
55
+ def supports_on_duplicate_key_update?
56
+ connection.supports_on_duplicate_key_update?
57
+ rescue NoMethodError
58
+ false
59
+ end
60
+
61
+ # Imports a collection of values to the database.
62
+ #
63
+ # This is more efficient than using ActiveRecord::Base#create or
64
+ # ActiveRecord::Base#save multiple times. This method works well if
65
+ # you want to create more than one record at a time and do not care
66
+ # about having ActiveRecord objects returned for each record
67
+ # inserted.
68
+ #
69
+ # This can be used with or without validations. It does not utilize
70
+ # the ActiveRecord::Callbacks during creation/modification while
71
+ # performing the import.
72
+ #
73
+ # == Usage
74
+ # Model.import array_of_models
75
+ # Model.import column_names, array_of_values
76
+ # Model.import column_names, array_of_values, options
77
+ #
78
+ # ==== Model.import array_of_models
79
+ #
80
+ # With this form you can call _import_ passing in an array of model
81
+ # objects that you want updated.
82
+ #
83
+ # ==== Model.import column_names, array_of_values
84
+ #
85
+ # The first parameter +column_names+ is an array of symbols or
86
+ # strings which specify the columns that you want to update.
87
+ #
88
+ # The second parameter, +array_of_values+, is an array of
89
+ # arrays. Each subarray is a single set of values for a new
90
+ # record. The order of values in each subarray should match up to
91
+ # the order of the +column_names+.
92
+ #
93
+ # ==== Model.import column_names, array_of_values, options
94
+ #
95
+ # The first two parameters are the same as the above form. The third
96
+ # parameter, +options+, is a hash. This is optional. Please see
97
+ # below for what +options+ are available.
98
+ #
99
+ # == Options
100
+ # * +validate+ - true|false, tells import whether or not to use \
101
+ # ActiveRecord validations. Validations are enforced by default.
102
+ # * +on_duplicate_key_update+ - an Array or Hash, tells import to \
103
+ # use MySQL's ON DUPLICATE KEY UPDATE ability. See On Duplicate\
104
+ # Key Update below.
105
+ # * +synchronize+ - an array of ActiveRecord instances for the model
106
+ # that you are currently importing data into. This synchronizes
107
+ # existing model instances in memory with updates from the import.
108
+ # * +timestamps+ - true|false, tells import to not add timestamps \
109
+ # (if false) even if record timestamps is disabled in ActiveRecord::Base
110
+ #
111
+ # == Examples
112
+ # class BlogPost < ActiveRecord::Base ; end
113
+ #
114
+ # # Example using array of model objects
115
+ # posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT',
116
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2',
117
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT3' ]
118
+ # BlogPost.import posts
119
+ #
120
+ # # Example using column_names and array_of_values
121
+ # columns = [ :author_name, :title ]
122
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
123
+ # BlogPost.import columns, values
124
+ #
125
+ # # Example using column_names, array_of_value and options
126
+ # columns = [ :author_name, :title ]
127
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
128
+ # BlogPost.import( columns, values, :validate => false )
129
+ #
130
+ # # Example synchronizing existing instances in memory
131
+ # post = BlogPost.where(author_name: 'zdennis').first
132
+ # puts post.author_name # => 'zdennis'
133
+ # columns = [ :author_name, :title ]
134
+ # values = [ [ 'yoda', 'test post' ] ]
135
+ # BlogPost.import posts, :synchronize=>[ post ]
136
+ # puts post.author_name # => 'yoda'
137
+ #
138
+ # # Example synchronizing unsaved/new instances in memory by using a uniqued imported field
139
+ # posts = [BlogPost.new(:title => "Foo"), BlogPost.new(:title => "Bar")]
140
+ # BlogPost.import posts, :synchronize => posts, :synchronize_keys => [:title]
141
+ # puts posts.first.persisted? # => true
142
+ #
143
+ # == On Duplicate Key Update (MySQL only)
144
+ #
145
+ # The :on_duplicate_key_update option can be either an Array or a Hash.
146
+ #
147
+ # ==== Using an Array
148
+ #
149
+ # The :on_duplicate_key_update option can be an array of column
150
+ # names. The column names are the only fields that are updated if
151
+ # a duplicate record is found. Below is an example:
152
+ #
153
+ # BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
154
+ #
155
+ # ==== Using A Hash
156
+ #
157
+ # The :on_duplicate_key_update option can be a hash of column name
158
+ # to model attribute name mappings. This gives you finer grained
159
+ # control over what fields are updated with what attributes on your
160
+ # model. Below is an example:
161
+ #
162
+ # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
163
+ #
164
+ # = Returns
165
+ # This returns an object which responds to +failed_instances+ and +num_inserts+.
166
+ # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
167
+ # * num_inserts - the number of insert statements it took to import the data
168
+ def import( *args )
169
+ options = { :validate=>true, :timestamps=>true }
170
+ options.merge!( args.pop ) if args.last.is_a? Hash
171
+
172
+ is_validating = options.delete( :validate )
173
+
174
+ # assume array of model objects
175
+ if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
176
+ if args.length == 2
177
+ models = args.last
178
+ column_names = args.first
179
+ else
180
+ models = args.first
181
+ column_names = self.column_names.dup
182
+ end
183
+
184
+ array_of_attributes = models.map do |model|
185
+ # this next line breaks sqlite.so with a segmentation fault
186
+ # if model.new_record? || options[:on_duplicate_key_update]
187
+ column_names.map do |name|
188
+ model.send( "#{name}_before_type_cast" )
189
+ end
190
+ # end
191
+ end
192
+ # supports empty array
193
+ elsif args.last.is_a?( Array ) and args.last.empty?
194
+ return ActiveRecord::Import::Result.new([], 0) if args.last.empty?
195
+ # supports 2-element array and array
196
+ elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
197
+ column_names, array_of_attributes = args
198
+ else
199
+ raise ArgumentError.new( "Invalid arguments!" )
200
+ end
201
+
202
+ # dup the passed in array so we don't modify it unintentionally
203
+ array_of_attributes = array_of_attributes.dup
204
+
205
+ # Force the primary key col into the insert if it's not
206
+ # on the list and we are using a sequence and stuff a nil
207
+ # value for it into each row so the sequencer will fire later
208
+ if !column_names.include?(primary_key) && sequence_name && connection.prefetch_primary_key?
209
+ column_names << primary_key
210
+ array_of_attributes.each { |a| a << nil }
211
+ end
212
+
213
+ # record timestamps unless disabled in ActiveRecord::Base
214
+ if record_timestamps && options.delete( :timestamps )
215
+ add_special_rails_stamps column_names, array_of_attributes, options
216
+ end
217
+
218
+ return_obj = if is_validating
219
+ import_with_validations( column_names, array_of_attributes, options )
220
+ else
221
+ num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
222
+ ActiveRecord::Import::Result.new([], num_inserts)
223
+ end
224
+
225
+ if options[:synchronize]
226
+ sync_keys = options[:synchronize_keys] || [self.primary_key]
227
+ synchronize( options[:synchronize], sync_keys)
228
+ end
229
+
230
+ return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
231
+ return_obj
232
+ end
233
+
234
+ # TODO import_from_table needs to be implemented.
235
+ def import_from_table( options ) # :nodoc:
236
+ end
237
+
238
+ # Imports the passed in +column_names+ and +array_of_attributes+
239
+ # given the passed in +options+ Hash with validations. Returns an
240
+ # object with the methods +failed_instances+ and +num_inserts+.
241
+ # +failed_instances+ is an array of instances that failed validations.
242
+ # +num_inserts+ is the number of inserts it took to import the data. See
243
+ # ActiveRecord::Base.import for more information on
244
+ # +column_names+, +array_of_attributes+ and +options+.
245
+ def import_with_validations( column_names, array_of_attributes, options={} )
246
+ failed_instances = []
247
+
248
+ # create instances for each of our column/value sets
249
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
250
+
251
+ # keep track of the instance and the position it is currently at. if this fails
252
+ # validation we'll use the index to remove it from the array_of_attributes
253
+ arr.each_with_index do |hsh,i|
254
+ instance = new do |model|
255
+ hsh.each_pair{ |k,v| model.send("#{k}=", v) }
256
+ end
257
+ if not instance.valid?
258
+ array_of_attributes[ i ] = nil
259
+ failed_instances << instance
260
+ end
261
+ end
262
+ array_of_attributes.compact!
263
+
264
+ num_inserts = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
265
+ 0
266
+ else
267
+ import_without_validations_or_callbacks( column_names, array_of_attributes, options )
268
+ end
269
+ ActiveRecord::Import::Result.new(failed_instances, num_inserts)
270
+ end
271
+
272
+ # Imports the passed in +column_names+ and +array_of_attributes+
273
+ # given the passed in +options+ Hash. This will return the number
274
+ # of insert operations it took to create these records without
275
+ # validations or callbacks. See ActiveRecord::Base.import for more
276
+ # information on +column_names+, +array_of_attributes_ and
277
+ # +options+.
278
+ def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
279
+ scope_columns, scope_values = scope_attributes.to_a.transpose
280
+
281
+ unless scope_columns.blank?
282
+ column_names.concat scope_columns
283
+ array_of_attributes.each { |a| a.concat scope_values }
284
+ end
285
+
286
+ columns = column_names.each_with_index.map do |name, i|
287
+ column = columns_hash[name.to_s]
288
+
289
+ raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
290
+
291
+ column
292
+ end
293
+
294
+ columns_sql = "(#{column_names.map{|name| connection.quote_column_name(name) }.join(',')})"
295
+ insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{quoted_table_name} #{columns_sql} VALUES "
296
+ values_sql = values_sql_for_columns_and_attributes(columns, array_of_attributes)
297
+ if not supports_import?
298
+ number_inserted = 0
299
+ values_sql.each do |values|
300
+ connection.execute(insert_sql + values)
301
+ number_inserted += 1
302
+ end
303
+ else
304
+ # generate the sql
305
+ post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
306
+
307
+ # perform the inserts
308
+ number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
309
+ values_sql,
310
+ "#{self.class.name} Create Many Without Validations Or Callbacks" )
311
+ end
312
+ number_inserted
313
+ end
314
+
315
+ private
316
+
317
+ # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
318
+ # and +array_of_attributes+.
319
+ def values_sql_for_columns_and_attributes(columns, array_of_attributes) # :nodoc:
320
+ # connection gets called a *lot* in this high intensity loop.
321
+ # Reuse the same one w/in the loop, otherwise it would keep being re-retreived (= lots of time for large imports)
322
+ connection_memo = connection
323
+ array_of_attributes.map do |arr|
324
+ my_values = arr.each_with_index.map do |val,j|
325
+ column = columns[j]
326
+
327
+ # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
328
+ if val.nil? && column.name == primary_key && !sequence_name.blank?
329
+ connection_memo.next_value_for_sequence(sequence_name)
330
+ else
331
+ if serialized_attributes.include?(column.name)
332
+ connection_memo.quote(serialized_attributes[column.name].dump(val), column)
333
+ else
334
+ connection_memo.quote(val, column)
335
+ end
336
+ end
337
+ end
338
+ "(#{my_values.join(',')})"
339
+ end
340
+ end
341
+
342
+ def add_special_rails_stamps( column_names, array_of_attributes, options )
343
+ AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
344
+ if self.column_names.include?(key)
345
+ value = blk.call
346
+ if index=column_names.index(key)
347
+ # replace every instance of the array of attributes with our value
348
+ array_of_attributes.each{ |arr| arr[index] = value }
349
+ else
350
+ column_names << key
351
+ array_of_attributes.each { |arr| arr << value }
352
+ end
353
+ end
354
+ end
355
+
356
+ AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
357
+ if self.column_names.include?(key)
358
+ value = blk.call
359
+ if index=column_names.index(key)
360
+ # replace every instance of the array of attributes with our value
361
+ array_of_attributes.each{ |arr| arr[index] = value }
362
+ else
363
+ column_names << key
364
+ array_of_attributes.each { |arr| arr << value }
365
+ end
366
+
367
+ if supports_on_duplicate_key_update?
368
+ if options[:on_duplicate_key_update]
369
+ options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array)
370
+ options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
371
+ else
372
+ options[:on_duplicate_key_update] = [ key.to_sym ]
373
+ end
374
+ end
375
+ end
376
+ end
377
+ end
378
+
379
+ # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
380
+ def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
381
+ array_of_attributes.map do |attributes|
382
+ Hash[attributes.each_with_index.map {|attr, c| [column_names[c], attr] }]
383
+ end
384
+ end
385
+
386
+ end
387
+ end
@@ -0,0 +1,8 @@
1
+ warn <<-MSG
2
+ [DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
3
+ is deprecated. Update to autorequire using 'require "activerecord-import"'. See
4
+ http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
5
+ MSG
6
+
7
+ require File.expand_path(File.join(File.dirname(__FILE__), "/../activerecord-import-rails4"))
8
+
@@ -0,0 +1,8 @@
1
+ warn <<-MSG
2
+ [DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
3
+ is deprecated. Update to autorequire using 'require "activerecord-import"'. See
4
+ http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
5
+ MSG
6
+
7
+ require File.expand_path(File.join(File.dirname(__FILE__), "/../activerecord-import-rails4"))
8
+