diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fbd0ebbf10f..f9b0a43ad59 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -382,7 +382,9 @@ CREATE TABLE people ( A generated column cannot be written to directly. In INSERT or UPDATE commands, a value cannot be specified for a generated column, but the keyword - DEFAULT may be specified. + DEFAULT may be specified. Also, a generated column + cannot be used in the FOR PORTION OF clause of an + UPDATE or DELETE command. diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index cac2ebb86c6..3225185d16f 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -848,6 +848,32 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name, */ transform_MERGE_to_join(parse); + /* + * Reject FOR PORTION OF on a generated column. We can't write to a + * virtual generated column, and a stored generated column should be + * written by its own expression. + * + * We do this in the planner rather than parse analysis so that updatable + * views have been rewritten; otherwise they would mask which columns are + * generated. We need to check before preprocess_relation_rtes(), so that + * for virtual generated columns we still have the rangeVar. After that + * it is replaced by the column's expression. + * + * XXX: We plan to implement PERIODs as stored generated columns, so later + * we will loosen this restriction if the column belongs to a PERIOD. + */ + if (parse->forPortionOf) + { + ForPortionOfExpr *forPortionOf = parse->forPortionOf; + RangeTblEntry *rte = rt_fetch(parse->resultRelation, parse->rtable); + + if (get_attgenerated(rte->relid, forPortionOf->rangeVar->varattno)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use generated column \"%s\" in FOR PORTION OF", + forPortionOf->range_name))); + } + /* * Scan the rangetable for relation RTEs and retrieve the necessary * catalog information for each relation. Using this information, clear diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index 207e370627e..a050fc2dadf 100644 --- a/src/test/regress/expected/for_portion_of.out +++ b/src/test/regress/expected/for_portion_of.out @@ -2208,6 +2208,79 @@ SELECT * FROM fpo_rule ORDER BY f1; (2 rows) DROP TABLE fpo_rule; +-- UPDATE/DELETE FOR PORTION OF on a GENERATED VIRTUAL range column: +CREATE TABLE fpo_gen_virtual ( + a int, + b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) VIRTUAL +); +INSERT INTO fpo_gen_virtual VALUES (1); +DELETE FROM fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2; -- fails +ERROR: cannot use generated column "b" in FOR PORTION OF +UPDATE fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails +ERROR: column "b" can only be updated to DEFAULT +DETAIL: Column "b" is a generated column. +DROP TABLE fpo_gen_virtual; +-- UPDATE/DELETE FOR PORTION OF on a GENERATED STORED range column: +CREATE TABLE fpo_gen_stored ( + a int, + b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED +); +INSERT INTO fpo_gen_stored VALUES (1); +DELETE FROM fpo_gen_stored FOR PORTION OF b FROM 1 TO 2; -- fails +ERROR: cannot use generated column "b" in FOR PORTION OF +UPDATE fpo_gen_stored FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails +ERROR: column "b" can only be updated to DEFAULT +DETAIL: Column "b" is a generated column. +DROP TABLE fpo_gen_stored; +-- FOR PORTION OF a generated column reached through an updatable view. +-- The view hides that b is generated during parse analysis, so the check +-- must happen later (in the planner), after the view is rewritten to its +-- underlying table. +CREATE TABLE fpo_gen_view ( + a int, + b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED +); +INSERT INTO fpo_gen_view VALUES (1); +CREATE VIEW fpo_gen_view_v AS SELECT * FROM fpo_gen_view; +DELETE FROM fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2; -- fails +ERROR: cannot use generated column "b" in FOR PORTION OF +UPDATE fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails +ERROR: column "b" can only be updated to DEFAULT +DETAIL: Column "b" is a generated column. +DROP VIEW fpo_gen_view_v; +DROP TABLE fpo_gen_view; +-- A new-style SQL function is parsed at CREATE FUNCTION time, but our +-- generated-column check is in the planner, so it sees the column's +-- current attgenerated when the function's plan is built at run time. +CREATE TABLE fpo_func_test ( + a int, + b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED +); +INSERT INTO fpo_func_test VALUES (1); +-- Definition succeeds even though b is a generated column today. +CREATE FUNCTION fpo_delete() RETURNS void + LANGUAGE SQL + BEGIN ATOMIC + DELETE FROM fpo_func_test FOR PORTION OF b FROM 1 TO 2; + END; +SELECT fpo_delete(); -- fails: b is generated +ERROR: cannot use generated column "b" in FOR PORTION OF +CONTEXT: SQL function "fpo_delete" statement 1 +-- Drop the generation expression and the same function now succeeds. +ALTER TABLE fpo_func_test ALTER COLUMN b DROP EXPRESSION; +SELECT fpo_delete(); + fpo_delete +------------ + +(1 row) + +TABLE fpo_func_test ORDER BY a, b; + a | b +---+--- +(0 rows) + +DROP FUNCTION fpo_delete(); +DROP TABLE fpo_func_test; -- UPDATE/DELETE FOR PORTION OF with table inheritance -- Leftover rows must stay in the child table, preserving child-specific columns. CREATE TABLE fpo_inh_parent ( diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index a3c41abf7b7..a1ee1d5e501 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -1448,6 +1448,63 @@ SELECT * FROM fpo_rule ORDER BY f1; DROP TABLE fpo_rule; +-- UPDATE/DELETE FOR PORTION OF on a GENERATED VIRTUAL range column: +CREATE TABLE fpo_gen_virtual ( + a int, + b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) VIRTUAL +); +INSERT INTO fpo_gen_virtual VALUES (1); +DELETE FROM fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2; -- fails +UPDATE fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails +DROP TABLE fpo_gen_virtual; + +-- UPDATE/DELETE FOR PORTION OF on a GENERATED STORED range column: +CREATE TABLE fpo_gen_stored ( + a int, + b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED +); +INSERT INTO fpo_gen_stored VALUES (1); +DELETE FROM fpo_gen_stored FOR PORTION OF b FROM 1 TO 2; -- fails +UPDATE fpo_gen_stored FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails +DROP TABLE fpo_gen_stored; + +-- FOR PORTION OF a generated column reached through an updatable view. +-- The view hides that b is generated during parse analysis, so the check +-- must happen later (in the planner), after the view is rewritten to its +-- underlying table. +CREATE TABLE fpo_gen_view ( + a int, + b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED +); +INSERT INTO fpo_gen_view VALUES (1); +CREATE VIEW fpo_gen_view_v AS SELECT * FROM fpo_gen_view; +DELETE FROM fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2; -- fails +UPDATE fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails +DROP VIEW fpo_gen_view_v; +DROP TABLE fpo_gen_view; + +-- A new-style SQL function is parsed at CREATE FUNCTION time, but our +-- generated-column check is in the planner, so it sees the column's +-- current attgenerated when the function's plan is built at run time. +CREATE TABLE fpo_func_test ( + a int, + b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED +); +INSERT INTO fpo_func_test VALUES (1); +-- Definition succeeds even though b is a generated column today. +CREATE FUNCTION fpo_delete() RETURNS void + LANGUAGE SQL + BEGIN ATOMIC + DELETE FROM fpo_func_test FOR PORTION OF b FROM 1 TO 2; + END; +SELECT fpo_delete(); -- fails: b is generated +-- Drop the generation expression and the same function now succeeds. +ALTER TABLE fpo_func_test ALTER COLUMN b DROP EXPRESSION; +SELECT fpo_delete(); +TABLE fpo_func_test ORDER BY a, b; +DROP FUNCTION fpo_delete(); +DROP TABLE fpo_func_test; + -- UPDATE/DELETE FOR PORTION OF with table inheritance -- Leftover rows must stay in the child table, preserving child-specific columns. CREATE TABLE fpo_inh_parent (