Last active
September 21, 2018 19:36
-
-
Save ryanvs/62138cb087881d80c657bdfedacd58cf to your computer and use it in GitHub Desktop.
Create SQL Merge using XML
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- ************************************************************************************************************************* | |
-- Name - Create-Merge-Script.sql | |
-- Description - Generates a SQL script to manage table data using XML with MERGE. | |
-- - Change variable @table to the desired output table name, and add @ignore_columns as necessary | |
-- - The output will be SQL lines that can be saved as a script | |
-- Author - Ryan van Slooten | |
---------------------------------------------------------------------------------------------------------------------------- | |
-- Change Date Change By Ver. Changes | |
---------------------------------------------------------------------------------------------------------------------------- | |
-- 2017-02-27 rjvs 1.0.4 Added comment block | |
-- 2017-10-19 rjvs 1.0.5 Used a temporary table for xml loading and comparison | |
-- 2018-09-21 rjvs 1.0.6 Added CAST for source/target columns | |
-- ************************************************************************************************************************* | |
SET NOCOUNT ON | |
DECLARE @table nvarchar(100) | |
DECLARE @ignore_columns TABLE (name nvarchar(40)) | |
-- ==================================== | |
-- PARAMETER: Name of table to merge (Assumes schema/owner) | |
-- ==================================== | |
SET @table = 'MES_sample_attr_ext' | |
-- ==================================== | |
-- PARAMETER: Add any columns to ignore | |
-- ==================================== | |
INSERT @ignore_columns VALUES ('last_edit_at'), ('last_edit_by') | |
-- ============================================================================ | |
DECLARE @temp_table nvarchar(100) | |
, @merge_table nvarchar(100) | |
, @tran_name nvarchar(100) | |
, @max_len int = 0 | |
, @has_identity bit = 0; | |
SET @temp_table = '@temp_' + @table | |
SET @merge_table = QUOTENAME('#merge_' + @table) | |
SET @tran_name = QUOTENAME('tran_' + @table) | |
DECLARE @lines TABLE (id int IDENTITY(1,1), line nvarchar(max)) | |
DECLARE @base_columns TABLE (id int IDENTITY(1,1), name nvarchar(80), quote_name nvarchar(80), data_type nvarchar(40), orig_data_type nvarchar(40), position int, is_pk bit DEFAULT 0, is_identity bit DEFAULT 0, cast_source bit DEFAULT 0, cast_target bit DEFAULT 0) | |
DECLARE @merge_columns TABLE (id int IDENTITY(1,1), name nvarchar(80), data_type nvarchar(40), orig_data_type nvarchar(40)) | |
DECLARE @pk_columns TABLE (id int IDENTITY(1,1), name nvarchar(80)) | |
INSERT @base_columns (name, quote_name, orig_data_type, data_type, cast_source, cast_target, position) | |
SELECT col.COLUMN_NAME | |
-- Quoted name (if required) | |
, CASE | |
WHEN col.COLUMN_NAME = SUBSTRING(QUOTENAME(col.COLUMN_NAME), 2, LEN(col.COLUMN_NAME)) THEN | |
col.COLUMN_NAME | |
ELSE | |
QUOTENAME(col.COLUMN_NAME) | |
END | |
-- Original data type | |
, CASE col.DATA_TYPE | |
WHEN 'nvarchar' THEN | |
col.DATA_TYPE + '(' + IIF(col.CHARACTER_MAXIMUM_LENGTH = -1, 'max', CAST(col.CHARACTER_MAXIMUM_LENGTH AS nvarchar)) + ')' | |
ELSE | |
col.DATA_TYPE | |
END | |
-- Mapped data type | |
, CASE col.DATA_TYPE | |
WHEN 'text' THEN | |
'varchar(max)' | |
WHEN 'varchar' THEN | |
col.DATA_TYPE + '(' + IIF(col.CHARACTER_MAXIMUM_LENGTH = -1, 'max', CAST(col.CHARACTER_MAXIMUM_LENGTH AS nvarchar)) + ')' | |
WHEN 'ntext' THEN | |
'nvarchar(max)' | |
WHEN 'nvarchar' THEN | |
col.DATA_TYPE + '(' + IIF(col.CHARACTER_MAXIMUM_LENGTH = -1, 'max', CAST(col.CHARACTER_MAXIMUM_LENGTH AS nvarchar)) + ')' | |
ELSE | |
col.DATA_TYPE | |
END | |
-- Is CAST required for SOURCE column comparison | |
, CASE | |
WHEN col.DATA_TYPE IN ('text', 'ntext', 'binary', 'varbinary', 'xml') THEN | |
1 | |
ELSE | |
0 | |
END | |
-- Is CAST required for TARGET column comparison | |
, CASE | |
WHEN col.DATA_TYPE IN ('binary', 'varbinary', 'xml') THEN | |
1 | |
ELSE | |
0 | |
END | |
, ORDINAL_POSITION | |
FROM INFORMATION_SCHEMA.COLUMNS AS col | |
WHERE col.TABLE_NAME = @table | |
AND col.COLUMN_NAME NOT IN (SELECT name FROM @ignore_columns) | |
ORDER BY ORDINAL_POSITION | |
INSERT @merge_columns (name, data_type) | |
VALUES ('[action]', 'nvarchar(10)') | |
INSERT @merge_columns (name, data_type) | |
SELECT name, data_type | |
FROM ( | |
SELECT QUOTENAME('old_' + name) AS name | |
, data_type | |
, position | |
FROM @base_columns | |
UNION | |
SELECT QUOTENAME('new_' + name) AS name | |
, data_type | |
, position | |
FROM @base_columns | |
) x | |
ORDER BY position, name DESC | |
-- Find the maximum column name length | |
SELECT @max_len = COALESCE(MAX(LEN(name)), 0) + 1 | |
FROM @merge_columns | |
-- Set the maximum length as a multiple of 4 for indentation | |
SET @max_len = CONVERT(int, @max_len / 4) * 4; | |
IF @max_len = 0 SET @max_len = 4; | |
-- Find the primary key(s) | |
INSERT @pk_columns | |
SELECT bc.quote_name | |
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc | |
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk ON (tc.TABLE_NAME = pk.TABLE_NAME AND tc.CONSTRAINT_NAME = pk.CONSTRAINT_NAME) | |
JOIN @base_columns bc ON (bc.name = pk.COLUMN_NAME) | |
WHERE pk.TABLE_NAME = @table | |
AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY' | |
UPDATE bc | |
SET is_pk = 1 | |
FROM @base_columns bc | |
JOIN @pk_columns pk ON (bc.name = pk.name) | |
-- Check if table has an identity | |
UPDATE bc | |
SET is_identity = 1 | |
FROM @base_columns bc | |
JOIN INFORMATION_SCHEMA.COLUMNS co ON (bc.name = co.COLUMN_NAME AND co.TABLE_NAME = @table) | |
WHERE COLUMNPROPERTY(OBJECT_ID(co.TABLE_NAME), co.COLUMN_NAME, 'IsIdentity') = 1 | |
IF (SELECT COUNT(*) FROM @base_columns WHERE is_identity = 1) > 0 | |
SET @has_identity = 1 | |
-- =================================================================== | |
-- Debugging | |
--SELECT * FROM @base_columns | |
--SELECT * FROM @merge_columns | |
-- =================================================================== | |
INSERT @lines VALUES | |
('DECLARE @script_action int = 0 -- 0=Select differences, -1=Rollback, 1=Commit') | |
, (' , @delete_target bit = 0 -- !!BE CAREFUL!!') | |
, ('') | |
, ('/* ==== To dump XML data ====') | |
, ('SELECT') | |
INSERT @lines | |
SELECT ' ' | |
+ IIF(id = 1, ' ', ', ') | |
+ QUOTENAME(name) | |
FROM @base_columns | |
ORDER BY position; | |
INSERT @lines VALUES | |
('FROM ' + QUOTENAME(@table) + ' AS ' + QUOTENAME(@table)) | |
, ('--WHERE TODO: Add conditions') | |
, ('FOR XML AUTO, ROOT(''xml'')') | |
, ('==== To dump XML data ==== */') | |
, ('') | |
, ('-- Internal variables') | |
, ('DECLARE @identity_insert bit = 0 -- IDENTITY_INSERT flag') | |
, (' , @text nvarchar(max) -- For OPENROWSET to bulk load XML file') | |
, (' , @xml xml; -- Converting text to xml') | |
, ('') | |
, ('BEGIN TRY') | |
, ('') | |
, (' SET @text = (') | |
, (' SELECT *') | |
, (' FROM OPENROWSET(') | |
, (' BULK ''C:\Data\' + @table + '.xml'', SINGLE_BLOB') | |
, (' ) AS x') | |
, (' );') | |
, (' SET @xml = CONVERT(xml, @text)') | |
, ('') | |
, (' --SELECT @text AS text, @xml AS xml') | |
, ('') | |
, (' --EXEC sp_xml_preparedocument @idoc OUTPUT, @text;') | |
, ('') | |
-- =================================================================== | |
, (' IF OBJECT_ID(''tempdb..' + @merge_table + ''') IS NOT NULL') | |
, (' DROP TABLE ' + @merge_table) | |
, ('') | |
, (' CREATE TABLE ' + @merge_table + ' (') | |
; | |
INSERT @lines | |
SELECT ' ' | |
+ CASE id | |
WHEN 1 THEN ' ' | |
ELSE ', ' | |
END | |
+ name | |
+ REPLICATE(' ', @max_len + 4 - LEN(name)) | |
+ data_type | |
-- + REPLICATE(' ', 25 - LEN(data_type)) | |
FROM @merge_columns | |
INSERT @lines VALUES | |
(' );') | |
, ('') | |
INSERT @lines VALUES | |
(' DECLARE ' + @temp_table + ' TABLE (') | |
INSERT @lines | |
SELECT ' ' | |
+ IIF(id = 1, ' ', ', ') | |
+ QUOTENAME(name) | |
+ REPLICATE(' ', @max_len + 4 - LEN(name)) | |
+ data_type | |
FROM @base_columns | |
ORDER BY position; | |
INSERT @lines VALUES | |
(' );') | |
, ('') | |
-- ================= | |
-- XML using NODES | |
INSERT @lines VALUES | |
(' INSERT ' + @temp_table + ' (') | |
INSERT @lines | |
SELECT ' ' | |
+ IIF(id = 1, ' ', ', ') | |
+ QUOTENAME(name) | |
FROM @base_columns | |
ORDER BY position; | |
INSERT @lines VALUES | |
(' )') | |
, ('/* ==== Using XML nodes() ==== */') | |
INSERT @lines | |
SELECT CONCAT( | |
' ' | |
, CASE id | |
WHEN 1 THEN 'SELECT ' | |
ELSE ' , ' | |
END | |
, CASE data_type | |
WHEN 'xml' THEN | |
'x.a.query(''' + name +'/*'')' | |
+ REPLICATE(' ', @max_len + 28 - LEN(name)) | |
ELSE | |
'x.a.value(''@' | |
+ name | |
+ '''' + REPLICATE(' ', @max_len - LEN(name)) | |
+ ', ''' | |
+ data_type | |
+ ''')' + REPLICATE(' ', 25 - LEN(data_type)) | |
END | |
, ' AS [' + name + ']' | |
) | |
FROM @base_columns | |
ORDER BY position; | |
INSERT @lines VALUES | |
(' FROM @xml.nodes(''/xml/' + @table + ''') AS x(a)') | |
, ('/* ==== */') | |
-- ================= | |
-- OPENXML | |
INSERT @lines VALUES | |
('/* ==== Using OPENXML ====') | |
, (' SELECT *') | |
, (' FROM OPENXML(@idoc, ''/xml/' + @table + ''', 1)') | |
, (' WITH ('); | |
INSERT @lines | |
SELECT ' ' | |
+ CASE id | |
WHEN 1 THEN ' ' | |
ELSE ', ' | |
END | |
+ name | |
+ REPLICATE(' ', @max_len - LEN(name)) | |
+ data_type | |
+ REPLICATE(' ', 25 - LEN(data_type)) | |
+ '''@' + name + '''' | |
FROM @base_columns; | |
INSERT @lines VALUES | |
(' )') | |
, ('==== */') | |
INSERT @lines VALUES | |
('') | |
, (' IF COALESCE(@script_action, 0) <> 0') | |
, (' BEGIN') | |
, (' BEGIN TRANSACTION ' + @tran_name + ';') | |
, (''); | |
IF @has_identity <> 0 | |
BEGIN | |
INSERT @lines VALUES | |
(' SET IDENTITY_INSERT ' + QUOTENAME(@table) + ' ON;') | |
, (' SET @identity_insert = 1;'); | |
END; | |
SELECT @max_len = COALESCE(MAX(LEN(name)), 0) + 1 | |
FROM @base_columns | |
INSERT @lines VALUES | |
('') | |
, (' MERGE ' + QUOTENAME(@table) + ' AS target') | |
, (' USING ' + @temp_table + ' AS source') | |
, (' ON (') | |
INSERT @lines | |
SELECT ' ' | |
+ CASE id | |
WHEN 1 THEN ' ' | |
ELSE ' AND ' | |
END | |
+ 'source.' + QUOTENAME(name) | |
+ ' = ' | |
+ 'target.' + QUOTENAME(name) | |
FROM @pk_columns | |
INSERT @lines VALUES | |
(' )') | |
, (' WHEN MATCHED AND (') | |
INSERT @lines | |
SELECT CONCAT( | |
' ' | |
, CASE ROW_NUMBER() OVER(ORDER BY id) | |
WHEN 1 THEN ' ' | |
ELSE ' OR ' | |
END | |
, 'NULLIF(' | |
, compare_source | |
, REPLICATE(' ', @max_len - LEN(name)) | |
, ', ' | |
, compare_target | |
, REPLICATE(' ', @max_len - LEN(name)) | |
, ') IS NOT NULL OR NULLIF(' | |
, compare_target | |
, REPLICATE(' ', @max_len - LEN(name)) | |
, ', ' | |
, compare_source | |
, REPLICATE(' ', @max_len - LEN(name)) | |
, ') IS NOT NULL' | |
) | |
FROM ( | |
SELECT b.* | |
, IIF(b.cast_source = 1, CONCAT('CAST(source.', b.quote_name, ' AS nvarchar(max))'), CONCAT('source.', b.quote_name)) AS compare_source | |
, IIF(b.cast_target = 1, CONCAT('CAST(target.', b.quote_name, ' AS nvarchar(max))'), CONCAT('target.', b.quote_name)) AS compare_target | |
FROM @base_columns b | |
) AS b | |
WHERE is_pk = 0 | |
AND name NOT IN (SELECT name FROM @ignore_columns); | |
INSERT @lines VALUES | |
(' ) THEN') | |
, (' UPDATE SET') | |
INSERT @lines | |
SELECT ' ' | |
+ CASE ROW_NUMBER() OVER(ORDER BY id) | |
WHEN 1 THEN ' ' | |
ELSE ' , ' | |
END | |
+ quote_name | |
+ REPLICATE(' ', @max_len - LEN(quote_name)) | |
+ '= source.' | |
+ quote_name | |
FROM @base_columns | |
WHERE is_pk = 0 | |
AND is_identity = 0; | |
INSERT @lines VALUES | |
(' WHEN NOT MATCHED BY TARGET THEN') | |
, (' INSERT (') | |
INSERT @lines | |
SELECT ' ' | |
+ CASE id | |
WHEN 1 THEN ' ' | |
ELSE ' , ' | |
END | |
+ quote_name | |
FROM @base_columns; | |
INSERT @lines VALUES | |
(' ) VALUES (') | |
INSERT @lines | |
SELECT ' ' | |
+ CASE id | |
WHEN 1 THEN ' ' | |
ELSE ' , ' | |
END | |
+ 'source.' | |
+ quote_name | |
FROM @base_columns | |
INSERT @lines VALUES | |
(' )') | |
, (' WHEN NOT MATCHED BY SOURCE AND @delete_target = 1 THEN') | |
, (' DELETE') | |
-- ================= | |
-- MERGE OUTPUT clause | |
INSERT @lines VALUES | |
(' OUTPUT $action') | |
INSERT @lines | |
SELECT ' ' | |
+ ', deleted.' + quote_name | |
+ REPLICATE(' ', @max_len - LEN(quote_name)) | |
+ ', inserted.' + quote_name | |
FROM @base_columns | |
INSERT @lines VALUES | |
(' INTO ' + @merge_table + ';') | |
, ('') | |
, (' SELECT * FROM ' + @merge_table + ' ' + @table + ' FOR XML AUTO, ROOT(''change_log'');') | |
, (''); | |
INSERT @lines VALUES | |
(' IF COALESCE(@script_action, 0) > 0') | |
, (' BEGIN') | |
, (' COMMIT TRANSACTION ' + @tran_name + ';') | |
, (' PRINT ''COMMIT changes to ' + @table + ''';') | |
, (' END') | |
, (' ELSE') | |
, (' BEGIN') | |
, (' ROLLBACK TRANSACTION ' + @tran_name + ';') | |
, (' PRINT ''ROLLBACK changes to ' + @table + ''';') | |
, (' END') | |
, (' END') | |
, (' ELSE') | |
, (' BEGIN') | |
, (' -- Show differences') | |
, (' SELECT *') | |
, (' FROM ' + QUOTENAME(@table) + ' AS target') | |
, (' FULL OUTER JOIN ' + @temp_table + ' AS source') | |
, (' ON (') | |
INSERT @lines | |
SELECT CONCAT( | |
' ' | |
, CASE id | |
WHEN 1 THEN ' ' | |
ELSE ' AND ' | |
END | |
, 'source.', name | |
, ' = ' | |
, 'target.', name | |
) | |
FROM @pk_columns | |
INSERT @lines VALUES | |
(' )') | |
, (' WHERE') | |
INSERT @lines | |
SELECT CONCAT( | |
' ' | |
, CASE id | |
WHEN 1 THEN ' ' | |
ELSE ' OR ' | |
END | |
, 'source.', name, ' IS NULL' | |
, ' OR target.', name, ' IS NULL' | |
) | |
FROM @pk_columns | |
INSERT @lines | |
SELECT CONCAT( | |
' OR ' | |
, 'NULLIF(' | |
, compare_source | |
, REPLICATE(' ', @max_len - LEN(name)) | |
, ', ' | |
, compare_target | |
, REPLICATE(' ', @max_len - LEN(name)) | |
, ') IS NOT NULL OR NULLIF(' | |
, compare_target | |
, REPLICATE(' ', @max_len - LEN(name)) | |
, ', ' | |
, compare_source | |
, REPLICATE(' ', @max_len - LEN(name)) | |
, ') IS NOT NULL' | |
) | |
FROM ( | |
SELECT b.* | |
, IIF(b.cast_source = 1, CONCAT('CAST(source.', b.quote_name, ' AS nvarchar(max))'), CONCAT('source.', b.quote_name)) AS compare_source | |
, IIF(b.cast_target = 1, CONCAT('CAST(target.', b.quote_name, ' AS nvarchar(max))'), CONCAT('target.', b.quote_name)) AS compare_target | |
FROM @base_columns b | |
) AS b | |
WHERE is_pk = 0 | |
AND name NOT IN (SELECT name FROM @ignore_columns); | |
INSERT @lines VALUES | |
(' END') | |
, ('') | |
, ('END TRY') | |
, ('BEGIN CATCH') | |
, ('') | |
, (' DECLARE @ERROR_NUMBER AS int = ERROR_NUMBER()') | |
, (' , @ERROR_SEVERITY AS int = ERROR_SEVERITY()') | |
, (' , @ERROR_STATE AS int = ERROR_STATE()') | |
, (' , @ERROR_LINE AS int = ERROR_LINE()') | |
, (' , @ERROR_MESSAGE AS nvarchar(max) = ERROR_MESSAGE()') | |
, (' , @ERROR_PROCEDURE AS nvarchar(256) = ISNULL(ERROR_PROCEDURE(), ''-'')') | |
, ('') | |
, (' PRINT ''ERROR '' + CONVERT(nvarchar(10), @ERROR_NUMBER) + '' - '' + @ERROR_MESSAGE;') | |
, ('') | |
, (' PRINT ''CATCH - ROLLBACK changes to ' + @table + ''';') | |
, (' ROLLBACK TRANSACTION ' + @tran_name + ';') | |
, ('') | |
, (' -- Clean up') | |
, (' --EXEC sp_xml_removedocument @idoc;') | |
, ('') | |
, (' SELECT @ERROR_NUMBER AS ERROR_NUMBER') | |
, (' , @ERROR_SEVERITY AS ERROR_SEVERITY') | |
, (' , @ERROR_STATE AS ERROR_STATE') | |
, (' , @ERROR_LINE AS ERROR_LINE') | |
, (' , @ERROR_MESSAGE AS ERROR_MESSAGE') | |
, (' , @ERROR_PROCEDURE AS ERROR_PROCEDURE') | |
, ('') | |
, ('END CATCH;') | |
, ('') | |
IF @has_identity <> 0 | |
BEGIN | |
INSERT @lines VALUES | |
('-- Turn off identity insert if it is on') | |
, ('IF @identity_insert = 1') | |
, ('BEGIN') | |
, (' SET IDENTITY_INSERT ' + QUOTENAME(@table) + ' OFF;') | |
, (' DBCC CHECKIDENT(' + QUOTENAME(@table) + ', RESEED);') | |
, ('END') | |
, (''); | |
END; | |
SELECT line FROM @lines ORDER BY id |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment