activerecord-import 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +32 -0
  3. data/.rubocop.yml +49 -0
  4. data/.rubocop_todo.yml +36 -0
  5. data/.travis.yml +74 -0
  6. data/Brewfile +3 -0
  7. data/CHANGELOG.md +430 -0
  8. data/Gemfile +59 -0
  9. data/LICENSE +56 -0
  10. data/README.markdown +619 -0
  11. data/Rakefile +68 -0
  12. data/activerecord-import.gemspec +23 -0
  13. data/benchmarks/README +32 -0
  14. data/benchmarks/benchmark.rb +68 -0
  15. data/benchmarks/lib/base.rb +138 -0
  16. data/benchmarks/lib/cli_parser.rb +107 -0
  17. data/benchmarks/lib/float.rb +15 -0
  18. data/benchmarks/lib/mysql2_benchmark.rb +19 -0
  19. data/benchmarks/lib/output_to_csv.rb +19 -0
  20. data/benchmarks/lib/output_to_html.rb +64 -0
  21. data/benchmarks/models/test_innodb.rb +3 -0
  22. data/benchmarks/models/test_memory.rb +3 -0
  23. data/benchmarks/models/test_myisam.rb +3 -0
  24. data/benchmarks/schema/mysql_schema.rb +16 -0
  25. data/gemfiles/3.2.gemfile +2 -0
  26. data/gemfiles/4.0.gemfile +2 -0
  27. data/gemfiles/4.1.gemfile +2 -0
  28. data/gemfiles/4.2.gemfile +2 -0
  29. data/gemfiles/5.0.gemfile +2 -0
  30. data/gemfiles/5.1.gemfile +2 -0
  31. data/gemfiles/5.2.gemfile +2 -0
  32. data/gemfiles/6.0.gemfile +1 -0
  33. data/gemfiles/6.1.gemfile +1 -0
  34. data/lib/activerecord-import.rb +6 -0
  35. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +9 -0
  36. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -0
  37. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +6 -0
  38. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +6 -0
  39. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +6 -0
  40. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +6 -0
  41. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +7 -0
  42. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +6 -0
  43. data/lib/activerecord-import/adapters/abstract_adapter.rb +66 -0
  44. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +5 -0
  45. data/lib/activerecord-import/adapters/mysql2_adapter.rb +5 -0
  46. data/lib/activerecord-import/adapters/mysql_adapter.rb +129 -0
  47. data/lib/activerecord-import/adapters/postgresql_adapter.rb +217 -0
  48. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +180 -0
  49. data/lib/activerecord-import/base.rb +43 -0
  50. data/lib/activerecord-import/import.rb +1059 -0
  51. data/lib/activerecord-import/mysql2.rb +7 -0
  52. data/lib/activerecord-import/postgresql.rb +7 -0
  53. data/lib/activerecord-import/sqlite3.rb +7 -0
  54. data/lib/activerecord-import/synchronize.rb +66 -0
  55. data/lib/activerecord-import/value_sets_parser.rb +77 -0
  56. data/lib/activerecord-import/version.rb +5 -0
  57. data/test/adapters/jdbcmysql.rb +1 -0
  58. data/test/adapters/jdbcpostgresql.rb +1 -0
  59. data/test/adapters/jdbcsqlite3.rb +1 -0
  60. data/test/adapters/makara_postgis.rb +1 -0
  61. data/test/adapters/mysql2.rb +1 -0
  62. data/test/adapters/mysql2_makara.rb +1 -0
  63. data/test/adapters/mysql2spatial.rb +1 -0
  64. data/test/adapters/postgis.rb +1 -0
  65. data/test/adapters/postgresql.rb +1 -0
  66. data/test/adapters/postgresql_makara.rb +1 -0
  67. data/test/adapters/seamless_database_pool.rb +1 -0
  68. data/test/adapters/spatialite.rb +1 -0
  69. data/test/adapters/sqlite3.rb +1 -0
  70. data/test/database.yml.sample +52 -0
  71. data/test/import_test.rb +903 -0
  72. data/test/jdbcmysql/import_test.rb +5 -0
  73. data/test/jdbcpostgresql/import_test.rb +4 -0
  74. data/test/jdbcsqlite3/import_test.rb +4 -0
  75. data/test/makara_postgis/import_test.rb +8 -0
  76. data/test/models/account.rb +3 -0
  77. data/test/models/alarm.rb +2 -0
  78. data/test/models/bike_maker.rb +7 -0
  79. data/test/models/book.rb +9 -0
  80. data/test/models/car.rb +3 -0
  81. data/test/models/chapter.rb +4 -0
  82. data/test/models/dictionary.rb +4 -0
  83. data/test/models/discount.rb +3 -0
  84. data/test/models/end_note.rb +4 -0
  85. data/test/models/group.rb +3 -0
  86. data/test/models/promotion.rb +3 -0
  87. data/test/models/question.rb +3 -0
  88. data/test/models/rule.rb +3 -0
  89. data/test/models/tag.rb +4 -0
  90. data/test/models/topic.rb +23 -0
  91. data/test/models/user.rb +3 -0
  92. data/test/models/user_token.rb +4 -0
  93. data/test/models/vendor.rb +7 -0
  94. data/test/models/widget.rb +24 -0
  95. data/test/mysql2/import_test.rb +5 -0
  96. data/test/mysql2_makara/import_test.rb +6 -0
  97. data/test/mysqlspatial2/import_test.rb +6 -0
  98. data/test/postgis/import_test.rb +8 -0
  99. data/test/postgresql/import_test.rb +4 -0
  100. data/test/schema/generic_schema.rb +194 -0
  101. data/test/schema/jdbcpostgresql_schema.rb +1 -0
  102. data/test/schema/mysql2_schema.rb +19 -0
  103. data/test/schema/postgis_schema.rb +1 -0
  104. data/test/schema/postgresql_schema.rb +47 -0
  105. data/test/schema/sqlite3_schema.rb +13 -0
  106. data/test/schema/version.rb +10 -0
  107. data/test/sqlite3/import_test.rb +4 -0
  108. data/test/support/active_support/test_case_extensions.rb +75 -0
  109. data/test/support/assertions.rb +73 -0
  110. data/test/support/factories.rb +64 -0
  111. data/test/support/generate.rb +29 -0
  112. data/test/support/mysql/import_examples.rb +98 -0
  113. data/test/support/postgresql/import_examples.rb +563 -0
  114. data/test/support/shared_examples/on_duplicate_key_ignore.rb +43 -0
  115. data/test/support/shared_examples/on_duplicate_key_update.rb +368 -0
  116. data/test/support/shared_examples/recursive_import.rb +216 -0
  117. data/test/support/sqlite3/import_examples.rb +231 -0
  118. data/test/synchronize_test.rb +41 -0
  119. data/test/test_helper.rb +75 -0
  120. data/test/travis/database.yml +66 -0
  121. data/test/value_sets_bytes_parser_test.rb +104 -0
  122. data/test/value_sets_records_parser_test.rb +32 -0
  123. metadata +259 -0
@@ -0,0 +1,19 @@
1
+ class Mysql2Benchmark < BenchmarkBase
2
+ def benchmark_all( array_of_cols_and_vals )
3
+ methods = self.methods.find_all { |m| m =~ /benchmark_/ }
4
+ methods.delete_if { |m| m =~ /benchmark_(all|model)/ }
5
+ methods.each { |method| send( method, array_of_cols_and_vals ) }
6
+ end
7
+
8
+ def benchmark_myisam( array_of_cols_and_vals )
9
+ bm_model( TestMyISAM, array_of_cols_and_vals )
10
+ end
11
+
12
+ def benchmark_innodb( array_of_cols_and_vals )
13
+ bm_model( TestInnoDb, array_of_cols_and_vals )
14
+ end
15
+
16
+ def benchmark_memory( array_of_cols_and_vals )
17
+ bm_model( TestMemory, array_of_cols_and_vals )
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'csv'
2
+
3
+ module OutputToCSV
4
+ def self.output_results( filename, results )
5
+ CSV.open( filename, 'w' ) do |csv|
6
+ # Iterate over each result set, which contains many results
7
+ results.each do |result_set|
8
+ columns = []
9
+ times = []
10
+ result_set.each do |result|
11
+ columns << result.description
12
+ times << result.tms.real
13
+ end
14
+ csv << columns
15
+ csv << times
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,64 @@
1
+ require 'erb'
2
+
3
+ module OutputToHTML
4
+ TEMPLATE_HEADER = <<"EOT".freeze
5
+ <div>
6
+ All times are rounded to the nearest thousandth for display purposes. Speedups next to each time are computed
7
+ before any rounding occurs. Also, all speedup calculations are computed by comparing a given time against
8
+ the very first column (which is always the default ActiveRecord::Base.create method.
9
+ </div>
10
+ EOT
11
+
12
+ TEMPLATE = <<"EOT".freeze
13
+ <style>
14
+ td#benchmarkTitle {
15
+ border: 1px solid black;
16
+ padding: 2px;
17
+ font-size: 0.8em;
18
+ background-color: black;
19
+ color: white;
20
+ }
21
+ td#benchmarkCell {
22
+ border: 1px solid black;
23
+ padding: 2px;
24
+ font-size: 0.8em;
25
+ }
26
+ </style>
27
+ <table>
28
+ <tr>
29
+ <% columns.each do |col| %>
30
+ <td id="benchmarkTitle"><%= col %></td>
31
+ <% end %>
32
+ </tr>
33
+ <tr>
34
+ <% times.each do |time| %>
35
+ <td id="benchmarkCell"><%= time %></td>
36
+ <% end %>
37
+ </tr>
38
+ <tr><td>&nbsp;</td></tr>
39
+ </table>
40
+ EOT
41
+
42
+ def self.output_results( filename, results )
43
+ html = ''
44
+ results.each do |result_set|
45
+ columns = []
46
+ times = []
47
+ result_set.each do |result|
48
+ columns << result.description
49
+ if result.failed
50
+ times << "failed"
51
+ else
52
+ time = result.tms.real.round_to( 3 )
53
+ speedup = ( result_set.first.tms.real / result.tms.real ).round
54
+ times << (result == result_set.first ? time.to_s : "#{time} (#{speedup}x speedup)")
55
+ end
56
+ end
57
+
58
+ template = ERB.new( TEMPLATE, 0, "%<>")
59
+ html << template.result( binding )
60
+ end
61
+
62
+ File.open( filename, 'w' ) { |file| file.write( TEMPLATE_HEADER + html ) }
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ class TestInnoDb < ActiveRecord::Base
2
+ self.table_name = 'test_innodb'
3
+ end
@@ -0,0 +1,3 @@
1
+ class TestMemory < ActiveRecord::Base
2
+ self.table_name = 'test_memory'
3
+ end
@@ -0,0 +1,3 @@
1
+ class TestMyISAM < ActiveRecord::Base
2
+ self.table_name = 'test_myisam'
3
+ end
@@ -0,0 +1,16 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :test_myisam, options: 'ENGINE=MyISAM', force: true do |t|
3
+ t.column :my_name, :string, null: false
4
+ t.column :description, :string
5
+ end
6
+
7
+ create_table :test_innodb, options: 'ENGINE=InnoDb', force: true do |t|
8
+ t.column :my_name, :string, null: false
9
+ t.column :description, :string
10
+ end
11
+
12
+ create_table :test_memory, options: 'ENGINE=Memory', force: true do |t|
13
+ t.column :my_name, :string, null: false
14
+ t.column :description, :string
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 3.2.0'
2
+ gem 'composite_primary_keys', '~> 5.0'
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 4.0.0'
2
+ gem 'composite_primary_keys', '~> 6.0'
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 4.1.0'
2
+ gem 'composite_primary_keys', '~> 7.0'
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 4.2.0'
2
+ gem 'composite_primary_keys', '~> 8.0'
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 5.0.0'
2
+ gem 'composite_primary_keys', '~> 9.0'
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 5.1.0'
2
+ gem 'composite_primary_keys', '~> 10.0'
@@ -0,0 +1,2 @@
1
+ gem 'activerecord', '~> 5.2.0'
2
+ gem 'composite_primary_keys', '~> 11.0'
@@ -0,0 +1 @@
1
+ gem 'activerecord', '~> 6.0.0'
@@ -0,0 +1 @@
1
+ gem 'activerecord', '~> 6.1.0.alpha', github: "rails/rails"
@@ -0,0 +1,6 @@
1
+ # rubocop:disable Style/FileName
2
+ require "active_support/lazy_load_hooks"
3
+
4
+ ActiveSupport.on_load(:active_record) do
5
+ require "activerecord-import/base"
6
+ end
@@ -0,0 +1,9 @@
1
+ require "activerecord-import/adapters/abstract_adapter"
2
+
3
+ module ActiveRecord # :nodoc:
4
+ module ConnectionAdapters # :nodoc:
5
+ class AbstractAdapter # :nodoc:
6
+ include ActiveRecord::Import::AbstractAdapter::InstanceMethods
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/mysql_adapter"
2
+ require "activerecord-import/adapters/mysql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::MysqlAdapter
5
+ include ActiveRecord::Import::MysqlAdapter
6
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/postgresql_adapter"
2
+ require "activerecord-import/adapters/postgresql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
+ include ActiveRecord::Import::PostgreSQLAdapter
6
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/sqlite3_adapter"
2
+ require "activerecord-import/adapters/sqlite3_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::SQLite3Adapter
5
+ include ActiveRecord::Import::SQLite3Adapter
6
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/mysql2_adapter"
2
+ require "activerecord-import/adapters/mysql2_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
5
+ include ActiveRecord::Import::Mysql2Adapter
6
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/postgresql_adapter"
2
+ require "activerecord-import/adapters/postgresql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
+ include ActiveRecord::Import::PostgreSQLAdapter
6
+ end
@@ -0,0 +1,7 @@
1
+ require "seamless_database_pool"
2
+ require "active_record/connection_adapters/seamless_database_pool_adapter"
3
+ require "activerecord-import/adapters/mysql_adapter"
4
+
5
+ class ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter
6
+ include ActiveRecord::Import::MysqlAdapter
7
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/sqlite3_adapter"
2
+ require "activerecord-import/adapters/sqlite3_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::SQLite3Adapter
5
+ include ActiveRecord::Import::SQLite3Adapter
6
+ end
@@ -0,0 +1,66 @@
1
+ module ActiveRecord::Import::AbstractAdapter
2
+ module InstanceMethods
3
+ def next_value_for_sequence(sequence_name)
4
+ %(#{sequence_name}.nextval)
5
+ end
6
+
7
+ def insert_many( sql, values, _options = {}, *args ) # :nodoc:
8
+ number_of_inserts = 1
9
+
10
+ base_sql, post_sql = if sql.is_a?( String )
11
+ [sql, '']
12
+ elsif sql.is_a?( Array )
13
+ [sql.shift, sql.join( ' ' )]
14
+ end
15
+
16
+ sql2insert = base_sql + values.join( ',' ) + post_sql
17
+ insert( sql2insert, *args )
18
+
19
+ ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
20
+ end
21
+
22
+ def pre_sql_statements(options)
23
+ sql = []
24
+ sql << options[:pre_sql] if options[:pre_sql]
25
+ sql << options[:command] if options[:command]
26
+
27
+ # add keywords like IGNORE or DELAYED
28
+ if options[:keywords].is_a?(Array)
29
+ sql.concat(options[:keywords])
30
+ elsif options[:keywords]
31
+ sql << options[:keywords].to_s
32
+ end
33
+
34
+ sql
35
+ end
36
+
37
+ # Synchronizes the passed in ActiveRecord instances with the records in
38
+ # the database by calling +reload+ on each instance.
39
+ def after_import_synchronize( instances )
40
+ instances.each(&:reload)
41
+ end
42
+
43
+ # Returns an array of post SQL statements given the passed in options.
44
+ def post_sql_statements( table_name, options ) # :nodoc:
45
+ post_sql_statements = []
46
+
47
+ if supports_on_duplicate_key_update? && options[:on_duplicate_key_update]
48
+ post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update], options[:primary_key], options[:locking_column] )
49
+ elsif logger && options[:on_duplicate_key_update]
50
+ logger.warn "Ignoring on_duplicate_key_update because it is not supported by the database."
51
+ end
52
+
53
+ # custom user post_sql
54
+ post_sql_statements << options[:post_sql] if options[:post_sql]
55
+
56
+ # with rollup
57
+ post_sql_statements << rollup_sql if options[:rollup]
58
+
59
+ post_sql_statements
60
+ end
61
+
62
+ def supports_on_duplicate_key_update?
63
+ false
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ require "activerecord-import/adapters/mysql_adapter"
2
+
3
+ module ActiveRecord::Import::EMMysql2Adapter
4
+ include ActiveRecord::Import::MysqlAdapter
5
+ end
@@ -0,0 +1,5 @@
1
+ require "activerecord-import/adapters/mysql_adapter"
2
+
3
+ module ActiveRecord::Import::Mysql2Adapter
4
+ include ActiveRecord::Import::MysqlAdapter
5
+ end
@@ -0,0 +1,129 @@
1
+ module ActiveRecord::Import::MysqlAdapter
2
+ include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
+
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.
7
+
8
+ # +sql+ can be a single string or an array. If it is an array all
9
+ # elements that are in position >= 1 will be appended to the final SQL.
10
+ def insert_many( sql, values, options = {}, *args ) # :nodoc:
11
+ # the number of inserts default
12
+ number_of_inserts = 0
13
+
14
+ base_sql, post_sql = if sql.is_a?( String )
15
+ [sql, '']
16
+ elsif sql.is_a?( Array )
17
+ [sql.shift, sql.join( ' ' )]
18
+ end
19
+
20
+ sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
21
+
22
+ # the number of bytes the requested insert statement values will take up
23
+ values_in_bytes = values.sum(&:bytesize)
24
+
25
+ # the number of bytes (commas) it will take to comma separate our values
26
+ comma_separated_bytes = values.size - 1
27
+
28
+ # the total number of bytes required if this statement is one statement
29
+ total_bytes = sql_size + values_in_bytes + comma_separated_bytes
30
+
31
+ max = max_allowed_packet
32
+
33
+ # if we can insert it all as one statement
34
+ if NO_MAX_PACKET == max || total_bytes <= max || options[:force_single_insert]
35
+ number_of_inserts += 1
36
+ sql2insert = base_sql + values.join( ',' ) + post_sql
37
+ insert( sql2insert, *args )
38
+ else
39
+ value_sets = ::ActiveRecord::Import::ValueSetsBytesParser.parse(values,
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
49
+ end
50
+ end
51
+
52
+ ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
53
+ end
54
+
55
+ # Returns the maximum number of bytes that the server will allow
56
+ # in a single packet
57
+ def max_allowed_packet # :nodoc:
58
+ @max_allowed_packet ||= begin
59
+ result = execute( "SHOW VARIABLES like 'max_allowed_packet'" )
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]
62
+ val.to_i
63
+ end
64
+ end
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
+
82
+ # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
83
+ # in +args+.
84
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
85
+ sql = ' ON DUPLICATE KEY UPDATE '
86
+ arg = args.first
87
+ locking_column = args.last
88
+ if arg.is_a?( Array )
89
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arg )
90
+ elsif arg.is_a?( Hash )
91
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, arg )
92
+ elsif arg.is_a?( String )
93
+ sql << arg
94
+ else
95
+ raise ArgumentError, "Expected Array or Hash"
96
+ end
97
+ sql
98
+ end
99
+
100
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
101
+ results = arr.map do |column|
102
+ qc = quote_column_name( column )
103
+ "#{table_name}.#{qc}=VALUES(#{qc})"
104
+ end
105
+ increment_locking_column!(results, table_name, locking_column)
106
+ results.join( ',' )
107
+ end
108
+
109
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
110
+ results = hsh.map do |column1, column2|
111
+ qc1 = quote_column_name( column1 )
112
+ qc2 = quote_column_name( column2 )
113
+ "#{table_name}.#{qc1}=VALUES( #{qc2} )"
114
+ end
115
+ increment_locking_column!(results, table_name, locking_column)
116
+ results.join( ',')
117
+ end
118
+
119
+ # Return true if the statement is a duplicate key record error
120
+ def duplicate_key_update_error?(exception) # :nodoc:
121
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
122
+ end
123
+
124
+ def increment_locking_column!(results, table_name, locking_column)
125
+ if locking_column.present?
126
+ results << "#{table_name}.`#{locking_column}`=`#{locking_column}`+1"
127
+ end
128
+ end
129
+ end