diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out index ae64dbed30375..bc0da420dbf55 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -1926,3 +1926,328 @@ CREATE OR REPLACE VIEW public.json_table_view_on_empty AS ) ERROR ON ERROR ) DROP VIEW json_table_view_on_empty; +-- ========================================================================== +-- Persisted JSON_TABLE views for dump/restore round-trip coverage +-- +-- The views above are all dropped, so their deparsed definitions are never +-- carried through a dump/restore (or pg_upgrade) cycle. The views below are +-- deliberately left in place at the end of this file so that +-- pg_upgrade's dump comparison round-trips them, pinning the backward +-- parsing of the trickier JSON_TABLE PLAN constructs: nested parent/child +-- (OUTER/INNER) joins that must stay parenthesized, sibling UNION/CROSS +-- joins, PLAN DEFAULT, generated-vs-user path-name collisions, and per-column +-- and table-level ON ERROR/ON EMPTY. Each view is accompanied by its pinned +-- pg_get_viewdef() output and a SELECT that checks the resulting rows, so a +-- regression that silently drops a column or mangles a plan is caught here. +-- ========================================================================== +-- Nested parent/child plan two levels deep, with an explicit parenthesized +-- child join: PLAN (p0 OUTER (p1 INNER p11)). The parentheses around +-- (p1 INNER p11) must survive deparsing, otherwise the plan reparses to a +-- different tree (a dump/restore hazard). The INNER join means array +-- elements of "a" whose "b" is empty (x = 2) are dropped from the output, +-- while the OUTER-joined parent still contributes its other rows. +CREATE VIEW jsonb_table_view_pc_plan AS +SELECT * FROM JSON_TABLE( + jsonb '{"a":[{"x":1,"b":[10,20]},{"x":2,"b":[]},{"x":3,"b":[30]}]}', '$' AS p0 + COLUMNS ( + NESTED PATH '$.a[*]' AS p1 COLUMNS ( + x int PATH '$.x', + NESTED PATH '$.b[*]' AS p11 COLUMNS (b int PATH '$') + ) + ) + PLAN (p0 OUTER (p1 INNER p11)) +); +SELECT pg_get_viewdef('jsonb_table_view_pc_plan'::regclass); + pg_get_viewdef +---------------------------------------------------------------------------------------------------------- + SELECT x, + + b + + FROM JSON_TABLE( + + '{"a": [{"b": [10, 20], "x": 1}, {"b": [], "x": 2}, {"b": [30], "x": 3}]}'::jsonb, '$' AS p0+ + COLUMNS ( + + NESTED PATH '$."a"[*]' AS p1 + + COLUMNS ( + + x integer PATH '$."x"', + + NESTED PATH '$."b"[*]' AS p11 + + COLUMNS ( + + b integer PATH '$' + + ) + + ) + + ) + + PLAN (p0 OUTER (p1 INNER p11)) + + ); +(1 row) + +SELECT * FROM jsonb_table_view_pc_plan ORDER BY x, b; + x | b +---+---- + 1 | 10 + 1 | 20 + 3 | 30 +(3 rows) + +-- Sibling paths joined with an explicit CROSS (not the default UNION), under +-- a parent/child OUTER join. CROSS produces the cartesian product of the two +-- sibling arrays, so the deparsed PLAN must keep CROSS and the parentheses. +CREATE VIEW jsonb_table_view_sibling_plan AS +SELECT * FROM JSON_TABLE( + jsonb '{"a":[1,2],"b":["x","y"]}', '$' AS p0 + COLUMNS ( + NESTED PATH '$.a[*]' AS pa COLUMNS (a int PATH '$'), + NESTED PATH '$.b[*]' AS pb COLUMNS (b text PATH '$') + ) + PLAN (p0 OUTER (pa CROSS pb)) +); +SELECT pg_get_viewdef('jsonb_table_view_sibling_plan'::regclass); + pg_get_viewdef +---------------------------------------------------------------- + SELECT a, + + b + + FROM JSON_TABLE( + + '{"a": [1, 2], "b": ["x", "y"]}'::jsonb, '$' AS p0+ + COLUMNS ( + + NESTED PATH '$."a"[*]' AS pa + + COLUMNS ( + + a integer PATH '$' + + ), + + NESTED PATH '$."b"[*]' AS pb + + COLUMNS ( + + b text PATH '$' + + ) + + ) + + PLAN (p0 OUTER (pa CROSS pb)) + + ); +(1 row) + +SELECT * FROM jsonb_table_view_sibling_plan ORDER BY a, b; + a | b +---+--- + 1 | x + 1 | y + 2 | x + 2 | y +(4 rows) + +-- The trickiest shape: a parent OUTER-joined to a CROSS of two sibling +-- parent/child subplans, each of which is itself a parent/child join that +-- must stay parenthesized: PLAN (p OUTER ((pb INNER pb1) CROSS (pc OUTER pc1))). +-- Exercises both the "parenthesize a parent/child child" fix and the sibling +-- operand parenthesization together. +CREATE VIEW jsonb_table_view_mixed_plan AS +SELECT * FROM JSON_TABLE( + jsonb '{"b":[{"b1":[1,2]}],"c":[{"c1":[3]},{"c1":[]}]}', '$' AS p + COLUMNS ( + NESTED PATH '$.b[*]' AS pb COLUMNS ( + NESTED PATH '$.b1[*]' AS pb1 COLUMNS (b1 int PATH '$') + ), + NESTED PATH '$.c[*]' AS pc COLUMNS ( + NESTED PATH '$.c1[*]' AS pc1 COLUMNS (c1 int PATH '$') + ) + ) + PLAN (p OUTER ((pb INNER pb1) CROSS (pc OUTER pc1))) +); +SELECT pg_get_viewdef('jsonb_table_view_mixed_plan'::regclass); + pg_get_viewdef +---------------------------------------------------------------------------------------- + SELECT b1, + + c1 + + FROM JSON_TABLE( + + '{"b": [{"b1": [1, 2]}], "c": [{"c1": [3]}, {"c1": []}]}'::jsonb, '$' AS p+ + COLUMNS ( + + NESTED PATH '$."b"[*]' AS pb + + COLUMNS ( + + NESTED PATH '$."b1"[*]' AS pb1 + + COLUMNS ( + + b1 integer PATH '$' + + ) + + ), + + NESTED PATH '$."c"[*]' AS pc + + COLUMNS ( + + NESTED PATH '$."c1"[*]' AS pc1 + + COLUMNS ( + + c1 integer PATH '$' + + ) + + ) + + ) + + PLAN (p OUTER ((pb INNER pb1) CROSS (pc OUTER pc1))) + + ); +(1 row) + +SELECT * FROM jsonb_table_view_mixed_plan ORDER BY b1, c1; + b1 | c1 +----+---- + 1 | 3 + 1 | + 2 | 3 + 2 | +(4 rows) + +-- Deparse-reparse round-trip stability check: recreate the trickiest view +-- from its own deparsed definition and confirm the deparse is a fixed point +-- (the second view's definition is byte-for-byte identical). This proves the +-- PLAN clause parses back to the same tree without relying on an external +-- dump/restore. +SELECT format('CREATE VIEW jsonb_table_view_mixed_plan2 AS %s', + pg_get_viewdef('jsonb_table_view_mixed_plan'::regclass)) +\gexec +CREATE VIEW jsonb_table_view_mixed_plan2 AS SELECT b1, + c1 + FROM JSON_TABLE( + '{"b": [{"b1": [1, 2]}], "c": [{"c1": [3]}, {"c1": []}]}'::jsonb, '$' AS p + COLUMNS ( + NESTED PATH '$."b"[*]' AS pb + COLUMNS ( + NESTED PATH '$."b1"[*]' AS pb1 + COLUMNS ( + b1 integer PATH '$' + ) + ), + NESTED PATH '$."c"[*]' AS pc + COLUMNS ( + NESTED PATH '$."c1"[*]' AS pc1 + COLUMNS ( + c1 integer PATH '$' + ) + ) + ) + PLAN (p OUTER ((pb INNER pb1) CROSS (pc OUTER pc1))) + ); +SELECT pg_get_viewdef('jsonb_table_view_mixed_plan'::regclass) = + pg_get_viewdef('jsonb_table_view_mixed_plan2'::regclass) AS deparse_roundtrip_stable; + deparse_roundtrip_stable +-------------------------- + t +(1 row) + +-- PLAN DEFAULT with explicit non-default choices (INNER, UNION). The default +-- plan is normally implied by the NESTED structure and omitted, but INNER is +-- not the default parent/child join, so this deparses back as an explicit +-- PLAN with INNER joins over the (unnamed) generated path names. The INNER +-- join drops the "c"-less element (x = 2). +CREATE VIEW jsonb_table_view_default_plan AS +SELECT * FROM JSON_TABLE( + jsonb '{"a":[{"x":1,"c":[10]},{"x":2,"c":[]}]}', '$' + COLUMNS ( + NESTED PATH '$.a[*]' COLUMNS ( + x int PATH '$.x', + NESTED PATH '$.c[*]' COLUMNS (c int PATH '$') + ) + ) + PLAN DEFAULT (INNER, UNION) +); +SELECT pg_get_viewdef('jsonb_table_view_default_plan'::regclass); + pg_get_viewdef +------------------------------------------------------------------------------------------------ + SELECT x, + + c + + FROM JSON_TABLE( + + '{"a": [{"c": [10], "x": 1}, {"c": [], "x": 2}]}'::jsonb, '$' AS json_table_path_0+ + COLUMNS ( + + NESTED PATH '$."a"[*]' AS json_table_path_1 + + COLUMNS ( + + x integer PATH '$."x"', + + NESTED PATH '$."c"[*]' AS json_table_path_2 + + COLUMNS ( + + c integer PATH '$' + + ) + + ) + + ) + + PLAN (json_table_path_0 INNER (json_table_path_1 INNER json_table_path_2)) + + ); +(1 row) + +SELECT * FROM jsonb_table_view_default_plan ORDER BY x, c; + x | c +---+---- + 1 | 10 +(1 row) + +-- User path/column names deliberately colliding with the generated +-- "json_table_path_N" pattern, mixed with unnamed paths. A generated name +-- must skip any name already used, and the unnamed row-pattern (root) path is +-- named only after the user-supplied names are collected, so none of the +-- columns is silently dropped. All of json_table_path_0 (a column), +-- json_table_path_1 (a NESTED path) and the unnamed sibling path's rows must +-- appear. +CREATE VIEW jsonb_table_view_name_collision AS +SELECT * FROM JSON_TABLE( + jsonb '{"json_table_path_1":[{"v":11},{"v":12}],"y":[{"w":21}]}', '$' + COLUMNS ( + json_table_path_0 int PATH '$.nosuch' DEFAULT -1 ON EMPTY, + NESTED PATH '$.json_table_path_1[*]' AS json_table_path_1 + COLUMNS (v int PATH '$.v'), + NESTED PATH '$.y[*]' COLUMNS (w int PATH '$.w') + ) +); +SELECT pg_get_viewdef('jsonb_table_view_name_collision'::regclass); + pg_get_viewdef +---------------------------------------------------------------------------------------------------------------- + SELECT json_table_path_0, + + v, + + w + + FROM JSON_TABLE( + + '{"y": [{"w": 21}], "json_table_path_1": [{"v": 11}, {"v": 12}]}'::jsonb, '$' AS json_table_path_2+ + COLUMNS ( + + json_table_path_0 integer PATH '$."nosuch"' DEFAULT '-1'::integer ON EMPTY, + + NESTED PATH '$."json_table_path_1"[*]' AS json_table_path_1 + + COLUMNS ( + + v integer PATH '$."v"' + + ), + + NESTED PATH '$."y"[*]' AS json_table_path_3 + + COLUMNS ( + + w integer PATH '$."w"' + + ) + + ) + + ); +(1 row) + +SELECT * FROM jsonb_table_view_name_collision ORDER BY v, w; + json_table_path_0 | v | w +-------------------+----+---- + -1 | 11 | + -1 | 12 | + -1 | | 21 +(3 rows) + +-- Per-column and table-level ON ERROR / ON EMPTY under an explicit PLAN. A +-- non-default per-column ON EMPTY (and EXISTS ... ON ERROR) must be preserved +-- verbatim through the deparse, independently of the table-level ERROR ON +-- ERROR (which does not cascade to the columns). +CREATE VIEW jsonb_table_view_on_behavior AS +SELECT * FROM JSON_TABLE( + jsonb '{"a":[{"n":5}]}', '$' AS p0 + COLUMNS ( + a int PATH '$.nosuch' DEFAULT -7 ON EMPTY, + e bool EXISTS PATH '$.nosuch' FALSE ON ERROR, + NESTED PATH '$.a[*]' AS p1 COLUMNS ( + n int PATH '$.n' ERROR ON EMPTY + ) + ) + PLAN (p0 OUTER p1) + ERROR ON ERROR +); +SELECT pg_get_viewdef('jsonb_table_view_on_behavior'::regclass); + pg_get_viewdef +----------------------------------------------------------------------------- + SELECT a, + + e, + + n + + FROM JSON_TABLE( + + '{"a": [{"n": 5}]}'::jsonb, '$' AS p0 + + COLUMNS ( + + a integer PATH '$."nosuch"' DEFAULT '-7'::integer ON EMPTY,+ + e boolean EXISTS PATH '$."nosuch"', + + NESTED PATH '$."a"[*]' AS p1 + + COLUMNS ( + + n integer PATH '$."n"' ERROR ON EMPTY + + ) + + ) ERROR ON ERROR + + ); +(1 row) + +SELECT * FROM jsonb_table_view_on_behavior ORDER BY n; + a | e | n +----+---+--- + -7 | f | 5 +(1 row) + diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql index 2a33aaec57fe7..fa7e5977bb3bf 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -1052,3 +1052,144 @@ SELECT * FROM JSON_TABLE(jsonb '{}', '$' AS p0 \sv json_table_view_on_empty; DROP VIEW json_table_view_on_empty; + +-- ========================================================================== +-- Persisted JSON_TABLE views for dump/restore round-trip coverage +-- +-- The views above are all dropped, so their deparsed definitions are never +-- carried through a dump/restore (or pg_upgrade) cycle. The views below are +-- deliberately left in place at the end of this file so that +-- pg_upgrade's dump comparison round-trips them, pinning the backward +-- parsing of the trickier JSON_TABLE PLAN constructs: nested parent/child +-- (OUTER/INNER) joins that must stay parenthesized, sibling UNION/CROSS +-- joins, PLAN DEFAULT, generated-vs-user path-name collisions, and per-column +-- and table-level ON ERROR/ON EMPTY. Each view is accompanied by its pinned +-- pg_get_viewdef() output and a SELECT that checks the resulting rows, so a +-- regression that silently drops a column or mangles a plan is caught here. +-- ========================================================================== + +-- Nested parent/child plan two levels deep, with an explicit parenthesized +-- child join: PLAN (p0 OUTER (p1 INNER p11)). The parentheses around +-- (p1 INNER p11) must survive deparsing, otherwise the plan reparses to a +-- different tree (a dump/restore hazard). The INNER join means array +-- elements of "a" whose "b" is empty (x = 2) are dropped from the output, +-- while the OUTER-joined parent still contributes its other rows. +CREATE VIEW jsonb_table_view_pc_plan AS +SELECT * FROM JSON_TABLE( + jsonb '{"a":[{"x":1,"b":[10,20]},{"x":2,"b":[]},{"x":3,"b":[30]}]}', '$' AS p0 + COLUMNS ( + NESTED PATH '$.a[*]' AS p1 COLUMNS ( + x int PATH '$.x', + NESTED PATH '$.b[*]' AS p11 COLUMNS (b int PATH '$') + ) + ) + PLAN (p0 OUTER (p1 INNER p11)) +); +SELECT pg_get_viewdef('jsonb_table_view_pc_plan'::regclass); +SELECT * FROM jsonb_table_view_pc_plan ORDER BY x, b; + +-- Sibling paths joined with an explicit CROSS (not the default UNION), under +-- a parent/child OUTER join. CROSS produces the cartesian product of the two +-- sibling arrays, so the deparsed PLAN must keep CROSS and the parentheses. +CREATE VIEW jsonb_table_view_sibling_plan AS +SELECT * FROM JSON_TABLE( + jsonb '{"a":[1,2],"b":["x","y"]}', '$' AS p0 + COLUMNS ( + NESTED PATH '$.a[*]' AS pa COLUMNS (a int PATH '$'), + NESTED PATH '$.b[*]' AS pb COLUMNS (b text PATH '$') + ) + PLAN (p0 OUTER (pa CROSS pb)) +); +SELECT pg_get_viewdef('jsonb_table_view_sibling_plan'::regclass); +SELECT * FROM jsonb_table_view_sibling_plan ORDER BY a, b; + +-- The trickiest shape: a parent OUTER-joined to a CROSS of two sibling +-- parent/child subplans, each of which is itself a parent/child join that +-- must stay parenthesized: PLAN (p OUTER ((pb INNER pb1) CROSS (pc OUTER pc1))). +-- Exercises both the "parenthesize a parent/child child" fix and the sibling +-- operand parenthesization together. +CREATE VIEW jsonb_table_view_mixed_plan AS +SELECT * FROM JSON_TABLE( + jsonb '{"b":[{"b1":[1,2]}],"c":[{"c1":[3]},{"c1":[]}]}', '$' AS p + COLUMNS ( + NESTED PATH '$.b[*]' AS pb COLUMNS ( + NESTED PATH '$.b1[*]' AS pb1 COLUMNS (b1 int PATH '$') + ), + NESTED PATH '$.c[*]' AS pc COLUMNS ( + NESTED PATH '$.c1[*]' AS pc1 COLUMNS (c1 int PATH '$') + ) + ) + PLAN (p OUTER ((pb INNER pb1) CROSS (pc OUTER pc1))) +); +SELECT pg_get_viewdef('jsonb_table_view_mixed_plan'::regclass); +SELECT * FROM jsonb_table_view_mixed_plan ORDER BY b1, c1; + +-- Deparse-reparse round-trip stability check: recreate the trickiest view +-- from its own deparsed definition and confirm the deparse is a fixed point +-- (the second view's definition is byte-for-byte identical). This proves the +-- PLAN clause parses back to the same tree without relying on an external +-- dump/restore. +SELECT format('CREATE VIEW jsonb_table_view_mixed_plan2 AS %s', + pg_get_viewdef('jsonb_table_view_mixed_plan'::regclass)) +\gexec +SELECT pg_get_viewdef('jsonb_table_view_mixed_plan'::regclass) = + pg_get_viewdef('jsonb_table_view_mixed_plan2'::regclass) AS deparse_roundtrip_stable; + +-- PLAN DEFAULT with explicit non-default choices (INNER, UNION). The default +-- plan is normally implied by the NESTED structure and omitted, but INNER is +-- not the default parent/child join, so this deparses back as an explicit +-- PLAN with INNER joins over the (unnamed) generated path names. The INNER +-- join drops the "c"-less element (x = 2). +CREATE VIEW jsonb_table_view_default_plan AS +SELECT * FROM JSON_TABLE( + jsonb '{"a":[{"x":1,"c":[10]},{"x":2,"c":[]}]}', '$' + COLUMNS ( + NESTED PATH '$.a[*]' COLUMNS ( + x int PATH '$.x', + NESTED PATH '$.c[*]' COLUMNS (c int PATH '$') + ) + ) + PLAN DEFAULT (INNER, UNION) +); +SELECT pg_get_viewdef('jsonb_table_view_default_plan'::regclass); +SELECT * FROM jsonb_table_view_default_plan ORDER BY x, c; + +-- User path/column names deliberately colliding with the generated +-- "json_table_path_N" pattern, mixed with unnamed paths. A generated name +-- must skip any name already used, and the unnamed row-pattern (root) path is +-- named only after the user-supplied names are collected, so none of the +-- columns is silently dropped. All of json_table_path_0 (a column), +-- json_table_path_1 (a NESTED path) and the unnamed sibling path's rows must +-- appear. +CREATE VIEW jsonb_table_view_name_collision AS +SELECT * FROM JSON_TABLE( + jsonb '{"json_table_path_1":[{"v":11},{"v":12}],"y":[{"w":21}]}', '$' + COLUMNS ( + json_table_path_0 int PATH '$.nosuch' DEFAULT -1 ON EMPTY, + NESTED PATH '$.json_table_path_1[*]' AS json_table_path_1 + COLUMNS (v int PATH '$.v'), + NESTED PATH '$.y[*]' COLUMNS (w int PATH '$.w') + ) +); +SELECT pg_get_viewdef('jsonb_table_view_name_collision'::regclass); +SELECT * FROM jsonb_table_view_name_collision ORDER BY v, w; + +-- Per-column and table-level ON ERROR / ON EMPTY under an explicit PLAN. A +-- non-default per-column ON EMPTY (and EXISTS ... ON ERROR) must be preserved +-- verbatim through the deparse, independently of the table-level ERROR ON +-- ERROR (which does not cascade to the columns). +CREATE VIEW jsonb_table_view_on_behavior AS +SELECT * FROM JSON_TABLE( + jsonb '{"a":[{"n":5}]}', '$' AS p0 + COLUMNS ( + a int PATH '$.nosuch' DEFAULT -7 ON EMPTY, + e bool EXISTS PATH '$.nosuch' FALSE ON ERROR, + NESTED PATH '$.a[*]' AS p1 COLUMNS ( + n int PATH '$.n' ERROR ON EMPTY + ) + ) + PLAN (p0 OUTER p1) + ERROR ON ERROR +); +SELECT pg_get_viewdef('jsonb_table_view_on_behavior'::regclass); +SELECT * FROM jsonb_table_view_on_behavior ORDER BY n;