activerecord-import 0.14.1 → 1.4.1

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 (131) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +74 -8
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +448 -2
  7. data/Gemfile +26 -19
  8. data/LICENSE +21 -56
  9. data/README.markdown +568 -32
  10. data/Rakefile +5 -1
  11. data/activerecord-import.gemspec +8 -7
  12. data/benchmarks/README +2 -2
  13. data/benchmarks/benchmark.rb +8 -1
  14. data/benchmarks/lib/base.rb +2 -0
  15. data/benchmarks/lib/cli_parser.rb +5 -2
  16. data/benchmarks/lib/float.rb +2 -0
  17. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  18. data/benchmarks/lib/output_to_csv.rb +2 -0
  19. data/benchmarks/lib/output_to_html.rb +4 -2
  20. data/benchmarks/models/test_innodb.rb +2 -0
  21. data/benchmarks/models/test_memory.rb +2 -0
  22. data/benchmarks/models/test_myisam.rb +2 -0
  23. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  24. data/gemfiles/4.2.gemfile +4 -3
  25. data/gemfiles/5.0.gemfile +4 -3
  26. data/gemfiles/5.1.gemfile +4 -0
  27. data/gemfiles/5.2.gemfile +4 -0
  28. data/gemfiles/6.0.gemfile +4 -0
  29. data/gemfiles/6.1.gemfile +4 -0
  30. data/gemfiles/7.0.gemfile +4 -0
  31. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  33. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +8 -0
  35. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  38. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  39. data/lib/activerecord-import/adapters/abstract_adapter.rb +12 -16
  40. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  41. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  42. data/lib/activerecord-import/adapters/mysql_adapter.rb +35 -18
  43. data/lib/activerecord-import/adapters/postgresql_adapter.rb +107 -28
  44. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +141 -18
  45. data/lib/activerecord-import/base.rb +15 -8
  46. data/lib/activerecord-import/import.rb +642 -178
  47. data/lib/activerecord-import/mysql2.rb +2 -0
  48. data/lib/activerecord-import/postgresql.rb +2 -0
  49. data/lib/activerecord-import/sqlite3.rb +2 -0
  50. data/lib/activerecord-import/synchronize.rb +5 -3
  51. data/lib/activerecord-import/value_sets_parser.rb +29 -3
  52. data/lib/activerecord-import/version.rb +3 -1
  53. data/lib/activerecord-import.rb +5 -16
  54. data/test/adapters/jdbcmysql.rb +2 -0
  55. data/test/adapters/jdbcpostgresql.rb +2 -0
  56. data/test/adapters/jdbcsqlite3.rb +3 -0
  57. data/test/adapters/makara_postgis.rb +3 -0
  58. data/test/adapters/mysql2.rb +2 -0
  59. data/test/adapters/mysql2_makara.rb +2 -0
  60. data/test/adapters/mysql2spatial.rb +2 -0
  61. data/test/adapters/postgis.rb +2 -0
  62. data/test/adapters/postgresql.rb +2 -0
  63. data/test/adapters/postgresql_makara.rb +2 -0
  64. data/test/adapters/seamless_database_pool.rb +2 -0
  65. data/test/adapters/spatialite.rb +2 -0
  66. data/test/adapters/sqlite3.rb +2 -0
  67. data/test/{travis → github}/database.yml +7 -1
  68. data/test/import_test.rb +498 -32
  69. data/test/jdbcmysql/import_test.rb +2 -1
  70. data/test/jdbcpostgresql/import_test.rb +2 -1
  71. data/test/jdbcsqlite3/import_test.rb +6 -0
  72. data/test/makara_postgis/import_test.rb +10 -0
  73. data/test/models/account.rb +5 -0
  74. data/test/models/alarm.rb +4 -0
  75. data/test/models/animal.rb +8 -0
  76. data/test/models/bike_maker.rb +9 -0
  77. data/test/models/book.rb +4 -0
  78. data/test/models/car.rb +5 -0
  79. data/test/models/card.rb +5 -0
  80. data/test/models/chapter.rb +2 -0
  81. data/test/models/customer.rb +8 -0
  82. data/test/models/deck.rb +8 -0
  83. data/test/models/dictionary.rb +6 -0
  84. data/test/models/discount.rb +2 -0
  85. data/test/models/end_note.rb +2 -0
  86. data/test/models/group.rb +2 -0
  87. data/test/models/order.rb +8 -0
  88. data/test/models/playing_card.rb +4 -0
  89. data/test/models/promotion.rb +2 -0
  90. data/test/models/question.rb +2 -0
  91. data/test/models/rule.rb +2 -0
  92. data/test/models/tag.rb +7 -0
  93. data/test/models/tag_alias.rb +5 -0
  94. data/test/models/topic.rb +16 -0
  95. data/test/models/user.rb +5 -0
  96. data/test/models/user_token.rb +6 -0
  97. data/test/models/vendor.rb +9 -0
  98. data/test/models/widget.rb +18 -0
  99. data/test/mysql2/import_test.rb +2 -0
  100. data/test/mysql2_makara/import_test.rb +2 -0
  101. data/test/mysqlspatial2/import_test.rb +2 -0
  102. data/test/postgis/import_test.rb +6 -0
  103. data/test/postgresql/import_test.rb +2 -4
  104. data/test/schema/generic_schema.rb +88 -3
  105. data/test/schema/jdbcpostgresql_schema.rb +3 -0
  106. data/test/schema/mysql2_schema.rb +21 -0
  107. data/test/schema/postgis_schema.rb +3 -0
  108. data/test/schema/postgresql_schema.rb +63 -0
  109. data/test/schema/sqlite3_schema.rb +15 -0
  110. data/test/schema/version.rb +2 -0
  111. data/test/sqlite3/import_test.rb +4 -50
  112. data/test/support/active_support/test_case_extensions.rb +8 -1
  113. data/test/support/assertions.rb +2 -0
  114. data/test/support/factories.rb +17 -8
  115. data/test/support/generate.rb +10 -8
  116. data/test/support/mysql/import_examples.rb +17 -3
  117. data/test/support/postgresql/import_examples.rb +442 -9
  118. data/test/support/shared_examples/on_duplicate_key_ignore.rb +45 -0
  119. data/test/support/shared_examples/on_duplicate_key_update.rb +278 -1
  120. data/test/support/shared_examples/recursive_import.rb +137 -12
  121. data/test/support/sqlite3/import_examples.rb +232 -0
  122. data/test/synchronize_test.rb +10 -0
  123. data/test/test_helper.rb +44 -3
  124. data/test/value_sets_bytes_parser_test.rb +15 -2
  125. data/test/value_sets_records_parser_test.rb +2 -0
  126. metadata +74 -22
  127. data/.travis.yml +0 -52
  128. data/gemfiles/3.2.gemfile +0 -3
  129. data/gemfiles/4.0.gemfile +0 -3
  130. data/gemfiles/4.1.gemfile +0 -3
  131. data/test/schema/mysql_schema.rb +0 -16
@@ -1,12 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::PostgreSQLAdapter
2
4
  include ActiveRecord::Import::ImportSupport
3
5
  include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
6
 
5
7
  MIN_VERSION_FOR_UPSERT = 90_500
6
8
 
7
- def insert_many( sql, values, *args ) # :nodoc:
9
+ def insert_many( sql, values, options = {}, *args ) # :nodoc:
8
10
  number_of_inserts = 1
11
+ returned_values = {}
9
12
  ids = []
13
+ results = []
10
14
 
11
15
  base_sql, post_sql = if sql.is_a?( String )
12
16
  [sql, '']
@@ -15,15 +19,54 @@ module ActiveRecord::Import::PostgreSQLAdapter
15
19
  end
16
20
 
17
21
  sql2insert = base_sql + values.join( ',' ) + post_sql
18
- if post_sql =~ /RETURNING\s/
19
- ids = select_values( sql2insert, *args )
20
- else
22
+
23
+ selections = returning_selections(options)
24
+ if selections.blank? || (options[:no_returning] && !options[:recursive])
21
25
  insert( sql2insert, *args )
26
+ else
27
+ returned_values = if selections.size > 1
28
+ # Select composite columns
29
+ db_result = select_all( sql2insert, *args )
30
+ { values: db_result.rows, columns: db_result.columns }
31
+ else
32
+ { values: select_values( sql2insert, *args ) }
33
+ end
34
+ clear_query_cache if query_cache_enabled
35
+ end
36
+
37
+ if options[:returning].blank?
38
+ ids = Array(returned_values[:values])
39
+ elsif options[:primary_key].blank?
40
+ options[:returning_columns] ||= returned_values[:columns]
41
+ results = Array(returned_values[:values])
42
+ else
43
+ # split primary key and returning columns
44
+ ids, results, options[:returning_columns] = split_ids_and_results(returned_values, options)
22
45
  end
23
46
 
24
- ActiveRecord::Base.connection.query_cache.clear
47
+ ActiveRecord::Import::Result.new([], number_of_inserts, ids, results)
48
+ end
49
+
50
+ def split_ids_and_results( selections, options )
51
+ ids = []
52
+ returning_values = []
53
+
54
+ columns = Array(selections[:columns])
55
+ values = Array(selections[:values])
56
+ id_indexes = Array(options[:primary_key]).map { |key| columns.index(key) }
57
+ returning_columns = columns.reject.with_index { |_, index| id_indexes.include?(index) }
58
+ returning_indexes = returning_columns.map { |column| columns.index(column) }
59
+
60
+ values.each do |value|
61
+ value_array = Array(value)
62
+ ids << id_indexes.map { |index| value_array[index] }
63
+ returning_values << returning_indexes.map { |index| value_array[index] }
64
+ end
25
65
 
26
- [number_of_inserts, ids]
66
+ ids.map!(&:first) if id_indexes.size == 1
67
+ returning_values.map!(&:first) if returning_columns.size == 1
68
+
69
+ [ids, returning_values, returning_columns]
27
70
  end
28
71
 
29
72
  def next_value_for_sequence(sequence_name)
@@ -31,10 +74,36 @@ module ActiveRecord::Import::PostgreSQLAdapter
31
74
  end
32
75
 
33
76
  def post_sql_statements( table_name, options ) # :nodoc:
34
- if options[:no_returning] || options[:primary_key].blank?
35
- super(table_name, options)
36
- else
37
- super(table_name, options) << "RETURNING #{options[:primary_key]}"
77
+ sql = []
78
+
79
+ if supports_on_duplicate_key_update?
80
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
81
+ if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update] && !options[:recursive]
82
+ sql << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
83
+ end
84
+ elsif logger && options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
85
+ logger.warn "Ignoring on_duplicate_key_ignore because it is not supported by the database."
86
+ end
87
+
88
+ sql += super(table_name, options)
89
+
90
+ selections = returning_selections(options)
91
+ unless selections.blank? || (options[:no_returning] && !options[:recursive])
92
+ sql << " RETURNING #{selections.join(', ')}"
93
+ end
94
+
95
+ sql
96
+ end
97
+
98
+ def returning_selections(options)
99
+ selections = []
100
+ column_names = Array(options[:model].column_names)
101
+
102
+ selections += Array(options[:primary_key]) if options[:primary_key].present?
103
+ selections += Array(options[:returning]) if options[:returning].present?
104
+
105
+ selections.map do |selection|
106
+ column_names.include?(selection.to_s) ? "\"#{selection}\"" : selection
38
107
  end
39
108
  end
40
109
 
@@ -63,65 +132,75 @@ module ActiveRecord::Import::PostgreSQLAdapter
63
132
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
64
133
  # in +args+.
65
134
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
66
- arg = args.first
135
+ arg, primary_key, locking_column = args
67
136
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
68
137
  return unless arg.is_a?( Hash )
69
138
 
70
- sql = " ON CONFLICT "
139
+ sql = ' ON CONFLICT '.dup
71
140
  conflict_target = sql_for_conflict_target( arg )
72
141
 
73
142
  columns = arg.fetch( :columns, [] )
143
+ condition = arg[:condition]
74
144
  if columns.respond_to?( :empty? ) && columns.empty?
75
145
  return sql << "#{conflict_target}DO NOTHING"
76
146
  end
77
147
 
78
- conflict_target ||= sql_for_default_conflict_target( table_name )
148
+ conflict_target ||= sql_for_default_conflict_target( table_name, primary_key )
79
149
  unless conflict_target
80
150
  raise ArgumentError, 'Expected :conflict_target or :constraint_name to be specified'
81
151
  end
82
152
 
83
153
  sql << "#{conflict_target}DO UPDATE SET "
84
154
  if columns.is_a?( Array )
85
- sql << sql_for_on_duplicate_key_update_as_array( table_name, columns )
155
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
86
156
  elsif columns.is_a?( Hash )
87
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, columns )
157
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
88
158
  elsif columns.is_a?( String )
89
159
  sql << columns
90
160
  else
91
161
  raise ArgumentError, 'Expected :columns to be an Array or Hash'
92
162
  end
163
+
164
+ sql << " WHERE #{condition}" if condition.present?
165
+
93
166
  sql
94
167
  end
95
168
 
96
- def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
169
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
97
170
  results = arr.map do |column|
98
171
  qc = quote_column_name( column )
99
172
  "#{qc}=EXCLUDED.#{qc}"
100
173
  end
174
+ increment_locking_column!(table_name, results, locking_column)
101
175
  results.join( ',' )
102
176
  end
103
177
 
104
- def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
178
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
105
179
  results = hsh.map do |column1, column2|
106
180
  qc1 = quote_column_name( column1 )
107
181
  qc2 = quote_column_name( column2 )
108
182
  "#{qc1}=EXCLUDED.#{qc2}"
109
183
  end
184
+ increment_locking_column!(table_name, results, locking_column)
110
185
  results.join( ',' )
111
186
  end
112
187
 
113
188
  def sql_for_conflict_target( args = {} )
114
189
  constraint_name = args[:constraint_name]
115
190
  conflict_target = args[:conflict_target]
116
- if constraint_name
191
+ index_predicate = args[:index_predicate]
192
+ if constraint_name.present?
117
193
  "ON CONSTRAINT #{constraint_name} "
118
- elsif conflict_target
119
- '(' << Array( conflict_target ).join( ', ' ) << ') '
194
+ elsif conflict_target.present?
195
+ sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
196
+ sql += "WHERE #{index_predicate} " if index_predicate
197
+ sql
120
198
  end
121
199
  end
122
200
 
123
- def sql_for_default_conflict_target( table_name )
124
- "(#{primary_key( table_name )}) "
201
+ def sql_for_default_conflict_target( table_name, primary_key )
202
+ conflict_target = Array(primary_key).join(', ')
203
+ "(#{conflict_target}) " if conflict_target.present?
125
204
  end
126
205
 
127
206
  # Return true if the statement is a duplicate key record error
@@ -129,15 +208,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
129
208
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
130
209
  end
131
210
 
132
- def supports_on_duplicate_key_update?(current_version = postgresql_version)
133
- current_version >= MIN_VERSION_FOR_UPSERT
211
+ def supports_on_duplicate_key_update?
212
+ database_version >= MIN_VERSION_FOR_UPSERT
134
213
  end
135
214
 
136
- def supports_on_duplicate_key_ignore?(current_version = postgresql_version)
137
- supports_on_duplicate_key_update?(current_version)
215
+ def supports_setting_primary_key_of_imported_objects?
216
+ true
138
217
  end
139
218
 
140
- def support_setting_primary_key_of_imported_objects?
141
- true
219
+ def database_version
220
+ defined?(postgresql_version) ? postgresql_version : super
142
221
  end
143
222
  end
@@ -1,25 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::SQLite3Adapter
2
4
  include ActiveRecord::Import::ImportSupport
5
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
6
 
4
- MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
7
+ MIN_VERSION_FOR_IMPORT = "3.7.11"
8
+ MIN_VERSION_FOR_UPSERT = "3.24.0"
5
9
  SQLITE_LIMIT_COMPOUND_SELECT = 500
6
10
 
7
11
  # Override our conformance to ActiveRecord::Import::ImportSupport interface
8
12
  # to ensure that we only support import in supported version of SQLite.
9
13
  # 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
14
+ def supports_import?
15
+ database_version >= MIN_VERSION_FOR_IMPORT
16
+ end
17
+
18
+ def supports_on_duplicate_key_update?
19
+ database_version >= MIN_VERSION_FOR_UPSERT
16
20
  end
17
21
 
18
22
  # +sql+ can be a single string or an array. If it is an array all
19
23
  # elements that are in position >= 1 will be appended to the final SQL.
20
- def insert_many(sql, values, *args) # :nodoc:
24
+ def insert_many( sql, values, _options = {}, *args ) # :nodoc:
21
25
  number_of_inserts = 0
22
- ids = []
23
26
 
24
27
  base_sql, post_sql = if sql.is_a?( String )
25
28
  [sql, '']
@@ -30,22 +33,142 @@ module ActiveRecord::Import::SQLite3Adapter
30
33
  value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values,
31
34
  max_records: SQLITE_LIMIT_COMPOUND_SELECT)
32
35
 
33
- value_sets.each do |value_set|
34
- number_of_inserts += 1
35
- sql2insert = base_sql + value_set.join( ',' ) + post_sql
36
- first_insert_id = insert( sql2insert, *args )
37
- last_insert_id = first_insert_id + value_set.size - 1
38
- ids.concat((first_insert_id..last_insert_id).to_a)
36
+ transaction(requires_new: true) do
37
+ value_sets.each do |value_set|
38
+ number_of_inserts += 1
39
+ sql2insert = base_sql + value_set.join( ',' ) + post_sql
40
+ insert( sql2insert, *args )
41
+ end
42
+ end
43
+
44
+ ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
45
+ end
46
+
47
+ def pre_sql_statements( options )
48
+ sql = []
49
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
50
+ if !supports_on_duplicate_key_update? && (options[:ignore] || options[:on_duplicate_key_ignore])
51
+ sql << "OR IGNORE"
52
+ end
53
+ sql + super
54
+ end
55
+
56
+ def post_sql_statements( table_name, options ) # :nodoc:
57
+ sql = []
58
+
59
+ if supports_on_duplicate_key_update?
60
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
61
+ if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update]
62
+ sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
63
+ end
39
64
  end
40
65
 
41
- [number_of_inserts, ids]
66
+ sql + super
42
67
  end
43
68
 
44
69
  def next_value_for_sequence(sequence_name)
45
70
  %{nextval('#{sequence_name}')}
46
71
  end
47
72
 
48
- def support_setting_primary_key_of_imported_objects?
49
- true
73
+ # Add a column to be updated on duplicate key update
74
+ def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
75
+ arg = options[:on_duplicate_key_update]
76
+ if arg.is_a?( Hash )
77
+ columns = arg.fetch( :columns ) { arg[:columns] = [] }
78
+ case columns
79
+ when Array then columns << column.to_sym unless columns.include?( column.to_sym )
80
+ when Hash then columns[column.to_sym] = column.to_sym
81
+ end
82
+ elsif arg.is_a?( Array )
83
+ arg << column.to_sym unless arg.include?( column.to_sym )
84
+ end
85
+ end
86
+
87
+ # Returns a generated ON CONFLICT DO NOTHING statement given the passed
88
+ # in +args+.
89
+ def sql_for_on_duplicate_key_ignore( *args ) # :nodoc:
90
+ arg = args.first
91
+ conflict_target = sql_for_conflict_target( arg ) if arg.is_a?( Hash )
92
+ " ON CONFLICT #{conflict_target}DO NOTHING"
93
+ end
94
+
95
+ # Returns a generated ON CONFLICT DO UPDATE statement given the passed
96
+ # in +args+.
97
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
98
+ arg, primary_key, locking_column = args
99
+ arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
100
+ return unless arg.is_a?( Hash )
101
+
102
+ sql = ' ON CONFLICT '.dup
103
+ conflict_target = sql_for_conflict_target( arg )
104
+
105
+ columns = arg.fetch( :columns, [] )
106
+ condition = arg[:condition]
107
+ if columns.respond_to?( :empty? ) && columns.empty?
108
+ return sql << "#{conflict_target}DO NOTHING"
109
+ end
110
+
111
+ conflict_target ||= sql_for_default_conflict_target( primary_key )
112
+ unless conflict_target
113
+ raise ArgumentError, 'Expected :conflict_target to be specified'
114
+ end
115
+
116
+ sql << "#{conflict_target}DO UPDATE SET "
117
+ if columns.is_a?( Array )
118
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
119
+ elsif columns.is_a?( Hash )
120
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
121
+ elsif columns.is_a?( String )
122
+ sql << columns
123
+ else
124
+ raise ArgumentError, 'Expected :columns to be an Array or Hash'
125
+ end
126
+
127
+ sql << " WHERE #{condition}" if condition.present?
128
+
129
+ sql
130
+ end
131
+
132
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
133
+ results = arr.map do |column|
134
+ qc = quote_column_name( column )
135
+ "#{qc}=EXCLUDED.#{qc}"
136
+ end
137
+ increment_locking_column!(table_name, results, locking_column)
138
+ results.join( ',' )
139
+ end
140
+
141
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
142
+ results = hsh.map do |column1, column2|
143
+ qc1 = quote_column_name( column1 )
144
+ qc2 = quote_column_name( column2 )
145
+ "#{qc1}=EXCLUDED.#{qc2}"
146
+ end
147
+ increment_locking_column!(table_name, results, locking_column)
148
+ results.join( ',' )
149
+ end
150
+
151
+ def sql_for_conflict_target( args = {} )
152
+ conflict_target = args[:conflict_target]
153
+ index_predicate = args[:index_predicate]
154
+ if conflict_target.present?
155
+ sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
156
+ sql += "WHERE #{index_predicate} " if index_predicate
157
+ sql
158
+ end
159
+ end
160
+
161
+ def sql_for_default_conflict_target( primary_key )
162
+ conflict_target = Array(primary_key).join(', ')
163
+ "(#{conflict_target}) " if conflict_target.present?
164
+ end
165
+
166
+ # Return true if the statement is a duplicate key record error
167
+ def duplicate_key_update_error?(exception) # :nodoc:
168
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
169
+ end
170
+
171
+ def database_version
172
+ defined?(sqlite_version) ? sqlite_version : super
50
173
  end
51
174
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pathname"
2
4
  require "active_record"
3
5
  require "active_record/version"
4
6
 
5
7
  module ActiveRecord::Import
6
- ADAPTER_PATH = "activerecord-import/active_record/adapters".freeze
8
+ ADAPTER_PATH = "activerecord-import/active_record/adapters"
7
9
 
8
10
  def self.base_adapter(adapter)
9
11
  case adapter
@@ -11,24 +13,29 @@ module ActiveRecord::Import
11
13
  when 'mysql2spatial' then 'mysql2'
12
14
  when 'spatialite' then 'sqlite3'
13
15
  when 'postgresql_makara' then 'postgresql'
16
+ when 'makara_postgis' then 'postgresql'
14
17
  when 'postgis' then 'postgresql'
18
+ when 'cockroachdb' then 'postgresql'
15
19
  else adapter
16
20
  end
17
21
  end
18
22
 
19
23
  # Loads the import functionality for a specific database adapter
20
24
  def self.require_adapter(adapter)
21
- require File.join(ADAPTER_PATH, "/abstract_adapter")
22
- begin
23
- require File.join(ADAPTER_PATH, "/#{base_adapter(adapter)}_adapter")
24
- rescue LoadError
25
- # fallback
26
- end
25
+ require File.join(ADAPTER_PATH, "/#{base_adapter(adapter)}_adapter")
26
+ rescue LoadError
27
+ # fallback
27
28
  end
28
29
 
29
30
  # Loads the import functionality for the passed in ActiveRecord connection
30
31
  def self.load_from_connection_pool(connection_pool)
31
- require_adapter connection_pool.spec.config[:adapter]
32
+ adapter =
33
+ if connection_pool.respond_to?(:db_config) # ActiveRecord >= 6.1
34
+ connection_pool.db_config.adapter
35
+ else
36
+ connection_pool.spec.config[:adapter]
37
+ end
38
+ require_adapter adapter
32
39
  end
33
40
  end
34
41