1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
|
# frozen_string_literal: true
module ActiveRecord::Import::SQLite3Adapter
include ActiveRecord::Import::ImportSupport
include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
MIN_VERSION_FOR_IMPORT = "3.7.11"
MIN_VERSION_FOR_UPSERT = "3.24.0"
SQLITE_LIMIT_COMPOUND_SELECT = 500
# Override our conformance to ActiveRecord::Import::ImportSupport interface
# to ensure that we only support import in supported version of SQLite.
# Which INSERT statements with multiple value sets was introduced in 3.7.11.
def supports_import?
database_version >= MIN_VERSION_FOR_IMPORT
end
def supports_on_duplicate_key_update?
database_version >= MIN_VERSION_FOR_UPSERT
end
# +sql+ can be a single string or an array. If it is an array all
# elements that are in position >= 1 will be appended to the final SQL.
def insert_many( sql, values, _options = {}, *args ) # :nodoc:
number_of_inserts = 0
base_sql, post_sql = if sql.is_a?( String )
[sql, '']
elsif sql.is_a?( Array )
[sql.shift, sql.join( ' ' )]
end
value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values,
max_records: SQLITE_LIMIT_COMPOUND_SELECT)
transaction(requires_new: true) do
value_sets.each do |value_set|
number_of_inserts += 1
sql2insert = base_sql + value_set.join( ',' ) + post_sql
insert( sql2insert, *args )
end
end
ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
end
def pre_sql_statements( options )
sql = []
# Options :recursive and :on_duplicate_key_ignore are mutually exclusive
if !supports_on_duplicate_key_update? && (options[:ignore] || options[:on_duplicate_key_ignore])
sql << "OR IGNORE"
end
sql + super
end
def post_sql_statements( table_name, options ) # :nodoc:
sql = []
if supports_on_duplicate_key_update?
# Options :recursive and :on_duplicate_key_ignore are mutually exclusive
if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update]
sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
end
end
sql + super
end
def next_value_for_sequence(sequence_name)
%{nextval('#{sequence_name}')}
end
# Add a column to be updated on duplicate key update
def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
arg = options[:on_duplicate_key_update]
if arg.is_a?( Hash )
columns = arg.fetch( :columns ) { arg[:columns] = [] }
case columns
when Array then columns << column.to_sym unless columns.include?( column.to_sym )
when Hash then columns[column.to_sym] = column.to_sym
end
elsif arg.is_a?( Array )
arg << column.to_sym unless arg.include?( column.to_sym )
end
end
# Returns a generated ON CONFLICT DO NOTHING statement given the passed
# in +args+.
def sql_for_on_duplicate_key_ignore( *args ) # :nodoc:
arg = args.first
conflict_target = sql_for_conflict_target( arg ) if arg.is_a?( Hash )
" ON CONFLICT #{conflict_target}DO NOTHING"
end
# Returns a generated ON CONFLICT DO UPDATE statement given the passed
# in +args+.
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
arg, primary_key, locking_column = args
arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
return unless arg.is_a?( Hash )
sql = ' ON CONFLICT '.dup
conflict_target = sql_for_conflict_target( arg )
columns = arg.fetch( :columns, [] )
condition = arg[:condition]
if columns.respond_to?( :empty? ) && columns.empty?
return sql << "#{conflict_target}DO NOTHING"
end
conflict_target ||= sql_for_default_conflict_target( primary_key )
unless conflict_target
raise ArgumentError, 'Expected :conflict_target to be specified'
end
sql << "#{conflict_target}DO UPDATE SET "
if columns.is_a?( Array )
sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
elsif columns.is_a?( Hash )
sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
elsif columns.is_a?( String )
sql << columns
else
raise ArgumentError, 'Expected :columns to be an Array or Hash'
end
sql << " WHERE #{condition}" if condition.present?
sql
end
def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
results = arr.map do |column|
qc = quote_column_name( column )
"#{qc}=EXCLUDED.#{qc}"
end
increment_locking_column!(table_name, results, locking_column)
results.join( ',' )
end
def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
results = hsh.map do |column1, column2|
qc1 = quote_column_name( column1 )
qc2 = quote_column_name( column2 )
"#{qc1}=EXCLUDED.#{qc2}"
end
increment_locking_column!(table_name, results, locking_column)
results.join( ',' )
end
def sql_for_conflict_target( args = {} )
conflict_target = args[:conflict_target]
index_predicate = args[:index_predicate]
if conflict_target.present?
sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
sql += "WHERE #{index_predicate} " if index_predicate
sql
end
end
def sql_for_default_conflict_target( primary_key )
conflict_target = Array(primary_key).join(', ')
"(#{conflict_target}) " if conflict_target.present?
end
# Return true if the statement is a duplicate key record error
def duplicate_key_update_error?(exception) # :nodoc:
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
end
def database_version
defined?(sqlite_version) ? sqlite_version : super
end
end
|