10ecdfa2e4
Adds the SQL Server Data Tools project for the fuchs_fds database — tables, table types, functions and stored procedures that the backend calls (e.g. fds__getInvoice, fds__merge_bankingtransactions, fds__tt__bankingtransactions, fds__admin_getReportCatalog, fis_* auth). Build/model caches (bin, obj, *.dbmdl, *.jfm, *.user) are git-ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
7.2 KiB
Transact-SQL
154 lines
7.2 KiB
Transact-SQL
|
|
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; |