activerecord-import 0.17.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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