postgres.git / summary / log / commit / refs

commit    e73841ffbceea314cf9fa3f64a5eae9f87a46449
Author:   Alexander Korotkov <akorotkov@postgresql.org>
Date:     Sat Sep 19 14:23:43 2026 +0000

    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.
    
    86ab7f4c721d introduced this cascade as part of the PLAN clause and
    af6fad879fbf 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>


doc/src/sgml/func/func-json.sgml | 16 ++++++++++---- src/backend/parser/parse_jsontable.c | 29 ++++++++++++++++++++----- src/test/regress/expected/sqljson_jsontable.out | 28 ++++++++++++++++++------ src/test/regress/sql/sqljson_jsontable.sql | 17 ++++++++++----- 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml index 0763ec81ed8..f8ff4355a31 100644 --- a/doc/src/sgml/func/func-json.sgml +++ b/doc/src/sgml/func/func-json.sgml @@ -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> diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c index d86c6946ee9..0c6c0a6dbf9 100644 --- a/src/backend/parser/parse_jsontable.c +++ b/src/backend/parser/parse_jsontable.c @@ -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; diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out index ae64dbed303..4129a731faa 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -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; diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql index 2a33aaec57f..b93b43899dd 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -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;