activerecord-import 0.11.0 → 0.12.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 (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