activerecord-import-rails4 0.5.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.
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
+