]> 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:58:30 +0000 (17:58 +0000)
committerdrh <>
Mon, 18 May 2026 17:58:30 +0000 (17:58 +0000)
that it correctly handles NULL entries.

FossilOrigin-Name: d29d9512a72972778c374524a3cd5d52a1377bf7fb7ff34028135146d75909b8

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

index ce4e423897b926b163fdfee2ce1a15a11c996471..59f3bc2ba47184757f9d7aa170bd3c19d9d7f5f1 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Do\snot\sattempt\sthe\sOR-optimization\son\sconjuncts\sthat\scontain\sCOLLATE\noperators.
-D 2026-05-18T14:32:45.867
+C Fix\sthe\swindow-function\svariant\sof\sthe\sjson_group_object()\sfunction\sso\nthat\sit\scorrectly\shandles\sNULL\sentries.
+D 2026-05-18T17:58:30.395
 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -696,7 +696,7 @@ F src/hash.h 46b92795a95bfefb210f52f0c316e9d7cdbcdd7e7fcfb0d8be796d3a5767cddf
 F src/hwtime.h 21c2cf1f736e7b97502c3674d0c386db3f06870d6f10d0cf8174e2a4b8cb726e
 F src/in-operator.md 10cd8f4bcd225a32518407c2fb2484089112fd71
 F src/insert.c dfd311b0ac2d4f6359e62013db67799757f4d2cc56cca5c10f4888acfbbfa3fd
-F src/json.c 5027b856cd9b621dc9ba66b211e21a440ccdc63cefdefb44c51e7d3ac550d1a4
+F src/json.c 047c4cec4d688f6aaca609c3cfb2403a4cf00fefab8b150a22362a2439c2caa8
 F src/legacy.c d7874bc885906868cd51e6c2156698f2754f02d9eee1bae2d687323c3ca8e5aa
 F src/loadext.c 56a542244fbefc739a2ef57fac007c16b2aefdb4377f584e9547db2ce3e071f9
 F src/main.c 387bb9d0216d6d35b221481ba8e661d94ad043060cd89581b6422c269ce680a0
@@ -2074,7 +2074,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
@@ -2198,9 +2198,9 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee
 F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2
 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
 F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c
-P 49c9b80e52642b94f1fa0b50ecd33a96b98a8c28d1175ac2c490d22e8c1433ce
-Q +622882529558b4779dfb7246bd5a9de776555c8f940bb941397fb56fb9f97e43
-R 571fe2c2636da511705d04f20fc45d71
+P 4c105d79010939bdb8cb0461c3926f80815d4f0213bc0206be76015218cb8125
+Q +ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c
+R cc0d8b204df572249cae1981c2082bd2
 U drh
-Z 7ce5555606c0c1397363a914ce08df66
+Z 25b56ebe2a290b89b29a55da76ed8b59
 # Remove this line to create a well-formed Fossil manifest.
index ef0b7a2772337457385787cdda34a5350f694fac..ed3892c1054042bc7af117fc82517f6bb274fc4c 100644 (file)
@@ -1 +1 @@
-4c105d79010939bdb8cb0461c3926f80815d4f0213bc0206be76015218cb8125
+d29d9512a72972778c374524a3cd5d52a1377bf7fb7ff34028135146d75909b8
index f3d5aadcf7d6a410c2f1a09ba65526df7187e67a..cf8b4c915767c31eaf321e83a42cbba1eb0f8929 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
-