activerecord-import 0.22.0 → 1.4.1

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +74 -8
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +235 -3
  7. data/Gemfile +22 -15
  8. data/LICENSE +21 -56
  9. data/README.markdown +574 -22
  10. data/Rakefile +4 -1
  11. data/activerecord-import.gemspec +6 -5
  12. data/benchmarks/benchmark.rb +7 -1
  13. data/benchmarks/lib/base.rb +2 -0
  14. data/benchmarks/lib/cli_parser.rb +3 -1
  15. data/benchmarks/lib/float.rb +2 -0
  16. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  17. data/benchmarks/lib/output_to_csv.rb +2 -0
  18. data/benchmarks/lib/output_to_html.rb +4 -2
  19. data/benchmarks/models/test_innodb.rb +2 -0
  20. data/benchmarks/models/test_memory.rb +2 -0
  21. data/benchmarks/models/test_myisam.rb +2 -0
  22. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  23. data/gemfiles/4.2.gemfile +2 -0
  24. data/gemfiles/5.0.gemfile +2 -0
  25. data/gemfiles/5.1.gemfile +2 -0
  26. data/gemfiles/5.2.gemfile +4 -0
  27. data/gemfiles/6.0.gemfile +4 -0
  28. data/gemfiles/6.1.gemfile +4 -0
  29. data/gemfiles/7.0.gemfile +4 -0
  30. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  31. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  32. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  33. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  35. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  38. data/lib/activerecord-import/adapters/abstract_adapter.rb +10 -2
  39. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  40. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  41. data/lib/activerecord-import/adapters/mysql_adapter.rb +19 -11
  42. data/lib/activerecord-import/adapters/postgresql_adapter.rb +56 -37
  43. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
  44. data/lib/activerecord-import/base.rb +12 -2
  45. data/lib/activerecord-import/import.rb +300 -136
  46. data/lib/activerecord-import/mysql2.rb +2 -0
  47. data/lib/activerecord-import/postgresql.rb +2 -0
  48. data/lib/activerecord-import/sqlite3.rb +2 -0
  49. data/lib/activerecord-import/synchronize.rb +4 -2
  50. data/lib/activerecord-import/value_sets_parser.rb +4 -0
  51. data/lib/activerecord-import/version.rb +3 -1
  52. data/lib/activerecord-import.rb +4 -1
  53. data/test/adapters/jdbcmysql.rb +2 -0
  54. data/test/adapters/jdbcpostgresql.rb +2 -0
  55. data/test/adapters/jdbcsqlite3.rb +2 -0
  56. data/test/adapters/makara_postgis.rb +3 -0
  57. data/test/adapters/mysql2.rb +2 -0
  58. data/test/adapters/mysql2_makara.rb +2 -0
  59. data/test/adapters/mysql2spatial.rb +2 -0
  60. data/test/adapters/postgis.rb +2 -0
  61. data/test/adapters/postgresql.rb +2 -0
  62. data/test/adapters/postgresql_makara.rb +2 -0
  63. data/test/adapters/seamless_database_pool.rb +2 -0
  64. data/test/adapters/spatialite.rb +2 -0
  65. data/test/adapters/sqlite3.rb +2 -0
  66. data/test/{travis → github}/database.yml +3 -1
  67. data/test/import_test.rb +159 -8
  68. data/test/jdbcmysql/import_test.rb +2 -0
  69. data/test/jdbcpostgresql/import_test.rb +2 -0
  70. data/test/jdbcsqlite3/import_test.rb +2 -0
  71. data/test/makara_postgis/import_test.rb +10 -0
  72. data/test/models/account.rb +5 -0
  73. data/test/models/alarm.rb +2 -0
  74. data/test/models/animal.rb +8 -0
  75. data/test/models/bike_maker.rb +9 -0
  76. data/test/models/book.rb +2 -0
  77. data/test/models/car.rb +2 -0
  78. data/test/models/card.rb +5 -0
  79. data/test/models/chapter.rb +2 -0
  80. data/test/models/customer.rb +8 -0
  81. data/test/models/deck.rb +8 -0
  82. data/test/models/dictionary.rb +2 -0
  83. data/test/models/discount.rb +2 -0
  84. data/test/models/end_note.rb +2 -0
  85. data/test/models/group.rb +2 -0
  86. data/test/models/order.rb +8 -0
  87. data/test/models/playing_card.rb +4 -0
  88. data/test/models/promotion.rb +2 -0
  89. data/test/models/question.rb +2 -0
  90. data/test/models/rule.rb +2 -0
  91. data/test/models/tag.rb +3 -0
  92. data/test/models/tag_alias.rb +5 -0
  93. data/test/models/topic.rb +2 -0
  94. data/test/models/user.rb +5 -0
  95. data/test/models/user_token.rb +6 -0
  96. data/test/models/vendor.rb +2 -0
  97. data/test/models/widget.rb +2 -0
  98. data/test/mysql2/import_test.rb +2 -0
  99. data/test/mysql2_makara/import_test.rb +2 -0
  100. data/test/mysqlspatial2/import_test.rb +2 -0
  101. data/test/postgis/import_test.rb +2 -0
  102. data/test/postgresql/import_test.rb +2 -0
  103. data/test/schema/generic_schema.rb +53 -0
  104. data/test/schema/jdbcpostgresql_schema.rb +2 -0
  105. data/test/schema/mysql2_schema.rb +21 -0
  106. data/test/schema/postgis_schema.rb +2 -0
  107. data/test/schema/postgresql_schema.rb +18 -0
  108. data/test/schema/sqlite3_schema.rb +15 -0
  109. data/test/schema/version.rb +2 -0
  110. data/test/sqlite3/import_test.rb +2 -0
  111. data/test/support/active_support/test_case_extensions.rb +2 -0
  112. data/test/support/assertions.rb +2 -0
  113. data/test/support/factories.rb +10 -8
  114. data/test/support/generate.rb +10 -8
  115. data/test/support/mysql/import_examples.rb +14 -1
  116. data/test/support/postgresql/import_examples.rb +140 -3
  117. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  118. data/test/support/shared_examples/on_duplicate_key_update.rb +263 -0
  119. data/test/support/shared_examples/recursive_import.rb +76 -4
  120. data/test/support/sqlite3/import_examples.rb +191 -26
  121. data/test/synchronize_test.rb +2 -0
  122. data/test/test_helper.rb +36 -3
  123. data/test/value_sets_bytes_parser_test.rb +2 -0
  124. data/test/value_sets_records_parser_test.rb +2 -0
  125. metadata +46 -18
  126. data/.travis.yml +0 -61
  127. data/gemfiles/3.2.gemfile +0 -2
  128. data/gemfiles/4.0.gemfile +0 -2
  129. data/gemfiles/4.1.gemfile +0 -2
  130. data/test/schema/mysql_schema.rb +0 -16
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
2
4
  require "fileutils"
3
5
  require "active_record"
@@ -20,7 +22,11 @@ FileUtils.mkdir_p 'log'
20
22
  ActiveRecord::Base.configurations["test"] = YAML.load_file(File.join(benchmark_dir, "../test/database.yml"))[options.adapter]
21
23
  ActiveRecord::Base.logger = Logger.new("log/test.log")
22
24
  ActiveRecord::Base.logger.level = Logger::DEBUG
23
- ActiveRecord::Base.default_timezone = :utc
25
+ if ActiveRecord.respond_to?(:default_timezone)
26
+ ActiveRecord.default_timezone = :utc
27
+ else
28
+ ActiveRecord::Base.default_timezone = :utc
29
+ end
24
30
 
25
31
  require "activerecord-import"
26
32
  ActiveRecord::Base.establish_connection(:test)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class BenchmarkBase
2
4
  attr_reader :results
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require 'ostruct'
3
5
 
@@ -8,7 +10,7 @@ require 'ostruct'
8
10
  # * t - the table types to test. ie: myisam, innodb, memory, temporary, etc.
9
11
  #
10
12
  module BenchmarkOptionParser
11
- BANNER = "Usage: ruby #{$0} [options]\nSee ruby #{$0} -h for more options.".freeze
13
+ BANNER = "Usage: ruby #{$0} [options]\nSee ruby #{$0} -h for more options."
12
14
 
13
15
  def self.print_banner
14
16
  puts BANNER
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Taken from http://www.programmingishard.com/posts/show/128
2
4
  # Posted by rbates
3
5
  class Float
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Mysql2Benchmark < BenchmarkBase
2
4
  def benchmark_all( array_of_cols_and_vals )
3
5
  methods = self.methods.find_all { |m| m =~ /benchmark_/ }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'csv'
2
4
 
3
5
  module OutputToCSV
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'erb'
2
4
 
3
5
  module OutputToHTML
4
- TEMPLATE_HEADER = <<"EOT".freeze
6
+ TEMPLATE_HEADER = <<"EOT"
5
7
  <div>
6
8
  All times are rounded to the nearest thousandth for display purposes. Speedups next to each time are computed
7
9
  before any rounding occurs. Also, all speedup calculations are computed by comparing a given time against
@@ -9,7 +11,7 @@ module OutputToHTML
9
11
  </div>
10
12
  EOT
11
13
 
12
- TEMPLATE = <<"EOT".freeze
14
+ TEMPLATE = <<"EOT"
13
15
  <style>
14
16
  td#benchmarkTitle {
15
17
  border: 1px solid black;
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class TestInnoDb < ActiveRecord::Base
2
4
  self.table_name = 'test_innodb'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class TestMemory < ActiveRecord::Base
2
4
  self.table_name = 'test_memory'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class TestMyISAM < ActiveRecord::Base
2
4
  self.table_name = 'test_myisam'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ActiveRecord::Schema.define do
2
4
  create_table :test_myisam, options: 'ENGINE=MyISAM', force: true do |t|
3
5
  t.column :my_name, :string, null: false
data/gemfiles/4.2.gemfile CHANGED
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  gem 'activerecord', '~> 4.2.0'
2
4
  gem 'composite_primary_keys', '~> 8.0'
data/gemfiles/5.0.gemfile CHANGED
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  gem 'activerecord', '~> 5.0.0'
2
4
  gem 'composite_primary_keys', '~> 9.0'
data/gemfiles/5.1.gemfile CHANGED
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  gem 'activerecord', '~> 5.1.0'
2
4
  gem 'composite_primary_keys', '~> 10.0'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'activerecord', '~> 5.2.0'
4
+ gem 'composite_primary_keys', '~> 11.0'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'activerecord', '~> 6.0.0'
4
+ gem 'composite_primary_keys', '~> 12.0'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'activerecord', '~> 6.1.0'
4
+ gem 'composite_primary_keys', '~> 13.0'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'activerecord', '~> 7.0.0'
4
+ gem 'composite_primary_keys', '~> 14.0'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "activerecord-import/adapters/abstract_adapter"
2
4
 
3
5
  module ActiveRecord # :nodoc:
@@ -1,6 +1,8 @@
1
- require "active_record/connection_adapters/mysql_adapter"
2
- require "activerecord-import/adapters/mysql_adapter"
1
+ # frozen_string_literal: true
3
2
 
4
- class ActiveRecord::ConnectionAdapters::MysqlAdapter
5
- include ActiveRecord::Import::MysqlAdapter
3
+ require "active_record/connection_adapters/mysql2_adapter"
4
+ require "activerecord-import/adapters/mysql2_adapter"
5
+
6
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
7
+ include ActiveRecord::Import::Mysql2Adapter
6
8
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_record/connection_adapters/postgresql_adapter"
2
4
  require "activerecord-import/adapters/postgresql_adapter"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_record/connection_adapters/sqlite3_adapter"
2
4
  require "activerecord-import/adapters/sqlite3_adapter"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_record/connection_adapters/mysql2_adapter"
2
4
  require "activerecord-import/adapters/mysql2_adapter"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_record/connection_adapters/postgresql_adapter"
2
4
  require "activerecord-import/adapters/postgresql_adapter"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "seamless_database_pool"
2
4
  require "active_record/connection_adapters/seamless_database_pool_adapter"
3
5
  require "activerecord-import/adapters/mysql_adapter"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_record/connection_adapters/sqlite3_adapter"
2
4
  require "activerecord-import/adapters/sqlite3_adapter"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::AbstractAdapter
2
4
  module InstanceMethods
3
5
  def next_value_for_sequence(sequence_name)
@@ -45,8 +47,8 @@ module ActiveRecord::Import::AbstractAdapter
45
47
  post_sql_statements = []
46
48
 
47
49
  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] )
49
- elsif options[:on_duplicate_key_update]
50
+ post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update], options[:primary_key], options[:locking_column] )
51
+ elsif logger && options[:on_duplicate_key_update]
50
52
  logger.warn "Ignoring on_duplicate_key_update because it is not supported by the database."
51
53
  end
52
54
 
@@ -59,6 +61,12 @@ module ActiveRecord::Import::AbstractAdapter
59
61
  post_sql_statements
60
62
  end
61
63
 
64
+ def increment_locking_column!(table_name, results, locking_column)
65
+ if locking_column.present?
66
+ results << "\"#{locking_column}\"=#{table_name}.\"#{locking_column}\"+1"
67
+ end
68
+ end
69
+
62
70
  def supports_on_duplicate_key_update?
63
71
  false
64
72
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "activerecord-import/adapters/mysql_adapter"
2
4
 
3
5
  module ActiveRecord::Import::EMMysql2Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "activerecord-import/adapters/mysql_adapter"
2
4
 
3
5
  module ActiveRecord::Import::Mysql2Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::MysqlAdapter
2
4
  include ActiveRecord::Import::ImportSupport
3
5
  include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
@@ -56,9 +58,9 @@ module ActiveRecord::Import::MysqlAdapter
56
58
  # in a single packet
57
59
  def max_allowed_packet # :nodoc:
58
60
  @max_allowed_packet ||= begin
59
- result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
61
+ result = execute( "SELECT @@max_allowed_packet" )
60
62
  # 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]
63
+ val = result.respond_to?(:fetch_row) ? result.fetch_row[0] : result.first[0]
62
64
  val.to_i
63
65
  end
64
66
  end
@@ -71,26 +73,24 @@ module ActiveRecord::Import::MysqlAdapter
71
73
 
72
74
  # Add a column to be updated on duplicate key update
73
75
  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]
76
+ if (columns = options[:on_duplicate_key_update])
76
77
  case columns
77
78
  when Array then columns << column.to_sym unless columns.include?(column.to_sym)
78
79
  when Hash then columns[column.to_sym] = column.to_sym
79
80
  end
80
- elsif !options[:ignore] && !options[:on_duplicate_key_ignore]
81
- options[:on_duplicate_key_update] = [column.to_sym]
82
81
  end
83
82
  end
84
83
 
85
84
  # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
86
85
  # in +args+.
87
86
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
88
- sql = ' ON DUPLICATE KEY UPDATE '
87
+ sql = ' ON DUPLICATE KEY UPDATE '.dup
89
88
  arg = args.first
89
+ locking_column = args.last
90
90
  if arg.is_a?( Array )
91
- sql << sql_for_on_duplicate_key_update_as_array( table_name, arg )
91
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arg )
92
92
  elsif arg.is_a?( Hash )
93
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, arg )
93
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, arg )
94
94
  elsif arg.is_a?( String )
95
95
  sql << arg
96
96
  else
@@ -99,20 +99,22 @@ module ActiveRecord::Import::MysqlAdapter
99
99
  sql
100
100
  end
101
101
 
102
- def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
102
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
103
103
  results = arr.map do |column|
104
104
  qc = quote_column_name( column )
105
105
  "#{table_name}.#{qc}=VALUES(#{qc})"
106
106
  end
107
+ increment_locking_column!(table_name, results, locking_column)
107
108
  results.join( ',' )
108
109
  end
109
110
 
110
- def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
111
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
111
112
  results = hsh.map do |column1, column2|
112
113
  qc1 = quote_column_name( column1 )
113
114
  qc2 = quote_column_name( column2 )
114
115
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
115
116
  end
117
+ increment_locking_column!(table_name, results, locking_column)
116
118
  results.join( ',')
117
119
  end
118
120
 
@@ -120,4 +122,10 @@ module ActiveRecord::Import::MysqlAdapter
120
122
  def duplicate_key_update_error?(exception) # :nodoc:
121
123
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
122
124
  end
125
+
126
+ def increment_locking_column!(table_name, results, locking_column)
127
+ if locking_column.present?
128
+ results << "`#{locking_column}`=#{table_name}.`#{locking_column}`+1"
129
+ end
130
+ end
123
131
  end
@@ -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,7 +8,7 @@ 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
 
@@ -18,47 +20,53 @@ module ActiveRecord::Import::PostgreSQLAdapter
18
20
 
19
21
  sql2insert = base_sql + values.join( ',' ) + post_sql
20
22
 
21
- columns = returning_columns(options)
22
- if columns.blank? || options[:no_returning]
23
+ selections = returning_selections(options)
24
+ if selections.blank? || (options[:no_returning] && !options[:recursive])
23
25
  insert( sql2insert, *args )
24
26
  else
25
- returned_values = if columns.size > 1
27
+ returned_values = if selections.size > 1
26
28
  # Select composite columns
27
- select_rows( sql2insert, *args )
29
+ db_result = select_all( sql2insert, *args )
30
+ { values: db_result.rows, columns: db_result.columns }
28
31
  else
29
- select_values( sql2insert, *args )
32
+ { values: select_values( sql2insert, *args ) }
30
33
  end
31
- query_cache.clear if query_cache_enabled
34
+ clear_query_cache if query_cache_enabled
32
35
  end
33
36
 
34
37
  if options[:returning].blank?
35
- ids = returned_values
38
+ ids = Array(returned_values[:values])
36
39
  elsif options[:primary_key].blank?
37
- results = returned_values
40
+ options[:returning_columns] ||= returned_values[:columns]
41
+ results = Array(returned_values[:values])
38
42
  else
39
43
  # split primary key and returning columns
40
- ids, results = split_ids_and_results(returned_values, columns, options)
44
+ ids, results, options[:returning_columns] = split_ids_and_results(returned_values, options)
41
45
  end
42
46
 
43
47
  ActiveRecord::Import::Result.new([], number_of_inserts, ids, results)
44
48
  end
45
49
 
46
- def split_ids_and_results(values, columns, options)
50
+ def split_ids_and_results( selections, options )
47
51
  ids = []
48
- results = []
52
+ returning_values = []
53
+
54
+ columns = Array(selections[:columns])
55
+ values = Array(selections[:values])
49
56
  id_indexes = Array(options[:primary_key]).map { |key| columns.index(key) }
50
- returning_indexes = Array(options[:returning]).map { |key| columns.index(key) }
57
+ returning_columns = columns.reject.with_index { |_, index| id_indexes.include?(index) }
58
+ returning_indexes = returning_columns.map { |column| columns.index(column) }
51
59
 
52
60
  values.each do |value|
53
61
  value_array = Array(value)
54
- ids << id_indexes.map { |i| value_array[i] }
55
- results << returning_indexes.map { |i| value_array[i] }
62
+ ids << id_indexes.map { |index| value_array[index] }
63
+ returning_values << returning_indexes.map { |index| value_array[index] }
56
64
  end
57
65
 
58
66
  ids.map!(&:first) if id_indexes.size == 1
59
- results.map!(&:first) if returning_indexes.size == 1
67
+ returning_values.map!(&:first) if returning_columns.size == 1
60
68
 
61
- [ids, results]
69
+ [ids, returning_values, returning_columns]
62
70
  end
63
71
 
64
72
  def next_value_for_sequence(sequence_name)
@@ -73,25 +81,30 @@ module ActiveRecord::Import::PostgreSQLAdapter
73
81
  if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update] && !options[:recursive]
74
82
  sql << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
75
83
  end
76
- elsif options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
84
+ elsif logger && options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
77
85
  logger.warn "Ignoring on_duplicate_key_ignore because it is not supported by the database."
78
86
  end
79
87
 
80
88
  sql += super(table_name, options)
81
89
 
82
- columns = returning_columns(options)
83
- unless columns.blank? || options[:no_returning]
84
- sql << " RETURNING \"#{columns.join('", "')}\""
90
+ selections = returning_selections(options)
91
+ unless selections.blank? || (options[:no_returning] && !options[:recursive])
92
+ sql << " RETURNING #{selections.join(', ')}"
85
93
  end
86
94
 
87
95
  sql
88
96
  end
89
97
 
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
98
+ def returning_selections(options)
99
+ selections = []
100
+ column_names = Array(options[:model].column_names)
101
+
102
+ selections += Array(options[:primary_key]) if options[:primary_key].present?
103
+ selections += Array(options[:returning]) if options[:returning].present?
104
+
105
+ selections.map do |selection|
106
+ column_names.include?(selection.to_s) ? "\"#{selection}\"" : selection
107
+ end
95
108
  end
96
109
 
97
110
  # Add a column to be updated on duplicate key update
@@ -119,11 +132,11 @@ module ActiveRecord::Import::PostgreSQLAdapter
119
132
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
120
133
  # in +args+.
121
134
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
122
- arg, primary_key = args
135
+ arg, primary_key, locking_column = args
123
136
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
124
137
  return unless arg.is_a?( Hash )
125
138
 
126
- sql = ' ON CONFLICT '
139
+ sql = ' ON CONFLICT '.dup
127
140
  conflict_target = sql_for_conflict_target( arg )
128
141
 
129
142
  columns = arg.fetch( :columns, [] )
@@ -139,9 +152,9 @@ module ActiveRecord::Import::PostgreSQLAdapter
139
152
 
140
153
  sql << "#{conflict_target}DO UPDATE SET "
141
154
  if columns.is_a?( Array )
142
- sql << sql_for_on_duplicate_key_update_as_array( table_name, columns )
155
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
143
156
  elsif columns.is_a?( Hash )
144
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, columns )
157
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
145
158
  elsif columns.is_a?( String )
146
159
  sql << columns
147
160
  else
@@ -153,20 +166,22 @@ module ActiveRecord::Import::PostgreSQLAdapter
153
166
  sql
154
167
  end
155
168
 
156
- def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
169
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
157
170
  results = arr.map do |column|
158
171
  qc = quote_column_name( column )
159
172
  "#{qc}=EXCLUDED.#{qc}"
160
173
  end
174
+ increment_locking_column!(table_name, results, locking_column)
161
175
  results.join( ',' )
162
176
  end
163
177
 
164
- def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
178
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
165
179
  results = hsh.map do |column1, column2|
166
180
  qc1 = quote_column_name( column1 )
167
181
  qc2 = quote_column_name( column2 )
168
182
  "#{qc1}=EXCLUDED.#{qc2}"
169
183
  end
184
+ increment_locking_column!(table_name, results, locking_column)
170
185
  results.join( ',' )
171
186
  end
172
187
 
@@ -177,9 +192,9 @@ module ActiveRecord::Import::PostgreSQLAdapter
177
192
  if constraint_name.present?
178
193
  "ON CONSTRAINT #{constraint_name} "
179
194
  elsif conflict_target.present?
180
- '(' << Array( conflict_target ).reject( &:blank? ).join( ', ' ) << ') '.tap do |sql|
181
- sql << "WHERE #{index_predicate} " if index_predicate
182
- end
195
+ sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
196
+ sql += "WHERE #{index_predicate} " if index_predicate
197
+ sql
183
198
  end
184
199
  end
185
200
 
@@ -193,11 +208,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
193
208
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
194
209
  end
195
210
 
196
- def supports_on_duplicate_key_update?(current_version = postgresql_version)
197
- current_version >= MIN_VERSION_FOR_UPSERT
211
+ def supports_on_duplicate_key_update?
212
+ database_version >= MIN_VERSION_FOR_UPSERT
198
213
  end
199
214
 
200
215
  def supports_setting_primary_key_of_imported_objects?
201
216
  true
202
217
  end
218
+
219
+ def database_version
220
+ defined?(postgresql_version) ? postgresql_version : super
221
+ end
203
222
  end