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