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 1x2Qei-005lUS-2t for pgsql-bugs@arkaria.postgresql.org; Fri, 04 Sep 2026 09:56:20 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1x2Qeg-005zt7-2m for pgsql-bugs@arkaria.postgresql.org; Fri, 04 Sep 2026 09:56:18 +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 1x2QLB-005lob-2o for pgsql-bugs@lists.postgresql.org; Fri, 04 Sep 2026 09:36:09 +0000 Received: from mahout.postgresql.org ([2001:4800:3e1:1::227]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1x2QL9-00000003xI8-1D5O for pgsql-bugs@lists.postgresql.org; Fri, 04 Sep 2026 09:36:09 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Message-ID:Date:Reply-To:Cc:From:To:Subject: Content-Transfer-Encoding:MIME-Version:Content-Type:Sender:Content-ID: Content-Description:In-Reply-To:References; bh=6tphlndBc+MhiiBOsOghXg3CFNl1jD1Jqfd6EoNNZ8Q=; b=Xc1ZUw1xiJt9UTz/Xy2sTtCe/W PMAYgtveFj2dIPs7gTu3u8Y7Ns+AsgOJLSbtJtwvW4skZOZgPz7DU+mn73Ealee1REBHW65ROs5ga BqKvJ7tJVCwVkMf2AB9q+1FFak0WuPkJ9ephmQ/o6H4sEvDG9Ei8Rb+RdjK2oZqh2QTKZ5oRkJNLN WYdZzPA6ijHtHuS4zEZ0fN/NijTbw+Ito0hTK2S6I5fFDn35B7fXaa0UPjzGhwKPrceuCIv9ZnULk FBXgjpmAJouphVa4rgOQeCdu0rmetl6ke9YB756N+jWMz9rL9DrCr8seJ7Ao19usFOpNtE4suedLZ 0VzPLEHg==; Received: from wrigleys.postgresql.org ([2a02:16a8:dc51::60]) by mahout.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1x2QL7-00Bw43-1N for pgsql-bugs@lists.postgresql.org; Fri, 04 Sep 2026 09:36:06 +0000 Received: from localhost ([127.0.0.1] helo=wrigleys.postgresql.org) by wrigleys.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1x2QL6-0000000EY2r-0LBF for pgsql-bugs@lists.postgresql.org; Fri, 04 Sep 2026 09:36:04 +0000 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Subject: BUG #19654: JSON_EXISTS returns ON ERROR value for SQL NULL after a prior error To: pgsql-bugs@lists.postgresql.org From: PG Bug reporting form Cc: 2530254482@qq.com Reply-To: 2530254482@qq.com, pgsql-bugs@lists.postgresql.org Date: Fri, 04 Sep 2026 09:35:37 +0000 Message-ID: <19654-3acd06154d027634@postgresql.org> X-Auto-Response-Suppress: All Auto-Submitted: auto-generated List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk The following bug has been logged on the website: Bug reference: 19654 Logged by: Ce Lyu Email address: 2530254482@qq.com PostgreSQL version: 18.6 Operating system: Linux, official Docker image postgres:18.6, Debian Description: =20 JSON_EXISTS / JSON_VALUE / JSON_QUERY should return SQL NULL when the input document is SQL NULL, independent of which rows were evaluated earlier in the same query. After a previous row of the same compiled expression has taken the ON ERROR path, a later SQL NULL input incorrectly returns the ON ERROR replacement value instead of NULL. Scan order therefore changes the result. This is on the latest minor of the current major version. PostgreSQL version: PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit Also reproduced on: PostgreSQL 19beta3 (Debian 19~beta3-1.pgdg13+1) How installed: Official Docker images library/postgres:18.6 and library/postgres:19beta3. Default postgresql.conf, no custom GUCs. Client: psql inside the container (docker exec ... psql -U postgres) Server logs: Nothing unusual. The queries succeed; the result value is wrong. Minimal self-contained reproducer (no tables): SELECT json_exists(j, 'strict $.a' FALSE ON ERROR) FROM (VALUES ('{}'::jsonb), (NULL)) AS t(j); -- actual: f, f -- expected: f, NULL SELECT json_exists(j, 'strict $.a' FALSE ON ERROR) FROM (VALUES (NULL::jsonb), ('{}')) AS t(j); -- actual: NULL, f -- expected: NULL, f Isolated scalars are correct: SELECT json_exists(NULL::jsonb, 'strict $.a' FALSE ON ERROR); -- NULL SELECT json_exists('{}'::jsonb, 'strict $.a' FALSE ON ERROR); -- f So the NULL-document case is only wrong when the same compiled expression has already taken ON ERROR on an earlier row. The leak is specifically the ON ERROR path, not "any prior non-NULL": -- LAX missing key is a clean false, not an error; NULL stays NULL SELECT json_exists(j, 'lax $.a' FALSE ON ERROR) FROM (VALUES ('{}'::jsonb), (NULL)) AS t(j); -- f, NULL (correct) -- successful match, then NULL stays NULL SELECT json_exists(j, 'strict $.a' FALSE ON ERROR) FROM (VALUES ('{"a":1}'::jsonb), (NULL)) AS t(j); -- t, NULL (correct) -- NULL, then a STRICT error, then NULL: the second NULL is poisoned SELECT json_exists(j, 'strict $.a' FALSE ON ERROR) FROM (VALUES (NULL::jsonb), ('{}'), (NULL)) AS t(j); -- NULL, f, f -- expected: NULL, f, NULL TRUE ON ERROR poisons NULL into true: SELECT json_exists(j, 'strict $.a' TRUE ON ERROR) FROM (VALUES ('{}'::jsonb), (NULL)) AS t(j); -- t, t -- expected: t, NULL The same sticky ON ERROR state affects JSON_VALUE and JSON_QUERY when the ON ERROR replacement is not NULL: SELECT json_value(j, 'strict $.a' RETURNING int DEFAULT 0 ON ERROR) FROM (VALUES ('{}'::jsonb), (NULL)) AS t(j); -- 0, 0 -- expected: 0, NULL SELECT json_value(j, 'strict $.a' RETURNING int DEFAULT 0 ON ERROR) FROM (VALUES (NULL::jsonb), ('{}')) AS t(j); -- NULL, 0 (correct) SELECT json_query(j, 'strict $.a' EMPTY OBJECT ON ERROR) FROM (VALUES ('{}'::jsonb), (NULL)) AS t(j); -- {}, {} -- expected: {}, NULL Two independent JSON_EXISTS calls in one SELECT list do not share the state, so this is per compiled expression, not per backend: SELECT json_exists(NULL::jsonb, 'strict $.a' FALSE ON ERROR), json_exists('{}'::jsonb, 'strict $.a' FALSE ON ERROR), json_exists(NULL::jsonb, 'strict $.a' FALSE ON ERROR); -- NULL, f, NULL Likely cause (REL_18_STABLE): ExecInitJsonExpr() emits JUMP_IF_NULL on a NULL input document, targeting an EEOP_CONST NULL step that is intended to skip jsonpath evaluation. That CONST step then falls through into the "if jsestate->error then evaluate ON ERROR" steps. ExecEvalJsonExprPath() is the only place that resets jsestate->error / jsestate->empty (memset at the start of the function). The NULL-input jump skips that reset, so a previous row's error flag is still true and the ON ERROR replacement overwrites the NULL that was just stored. This matches every observation above: only a prior ON ERROR poisons later NULL inputs; a prior clean success does not; a later error poisons NULLs after it; UNKNOWN/NULL ON ERROR looks fine only because the replacement is already NULL. Originally visible as a Citus DISTINCT/WHERE discrepancy (https://github.com/citusdata/citus/issues/8792) because different shard scan orders hit NULL rows before or after a STRICT path error. The behavior reproduces on single-node vanilla PostgreSQL with VALUES.