Skip to content

Commit 97d6716

Browse files
committed
Fix failure for generated column with a not-null domain constraint.
If a GENERATED column is declared to have a domain data type where the domain's constraints disallow null values, INSERT commands failed because we built a targetlist that included coercing a null constant to the domain's type. The failure occurred even when the generated value would have been perfectly OK. This is adjacent to the issues fixed in 0da39aa, but we didn't notice for lack of testing a domain with such a constraint. We aren't going to use the result of the targetlist entry for the generated column --- ExecComputeStoredGenerated will overwrite it. So it's not really necessary that it have the exact datatype of the generated column. This patch fixes the problem by changing the targetlist entry to be a null Const of the domain's base type, which should be sufficiently legal. (We do have to tweak ExecCheckPlanOutput to accept the situation, though.) This has been broken since we implemented generated columns. However, this patch only applies easily as far back as v14, partly because I (tgl) only carried 0da39aa back that far, but mostly because v14 significantly refactored the handling of INSERT/UPDATE targetlists. Given the lack of field complaints and the short remaining support lifetime of v13, I judge the cost-benefit ratio not good for devising a version that would work in v13. Reported-by: jian he <jian.universality@gmail.com> Author: jian he <jian.universality@gmail.com> Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us> Discussion: https://postgr.es/m/CACJufxG59tip2+9h=rEv-ykOFjt0cbsPVchhi0RTij8bABBA0Q@mail.gmail.com Backpatch-through: 14
1 parent 9a8c16a commit 97d6716

File tree

4 files changed

+80
-27
lines changed

4 files changed

+80
-27
lines changed

src/backend/executor/nodeModifyTable.c

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -207,33 +207,53 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
207207
attr = TupleDescAttr(resultDesc, attno);
208208
attno++;
209209

210-
if (!attr->attisdropped)
210+
/*
211+
* Special cases here should match planner's expand_insert_targetlist.
212+
*/
213+
if (attr->attisdropped)
211214
{
212-
/* Normal case: demand type match */
213-
if (exprType((Node *) tle->expr) != attr->atttypid)
215+
/*
216+
* For a dropped column, we can't check atttypid (it's likely 0).
217+
* In any case the planner has most likely inserted an INT4 null.
218+
* What we insist on is just *some* NULL constant.
219+
*/
220+
if (!IsA(tle->expr, Const) ||
221+
!((Const *) tle->expr)->constisnull)
214222
ereport(ERROR,
215223
(errcode(ERRCODE_DATATYPE_MISMATCH),
216224
errmsg("table row type and query-specified row type do not match"),
217-
errdetail("Table has type %s at ordinal position %d, but query expects %s.",
218-
format_type_be(attr->atttypid),
219-
attno,
220-
format_type_be(exprType((Node *) tle->expr)))));
225+
errdetail("Query provides a value for a dropped column at ordinal position %d.",
226+
attno)));
221227
}
222-
else
228+
else if (attr->attgenerated)
223229
{
224230
/*
225-
* For a dropped column, we can't check atttypid (it's likely 0).
226-
* In any case the planner has most likely inserted an INT4 null.
227-
* What we insist on is just *some* NULL constant.
231+
* For a generated column, the planner will have inserted a null
232+
* of the column's base type (to avoid possibly failing on domain
233+
* not-null constraints). It doesn't seem worth insisting on that
234+
* exact type though, since a null value is type-independent. As
235+
* above, just insist on *some* NULL constant.
228236
*/
229237
if (!IsA(tle->expr, Const) ||
230238
!((Const *) tle->expr)->constisnull)
231239
ereport(ERROR,
232240
(errcode(ERRCODE_DATATYPE_MISMATCH),
233241
errmsg("table row type and query-specified row type do not match"),
234-
errdetail("Query provides a value for a dropped column at ordinal position %d.",
242+
errdetail("Query provides a value for a generated column at ordinal position %d.",
235243
attno)));
236244
}
245+
else
246+
{
247+
/* Normal case: demand type match */
248+
if (exprType((Node *) tle->expr) != attr->atttypid)
249+
ereport(ERROR,
250+
(errcode(ERRCODE_DATATYPE_MISMATCH),
251+
errmsg("table row type and query-specified row type do not match"),
252+
errdetail("Table has type %s at ordinal position %d, but query expects %s.",
253+
format_type_be(attr->atttypid),
254+
attno,
255+
format_type_be(exprType((Node *) tle->expr)))));
256+
}
237257
}
238258
if (attno != resultDesc->natts)
239259
ereport(ERROR,

src/backend/optimizer/prep/preptlist.c

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include "optimizer/tlist.h"
4545
#include "parser/parse_coerce.h"
4646
#include "parser/parsetree.h"
47+
#include "utils/lsyscache.h"
4748
#include "utils/rel.h"
4849

4950
static List *expand_insert_targetlist(PlannerInfo *root, List *tlist,
@@ -395,9 +396,8 @@ expand_insert_targetlist(PlannerInfo *root, List *tlist, Relation rel)
395396
*
396397
* INSERTs should insert NULL in this case. (We assume the
397398
* rewriter would have inserted any available non-NULL default
398-
* value.) Also, if the column isn't dropped, apply any domain
399-
* constraints that might exist --- this is to catch domain NOT
400-
* NULL.
399+
* value.) Also, normally we must apply any domain constraints
400+
* that might exist --- this is to catch domain NOT NULL.
401401
*
402402
* When generating a NULL constant for a dropped column, we label
403403
* it INT4 (any other guaranteed-to-exist datatype would do as
@@ -407,21 +407,17 @@ expand_insert_targetlist(PlannerInfo *root, List *tlist, Relation rel)
407407
* representation is datatype-independent. This could perhaps
408408
* confuse code comparing the finished plan to the target
409409
* relation, however.
410+
*
411+
* Another exception is that if the column is generated, the value
412+
* we produce here will be ignored, and we don't want to risk
413+
* throwing an error. So in that case we *don't* want to apply
414+
* domain constraints, so we must produce a NULL of the base type.
415+
* Again, code comparing the finished plan to the target relation
416+
* must account for this.
410417
*/
411418
Node *new_expr;
412419

413-
if (!att_tup->attisdropped)
414-
{
415-
new_expr = coerce_null_to_domain(att_tup->atttypid,
416-
att_tup->atttypmod,
417-
att_tup->attcollation,
418-
att_tup->attlen,
419-
att_tup->attbyval);
420-
/* Must run expression preprocessing on any non-const nodes */
421-
if (!IsA(new_expr, Const))
422-
new_expr = eval_const_expressions(root, new_expr);
423-
}
424-
else
420+
if (att_tup->attisdropped)
425421
{
426422
/* Insert NULL for dropped column */
427423
new_expr = (Node *) makeConst(INT4OID,
@@ -432,6 +428,33 @@ expand_insert_targetlist(PlannerInfo *root, List *tlist, Relation rel)
432428
true, /* isnull */
433429
true /* byval */ );
434430
}
431+
else if (att_tup->attgenerated)
432+
{
433+
/* Generated column, insert a NULL of the base type */
434+
Oid baseTypeId = att_tup->atttypid;
435+
int32 baseTypeMod = att_tup->atttypmod;
436+
437+
baseTypeId = getBaseTypeAndTypmod(baseTypeId, &baseTypeMod);
438+
new_expr = (Node *) makeConst(baseTypeId,
439+
baseTypeMod,
440+
att_tup->attcollation,
441+
att_tup->attlen,
442+
(Datum) 0,
443+
true, /* isnull */
444+
att_tup->attbyval);
445+
}
446+
else
447+
{
448+
/* Normal column, insert a NULL of the column datatype */
449+
new_expr = coerce_null_to_domain(att_tup->atttypid,
450+
att_tup->atttypmod,
451+
att_tup->attcollation,
452+
att_tup->attlen,
453+
att_tup->attbyval);
454+
/* Must run expression preprocessing on any non-const nodes */
455+
if (!IsA(new_expr, Const))
456+
new_expr = eval_const_expressions(root, new_expr);
457+
}
435458

436459
new_tle = makeTargetEntry((Expr *) new_expr,
437460
attrno,

src/test/regress/expected/generated.out

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,11 @@ CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a *
714714
INSERT INTO gtest24 (a) VALUES (4); -- ok
715715
INSERT INTO gtest24 (a) VALUES (6); -- error
716716
ERROR: value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
717+
CREATE DOMAIN gtestdomainnn AS int CHECK (VALUE IS NOT NULL);
718+
CREATE TABLE gtest24nn (a int, b gtestdomainnn GENERATED ALWAYS AS (a * 2) STORED);
719+
INSERT INTO gtest24nn (a) VALUES (4); -- ok
720+
INSERT INTO gtest24nn (a) VALUES (NULL); -- error
721+
ERROR: value for domain gtestdomainnn violates check constraint "gtestdomainnn_check"
717722
-- typed tables (currently not supported)
718723
CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
719724
CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) STORED);

src/test/regress/sql/generated.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,11 @@ CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a *
378378
INSERT INTO gtest24 (a) VALUES (4); -- ok
379379
INSERT INTO gtest24 (a) VALUES (6); -- error
380380

381+
CREATE DOMAIN gtestdomainnn AS int CHECK (VALUE IS NOT NULL);
382+
CREATE TABLE gtest24nn (a int, b gtestdomainnn GENERATED ALWAYS AS (a * 2) STORED);
383+
INSERT INTO gtest24nn (a) VALUES (4); -- ok
384+
INSERT INTO gtest24nn (a) VALUES (NULL); -- error
385+
381386
-- typed tables (currently not supported)
382387
CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
383388
CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) STORED);

0 commit comments

Comments
 (0)