activerecord-import 0.23.0 → 1.4.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 (58) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +214 -4
  5. data/Gemfile +11 -9
  6. data/LICENSE +21 -56
  7. data/README.markdown +574 -22
  8. data/Rakefile +2 -1
  9. data/activerecord-import.gemspec +4 -4
  10. data/benchmarks/benchmark.rb +5 -1
  11. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +0 -0
  12. data/gemfiles/5.0.gemfile +1 -0
  13. data/gemfiles/5.1.gemfile +1 -0
  14. data/gemfiles/5.2.gemfile +2 -2
  15. data/gemfiles/6.0.gemfile +2 -0
  16. data/gemfiles/6.1.gemfile +2 -0
  17. data/gemfiles/7.0.gemfile +1 -0
  18. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +4 -4
  19. data/lib/activerecord-import/adapters/abstract_adapter.rb +7 -1
  20. data/lib/activerecord-import/adapters/mysql_adapter.rb +8 -11
  21. data/lib/activerecord-import/adapters/postgresql_adapter.rb +14 -16
  22. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +125 -8
  23. data/lib/activerecord-import/base.rb +9 -1
  24. data/lib/activerecord-import/import.rb +269 -123
  25. data/lib/activerecord-import/synchronize.rb +2 -2
  26. data/lib/activerecord-import/value_sets_parser.rb +2 -0
  27. data/lib/activerecord-import/version.rb +1 -1
  28. data/lib/activerecord-import.rb +1 -0
  29. data/test/adapters/makara_postgis.rb +1 -0
  30. data/test/{travis → github}/database.yml +3 -1
  31. data/test/import_test.rb +138 -8
  32. data/test/makara_postgis/import_test.rb +8 -0
  33. data/test/models/animal.rb +6 -0
  34. data/test/models/card.rb +3 -0
  35. data/test/models/customer.rb +6 -0
  36. data/test/models/deck.rb +6 -0
  37. data/test/models/order.rb +6 -0
  38. data/test/models/playing_card.rb +2 -0
  39. data/test/models/user.rb +3 -1
  40. data/test/models/user_token.rb +4 -0
  41. data/test/schema/generic_schema.rb +30 -0
  42. data/test/schema/mysql2_schema.rb +19 -0
  43. data/test/schema/postgresql_schema.rb +16 -0
  44. data/test/schema/sqlite3_schema.rb +13 -0
  45. data/test/support/factories.rb +8 -8
  46. data/test/support/generate.rb +6 -6
  47. data/test/support/mysql/import_examples.rb +12 -0
  48. data/test/support/postgresql/import_examples.rb +100 -2
  49. data/test/support/shared_examples/on_duplicate_key_update.rb +54 -0
  50. data/test/support/shared_examples/recursive_import.rb +74 -4
  51. data/test/support/sqlite3/import_examples.rb +189 -25
  52. data/test/test_helper.rb +28 -3
  53. metadata +37 -18
  54. data/.travis.yml +0 -62
  55. data/gemfiles/3.2.gemfile +0 -2
  56. data/gemfiles/4.0.gemfile +0 -2
  57. data/gemfiles/4.1.gemfile +0 -2
  58. data/test/schema/mysql_schema.rb +0 -16
@@ -20,7 +20,11 @@ FileUtils.mkdir_p 'log'
20
20
  ActiveRecord::Base.configurations["test"] = YAML.load_file(File.join(benchmark_dir, "../test/database.yml"))[options.adapter]
21
21
  ActiveRecord::Base.logger = Logger.new("log/test.log")
22
22
  ActiveRecord::Base.logger.level = Logger::DEBUG
23
- ActiveRecord::Base.default_timezone = :utc
23
+ if ActiveRecord.respond_to?(:default_timezone)
24
+ ActiveRecord.default_timezone = :utc
25
+ else
26
+ ActiveRecord::Base.default_timezone = :utc
27
+ end
24
28
 
25
29
  require "activerecord-import"
26
30
  ActiveRecord::Base.establish_connection(:test)
data/gemfiles/5.0.gemfile CHANGED
@@ -1 +1,2 @@
1
1
  gem 'activerecord', '~> 5.0.0'
2
+ gem 'composite_primary_keys', '~> 9.0'
data/gemfiles/5.1.gemfile CHANGED
@@ -1 +1,2 @@
1
1
  gem 'activerecord', '~> 5.1.0'
2
+ gem 'composite_primary_keys', '~> 10.0'
data/gemfiles/5.2.gemfile CHANGED
@@ -1,2 +1,2 @@
1
- gem 'activerecord', '~> 5.2.0.rc1'
2
- gem 'composite_primary_keys', '~> 11.0.0.rc2'
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,2 @@
1
+ gem 'activerecord', '~> 6.1.0'
2
+ gem 'composite_primary_keys', '~> 13.0'
@@ -0,0 +1 @@
1
+ gem 'activerecord', '~> 7.0.0'
@@ -1,6 +1,6 @@
1
- require "active_record/connection_adapters/mysql_adapter"
2
- require "activerecord-import/adapters/mysql_adapter"
1
+ require "active_record/connection_adapters/mysql2_adapter"
2
+ require "activerecord-import/adapters/mysql2_adapter"
3
3
 
4
- class ActiveRecord::ConnectionAdapters::MysqlAdapter
5
- include ActiveRecord::Import::MysqlAdapter
4
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
5
+ include ActiveRecord::Import::Mysql2Adapter
6
6
  end
@@ -46,7 +46,7 @@ module ActiveRecord::Import::AbstractAdapter
46
46
 
47
47
  if supports_on_duplicate_key_update? && options[:on_duplicate_key_update]
48
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 options[:on_duplicate_key_update]
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
@@ -56,9 +56,9 @@ module ActiveRecord::Import::MysqlAdapter
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,21 +71,18 @@ 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
 
85
82
  # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
86
83
  # in +args+.
87
84
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
88
- sql = ' ON DUPLICATE KEY UPDATE '
85
+ sql = ' ON DUPLICATE KEY UPDATE '.dup
89
86
  arg = args.first
90
87
  locking_column = args.last
91
88
  if arg.is_a?( Array )
@@ -105,7 +102,7 @@ module ActiveRecord::Import::MysqlAdapter
105
102
  qc = quote_column_name( column )
106
103
  "#{table_name}.#{qc}=VALUES(#{qc})"
107
104
  end
108
- increment_locking_column!(results, table_name, locking_column)
105
+ increment_locking_column!(table_name, results, locking_column)
109
106
  results.join( ',' )
110
107
  end
111
108
 
@@ -115,7 +112,7 @@ module ActiveRecord::Import::MysqlAdapter
115
112
  qc2 = quote_column_name( column2 )
116
113
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
117
114
  end
118
- increment_locking_column!(results, table_name, locking_column)
115
+ increment_locking_column!(table_name, results, locking_column)
119
116
  results.join( ',')
120
117
  end
121
118
 
@@ -124,9 +121,9 @@ module ActiveRecord::Import::MysqlAdapter
124
121
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
125
122
  end
126
123
 
127
- def increment_locking_column!(results, table_name, locking_column)
124
+ def increment_locking_column!(table_name, results, locking_column)
128
125
  if locking_column.present?
129
- results << "#{table_name}.`#{locking_column}`=`#{locking_column}`+1"
126
+ results << "`#{locking_column}`=#{table_name}.`#{locking_column}`+1"
130
127
  end
131
128
  end
132
129
  end
@@ -19,7 +19,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
19
19
  sql2insert = base_sql + values.join( ',' ) + post_sql
20
20
 
21
21
  columns = returning_columns(options)
22
- if columns.blank? || options[:no_returning]
22
+ if columns.blank? || (options[:no_returning] && !options[:recursive])
23
23
  insert( sql2insert, *args )
24
24
  else
25
25
  returned_values = if columns.size > 1
@@ -28,7 +28,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
28
28
  else
29
29
  select_values( sql2insert, *args )
30
30
  end
31
- query_cache.clear if query_cache_enabled
31
+ clear_query_cache if query_cache_enabled
32
32
  end
33
33
 
34
34
  if options[:returning].blank?
@@ -73,14 +73,14 @@ module ActiveRecord::Import::PostgreSQLAdapter
73
73
  if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update] && !options[:recursive]
74
74
  sql << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
75
75
  end
76
- elsif options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
76
+ elsif logger && options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
77
77
  logger.warn "Ignoring on_duplicate_key_ignore because it is not supported by the database."
78
78
  end
79
79
 
80
80
  sql += super(table_name, options)
81
81
 
82
82
  columns = returning_columns(options)
83
- unless columns.blank? || options[:no_returning]
83
+ unless columns.blank? || (options[:no_returning] && !options[:recursive])
84
84
  sql << " RETURNING \"#{columns.join('", "')}\""
85
85
  end
86
86
 
@@ -123,7 +123,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
123
123
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
124
124
  return unless arg.is_a?( Hash )
125
125
 
126
- sql = ' ON CONFLICT '
126
+ sql = ' ON CONFLICT '.dup
127
127
  conflict_target = sql_for_conflict_target( arg )
128
128
 
129
129
  columns = arg.fetch( :columns, [] )
@@ -158,7 +158,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
158
158
  qc = quote_column_name( column )
159
159
  "#{qc}=EXCLUDED.#{qc}"
160
160
  end
161
- increment_locking_column!(results, locking_column)
161
+ increment_locking_column!(table_name, results, locking_column)
162
162
  results.join( ',' )
163
163
  end
164
164
 
@@ -168,7 +168,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
168
168
  qc2 = quote_column_name( column2 )
169
169
  "#{qc1}=EXCLUDED.#{qc2}"
170
170
  end
171
- increment_locking_column!(results, locking_column)
171
+ increment_locking_column!(table_name, results, locking_column)
172
172
  results.join( ',' )
173
173
  end
174
174
 
@@ -179,9 +179,9 @@ module ActiveRecord::Import::PostgreSQLAdapter
179
179
  if constraint_name.present?
180
180
  "ON CONSTRAINT #{constraint_name} "
181
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
182
+ sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
183
+ sql += "WHERE #{index_predicate} " if index_predicate
184
+ sql
185
185
  end
186
186
  end
187
187
 
@@ -195,17 +195,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
195
195
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
196
196
  end
197
197
 
198
- def supports_on_duplicate_key_update?(current_version = postgresql_version)
199
- current_version >= MIN_VERSION_FOR_UPSERT
198
+ def supports_on_duplicate_key_update?
199
+ database_version >= MIN_VERSION_FOR_UPSERT
200
200
  end
201
201
 
202
202
  def supports_setting_primary_key_of_imported_objects?
203
203
  true
204
204
  end
205
205
 
206
- def increment_locking_column!(results, locking_column)
207
- if locking_column.present?
208
- results << "\"#{locking_column}\"=EXCLUDED.\"#{locking_column}\"+1"
209
- end
206
+ def database_version
207
+ defined?(postgresql_version) ? postgresql_version : super
210
208
  end
211
209
  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
@@ -40,16 +42,131 @@ module ActiveRecord::Import::SQLite3Adapter
40
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 '.dup
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
+ sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
154
+ sql += "WHERE #{index_predicate} " if index_predicate
155
+ sql
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
+ def database_version
170
+ defined?(sqlite_version) ? sqlite_version : super
171
+ end
55
172
  end
@@ -11,7 +11,9 @@ module ActiveRecord::Import
11
11
  when 'mysql2spatial' then 'mysql2'
12
12
  when 'spatialite' then 'sqlite3'
13
13
  when 'postgresql_makara' then 'postgresql'
14
+ when 'makara_postgis' then 'postgresql'
14
15
  when 'postgis' then 'postgresql'
16
+ when 'cockroachdb' then 'postgresql'
15
17
  else adapter
16
18
  end
17
19
  end
@@ -25,7 +27,13 @@ module ActiveRecord::Import
25
27
 
26
28
  # Loads the import functionality for the passed in ActiveRecord connection
27
29
  def self.load_from_connection_pool(connection_pool)
28
- 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
29
37
  end
30
38
  end
31
39