@@ -154,6 +154,73 @@ def default_insert_value(column)
154
154
private :default_insert_value
155
155
156
156
def build_insert_sql ( insert ) # :nodoc:
157
+ if insert . skip_duplicates?
158
+ insert_all = insert . send ( :insert_all )
159
+ conflict_columns = get_conflicted_columns ( insert_all :, insert :)
160
+
161
+ # if we do not have any columns that might have conflicting values, just execute a regular insert
162
+ return build_sql_for_regular_insert ( insert ) if conflict_columns . empty?
163
+
164
+ make_inserts_unique ( insert_all :, conflict_columns :)
165
+
166
+ primary_keys_for_insert = insert_all . primary_keys . to_set
167
+
168
+ # if we receive a composite primary key, MSSQL will not be happy when we want to call "IDENTITY_INSERT"
169
+ # as there is likely no IDENTITY column
170
+ # so we need to check if there is exacty one
171
+ # TODO: Refactor to use existing "SET IDENTITY_INSERT" settings
172
+ enable_identity_insert = primary_keys_for_insert . length == 1 &&
173
+ ( insert_all . primary_keys . to_set & insert . keys ) . present?
174
+
175
+ sql = +""
176
+ sql << "SET IDENTITY_INSERT #{ insert . model . quoted_table_name } ON;" if enable_identity_insert
177
+ sql << "MERGE INTO #{ insert . model . quoted_table_name } WITH (UPDLOCK, HOLDLOCK) AS target"
178
+ sql << " USING (SELECT DISTINCT * FROM (#{ insert . values_list } ) AS t1 (#{ insert . send ( :columns_list ) } )) AS source"
179
+ sql << " ON (#{ conflict_columns . map do |columns |
180
+ columns . map do |column |
181
+ "target.#{ quote_column_name ( column ) } = source.#{ quote_column_name ( column ) } "
182
+ end . join ( " AND " )
183
+ end . join ( ") OR (" ) } )"
184
+ sql << " WHEN NOT MATCHED BY TARGET THEN"
185
+ sql << " INSERT (#{ insert . send ( :columns_list ) } ) #{ insert . values_list } "
186
+ if ( returning = insert_all . returning )
187
+ sql << " OUTPUT " << returning . map { |column | "INSERTED.#{ quote_column_name ( column ) } " } . join ( ", " )
188
+ end
189
+ sql << ";"
190
+ sql << "SET IDENTITY_INSERT #{ insert . model . quoted_table_name } OFF;" if enable_identity_insert
191
+ return sql
192
+ end
193
+
194
+ build_sql_for_regular_insert ( insert )
195
+ end
196
+
197
+ # MERGE executes a JOIN between our data we would like to insert and the existing data in the table
198
+ # but since it is a JOIN, it requires the data in the source also to be unique (aka our values to insert)
199
+ # here we modify @inserts in place of the "insert_all" object to be unique
200
+ # keeping the last occurence
201
+ # note that for other DBMS, this job is usually handed off to them by specifying something like
202
+ # "ON DUPLICATE SKIP" or "ON DUPLICATE UPDATE"
203
+ def make_inserts_unique ( insert_all :, conflict_columns :)
204
+ unique_inserts = insert_all . inserts . reverse . uniq { |insert | conflict_columns . map { |column | insert [ column ] } } . reverse
205
+ insert_all . instance_variable_set ( :@inserts , unique_inserts )
206
+ end
207
+ private :make_inserts_unique
208
+
209
+ def get_conflicted_columns ( insert_all :, insert :)
210
+ if ( unique_by = insert_all . unique_by )
211
+ [ unique_by . columns ]
212
+ else
213
+ # Compare against every unique constraint (primary key included).
214
+ # Discard constraints that are not fully included on insert.keys. Prevents invalid queries.
215
+ # Example: ignore unique index for columns ["name"] if insert keys is ["description"]
216
+ ( insert_all . send ( :unique_indexes ) . map ( &:columns ) + [ insert_all . primary_keys ] ) . select do |columns |
217
+ columns . to_set . subset? ( insert . keys )
218
+ end
219
+ end
220
+ end
221
+ private :get_conflicted_columns
222
+
223
+ def build_sql_for_regular_insert ( insert )
157
224
sql = "INSERT #{ insert . into } "
158
225
159
226
returning = insert . send ( :insert_all ) . returning
@@ -170,6 +237,7 @@ def build_insert_sql(insert) # :nodoc:
170
237
sql << " #{ insert . values_list } "
171
238
sql
172
239
end
240
+ private :build_sql_for_regular_insert
173
241
174
242
# === SQLServer Specific ======================================== #
175
243
0 commit comments