Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wgoOr-006Q1u-0A for pgsql-hackers@arkaria.postgresql.org; Mon, 06 Jul 2026 18:50:37 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wgoOo-002XHW-3C for pgsql-hackers@arkaria.postgresql.org; Mon, 06 Jul 2026 18:50:35 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wgoOo-002XHJ-16 for pgsql-hackers@lists.postgresql.org; Mon, 06 Jul 2026 18:50:34 +0000 Received: from mail-lj1-x22a.google.com ([2a00:1450:4864:20::22a]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wgoOl-00000001tMf-0EqZ for pgsql-hackers@lists.postgresql.org; Mon, 06 Jul 2026 18:50:33 +0000 Received: by mail-lj1-x22a.google.com with SMTP id 38308e7fff4ca-39b349ae174so29637011fa.1 for ; Mon, 06 Jul 2026 11:50:30 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1783363829; x=1783968629; darn=lists.postgresql.org; h=content-type:mime-version:message-id:subject:cc:to:from:date:from :to:cc:subject:date:message-id:reply-to:content-type; bh=3OmFLBleK+EX5ZC+SAskSOhgvjPFq5VJuhPRBgY2B14=; b=BOAIreBDICYRQZjhibyvJ30q9wAFlfEIhROehNK7hGUvevZrvR1P7gXaQmNPcW3ikP Db4IUDqw3KJdluqneTPMNY740xr2OMMDJk+mSszwRuj8rA0IxqUlquhIdJIq4pV6a71/ IUgzdYTq0NZiYf/yRbfTterswuEcbpWuMwuA0szSWZ3qh/xCitSJap2Esz+72utTjWx0 BtXl8PWaKRcgZ6Ei0XmVedpCvhkVx2weWvEynt3mV609GFxaHWAKVLe3J0qD0IQ3ppmM CzOMb6itRnBxacL8J/WbkGZbAhCaa1mtdOKlnDVPU2ExF1GQxBH/5F6i2g3UxPMGxfPu 3/+w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1783363829; x=1783968629; h=content-type:mime-version:message-id:subject:cc:to:from:date :x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to:content-type; bh=3OmFLBleK+EX5ZC+SAskSOhgvjPFq5VJuhPRBgY2B14=; b=Sgum1Al8xjyyXdg8/TTVQIxy+jLFu5OwnlxrsmDE2gkKm1po0FH6RikSP9Bw+Hh8Gb wvdP1tzHK2QWJB2JlfG9kV9/UEJ2XwNP9esI71JWtIPHrS1dL2CZnnsmurwmuGowKAuf hAzU5o6l7feL5j36CW1QlKh4U3KkSPQeMnfwrHm6iY+ETyTVvYR0HllZ+A2xsvMu2ND8 Z0mrv+KiXIoQ4uSGuffGjDkjBGxy6MCkMRi0OCxIpJsZeQ/m+slMOWNt4egjtGgXS66G z23PLfhJNRshwJA1cclQ+aJer9DlhkxgDcv9QC6E0U0FJNqE4CFMhv0r0lrl30hzH+y2 d6Dg== X-Gm-Message-State: AOJu0Yy9ewLLALYvXXrgGyfk7gl8PDSuJlqnZlf4KmxdJgd44kkXZIeM IgOVKCor28CXpd7+zX6eQt97qwk2iGr4nIF8Aj04VMHweNfG/aaP78B2hQ2fUB+nMWqELw== X-Gm-Gg: AfdE7cmpKLR8oIRveb3ZxFMpkEneVRX5dv9nXGMB18XdWBUIF1JuRbrkTWdYBxguruf qGFcN6iPs7VL/mPeNtO5XVmgyOzQKiKEKt3gO/pG4cZ6iU19deMOEjrsM+u/Zpg1aZ4v1FbCusJ 3I3OekBhAr3drd5iLxKxYZAwhvZrXaAwCIZKdhhh9pE90Dx6y9Sy0LLiAqwedc+qjEGWJSq458+ HFcLzxFyo1LlvTlbMq5nYhsD9fiVI5MQQobMJBpREUC5TWftE+XgKNnQK8osGnIVg1y72hwwJHV 4tBTJ14gY/gqdJ0U/N/BUShDrL/XnGsJogK2Q0thtBPjEiW9JyzibaWVOJ0qToj1gcG7HnqmMqQ OnHDjH0KJGkx1bWPm94kZrKcWbWbfQJIEmR8EGdWkZgSfiysFLCqIaPkCf+YX8ZuOpW7/JemC+v Ki/E/o1dkVoKOPd9jI2SsGafw= X-Received: by 2002:a2e:881a:0:b0:39a:fffc:ccac with SMTP id 38308e7fff4ca-39c60032d9emr3325461fa.35.1783363828972; Mon, 06 Jul 2026 11:50:28 -0700 (PDT) Received: from pg-ThinkPad-T14-Gen-4 ([93.174.132.69]) by smtp.gmail.com with ESMTPSA id 38308e7fff4ca-39b4ae3802dsm21538841fa.30.2026.07.06.11.50.25 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 06 Jul 2026 11:50:27 -0700 (PDT) Date: Mon, 6 Jul 2026 23:50:21 +0500 From: Andrey Rachitskiy To: PostgreSQL Hackers Cc: Nikolay Shaplov , Amit Langote , Andrey Borodin Subject: [PATCH] Collapse consecutive .** accessors for jsonpath exists queries Message-ID: <20260706235021.362cab3c@pg-ThinkPad-T14-Gen-4> X-Mailer: Claws Mail 4.3.1 (GTK 3.24.52; x86_64-pc-linux-gnu) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="MP_/BlgiDE/t55y8Vb7V2uvNybc" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --MP_/BlgiDE/t55y8Vb7V2uvNybc Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Content-Disposition: inline Hi, Hackers! When a jsonpath expression contains multiple consecutive .** (jpiAny) accessors, each one triggers a full subtree traversal in executeAnyItem(). A chain of k such accessors can cost O(N^k) on a document with N nodes, even though for existence semantics it is equivalent to a single .** with merged level bounds. Background ---------- We originally reported this as BUG #19362 [1], with a follow-up analysis [2]. The original report used a fuzzer-generated sql statement; the follow-up gave a clearer reproduction. For example, on a JSON array nested 1000 levels deep: SELECT data @? '$.**.**.**.**' FROM test_json; -- ~0.5 ms SELECT data @? '$.**.**.**.**.*' FROM test_json; -- >23 min (cancelled) Andrey Borodin replied that this looks like a performance optimization opportunity rather than a bug, and asked for a more realistic example of what a user might want but not get in reasonable time [3]. We agree, and are sending this to pgsql-hackers. A developer searching semi-structured JSON (nested categories, comment threads, task lists) might use @? / jsonb_path_exists with paths such as $.**.b ? (@ > 0) =E2=80=94 "does key b exist anywhere, with this predicate?" Templates, copy-paste, or auto-generated paths can accidentally accumulate redundant .** segments (.**.**.b), which is semantically the same as $.**.b for existence checks but much slower. If an application accepts user-supplied jsonpath for @?, a path with many consecutive .** operators also becomes an easy DoS vector: no special privileges beyond the ability to run a query. Proposal -------- Collapse consecutive jpiAny nodes at execution time for existence queries (@? and jsonb_path_exists): merge level bounds and perform a single executeAnyItem() pass (.** {a,b} .** {c,d} -> .** {a+c,b+d}). For existence checks this bounds the cost of a k-deep .** chain to O(N) instead of O(N^k), so paths that previously hung the backend for many minutes on the BUG #19362 reproduction now finish in under a millisecond. That closes a practical DoS vector for applications that pass user-supplied jsonpath to @?. This patch intentionally does NOT change jsonb_path_query or @@: those functions collect item sequences, and consecutive .** affects result multiplicity (e.g. lax $.**.** vs $.**). The optimization is gated on existsOnly (result =3D=3D NULL in executeJsonPath()). jsonb_jsonpath regress tests are included. Backpatch-through: 15. Separate patches for REL_15_STABLE through master are attached. [1] https://postgr.es/m/19362-9e824863c543cafa@postgresql.org [2] https://postgr.es/m/1766516577.178080141@f533.i.mail.ru [3] https://postgr.es/m/DD2A3250-D456-4871-A245-9E851BC59B66@yandex-team.ru Author: Andrey Rachitskiy Reported-by: Nikolay Shaplov Tested-on: REL_15_STABLE .. master --=20 Regards, Andrey Rachitskiy --MP_/BlgiDE/t55y8Vb7V2uvNybc Content-Type: text/x-patch Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename=0001-collapse-consecutive-jsonpath-any.patch From f04781df5daf112840d08eab39fb95505fda6c9c Mon Sep 17 00:00:00 2001 From: Andrey Rachitskiy Date: Thu, 19 Jun 2026 01:30:00 +0500 Subject: [PATCH] Collapse consecutive .** accessors for jsonpath exists queries When a jsonpath expression contains multiple consecutive .** (jpiAny) accessors, each one triggers a full subtree traversal in executeAnyItem(). k consecutive .** operators can degrade performance to O(N^k) on a document with N nodes, even though a chain like $.**.**.** is redundant for existence semantics and equivalent to a single $.** with merged level bounds. This was reported as a performance problem when expressions such as $.**.**.**.**.* are evaluated with @? against deeply nested JSON: the same query without the trailing .* completes in sub-millisecond time, while the form with redundant .** segments can run for many minutes. A similar pattern in strict mode can also provoke very large memory allocation attempts. Collapse consecutive jpiAny nodes at execution time for existence queries (@? and jsonb_path_exists), merging their level bounds and performing a single executeAnyItem() pass. Author: Andrey Rachitskiy Reported-by: Andrey Rachitskiy Backpatch-through: 15 --- src/backend/utils/adt/jsonpath_exec.c | 47 +++++++++++++++++++--- .../src/test/regress/expected/jsonb_jsonpath.out | 40 ++++++++++++++++++ src/test/regress/sql/jsonb_jsonpath.sql | 11 +++++ 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 6cc2acb..7dfc311 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -113,6 +113,8 @@ typedef struct JsonPathExecContext * ignored */ bool throwErrors; /* with "false" all suppressible errors are * suppressed */ + bool existsOnly; /* @? / jsonb_path_exists: may collapse + * redundant consecutive .** accessors */ bool useTz; } JsonPathExecContext; @@ -310,6 +312,7 @@ static JsonPathExecResult executeAnyItem(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbContainer *jbc, JsonValueList *found, uint32 level, uint32 first, uint32 last, bool ignoreStructuralErrors, bool unwrapNext); +static uint32 mergeAnyBound(uint32 a, uint32 b); static JsonPathBool executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, JsonPathItem *larg, JsonPathItem *rarg, JsonbValue *jb, bool unwrapRightArg, @@ -739,6 +742,7 @@ executeJsonPath(JsonPath *path, void *vars, JsonPathGetVarCallback getVar, cxt.lastGeneratedObjectId = 1 + countVars(vars); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; + cxt.existsOnly = (result == NULL); cxt.useTz = useTz; if (jspStrictAbsenceOfErrors(&cxt) && !result) @@ -1017,16 +1021,37 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, case jpiAny: { - bool hasNext = jspGetNext(jsp, &elem); + JsonPathItem elem; + bool hasNext; + uint32 first; + uint32 last; + + hasNext = jspGetNext(jsp, &elem); + first = jsp->content.anybounds.first; + last = jsp->content.anybounds.last; + + /* + * Consecutive .** accessors are redundant for existence + * queries and multiply traversal cost. + */ + if (cxt->existsOnly) + { + while (hasNext && elem.type == jpiAny) + { + first = mergeAnyBound(first, elem.content.anybounds.first); + last = mergeAnyBound(last, elem.content.anybounds.last); + hasNext = jspGetNext(&elem, &elem); + } + } /* first try without any intermediate steps */ - if (jsp->content.anybounds.first == 0) + if (first == 0) { bool savedIgnoreStructuralErrors; savedIgnoreStructuralErrors = cxt->ignoreStructuralErrors; cxt->ignoreStructuralErrors = true; - res = executeNextItem(cxt, jsp, &elem, + res = executeNextItem(cxt, jsp, hasNext ? &elem : NULL, jb, found); cxt->ignoreStructuralErrors = savedIgnoreStructuralErrors; @@ -1039,8 +1064,8 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, (cxt, hasNext ? &elem : NULL, jb->val.binary.data, found, 1, - jsp->content.anybounds.first, - jsp->content.anybounds.last, + first, + last, true, jspAutoUnwrap(cxt)); break; } @@ -1978,6 +2003,18 @@ executeNestedBoolItem(JsonPathExecContext *cxt, JsonPathItem *jsp, return res; } + +/* + * Merge level bounds of two consecutive .** accessors. + */ +static uint32 +mergeAnyBound(uint32 a, uint32 b) +{ + if (a == PG_UINT32_MAX || b == PG_UINT32_MAX) + return PG_UINT32_MAX; + return a + b; +} + /* * Implementation of several jsonpath nodes: * - jpiAny (.** accessor), diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out index 81efebc..21f1362 100644 --- a/src/test/regress/expected/jsonb_jsonpath.out +++ b/src/test/regress/expected/jsonb_jsonpath.out @@ -785,6 +785,46 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; t (1 row) +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; + ?column? +---------- + f +(1 row) + +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); + jsonb_path_exists +------------------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; + ?column? +---------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + ?column? +---------- + t +(1 row) + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); jsonb_path_query ------------------ diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql index c1f4ab5..92abe13 100644 --- a/src/test/regress/sql/jsonb_jsonpath.sql +++ b/src/test/regress/sql/jsonb_jsonpath.sql @@ -150,6 +150,17 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{0 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to 2}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.y))'); -- 2.47.3 --MP_/BlgiDE/t55y8Vb7V2uvNybc Content-Type: text/x-patch Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename=0001-collapse-consecutive-jsonpath-any-pg15.patch From d0db43e880be7cd2113d78ad8500660050a949e7 Mon Sep 17 00:00:00 2001 From: Andrey Rachitskiy Date: Thu, 19 Jun 2026 01:30:00 +0500 Subject: [PATCH] Collapse consecutive .** accessors for jsonpath exists queries When a jsonpath expression contains multiple consecutive .** (jpiAny) accessors, each one triggers a full subtree traversal in executeAnyItem(). k consecutive .** operators can degrade performance to O(N^k) on a document with N nodes, even though a chain like $.**.**.** is redundant for existence semantics and equivalent to a single $.** with merged level bounds. This was reported as a performance problem when expressions such as $.**.**.**.**.* are evaluated with @? against deeply nested JSON: the same query without the trailing .* completes in sub-millisecond time, while the form with redundant .** segments can run for many minutes. A similar pattern in strict mode can also provoke very large memory allocation attempts. Collapse consecutive jpiAny nodes at execution time for existence queries (@? and jsonb_path_exists), merging their level bounds and performing a single executeAnyItem() pass. Author: Andrey Rachitskiy Reported-by: Andrey Rachitskiy Backpatch-through: 15 --- src/backend/utils/adt/jsonpath_exec.c | 47 +++++++++++++++++++--- .../src/test/regress/expected/jsonb_jsonpath.out | 40 ++++++++++++++++++ src/test/regress/sql/jsonb_jsonpath.sql | 11 +++++ 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index ba9bbb6..74fc499 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -107,6 +107,8 @@ typedef struct JsonPathExecContext * ignored */ bool throwErrors; /* with "false" all suppressible errors are * suppressed */ + bool existsOnly; /* @? / jsonb_path_exists: may collapse + * redundant consecutive .** accessors */ bool useTz; } JsonPathExecContext; @@ -199,6 +201,7 @@ static JsonPathExecResult executeAnyItem(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbContainer *jbc, JsonValueList *found, uint32 level, uint32 first, uint32 last, bool ignoreStructuralErrors, bool unwrapNext); +static uint32 mergeAnyBound(uint32 a, uint32 b); static JsonPathBool executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, JsonPathItem *larg, JsonPathItem *rarg, JsonbValue *jb, bool unwrapRightArg, @@ -567,6 +570,7 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, cxt.lastGeneratedObjectId = vars ? 2 : 1; cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; + cxt.existsOnly = (result == NULL); cxt.useTz = useTz; if (jspStrictAbsenseOfErrors(&cxt) && !result) @@ -920,16 +924,37 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, case jpiAny: { - bool hasNext = jspGetNext(jsp, &elem); + JsonPathItem elem; + bool hasNext; + uint32 first; + uint32 last; + + hasNext = jspGetNext(jsp, &elem); + first = jsp->content.anybounds.first; + last = jsp->content.anybounds.last; + + /* + * Consecutive .** accessors are redundant for existence + * queries and multiply traversal cost. + */ + if (cxt->existsOnly) + { + while (hasNext && elem.type == jpiAny) + { + first = mergeAnyBound(first, elem.content.anybounds.first); + last = mergeAnyBound(last, elem.content.anybounds.last); + hasNext = jspGetNext(&elem, &elem); + } + } /* first try without any intermediate steps */ - if (jsp->content.anybounds.first == 0) + if (first == 0) { bool savedIgnoreStructuralErrors; savedIgnoreStructuralErrors = cxt->ignoreStructuralErrors; cxt->ignoreStructuralErrors = true; - res = executeNextItem(cxt, jsp, &elem, + res = executeNextItem(cxt, jsp, hasNext ? &elem : NULL, jb, found, true); cxt->ignoreStructuralErrors = savedIgnoreStructuralErrors; @@ -942,8 +967,8 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, (cxt, hasNext ? &elem : NULL, jb->val.binary.data, found, 1, - jsp->content.anybounds.first, - jsp->content.anybounds.last, + first, + last, true, jspAutoUnwrap(cxt)); break; } @@ -1373,6 +1398,18 @@ executeNestedBoolItem(JsonPathExecContext *cxt, JsonPathItem *jsp, return res; } + +/* + * Merge level bounds of two consecutive .** accessors. + */ +static uint32 +mergeAnyBound(uint32 a, uint32 b) +{ + if (a == PG_UINT32_MAX || b == PG_UINT32_MAX) + return PG_UINT32_MAX; + return a + b; +} + /* * Implementation of several jsonpath nodes: * - jpiAny (.** accessor), diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out index eafb421..5e1bd19 100644 --- a/src/test/regress/expected/jsonb_jsonpath.out +++ b/src/test/regress/expected/jsonb_jsonpath.out @@ -792,6 +792,46 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; t (1 row) +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; + ?column? +---------- + f +(1 row) + +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); + jsonb_path_exists +------------------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; + ?column? +---------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + ?column? +---------- + t +(1 row) + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); jsonb_path_query ------------------ diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql index 8163fc6..4c62fe2 100644 --- a/src/test/regress/sql/jsonb_jsonpath.sql +++ b/src/test/regress/sql/jsonb_jsonpath.sql @@ -155,6 +155,17 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{0 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to 2}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.y))'); -- 2.47.3 --MP_/BlgiDE/t55y8Vb7V2uvNybc Content-Type: text/x-patch Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename=0001-collapse-consecutive-jsonpath-any-pg16.patch From e86b7a05fd0121a2e143b64b1b2bee28129351ff Mon Sep 17 00:00:00 2001 From: Andrey Rachitskiy Date: Thu, 19 Jun 2026 01:30:00 +0500 Subject: [PATCH] Collapse consecutive .** accessors for jsonpath exists queries When a jsonpath expression contains multiple consecutive .** (jpiAny) accessors, each one triggers a full subtree traversal in executeAnyItem(). k consecutive .** operators can degrade performance to O(N^k) on a document with N nodes, even though a chain like $.**.**.** is redundant for existence semantics and equivalent to a single $.** with merged level bounds. This was reported as a performance problem when expressions such as $.**.**.**.**.* are evaluated with @? against deeply nested JSON: the same query without the trailing .* completes in sub-millisecond time, while the form with redundant .** segments can run for many minutes. A similar pattern in strict mode can also provoke very large memory allocation attempts. Collapse consecutive jpiAny nodes at execution time for existence queries (@? and jsonb_path_exists), merging their level bounds and performing a single executeAnyItem() pass. Author: Andrey Rachitskiy Reported-by: Andrey Rachitskiy Backpatch-through: 15 --- src/backend/utils/adt/jsonpath_exec.c | 47 +++++++++++++++++++--- .../src/test/regress/expected/jsonb_jsonpath.out | 40 ++++++++++++++++++ src/test/regress/sql/jsonb_jsonpath.sql | 11 +++++ 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index a758e1a..d08b122 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -108,6 +108,8 @@ typedef struct JsonPathExecContext * ignored */ bool throwErrors; /* with "false" all suppressible errors are * suppressed */ + bool existsOnly; /* @? / jsonb_path_exists: may collapse + * redundant consecutive .** accessors */ bool useTz; } JsonPathExecContext; @@ -200,6 +202,7 @@ static JsonPathExecResult executeAnyItem(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbContainer *jbc, JsonValueList *found, uint32 level, uint32 first, uint32 last, bool ignoreStructuralErrors, bool unwrapNext); +static uint32 mergeAnyBound(uint32 a, uint32 b); static JsonPathBool executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, JsonPathItem *larg, JsonPathItem *rarg, JsonbValue *jb, bool unwrapRightArg, @@ -568,6 +571,7 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors, cxt.lastGeneratedObjectId = vars ? 2 : 1; cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; + cxt.existsOnly = (result == NULL); cxt.useTz = useTz; if (jspStrictAbsenseOfErrors(&cxt) && !result) @@ -921,16 +925,37 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, case jpiAny: { - bool hasNext = jspGetNext(jsp, &elem); + JsonPathItem elem; + bool hasNext; + uint32 first; + uint32 last; + + hasNext = jspGetNext(jsp, &elem); + first = jsp->content.anybounds.first; + last = jsp->content.anybounds.last; + + /* + * Consecutive .** accessors are redundant for existence + * queries and multiply traversal cost. + */ + if (cxt->existsOnly) + { + while (hasNext && elem.type == jpiAny) + { + first = mergeAnyBound(first, elem.content.anybounds.first); + last = mergeAnyBound(last, elem.content.anybounds.last); + hasNext = jspGetNext(&elem, &elem); + } + } /* first try without any intermediate steps */ - if (jsp->content.anybounds.first == 0) + if (first == 0) { bool savedIgnoreStructuralErrors; savedIgnoreStructuralErrors = cxt->ignoreStructuralErrors; cxt->ignoreStructuralErrors = true; - res = executeNextItem(cxt, jsp, &elem, + res = executeNextItem(cxt, jsp, hasNext ? &elem : NULL, jb, found, true); cxt->ignoreStructuralErrors = savedIgnoreStructuralErrors; @@ -943,8 +968,8 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, (cxt, hasNext ? &elem : NULL, jb->val.binary.data, found, 1, - jsp->content.anybounds.first, - jsp->content.anybounds.last, + first, + last, true, jspAutoUnwrap(cxt)); break; } @@ -1374,6 +1399,18 @@ executeNestedBoolItem(JsonPathExecContext *cxt, JsonPathItem *jsp, return res; } + +/* + * Merge level bounds of two consecutive .** accessors. + */ +static uint32 +mergeAnyBound(uint32 a, uint32 b) +{ + if (a == PG_UINT32_MAX || b == PG_UINT32_MAX) + return PG_UINT32_MAX; + return a + b; +} + /* * Implementation of several jsonpath nodes: * - jpiAny (.** accessor), diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out index eafb421..5e1bd19 100644 --- a/src/test/regress/expected/jsonb_jsonpath.out +++ b/src/test/regress/expected/jsonb_jsonpath.out @@ -792,6 +792,46 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; t (1 row) +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; + ?column? +---------- + f +(1 row) + +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); + jsonb_path_exists +------------------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; + ?column? +---------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + ?column? +---------- + t +(1 row) + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); jsonb_path_query ------------------ diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql index 8163fc6..4c62fe2 100644 --- a/src/test/regress/sql/jsonb_jsonpath.sql +++ b/src/test/regress/sql/jsonb_jsonpath.sql @@ -155,6 +155,17 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{0 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to 2}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.y))'); -- 2.47.3 --MP_/BlgiDE/t55y8Vb7V2uvNybc Content-Type: text/x-patch Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename=0001-collapse-consecutive-jsonpath-any-pg17.patch From 7fbbe75875ca510c4fe730c2ecbb48bf54001574 Mon Sep 17 00:00:00 2001 From: Andrey Rachitskiy Date: Thu, 19 Jun 2026 01:30:00 +0500 Subject: [PATCH] Collapse consecutive .** accessors for jsonpath exists queries When a jsonpath expression contains multiple consecutive .** (jpiAny) accessors, each one triggers a full subtree traversal in executeAnyItem(). k consecutive .** operators can degrade performance to O(N^k) on a document with N nodes, even though a chain like $.**.**.** is redundant for existence semantics and equivalent to a single $.** with merged level bounds. This was reported as a performance problem when expressions such as $.**.**.**.**.* are evaluated with @? against deeply nested JSON: the same query without the trailing .* completes in sub-millisecond time, while the form with redundant .** segments can run for many minutes. A similar pattern in strict mode can also provoke very large memory allocation attempts. Collapse consecutive jpiAny nodes at execution time for existence queries (@? and jsonb_path_exists), merging their level bounds and performing a single executeAnyItem() pass. Author: Andrey Rachitskiy Reported-by: Andrey Rachitskiy Backpatch-through: 15 --- src/backend/utils/adt/jsonpath_exec.c | 47 +++++++++++++++++++--- .../src/test/regress/expected/jsonb_jsonpath.out | 40 ++++++++++++++++++ src/test/regress/sql/jsonb_jsonpath.sql | 11 +++++ 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 9b7187e..21d21ea 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -115,6 +115,8 @@ typedef struct JsonPathExecContext * ignored */ bool throwErrors; /* with "false" all suppressible errors are * suppressed */ + bool existsOnly; /* @? / jsonb_path_exists: may collapse + * redundant consecutive .** accessors */ bool useTz; } JsonPathExecContext; @@ -284,6 +286,7 @@ static JsonPathExecResult executeAnyItem(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbContainer *jbc, JsonValueList *found, uint32 level, uint32 first, uint32 last, bool ignoreStructuralErrors, bool unwrapNext); +static uint32 mergeAnyBound(uint32 a, uint32 b); static JsonPathBool executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, JsonPathItem *larg, JsonPathItem *rarg, JsonbValue *jb, bool unwrapRightArg, @@ -703,6 +706,7 @@ executeJsonPath(JsonPath *path, void *vars, JsonPathGetVarCallback getVar, cxt.lastGeneratedObjectId = 1 + countVars(vars); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; + cxt.existsOnly = (result == NULL); cxt.useTz = useTz; if (jspStrictAbsenceOfErrors(&cxt) && !result) @@ -983,16 +987,37 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, case jpiAny: { - bool hasNext = jspGetNext(jsp, &elem); + JsonPathItem elem; + bool hasNext; + uint32 first; + uint32 last; + + hasNext = jspGetNext(jsp, &elem); + first = jsp->content.anybounds.first; + last = jsp->content.anybounds.last; + + /* + * Consecutive .** accessors are redundant for existence + * queries and multiply traversal cost. + */ + if (cxt->existsOnly) + { + while (hasNext && elem.type == jpiAny) + { + first = mergeAnyBound(first, elem.content.anybounds.first); + last = mergeAnyBound(last, elem.content.anybounds.last); + hasNext = jspGetNext(&elem, &elem); + } + } /* first try without any intermediate steps */ - if (jsp->content.anybounds.first == 0) + if (first == 0) { bool savedIgnoreStructuralErrors; savedIgnoreStructuralErrors = cxt->ignoreStructuralErrors; cxt->ignoreStructuralErrors = true; - res = executeNextItem(cxt, jsp, &elem, + res = executeNextItem(cxt, jsp, hasNext ? &elem : NULL, jb, found, true); cxt->ignoreStructuralErrors = savedIgnoreStructuralErrors; @@ -1005,8 +1030,8 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, (cxt, hasNext ? &elem : NULL, jb->val.binary.data, found, 1, - jsp->content.anybounds.first, - jsp->content.anybounds.last, + first, + last, true, jspAutoUnwrap(cxt)); break; } @@ -1924,6 +1949,18 @@ executeNestedBoolItem(JsonPathExecContext *cxt, JsonPathItem *jsp, return res; } + +/* + * Merge level bounds of two consecutive .** accessors. + */ +static uint32 +mergeAnyBound(uint32 a, uint32 b) +{ + if (a == PG_UINT32_MAX || b == PG_UINT32_MAX) + return PG_UINT32_MAX; + return a + b; +} + /* * Implementation of several jsonpath nodes: * - jpiAny (.** accessor), diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out index 8cf6ecf..0bbad25 100644 --- a/src/test/regress/expected/jsonb_jsonpath.out +++ b/src/test/regress/expected/jsonb_jsonpath.out @@ -785,6 +785,46 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; t (1 row) +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; + ?column? +---------- + f +(1 row) + +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); + jsonb_path_exists +------------------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; + ?column? +---------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + ?column? +---------- + t +(1 row) + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); jsonb_path_query ------------------ diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql index acb508c..1d867d8 100644 --- a/src/test/regress/sql/jsonb_jsonpath.sql +++ b/src/test/regress/sql/jsonb_jsonpath.sql @@ -150,6 +150,17 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{0 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to 2}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.y))'); -- 2.47.3 --MP_/BlgiDE/t55y8Vb7V2uvNybc Content-Type: text/x-patch Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename=0001-collapse-consecutive-jsonpath-any-pg18.patch From 78d89a7b531623855318b85421004b1aeccccff0 Mon Sep 17 00:00:00 2001 From: Andrey Rachitskiy Date: Thu, 19 Jun 2026 01:30:00 +0500 Subject: [PATCH] Collapse consecutive .** accessors for jsonpath exists queries When a jsonpath expression contains multiple consecutive .** (jpiAny) accessors, each one triggers a full subtree traversal in executeAnyItem(). k consecutive .** operators can degrade performance to O(N^k) on a document with N nodes, even though a chain like $.**.**.** is redundant for existence semantics and equivalent to a single $.** with merged level bounds. This was reported as a performance problem when expressions such as $.**.**.**.**.* are evaluated with @? against deeply nested JSON: the same query without the trailing .* completes in sub-millisecond time, while the form with redundant .** segments can run for many minutes. A similar pattern in strict mode can also provoke very large memory allocation attempts. Collapse consecutive jpiAny nodes at execution time for existence queries (@? and jsonb_path_exists), merging their level bounds and performing a single executeAnyItem() pass. Author: Andrey Rachitskiy Reported-by: Andrey Rachitskiy Backpatch-through: 15 --- src/backend/utils/adt/jsonpath_exec.c | 47 +++++++++++++++++++--- .../src/test/regress/expected/jsonb_jsonpath.out | 40 ++++++++++++++++++ src/test/regress/sql/jsonb_jsonpath.sql | 11 +++++ 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eedc5c8..c4d5cb1 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -113,6 +113,8 @@ typedef struct JsonPathExecContext * ignored */ bool throwErrors; /* with "false" all suppressible errors are * suppressed */ + bool existsOnly; /* @? / jsonb_path_exists: may collapse + * redundant consecutive .** accessors */ bool useTz; } JsonPathExecContext; @@ -282,6 +284,7 @@ static JsonPathExecResult executeAnyItem(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbContainer *jbc, JsonValueList *found, uint32 level, uint32 first, uint32 last, bool ignoreStructuralErrors, bool unwrapNext); +static uint32 mergeAnyBound(uint32 a, uint32 b); static JsonPathBool executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, JsonPathItem *larg, JsonPathItem *rarg, JsonbValue *jb, bool unwrapRightArg, @@ -701,6 +704,7 @@ executeJsonPath(JsonPath *path, void *vars, JsonPathGetVarCallback getVar, cxt.lastGeneratedObjectId = 1 + countVars(vars); cxt.innermostArraySize = -1; cxt.throwErrors = throwErrors; + cxt.existsOnly = (result == NULL); cxt.useTz = useTz; if (jspStrictAbsenceOfErrors(&cxt) && !result) @@ -981,16 +985,37 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, case jpiAny: { - bool hasNext = jspGetNext(jsp, &elem); + JsonPathItem elem; + bool hasNext; + uint32 first; + uint32 last; + + hasNext = jspGetNext(jsp, &elem); + first = jsp->content.anybounds.first; + last = jsp->content.anybounds.last; + + /* + * Consecutive .** accessors are redundant for existence + * queries and multiply traversal cost. + */ + if (cxt->existsOnly) + { + while (hasNext && elem.type == jpiAny) + { + first = mergeAnyBound(first, elem.content.anybounds.first); + last = mergeAnyBound(last, elem.content.anybounds.last); + hasNext = jspGetNext(&elem, &elem); + } + } /* first try without any intermediate steps */ - if (jsp->content.anybounds.first == 0) + if (first == 0) { bool savedIgnoreStructuralErrors; savedIgnoreStructuralErrors = cxt->ignoreStructuralErrors; cxt->ignoreStructuralErrors = true; - res = executeNextItem(cxt, jsp, &elem, + res = executeNextItem(cxt, jsp, hasNext ? &elem : NULL, jb, found, true); cxt->ignoreStructuralErrors = savedIgnoreStructuralErrors; @@ -1003,8 +1028,8 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, (cxt, hasNext ? &elem : NULL, jb->val.binary.data, found, 1, - jsp->content.anybounds.first, - jsp->content.anybounds.last, + first, + last, true, jspAutoUnwrap(cxt)); break; } @@ -1923,6 +1948,18 @@ executeNestedBoolItem(JsonPathExecContext *cxt, JsonPathItem *jsp, return res; } + +/* + * Merge level bounds of two consecutive .** accessors. + */ +static uint32 +mergeAnyBound(uint32 a, uint32 b) +{ + if (a == PG_UINT32_MAX || b == PG_UINT32_MAX) + return PG_UINT32_MAX; + return a + b; +} + /* * Implementation of several jsonpath nodes: * - jpiAny (.** accessor), diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out index 4bcd4e9..03fbd97 100644 --- a/src/test/regress/expected/jsonb_jsonpath.out +++ b/src/test/regress/expected/jsonb_jsonpath.out @@ -785,6 +785,46 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; t (1 row) +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; + ?column? +---------- + t +(1 row) + +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; + ?column? +---------- + f +(1 row) + +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); + jsonb_path_exists +------------------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; + ?column? +---------- + t +(1 row) + +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + ?column? +---------- + t +(1 row) + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); jsonb_path_query ------------------ diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql index 3e8929a..db2a21d 100644 --- a/src/test/regress/sql/jsonb_jsonpath.sql +++ b/src/test/regress/sql/jsonb_jsonpath.sql @@ -150,6 +150,17 @@ select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{0 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to last}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{1 to 2}.b ? ( @ > 0)'; select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**{2 to 3}.b ? ( @ > 0)'; +-- Redundant consecutive .** accessors are collapsed for existence queries +-- (@? and jsonb_path_exists), avoiding multiplicative traversal cost. +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 0)'; +select jsonb '{"a": {"c": {"b": 1}}}' @? '$.**.**.b ? (@ > 99)'; +select jsonb_path_exists('{"a": {"c": {"b": 1}}}', '$.**.**.b ? (@ > 0)'); +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'lax $.**.**.**.**'; +select ('[' || repeat('[', 50) || '0' || repeat(']', 50) || ']')::jsonb + @? 'strict $.**.**.**'; + select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.x))'); select jsonb_path_query('{"g": {"x": 2}}', '$.g ? (exists (@.y))'); -- 2.47.3 --MP_/BlgiDE/t55y8Vb7V2uvNybc--