From 633647af75b3e680abe57922a8f1ec495b4150c6 Mon Sep 17 00:00:00 2001 From: drh Date: Wed, 22 Mar 2017 21:24:31 +0000 Subject: [PATCH] Initial implementation of the json_mergepatch(A,B) SQL function. FossilOrigin-Name: a267444039af519f088dd8f8ee33f686cc3071c087677075af2364ebc2587514 --- ext/misc/json1.c | 138 +++++++++++++++++++++++++++++++++++++++------- manifest | 16 ++++-- manifest.uuid | 2 +- test/json104.test | 63 +++++++++++++++++++++ 4 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 test/json104.test diff --git a/ext/misc/json1.c b/ext/misc/json1.c index 3063f9f261..0db946fdc6 100644 --- a/ext/misc/json1.c +++ b/ext/misc/json1.c @@ -138,9 +138,10 @@ static const char * const jsonType[] = { #define JNODE_RAW 0x01 /* Content is raw, not JSON encoded */ #define JNODE_ESCAPE 0x02 /* Content is text with \ escapes */ #define JNODE_REMOVE 0x04 /* Do not output */ -#define JNODE_REPLACE 0x08 /* Replace with JsonNode.iVal */ -#define JNODE_APPEND 0x10 /* More ARRAY/OBJECT entries at u.iAppend */ -#define JNODE_LABEL 0x20 /* Is a label of an object */ +#define JNODE_REPLACE 0x08 /* Replace with JsonNode.u.iReplace */ +#define JNODE_PATCH 0x10 /* Patch with JsonNode.u.pPatch */ +#define JNODE_APPEND 0x20 /* More ARRAY/OBJECT entries at u.iAppend */ +#define JNODE_LABEL 0x40 /* Is a label of an object */ /* A single node of parsed JSON @@ -148,12 +149,13 @@ static const char * const jsonType[] = { struct JsonNode { u8 eType; /* One of the JSON_ type values */ u8 jnFlags; /* JNODE flags */ - u8 iVal; /* Replacement value when JNODE_REPLACE */ u32 n; /* Bytes of content, or number of sub-nodes */ union { const char *zJContent; /* Content for INT, REAL, and STRING */ u32 iAppend; /* More terms for ARRAY and OBJECT */ u32 iKey; /* Key for ARRAY objects in json_tree() */ + u32 iReplace; /* Replacement content for JNODE_REPLACE */ + JsonNode *pPatch; /* Node chain of patch for JNODE_PATCH */ } u; }; @@ -410,6 +412,13 @@ static void jsonRenderNode( JsonString *pOut, /* Write JSON here */ sqlite3_value **aReplace /* Replacement values */ ){ + if( pNode->jnFlags & (JNODE_REPLACE|JNODE_PATCH) ){ + if( pNode->jnFlags & JNODE_REPLACE ){ + jsonAppendValue(pOut, aReplace[pNode->u.iReplace]); + return; + } + pNode = pNode->u.pPatch; + } switch( pNode->eType ){ default: { assert( pNode->eType==JSON_NULL ); @@ -441,12 +450,7 @@ static void jsonRenderNode( jsonAppendChar(pOut, '['); for(;;){ while( j<=pNode->n ){ - if( pNode[j].jnFlags & (JNODE_REMOVE|JNODE_REPLACE) ){ - if( pNode[j].jnFlags & JNODE_REPLACE ){ - jsonAppendSeparator(pOut); - jsonAppendValue(pOut, aReplace[pNode[j].iVal]); - } - }else{ + if( (pNode[j].jnFlags & JNODE_REMOVE)==0 ){ jsonAppendSeparator(pOut); jsonRenderNode(&pNode[j], pOut, aReplace); } @@ -468,11 +472,7 @@ static void jsonRenderNode( jsonAppendSeparator(pOut); jsonRenderNode(&pNode[j], pOut, aReplace); jsonAppendChar(pOut, ':'); - if( pNode[j+1].jnFlags & JNODE_REPLACE ){ - jsonAppendValue(pOut, aReplace[pNode[j+1].iVal]); - }else{ - jsonRenderNode(&pNode[j+1], pOut, aReplace); - } + jsonRenderNode(&pNode[j+1], pOut, aReplace); } j += 1 + jsonNodeSize(&pNode[j+1]); } @@ -699,7 +699,6 @@ static int jsonParseAddNode( p = &pParse->aNode[pParse->nNode]; p->eType = (u8)eType; p->jnFlags = 0; - p->iVal = 0; p->n = n; p->u.zJContent = zContent; return pParse->nNode++; @@ -1357,6 +1356,104 @@ static void jsonExtractFunc( jsonParseReset(&x); } +/* This is the RFC 7396 MergePatch algorithm. +*/ +static JsonNode *jsonMergePatch( + JsonParse *pParse, /* The JSON parser that contains the TARGET */ + int iTarget, /* Node of the TARGET in pParse */ + JsonNode *pPatch /* The PATCH */ +){ + int i, j; + int iApnd; + JsonNode *pTarget; + if( pPatch->eType!=JSON_OBJECT ){ + return pPatch; + } + assert( iTarget>=0 && iTargetnNode ); + pTarget = &pParse->aNode[iTarget]; + assert( (pPatch->jnFlags & JNODE_APPEND)==0 ); + if( pTarget->eType!=JSON_OBJECT ){ + for(i=2; in; i += jsonNodeSize(&pPatch[i])+1){ + if( pPatch[i].eType==JSON_NULL ){ + pPatch[i-1].jnFlags |= JNODE_REMOVE; + } + } + return pPatch; + } + iApnd = iTarget; + for(i=1; in; i += jsonNodeSize(&pPatch[i+1])+1){ + int nKey; + const char *zKey; + assert( pPatch[i].eType==JSON_STRING ); + assert( pPatch[i].jnFlags & JNODE_LABEL ); + nKey = pPatch[i].n; + zKey = pPatch[i].u.zJContent; + if( (pPatch[i].jnFlags & JNODE_RAW)==0 ){ + assert( nKey>=2 && zKey[0]=='"' && zKey[nKey-1]=='"' ); + nKey -= 2; + zKey ++; + } + for(j=1; jn; j += jsonNodeSize(&pTarget[j+1])+1 ){ + assert( pTarget[j].eType==JSON_STRING ); + assert( pTarget[j].jnFlags & JNODE_LABEL ); + if( jsonLabelCompare(&pTarget[j], zKey, nKey) + && (pTarget[j+1].jnFlags & (JNODE_REMOVE|JNODE_PATCH))==0 + ){ + if( pPatch[i+1].eType==JSON_NULL ){ + pTarget[j+1].jnFlags |= JNODE_REMOVE; + }else{ + JsonNode *pNew = jsonMergePatch(pParse, j+1, &pPatch[i+1]); + if( pNew==0 ) return 0; + pTarget = &pParse->aNode[iTarget]; + if( pNew!=&pTarget[j+1] ){ + pTarget[j+1].u.pPatch = pNew; + pTarget[j+1].jnFlags |= JNODE_PATCH; + } + } + break; + } + } + if( j>=pTarget->n ){ + int iStart, iPatch; + iStart = jsonParseAddNode(pParse, JSON_OBJECT, 2, 0); + jsonParseAddNode(pParse, JSON_STRING, nKey, zKey); + iPatch = jsonParseAddNode(pParse, JSON_TRUE, 0, 0); + if( pParse->oom ) return 0; + pTarget = &pParse->aNode[iTarget]; + pParse->aNode[iApnd].jnFlags |= JNODE_APPEND; + pParse->aNode[iApnd].u.iAppend = iStart; + iApnd = iStart; + pParse->aNode[iPatch].jnFlags |= JNODE_PATCH; + pParse->aNode[iPatch].u.pPatch = &pPatch[i+1]; + } + } + return pTarget; +} + +/* +** Implementation of the json_mergepatch(JSON1,JSON2) function. Return a JSON +** object that is the result of running the RFC 7396 MergePatch() algorithm +** on the two arguments. +*/ +static void jsonMergePatchFunc( + sqlite3_context *ctx, + int argc, + sqlite3_value **argv +){ + JsonParse x; /* The JSON that is being patched */ + JsonParse y; /* The patch */ + + if( jsonParse(&x, ctx, (const char*)sqlite3_value_text(argv[0])) ) return; + if( jsonParse(&y, ctx, (const char*)sqlite3_value_text(argv[1])) ){ + jsonParseReset(&x); + return; + } + jsonReturnJson(jsonMergePatch(&x, 0, y.aNode), ctx, 0); + jsonParseReset(&x); + jsonParseReset(&y); +} + + /* ** Implementation of the json_object(NAME,VALUE,...) function. Return a JSON ** object that contains all name/value given in arguments. Or if any name @@ -1460,11 +1557,11 @@ static void jsonReplaceFunc( if( x.nErr ) goto replace_err; if( pNode ){ pNode->jnFlags |= (u8)JNODE_REPLACE; - pNode->iVal = (u8)(i+1); + pNode->u.iReplace = i + 1; } } if( x.aNode[0].jnFlags & JNODE_REPLACE ){ - sqlite3_result_value(ctx, argv[x.aNode[0].iVal]); + sqlite3_result_value(ctx, argv[x.aNode[0].u.iReplace]); }else{ jsonReturnJson(x.aNode, ctx, argv); } @@ -1514,11 +1611,11 @@ static void jsonSetFunc( goto jsonSetDone; }else if( pNode && (bApnd || bIsSet) ){ pNode->jnFlags |= (u8)JNODE_REPLACE; - pNode->iVal = (u8)(i+1); + pNode->u.iReplace = i + 1; } } if( x.aNode[0].jnFlags & JNODE_REPLACE ){ - sqlite3_result_value(ctx, argv[x.aNode[0].iVal]); + sqlite3_result_value(ctx, argv[x.aNode[0].u.iReplace]); }else{ jsonReturnJson(x.aNode, ctx, argv); } @@ -2160,6 +2257,7 @@ int sqlite3Json1Init(sqlite3 *db){ { "json_array_length", 2, 0, jsonArrayLengthFunc }, { "json_extract", -1, 0, jsonExtractFunc }, { "json_insert", -1, 0, jsonSetFunc }, + { "json_mergepatch", 2, 0, jsonMergePatchFunc }, { "json_object", -1, 0, jsonObjectFunc }, { "json_quote", 1, 0, jsonQuoteFunc }, { "json_remove", -1, 0, jsonRemoveFunc }, diff --git a/manifest b/manifest index 834fc1ba0f..cceedf00d1 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C New\ssimplified\smemory\sinitialization\sfor\sMacOS. -D 2017-03-21T20:17:24.121 +C Initial\simplementation\sof\sthe\sjson_mergepatch(A,B)\sSQL\sfunction. +D 2017-03-22T21:24:31.714 F Makefile.in 1cc758ce3374a32425e4d130c2fe7b026b20de5b8843243de75f087c0a2661fb F Makefile.linux-gcc 7bc79876b875010e8c8f9502eb935ca92aa3c434 F Makefile.msc 1faf9f06aadc9284c212dea7bbc7c0dea7e8337f0287c81001eff500912c790a @@ -218,7 +218,7 @@ F ext/misc/eval.c f971962e92ebb8b0a4e6b62949463ee454d88fa2 F ext/misc/fileio.c d4171c815d6543a9edef8308aab2951413cd8d0f F ext/misc/fuzzer.c 7c64b8197bb77b7d64eff7cac7848870235d4c25 F ext/misc/ieee754.c f190d0cc5182529acb15babd177781be1ac1718c -F ext/misc/json1.c 552a7d730863419e05dc947265b28bd411680e2e +F ext/misc/json1.c ca27a98c0a7a90fcbaccd3157204e19afc7eec0ba3f4c08ed06bb638b773f523 F ext/misc/memvfs.c e5225bc22e79dde6b28380f3a068ddf600683a33 F ext/misc/nextchar.c 35c8b8baacb96d92abbb34a83a997b797075b342 F ext/misc/percentile.c 92699c8cd7d517ff610e6037e56506f8904dae2e @@ -913,6 +913,7 @@ F test/jrnlmode3.test 556b447a05be0e0963f4311e95ab1632b11c9eaa F test/json101.test c0897616f32d95431f37fd291cb78742181980ac F test/json102.test bf3fe7a706d30936a76a0f7a0375e1e8e73aff5a F test/json103.test c5f6b85e69de05f6b3195f9f9d5ce9cd179099a0 +F test/json104.test 83fd7a15eadb0cde34a37200842318d1cd98abe908e2847fb93d337d969815cc F test/keyword1.test 37ef6bba5d2ed5b07ecdd6810571de2956599dff F test/kvtest.c b9a9822dda05a1aa481215a52e2fc93cd8b22ee5 F test/lastinsert.test 42e948fd6442f07d60acbd15d33fb86473e0ef63 @@ -1567,7 +1568,10 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P ad741976c8c29bcc94f9ea9ed7deb580bb00c8a81d1a7fba1a4e03849728433d -R c272fa429602630b64ee19437bd8d503 +P 055b36f1c1593bb123f7319a07c476143d71af052b5b8d34afcd0d500f197882 +R 1d5a57aea0e07c508840a776caf26b14 +T *branch * json_mergepatch +T *sym-json_mergepatch * +T -sym-trunk * U drh -Z a3a73b80a262fb4daeb6a94d5a29c210 +Z 2961dcef6bde9e861663934a25c90d24 diff --git a/manifest.uuid b/manifest.uuid index e5a19cd1b9..980cac0acf 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -055b36f1c1593bb123f7319a07c476143d71af052b5b8d34afcd0d500f197882 \ No newline at end of file +a267444039af519f088dd8f8ee33f686cc3071c087677075af2364ebc2587514 \ No newline at end of file diff --git a/test/json104.test b/test/json104.test new file mode 100644 index 0000000000..fc2e0e38b4 --- /dev/null +++ b/test/json104.test @@ -0,0 +1,63 @@ +# 2017-03-22 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements tests for json_mergepatch(A,B) SQL function. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +ifcapable !json1 { + finish_test + return +} + +# This is the example from pages 2 and 3 of RFC-7396 +do_execsql_test json104-100 { + SELECT json_mergepatch( + json('{ + "a": "b", + "c": { + "d": "e", + "f": "g" + } + }'), + json('{ + "a":"z", + "c": { + "f": null + } + }')); +} {{{"a":"z","c":{"d":"e"}}}} + + +# This is the example from pages 4 and 5 of RFC-7396 +do_execsql_test json104-110 { + SELECT json_mergepatch( + json('{ + "title": "Goodbye!", + "author" : { + "givenName" : "John", + "familyName" : "Doe" + }, + "tags":[ "example", "sample" ], + "content": "This will be unchanged" + }'), + json('{ + "title": "Hello!", + "phoneNumber": "+01-123-456-7890", + "author": { + "familyName": null + }, + "tags": [ "example" ] + }')); +} {{{"title":"Hello!","author":{"givenName":"John"},"tags":["example"],"content":"This will be unchanged",phoneNumber:"+01-123-456-7890"}}} + +finish_test