]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Enforce RETURNING typmod on SQL/JSON DEFAULT behavior expressions
authorAmit Langote <amitlan@postgresql.org>
Mon, 6 Jul 2026 23:14:04 +0000 (08:14 +0900)
committerAmit Langote <amitlan@postgresql.org>
Mon, 6 Jul 2026 23:27:32 +0000 (08:27 +0900)
transformJsonBehavior() coerced an ON EMPTY / ON ERROR DEFAULT
expression only when its type differed from the RETURNING type's OID.
When the base type matched but the RETURNING type carried a type
modifier (e.g. numeric(4,1) or varchar(3)), the coercion that enforces
the typmod was skipped, so the DEFAULT value could violate the
declared type:

    SELECT JSON_VALUE(jsonb '{}', '$.a'
                      RETURNING numeric(4,1) DEFAULT 99999.999 ON EMPTY);

returned 99999.999, which 99999.999::numeric(4,1) would reject; the
value could even be stored into a numeric(4,1) column, as later
coercions trust its already-correct type label.

Fix by also coercing when the RETURNING type has a typmod, except for
a NULL constant.  coerce_to_target_type() is a no-op when the typmod
already matches.  The matching-OID short-circuit dates to 74c96699be3.

Reported-by: Ewan Young <kdbase.hack@gmail.com>
Author: Ewan Young <kdbase.hack@gmail.com>
Discussion: https://postgr.es/m/CAON2xHPO9f4cAmyGn1mQ=VqoS7wN5rz4yOiqudxX78zninZpCw@mail.gmail.com
Backpatch-through: 17

src/backend/parser/parse_expr.c
src/test/regress/expected/sqljson_jsontable.out
src/test/regress/expected/sqljson_queryfuncs.out
src/test/regress/sql/sqljson_jsontable.sql
src/test/regress/sql/sqljson_queryfuncs.sql

index 9adc9d4c0f63768a5bf05cde0230f84a22ba607c..e6ea34a7809371e7bb1a2b946f5fca1317124196 100644 (file)
@@ -4931,8 +4931,17 @@ transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr,
         *
         * For other non-NULL expressions, try to find a cast and error out if one
         * is not found.
+        *
+        * The DEFAULT expression's base type may already match the RETURNING type
+        * yet still need coercion: when the RETURNING type carries a type
+        * modifier (e.g. numeric(4,1)), the cast below is what enforces it, so
+        * skipping it here would let the DEFAULT yield a value that violates its
+        * declared RETURNING type.  A NULL constant needs no such enforcement.
         */
-       if (expr && exprType(expr) != returning->typid)
+       if (expr &&
+               (exprType(expr) != returning->typid ||
+                (returning->typmod >= 0 &&
+                 !(IsA(expr, Const) && ((Const *) expr)->constisnull))))
        {
                bool            isnull = (IsA(expr, Const) && ((Const *) expr)->constisnull);
 
index 458c5aaa5b0683a1854acf6d6eedcd9b2f237426..4d500e7de2d2133d0171837a413ccb61304c7730 100644 (file)
@@ -250,6 +250,22 @@ SELECT * FROM JSON_TABLE(jsonb '{"d1": "foo"}', '$'
  {1}
 (1 row)
 
+-- A DEFAULT expression whose base type matches the column type must still be
+-- coerced to the column's typmod.
+SELECT * FROM JSON_TABLE(jsonb '{}', '$'
+    COLUMNS (c numeric(4,1) PATH '$.x' DEFAULT 99999.999 ON EMPTY));
+ERROR:  numeric field overflow
+DETAIL:  A field with precision 4, scale 1 must round to an absolute value less than 10^3.
+SELECT * FROM JSON_TABLE(jsonb '{}', '$'
+    COLUMNS (c bit(3) PATH '$.x' DEFAULT b'10101' ON EMPTY));
+ERROR:  bit string length 5 does not match type bit(3)
+SELECT * FROM JSON_TABLE(jsonb '{}', '$'
+    COLUMNS (c numeric(4,1) PATH '$.x' DEFAULT abs(NULL::numeric) ON EMPTY));
+ c 
+---
+  
+(1 row)
+
 -- JSON_TABLE: Test backward parsing
 CREATE VIEW jsonb_table_view2 AS
 SELECT * FROM
index 57e52e963f68507d037803142a80b280879c7a0f..ff64dce0c5916f1d6ca0c62072ed0a877138e642 100644 (file)
@@ -433,6 +433,27 @@ SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING
 ERROR:  cannot specify FORMAT JSON in RETURNING clause of JSON_VALUE()
 LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO...
                                                              ^
+-- A DEFAULT expression must be coerced to the RETURNING type's typmod even
+-- when its base type already matches, but a matching NULL needs no coercion.
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT 99999.999 ON EMPTY);
+ERROR:  numeric field overflow
+DETAIL:  A field with precision 4, scale 1 must round to an absolute value less than 10^3.
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING varchar(3) DEFAULT 'toolong'::varchar(10) ON EMPTY);
+ERROR:  value too long for type character varying(3)
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT NULL::numeric ON EMPTY);
+ json_value 
+------------
+           
+(1 row)
+
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING bit(3) DEFAULT b'10101' ON EMPTY);
+ERROR:  bit string length 5 does not match type bit(3)
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT abs(NULL::numeric) ON EMPTY);
+ json_value 
+------------
+           
+(1 row)
+
 -- RETUGNING pseudo-types not allowed
 SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING record);
 ERROR:  returning pseudo-types is not supported in SQL/JSON functions
index 154eea79c768acf1f82896d84e4c59eb328e8111..41824094b9627a1c551962cfc62003098bebeae3 100644 (file)
@@ -132,6 +132,15 @@ SELECT * FROM JSON_TABLE(jsonb '{"d1": "foo"}', '$'
 SELECT * FROM JSON_TABLE(jsonb '{"d1": "foo"}', '$'
     COLUMNS (js1 oid[] PATH '$.d2' DEFAULT '{1}'::int[]::oid[] ON EMPTY));
 
+-- A DEFAULT expression whose base type matches the column type must still be
+-- coerced to the column's typmod.
+SELECT * FROM JSON_TABLE(jsonb '{}', '$'
+    COLUMNS (c numeric(4,1) PATH '$.x' DEFAULT 99999.999 ON EMPTY));
+SELECT * FROM JSON_TABLE(jsonb '{}', '$'
+    COLUMNS (c bit(3) PATH '$.x' DEFAULT b'10101' ON EMPTY));
+SELECT * FROM JSON_TABLE(jsonb '{}', '$'
+    COLUMNS (c numeric(4,1) PATH '$.x' DEFAULT abs(NULL::numeric) ON EMPTY));
+
 -- JSON_TABLE: Test backward parsing
 
 CREATE VIEW jsonb_table_view2 AS
index d218b44ea479b3ba9c8d6af04d6c08b53bff6289..a69ef253f66a067f082713b81d48562ddfe5d2f8 100644 (file)
@@ -105,6 +105,14 @@ SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR);
 SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR);
 SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed
 
+-- A DEFAULT expression must be coerced to the RETURNING type's typmod even
+-- when its base type already matches, but a matching NULL needs no coercion.
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT 99999.999 ON EMPTY);
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING varchar(3) DEFAULT 'toolong'::varchar(10) ON EMPTY);
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT NULL::numeric ON EMPTY);
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING bit(3) DEFAULT b'10101' ON EMPTY);
+SELECT JSON_VALUE(jsonb '{}', '$.a' RETURNING numeric(4,1) DEFAULT abs(NULL::numeric) ON EMPTY);
+
 -- RETUGNING pseudo-types not allowed
 SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING record);