-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
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
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
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.
-622882529558b4779dfb7246bd5a9de776555c8f940bb941397fb56fb9f97e43
+ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c
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=='"' ){
** 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,
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;
jsonAppendString(pStr, z, n);
jsonAppendChar(pStr, ':');
jsonAppendSqlValue(pStr, argv[1]);
+ }else{
+ pStr->zBuf[0] = '@';
+ jsonAppendRawNZ(pStr, "@", 1);
}
}
}
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 :
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);
{{"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}}
+ }
}
#-------------------------------------------------------------------------
}}
finish_test
-