From e73841ffbceea314cf9fa3f64a5eae9f87a46449 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Sat, 19 Sep 2026 17:23:43 +0300 Subject: [PATCH] JSON_TABLE: propagate table-level ON ERROR to columns per SQL standard Per ISO/IEC 9075-2:2023, 7.11 , 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 --- doc/src/sgml/func/func-json.sgml | 16 +++++++--- src/backend/parser/parse_jsontable.c | 29 +++++++++++++++---- .../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 json_table_column is: handle errors when evaluating the top-level path_expression. Use ERROR if you want the errors to be thrown and EMPTY 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 - ON ERROR clause is specified against a given column. + return an empty table, that is, a table containing 0 rows. + + + This clause also provides the default error behavior for the individual + columns: a regular or formatted column that does not specify its own + ON ERROR clause uses ERROR ON ERROR + when ERROR is specified here, and + NULL ON ERROR otherwise (including when this clause is + omitted, in which case EMPTY is the default). A column + that specifies its own ON ERROR clause is unaffected. + EXISTS columns are an exception: they always default to + FALSE ON ERROR regardless of this clause. 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 , 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 , 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 , 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;