Empact-activerecord-import 0.3.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 (41) hide show
  1. data/README.markdown +25 -0
  2. data/Rakefile +72 -0
  3. data/VERSION +1 -0
  4. data/lib/activerecord-import.rb +16 -0
  5. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +10 -0
  6. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +6 -0
  7. data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +6 -0
  8. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +7 -0
  9. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +7 -0
  10. data/lib/activerecord-import/adapters/abstract_adapter.rb +125 -0
  11. data/lib/activerecord-import/adapters/mysql_adapter.rb +50 -0
  12. data/lib/activerecord-import/adapters/postgresql_adapter.rb +13 -0
  13. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -0
  14. data/lib/activerecord-import/base.rb +27 -0
  15. data/lib/activerecord-import/import.rb +347 -0
  16. data/lib/activerecord-import/mysql.rb +8 -0
  17. data/lib/activerecord-import/mysql2.rb +8 -0
  18. data/lib/activerecord-import/postgresql.rb +8 -0
  19. data/lib/activerecord-import/sqlite3.rb +8 -0
  20. data/test/active_record/connection_adapter_test.rb +52 -0
  21. data/test/adapters/mysql.rb +1 -0
  22. data/test/adapters/mysql2.rb +1 -0
  23. data/test/adapters/postgresql.rb +1 -0
  24. data/test/adapters/sqlite3.rb +1 -0
  25. data/test/import_test.rb +202 -0
  26. data/test/models/book.rb +3 -0
  27. data/test/models/group.rb +3 -0
  28. data/test/models/topic.rb +7 -0
  29. data/test/mysql/import_test.rb +6 -0
  30. data/test/mysql2/import_test.rb +6 -0
  31. data/test/postgresql/import_test.rb +20 -0
  32. data/test/schema/generic_schema.rb +98 -0
  33. data/test/schema/mysql_schema.rb +17 -0
  34. data/test/schema/version.rb +4 -0
  35. data/test/support/active_support/test_case_extensions.rb +67 -0
  36. data/test/support/factories.rb +13 -0
  37. data/test/support/generate.rb +29 -0
  38. data/test/support/mysql/assertions.rb +55 -0
  39. data/test/support/mysql/import_examples.rb +117 -0
  40. data/test/test_helper.rb +46 -0
  41. metadata +169 -0
data/README.markdown ADDED
@@ -0,0 +1,25 @@
1
+ # activerecord-import
2
+
3
+ activerecord-import is a library for bulk inserting data using ActiveRecord.
4
+
5
+ For more information on activerecord-import please see its wiki: http://wiki.github.com/zdennis/activerecord-import/
6
+
7
+ # License
8
+
9
+ This is licensed under the ruby license.
10
+
11
+ # Author
12
+
13
+ Zach Dennis (zach.dennis@gmail.com)
14
+
15
+ # Contributors
16
+
17
+ * Blythe Dunham
18
+ * Gabe da Silveira
19
+ * Henry Work
20
+ * James Herdman
21
+ * Marcus Crafter
22
+ * Thibaud Guillaume-Gentil
23
+ * Mark Van Holstyn
24
+ * Victor Costan
25
+ * Ben Woosley - Postgres support, general cleanup
data/Rakefile ADDED
@@ -0,0 +1,72 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+
7
+ begin
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = "Empact-activerecord-import"
11
+ gem.summary = %Q{Bulk-loading extension for ActiveRecord}
12
+ gem.description = %Q{Extraction of the ActiveRecord::Base#import functionality from ar-extensions for Rails 3 and beyond}
13
+ gem.email = "ben.woosley@gmail.com"
14
+ gem.homepage = "http://github.com/Empact/activerecord-import"
15
+ gem.authors = ["Zach Dennis", "Ben Woosley"]
16
+ gem.files = FileList["VERSION", "Rakefile", "README*", "lib/**/*"]
17
+
18
+ bundler = Bundler.load
19
+ bundler.dependencies_for(:default).each do |dependency|
20
+ gem.add_dependency dependency.name, *dependency.requirements_list
21
+ end
22
+
23
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
24
+ end
25
+ Jeweler::GemcutterTasks.new
26
+ rescue LoadError
27
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
28
+ end
29
+
30
+ namespace :display do
31
+ task :notice do
32
+ puts
33
+ puts "To run tests you must supply the adapter, see rake -T for more information."
34
+ puts
35
+ end
36
+ end
37
+ task :default => ["display:notice"]
38
+
39
+ ADAPTERS = %w(mysql mysql2 postgresql sqlite3)
40
+ ADAPTERS.each do |adapter|
41
+ namespace :test do
42
+ desc "Runs #{adapter} database tests."
43
+ Rake::TestTask.new(adapter) do |t|
44
+ t.test_files = FileList["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/#{adapter}/**/*_test.rb"]
45
+ end
46
+ task adapter
47
+ end
48
+ end
49
+
50
+ begin
51
+ require 'rcov/rcovtask'
52
+ adapter = ENV['ARE_DB']
53
+ Rcov::RcovTask.new do |test|
54
+ test.libs << 'test'
55
+ test.pattern = ["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/#{adapter}/**/*_test.rb"]
56
+ test.verbose = true
57
+ end
58
+ rescue LoadError
59
+ task :rcov do
60
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install rcov"
61
+ end
62
+ end
63
+
64
+ require 'rake/rdoctask'
65
+ Rake::RDocTask.new do |rdoc|
66
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
67
+
68
+ rdoc.rdoc_dir = 'rdoc'
69
+ rdoc.title = "activerecord-import #{version}"
70
+ rdoc.rdoc_files.include('README*')
71
+ rdoc.rdoc_files.include('lib/**/*.rb')
72
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1,16 @@
1
+ class ActiveRecord::Base
2
+ class << self
3
+ def establish_connection_with_activerecord_import(*args)
4
+ establish_connection_without_activerecord_import(*args)
5
+ ActiveSupport.run_load_hooks(:active_record_connection_established, connection)
6
+ end
7
+ alias_method_chain :establish_connection, :activerecord_import
8
+ end
9
+ end
10
+
11
+ ActiveSupport.on_load(:active_record_connection_established) do |connection|
12
+ if !ActiveRecord.const_defined?(:Import) || !ActiveRecord::Import.respond_to?(:load_from_connection)
13
+ require File.join File.dirname(__FILE__), "activerecord-import/base"
14
+ end
15
+ ActiveRecord::Import.load_from_connection connection
16
+ end
@@ -0,0 +1,10 @@
1
+ require "activerecord-import/adapters/abstract_adapter"
2
+
3
+ module ActiveRecord # :nodoc:
4
+ module ConnectionAdapters # :nodoc:
5
+ class AbstractAdapter # :nodoc:
6
+ extend ActiveRecord::Import::AbstractAdapter::ClassMethods
7
+ include ActiveRecord::Import::AbstractAdapter::InstanceMethods
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/mysql2_adapter"
2
+ require "activerecord-import/adapters/mysql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
5
+ include ActiveRecord::Import::MysqlAdapter::InstanceMethods
6
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/mysql_adapter"
2
+ require "activerecord-import/adapters/mysql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::MysqlAdapter
5
+ include ActiveRecord::Import::MysqlAdapter::InstanceMethods
6
+ end
@@ -0,0 +1,7 @@
1
+ require "active_record/connection_adapters/postgresql_adapter"
2
+ require "activerecord-import/adapters/postgresql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
+ include ActiveRecord::Import::PostgreSQLAdapter::InstanceMethods
6
+ end
7
+
@@ -0,0 +1,7 @@
1
+ require "active_record/connection_adapters/sqlite3_adapter"
2
+ require "activerecord-import/adapters/sqlite3_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::Sqlite3Adapter
5
+ include ActiveRecord::Import::Sqlite3Adapter::InstanceMethods
6
+ end
7
+
@@ -0,0 +1,125 @@
1
+ module ActiveRecord::Import::AbstractAdapter
2
+ NO_MAX_PACKET = 0
3
+ QUERY_OVERHEAD = 8 #This was shown to be true for MySQL, but it's not clear where the overhead is from.
4
+
5
+ module ClassMethods
6
+ # Returns the sum of the sizes of the passed in objects. This should
7
+ # probably be moved outside this class, but to where?
8
+ def sum_sizes( *objects ) # :nodoc:
9
+ objects.inject( 0 ){|sum,o| sum += o.size }
10
+ end
11
+
12
+ def get_insert_value_sets( values, sql_size, max_bytes ) # :nodoc:
13
+ value_sets = []
14
+ arr, current_arr_values_size, current_size = [], 0, 0
15
+ values.each_with_index do |val,i|
16
+ comma_bytes = arr.size
17
+ sql_size_thus_far = sql_size + current_size + val.size + comma_bytes
18
+ if NO_MAX_PACKET == max_bytes or sql_size_thus_far <= max_bytes
19
+ current_size += val.size
20
+ arr << val
21
+ else
22
+ value_sets << arr
23
+ arr = [ val ]
24
+ current_size = val.size
25
+ end
26
+
27
+ # if we're on the last iteration push whatever we have in arr to value_sets
28
+ value_sets << arr if i == (values.size-1)
29
+ end
30
+ [ *value_sets ]
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+ def next_value_for_sequence(sequence_name)
36
+ %{#{sequence_name}.nextval}
37
+ end
38
+
39
+ # +sql+ can be a single string or an array. If it is an array all
40
+ # elements that are in position >= 1 will be appended to the final SQL.
41
+ def insert_many( sql, values, *args ) # :nodoc:
42
+ # the number of inserts default
43
+ number_of_inserts = 0
44
+
45
+ base_sql,post_sql = if sql.is_a?( String )
46
+ [ sql, '' ]
47
+ elsif sql.is_a?( Array )
48
+ [ sql.shift, sql.join( ' ' ) ]
49
+ end
50
+
51
+ sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
52
+
53
+ # the number of bytes the requested insert statement values will take up
54
+ values_in_bytes = self.class.sum_sizes( *values )
55
+
56
+ # the number of bytes (commas) it will take to comma separate our values
57
+ comma_separated_bytes = values.size-1
58
+
59
+ # the total number of bytes required if this statement is one statement
60
+ total_bytes = sql_size + values_in_bytes + comma_separated_bytes
61
+
62
+ max = max_allowed_packet
63
+
64
+ # if we can insert it all as one statement
65
+ if NO_MAX_PACKET == max or total_bytes < max
66
+ number_of_inserts += 1
67
+ sql2insert = base_sql + values.join( ',' ) + post_sql
68
+ insert( sql2insert, *args )
69
+ else
70
+ value_sets = self.class.get_insert_value_sets( values, sql_size, max )
71
+ value_sets.each do |values|
72
+ number_of_inserts += 1
73
+ sql2insert = base_sql + values.join( ',' ) + post_sql
74
+ insert( sql2insert, *args )
75
+ end
76
+ end
77
+
78
+ number_of_inserts
79
+ end
80
+
81
+ def pre_sql_statements(options)
82
+ sql = []
83
+ sql << options[:pre_sql] if options[:pre_sql]
84
+ sql << options[:command] if options[:command]
85
+ sql << "IGNORE" if options[:ignore]
86
+
87
+ #add keywords like IGNORE or DELAYED
88
+ if options[:keywords].is_a?(Array)
89
+ sql.concat(options[:keywords])
90
+ elsif options[:keywords]
91
+ sql << options[:keywords].to_s
92
+ end
93
+
94
+ sql
95
+ end
96
+
97
+ # Synchronizes the passed in ActiveRecord instances with the records in
98
+ # the database by calling +reload+ on each instance.
99
+ def after_import_synchronize( instances )
100
+ instances.each { |e| e.reload }
101
+ end
102
+
103
+ # Returns an array of post SQL statements given the passed in options.
104
+ def post_sql_statements( table_name, options ) # :nodoc:
105
+ post_sql_statements = []
106
+ if options[:on_duplicate_key_update]
107
+ post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
108
+ end
109
+
110
+ #custom user post_sql
111
+ post_sql_statements << options[:post_sql] if options[:post_sql]
112
+
113
+ #with rollup
114
+ post_sql_statements << rollup_sql if options[:rollup]
115
+
116
+ post_sql_statements
117
+ end
118
+
119
+ # Returns the maximum number of bytes that the server will allow
120
+ # in a single packet
121
+ def max_allowed_packet
122
+ NO_MAX_PACKET
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,50 @@
1
+ module ActiveRecord::Import::MysqlAdapter
2
+ module InstanceMethods
3
+ def self.included(klass)
4
+ klass.instance_eval do
5
+ include ActiveRecord::Import::ImportSupport
6
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
7
+ end
8
+ end
9
+
10
+ # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
11
+ # in +args+.
12
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
13
+ sql = ' ON DUPLICATE KEY UPDATE '
14
+ arg = args.first
15
+ if arg.is_a?( Array )
16
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, arg )
17
+ elsif arg.is_a?( Hash )
18
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, arg )
19
+ elsif arg.is_a?( String )
20
+ sql << arg
21
+ else
22
+ raise ArgumentError.new( "Expected Array or Hash" )
23
+ end
24
+ sql
25
+ end
26
+
27
+ def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
28
+ results = arr.map do |column|
29
+ qc = quote_column_name( column )
30
+ "#{table_name}.#{qc}=VALUES(#{qc})"
31
+ end
32
+ results.join( ',' )
33
+ end
34
+
35
+ def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
36
+ sql = ' ON DUPLICATE KEY UPDATE '
37
+ results = hsh.map do |column1, column2|
38
+ qc1 = quote_column_name( column1 )
39
+ qc2 = quote_column_name( column2 )
40
+ "#{table_name}.#{qc1}=VALUES( #{qc2} )"
41
+ end
42
+ results.join( ',')
43
+ end
44
+
45
+ #return true if the statement is a duplicate key record error
46
+ def duplicate_key_update_error?(exception)# :nodoc:
47
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveRecord::Import::PostgreSQLAdapter
2
+ module InstanceMethods
3
+ def self.included(klass)
4
+ klass.instance_eval do
5
+ include ActiveRecord::Import::ImportSupport
6
+ end
7
+ end
8
+
9
+ def next_value_for_sequence(sequence_name)
10
+ %{nextval('#{sequence_name}')}
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveRecord::Import::Sqlite3Adapter
2
+ module InstanceMethods
3
+ def next_value_for_sequence(sequence_name)
4
+ %{nextval('#{sequence_name}')}
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,27 @@
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
+ # Loads the import functionality for a specific database adapter
9
+ def self.require_adapter(adapter)
10
+ require File.join(AdapterPath,"/abstract_adapter")
11
+ require File.join(AdapterPath,"/#{adapter}_adapter")
12
+ end
13
+
14
+ # Loads the import functionality for the passed in ActiveRecord connection
15
+ def self.load_from_connection(connection)
16
+ import_adapter = "ActiveRecord::Import::#{connection.class.name.demodulize}::InstanceMethods"
17
+ unless connection.class.ancestors.map(&:name).include?(import_adapter)
18
+ config = connection.instance_variable_get :@config
19
+ require_adapter config[:adapter]
20
+ end
21
+ end
22
+ end
23
+
24
+
25
+ this_dir = Pathname.new File.dirname(__FILE__)
26
+ require this_dir.join("import")
27
+ require this_dir.join("active_record/adapters/abstract_adapter")
@@ -0,0 +1,347 @@
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
+ end
21
+
22
+ class ActiveRecord::Base
23
+ class << self
24
+
25
+ # use tz as set in ActiveRecord::Base
26
+ tproc = lambda do
27
+ ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
28
+ end
29
+
30
+ AREXT_RAILS_COLUMNS = {
31
+ :create => { "created_on" => tproc ,
32
+ "created_at" => tproc },
33
+ :update => { "updated_on" => tproc ,
34
+ "updated_at" => tproc }
35
+ }
36
+ AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
37
+
38
+ # Returns true if the current database connection adapter
39
+ # supports import functionality, otherwise returns false.
40
+ def supports_import?
41
+ connection.supports_import?
42
+ rescue NoMethodError
43
+ false
44
+ end
45
+
46
+ # Returns true if the current database connection adapter
47
+ # supports on duplicate key update functionality, otherwise
48
+ # returns false.
49
+ def supports_on_duplicate_key_update?
50
+ connection.supports_on_duplicate_key_update?
51
+ rescue NoMethodError
52
+ false
53
+ end
54
+
55
+ # Imports a collection of values to the database.
56
+ #
57
+ # This is more efficient than using ActiveRecord::Base#create or
58
+ # ActiveRecord::Base#save multiple times. This method works well if
59
+ # you want to create more than one record at a time and do not care
60
+ # about having ActiveRecord objects returned for each record
61
+ # inserted.
62
+ #
63
+ # This can be used with or without validations. It does not utilize
64
+ # the ActiveRecord::Callbacks during creation/modification while
65
+ # performing the import.
66
+ #
67
+ # == Usage
68
+ # Model.import array_of_models
69
+ # Model.import column_names, array_of_values
70
+ # Model.import column_names, array_of_values, options
71
+ #
72
+ # ==== Model.import array_of_models
73
+ #
74
+ # With this form you can call _import_ passing in an array of model
75
+ # objects that you want updated.
76
+ #
77
+ # ==== Model.import column_names, array_of_values
78
+ #
79
+ # The first parameter +column_names+ is an array of symbols or
80
+ # strings which specify the columns that you want to update.
81
+ #
82
+ # The second parameter, +array_of_values+, is an array of
83
+ # arrays. Each subarray is a single set of values for a new
84
+ # record. The order of values in each subarray should match up to
85
+ # the order of the +column_names+.
86
+ #
87
+ # ==== Model.import column_names, array_of_values, options
88
+ #
89
+ # The first two parameters are the same as the above form. The third
90
+ # parameter, +options+, is a hash. This is optional. Please see
91
+ # below for what +options+ are available.
92
+ #
93
+ # == Options
94
+ # * +validate+ - true|false, tells import whether or not to use \
95
+ # ActiveRecord validations. Validations are enforced by default.
96
+ # * +on_duplicate_key_update+ - an Array or Hash, tells import to \
97
+ # use MySQL's ON DUPLICATE KEY UPDATE ability. See On Duplicate\
98
+ # Key Update below.
99
+ # * +synchronize+ - an array of ActiveRecord instances for the model
100
+ # that you are currently importing data into. This synchronizes
101
+ # existing model instances in memory with updates from the import.
102
+ # * +timestamps+ - true|false, tells import to not add timestamps \
103
+ # (if false) even if record timestamps is disabled in ActiveRecord::Base
104
+ #
105
+ # == Examples
106
+ # class BlogPost < ActiveRecord::Base ; end
107
+ #
108
+ # # Example using array of model objects
109
+ # posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT',
110
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2',
111
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT3' ]
112
+ # BlogPost.import posts
113
+ #
114
+ # # Example using column_names and array_of_values
115
+ # columns = [ :author_name, :title ]
116
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
117
+ # BlogPost.import columns, values
118
+ #
119
+ # # Example using column_names, array_of_value and options
120
+ # columns = [ :author_name, :title ]
121
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
122
+ # BlogPost.import( columns, values, :validate => false )
123
+ #
124
+ # # Example synchronizing existing instances in memory
125
+ # post = BlogPost.find_by_author_name( 'zdennis' )
126
+ # puts post.author_name # => 'zdennis'
127
+ # columns = [ :author_name, :title ]
128
+ # values = [ [ 'yoda', 'test post' ] ]
129
+ # BlogPost.import posts, :synchronize=>[ post ]
130
+ # puts post.author_name # => 'yoda'
131
+ #
132
+ # == On Duplicate Key Update (MySQL only)
133
+ #
134
+ # The :on_duplicate_key_update option can be either an Array or a Hash.
135
+ #
136
+ # ==== Using an Array
137
+ #
138
+ # The :on_duplicate_key_update option can be an array of column
139
+ # names. The column names are the only fields that are updated if
140
+ # a duplicate record is found. Below is an example:
141
+ #
142
+ # BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
143
+ #
144
+ # ==== Using A Hash
145
+ #
146
+ # The :on_duplicate_key_update option can be a hash of column name
147
+ # to model attribute name mappings. This gives you finer grained
148
+ # control over what fields are updated with what attributes on your
149
+ # model. Below is an example:
150
+ #
151
+ # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
152
+ #
153
+ # = Returns
154
+ # This returns an object which responds to +failed_instances+ and +num_inserts+.
155
+ # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
156
+ # * num_inserts - the number of insert statements it took to import the data
157
+ def import( *args )
158
+ options = { :validate=>true, :timestamps=>true }
159
+ options.merge!( args.pop ) if args.last.is_a? Hash
160
+
161
+ is_validating = options.delete( :validate )
162
+
163
+ # assume array of model objects
164
+ if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
165
+ if args.length == 2
166
+ models = args.last
167
+ column_names = args.first
168
+ else
169
+ models = args.first
170
+ column_names = self.column_names.dup
171
+ end
172
+
173
+ array_of_attributes = models.map do |model|
174
+ # this next line breaks sqlite.so with a segmentation fault
175
+ # if model.new_record? || options[:on_duplicate_key_update]
176
+ column_names.map do |name|
177
+ model.send( "#{name}_before_type_cast" )
178
+ end
179
+ # end
180
+ end
181
+ # supports 2-element array and array
182
+ elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
183
+ column_names, array_of_attributes = args
184
+ else
185
+ raise ArgumentError.new( "Invalid arguments!" )
186
+ end
187
+
188
+ # dup the passed in array so we don't modify it unintentionally
189
+ array_of_attributes = array_of_attributes.dup
190
+
191
+ # Force the primary key col into the insert if it's not
192
+ # on the list and we are using a sequence and stuff a nil
193
+ # value for it into each row so the sequencer will fire later
194
+ if !column_names.include?(primary_key) && sequence_name && connection.prefetch_primary_key?
195
+ column_names << primary_key
196
+ array_of_attributes.each { |a| a << nil }
197
+ end
198
+
199
+ # record timestamps unless disabled in ActiveRecord::Base
200
+ if record_timestamps && options.delete( :timestamps )
201
+ add_special_rails_stamps column_names, array_of_attributes, options
202
+ end
203
+
204
+ return_obj = if is_validating
205
+ import_with_validations( column_names, array_of_attributes, options )
206
+ else
207
+ num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
208
+ ActiveRecord::Import::Result.new([], num_inserts)
209
+ end
210
+
211
+ if options[:synchronize]
212
+ synchronize( options[:synchronize] )
213
+ end
214
+
215
+ return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
216
+ return_obj
217
+ end
218
+
219
+ # TODO import_from_table needs to be implemented.
220
+ def import_from_table( options ) # :nodoc:
221
+ end
222
+
223
+ # Imports the passed in +column_names+ and +array_of_attributes+
224
+ # given the passed in +options+ Hash with validations. Returns an
225
+ # object with the methods +failed_instances+ and +num_inserts+.
226
+ # +failed_instances+ is an array of instances that failed validations.
227
+ # +num_inserts+ is the number of inserts it took to import the data. See
228
+ # ActiveRecord::Base.import for more information on
229
+ # +column_names+, +array_of_attributes+ and +options+.
230
+ def import_with_validations( column_names, array_of_attributes, options={} )
231
+ failed_instances = []
232
+
233
+ # create instances for each of our column/value sets
234
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
235
+
236
+ # keep track of the instance and the position it is currently at. if this fails
237
+ # validation we'll use the index to remove it from the array_of_attributes
238
+ arr.each_with_index do |hsh,i|
239
+ instance = new( hsh )
240
+ if not instance.valid?
241
+ array_of_attributes[ i ] = nil
242
+ failed_instances << instance
243
+ end
244
+ end
245
+ array_of_attributes.compact!
246
+
247
+ num_inserts = array_of_attributes.empty? ? 0 : import_without_validations_or_callbacks( column_names, array_of_attributes, options )
248
+ ActiveRecord::Import::Result.new(failed_instances, num_inserts)
249
+ end
250
+
251
+ # Imports the passed in +column_names+ and +array_of_attributes+
252
+ # given the passed in +options+ Hash. This will return the number
253
+ # of insert operations it took to create these records without
254
+ # validations or callbacks. See ActiveRecord::Base.import for more
255
+ # information on +column_names+, +array_of_attributes_ and
256
+ # +options+.
257
+ def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
258
+ columns_sql = "(#{column_names.map{|name| connection.quote_column_name(name) }.join(',')})"
259
+ insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{quoted_table_name} #{columns_sql} VALUES "
260
+ values_sql = values_sql_for_column_names_and_attributes(column_names, array_of_attributes)
261
+ if not supports_import?
262
+ number_inserted = 0
263
+ values_sql.each do |values|
264
+ connection.execute(insert_sql + values)
265
+ number_inserted += 1
266
+ end
267
+ else
268
+ # generate the sql
269
+ post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
270
+
271
+ # perform the inserts
272
+ number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
273
+ values_sql,
274
+ "#{self.class.name} Create Many Without Validations Or Callbacks" )
275
+ end
276
+ number_inserted
277
+ end
278
+
279
+ private
280
+
281
+ # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
282
+ # and +array_of_attributes+.
283
+ def values_sql_for_column_names_and_attributes(column_names, array_of_attributes) # :nodoc:
284
+ columns = column_names.map { |name| columns_hash[name] }
285
+
286
+ array_of_attributes.map do |arr|
287
+ my_values = arr.each_with_index.map do |val,j|
288
+ if !sequence_name.blank? && column_names[j] == primary_key && val.nil?
289
+ connection.next_value_for_sequence(sequence_name)
290
+ else
291
+ connection.quote(val, columns[j])
292
+ end
293
+ end
294
+ "(#{my_values.join(',')})"
295
+ end
296
+ end
297
+
298
+ def add_special_rails_stamps( column_names, array_of_attributes, options )
299
+ AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
300
+ if self.column_names.include?(key)
301
+ value = blk.call
302
+ if index=column_names.index(key)
303
+ # replace every instance of the array of attributes with our value
304
+ array_of_attributes.each{ |arr| arr[index] = value }
305
+ else
306
+ column_names << key
307
+ array_of_attributes.each { |arr| arr << value }
308
+ end
309
+ end
310
+ end
311
+
312
+ AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
313
+ if self.column_names.include?(key)
314
+ value = blk.call
315
+ if index=column_names.index(key)
316
+ # replace every instance of the array of attributes with our value
317
+ array_of_attributes.each{ |arr| arr[index] = value }
318
+ else
319
+ column_names << key
320
+ array_of_attributes.each { |arr| arr << value }
321
+ end
322
+
323
+ if supports_on_duplicate_key_update?
324
+ if options[:on_duplicate_key_update]
325
+ options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array)
326
+ options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
327
+ else
328
+ options[:on_duplicate_key_update] = [ key.to_sym ]
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+
335
+ # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
336
+ def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
337
+ arr = []
338
+ array_of_attributes.each do |attributes|
339
+ c = 0
340
+ hsh = attributes.inject( {} ){|hsh,attr| hsh[ column_names[c] ] = attr ; c+=1 ; hsh }
341
+ arr << hsh
342
+ end
343
+ arr
344
+ end
345
+
346
+ end
347
+ end