activerecord-import 0.17.2 → 1.1.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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +40 -23
- data/CHANGELOG.md +315 -1
- data/Gemfile +23 -13
- data/LICENSE +21 -56
- data/README.markdown +564 -33
- data/Rakefile +2 -1
- data/activerecord-import.gemspec +3 -3
- data/benchmarks/lib/cli_parser.rb +2 -1
- data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +0 -0
- data/gemfiles/5.1.gemfile +2 -0
- data/gemfiles/5.2.gemfile +2 -0
- data/gemfiles/6.0.gemfile +2 -0
- data/gemfiles/6.1.gemfile +1 -0
- data/lib/activerecord-import.rb +2 -15
- data/lib/activerecord-import/adapters/abstract_adapter.rb +9 -3
- data/lib/activerecord-import/adapters/mysql_adapter.rb +17 -11
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +68 -20
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
- data/lib/activerecord-import/base.rb +12 -7
- data/lib/activerecord-import/import.rb +514 -166
- data/lib/activerecord-import/synchronize.rb +2 -2
- data/lib/activerecord-import/value_sets_parser.rb +16 -0
- data/lib/activerecord-import/version.rb +1 -1
- data/test/adapters/makara_postgis.rb +1 -0
- data/test/import_test.rb +274 -23
- data/test/makara_postgis/import_test.rb +8 -0
- data/test/models/account.rb +3 -0
- data/test/models/animal.rb +6 -0
- data/test/models/bike_maker.rb +7 -0
- data/test/models/tag.rb +1 -1
- data/test/models/topic.rb +14 -0
- data/test/models/user.rb +3 -0
- data/test/models/user_token.rb +4 -0
- data/test/schema/generic_schema.rb +30 -8
- data/test/schema/mysql2_schema.rb +19 -0
- data/test/schema/postgresql_schema.rb +18 -0
- data/test/schema/sqlite3_schema.rb +13 -0
- data/test/support/factories.rb +9 -8
- data/test/support/generate.rb +6 -6
- data/test/support/mysql/import_examples.rb +14 -2
- data/test/support/postgresql/import_examples.rb +220 -1
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +15 -9
- data/test/support/shared_examples/on_duplicate_key_update.rb +271 -8
- data/test/support/shared_examples/recursive_import.rb +91 -21
- data/test/support/sqlite3/import_examples.rb +189 -25
- data/test/synchronize_test.rb +8 -0
- data/test/test_helper.rb +24 -3
- data/test/value_sets_bytes_parser_test.rb +13 -2
- metadata +32 -13
- data/test/schema/mysql_schema.rb +0 -16
data/Rakefile
CHANGED
@@ -23,6 +23,7 @@ ADAPTERS = %w(
|
|
23
23
|
postgresql
|
24
24
|
postgresql_makara
|
25
25
|
postgis
|
26
|
+
makara_postgis
|
26
27
|
sqlite3
|
27
28
|
spatialite
|
28
29
|
seamless_database_pool
|
@@ -31,7 +32,7 @@ ADAPTERS.each do |adapter|
|
|
31
32
|
namespace :test do
|
32
33
|
desc "Runs #{adapter} database tests."
|
33
34
|
Rake::TestTask.new(adapter) do |t|
|
34
|
-
#
|
35
|
+
# FactoryBot has an issue with warnings, so turn off, so noisy
|
35
36
|
# t.warning = true
|
36
37
|
t.test_files = FileList["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/active_record/*_test.rb", "test/#{adapter}/**/*_test.rb"]
|
37
38
|
end
|
data/activerecord-import.gemspec
CHANGED
@@ -6,8 +6,8 @@ Gem::Specification.new do |gem|
|
|
6
6
|
gem.email = ["zach.dennis@gmail.com"]
|
7
7
|
gem.summary = "Bulk insert extension for ActiveRecord"
|
8
8
|
gem.description = "A library for bulk inserting data using ActiveRecord."
|
9
|
-
gem.homepage = "
|
10
|
-
gem.license = "
|
9
|
+
gem.homepage = "https://github.com/zdennis/activerecord-import"
|
10
|
+
gem.license = "MIT"
|
11
11
|
|
12
12
|
gem.files = `git ls-files`.split($\)
|
13
13
|
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.require_paths = ["lib"]
|
17
17
|
gem.version = ActiveRecord::Import::VERSION
|
18
18
|
|
19
|
-
gem.required_ruby_version = ">=
|
19
|
+
gem.required_ruby_version = ">= 2.0.0"
|
20
20
|
|
21
21
|
gem.add_runtime_dependency "activerecord", ">= 3.2"
|
22
22
|
gem.add_development_dependency "rake"
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
gem 'activerecord', '~> 6.1.0'
|
data/lib/activerecord-import.rb
CHANGED
@@ -1,19 +1,6 @@
|
|
1
1
|
# rubocop:disable Style/FileName
|
2
|
+
require "active_support/lazy_load_hooks"
|
2
3
|
|
3
4
|
ActiveSupport.on_load(:active_record) do
|
4
|
-
|
5
|
-
class << self
|
6
|
-
def establish_connection_with_activerecord_import(*args)
|
7
|
-
conn = establish_connection_without_activerecord_import(*args)
|
8
|
-
if !ActiveRecord.const_defined?(:Import) || !ActiveRecord::Import.respond_to?(:load_from_connection_pool)
|
9
|
-
require "activerecord-import/base"
|
10
|
-
end
|
11
|
-
|
12
|
-
ActiveRecord::Import.load_from_connection_pool connection_pool
|
13
|
-
conn
|
14
|
-
end
|
15
|
-
alias establish_connection_without_activerecord_import establish_connection
|
16
|
-
alias establish_connection establish_connection_with_activerecord_import
|
17
|
-
end
|
18
|
-
end
|
5
|
+
require "activerecord-import/base"
|
19
6
|
end
|
@@ -16,7 +16,7 @@ module ActiveRecord::Import::AbstractAdapter
|
|
16
16
|
sql2insert = base_sql + values.join( ',' ) + post_sql
|
17
17
|
insert( sql2insert, *args )
|
18
18
|
|
19
|
-
[number_of_inserts, []]
|
19
|
+
ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
|
20
20
|
end
|
21
21
|
|
22
22
|
def pre_sql_statements(options)
|
@@ -45,8 +45,8 @@ module ActiveRecord::Import::AbstractAdapter
|
|
45
45
|
post_sql_statements = []
|
46
46
|
|
47
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] )
|
49
|
-
elsif 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
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
|
@@ -49,16 +49,16 @@ module ActiveRecord::Import::MysqlAdapter
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
-
[number_of_inserts, []]
|
52
|
+
ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
|
53
53
|
end
|
54
54
|
|
55
55
|
# Returns the maximum number of bytes that the server will allow
|
56
56
|
# in a single packet
|
57
57
|
def max_allowed_packet # :nodoc:
|
58
58
|
@max_allowed_packet ||= begin
|
59
|
-
result = execute( "
|
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[
|
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,14 +71,11 @@ 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
|
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
|
|
@@ -87,10 +84,11 @@ module ActiveRecord::Import::MysqlAdapter
|
|
87
84
|
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
|
88
85
|
sql = ' ON DUPLICATE KEY UPDATE '
|
89
86
|
arg = args.first
|
87
|
+
locking_column = args.last
|
90
88
|
if arg.is_a?( Array )
|
91
|
-
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 )
|
92
90
|
elsif arg.is_a?( Hash )
|
93
|
-
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 )
|
94
92
|
elsif arg.is_a?( String )
|
95
93
|
sql << arg
|
96
94
|
else
|
@@ -99,20 +97,22 @@ module ActiveRecord::Import::MysqlAdapter
|
|
99
97
|
sql
|
100
98
|
end
|
101
99
|
|
102
|
-
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:
|
103
101
|
results = arr.map do |column|
|
104
102
|
qc = quote_column_name( column )
|
105
103
|
"#{table_name}.#{qc}=VALUES(#{qc})"
|
106
104
|
end
|
105
|
+
increment_locking_column!(table_name, results, locking_column)
|
107
106
|
results.join( ',' )
|
108
107
|
end
|
109
108
|
|
110
|
-
def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
|
109
|
+
def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
|
111
110
|
results = hsh.map do |column1, column2|
|
112
111
|
qc1 = quote_column_name( column1 )
|
113
112
|
qc2 = quote_column_name( column2 )
|
114
113
|
"#{table_name}.#{qc1}=VALUES( #{qc2} )"
|
115
114
|
end
|
115
|
+
increment_locking_column!(table_name, results, locking_column)
|
116
116
|
results.join( ',')
|
117
117
|
end
|
118
118
|
|
@@ -120,4 +120,10 @@ module ActiveRecord::Import::MysqlAdapter
|
|
120
120
|
def duplicate_key_update_error?(exception) # :nodoc:
|
121
121
|
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
|
122
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
|
123
129
|
end
|
@@ -5,9 +5,10 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
5
5
|
MIN_VERSION_FOR_UPSERT = 90_500
|
6
6
|
|
7
7
|
def insert_many( sql, values, options = {}, *args ) # :nodoc:
|
8
|
-
primary_key = options[:primary_key]
|
9
8
|
number_of_inserts = 1
|
9
|
+
returned_values = []
|
10
10
|
ids = []
|
11
|
+
results = []
|
11
12
|
|
12
13
|
base_sql, post_sql = if sql.is_a?( String )
|
13
14
|
[sql, '']
|
@@ -17,19 +18,47 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
17
18
|
|
18
19
|
sql2insert = base_sql + values.join( ',' ) + post_sql
|
19
20
|
|
20
|
-
|
21
|
+
columns = returning_columns(options)
|
22
|
+
if columns.blank? || (options[:no_returning] && !options[:recursive])
|
21
23
|
insert( sql2insert, *args )
|
22
24
|
else
|
23
|
-
|
24
|
-
# Select composite
|
25
|
+
returned_values = if columns.size > 1
|
26
|
+
# Select composite columns
|
25
27
|
select_rows( sql2insert, *args )
|
26
28
|
else
|
27
29
|
select_values( sql2insert, *args )
|
28
30
|
end
|
29
|
-
|
31
|
+
clear_query_cache if query_cache_enabled
|
30
32
|
end
|
31
33
|
|
32
|
-
[
|
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]
|
33
62
|
end
|
34
63
|
|
35
64
|
def next_value_for_sequence(sequence_name)
|
@@ -44,20 +73,27 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
44
73
|
if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update] && !options[:recursive]
|
45
74
|
sql << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
|
46
75
|
end
|
47
|
-
elsif options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
|
76
|
+
elsif logger && options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
|
48
77
|
logger.warn "Ignoring on_duplicate_key_ignore because it is not supported by the database."
|
49
78
|
end
|
50
79
|
|
51
80
|
sql += super(table_name, options)
|
52
81
|
|
53
|
-
|
54
|
-
|
55
|
-
sql << " RETURNING \"#{
|
82
|
+
columns = returning_columns(options)
|
83
|
+
unless columns.blank? || (options[:no_returning] && !options[:recursive])
|
84
|
+
sql << " RETURNING \"#{columns.join('", "')}\""
|
56
85
|
end
|
57
86
|
|
58
87
|
sql
|
59
88
|
end
|
60
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
|
+
|
61
97
|
# Add a column to be updated on duplicate key update
|
62
98
|
def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
|
63
99
|
arg = options[:on_duplicate_key_update]
|
@@ -83,14 +119,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
83
119
|
# Returns a generated ON CONFLICT DO UPDATE statement given the passed
|
84
120
|
# in +args+.
|
85
121
|
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
|
86
|
-
arg, primary_key = args
|
122
|
+
arg, primary_key, locking_column = args
|
87
123
|
arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
|
88
124
|
return unless arg.is_a?( Hash )
|
89
125
|
|
90
|
-
sql =
|
126
|
+
sql = ' ON CONFLICT '
|
91
127
|
conflict_target = sql_for_conflict_target( arg )
|
92
128
|
|
93
129
|
columns = arg.fetch( :columns, [] )
|
130
|
+
condition = arg[:condition]
|
94
131
|
if columns.respond_to?( :empty? ) && columns.empty?
|
95
132
|
return sql << "#{conflict_target}DO NOTHING"
|
96
133
|
end
|
@@ -102,31 +139,36 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
102
139
|
|
103
140
|
sql << "#{conflict_target}DO UPDATE SET "
|
104
141
|
if columns.is_a?( Array )
|
105
|
-
sql << sql_for_on_duplicate_key_update_as_array( table_name, columns )
|
142
|
+
sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
|
106
143
|
elsif columns.is_a?( Hash )
|
107
|
-
sql << sql_for_on_duplicate_key_update_as_hash( table_name, columns )
|
144
|
+
sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
|
108
145
|
elsif columns.is_a?( String )
|
109
146
|
sql << columns
|
110
147
|
else
|
111
148
|
raise ArgumentError, 'Expected :columns to be an Array or Hash'
|
112
149
|
end
|
150
|
+
|
151
|
+
sql << " WHERE #{condition}" if condition.present?
|
152
|
+
|
113
153
|
sql
|
114
154
|
end
|
115
155
|
|
116
|
-
def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
|
156
|
+
def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
|
117
157
|
results = arr.map do |column|
|
118
158
|
qc = quote_column_name( column )
|
119
159
|
"#{qc}=EXCLUDED.#{qc}"
|
120
160
|
end
|
161
|
+
increment_locking_column!(table_name, results, locking_column)
|
121
162
|
results.join( ',' )
|
122
163
|
end
|
123
164
|
|
124
|
-
def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
|
165
|
+
def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
|
125
166
|
results = hsh.map do |column1, column2|
|
126
167
|
qc1 = quote_column_name( column1 )
|
127
168
|
qc2 = quote_column_name( column2 )
|
128
169
|
"#{qc1}=EXCLUDED.#{qc2}"
|
129
170
|
end
|
171
|
+
increment_locking_column!(table_name, results, locking_column)
|
130
172
|
results.join( ',' )
|
131
173
|
end
|
132
174
|
|
@@ -137,7 +179,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
137
179
|
if constraint_name.present?
|
138
180
|
"ON CONSTRAINT #{constraint_name} "
|
139
181
|
elsif conflict_target.present?
|
140
|
-
'(' << Array( conflict_target ).reject( &:
|
182
|
+
'(' << Array( conflict_target ).reject( &:blank? ).join( ', ' ) << ') '.tap do |sql|
|
141
183
|
sql << "WHERE #{index_predicate} " if index_predicate
|
142
184
|
end
|
143
185
|
end
|
@@ -153,11 +195,17 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
153
195
|
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
|
154
196
|
end
|
155
197
|
|
156
|
-
def supports_on_duplicate_key_update?
|
157
|
-
|
198
|
+
def supports_on_duplicate_key_update?
|
199
|
+
database_version >= MIN_VERSION_FOR_UPSERT
|
158
200
|
end
|
159
201
|
|
160
|
-
def
|
202
|
+
def supports_setting_primary_key_of_imported_objects?
|
161
203
|
true
|
162
204
|
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def database_version
|
209
|
+
defined?(postgresql_version) ? postgresql_version : super
|
210
|
+
end
|
163
211
|
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?
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
@@ -37,19 +39,136 @@ module ActiveRecord::Import::SQLite3Adapter
|
|
37
39
|
end
|
38
40
|
end
|
39
41
|
|
40
|
-
[number_of_inserts, []]
|
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])
|
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 '
|
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
|
55
174
|
end
|