JSON_TABLE deparse drops a column-level NULL ON ERROR that overrides a table-level ERROR ON ERROR, so the view changes behavior on restore Problem ------- Since commit e73841ffbce ("JSON_TABLE: propagate table-level ON ERROR to columns per SQL standard", 2026-09-19), a regular or formatted JSON_TABLE column without its own ON ERROR clause inherits ERROR ON ERROR from a table-level ERROR ON ERROR clause. A column that explicitly says NULL ON ERROR still overrides that, as the standard requires and as the commit's own tests check. However, ruleutils.c still treats NULL ON ERROR as "the default" for such a column and omits it from the deparsed text. Under a table-level ERROR ON ERROR the omitted clause is no longer implied: re-parsing the deparsed text makes the column inherit ERROR ON ERROR. So pg_get_viewdef() (and hence pg_dump, pg_restore, pg_upgrade, and CREATE OR REPLACE VIEW from \d+ output) turns a column that returned NULL on a conversion failure into one that raises an error. The stored view itself keeps working; only the textual definition is wrong, which is exactly the form that survives a dump. Affected branches and commits ----------------------------- master only (20devel). Introduced by e73841ffbce, which changed the parser default without a matching change in get_json_table_columns() / get_json_expr_options() in src/backend/utils/adt/ruleutils.c. The deparse code itself is unchanged since JSON_TABLE was added in 17 (de3600452b6), where omitting NULL ON ERROR was harmless because the table-level clause never affected columns. Released branches are therefore not wrong on their own, but a view created on 17-19 as COLUMNS (a int PATH '$' NULL ON ERROR) ERROR ON ERROR is dumped by those servers as COLUMNS (a integer PATH '$') ERROR ON ERROR and will change behavior when restored on a master with e73841ffbce, including via pg_upgrade. Fixing master alone does not cover that path; only teaching the back branches to print the column clause in this situation (or a release note) would. Reproduction ------------ On master (tested at 09a579abaca, which includes e73841ffbce): -- explicit column NULL ON ERROR wins over table ERROR ON ERROR SELECT * FROM JSON_TABLE(jsonb '"err"', '$' COLUMNS (a int PATH '$' NULL ON ERROR) ERROR ON ERROR) jt; a --- (1 row) CREATE VIEW v AS SELECT * FROM JSON_TABLE(jsonb '"err"', '$' COLUMNS (a int PATH '$' NULL ON ERROR) ERROR ON ERROR) jt; SELECT * FROM v; -- NULL row, as above SELECT pg_get_viewdef('v', true); SELECT a FROM JSON_TABLE( '"err"'::jsonb, '$' AS json_table_path_0 COLUMNS ( a integer PATH '$' ) ERROR ON ERROR ) jt; -- Feed the deparsed definition back (what pg_dump/restore does) CREATE OR REPLACE VIEW v AS SELECT a FROM JSON_TABLE('"err"'::jsonb, '$' AS json_table_path_0 COLUMNS (a integer PATH '$') ERROR ON ERROR) jt; SELECT * FROM v; ERROR: invalid input syntax for type integer: "err" Expected: pg_get_viewdef() prints "a integer PATH '$' NULL ON ERROR" and the re-created view (or a pg_dump/pg_restore of it) still returns a NULL row. Actual: the column clause is omitted and the restored view raises an error. The same happens for formatted columns, e.g. "a int[] PATH '$.a' NULL ON ERROR" under ERROR ON ERROR deparses as "a integer[] PATH '$."a"' WITHOUT WRAPPER KEEP QUOTES" with no ON ERROR clause. A plain "pg_dump src | psql dst" of a database containing the view above produces a view in dst that raises where the one in src returned NULL. Diagnosis --------- transformJsonTableColumn() (parse_jsontable.c) now synthesizes ERROR ON ERROR for a non-EXISTS column that has no on_error when the table-level on_error is JSON_BEHAVIOR_ERROR. The column-level default is therefore a function of the table-level clause. get_json_table_columns() (ruleutils.c) still passes a fixed default_behavior of JSON_BEHAVIOR_NULL (JSON_BEHAVIOR_FALSE for EXISTS) to get_json_expr_options(), which suppresses ON EMPTY and ON ERROR clauses whose btype equals that single default. So under a table-level ERROR ON ERROR: - a column with on_error = ERROR (whether written or inherited) is printed explicitly, which is redundant but harmless; and - a column with on_error = NULL (written explicitly) is dropped, and re-parses as ERROR ON ERROR. The fix is to make the ON ERROR default used for suppression follow the table-level clause: for non-EXISTS columns use JSON_BEHAVIOR_ERROR when castNode(JsonExpr, tf->docexpr)->on_error->btype is JSON_BEHAVIOR_ERROR, else JSON_BEHAVIOR_NULL, while leaving the ON EMPTY default alone (it does not cascade). That needs get_json_expr_options() to take separate ON EMPTY and ON ERROR defaults. Verified: with that change the view above deparses with "NULL ON ERROR", pg_dump/restore round-trips to a NULL row, and the only regression change is that the two views in sqljson_jsontable whose ERROR ON ERROR was inherited are now printed without the redundant clause. Alternatively, always print the column's ON ERROR when the table-level clause is ERROR ON ERROR; that keeps the current expected output and is more robust against future default changes. The commit's tests exercise the explicit NULL ON ERROR override only at execution time; a pg_get_viewdef() test for it would have caught this.