activerecord-import 0.17.2 → 1.1.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 (52) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +40 -23
  4. data/CHANGELOG.md +315 -1
  5. data/Gemfile +23 -13
  6. data/LICENSE +21 -56
  7. data/README.markdown +564 -33
  8. data/Rakefile +2 -1
  9. data/activerecord-import.gemspec +3 -3
  10. data/benchmarks/lib/cli_parser.rb +2 -1
  11. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +0 -0
  12. data/gemfiles/5.1.gemfile +2 -0
  13. data/gemfiles/5.2.gemfile +2 -0
  14. data/gemfiles/6.0.gemfile +2 -0
  15. data/gemfiles/6.1.gemfile +1 -0
  16. data/lib/activerecord-import.rb +2 -15
  17. data/lib/activerecord-import/adapters/abstract_adapter.rb +9 -3
  18. data/lib/activerecord-import/adapters/mysql_adapter.rb +17 -11
  19. data/lib/activerecord-import/adapters/postgresql_adapter.rb +68 -20
  20. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
  21. data/lib/activerecord-import/base.rb +12 -7
  22. data/lib/activerecord-import/import.rb +514 -166
  23. data/lib/activerecord-import/synchronize.rb +2 -2
  24. data/lib/activerecord-import/value_sets_parser.rb +16 -0
  25. data/lib/activerecord-import/version.rb +1 -1
  26. data/test/adapters/makara_postgis.rb +1 -0
  27. data/test/import_test.rb +274 -23
  28. data/test/makara_postgis/import_test.rb +8 -0
  29. data/test/models/account.rb +3 -0
  30. data/test/models/animal.rb +6 -0
  31. data/test/models/bike_maker.rb +7 -0
  32. data/test/models/tag.rb +1 -1
  33. data/test/models/topic.rb +14 -0
  34. data/test/models/user.rb +3 -0
  35. data/test/models/user_token.rb +4 -0
  36. data/test/schema/generic_schema.rb +30 -8
  37. data/test/schema/mysql2_schema.rb +19 -0
  38. data/test/schema/postgresql_schema.rb +18 -0
  39. data/test/schema/sqlite3_schema.rb +13 -0
  40. data/test/support/factories.rb +9 -8
  41. data/test/support/generate.rb +6 -6
  42. data/test/support/mysql/import_examples.rb +14 -2
  43. data/test/support/postgresql/import_examples.rb +220 -1
  44. data/test/support/shared_examples/on_duplicate_key_ignore.rb +15 -9
  45. data/test/support/shared_examples/on_duplicate_key_update.rb +271 -8
  46. data/test/support/shared_examples/recursive_import.rb +91 -21
  47. data/test/support/sqlite3/import_examples.rb +189 -25
  48. data/test/synchronize_test.rb +8 -0
  49. data/test/test_helper.rb +24 -3
  50. data/test/value_sets_bytes_parser_test.rb +13 -2
  51. metadata +32 -13
  52. data/test/schema/mysql_schema.rb +0 -16
data/Rakefile CHANGED
@@ -23,6 +23,7 @@ ADAPTERS = %w(
23
23
  postgresql
24
24
  postgresql_makara
25
25
  postgis
26
+ makara_postgis
26
27
  sqlite3
27
28
  spatialite
28
29
  seamless_database_pool
@@ -31,7 +32,7 @@ ADAPTERS.each do |adapter|
31
32
  namespace :test do
32
33
  desc "Runs #{adapter} database tests."
33
34
  Rake::TestTask.new(adapter) do |t|
34
- # FactoryGirl has an issue with warnings, so turn off, so noisy
35
+ # FactoryBot has an issue with warnings, so turn off, so noisy
35
36
  # t.warning = true
36
37
  t.test_files = FileList["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/active_record/*_test.rb", "test/#{adapter}/**/*_test.rb"]
37
38
  end
@@ -6,8 +6,8 @@ Gem::Specification.new do |gem|
6
6
  gem.email = ["zach.dennis@gmail.com"]
7
7
  gem.summary = "Bulk insert extension for ActiveRecord"
8
8
  gem.description = "A library for bulk inserting data using ActiveRecord."
9
- gem.homepage = "http://github.com/zdennis/activerecord-import"
10
- gem.license = "Ruby"
9
+ gem.homepage = "https://github.com/zdennis/activerecord-import"
10
+ gem.license = "MIT"
11
11
 
12
12
  gem.files = `git ls-files`.split($\)
13
13
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
  gem.version = ActiveRecord::Import::VERSION
18
18
 
19
- gem.required_ruby_version = ">= 1.9.2"
19
+ gem.required_ruby_version = ">= 2.0.0"
20
20
 
21
21
  gem.add_runtime_dependency "activerecord", ">= 3.2"
22
22
  gem.add_development_dependency "rake"
@@ -42,7 +42,8 @@ module BenchmarkOptionParser
42
42
  table_types: {},
43
43
  delete_on_finish: true,
44
44
  number_of_objects: [],
45
- outputs: [] )
45
+ outputs: []
46
+ )
46
47
 
47
48
  opt_parser = OptionParser.new do |opts|
48
49
  opts.banner = BANNER
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 5.1.0'
2
+ gem 'composite_primary_keys', '~> 10.0'
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 5.2.0'
2
+ gem 'composite_primary_keys', '~> 11.0'
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 6.0.0'
2
+ gem 'composite_primary_keys', '~> 12.0'
@@ -0,0 +1 @@
1
+ gem 'activerecord', '~> 6.1.0'
@@ -1,19 +1,6 @@
1
1
  # rubocop:disable Style/FileName
2
+ require "active_support/lazy_load_hooks"
2
3
 
3
4
  ActiveSupport.on_load(:active_record) do
4
- class ActiveRecord::Base
5
- class << self
6
- def establish_connection_with_activerecord_import(*args)
7
- conn = establish_connection_without_activerecord_import(*args)
8
- if !ActiveRecord.const_defined?(:Import) || !ActiveRecord::Import.respond_to?(:load_from_connection_pool)
9
- require "activerecord-import/base"
10
- end
11
-
12
- ActiveRecord::Import.load_from_connection_pool connection_pool
13
- conn
14
- end
15
- alias establish_connection_without_activerecord_import establish_connection
16
- alias establish_connection establish_connection_with_activerecord_import
17
- end
18
- end
5
+ require "activerecord-import/base"
19
6
  end
@@ -16,7 +16,7 @@ module ActiveRecord::Import::AbstractAdapter
16
16
  sql2insert = base_sql + values.join( ',' ) + post_sql
17
17
  insert( sql2insert, *args )
18
18
 
19
- [number_of_inserts, []]
19
+ ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
20
20
  end
21
21
 
22
22
  def pre_sql_statements(options)
@@ -45,8 +45,8 @@ module ActiveRecord::Import::AbstractAdapter
45
45
  post_sql_statements = []
46
46
 
47
47
  if supports_on_duplicate_key_update? && options[:on_duplicate_key_update]
48
- post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update], options[:primary_key] )
49
- elsif options[:on_duplicate_key_update]
48
+ post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update], options[:primary_key], options[:locking_column] )
49
+ elsif logger && options[:on_duplicate_key_update]
50
50
  logger.warn "Ignoring on_duplicate_key_update because it is not supported by the database."
51
51
  end
52
52
 
@@ -59,6 +59,12 @@ module ActiveRecord::Import::AbstractAdapter
59
59
  post_sql_statements
60
60
  end
61
61
 
62
+ def increment_locking_column!(table_name, results, locking_column)
63
+ if locking_column.present?
64
+ results << "\"#{locking_column}\"=#{table_name}.\"#{locking_column}\"+1"
65
+ end
66
+ end
67
+
62
68
  def supports_on_duplicate_key_update?
63
69
  false
64
70
  end
@@ -49,16 +49,16 @@ module ActiveRecord::Import::MysqlAdapter
49
49
  end
50
50
  end
51
51
 
52
- [number_of_inserts, []]
52
+ ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
53
53
  end
54
54
 
55
55
  # Returns the maximum number of bytes that the server will allow
56
56
  # in a single packet
57
57
  def max_allowed_packet # :nodoc:
58
58
  @max_allowed_packet ||= begin
59
- result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
59
+ result = execute( "SELECT @@max_allowed_packet" )
60
60
  # original Mysql gem responds to #fetch_row while Mysql2 responds to #first
61
- val = result.respond_to?(:fetch_row) ? result.fetch_row[1] : result.first[1]
61
+ val = result.respond_to?(:fetch_row) ? result.fetch_row[0] : result.first[0]
62
62
  val.to_i
63
63
  end
64
64
  end
@@ -71,14 +71,11 @@ module ActiveRecord::Import::MysqlAdapter
71
71
 
72
72
  # Add a column to be updated on duplicate key update
73
73
  def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
74
- if options.include?(:on_duplicate_key_update)
75
- columns = options[:on_duplicate_key_update]
74
+ if (columns = options[:on_duplicate_key_update])
76
75
  case columns
77
76
  when Array then columns << column.to_sym unless columns.include?(column.to_sym)
78
77
  when Hash then columns[column.to_sym] = column.to_sym
79
78
  end
80
- elsif !options[:ignore] && !options[:on_duplicate_key_ignore]
81
- options[:on_duplicate_key_update] = [column.to_sym]
82
79
  end
83
80
  end
84
81
 
@@ -87,10 +84,11 @@ module ActiveRecord::Import::MysqlAdapter
87
84
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
88
85
  sql = ' ON DUPLICATE KEY UPDATE '
89
86
  arg = args.first
87
+ locking_column = args.last
90
88
  if arg.is_a?( Array )
91
- sql << sql_for_on_duplicate_key_update_as_array( table_name, arg )
89
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arg )
92
90
  elsif arg.is_a?( Hash )
93
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, arg )
91
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, arg )
94
92
  elsif arg.is_a?( String )
95
93
  sql << arg
96
94
  else
@@ -99,20 +97,22 @@ module ActiveRecord::Import::MysqlAdapter
99
97
  sql
100
98
  end
101
99
 
102
- def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
100
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
103
101
  results = arr.map do |column|
104
102
  qc = quote_column_name( column )
105
103
  "#{table_name}.#{qc}=VALUES(#{qc})"
106
104
  end
105
+ increment_locking_column!(table_name, results, locking_column)
107
106
  results.join( ',' )
108
107
  end
109
108
 
110
- def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
109
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
111
110
  results = hsh.map do |column1, column2|
112
111
  qc1 = quote_column_name( column1 )
113
112
  qc2 = quote_column_name( column2 )
114
113
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
115
114
  end
115
+ increment_locking_column!(table_name, results, locking_column)
116
116
  results.join( ',')
117
117
  end
118
118
 
@@ -120,4 +120,10 @@ module ActiveRecord::Import::MysqlAdapter
120
120
  def duplicate_key_update_error?(exception) # :nodoc:
121
121
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
122
122
  end
123
+
124
+ def increment_locking_column!(table_name, results, locking_column)
125
+ if locking_column.present?
126
+ results << "`#{locking_column}`=#{table_name}.`#{locking_column}`+1"
127
+ end
128
+ end
123
129
  end
@@ -5,9 +5,10 @@ module ActiveRecord::Import::PostgreSQLAdapter
5
5
  MIN_VERSION_FOR_UPSERT = 90_500
6
6
 
7
7
  def insert_many( sql, values, options = {}, *args ) # :nodoc:
8
- primary_key = options[:primary_key]
9
8
  number_of_inserts = 1
9
+ returned_values = []
10
10
  ids = []
11
+ results = []
11
12
 
12
13
  base_sql, post_sql = if sql.is_a?( String )
13
14
  [sql, '']
@@ -17,19 +18,47 @@ module ActiveRecord::Import::PostgreSQLAdapter
17
18
 
18
19
  sql2insert = base_sql + values.join( ',' ) + post_sql
19
20
 
20
- if primary_key.blank? || options[:no_returning]
21
+ columns = returning_columns(options)
22
+ if columns.blank? || (options[:no_returning] && !options[:recursive])
21
23
  insert( sql2insert, *args )
22
24
  else
23
- ids = if primary_key.is_a?( Array )
24
- # Select composite primary keys
25
+ returned_values = if columns.size > 1
26
+ # Select composite columns
25
27
  select_rows( sql2insert, *args )
26
28
  else
27
29
  select_values( sql2insert, *args )
28
30
  end
29
- query_cache.clear if query_cache_enabled
31
+ clear_query_cache if query_cache_enabled
30
32
  end
31
33
 
32
- [number_of_inserts, ids]
34
+ if options[:returning].blank?
35
+ ids = returned_values
36
+ elsif options[:primary_key].blank?
37
+ results = returned_values
38
+ else
39
+ # split primary key and returning columns
40
+ ids, results = split_ids_and_results(returned_values, columns, options)
41
+ end
42
+
43
+ ActiveRecord::Import::Result.new([], number_of_inserts, ids, results)
44
+ end
45
+
46
+ def split_ids_and_results(values, columns, options)
47
+ ids = []
48
+ results = []
49
+ id_indexes = Array(options[:primary_key]).map { |key| columns.index(key) }
50
+ returning_indexes = Array(options[:returning]).map { |key| columns.index(key) }
51
+
52
+ values.each do |value|
53
+ value_array = Array(value)
54
+ ids << id_indexes.map { |i| value_array[i] }
55
+ results << returning_indexes.map { |i| value_array[i] }
56
+ end
57
+
58
+ ids.map!(&:first) if id_indexes.size == 1
59
+ results.map!(&:first) if returning_indexes.size == 1
60
+
61
+ [ids, results]
33
62
  end
34
63
 
35
64
  def next_value_for_sequence(sequence_name)
@@ -44,20 +73,27 @@ module ActiveRecord::Import::PostgreSQLAdapter
44
73
  if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update] && !options[:recursive]
45
74
  sql << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
46
75
  end
47
- elsif options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
76
+ elsif logger && options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
48
77
  logger.warn "Ignoring on_duplicate_key_ignore because it is not supported by the database."
49
78
  end
50
79
 
51
80
  sql += super(table_name, options)
52
81
 
53
- unless options[:no_returning] || options[:primary_key].blank?
54
- primary_key = Array(options[:primary_key])
55
- sql << " RETURNING \"#{primary_key.join('", "')}\""
82
+ columns = returning_columns(options)
83
+ unless columns.blank? || (options[:no_returning] && !options[:recursive])
84
+ sql << " RETURNING \"#{columns.join('", "')}\""
56
85
  end
57
86
 
58
87
  sql
59
88
  end
60
89
 
90
+ def returning_columns(options)
91
+ columns = []
92
+ columns += Array(options[:primary_key]) if options[:primary_key].present?
93
+ columns |= Array(options[:returning]) if options[:returning].present?
94
+ columns
95
+ end
96
+
61
97
  # Add a column to be updated on duplicate key update
62
98
  def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
63
99
  arg = options[:on_duplicate_key_update]
@@ -83,14 +119,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
83
119
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
84
120
  # in +args+.
85
121
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
86
- arg, primary_key = args
122
+ arg, primary_key, locking_column = args
87
123
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
88
124
  return unless arg.is_a?( Hash )
89
125
 
90
- sql = " ON CONFLICT "
126
+ sql = ' ON CONFLICT '
91
127
  conflict_target = sql_for_conflict_target( arg )
92
128
 
93
129
  columns = arg.fetch( :columns, [] )
130
+ condition = arg[:condition]
94
131
  if columns.respond_to?( :empty? ) && columns.empty?
95
132
  return sql << "#{conflict_target}DO NOTHING"
96
133
  end
@@ -102,31 +139,36 @@ module ActiveRecord::Import::PostgreSQLAdapter
102
139
 
103
140
  sql << "#{conflict_target}DO UPDATE SET "
104
141
  if columns.is_a?( Array )
105
- sql << sql_for_on_duplicate_key_update_as_array( table_name, columns )
142
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
106
143
  elsif columns.is_a?( Hash )
107
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, columns )
144
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
108
145
  elsif columns.is_a?( String )
109
146
  sql << columns
110
147
  else
111
148
  raise ArgumentError, 'Expected :columns to be an Array or Hash'
112
149
  end
150
+
151
+ sql << " WHERE #{condition}" if condition.present?
152
+
113
153
  sql
114
154
  end
115
155
 
116
- def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
156
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
117
157
  results = arr.map do |column|
118
158
  qc = quote_column_name( column )
119
159
  "#{qc}=EXCLUDED.#{qc}"
120
160
  end
161
+ increment_locking_column!(table_name, results, locking_column)
121
162
  results.join( ',' )
122
163
  end
123
164
 
124
- def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
165
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
125
166
  results = hsh.map do |column1, column2|
126
167
  qc1 = quote_column_name( column1 )
127
168
  qc2 = quote_column_name( column2 )
128
169
  "#{qc1}=EXCLUDED.#{qc2}"
129
170
  end
171
+ increment_locking_column!(table_name, results, locking_column)
130
172
  results.join( ',' )
131
173
  end
132
174
 
@@ -137,7 +179,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
137
179
  if constraint_name.present?
138
180
  "ON CONSTRAINT #{constraint_name} "
139
181
  elsif conflict_target.present?
140
- '(' << Array( conflict_target ).reject( &:empty? ).join( ', ' ) << ') '.tap do |sql|
182
+ '(' << Array( conflict_target ).reject( &:blank? ).join( ', ' ) << ') '.tap do |sql|
141
183
  sql << "WHERE #{index_predicate} " if index_predicate
142
184
  end
143
185
  end
@@ -153,11 +195,17 @@ module ActiveRecord::Import::PostgreSQLAdapter
153
195
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
154
196
  end
155
197
 
156
- def supports_on_duplicate_key_update?(current_version = postgresql_version)
157
- current_version >= MIN_VERSION_FOR_UPSERT
198
+ def supports_on_duplicate_key_update?
199
+ database_version >= MIN_VERSION_FOR_UPSERT
158
200
  end
159
201
 
160
- def support_setting_primary_key_of_imported_objects?
202
+ def supports_setting_primary_key_of_imported_objects?
161
203
  true
162
204
  end
205
+
206
+ private
207
+
208
+ def database_version
209
+ defined?(postgresql_version) ? postgresql_version : super
210
+ end
163
211
  end
@@ -1,18 +1,20 @@
1
1
  module ActiveRecord::Import::SQLite3Adapter
2
2
  include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
4
 
4
5
  MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
6
+ MIN_VERSION_FOR_UPSERT = "3.24.0".freeze
5
7
  SQLITE_LIMIT_COMPOUND_SELECT = 500
6
8
 
7
9
  # Override our conformance to ActiveRecord::Import::ImportSupport interface
8
10
  # to ensure that we only support import in supported version of SQLite.
9
11
  # Which INSERT statements with multiple value sets was introduced in 3.7.11.
10
- def supports_import?(current_version = sqlite_version)
11
- if current_version >= MIN_VERSION_FOR_IMPORT
12
- true
13
- else
14
- false
15
- end
12
+ def supports_import?
13
+ database_version >= MIN_VERSION_FOR_IMPORT
14
+ end
15
+
16
+ def supports_on_duplicate_key_update?
17
+ database_version >= MIN_VERSION_FOR_UPSERT
16
18
  end
17
19
 
18
20
  # +sql+ can be a single string or an array. If it is an array all
@@ -37,19 +39,136 @@ module ActiveRecord::Import::SQLite3Adapter
37
39
  end
38
40
  end
39
41
 
40
- [number_of_inserts, []]
42
+ ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
41
43
  end
42
44
 
43
- def pre_sql_statements( options)
45
+ def pre_sql_statements( options )
44
46
  sql = []
45
47
  # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
46
- if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:recursive]
48
+ if !supports_on_duplicate_key_update? && (options[:ignore] || options[:on_duplicate_key_ignore])
47
49
  sql << "OR IGNORE"
48
50
  end
49
51
  sql + super
50
52
  end
51
53
 
54
+ def post_sql_statements( table_name, options ) # :nodoc:
55
+ sql = []
56
+
57
+ if supports_on_duplicate_key_update?
58
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
59
+ if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update]
60
+ sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
61
+ end
62
+ end
63
+
64
+ sql + super
65
+ end
66
+
52
67
  def next_value_for_sequence(sequence_name)
53
68
  %{nextval('#{sequence_name}')}
54
69
  end
70
+
71
+ # Add a column to be updated on duplicate key update
72
+ def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
73
+ arg = options[:on_duplicate_key_update]
74
+ if arg.is_a?( Hash )
75
+ columns = arg.fetch( :columns ) { arg[:columns] = [] }
76
+ case columns
77
+ when Array then columns << column.to_sym unless columns.include?( column.to_sym )
78
+ when Hash then columns[column.to_sym] = column.to_sym
79
+ end
80
+ elsif arg.is_a?( Array )
81
+ arg << column.to_sym unless arg.include?( column.to_sym )
82
+ end
83
+ end
84
+
85
+ # Returns a generated ON CONFLICT DO NOTHING statement given the passed
86
+ # in +args+.
87
+ def sql_for_on_duplicate_key_ignore( *args ) # :nodoc:
88
+ arg = args.first
89
+ conflict_target = sql_for_conflict_target( arg ) if arg.is_a?( Hash )
90
+ " ON CONFLICT #{conflict_target}DO NOTHING"
91
+ end
92
+
93
+ # Returns a generated ON CONFLICT DO UPDATE statement given the passed
94
+ # in +args+.
95
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
96
+ arg, primary_key, locking_column = args
97
+ arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
98
+ return unless arg.is_a?( Hash )
99
+
100
+ sql = ' ON CONFLICT '
101
+ conflict_target = sql_for_conflict_target( arg )
102
+
103
+ columns = arg.fetch( :columns, [] )
104
+ condition = arg[:condition]
105
+ if columns.respond_to?( :empty? ) && columns.empty?
106
+ return sql << "#{conflict_target}DO NOTHING"
107
+ end
108
+
109
+ conflict_target ||= sql_for_default_conflict_target( primary_key )
110
+ unless conflict_target
111
+ raise ArgumentError, 'Expected :conflict_target to be specified'
112
+ end
113
+
114
+ sql << "#{conflict_target}DO UPDATE SET "
115
+ if columns.is_a?( Array )
116
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
117
+ elsif columns.is_a?( Hash )
118
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
119
+ elsif columns.is_a?( String )
120
+ sql << columns
121
+ else
122
+ raise ArgumentError, 'Expected :columns to be an Array or Hash'
123
+ end
124
+
125
+ sql << " WHERE #{condition}" if condition.present?
126
+
127
+ sql
128
+ end
129
+
130
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
131
+ results = arr.map do |column|
132
+ qc = quote_column_name( column )
133
+ "#{qc}=EXCLUDED.#{qc}"
134
+ end
135
+ increment_locking_column!(table_name, results, locking_column)
136
+ results.join( ',' )
137
+ end
138
+
139
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
140
+ results = hsh.map do |column1, column2|
141
+ qc1 = quote_column_name( column1 )
142
+ qc2 = quote_column_name( column2 )
143
+ "#{qc1}=EXCLUDED.#{qc2}"
144
+ end
145
+ increment_locking_column!(table_name, results, locking_column)
146
+ results.join( ',' )
147
+ end
148
+
149
+ def sql_for_conflict_target( args = {} )
150
+ conflict_target = args[:conflict_target]
151
+ index_predicate = args[:index_predicate]
152
+ if conflict_target.present?
153
+ '(' << Array( conflict_target ).reject( &:blank? ).join( ', ' ) << ') '.tap do |sql|
154
+ sql << "WHERE #{index_predicate} " if index_predicate
155
+ end
156
+ end
157
+ end
158
+
159
+ def sql_for_default_conflict_target( primary_key )
160
+ conflict_target = Array(primary_key).join(', ')
161
+ "(#{conflict_target}) " if conflict_target.present?
162
+ end
163
+
164
+ # Return true if the statement is a duplicate key record error
165
+ def duplicate_key_update_error?(exception) # :nodoc:
166
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
167
+ end
168
+
169
+ private
170
+
171
+ def database_version
172
+ defined?(sqlite_version) ? sqlite_version : super
173
+ end
55
174
  end