]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Fix the window-function variant of the json_group_object() function so
authordrh <>
Mon, 18 May 2026 17:55:40 +0000 (17:55 +0000)
committerdrh <>
Mon, 18 May 2026 17:55:40 +0000 (17:55 +0000)
that it correctly handles NULL entries.
[bugs:/forumpost/0de87b23b3|Bug report 0de87b23b3].

FossilOrigin-Name: ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c

manifest
manifest.uuid
src/json.c
test/windowB.test

index 3777f17bb75c8c09f9837d1535af682e8e11a133..7a6119e7f23fc1ae7bfaaf3270e212343b9fdf11 100644 (file)
--- 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.
index 7e867d6575053bebcf59f236f26732420c941d94..9cf5dd0088a7294664cf80422d4da1173881cbd0 100644 (file)
@@ -1 +1 @@
-622882529558b4779dfb7246bd5a9de776555c8f940bb941397fb56fb9f97e43
+ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c
index 8bd667191764932bb6d31c865559f8adf0bd0220..09c77308bb83f14e3bc191d38e2f43f89e4d4d1d 100644 (file)
@@ -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; i<pStr->nUsed && ((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; i<pStr->nUsed; 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+1<pStr->nUsed );
+          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); 
index 32193a378a6541b51064056c951885a2969e8bbc..dfd5948a56da26329f1f132edfeb153eace69a0f 100644 (file)
@@ -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
-