@@ -154,6 +154,73 @@ def default_insert_value(column)
154154 private :default_insert_value
155155
156156 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 )
157224 sql = "INSERT #{ insert . into } "
158225
159226 returning = insert . send ( :insert_all ) . returning
@@ -170,6 +237,7 @@ def build_insert_sql(insert) # :nodoc:
170237 sql << " #{ insert . values_list } "
171238 sql
172239 end
240+ private :build_sql_for_regular_insert
173241
174242 # === SQLServer Specific ======================================== #
175243
0 commit comments