hi. new patch attached.
rewriteTargetListIU, expand_insert_targetlist these two places can make a null Const TargetEntry for the generated column in an INSERT operation. but since this problem only occurs in INSERT, so i placed the logic within expand_insert_targetlist would be appropriate? The following are excerpts of the commit message. -------------------------------- create domain d3 as int check (value is not null); create table t0(b int, a d3 GENERATED ALWAYS as (b + 11) stored); insert into t0 values (1, default); ERROR: value for domain d3 violates check constraint "d3_check" explain(costs off, verbose) insert into t0 values (1, default); QUERY PLAN --------------------------------------- Insert on public.t0 -> Result Output: 1, NULL::integer For INSERT operation, for Query->targetList, we should not make a generated column over domain with constraint to a CoerceToDomain node, instead, we make it as a simple null Const over domain's base type. When a column is a generated column in an INSERT, expand_insert_targetlist should unconditionally generate a null Const to be inserted. If we are not doing this way, we might end up wrapping the null Const in a CoerceToDomain node, which may trigger runtime error earlier if the domain has a NOT NULL constraint. That's not fine, as generated columns are already handled in ExecComputeStoredGenerated. --------------------------------
From 610ffedb4ee55207489397ad139a712c54dcdb1a Mon Sep 17 00:00:00 2001 From: jian he <jian.universal...@gmail.com> Date: Thu, 10 Apr 2025 16:47:37 +0800 Subject: [PATCH v3 1/1] fix INSERT generated column over domain with constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create domain d3 as int check (value is not null); create table t0(b int, a d3 GENERATED ALWAYS as (b + 11) stored); insert into t0 values (1, default); ERROR: value for domain d3 violates check constraint "d3_check" explain(costs off, verbose) insert into t0 values (1, default); QUERY PLAN --------------------------------------- Insert on public.t0 -> Result Output: 1, NULL::integer For INSERT operation, for Query->targetList, we should not make a generated column over domain with constraint to a CoerceToDomain node, instead, we make it as a simple null Const over domain's base type. When a column is a generated column in an INSERT, expand_insert_targetlist should unconditionally generate a null Const to be inserted. If we are not doing this way, we might end up wrapping the null Const in a CoerceToDomain node, which may trigger runtime error earlier if the domain has a NOT NULL constraint. That's not fine, as generated columns are already handled in ExecComputeStoredGenerated. context: https://git.postgresql.org/cgit/postgresql.git/commit/?id=0da39aa7667b06e16189d318f7850d559d446d52 discussion: https://postgr.es/m/CACJufxG59tip2+9h=rev-ykofjt0cbspvchhi0rtij8babb...@mail.gmail.com --- src/backend/executor/nodeModifyTable.c | 36 ++++++++++++++---- src/backend/optimizer/prep/preptlist.c | 37 ++++++++++++++++++- .../regress/expected/generated_stored.out | 32 ++++++++++++++++ src/test/regress/sql/generated_stored.sql | 15 ++++++++ 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 309e27f8b5f..5bd80456c3e 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -67,6 +67,7 @@ #include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/snapmgr.h" @@ -216,13 +217,34 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList) { /* Normal case: demand type match */ if (exprType((Node *) tle->expr) != attr->atttypid) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table row type and query-specified row type do not match"), - errdetail("Table has type %s at ordinal position %d, but query expects %s.", - format_type_be(attr->atttypid), - attno, - format_type_be(exprType((Node *) tle->expr))))); + { + if (attr->attgenerated == '\0') + { + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table row type and query-specified row type do not match"), + errdetail("Table has type %s at ordinal position %d, but query expects %s.", + format_type_be(attr->atttypid), + attno, + format_type_be(exprType((Node *) tle->expr)))); + } + else + { + /* see rewriteTargetListIU comments for domain over generated column */ + Oid baseTypeId; + int32 baseTypeMod = attr->atttypmod; + baseTypeId = getBaseTypeAndTypmod(attr->atttypid, &baseTypeMod); + + if (exprType((Node *) tle->expr) != baseTypeId) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table row type and query-specified row type do not match"), + errdetail("Table has type %s at ordinal position %d, but query expects %s.", + format_type_be(attr->atttypid), + attno, + format_type_be(exprType((Node *) tle->expr)))); + } + } } else { diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c index c2a77503d4b..d28ea7ed071 100644 --- a/src/backend/optimizer/prep/preptlist.c +++ b/src/backend/optimizer/prep/preptlist.c @@ -44,6 +44,7 @@ #include "optimizer/tlist.h" #include "parser/parse_coerce.h" #include "parser/parsetree.h" +#include "utils/lsyscache.h" #include "utils/rel.h" static List *expand_insert_targetlist(PlannerInfo *root, List *tlist, @@ -434,7 +435,7 @@ expand_insert_targetlist(PlannerInfo *root, List *tlist, Relation rel) */ Node *new_expr; - if (!att_tup->attisdropped) + if (!att_tup->attisdropped && att_tup->attgenerated == '\0') { new_expr = coerce_null_to_domain(att_tup->atttypid, att_tup->atttypmod, @@ -445,6 +446,40 @@ expand_insert_targetlist(PlannerInfo *root, List *tlist, Relation rel) if (!IsA(new_expr, Const)) new_expr = eval_const_expressions(root, new_expr); } + else if (!att_tup->attisdropped && att_tup->attgenerated != '\0') + { + /* + * During an INSERT, we first process the target list (tlist) via + * Result node for projection and input datatype checks. Later we + * do evaluation of generation expression in + * ExecComputeStoredGenerated. The target list (tlist) entries may + * have generated column over on domain types. Normally, this works + * fine. + * + * However, if that domain type has a NOT NULL constraint or a + * CHECK constraint is logically equivalent to NOT NULL, the + * INSERT may throw run-time error earlier due to domain not-null + * violation. This happens because generated column entries in + * the target list may initialized as null Const warpped inside + * CoerceToDomain node, and we actually evaulate it before + * ExecComputeStoredGenerated. + * + * To avoid it, the generated column's TargetEntry is + * unconditionally set as a null Const, So no need to worry about + * run-time error while evaluating CoerceToDomain earlier. + */ + Oid baseTypeId; + int32 baseTypeMod = att_tup->atttypmod; + + baseTypeId = getBaseTypeAndTypmod(att_tup->atttypid, &baseTypeMod); + new_expr = (Node *) makeConst(baseTypeId, + baseTypeMod, + att_tup->attcollation, + att_tup->attlen, + (Datum) 0, + true, /* isnull */ + att_tup->attbyval); + } else { /* Insert NULL for dropped column */ diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index 8cccd1d7fe9..ffa844ca903 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -847,6 +847,38 @@ CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS A INSERT INTO gtest24r (a) VALUES (4); -- ok INSERT INTO gtest24r (a) VALUES (6); -- error ERROR: value for domain gtestdomain1 violates check constraint "gtestdomain1_check" +CREATE DOMAIN dnn AS int NOT NULL; +CREATE DOMAIN dnn_check AS int CHECK (value IS NOT NULL); +CREATE TABLE gtest24nn ( + a int, + b dnn GENERATED ALWAYS AS (a + 1) STORED, + c dnn GENERATED ALWAYS AS (11) STORED, + d dnn_check GENERATED ALWAYS AS (a + 11) STORED +); +EXPLAIN (COSTS OFF, VERBOSE) INSERT INTO gtest24nn VALUES (1, DEFAULT, DEFAULT, DEFAULT); --ok + QUERY PLAN +---------------------------------------------------------------- + Insert on generated_stored_tests.gtest24nn + -> Result + Output: 1, NULL::integer, NULL::integer, NULL::integer +(3 rows) + +INSERT INTO gtest24nn VALUES (NULL, DEFAULT, DEFAULT, DEFAULT); --error +ERROR: domain dnn does not allow null values +INSERT INTO gtest24nn VALUES (1, DEFAULT, DEFAULT, DEFAULT) RETURNING *; --ok + a | b | c | d +---+---+----+---- + 1 | 2 | 11 | 12 +(1 row) + +UPDATE gtest24nn SET b = DEFAULT, c = default, d = default, a = 2 WHERE a = 1 RETURNING *; --ok + a | b | c | d +---+---+----+---- + 2 | 3 | 11 | 13 +(1 row) + +UPDATE gtest24nn SET a = NULL WHERE a = 2 RETURNING *; --error +ERROR: domain dnn does not allow null values -- typed tables (currently not supported) CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint); CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) STORED); diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql index 50e94e5c673..ba8ae62dea0 100644 --- a/src/test/regress/sql/generated_stored.sql +++ b/src/test/regress/sql/generated_stored.sql @@ -419,6 +419,21 @@ CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS A INSERT INTO gtest24r (a) VALUES (4); -- ok INSERT INTO gtest24r (a) VALUES (6); -- error +CREATE DOMAIN dnn AS int NOT NULL; +CREATE DOMAIN dnn_check AS int CHECK (value IS NOT NULL); +CREATE TABLE gtest24nn ( + a int, + b dnn GENERATED ALWAYS AS (a + 1) STORED, + c dnn GENERATED ALWAYS AS (11) STORED, + d dnn_check GENERATED ALWAYS AS (a + 11) STORED +); + +EXPLAIN (COSTS OFF, VERBOSE) INSERT INTO gtest24nn VALUES (1, DEFAULT, DEFAULT, DEFAULT); --ok +INSERT INTO gtest24nn VALUES (NULL, DEFAULT, DEFAULT, DEFAULT); --error +INSERT INTO gtest24nn VALUES (1, DEFAULT, DEFAULT, DEFAULT) RETURNING *; --ok +UPDATE gtest24nn SET b = DEFAULT, c = default, d = default, a = 2 WHERE a = 1 RETURNING *; --ok +UPDATE gtest24nn SET a = NULL WHERE a = 2 RETURNING *; --error + -- typed tables (currently not supported) CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint); CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) STORED); -- 2.34.1