From e89abf494dd2009e294ad2900791004ca9550b99 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Wed, 28 Oct 2026 19:45:26 +0900 Subject: [PATCH v4 1/2] SQL/JSON: always rethrow soft errors from DEFAULT expressions The DEFAULT expression of an ON ERROR or ON EMPTY clause is compiled with the JsonExpr's ErrorSaveContext in effect, so that any error raised while evaluating it is saved rather than thrown and can be reported later as "could not coerce ON ERROR expression (DEFAULT) to the RETURNING type". That reporting is done by an EEOP_JSONEXPR_COERCION_FINISH step, but ExecInitJsonExpr only added that step when the DEFAULT expression needed a runtime coercion, or when its top-level node was a CoerceViaIO or a CoerceToDomain. Those are not the only nodes that can save an error: the ErrorSaveContext applies to the whole expression tree, so a cast nested inside, say, an OpExpr saves its error just the same. In that case there was nothing to rethrow it and the query silently returned NULL instead of failing, for example: SELECT JSON_VALUE('"a"', '$' RETURNING int DEFAULT (('a' || (random() * 0)::int)::int + 0) ON ERROR); Fix by adding the COERCION_FINISH step after every DEFAULT expression that is evaluated softly, that is, whenever the ON ERROR behavior is not ERROR. Reported-by: jian he Author: jian he Discussion: https://postgr.es/m/CACJufxHDK39zF+1zZF6prH_wtn1SDt74_mrqYk1Oo82cCu44ng@mail.gmail.com Backpatch-through: 17 --- src/backend/executor/execExpr.c | 35 ++++++++----------- .../regress/expected/sqljson_jsontable.out | 13 +++++++ .../regress/expected/sqljson_queryfuncs.out | 17 +++++++++ src/test/regress/sql/sqljson_jsontable.sql | 5 +++ src/test/regress/sql/sqljson_queryfuncs.sql | 8 +++++ 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 3f063b79f4d..9a7424273c0 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -4948,19 +4948,14 @@ ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state, resv, resnull); /* - * Add a COERCION_FINISH step to check for errors that may occur when - * coercing and rethrow them. + * Add a COERCION_FINISH step to throw any error saved while + * evaluating the ON ERROR expression or coercing its result. */ - if (jsexpr->on_error->coerce || - IsA(jsexpr->on_error->expr, CoerceViaIO) || - IsA(jsexpr->on_error->expr, CoerceToDomain)) - { - scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; - scratch->resvalue = resv; - scratch->resnull = resnull; - scratch->d.jsonexpr.jsestate = jsestate; - ExprEvalPushStep(state, scratch); - } + scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; + scratch->resvalue = resv; + scratch->resnull = resnull; + scratch->d.jsonexpr.jsestate = jsestate; + ExprEvalPushStep(state, scratch); /* JUMP to end to skip the ON EMPTY steps added below. */ jumps_to_end = lappend_int(jumps_to_end, state->steps_len); @@ -4995,8 +4990,10 @@ ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state, ExprEvalPushStep(state, scratch); /* - * Steps to evaluate the ON EMPTY expression; handle errors softly to - * rethrow them in COERCION_FINISH step that will be added later. + * Steps to evaluate the ON EMPTY expression. Unless ON ERROR is + * ERROR, errors are handled softly, to be rethrown by the + * COERCION_FINISH step added below; otherwise they are thrown + * directly. */ saved_escontext = state->escontext; state->escontext = escontext; @@ -5011,14 +5008,12 @@ ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state, resv, resnull); /* - * Add a COERCION_FINISH step to check for errors that may occur when - * coercing and rethrow them. + * If the ON EMPTY expression was evaluated softly, add a + * COERCION_FINISH step to throw any error saved while evaluating it + * or coercing its result. */ - if (jsexpr->on_empty->coerce || - IsA(jsexpr->on_empty->expr, CoerceViaIO) || - IsA(jsexpr->on_empty->expr, CoerceToDomain)) + if (escontext) { - scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH; scratch->resvalue = resv; scratch->resnull = resnull; diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out index ae64dbed303..d6a22ba8205 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -672,6 +672,19 @@ SELECT a, a::bool FROM JSON_TABLE(jsonb '{"a":1}', '$' COLUMNS (a dint4_0 EXISTS 1 | t (1 row) +-- soft errors in the ON EMPTY expression must be rethrown +SELECT * FROM JSON_TABLE('{}', '$' COLUMNS (a int PATH '$.a' DEFAULT (0::dint4_0 + 0) ON EMPTY)); +ERROR: could not coerce ON EMPTY expression (DEFAULT) to the RETURNING type +DETAIL: value for domain dint4_0 violates check constraint "dint4_0_check" +SELECT * FROM JSON_TABLE('{}', '$' COLUMNS (a int PATH '$.a' KEEP QUOTES DEFAULT (0::dint4_0 + 0) ON EMPTY)); +ERROR: could not coerce ON EMPTY expression (DEFAULT) to the RETURNING type +DETAIL: value for domain dint4_0 violates check constraint "dint4_0_check" +SELECT * FROM JSON_TABLE('{}', '$' COLUMNS (a int PATH '$.a' DEFAULT (('a' || (random() * 0)::int)::int + 0) ON EMPTY)); +ERROR: could not coerce ON EMPTY expression (DEFAULT) to the RETURNING type +DETAIL: invalid input syntax for type integer: "a0" +SELECT * FROM JSON_TABLE('{}', '$' COLUMNS (a int PATH '$.a' KEEP QUOTES DEFAULT (('a' || (random() * 0)::int)::int + 0) ON EMPTY)); +ERROR: could not coerce ON EMPTY expression (DEFAULT) to the RETURNING type +DETAIL: invalid input syntax for type integer: "a0" DROP DOMAIN dint4, dint4_0; -- JSON_TABLE: WRAPPER/QUOTES clauses on scalar columns SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' KEEP QUOTES ON SCALAR STRING)); diff --git a/src/test/regress/expected/sqljson_queryfuncs.out b/src/test/regress/expected/sqljson_queryfuncs.out index fe8aa927b5e..a99429a08e6 100644 --- a/src/test/regress/expected/sqljson_queryfuncs.out +++ b/src/test/regress/expected/sqljson_queryfuncs.out @@ -1515,6 +1515,23 @@ ERROR: invalid ON ERROR behavior LINE 1: SELECT JSON_QUERY(jsonb '1', '$' TRUE ON ERROR); ^ DETAIL: Only ERROR, NULL, EMPTY ARRAY, EMPTY OBJECT, or DEFAULT expression is allowed in ON ERROR for JSON_QUERY(). +-- soft errors raised by the ON ERROR or ON EMPTY DEFAULT expression must always +-- be rethrown +SELECT JSON_VALUE('"a"', '$' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +ERROR: could not coerce ON ERROR expression (DEFAULT) to the RETURNING type +DETAIL: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE('"a"', '$' RETURNING int DEFAULT (NULL::sqljsonb_int_not_null + 0) ON ERROR); +ERROR: could not coerce ON ERROR expression (DEFAULT) to the RETURNING type +DETAIL: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_QUERY('"a"', '$' RETURNING int DEFAULT (NULL::sqljsonb_int_not_null + 0) ON ERROR); +ERROR: could not coerce ON ERROR expression (DEFAULT) to the RETURNING type +DETAIL: domain sqljsonb_int_not_null does not allow null values +SELECT JSON_VALUE('"a"', '$' RETURNING int DEFAULT (('a' || (random() * 0)::int)::int + 0) ON ERROR); +ERROR: could not coerce ON ERROR expression (DEFAULT) to the RETURNING type +DETAIL: invalid input syntax for type integer: "a0" +SELECT JSON_QUERY('"a"', '$' RETURNING int DEFAULT (('a' || (random() * 0)::int)::int + 0) ON ERROR); +ERROR: could not coerce ON ERROR expression (DEFAULT) to the RETURNING type +DETAIL: invalid input syntax for type integer: "a0" -- Test implicit coercion to a domain over fixed-length type specified in -- RETURNING CREATE DOMAIN queryfuncs_char2 AS char(2); diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql index 2a33aaec57f..7f0b54b1121 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -311,6 +311,11 @@ SELECT a, a::bool FROM JSON_TABLE(jsonb '{"a":1}', '$' COLUMNS (a dint4_0 EXISTS SELECT a, a::bool FROM JSON_TABLE(jsonb '{"a":1}', '$' COLUMNS (a dint4_0 EXISTS PATH '$.b' ERROR ON ERROR)); SELECT a, a::bool FROM JSON_TABLE(jsonb '{"a":1}', '$' COLUMNS (a dint4_0 EXISTS PATH '$.b' FALSE ON ERROR)); SELECT a, a::bool FROM JSON_TABLE(jsonb '{"a":1}', '$' COLUMNS (a dint4_0 EXISTS PATH '$.b' TRUE ON ERROR)); +-- soft errors in the ON EMPTY expression must be rethrown +SELECT * FROM JSON_TABLE('{}', '$' COLUMNS (a int PATH '$.a' DEFAULT (0::dint4_0 + 0) ON EMPTY)); +SELECT * FROM JSON_TABLE('{}', '$' COLUMNS (a int PATH '$.a' KEEP QUOTES DEFAULT (0::dint4_0 + 0) ON EMPTY)); +SELECT * FROM JSON_TABLE('{}', '$' COLUMNS (a int PATH '$.a' DEFAULT (('a' || (random() * 0)::int)::int + 0) ON EMPTY)); +SELECT * FROM JSON_TABLE('{}', '$' COLUMNS (a int PATH '$.a' KEEP QUOTES DEFAULT (('a' || (random() * 0)::int)::int + 0) ON EMPTY)); DROP DOMAIN dint4, dint4_0; -- JSON_TABLE: WRAPPER/QUOTES clauses on scalar columns diff --git a/src/test/regress/sql/sqljson_queryfuncs.sql b/src/test/regress/sql/sqljson_queryfuncs.sql index 0767db9cfff..9b81429b4bc 100644 --- a/src/test/regress/sql/sqljson_queryfuncs.sql +++ b/src/test/regress/sql/sqljson_queryfuncs.sql @@ -509,6 +509,14 @@ SELECT JSON_EXISTS(jsonb '1', '$' DEFAULT 1 ON ERROR); SELECT JSON_VALUE(jsonb '1', '$' EMPTY ON ERROR); SELECT JSON_QUERY(jsonb '1', '$' TRUE ON ERROR); +-- soft errors raised by the ON ERROR or ON EMPTY DEFAULT expression must always +-- be rethrown +SELECT JSON_VALUE('"a"', '$' RETURNING sqljsonb_int_not_null DEFAULT NULL ON ERROR); +SELECT JSON_VALUE('"a"', '$' RETURNING int DEFAULT (NULL::sqljsonb_int_not_null + 0) ON ERROR); +SELECT JSON_QUERY('"a"', '$' RETURNING int DEFAULT (NULL::sqljsonb_int_not_null + 0) ON ERROR); +SELECT JSON_VALUE('"a"', '$' RETURNING int DEFAULT (('a' || (random() * 0)::int)::int + 0) ON ERROR); +SELECT JSON_QUERY('"a"', '$' RETURNING int DEFAULT (('a' || (random() * 0)::int)::int + 0) ON ERROR); + -- Test implicit coercion to a domain over fixed-length type specified in -- RETURNING CREATE DOMAIN queryfuncs_char2 AS char(2); -- 2.47.3