From: drh <> Date: Mon, 18 May 2026 17:55:40 +0000 (+0000) Subject: Fix the window-function variant of the json_group_object() function so X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=de94ec87fc1baa796d6f515f66fe2ab9ffa81b24;p=thirdparty%2Fsqlite.git Fix the window-function variant of the json_group_object() function so that it correctly handles NULL entries. [bugs:/forumpost/0de87b23b3|Bug report 0de87b23b3]. FossilOrigin-Name: ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c --- diff --git a/manifest b/manifest index 3777f17bb7..7a6119e7f2 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Do\snot\sattempt\sthe\sOR-optimization\son\sconjuncts\sthat\scontain\sCOLLATE\noperators\sbecause\s(1)\ssuch\scases\sare\srare\sand\s(2)\sthey\sare\stricky\sto\nget\sright\sand\sare\sthus\sprone\sto\sbugs\sand\shard\sto\stest.\n[bugs:/forumpost/329521b269|Bugs\sreport\s329521b269]\sis\sone\nsuch\sbug\sthat\sgoes\sback\sabout\s20\syears\sand\sthus\sdemonstrates\sboth\npoints\sof\sthe\sprevious\ssentence. -D 2026-05-18T14:28:53.777 +C Fix\sthe\swindow-function\svariant\sof\sthe\sjson_group_object()\sfunction\sso\nthat\sit\scorrectly\shandles\sNULL\sentries.\n[bugs:/forumpost/0de87b23b3|Bug\sreport\s0de87b23b3]. +D 2026-05-18T17:55:40.195 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -697,7 +697,7 @@ F src/hash.h 46b92795a95bfefb210f52f0c316e9d7cdbcdd7e7fcfb0d8be796d3a5767cddf F src/hwtime.h 21c2cf1f736e7b97502c3674d0c386db3f06870d6f10d0cf8174e2a4b8cb726e F src/in-operator.md 10cd8f4bcd225a32518407c2fb2484089112fd71 F src/insert.c 66cb27a8cb3509ca4fa9204d8beba31baaf23a40dca2e23156393bce09655417 -F src/json.c ed93368fab7943a4822bc179fd914e63f5a2a18d6ef429c16ac49ea13eaffd49 +F src/json.c fadf5f0a00c1af99dbc6ac78dd3c2064c40bb28e602a5746f7c66c1ec8cbb006 F src/legacy.c d7874bc885906868cd51e6c2156698f2754f02d9eee1bae2d687323c3ca8e5aa F src/loadext.c 78d5b06f18996ffa1203129b28fea043f63a87a4117539678f1d761c30b4ff65 F src/main.c 6180079f53ccdd784df2eddc3751f49ea7153c5959bee792b19ad9f4bdbcf437 @@ -2080,7 +2080,7 @@ F test/window8.tcl c57364e64d816f6e26df60437e1202e2c1031c7b818a1a67535d1006862a0 F test/window8.test 3d931e58802b8ab8063da00f0cf30aa3351640238a952c0efb5a129e2349a4bb F test/window9.test 7b98a7916dd87763ea35f56ea023e3b29e99744582204ccf2937a3bac411cd4d F test/windowA.test 6d63dc1260daa17141a55007600581778523a8b420629f1282d2acfc36af23be -F test/windowB.test aad7c31739999f68a98a813cfd78390918fc70f56d2d925317a1523cab548ecf +F test/windowB.test bba3bee77c1d3321077ff8951d543161d74f08f78633d84433ced7d6261dc2c2 F test/windowC.test 6fd75f5bb2f1343d34e470e36e68f0ff638d8a42f6aa7d99471261b31a0d42f2 F test/windowD.test 65cf5a765fb8072450e8a0de2979ce7f09a38d87724fe1280c6444073e3da49b F test/windowE.test a82a4213c7b923220de5e457cb76c537a5d10d5e94641e3055ba21b79870a7fb @@ -2205,8 +2205,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P e076ac7b648988b48b546e19b5ef2e50061b44c85bc2645362409e295e3b4b11 -R ea458682148485135a388a8cd079072c +P 622882529558b4779dfb7246bd5a9de776555c8f940bb941397fb56fb9f97e43 +R d24f0da7d2c47dc861907e18b0a3ec5b U drh -Z 57973d6ccb3dee1f784470469cc665de +Z d5e5c346537f43415ecc1cfb3b15b0e6 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 7e867d6575..9cf5dd0088 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -622882529558b4779dfb7246bd5a9de776555c8f940bb941397fb56fb9f97e43 +ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c diff --git a/src/json.c b/src/json.c index 8bd6671917..09c77308bb 100644 --- a/src/json.c +++ b/src/json.c @@ -4909,11 +4909,9 @@ static void jsonGroupInverse( UNUSED_PARAMETER(argc); UNUSED_PARAMETER(argv); pStr = (JsonString*)sqlite3_aggregate_context(ctx, 0); -#ifdef NEVER /* pStr is always non-NULL since jsonArrayStep() or jsonObjectStep() will ** always have been called to initialize it */ if( NEVER(!pStr) ) return; -#endif z = pStr->zBuf; for(i=1; inUsed && ((c = z[i])!=',' || inStr || nNest); i++){ if( c=='"' ){ @@ -4942,6 +4940,13 @@ static void jsonGroupInverse( ** json_group_obj(NAME,VALUE) ** ** Return a JSON object composed of all names and values in the aggregate. +** +** Rows for which NAME is NULL do not result in a new entry. However, we +** do initially insert a "@" entry into the growing string for each null entry +** and change the first character of the string to "@" to signal that the +** string contains null entries. The "@" markers are needed in order to +** correctly process xInverse() requests. The initial "@" is converted +** back into "{" and the "@" null values are removed by jsonObjectCompute(). */ static void jsonObjectStep( sqlite3_context *ctx, @@ -4959,7 +4964,7 @@ static void jsonObjectStep( if( pStr->zBuf==0 ){ jsonStringInit(pStr, ctx); jsonAppendChar(pStr, '{'); - }else if( pStr->nUsed>1 && z!=0 ){ + }else if( pStr->nUsed>1 ){ jsonAppendChar(pStr, ','); } pStr->pCtx = ctx; @@ -4967,6 +4972,9 @@ static void jsonObjectStep( jsonAppendString(pStr, z, n); jsonAppendChar(pStr, ':'); jsonAppendSqlValue(pStr, argv[1]); + }else{ + pStr->zBuf[0] = '@'; + jsonAppendRawNZ(pStr, "@", 1); } } } @@ -4975,20 +4983,64 @@ static void jsonObjectCompute(sqlite3_context *ctx, int isFinal){ int flags = SQLITE_PTR_TO_INT(sqlite3_user_data(ctx)); pStr = (JsonString*)sqlite3_aggregate_context(ctx, 0); if( pStr ){ - jsonAppendRawNZ(pStr, "}", 2); - jsonStringTrimOneChar(pStr); + JsonString *pOgStr = pStr; + JsonString tmpStr; + jsonAppendRawNZ(pOgStr, "}", 2); /* Ensure it is zero-terminated */ + jsonStringTrimOneChar(pOgStr); /* Remove the zero terminator */ pStr->pCtx = ctx; if( pStr->eErr ){ jsonReturnString(pStr, 0, 0); return; - }else if( flags & JSON_BLOB ){ + } + if( pStr->zBuf[0]!='{' ){ + /* The string contains null entries that need to be removed */ + u64 i, j; + int inStr = 0; + if( !isFinal ){ + /* Work with a temporary copy of the string if this is not the + ** final result */ + jsonStringInit(&tmpStr, ctx); + jsonAppendRawNZ(&tmpStr, pStr->zBuf, pStr->nUsed+1); + pStr = &tmpStr; + if( pStr->eErr ){ + jsonReturnString(pStr, 0, 0); + return; + } + jsonStringTrimOneChar(pStr); /* Remove zero terminator */ + } + /* Fix up the string by changing the initial "@" flag back to + ** to "{" and removing all subsequence "@" entries, with their + ** associated comma delimeters. */ + pStr->zBuf[0] = '{'; + for(i=j=1; inUsed; i++){ + char c = pStr->zBuf[i]; + if( c=='"' ){ + inStr = !inStr; + pStr->zBuf[j++] = '"'; + }else if( c=='\\' ){ + pStr->zBuf[j++] = '\\'; + pStr->zBuf[j++] = pStr->zBuf[++i]; + }else if( c=='@' && !inStr ){ + assert( i+1nUsed ); + if( pStr->zBuf[i+1]==',' ){ + i++; + }else if( pStr->zBuf[j-1]==',' ){ + j--; + } + }else{ + pStr->zBuf[j++] = c; + } + } + pStr->zBuf[j] = 0; /* Restore zero terminator */ + pStr->nUsed = j; /* Truncate the string */ + } + if( flags & JSON_BLOB ){ jsonReturnStringAsBlob(pStr); if( isFinal ){ if( !pStr->bStatic ) sqlite3RCStrUnref(pStr->zBuf); }else{ - jsonStringTrimOneChar(pStr); + jsonStringTrimOneChar(pOgStr); } - return; }else if( isFinal ){ sqlite3_result_text(ctx, pStr->zBuf, (int)pStr->nUsed, pStr->bStatic ? SQLITE_TRANSIENT : @@ -4996,8 +5048,9 @@ static void jsonObjectCompute(sqlite3_context *ctx, int isFinal){ pStr->bStatic = 1; }else{ sqlite3_result_text(ctx, pStr->zBuf, (int)pStr->nUsed, SQLITE_TRANSIENT); - jsonStringTrimOneChar(pStr); + jsonStringTrimOneChar(pOgStr); } + if( pStr!=pOgStr ) jsonStringReset(pStr); }else if( flags & JSON_BLOB ){ static const unsigned char emptyObject = 0x0c; sqlite3_result_blob(ctx, &emptyObject, 1, SQLITE_STATIC); diff --git a/test/windowB.test b/test/windowB.test index 32193a378a..dfd5948a56 100644 --- a/test/windowB.test +++ b/test/windowB.test @@ -201,6 +201,128 @@ ifcapable json1 { {{"a":{"a":1,"e":9}}} {{"c":{"c":3,"e":9}}} } + + #----------------------------------------------------------------------- + # Bug report 2026-05-18T12:34:42Z + # + # The xInverse() method used by json_group_object() should correctly + # account for NULL entries. + # + do_execsql_test 3.8 { + CREATE TABLE t1(id INT, k TEXT, v INT); + INSERT INTO t1 VALUES + (1, 'a', 1), + (2, 'b', 2), + (3, 'c', 3), + (4, 'd', 4), + (5, 'f', 5), + (6, 'g', 6), + (7, 'h', 7); + } + do_execsql_test 3.9 { + SELECT id, json_group_object(if(id<>1,k),v) OVER + (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jx + FROM t1; + } { + 1 {{"b":2}} + 2 {{"b":2,"c":3}} + 3 {{"b":2,"c":3,"d":4}} + 4 {{"c":3,"d":4,"f":5}} + 5 {{"d":4,"f":5,"g":6}} + 6 {{"f":5,"g":6,"h":7}} + 7 {{"g":6,"h":7}} + } + do_execsql_test 3.10 { + SELECT id, json_group_object(if(id>4,k),v) OVER + (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jx + FROM t1; + } { + 1 {{}} + 2 {{}} + 3 {{}} + 4 {{"f":5}} + 5 {{"f":5,"g":6}} + 6 {{"f":5,"g":6,"h":7}} + 7 {{"g":6,"h":7}} + } + do_execsql_test 3.11 { + SELECT id, json_group_object(if(id>4,k||'@'),v) OVER + (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jx + FROM t1; + } { + 1 {{}} + 2 {{}} + 3 {{}} + 4 {{"f@":5}} + 5 {{"f@":5,"g@":6}} + 6 {{"f@":5,"g@":6,"h@":7}} + 7 {{"g@":6,"h@":7}} + } + do_execsql_test 3.12 { + SELECT id, json_group_object(k,v) OVER + (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jx + FROM t1; + } { + 1 {{"a":1,"b":2}} + 2 {{"a":1,"b":2,"c":3}} + 3 {{"b":2,"c":3,"d":4}} + 4 {{"c":3,"d":4,"f":5}} + 5 {{"d":4,"f":5,"g":6}} + 6 {{"f":5,"g":6,"h":7}} + 7 {{"g":6,"h":7}} + } + do_execsql_test 3.13 { + SELECT id, json_group_object(if(id>1 AND id<7,k),v) OVER + (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jx + FROM t1; + } { + 1 {{"b":2}} + 2 {{"b":2,"c":3}} + 3 {{"b":2,"c":3,"d":4}} + 4 {{"c":3,"d":4,"f":5}} + 5 {{"d":4,"f":5,"g":6}} + 6 {{"f":5,"g":6}} + 7 {{"g":6}} + } + do_execsql_test 3.14 { + SELECT id, json_group_object(if(id>2 AND id<6,k),v) OVER + (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jx + FROM t1; + } { + 1 {{}} + 2 {{"c":3}} + 3 {{"c":3,"d":4}} + 4 {{"c":3,"d":4,"f":5}} + 5 {{"d":4,"f":5}} + 6 {{"f":5}} + 7 {{}} + } + do_execsql_test 3.15 { + SELECT id, json_group_object(if(id<2 OR id>6,k),v) OVER + (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jx + FROM t1; + } { + 1 {{"a":1}} + 2 {{"a":1}} + 3 {{}} + 4 {{}} + 5 {{}} + 6 {{"h":7}} + 7 {{"h":7}} + } + do_execsql_test 3.16 { + SELECT id, json_group_object(if(id<3 OR id>5,k),v) OVER + (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jx + FROM t1; + } { + 1 {{"a":1,"b":2}} + 2 {{"a":1,"b":2}} + 3 {{"b":2}} + 4 {{}} + 5 {{"g":6}} + 6 {{"g":6,"h":7}} + 7 {{"g":6,"h":7}} + } } #------------------------------------------------------------------------- @@ -483,4 +605,3 @@ if {[permutation]!="no_optimization"} { }} finish_test -