1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

JSON_TABLE: propagate table-level ON ERROR to columns per SQL standard

Per ISO/IEC 9075-2:2023, 7.11 <JSON table>, Syntax Rules 1)e)iv) and
1)f)xi), a regular or formatted JSON_TABLE column that does not specify
its own ON ERROR clause takes its default error behavior from the
table-level ON ERROR clause: with ERROR ON ERROR on the table, the
column behaves as ERROR ON ERROR; otherwise the column defaults to
NULL ON ERROR.

PostgreSQL instead always defaulted such columns to NULL ON ERROR, so

  SELECT * FROM JSON_TABLE(jsonb '"err"', '$'
                           COLUMNS (a int PATH '$') ERROR ON ERROR) jt;

returned a NULL row where the standard requires an error.  The
documentation stated the divergence as if it were a rule, saying that
the table-level clause "does not affect the errors that occur when
evaluating columns".

Implement the standard behavior in the JSON_TABLE syntactic
transformation: when a column lacks its own ON ERROR clause and the
table-level behavior is ERROR ON ERROR, synthesize an implicit ERROR ON
ERROR for the column before it is transformed into a JsonExpr.  A column
with its own ON ERROR clause is unaffected.  EXISTS columns are not
covered by those syntax rules, so they keep their FALSE ON ERROR
default.  A JSON_TABLE stored in a view is now deparsed with the
previously implicit ERROR ON ERROR shown explicitly on the affected
columns, which is a semantically equivalent, round-trip-stable form.

86ab7f4c72 introduced this cascade as part of the PLAN clause and
af6fad879f reverted it, on the grounds that it should be a deliberate
and separately documented change rather than a side effect of an
unrelated feature.  This is that change.

Do not back-patch.  A query that specifies ERROR ON ERROR at the table
level and relies on its columns still yielding NULL now raises an error
instead of returning rows.  Nothing is silently wrong in the released
branches: the table-level clause is ignored for columns consistently,
and that is what the documentation has promised since PostgreSQL 17, so
users could reasonably have depended on it.

Discussion: https://postgr.es/m/CAPpHfdtDXseGhrL14a2asOkwGnFTVrDk8SQc3iWZgWEEhXXMGw%40mail.gmail.com
Reviewed-by: Nikita Malakhov <hukutoc@gmail.com>
This commit is contained in:
Alexander Korotkov
2026-09-19 17:23:43 +03:00
parent a8bb1a7779
commit e73841ffbc
4 changed files with 68 additions and 22 deletions

View File

@@ -4021,10 +4021,18 @@ where <replaceable class="parameter">json_table_column</replaceable> is:
handle errors when evaluating the top-level
<replaceable>path_expression</replaceable>. Use <literal>ERROR</literal>
if you want the errors to be thrown and <literal>EMPTY</literal> to
return an empty table, that is, a table containing 0 rows. Note that
this clause does not affect the errors that occur when evaluating
columns, for which the behavior depends on whether the
<literal>ON ERROR</literal> clause is specified against a given column.
return an empty table, that is, a table containing 0 rows.
</para>
<para>
This clause also provides the default error behavior for the individual
columns: a regular or formatted column that does not specify its own
<literal>ON ERROR</literal> clause uses <literal>ERROR ON ERROR</literal>
when <literal>ERROR</literal> is specified here, and
<literal>NULL ON ERROR</literal> otherwise (including when this clause is
omitted, in which case <literal>EMPTY</literal> is the default). A column
that specifies its own <literal>ON ERROR</literal> clause is unaffected.
<literal>EXISTS</literal> columns are an exception: they always default to
<literal>FALSE ON ERROR</literal> regardless of this clause.
</para>
</listitem>
</varlistentry>

View File

@@ -49,7 +49,8 @@ static JsonTablePlan *transformJsonTableNestedColumns(JsonTableParseContext *cxt
List *columns);
static JsonFuncExpr *transformJsonTableColumn(JsonTableColumn *jtc,
Node *contextItemExpr,
List *passingArgs);
List *passingArgs,
bool errorOnError);
static bool isCompositeType(Oid typid);
static JsonTablePlan *makeJsonTablePathScan(JsonTableParseContext *cxt,
JsonTablePathSpec *pathspec,
@@ -68,7 +69,8 @@ static JsonTablePlan *makeJsonTableSiblingJoin(bool cross,
JsonTablePlan *lplan,
JsonTablePlan *rplan);
static void
appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingArgs);
appendJsonTableColumns(JsonTableParseContext *cxt, List *columns,
List *passingArgs, bool errorOnError);
/*
* transformJsonTable -
@@ -344,7 +346,7 @@ transformJsonTableColumns(JsonTableParseContext *cxt,
validateJsonTableChildPlan(cxt, childPlanSpec, columns);
}
appendJsonTableColumns(cxt, columns, passingArgs);
appendJsonTableColumns(cxt, columns, passingArgs, errorOnError);
/* End of column range. */
if (list_length(tf->colvalexprs) == colMin)
@@ -375,7 +377,8 @@ transformJsonTableColumns(JsonTableParseContext *cxt,
/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */
static void
appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingArgs)
appendJsonTableColumns(JsonTableParseContext *cxt, List *columns,
List *passingArgs, bool errorOnError)
{
ListCell *col;
ParseState *pstate = cxt->pstate;
@@ -439,7 +442,7 @@ appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingA
param->typeMod = -1;
jfe = transformJsonTableColumn(rawc, (Node *) param,
passingArgs);
passingArgs, errorOnError);
colexpr = transformExpr(pstate, (Node *) jfe,
EXPR_KIND_FROM_FUNCTION);
@@ -494,7 +497,7 @@ isCompositeType(Oid typid)
*/
static JsonFuncExpr *
transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr,
List *passingArgs)
List *passingArgs, bool errorOnError)
{
Node *pathspec;
JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr);
@@ -536,6 +539,20 @@ transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr,
jfexpr->output->returning->format = jtc->format;
jfexpr->on_empty = jtc->on_empty;
jfexpr->on_error = jtc->on_error;
/*
* Per the SQL/JSON standard, a regular or formatted column that does not
* specify its own ON ERROR clause inherits ERROR ON ERROR from a
* table-level ERROR ON ERROR clause; otherwise it defaults to NULL ON
* ERROR (applied downstream in transformJsonExprCommon()). See ISO/IEC
* 9075-2:2023, 7.11 <JSON table>, Syntax Rules 1)e)iv) (regular columns)
* and 1)f)xi) (formatted columns). EXISTS columns are not covered by the
* standard, so they keep their own default (FALSE ON ERROR).
*/
if (jfexpr->on_error == NULL && errorOnError &&
jfexpr->op != JSON_EXISTS_OP)
jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, -1);
jfexpr->quotes = jtc->quotes;
jfexpr->wrapper = jtc->wrapper;
jfexpr->location = jtc->location;

View File

@@ -562,12 +562,13 @@ FROM
JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt
ON true;
ERROR: invalid input syntax for type integer: "err"
-- TABLE-level ERROR ON ERROR is not propagated to columns
-- TABLE-level ERROR ON ERROR is propagated to a column without its own
-- ON ERROR clause (per SQL standard), so "err" raises instead of yielding NULL.
SELECT *
FROM
(VALUES ('1'), ('"err"')) vals(js)
LEFT OUTER JOIN
JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt
JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt
ON true;
ERROR: invalid input syntax for type integer: "err"
SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt;
@@ -576,15 +577,28 @@ SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR O
ERROR: jsonpath member accessor can only be applied to an object
SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt;
ERROR: no SQL/JSON item found for specified path of column "a"
-- Table-level ERROR ON ERROR is not propagated to a column lacking its own
-- ON ERROR clause: the column keeps the default NULL ON ERROR behavior, so a
-- conversion failure yields NULL rather than raising an error.
-- Table-level ERROR ON ERROR is propagated to a column lacking its own ON ERROR
-- clause, so a conversion failure raises an error rather than yielding NULL
-- (ISO/IEC 9075-2:2023, 7.11 <JSON table>, SR 1)e)iv)).
SELECT * FROM JSON_TABLE(jsonb '"err"', '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt;
ERROR: invalid input syntax for type integer: "err"
-- ... but an explicit column-level ON ERROR still wins over the table-level one.
SELECT * FROM JSON_TABLE(jsonb '"err"', '$' COLUMNS (a int PATH '$' NULL ON ERROR) ERROR ON ERROR) jt;
a
---
(1 row)
-- Propagation likewise applies to formatted columns (SR 1)f)xi)), but not to
-- EXISTS columns, which are not part of the standard and keep FALSE ON ERROR.
SELECT * FROM JSON_TABLE(jsonb '{"a":1}', '$' COLUMNS (a int[] PATH '$.a') ERROR ON ERROR) jt;
ERROR: expected JSON array
SELECT * FROM JSON_TABLE(jsonb '{}', 'strict $' COLUMNS (a int EXISTS PATH 'strict $.x') ERROR ON ERROR) jt;
a
---
0
(1 row)
SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt;
a
---
@@ -1884,7 +1898,7 @@ CREATE OR REPLACE VIEW public.json_table_view9 AS
FROM JSON_TABLE(
'"a"'::text, '$' AS json_table_path_0
COLUMNS (
a text PATH '$'
a text PATH '$' ERROR ON ERROR
) ERROR ON ERROR
)
DROP VIEW json_table_view8, json_table_view9;
@@ -1922,7 +1936,7 @@ CREATE OR REPLACE VIEW public.json_table_view_on_empty AS
FROM JSON_TABLE(
'{}'::jsonb, '$' AS p0
COLUMNS (
a integer PATH '$."nosuch"' ERROR ON EMPTY
a integer PATH '$."nosuch"' ERROR ON EMPTY ERROR ON ERROR
) ERROR ON ERROR
)
DROP VIEW json_table_view_on_empty;

View File

@@ -269,22 +269,29 @@ FROM
JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt
ON true;
-- TABLE-level ERROR ON ERROR is not propagated to columns
-- TABLE-level ERROR ON ERROR is propagated to a column without its own
-- ON ERROR clause (per SQL standard), so "err" raises instead of yielding NULL.
SELECT *
FROM
(VALUES ('1'), ('"err"')) vals(js)
LEFT OUTER JOIN
JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt
JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt
ON true;
SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt;
SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON ERROR) ERROR ON ERROR) jt;
SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt;
-- Table-level ERROR ON ERROR is not propagated to a column lacking its own
-- ON ERROR clause: the column keeps the default NULL ON ERROR behavior, so a
-- conversion failure yields NULL rather than raising an error.
-- Table-level ERROR ON ERROR is propagated to a column lacking its own ON ERROR
-- clause, so a conversion failure raises an error rather than yielding NULL
-- (ISO/IEC 9075-2:2023, 7.11 <JSON table>, SR 1)e)iv)).
SELECT * FROM JSON_TABLE(jsonb '"err"', '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt;
-- ... but an explicit column-level ON ERROR still wins over the table-level one.
SELECT * FROM JSON_TABLE(jsonb '"err"', '$' COLUMNS (a int PATH '$' NULL ON ERROR) ERROR ON ERROR) jt;
-- Propagation likewise applies to formatted columns (SR 1)f)xi)), but not to
-- EXISTS columns, which are not part of the standard and keep FALSE ON ERROR.
SELECT * FROM JSON_TABLE(jsonb '{"a":1}', '$' COLUMNS (a int[] PATH '$.a') ERROR ON ERROR) jt;
SELECT * FROM JSON_TABLE(jsonb '{}', 'strict $' COLUMNS (a int EXISTS PATH 'strict $.x') ERROR ON ERROR) jt;
SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt;
SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt;