activerecord-import 1.0.3

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 (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