activerecord-import 0.10.0 → 1.0.8

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 (118) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +49 -0
  4. data/.rubocop_todo.yml +36 -0
  5. data/.travis.yml +64 -8
  6. data/CHANGELOG.md +475 -0
  7. data/Gemfile +32 -15
  8. data/LICENSE +21 -56
  9. data/README.markdown +564 -35
  10. data/Rakefile +20 -3
  11. data/activerecord-import.gemspec +7 -7
  12. data/benchmarks/README +2 -2
  13. data/benchmarks/benchmark.rb +68 -64
  14. data/benchmarks/lib/base.rb +138 -137
  15. data/benchmarks/lib/cli_parser.rb +107 -103
  16. data/benchmarks/lib/{mysql_benchmark.rb → mysql2_benchmark.rb} +19 -22
  17. data/benchmarks/lib/output_to_csv.rb +5 -4
  18. data/benchmarks/lib/output_to_html.rb +8 -13
  19. data/benchmarks/models/test_innodb.rb +1 -1
  20. data/benchmarks/models/test_memory.rb +1 -1
  21. data/benchmarks/models/test_myisam.rb +1 -1
  22. data/benchmarks/schema/mysql2_schema.rb +16 -0
  23. data/gemfiles/3.2.gemfile +2 -4
  24. data/gemfiles/4.0.gemfile +2 -4
  25. data/gemfiles/4.1.gemfile +2 -4
  26. data/gemfiles/4.2.gemfile +2 -4
  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 +2 -0
  31. data/gemfiles/6.1.gemfile +1 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +6 -0
  33. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +0 -1
  34. data/lib/activerecord-import/adapters/abstract_adapter.rb +23 -17
  35. data/lib/activerecord-import/adapters/mysql_adapter.rb +52 -25
  36. data/lib/activerecord-import/adapters/postgresql_adapter.rb +187 -10
  37. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +148 -17
  38. data/lib/activerecord-import/base.rb +15 -9
  39. data/lib/activerecord-import/import.rb +740 -191
  40. data/lib/activerecord-import/synchronize.rb +21 -21
  41. data/lib/activerecord-import/value_sets_parser.rb +33 -8
  42. data/lib/activerecord-import/version.rb +1 -1
  43. data/lib/activerecord-import.rb +4 -15
  44. data/test/adapters/jdbcsqlite3.rb +1 -0
  45. data/test/adapters/makara_postgis.rb +1 -0
  46. data/test/adapters/mysql2_makara.rb +1 -0
  47. data/test/adapters/mysql2spatial.rb +1 -1
  48. data/test/adapters/postgis.rb +1 -1
  49. data/test/adapters/postgresql.rb +1 -1
  50. data/test/adapters/postgresql_makara.rb +1 -0
  51. data/test/adapters/spatialite.rb +1 -1
  52. data/test/adapters/sqlite3.rb +1 -1
  53. data/test/database.yml.sample +13 -18
  54. data/test/import_test.rb +608 -89
  55. data/test/jdbcmysql/import_test.rb +2 -3
  56. data/test/jdbcpostgresql/import_test.rb +0 -2
  57. data/test/jdbcsqlite3/import_test.rb +4 -0
  58. data/test/makara_postgis/import_test.rb +8 -0
  59. data/test/models/account.rb +3 -0
  60. data/test/models/alarm.rb +2 -0
  61. data/test/models/animal.rb +6 -0
  62. data/test/models/bike_maker.rb +7 -0
  63. data/test/models/book.rb +7 -6
  64. data/test/models/car.rb +3 -0
  65. data/test/models/chapter.rb +2 -2
  66. data/test/models/dictionary.rb +4 -0
  67. data/test/models/discount.rb +3 -0
  68. data/test/models/end_note.rb +2 -2
  69. data/test/models/promotion.rb +3 -0
  70. data/test/models/question.rb +3 -0
  71. data/test/models/rule.rb +3 -0
  72. data/test/models/tag.rb +4 -0
  73. data/test/models/topic.rb +17 -3
  74. data/test/models/user.rb +3 -0
  75. data/test/models/user_token.rb +4 -0
  76. data/test/models/vendor.rb +7 -0
  77. data/test/models/widget.rb +19 -2
  78. data/test/mysql2/import_test.rb +2 -3
  79. data/test/{em_mysql2 → mysql2_makara}/import_test.rb +1 -1
  80. data/test/mysqlspatial2/import_test.rb +2 -2
  81. data/test/postgis/import_test.rb +5 -1
  82. data/test/schema/generic_schema.rb +159 -85
  83. data/test/schema/jdbcpostgresql_schema.rb +1 -0
  84. data/test/schema/mysql2_schema.rb +19 -0
  85. data/test/schema/postgis_schema.rb +1 -0
  86. data/test/schema/postgresql_schema.rb +61 -0
  87. data/test/schema/sqlite3_schema.rb +13 -0
  88. data/test/sqlite3/import_test.rb +2 -50
  89. data/test/support/active_support/test_case_extensions.rb +21 -13
  90. data/test/support/{mysql/assertions.rb → assertions.rb} +20 -2
  91. data/test/support/factories.rb +39 -14
  92. data/test/support/generate.rb +10 -10
  93. data/test/support/mysql/import_examples.rb +49 -98
  94. data/test/support/postgresql/import_examples.rb +535 -57
  95. data/test/support/shared_examples/on_duplicate_key_ignore.rb +43 -0
  96. data/test/support/shared_examples/on_duplicate_key_update.rb +378 -0
  97. data/test/support/shared_examples/recursive_import.rb +225 -0
  98. data/test/support/sqlite3/import_examples.rb +231 -0
  99. data/test/synchronize_test.rb +10 -2
  100. data/test/test_helper.rb +36 -8
  101. data/test/travis/database.yml +26 -17
  102. data/test/value_sets_bytes_parser_test.rb +25 -17
  103. data/test/value_sets_records_parser_test.rb +6 -6
  104. metadata +86 -42
  105. data/benchmarks/boot.rb +0 -18
  106. data/benchmarks/schema/mysql_schema.rb +0 -16
  107. data/gemfiles/3.1.gemfile +0 -4
  108. data/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb +0 -8
  109. data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +0 -6
  110. data/lib/activerecord-import/em_mysql2.rb +0 -7
  111. data/lib/activerecord-import/mysql.rb +0 -7
  112. data/test/adapters/em_mysql2.rb +0 -1
  113. data/test/adapters/mysql.rb +0 -1
  114. data/test/adapters/mysqlspatial.rb +0 -1
  115. data/test/mysql/import_test.rb +0 -6
  116. data/test/mysqlspatial/import_test.rb +0 -6
  117. data/test/schema/mysql_schema.rb +0 -18
  118. data/test/travis/build.sh +0 -30
@@ -3,27 +3,27 @@ module ActiveRecord::Import::MysqlAdapter
3
3
  include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
4
 
5
5
  NO_MAX_PACKET = 0
6
- QUERY_OVERHEAD = 8 #This was shown to be true for MySQL, but it's not clear where the overhead is from.
6
+ QUERY_OVERHEAD = 8 # This was shown to be true for MySQL, but it's not clear where the overhead is from.
7
7
 
8
8
  # +sql+ can be a single string or an array. If it is an array all
9
9
  # elements that are in position >= 1 will be appended to the final SQL.
10
- def insert_many( sql, values, *args ) # :nodoc:
10
+ def insert_many( sql, values, options = {}, *args ) # :nodoc:
11
11
  # the number of inserts default
12
12
  number_of_inserts = 0
13
13
 
14
- base_sql,post_sql = if sql.is_a?( String )
15
- [ sql, '' ]
14
+ base_sql, post_sql = if sql.is_a?( String )
15
+ [sql, '']
16
16
  elsif sql.is_a?( Array )
17
- [ sql.shift, sql.join( ' ' ) ]
17
+ [sql.shift, sql.join( ' ' )]
18
18
  end
19
19
 
20
20
  sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
21
21
 
22
22
  # the number of bytes the requested insert statement values will take up
23
- values_in_bytes = values.sum {|value| value.bytesize }
23
+ values_in_bytes = values.sum(&:bytesize)
24
24
 
25
25
  # the number of bytes (commas) it will take to comma separate our values
26
- comma_separated_bytes = values.size-1
26
+ comma_separated_bytes = values.size - 1
27
27
 
28
28
  # the total number of bytes required if this statement is one statement
29
29
  total_bytes = sql_size + values_in_bytes + comma_separated_bytes
@@ -31,72 +31,99 @@ module ActiveRecord::Import::MysqlAdapter
31
31
  max = max_allowed_packet
32
32
 
33
33
  # if we can insert it all as one statement
34
- if NO_MAX_PACKET == max or total_bytes < max
34
+ if NO_MAX_PACKET == max || total_bytes <= max || options[:force_single_insert]
35
35
  number_of_inserts += 1
36
36
  sql2insert = base_sql + values.join( ',' ) + post_sql
37
37
  insert( sql2insert, *args )
38
38
  else
39
39
  value_sets = ::ActiveRecord::Import::ValueSetsBytesParser.parse(values,
40
- :reserved_bytes => sql_size,
41
- :max_bytes => max)
42
- value_sets.each do |values|
43
- number_of_inserts += 1
44
- sql2insert = base_sql + values.join( ',' ) + post_sql
45
- insert( sql2insert, *args )
40
+ reserved_bytes: sql_size,
41
+ max_bytes: max)
42
+
43
+ transaction(requires_new: true) do
44
+ value_sets.each do |value_set|
45
+ number_of_inserts += 1
46
+ sql2insert = base_sql + value_set.join( ',' ) + post_sql
47
+ insert( sql2insert, *args )
48
+ end
46
49
  end
47
50
  end
48
51
 
49
- [number_of_inserts,[]]
52
+ ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
50
53
  end
51
54
 
52
55
  # Returns the maximum number of bytes that the server will allow
53
56
  # in a single packet
54
57
  def max_allowed_packet # :nodoc:
55
58
  @max_allowed_packet ||= begin
56
- result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
59
+ result = execute( "SELECT @@max_allowed_packet" )
57
60
  # original Mysql gem responds to #fetch_row while Mysql2 responds to #first
58
- 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]
59
62
  val.to_i
60
63
  end
61
64
  end
62
65
 
66
+ def pre_sql_statements( options)
67
+ sql = []
68
+ sql << "IGNORE" if options[:ignore] || options[:on_duplicate_key_ignore]
69
+ sql + super
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
+ if (columns = options[:on_duplicate_key_update])
75
+ case columns
76
+ when Array then columns << column.to_sym unless columns.include?(column.to_sym)
77
+ when Hash then columns[column.to_sym] = column.to_sym
78
+ end
79
+ end
80
+ end
81
+
63
82
  # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
64
83
  # in +args+.
65
84
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
66
85
  sql = ' ON DUPLICATE KEY UPDATE '
67
86
  arg = args.first
87
+ locking_column = args.last
68
88
  if arg.is_a?( Array )
69
- 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 )
70
90
  elsif arg.is_a?( Hash )
71
- 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 )
72
92
  elsif arg.is_a?( String )
73
93
  sql << arg
74
94
  else
75
- raise ArgumentError.new( "Expected Array or Hash" )
95
+ raise ArgumentError, "Expected Array or Hash"
76
96
  end
77
97
  sql
78
98
  end
79
99
 
80
- 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:
81
101
  results = arr.map do |column|
82
102
  qc = quote_column_name( column )
83
103
  "#{table_name}.#{qc}=VALUES(#{qc})"
84
104
  end
105
+ increment_locking_column!(table_name, results, locking_column)
85
106
  results.join( ',' )
86
107
  end
87
108
 
88
- def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
89
- sql = ' ON DUPLICATE KEY UPDATE '
109
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
90
110
  results = hsh.map do |column1, column2|
91
111
  qc1 = quote_column_name( column1 )
92
112
  qc2 = quote_column_name( column2 )
93
113
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
94
114
  end
115
+ increment_locking_column!(table_name, results, locking_column)
95
116
  results.join( ',')
96
117
  end
97
118
 
98
- #return true if the statement is a duplicate key record error
99
- def duplicate_key_update_error?(exception)# :nodoc:
119
+ # Return true if the statement is a duplicate key record error
120
+ def duplicate_key_update_error?(exception) # :nodoc:
100
121
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
101
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
102
129
  end
@@ -1,19 +1,64 @@
1
1
  module ActiveRecord::Import::PostgreSQLAdapter
2
2
  include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
4
 
4
- def insert_many( sql, values, *args ) # :nodoc:
5
+ MIN_VERSION_FOR_UPSERT = 90_500
6
+
7
+ def insert_many( sql, values, options = {}, *args ) # :nodoc:
5
8
  number_of_inserts = 1
9
+ returned_values = []
10
+ ids = []
11
+ results = []
6
12
 
7
- base_sql,post_sql = if sql.is_a?( String )
8
- [ sql, '' ]
13
+ base_sql, post_sql = if sql.is_a?( String )
14
+ [sql, '']
9
15
  elsif sql.is_a?( Array )
10
- [ sql.shift, sql.join( ' ' ) ]
16
+ [sql.shift, sql.join( ' ' )]
11
17
  end
12
18
 
13
19
  sql2insert = base_sql + values.join( ',' ) + post_sql
14
- ids = select_values( sql2insert, *args )
15
20
 
16
- [number_of_inserts,ids]
21
+ columns = returning_columns(options)
22
+ if columns.blank? || (options[:no_returning] && !options[:recursive])
23
+ insert( sql2insert, *args )
24
+ else
25
+ returned_values = if columns.size > 1
26
+ # Select composite columns
27
+ select_rows( sql2insert, *args )
28
+ else
29
+ select_values( sql2insert, *args )
30
+ end
31
+ clear_query_cache if query_cache_enabled
32
+ end
33
+
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]
17
62
  end
18
63
 
19
64
  def next_value_for_sequence(sequence_name)
@@ -21,14 +66,146 @@ module ActiveRecord::Import::PostgreSQLAdapter
21
66
  end
22
67
 
23
68
  def post_sql_statements( table_name, options ) # :nodoc:
24
- unless options[:primary_key].blank?
25
- super(table_name, options) << (" RETURNING #{options[:primary_key]}")
69
+ sql = []
70
+
71
+ if supports_on_duplicate_key_update?
72
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
73
+ if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update] && !options[:recursive]
74
+ sql << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
75
+ end
76
+ elsif logger && options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
77
+ logger.warn "Ignoring on_duplicate_key_ignore because it is not supported by the database."
78
+ end
79
+
80
+ sql += super(table_name, options)
81
+
82
+ columns = returning_columns(options)
83
+ unless columns.blank? || (options[:no_returning] && !options[:recursive])
84
+ sql << " RETURNING \"#{columns.join('", "')}\""
85
+ end
86
+
87
+ sql
88
+ end
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
+
97
+ # Add a column to be updated on duplicate key update
98
+ def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
99
+ arg = options[:on_duplicate_key_update]
100
+ if arg.is_a?( Hash )
101
+ columns = arg.fetch( :columns ) { arg[:columns] = [] }
102
+ case columns
103
+ when Array then columns << column.to_sym unless columns.include?( column.to_sym )
104
+ when Hash then columns[column.to_sym] = column.to_sym
105
+ end
106
+ elsif arg.is_a?( Array )
107
+ arg << column.to_sym unless arg.include?( column.to_sym )
108
+ end
109
+ end
110
+
111
+ # Returns a generated ON CONFLICT DO NOTHING statement given the passed
112
+ # in +args+.
113
+ def sql_for_on_duplicate_key_ignore( table_name, *args ) # :nodoc:
114
+ arg = args.first
115
+ conflict_target = sql_for_conflict_target( arg ) if arg.is_a?( Hash )
116
+ " ON CONFLICT #{conflict_target}DO NOTHING"
117
+ end
118
+
119
+ # Returns a generated ON CONFLICT DO UPDATE statement given the passed
120
+ # in +args+.
121
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
122
+ arg, primary_key, locking_column = args
123
+ arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
124
+ return unless arg.is_a?( Hash )
125
+
126
+ sql = ' ON CONFLICT '
127
+ conflict_target = sql_for_conflict_target( arg )
128
+
129
+ columns = arg.fetch( :columns, [] )
130
+ condition = arg[:condition]
131
+ if columns.respond_to?( :empty? ) && columns.empty?
132
+ return sql << "#{conflict_target}DO NOTHING"
133
+ end
134
+
135
+ conflict_target ||= sql_for_default_conflict_target( table_name, primary_key )
136
+ unless conflict_target
137
+ raise ArgumentError, 'Expected :conflict_target or :constraint_name to be specified'
138
+ end
139
+
140
+ 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 )
146
+ sql << columns
26
147
  else
27
- super(table_name, options)
148
+ raise ArgumentError, 'Expected :columns to be an Array or Hash'
28
149
  end
150
+
151
+ sql << " WHERE #{condition}" if condition.present?
152
+
153
+ sql
29
154
  end
30
155
 
31
- def support_setting_primary_key_of_imported_objects?
156
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
157
+ results = arr.map do |column|
158
+ qc = quote_column_name( column )
159
+ "#{qc}=EXCLUDED.#{qc}"
160
+ end
161
+ increment_locking_column!(table_name, results, locking_column)
162
+ results.join( ',' )
163
+ end
164
+
165
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
166
+ results = hsh.map do |column1, column2|
167
+ qc1 = quote_column_name( column1 )
168
+ qc2 = quote_column_name( column2 )
169
+ "#{qc1}=EXCLUDED.#{qc2}"
170
+ end
171
+ increment_locking_column!(table_name, results, locking_column)
172
+ results.join( ',' )
173
+ end
174
+
175
+ def sql_for_conflict_target( args = {} )
176
+ constraint_name = args[:constraint_name]
177
+ conflict_target = args[:conflict_target]
178
+ index_predicate = args[:index_predicate]
179
+ if constraint_name.present?
180
+ "ON CONSTRAINT #{constraint_name} "
181
+ elsif conflict_target.present?
182
+ '(' << Array( conflict_target ).reject( &:blank? ).join( ', ' ) << ') '.tap do |sql|
183
+ sql << "WHERE #{index_predicate} " if index_predicate
184
+ end
185
+ end
186
+ end
187
+
188
+ def sql_for_default_conflict_target( table_name, primary_key )
189
+ conflict_target = Array(primary_key).join(', ')
190
+ "(#{conflict_target}) " if conflict_target.present?
191
+ end
192
+
193
+ # Return true if the statement is a duplicate key record error
194
+ def duplicate_key_update_error?(exception) # :nodoc:
195
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
196
+ end
197
+
198
+ def supports_on_duplicate_key_update?
199
+ database_version >= MIN_VERSION_FOR_UPSERT
200
+ end
201
+
202
+ def supports_setting_primary_key_of_imported_objects?
32
203
  true
33
204
  end
205
+
206
+ private
207
+
208
+ def database_version
209
+ defined?(postgresql_version) ? postgresql_version : super
210
+ end
34
211
  end
@@ -1,43 +1,174 @@
1
1
  module ActiveRecord::Import::SQLite3Adapter
2
2
  include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
4
 
4
- MIN_VERSION_FOR_IMPORT = "3.7.11"
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=self.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
19
21
  # elements that are in position >= 1 will be appended to the final SQL.
20
- def insert_many(sql, values, *args) # :nodoc:
22
+ def insert_many( sql, values, _options = {}, *args ) # :nodoc:
21
23
  number_of_inserts = 0
22
- base_sql,post_sql = if sql.is_a?( String )
23
- [ sql, '' ]
24
+
25
+ base_sql, post_sql = if sql.is_a?( String )
26
+ [sql, '']
24
27
  elsif sql.is_a?( Array )
25
- [ sql.shift, sql.join( ' ' ) ]
28
+ [sql.shift, sql.join( ' ' )]
26
29
  end
27
30
 
28
31
  value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values,
29
- :max_records => SQLITE_LIMIT_COMPOUND_SELECT)
32
+ max_records: SQLITE_LIMIT_COMPOUND_SELECT)
30
33
 
31
- value_sets.each do |values|
32
- number_of_inserts += 1
33
- sql2insert = base_sql + values.join( ',' ) + post_sql
34
- insert( sql2insert, *args )
34
+ transaction(requires_new: true) do
35
+ value_sets.each do |value_set|
36
+ number_of_inserts += 1
37
+ sql2insert = base_sql + value_set.join( ',' ) + post_sql
38
+ insert( sql2insert, *args )
39
+ end
35
40
  end
36
41
 
37
- [number_of_inserts,[]]
42
+ ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
43
+ end
44
+
45
+ def pre_sql_statements( options )
46
+ sql = []
47
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
48
+ if !supports_on_duplicate_key_update? && (options[:ignore] || options[:on_duplicate_key_ignore])
49
+ sql << "OR IGNORE"
50
+ end
51
+ sql + super
52
+ end
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
38
65
  end
39
66
 
40
67
  def next_value_for_sequence(sequence_name)
41
68
  %{nextval('#{sequence_name}')}
42
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
43
174
  end
@@ -3,31 +3,37 @@ require "active_record"
3
3
  require "active_record/version"
4
4
 
5
5
  module ActiveRecord::Import
6
- AdapterPath = "activerecord-import/active_record/adapters"
6
+ ADAPTER_PATH = "activerecord-import/active_record/adapters".freeze
7
7
 
8
8
  def self.base_adapter(adapter)
9
9
  case adapter
10
- when 'mysqlspatial' then 'mysql'
10
+ when 'mysql2_makara' then 'mysql2'
11
11
  when 'mysql2spatial' then 'mysql2'
12
12
  when 'spatialite' then 'sqlite3'
13
+ when 'postgresql_makara' then 'postgresql'
14
+ when 'makara_postgis' then 'postgresql'
13
15
  when 'postgis' then 'postgresql'
16
+ when 'cockroachdb' then 'postgresql'
14
17
  else adapter
15
18
  end
16
19
  end
17
20
 
18
21
  # Loads the import functionality for a specific database adapter
19
22
  def self.require_adapter(adapter)
20
- require File.join(AdapterPath,"/abstract_adapter")
21
- begin
22
- require File.join(AdapterPath,"/#{base_adapter(adapter)}_adapter")
23
- rescue LoadError
24
- # fallback
25
- end
23
+ require File.join(ADAPTER_PATH, "/#{base_adapter(adapter)}_adapter")
24
+ rescue LoadError
25
+ # fallback
26
26
  end
27
27
 
28
28
  # Loads the import functionality for the passed in ActiveRecord connection
29
29
  def self.load_from_connection_pool(connection_pool)
30
- require_adapter connection_pool.spec.config[:adapter]
30
+ adapter =
31
+ if connection_pool.respond_to?(:db_config) # ActiveRecord >= 6.1
32
+ connection_pool.db_config.adapter
33
+ else
34
+ connection_pool.spec.config[:adapter]
35
+ end
36
+ require_adapter adapter
31
37
  end
32
38
  end
33
39