No rows in results generates invalid MERGE statement
dnlnln opened this issue · comments
Originally reported by @Dunnymeister in PR #26
Running sp_generate_merge on a table where there are no rows, or with a @from filter which returns no rows, created invalid SQL.
Steps to reproduce
Execute the following (AdventureWorks.dbo.ErrorLog
table should have zero rows):
EXEC [AdventureWorks2014]..[sp_generate_merge] @table_name = 'ErrorLog', @schema = 'dbo'
This generates the following T-SQL:
USE AdventureWorks2014
GO
SET NOCOUNT ON
SET IDENTITY_INSERT [dbo].[ErrorLog] ON
MERGE INTO [dbo].[ErrorLog] AS Target
USING (VALUES
) AS Source ([ErrorLogID],[ErrorTime],[UserName],[ErrorNumber],[ErrorSeverity],[ErrorState],[ErrorProcedure],[ErrorLine],[ErrorMessage])
ON (Target.[ErrorLogID] = Source.[ErrorLogID])
WHEN MATCHED AND (
NULLIF(Source.[ErrorTime], Target.[ErrorTime]) IS NOT NULL OR NULLIF(Target.[ErrorTime], Source.[ErrorTime]) IS NOT NULL OR
NULLIF(Source.[UserName], Target.[UserName]) IS NOT NULL OR NULLIF(Target.[UserName], Source.[UserName]) IS NOT NULL OR
NULLIF(Source.[ErrorNumber], Target.[ErrorNumber]) IS NOT NULL OR NULLIF(Target.[ErrorNumber], Source.[ErrorNumber]) IS NOT NULL OR
NULLIF(Source.[ErrorSeverity], Target.[ErrorSeverity]) IS NOT NULL OR NULLIF(Target.[ErrorSeverity], Source.[ErrorSeverity]) IS NOT NULL OR
NULLIF(Source.[ErrorState], Target.[ErrorState]) IS NOT NULL OR NULLIF(Target.[ErrorState], Source.[ErrorState]) IS NOT NULL OR
NULLIF(Source.[ErrorProcedure], Target.[ErrorProcedure]) IS NOT NULL OR NULLIF(Target.[ErrorProcedure], Source.[ErrorProcedure]) IS NOT NULL OR
NULLIF(Source.[ErrorLine], Target.[ErrorLine]) IS NOT NULL OR NULLIF(Target.[ErrorLine], Source.[ErrorLine]) IS NOT NULL OR
NULLIF(Source.[ErrorMessage], Target.[ErrorMessage]) IS NOT NULL OR NULLIF(Target.[ErrorMessage], Source.[ErrorMessage]) IS NOT NULL) THEN
UPDATE SET
[ErrorTime] = Source.[ErrorTime],
[UserName] = Source.[UserName],
[ErrorNumber] = Source.[ErrorNumber],
[ErrorSeverity] = Source.[ErrorSeverity],
[ErrorState] = Source.[ErrorState],
[ErrorProcedure] = Source.[ErrorProcedure],
[ErrorLine] = Source.[ErrorLine],
[ErrorMessage] = Source.[ErrorMessage]
WHEN NOT MATCHED BY TARGET THEN
INSERT([ErrorLogID],[ErrorTime],[UserName],[ErrorNumber],[ErrorSeverity],[ErrorState],[ErrorProcedure],[ErrorLine],[ErrorMessage])
VALUES(Source.[ErrorLogID],Source.[ErrorTime],Source.[UserName],Source.[ErrorNumber],Source.[ErrorSeverity],Source.[ErrorState],Source.[ErrorProcedure],Source.[ErrorLine],Source.[ErrorMessage])
WHEN NOT MATCHED BY SOURCE THEN
DELETE
;
GO
DECLARE @mergeError int
, @mergeCount int
SELECT @mergeError = @@ERROR, @mergeCount = @@ROWCOUNT
IF @mergeError != 0
BEGIN
PRINT 'ERROR OCCURRED IN MERGE FOR [dbo].[ErrorLog]. Rows affected: ' + CAST(@mergeCount AS VARCHAR(100)); -- SQL should always return zero rows affected
END
ELSE
BEGIN
PRINT '[dbo].[ErrorLog] rows affected by MERGE: ' + CAST(@mergeCount AS VARCHAR(100));
END
GO
SET IDENTITY_INSERT [dbo].[ErrorLog] OFF
GO
SET NOCOUNT OFF
GO
Execute the generated T-SQL to produce the following error:
Msg 102, Level 15, State 1, Line 15
Incorrect syntax near ')'.
ERROR OCCURRED IN MERGE FOR [dbo].[ErrorLog]. Rows affected: 0
A solution that has worked for a few people over on SO is to replace the VALUES
clause of the source with a SELECT
on the target table that returns an empty result set. This workaround would change the above (erroneous) MERGE
statement from:
MERGE INTO [dbo].[ErrorLog] AS Target
USING (VALUES
) AS Source ([ErrorLogID],[ErrorTime],[UserName],[ErrorNumber],[ErrorSeverity],[ErrorState],[ErrorProcedure],[ErrorLine],[ErrorMessage])
...
To the following (valid) T-SQL:
MERGE INTO [dbo].[ErrorLog] AS Target
USING (SELECT [ErrorLogID],[ErrorTime],[UserName],[ErrorNumber],[ErrorSeverity],[ErrorState],[ErrorProcedure],[ErrorLine],[ErrorMessage] FROM [dbo].[ErrorLog] WHERE 1 = 0
) AS Source ([ErrorLogID],[ErrorTime],[UserName],[ErrorNumber],[ErrorSeverity],[ErrorState],[ErrorProcedure],[ErrorLine],[ErrorMessage])
...
Note that, if this T-SQL were to be substituted into the above sample MERGE
statement and executed, this would result in all rows being deleted from the target table. This would be the correct behaviour, however, as the intention is to always have the target
table reflect the state of the source
dataset, even if it means emptying the target. The only rows that wouldn't be deleted are any that have been filtered out by a WHERE
clause specified within the @from
parameter (although you'll note in the above sp_generate_merge
example, this parameter has not been specified).