﻿
CREATE   FUNCTION [dbo].[json_compareObjectArrays] (@SourceJSON NVARCHAR(MAX), @TargetJSON NVARCHAR(MAX), @uniquekey varchar(50) )
/**
	Summary: 
	  This function 'diffs' a source JSON document with a target JSON document and produces an
	  analysis of which properties are missing in either the source or target, or the values
	  of these properties that are different. It reports on the properties and values for 
	  both source and target as well as the path that references that scalar value. The 
	  path reference to the object's parent is exposed in the result to enable a query to
	  reference the value of any other object in the parent that is needed. 
	Author: Phil Factor
	based on Author: Phil Factor (06/07/2020)
	Date: 06/30/2023
	
	Returns: >
	  equal:	1 = equal, 0 not equal
	  SideIndicator:  ( == equal, <- not in target, ->  not in source, <> not equal )
	  uid:
	  parent:
	  path:   the JSON path used by the SQL JSON functions 
	  key:  the key field without the path
	  SourceValue: the value IN the SOURCE JSON document
	  TargetValue: the value IN the TARGET JSON document
 
**/
RETURNS @returntable TABLE
  (
	  [equal] [bit],
	  [SideIndicator] CHAR(2),		--	== means equal, <- means not in target, -> means not in source, <> means not equal
	  [UID] varchar(100),
	  [parent]  VARCHAR(2000),	--	the parent object
	  [path] VARCHAR(2000),		--	the JSON path used by the SQL JSON functions 
	  [key] VARCHAR(255),			--	the key field without the path
	  [SourceValue] NVARCHAR(max), --	the value IN the SOURCE JSON document
	  [TargetValue] NVARCHAR(max)	--	the value IN the TARGET JSON document
  )
AS
  BEGIN
    
	IF (IsJson(ISNULL(@SourceJSON, '{}')) = 1 AND IsJson(ISNULL(@TargetJSON, '{}')) = 1) --don't try anything if either json is invalid
      BEGIN
        DECLARE @map TABLE --these contain all properties or array elements with scalar values
			  (
				  iteration INT, --the number of times that more arrays or objects were found
				  SourceOrTarget CHAR(1), --is this the source 's' OR the target 't'
 				  [UID] varchar(100),
				  [mparent] VARCHAR(2000), --the parent object
				  [mPath] VARCHAR(2000), -- the JSON path to the key/value pair or array element
				  [mOPath] NVARCHAR(2000),
				  [mKey] VARCHAR(255), --the key to the property
				  [mValue] NVARCHAR(MAX),-- the value
				  [mType] INT --the type of value it is
			  );
        DECLARE @objects TABLE --this contains all the properties with arrays and objects 
			  (
				  iteration INT,
				  SourceOrTarget CHAR(1),
				  [UID] varchar(100),
				  [oParent] VARCHAR(2000),
				  [oPath] VARCHAR(2000),
				  [oOPath] VARCHAR(2000),
				  [oKey] NVARCHAR(2000),
				  [oValue] NVARCHAR(MAX),
				  [oType] INT
			  );
        DECLARE @depth INT = 1; --we start in shallow water
        DECLARE @HowManyObjectsNext INT = 1, @SourceType INT, @TargetType INT;

        SELECT --firstly, we try to work out if the source is an array or object
          @SourceType = 
            CASE IsNumeric((SELECT TOP 1 [key] FROM OpenJson(@SourceJSON))) 
              WHEN 1 THEN 4 ELSE 5 END,
          @TargetType= --and if the target is an array or object
            CASE IsNumeric((SELECT TOP 1 [key] FROM OpenJson(@TargetJSON))) 
              WHEN 1 THEN 4 ELSE 5 END
        --now we insert the base objects or arrays into the object table      
        INSERT INTO @objects (iteration, SourceOrTarget, [oParent], [oPath], [oOPath], [oKey], [oValue], [oType])
			SELECT 0, 's' AS SourceOrTarget,'' AS [oParent], [oPath] = '$', [oOPath] = '$', [oKey] = '', @SourceJSON, @SourceType;
        INSERT INTO @objects (iteration, SourceOrTarget, [oParent], [oPath], [oOPath], [oKey], [oValue], [oType])
			SELECT 0, 't' AS SourceOrTarget, '' AS [oParent], [oPath] = '$', [oOPath] = '$', [oKey] = '', @TargetJSON, @TargetType;

        --we now set the depth and how many objects are in the next iteration
        SELECT @depth = 0, @HowManyObjectsNext = 2; 

        WHILE @HowManyObjectsNext > 0 AND @depth < 2
          BEGIN
            INSERT INTO @map --get the scalar values into the @map table
					(iteration, SourceOrTarget, [UID], [mParent], [mPath], [mOPath], [mKey], [mValue], [mType])
              SELECT -- 
					  [iteration] = o.[iteration] + 1
					, SourceOrTarget
					, [UID]		= [UID]
					, [mParent]		= [oPath]
					, [mPath]		= [oPath] + CASE [otype] WHEN 4 THEN '[' + [Key] + ']' ELSE '.' + [key] END
					, [mOPath]		= [oOPath] + CASE [otype] WHEN 4 THEN '[' + [Key] + ']' ELSE '.' + [key] END
					, [mkey]		= [key]
					, [mvalue]		= [value]
					, [mtype]		= [type]
                FROM @objects AS o
                  CROSS APPLY OpenJson([oValue]) as j
                WHERE j.[type] IN (0, 1, 2, 3) AND o.[iteration] = @depth;

			--now we do the same for the objects and arrays
            INSERT INTO @objects (iteration, SourceOrTarget, [UID], [oParent], [oPath], [oOPath], [oKey], [oValue], [oType])
              SELECT 
					  [iteration]		= o.[iteration] + 1
					, [SourceOrTarget]	= SourceOrTarget
					, [UID]				= JSON_VALUE(j.[Value], '$.' + @uniquekey)
					, [oParent]		= [oPath]
					, [oPath]		= [oPath] + CASE [oType] WHEN 4 THEN '[' + JSON_VALUE(j.[Value], '$.' + @uniquekey) + ']' ELSE '.' + [key] END
					, [oOPath]		= [oOPath] + CASE [oType] WHEN 4 THEN '[' + j.[key] + ']' ELSE '.' + j.[key] END
					, [oKey]		= [key]
					, [oValue]		= [value]
					, [oType]		= [type]
              FROM @objects o 
				CROSS APPLY OpenJson([oValue]) as j
			  WHERE j.[type] IN (4,5) AND o.[iteration] = @depth;

           SELECT @HowManyObjectsNext = @@RowCount --how many objects or arrays?
           SELECT @depth = @depth + 1; --and so to the next depth maybe

         END;  --while

		--now we just do a full join on the columns we are comparing and work out the comparison
         INSERT INTO @returntable
          SELECT 
		   --first we work out the side-indicator that summarises the comparison
				[equal]				= CASE WHEN src.[UID] is null		OR tgt.[UID] is null		THEN 0
										   WHEN src.[mValue] is NULL	AND tgt.[mValue] is null		THEN 1 
										   WHEN src.[mValue] = tgt.[mValue]						THEN 1
										   ELSE 0 
									  END
				, [Sideindicator]	= CASE WHEN src.[UID] is null		AND tgt.[UID] IS NOT null	THEN '->'
										   WHEN src.[UID] is NOT null	AND tgt.[UID] IS null		THEN '<-'
											WHEN src.[mValue] is NULL	AND tgt.[mValue] is null		THEN '==' 
											WHEN src.[mValue] = tgt.[mValue]							THEN '=='
											ELSE IIF(src.[mPath] IS NULL, '-', '<') + IIF(tgt.[mPath] IS NULL, '-', '>')
									  END
				--these columns could be in either table
				, [UID]				= Coalesce(src.[UID], tgt.[UID])
				, [parent]			= Coalesce(src.[mParent], tgt.[mParent])
				, [path]			= Coalesce(src.[mOPath], tgt.[mOPath])
				, [key]				= Coalesce(src.[mKey], tgt.[mKey])
				, [sourceValue]		= src.[mValue]
				, [targetValue]		= tgt.[mValue]
            FROM 
               (SELECT [UID], [mParent], [mPath], [mOPath], [mKey], [mValue] FROM @map WHERE SourceOrTarget = 's') AS src -- the source scalar literals
              FULL OUTER JOIN 
               (SELECT [UID], [mParent], [mPath], [mOPath], [mKey], [mValue] FROM @map WHERE SourceOrTarget = 't') AS tgt --the target scalar literals
                ON src.[mPath] = tgt.[mPath]
            ORDER BY [path];
      END;
    RETURN;
  END;