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;