activerecord-import 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/Gemfile +4 -2
  4. data/README.markdown +8 -7
  5. data/Rakefile +1 -1
  6. data/benchmarks/benchmark.rb +2 -1
  7. data/benchmarks/lib/cli_parser.rb +1 -1
  8. data/benchmarks/lib/{mysql_benchmark.rb → mysql2_benchmark.rb} +6 -7
  9. data/gemfiles/3.1.gemfile +0 -2
  10. data/gemfiles/3.2.gemfile +0 -2
  11. data/gemfiles/4.0.gemfile +0 -2
  12. data/gemfiles/4.1.gemfile +0 -2
  13. data/gemfiles/4.2.gemfile +0 -2
  14. data/gemfiles/5.0.gemfile +0 -2
  15. data/lib/activerecord-import/adapters/abstract_adapter.rb +11 -2
  16. data/lib/activerecord-import/adapters/mysql_adapter.rb +14 -2
  17. data/lib/activerecord-import/adapters/postgresql_adapter.rb +105 -1
  18. data/lib/activerecord-import/base.rb +0 -1
  19. data/lib/activerecord-import/import.rb +92 -21
  20. data/lib/activerecord-import/version.rb +1 -1
  21. data/test/database.yml.sample +0 -12
  22. data/test/import_test.rb +1 -1
  23. data/test/jdbcmysql/import_test.rb +2 -2
  24. data/test/jdbcpostgresql/import_test.rb +0 -1
  25. data/test/models/book.rb +4 -4
  26. data/test/models/promotion.rb +3 -0
  27. data/test/models/question.rb +3 -0
  28. data/test/models/rule.rb +3 -0
  29. data/test/models/topic.rb +1 -1
  30. data/test/mysql2/import_test.rb +2 -3
  31. data/test/mysqlspatial2/import_test.rb +2 -2
  32. data/test/postgresql/import_test.rb +4 -0
  33. data/test/schema/generic_schema.rb +19 -2
  34. data/test/support/{mysql/assertions.rb → assertions.rb} +12 -3
  35. data/test/support/factories.rb +14 -0
  36. data/test/support/mysql/import_examples.rb +28 -119
  37. data/test/support/postgresql/import_examples.rb +156 -1
  38. data/test/support/shared_examples/on_duplicate_key_update.rb +92 -0
  39. data/test/test_helper.rb +5 -1
  40. data/test/travis/build.sh +12 -8
  41. data/test/travis/database.yml +0 -12
  42. metadata +14 -23
  43. data/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb +0 -8
  44. data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +0 -6
  45. data/lib/activerecord-import/em_mysql2.rb +0 -7
  46. data/lib/activerecord-import/mysql.rb +0 -7
  47. data/test/adapters/em_mysql2.rb +0 -1
  48. data/test/adapters/mysql.rb +0 -1
  49. data/test/adapters/mysqlspatial.rb +0 -1
  50. data/test/em_mysql2/import_test.rb +0 -6
  51. data/test/mysql/import_test.rb +0 -6
  52. data/test/mysqlspatial/import_test.rb +0 -6
  53. data/test/support/em-synchrony_extensions.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a877605fbbb5a9a7100063ad764a46d8a497708c
4
- data.tar.gz: 4eee2b275df655fb6f63f47de0b1b60ae669629c
3
+ metadata.gz: 9f8e8fa0974d52a903ef20a78f196030a515687d
4
+ data.tar.gz: 0ccc98cac5b73a78e9e9ffe57c2b74504c2b9e81
5
5
  SHA512:
6
- metadata.gz: f3457053406910b5e2d4d740ddb5dc2efa716f7ef8e54596998aec575568e62ebfa55570c46270c837bdc1f3c1e145e1ff954441ce58f881343f44cb9ea01be3
7
- data.tar.gz: ae534e1dd917a93dc3c0907bba45e7d1e6a6fb553e1a3a86c08ab8d85cb019c322d70a7cc6c90e7c6b25367b059a730e2c6d886336abc7118d89e8fc22b253d4
6
+ metadata.gz: 1562542547afcea6d646e3a5fcf2aabd806471f0000b2434d6fd979a60c2bc93b90e115e09e2b67df00b35a2244c67d0f2554e9480f0c5c6bb6f3dbdebd2e8be
7
+ data.tar.gz: 45f2552a368c3b9c70bf7abec385462f34a66e423a65db1e99536b8f3b383c02d21af4cb5b5a313c3769aa59a919bd4db34ddb6927ced52fae88272afaeb28de
@@ -0,0 +1,24 @@
1
+ ## Changes in 0.12.0
2
+
3
+ ### New Features
4
+
5
+ * PostgreSQL UPSERT support has been added. Thanks @jkowens via \#218
6
+
7
+ ### Fixes
8
+
9
+ * has_one and has_many associations will now be recursively imported regardless of :autosave being set. Thanks @sferik, @jkowens via \#243, \#234
10
+ * Fixing an issue with enum column support for Rails > 4.1. Thanks @aquajach via \#235
11
+
12
+ ### Removals
13
+
14
+ * Support for em-synchrony has been removed since it appears the project has been abandoned. Thanks @sferik, @zdennis via \#239
15
+ * Support for the mysql gem/adapter has been removed since it has officially been abandoned. Use the mysql2 gem/adapter instead. Thanks @sferik, @zdennis via \#239
16
+
17
+ ### Misc
18
+
19
+ * Cleaned up TravisCI output and removing deprecation warnings. Thanks @jkowens, @zdennis \#242
20
+
21
+
22
+ ## Changes before 0.12.0
23
+
24
+ > Never look back. What's gone is now history. But in the process make memory of events to help you understand what will help you to make your dream a true story. Mistakes of the past are lessons, success of the past is inspiration. – Dr. Anil Kr Sinha
data/Gemfile CHANGED
@@ -2,8 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem "pry-byebug"
6
-
7
5
  # Database Adapters
8
6
  platforms :ruby do
9
7
  gem "mysql2", "~> 0.3.0"
@@ -38,6 +36,10 @@ platforms :mri_19 do
38
36
  gem "debugger"
39
37
  end
40
38
 
39
+ platforms :ruby do
40
+ gem "pry-byebug"
41
+ end
42
+
41
43
  version = ENV['AR_VERSION'] || "4.2"
42
44
 
43
45
  if version > "4.0"
@@ -5,19 +5,19 @@ activerecord-import is a library for bulk inserting data using ActiveRecord.
5
5
  One of its major features is following activerecord associations and generating the minimal
6
6
  number of SQL insert statements required, avoiding the N+1 insert problem. An example probably
7
7
  explains it best. Say you had a schema like this:
8
-
8
+
9
9
  - Publishers have Books
10
10
  - Books have Reviews
11
-
11
+
12
12
  and you wanted to bulk insert 100 new publishers with 10K books and 3 reviews per book. This library will follow the associations
13
13
  down and generate only 3 SQL insert statements - one for the publishers, one for the books, and one for the reviews.
14
-
14
+
15
15
  In contrast, the standard ActiveRecord save would generate
16
16
  100 insert statements for the publishers, then it would visit each publisher and save all the books:
17
17
  100 * 10,000 = 1,000,000 SQL insert statements
18
18
  and then the reviews:
19
19
  100 * 10,000 * 3 = 3M SQL insert statements,
20
-
20
+
21
21
  That would be about 4M SQL insert statements vs 3, which results in vastly improved performance. In our case, it converted
22
22
  an 18 hour batch process to <2 hrs.
23
23
 
@@ -54,7 +54,7 @@ To understand how rubygems loads code you can reference the following:
54
54
  http://guides.rubygems.org/patterns/#loading_code
55
55
 
56
56
  And an example of how active_record dynamically load adapters:
57
- https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/connection_specification.rb
57
+ https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/connection_specification.rb
58
58
 
59
59
  In summary, when a gem is loaded rubygems adds the `lib` folder of the gem to the global load path `$LOAD_PATH` so that all `require` lookups will not propegate through all of the folders on the load path. When a `require` is issued each folder on the `$LOAD_PATH` is checked for the file and/or folder referenced. This allows a gem (like activerecord-import) to define push the activerecord-import folder (or namespace) on the `$LOAD_PATH` and any adapters provided by activerecord-import will be found by rubygems when the require is issued.
60
60
 
@@ -77,7 +77,7 @@ When rubygems pushes the `lib` folder onto the load path a `require` will now fi
77
77
 
78
78
  # License
79
79
 
80
- This is licensed under the ruby license.
80
+ This is licensed under the ruby license.
81
81
 
82
82
  # Author
83
83
 
@@ -85,11 +85,12 @@ Zach Dennis (zach.dennis@gmail.com)
85
85
 
86
86
  # Contributors
87
87
 
88
+ * Jordan Owens
88
89
  * Blythe Dunham
89
90
  * Gabe da Silveira
90
91
  * Henry Work
91
92
  * James Herdman
92
93
  * Marcus Crafter
93
94
  * Thibaud Guillaume-Gentil
94
- * Mark Van Holstyn
95
+ * Mark Van Holstyn
95
96
  * Victor Costan
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ namespace :display do
13
13
  end
14
14
  task :default => ["display:notice"]
15
15
 
16
- ADAPTERS = %w(mysql mysql2 em_mysql2 jdbcmysql jdbcpostgresql postgresql sqlite3 seamless_database_pool mysqlspatial mysql2spatial spatialite postgis)
16
+ ADAPTERS = %w(mysql2 jdbcmysql jdbcpostgresql postgresql sqlite3 seamless_database_pool mysql2spatial spatialite postgis)
17
17
  ADAPTERS.each do |adapter|
18
18
  namespace :test do
19
19
  desc "Runs #{adapter} database tests."
@@ -4,6 +4,8 @@ require "active_record"
4
4
 
5
5
  benchmark_dir = File.dirname(__FILE__)
6
6
 
7
+ $LOAD_PATH.unshift('.')
8
+
7
9
  # Get the gem into the load path
8
10
  $LOAD_PATH.unshift(File.join(benchmark_dir, '..', 'lib'))
9
11
 
@@ -64,4 +66,3 @@ end
64
66
 
65
67
  puts
66
68
  puts "Done with benchmark!"
67
-
@@ -38,7 +38,7 @@ module BenchmarkOptionParser
38
38
 
39
39
  def self.parse( args )
40
40
  options = OpenStruct.new(
41
- :adapter => 'mysql',
41
+ :adapter => 'mysql2',
42
42
  :table_types => {},
43
43
  :delete_on_finish => true,
44
44
  :number_of_objects => [],
@@ -1,22 +1,21 @@
1
- class MysqlBenchmark < BenchmarkBase
2
-
1
+ class Mysql2Benchmark < BenchmarkBase
2
+
3
3
  def benchmark_all( array_of_cols_and_vals )
4
4
  methods = self.methods.find_all { |m| m =~ /benchmark_/ }
5
5
  methods.delete_if{ |m| m =~ /benchmark_(all|model)/ }
6
6
  methods.each { |method| self.send( method, array_of_cols_and_vals ) }
7
7
  end
8
-
8
+
9
9
  def benchmark_myisam( array_of_cols_and_vals )
10
10
  bm_model( TestMyISAM, array_of_cols_and_vals )
11
11
  end
12
-
12
+
13
13
  def benchmark_innodb( array_of_cols_and_vals )
14
14
  bm_model( TestInnoDb, array_of_cols_and_vals )
15
15
  end
16
-
16
+
17
17
  def benchmark_memory( array_of_cols_and_vals )
18
18
  bm_model( TestMemory, array_of_cols_and_vals )
19
19
  end
20
-
21
- end
22
20
 
21
+ end
@@ -1,5 +1,3 @@
1
1
  platforms :ruby do
2
- gem 'mysql', '>= 2.8.1'
3
2
  gem 'activerecord', '~> 3.1.0'
4
- gem 'em-synchrony', '1.0.3'
5
3
  end
@@ -1,5 +1,3 @@
1
1
  platforms :ruby do
2
- gem 'mysql', '>= 2.8.1'
3
2
  gem 'activerecord', '~> 3.2.0'
4
- gem 'em-synchrony', '1.0.3'
5
3
  end
@@ -1,5 +1,3 @@
1
1
  platforms :ruby do
2
- gem 'mysql', '~> 2.9'
3
2
  gem 'activerecord', '~> 4.0'
4
- gem 'em-synchrony', '1.0.3'
5
3
  end
@@ -1,5 +1,3 @@
1
1
  platforms :ruby do
2
- gem 'mysql', '~> 2.9'
3
2
  gem 'activerecord', '~> 4.1'
4
- gem 'em-synchrony', '1.0.4'
5
3
  end
@@ -1,5 +1,3 @@
1
1
  platforms :ruby do
2
- gem 'mysql', '~> 2.9'
3
2
  gem 'activerecord', '~> 4.2'
4
- gem 'em-synchrony', '1.0.4'
5
3
  end
@@ -1,5 +1,3 @@
1
1
  platforms :ruby do
2
- gem 'mysql', '~> 2.9'
3
2
  gem 'activerecord', '~> 5.0.0.beta1'
4
- gem 'em-synchrony', '1.0.4'
5
3
  end
@@ -44,8 +44,13 @@ module ActiveRecord::Import::AbstractAdapter
44
44
  # Returns an array of post SQL statements given the passed in options.
45
45
  def post_sql_statements( table_name, options ) # :nodoc:
46
46
  post_sql_statements = []
47
- if options[:on_duplicate_key_update]
48
- post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
47
+
48
+ if supports_on_duplicate_key_update?
49
+ if options[:on_duplicate_key_ignore] && respond_to?(:sql_for_on_duplicate_key_ignore)
50
+ post_sql_statements << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
51
+ elsif options[:on_duplicate_key_update]
52
+ post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
53
+ end
49
54
  end
50
55
 
51
56
  #custom user post_sql
@@ -62,5 +67,9 @@ module ActiveRecord::Import::AbstractAdapter
62
67
  def max_allowed_packet
63
68
  NO_MAX_PACKET
64
69
  end
70
+
71
+ def supports_on_duplicate_key_update?
72
+ false
73
+ end
65
74
  end
66
75
  end
@@ -60,6 +60,19 @@ module ActiveRecord::Import::MysqlAdapter
60
60
  end
61
61
  end
62
62
 
63
+ # Add a column to be updated on duplicate key update
64
+ def add_column_for_on_duplicate_key_update( column, options={} ) # :nodoc:
65
+ if options.include?(:on_duplicate_key_update)
66
+ columns = options[:on_duplicate_key_update]
67
+ case columns
68
+ when Array then columns << column.to_sym unless columns.include?(column.to_sym)
69
+ when Hash then columns[column.to_sym] = column.to_sym
70
+ end
71
+ else
72
+ options[:on_duplicate_key_update] = [ column.to_sym ]
73
+ end
74
+ end
75
+
63
76
  # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
64
77
  # in +args+.
65
78
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
@@ -86,7 +99,6 @@ module ActiveRecord::Import::MysqlAdapter
86
99
  end
87
100
 
88
101
  def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
89
- sql = ' ON DUPLICATE KEY UPDATE '
90
102
  results = hsh.map do |column1, column2|
91
103
  qc1 = quote_column_name( column1 )
92
104
  qc2 = quote_column_name( column2 )
@@ -95,7 +107,7 @@ module ActiveRecord::Import::MysqlAdapter
95
107
  results.join( ',')
96
108
  end
97
109
 
98
- #return true if the statement is a duplicate key record error
110
+ # Return true if the statement is a duplicate key record error
99
111
  def duplicate_key_update_error?(exception)# :nodoc:
100
112
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
101
113
  end
@@ -1,5 +1,8 @@
1
1
  module ActiveRecord::Import::PostgreSQLAdapter
2
2
  include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
+
5
+ MIN_VERSION_FOR_UPSERT = 90500
3
6
 
4
7
  def insert_many( sql, values, *args ) # :nodoc:
5
8
  number_of_inserts = 1
@@ -24,12 +27,113 @@ module ActiveRecord::Import::PostgreSQLAdapter
24
27
 
25
28
  def post_sql_statements( table_name, options ) # :nodoc:
26
29
  unless options[:primary_key].blank?
27
- super(table_name, options) << (" RETURNING #{options[:primary_key]}")
30
+ super(table_name, options) << ("RETURNING #{options[:primary_key]}")
28
31
  else
29
32
  super(table_name, options)
30
33
  end
31
34
  end
32
35
 
36
+ # Add a column to be updated on duplicate key update
37
+ def add_column_for_on_duplicate_key_update( column, options={} ) # :nodoc:
38
+ arg = options[:on_duplicate_key_update]
39
+ if arg.is_a?( Hash )
40
+ columns = arg.fetch( :columns ) { arg[:columns] = [] }
41
+ case columns
42
+ when Array then columns << column.to_sym unless columns.include?( column.to_sym )
43
+ when Hash then columns[column.to_sym] = column.to_sym
44
+ end
45
+ elsif arg.is_a?( Array )
46
+ arg << column.to_sym unless arg.include?( column.to_sym )
47
+ end
48
+ end
49
+
50
+ # Returns a generated ON CONFLICT DO NOTHING statement given the passed
51
+ # in +args+.
52
+ def sql_for_on_duplicate_key_ignore( table_name, *args ) # :nodoc:
53
+ arg = args.first
54
+ if arg.is_a?( Hash )
55
+ conflict_target = sql_for_conflict_target( arg )
56
+ end
57
+ " ON CONFLICT #{conflict_target}DO NOTHING"
58
+ end
59
+
60
+ # Returns a generated ON CONFLICT DO UPDATE statement given the passed
61
+ # in +args+.
62
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
63
+ arg = args.first
64
+ if arg.is_a?( Array ) || arg.is_a?( String )
65
+ arg = { :columns => arg }
66
+ end
67
+ return unless arg.is_a?( Hash )
68
+
69
+ sql = " ON CONFLICT "
70
+ conflict_target = sql_for_conflict_target( arg )
71
+
72
+ columns = arg.fetch( :columns, [] )
73
+ if columns.respond_to?( :empty? ) && columns.empty?
74
+ return sql << "#{conflict_target}DO NOTHING"
75
+ end
76
+
77
+ conflict_target ||= sql_for_default_conflict_target( table_name )
78
+ unless conflict_target
79
+ raise ArgumentError, 'Expected :conflict_target or :constraint_name to be specified'
80
+ end
81
+
82
+ sql << "#{conflict_target}DO UPDATE SET "
83
+ if columns.is_a?( Array )
84
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, columns )
85
+ elsif columns.is_a?( Hash )
86
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, columns )
87
+ elsif columns.is_a?( String )
88
+ sql << columns
89
+ else
90
+ raise ArgumentError, 'Expected :columns to be an Array or Hash'
91
+ end
92
+ sql
93
+ end
94
+
95
+ def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
96
+ results = arr.map do |column|
97
+ qc = quote_column_name( column )
98
+ "#{qc}=EXCLUDED.#{qc}"
99
+ end
100
+ results.join( ',' )
101
+ end
102
+
103
+ def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
104
+ results = hsh.map do |column1, column2|
105
+ qc1 = quote_column_name( column1 )
106
+ qc2 = quote_column_name( column2 )
107
+ "#{qc1}=EXCLUDED.#{qc2}"
108
+ end
109
+ results.join( ',' )
110
+ end
111
+
112
+ def sql_for_conflict_target( args={} )
113
+ if constraint_name = args[:constraint_name]
114
+ "ON CONSTRAINT #{constraint_name} "
115
+ elsif conflict_target = args[:conflict_target]
116
+ '(' << Array( conflict_target ).join( ', ' ) << ') '
117
+ end
118
+ end
119
+
120
+ def sql_for_default_conflict_target( table_name )
121
+ "(#{primary_key( table_name )}) "
122
+ end
123
+
124
+ # Return true if the statement is a duplicate key record error
125
+ def duplicate_key_update_error?(exception)# :nodoc:
126
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
127
+ end
128
+
129
+ def supports_on_duplicate_key_update?(current_version=self.postgresql_version)
130
+ current_version >= MIN_VERSION_FOR_UPSERT
131
+ end
132
+
133
+ def supports_on_duplicate_key_ignore?(current_version=self.postgresql_version)
134
+ supports_on_duplicate_key_update?(current_version)
135
+ end
136
+
33
137
  def support_setting_primary_key_of_imported_objects?
34
138
  true
35
139
  end
@@ -7,7 +7,6 @@ module ActiveRecord::Import
7
7
 
8
8
  def self.base_adapter(adapter)
9
9
  case adapter
10
- when 'mysqlspatial' then 'mysql'
11
10
  when 'mysql2spatial' then 'mysql2'
12
11
  when 'spatialite' then 'sqlite3'
13
12
  when 'postgis' then 'postgresql'
@@ -116,7 +116,7 @@ class ActiveRecord::Base
116
116
  # supports on duplicate key update functionality, otherwise
117
117
  # returns false.
118
118
  def supports_on_duplicate_key_update?
119
- connection.respond_to?(:supports_on_duplicate_key_update?) && connection.supports_on_duplicate_key_update?
119
+ connection.supports_on_duplicate_key_update?
120
120
  end
121
121
 
122
122
  # returns true if the current database connection adapter
@@ -165,19 +165,23 @@ class ActiveRecord::Base
165
165
  # below for what +options+ are available.
166
166
  #
167
167
  # == Options
168
- # * +validate+ - true|false, tells import whether or not to use \
168
+ # * +validate+ - true|false, tells import whether or not to use
169
169
  # ActiveRecord validations. Validations are enforced by default.
170
- # * +on_duplicate_key_update+ - an Array or Hash, tells import to \
171
- # use MySQL's ON DUPLICATE KEY UPDATE ability. See On Duplicate\
172
- # Key Update below.
170
+ # * +ignore+ - true|false, tells import to use MySQL's INSERT IGNORE
171
+ # to discard records that contain duplicate keys.
172
+ # * +on_duplicate_key_ignore+ - true|false, tells import to use
173
+ # Postgres 9.5+ ON CONFLICT DO NOTHING.
174
+ # * +on_duplicate_key_update+ - an Array or Hash, tells import to
175
+ # use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+ ON CONFLICT
176
+ # DO UPDATE ability. See On Duplicate Key Update below.
173
177
  # * +synchronize+ - an array of ActiveRecord instances for the model
174
178
  # that you are currently importing data into. This synchronizes
175
179
  # existing model instances in memory with updates from the import.
176
- # * +timestamps+ - true|false, tells import to not add timestamps \
180
+ # * +timestamps+ - true|false, tells import to not add timestamps
177
181
  # (if false) even if record timestamps is disabled in ActiveRecord::Base
178
- # * +recursive - true|false, tells import to import all autosave association
179
- # if the adapter supports setting the primary keys of the newly imported
180
- # objects.
182
+ # * +recursive - true|false, tells import to import all has_many/has_one
183
+ # associations if the adapter supports setting the primary keys of the
184
+ # newly imported objects.
181
185
  #
182
186
  # == Examples
183
187
  # class BlogPost < ActiveRecord::Base ; end
@@ -211,7 +215,7 @@ class ActiveRecord::Base
211
215
  # BlogPost.import posts, :synchronize => posts, :synchronize_keys => [:title]
212
216
  # puts posts.first.persisted? # => true
213
217
  #
214
- # == On Duplicate Key Update (MySQL only)
218
+ # == On Duplicate Key Update (MySQL)
215
219
  #
216
220
  # The :on_duplicate_key_update option can be either an Array or a Hash.
217
221
  #
@@ -225,13 +229,73 @@ class ActiveRecord::Base
225
229
  #
226
230
  # ==== Using A Hash
227
231
  #
228
- # The :on_duplicate_key_update option can be a hash of column name
232
+ # The :on_duplicate_key_update option can be a hash of column names
229
233
  # to model attribute name mappings. This gives you finer grained
230
234
  # control over what fields are updated with what attributes on your
231
235
  # model. Below is an example:
232
236
  #
233
237
  # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
234
238
  #
239
+ # == On Duplicate Key Update (Postgres 9.5+)
240
+ #
241
+ # The :on_duplicate_key_update option can be an Array or a Hash with up to
242
+ # two attributes, :conflict_target or :constraint_name and :columns.
243
+ #
244
+ # ==== Using an Array
245
+ #
246
+ # The :on_duplicate_key_update option can be an array of column
247
+ # names. This option only handles inserts that conflict with the
248
+ # primary key. If a table does not have a primary key, this will
249
+ # not work. The column names are the only fields that are updated
250
+ # if a duplicate record is found. Below is an example:
251
+ #
252
+ # BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
253
+ #
254
+ # ==== Using a Hash
255
+ #
256
+ # The :on_duplicate_update option can be a hash with up to two attributes,
257
+ # :conflict_target or constraint_name, and :columns. Unlike MySQL, Postgres
258
+ # requires the conflicting constraint to be explicitly specified. Using this
259
+ # option allows you to specify a constraint other than the primary key.
260
+ #
261
+ # ====== :conflict_target
262
+ #
263
+ # The :conflict_target attribute specifies the columns that make up the
264
+ # conflicting unique constraint and can be a single column or an array of
265
+ # column names. This attribute is ignored if :constraint_name is included,
266
+ # but it is the preferred method of identifying a constraint. It will
267
+ # default to the primary key. Below is an example:
268
+ #
269
+ # BlogPost.import columns, values, :on_duplicate_key_update=>{ :conflict_target => [:author_id, :slug], :columns => [ :date_modified ] }
270
+ #
271
+ # ====== :constraint_name
272
+ #
273
+ # The :constraint_name attribute explicitly identifies the conflicting
274
+ # unique index by name. Postgres documentation discourages using this method
275
+ # of identifying an index unless absolutely necessary. Below is an example:
276
+ #
277
+ # BlogPost.import columns, values, :on_duplicate_key_update=>{ :constraint_name => :blog_posts_pkey, :columns => [ :date_modified ] }
278
+ #
279
+ # ====== :columns
280
+ #
281
+ # The :columns attribute can be either an Array or a Hash.
282
+ #
283
+ # ======== Using an Array
284
+ #
285
+ # The :columns attribute can be an array of column names. The column names
286
+ # are the only fields that are updated if a duplicate record is found.
287
+ # Below is an example:
288
+ #
289
+ # BlogPost.import columns, values, :on_duplicate_key_update=>{ :conflict_target => :slug, :columns => [ :date_modified, :content, :author ] }
290
+ #
291
+ # ======== Using a Hash
292
+ #
293
+ # The :columns option can be a hash of column names to model attribute name
294
+ # mappings. This gives you finer grained control over what fields are updated
295
+ # with what attributes on your model. Below is an example:
296
+ #
297
+ # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :conflict_target => :slug, :columns => { :title => :title } }
298
+ #
235
299
  # = Returns
236
300
  # This returns an object which responds to +failed_instances+ and +num_inserts+.
237
301
  # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
@@ -275,7 +339,14 @@ class ActiveRecord::Base
275
339
  # this next line breaks sqlite.so with a segmentation fault
276
340
  # if model.new_record? || options[:on_duplicate_key_update]
277
341
  column_names.map do |name|
278
- model.read_attribute_before_type_cast(name.to_s)
342
+ name = name.to_s
343
+ if respond_to?(:defined_enums) && defined_enums.has_key?(name) # ActiveRecord 5
344
+ model.read_attribute(name)
345
+ elsif model.class.column_defaults[name].is_a?(Integer)
346
+ model.read_attribute(name)
347
+ else
348
+ model.read_attribute_before_type_cast(name)
349
+ end
279
350
  end
280
351
  # end
281
352
  end
@@ -445,7 +516,6 @@ class ActiveRecord::Base
445
516
  # now, for all the dirty associations, collect them into a new set of models, then recurse.
446
517
  # notes:
447
518
  # does not handle associations that reference themselves
448
- # assumes that the only associations to be saved are marked with :autosave
449
519
  # should probably take a hash to associations to follow.
450
520
  associated_objects_by_class={}
451
521
  models.each {|model| find_associated_objects_for_import(associated_objects_by_class, model) }
@@ -462,12 +532,18 @@ class ActiveRecord::Base
462
532
  def find_associated_objects_for_import(associated_objects_by_class, model)
463
533
  associated_objects_by_class[model.class.name]||={}
464
534
 
465
- model.class.reflect_on_all_autosave_associations.each do |association_reflection|
535
+ association_reflections =
536
+ model.class.reflect_on_all_associations(:has_one) +
537
+ model.class.reflect_on_all_associations(:has_many)
538
+ association_reflections.each do |association_reflection|
466
539
  associated_objects_by_class[model.class.name][association_reflection.name]||=[]
467
540
 
468
541
  association = model.association(association_reflection.name)
469
542
  association.loaded!
470
543
 
544
+ # Wrap target in an array if not already
545
+ association = Array(association.target)
546
+
471
547
  changed_objects = association.select {|a| a.new_record? || a.changed?}
472
548
  changed_objects.each do |child|
473
549
  child.send("#{association_reflection.foreign_key}=", model.id)
@@ -529,13 +605,8 @@ class ActiveRecord::Base
529
605
  array_of_attributes.each { |arr| arr << value }
530
606
  end
531
607
 
532
- if supports_on_duplicate_key_update? and options[:on_duplicate_key_update] != false
533
- if options[:on_duplicate_key_update]
534
- options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array) && !options[:on_duplicate_key_update].include?(key.to_sym)
535
- options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
536
- else
537
- options[:on_duplicate_key_update] = [ key.to_sym ]
538
- end
608
+ if supports_on_duplicate_key_update?
609
+ connection.add_column_for_on_duplicate_key_update(key, options)
539
610
  end
540
611
  end
541
612
  end