From 942682bde1876ceca95248c2bc8b1aa629286ef3 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Tue, 13 Oct 2026 23:13:26 -0400 Subject: [PATCH v2 1/3] Fix jsonb_object_agg() used as a window function. uniqueifyJsonbObject() did not renumber the pairs it kept, so a pair added later could lose to an older one with the same key. Renumber them. The unique strict variant keeps null pairs for the duplicate check, so its final function now works on a copy. --- src/backend/utils/adt/jsonb.c | 24 +++++++++++++++++++++--- src/backend/utils/adt/jsonb_util.c | 6 ++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index da0061ba1b8..804bdbc93ce 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -1725,12 +1725,30 @@ jsonb_object_agg_finalfn(PG_FUNCTION_ARGS) * itself, so we can simply invoke pushJsonbValue on a local copy of that. * Note that this will run uniqueifyJsonbObject each time; that's hard to * avoid, since duplicate pairs may have been added since the previous - * finalization. We assume uniqueifyJsonbObject can be applied repeatedly - * (with the same unique_keys/skip_nulls options) without damaging the - * data structure. + * finalization. uniqueifyJsonbObject can be applied repeatedly (with the + * same unique_keys/skip_nulls options) without damaging the data + * structure, except that it removes null values. That is harmless unless + * unique_keys is also set, since nulls are skipped before duplicates are + * looked for. But the unique check must still see them, so in that case + * run it on a copy of the pairs. */ result = arg->pstate; + if (result.parseState->unique_keys && result.parseState->skip_nulls) + { + JsonbParseState *ps = result.parseState; + int npairs = ps->contVal.val.object.nPairs; + + Assert(ps->next == NULL); + result.parseState = palloc_object(JsonbParseState); + *result.parseState = *ps; + result.parseState->contVal.val.object.pairs = + palloc_array(JsonbPair, npairs); + memcpy(result.parseState->contVal.val.object.pairs, + ps->contVal.val.object.pairs, + npairs * sizeof(JsonbPair)); + } + pushJsonbValue(&result, WJB_END_OBJECT, NULL); /* We expect result.parseState == NULL after closing the object */ diff --git a/src/backend/utils/adt/jsonb_util.c b/src/backend/utils/adt/jsonb_util.c index 509082cfb94..83959dcc197 100644 --- a/src/backend/utils/adt/jsonb_util.c +++ b/src/backend/utils/adt/jsonb_util.c @@ -2103,6 +2103,12 @@ uniqueifyJsonbObject(JsonbValue *object, bool unique_keys, bool skip_nulls) /* Emit this pair, but avoid no-op copy */ if (i > nNewPairs) pairs[nNewPairs] = *ptr; + + /* + * Renumber so the orders stay below nPairs. Pairs appended later + * must still win if we are called again. + */ + pairs[nNewPairs].order = nNewPairs; nNewPairs++; } object->val.object.nPairs = nNewPairs; -- 2.37.1 (Apple Git-137.1)