Skip to content

Commit 8bf6ec3

Browse files
committed
Improve handling of inherited GENERATED expressions.
In both partitioning and traditional inheritance, require child columns to be GENERATED if and only if their parent(s) are. Formerly we allowed the case of an inherited column being GENERATED when its parent isn't, but that results in inconsistent behavior: the column can be directly updated through an UPDATE on the parent table, leading to it containing a user-supplied value that might not match the generation expression. This also fixes an oversight that we enforced partition-key-columns-can't- be-GENERATED against parent tables, but not against child tables that were dynamically attached to them. Also, remove the restriction that the child's generation expression be equivalent to the parent's. In the wake of commit 3f7836f, there doesn't seem to be any reason that we need that restriction, since generation expressions are always computed per-table anyway. By removing this, we can also allow a child to merge multiple inheritance parents with inconsistent generation expressions, by overriding them with its own expression, much as we've long allowed for DEFAULT expressions. Since we're rejecting a case that we used to accept, this doesn't seem like a back-patchable change. Given the lack of field complaints about the inconsistent behavior, it's likely that no one is doing this anyway, but we won't change it in minor releases. Amit Langote and Tom Lane Discussion: https://postgr.es/m/2793383.1672944799@sss.pgh.pa.us
1 parent d0d9683 commit 8bf6ec3

File tree

7 files changed

+240
-196
lines changed

7 files changed

+240
-196
lines changed

doc/src/sgml/ddl.sgml

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -325,27 +325,54 @@ CREATE TABLE people (
325325
</para>
326326
</listitem>
327327
<listitem>
328-
<para>For inheritance:</para>
328+
<para>For inheritance and partitioning:</para>
329329
<itemizedlist>
330330
<listitem>
331331
<para>
332-
If a parent column is a generated column, a child column must also be
333-
a generated column using the same expression. In the definition of
334-
the child column, leave off the <literal>GENERATED</literal> clause,
335-
as it will be copied from the parent.
332+
If a parent column is a generated column, its child column must also
333+
be a generated column; however, the child column can have a
334+
different generation expression. The generation expression that is
335+
actually applied during insert or update of a row is the one
336+
associated with the table that the row is physically in.
337+
(This is unlike the behavior for column defaults: for those, the
338+
default value associated with the table named in the query applies.)
336339
</para>
337340
</listitem>
338341
<listitem>
339342
<para>
340-
In case of multiple inheritance, if one parent column is a generated
341-
column, then all parent columns must be generated columns and with the
342-
same expression.
343+
If a parent column is not a generated column, its child column must
344+
not be generated either.
345+
</para>
346+
</listitem>
347+
<listitem>
348+
<para>
349+
For inherited tables, if you write a child column definition without
350+
any <literal>GENERATED</literal> clause in <command>CREATE TABLE
351+
... INHERITS</command>, then its <literal>GENERATED</literal> clause
352+
will automatically be copied from the parent. <command>ALTER TABLE
353+
... INHERIT</command> will insist that parent and child columns
354+
already match as to generation status, but it will not require their
355+
generation expressions to match.
343356
</para>
344357
</listitem>
345358
<listitem>
346359
<para>
347-
If a parent column is not a generated column, a child column may be
348-
defined to be a generated column or not.
360+
Similarly for partitioned tables, if you write a child column
361+
definition without any <literal>GENERATED</literal> clause
362+
in <command>CREATE TABLE ... PARTITION OF</command>, then
363+
its <literal>GENERATED</literal> clause will automatically be copied
364+
from the parent. <command>ALTER TABLE ... ADD PARTITION</command>
365+
will insist that parent and child columns already match as to
366+
generation status, but it will not require their generation
367+
expressions to match.
368+
</para>
369+
</listitem>
370+
<listitem>
371+
<para>
372+
In case of multiple inheritance, if one parent column is a generated
373+
column, then all parent columns must be generated columns. If they
374+
do not all have the same generation expression, then the desired
375+
expression for the child must be specified explicitly.
349376
</para>
350377
</listitem>
351378
</itemizedlist>

src/backend/commands/tablecmds.c

Lines changed: 46 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2320,8 +2320,12 @@ storage_name(char c)
23202320
* (3) If conflicting defaults are inherited from different parents
23212321
* (and not overridden by the child), an error is raised.
23222322
* (4) Otherwise the inherited default is used.
2323-
* Rule (3) is new in Postgres 7.1; in earlier releases you got a
2324-
* rather arbitrary choice of which parent default to use.
2323+
*
2324+
* Note that the default-value infrastructure is used for generated
2325+
* columns' expressions too, so most of the preceding paragraph applies
2326+
* to generation expressions too. We insist that a child column be
2327+
* generated if and only if its parent(s) are, but it need not have
2328+
* the same generation expression.
23252329
*----------
23262330
*/
23272331
static List *
@@ -2659,7 +2663,7 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
26592663
}
26602664

26612665
/*
2662-
* Locate default if any
2666+
* Locate default/generation expression if any
26632667
*/
26642668
if (attribute->atthasdef)
26652669
{
@@ -2923,23 +2927,20 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
29232927
/*
29242928
* Check for conflicts related to generated columns.
29252929
*
2926-
* If the parent column is generated, the child column must be
2927-
* unadorned and will be made a generated column. (We could
2928-
* in theory allow the child column definition specifying the
2929-
* exact same generation expression, but that's a bit
2930-
* complicated to implement and doesn't seem very useful.) We
2931-
* also check that the child column doesn't specify a default
2932-
* value or identity, which matches the rules for a single
2933-
* column in parse_util.c.
2930+
* If the parent column is generated, the child column will be
2931+
* made a generated column if it isn't already. If it is a
2932+
* generated column, we'll take its generation expression in
2933+
* preference to the parent's. We must check that the child
2934+
* column doesn't specify a default value or identity, which
2935+
* matches the rules for a single column in parse_util.c.
2936+
*
2937+
* Conversely, if the parent column is not generated, the
2938+
* child column can't be either. (We used to allow that, but
2939+
* it results in being able to override the generation
2940+
* expression via UPDATEs through the parent.)
29342941
*/
29352942
if (def->generated)
29362943
{
2937-
if (newdef->generated)
2938-
ereport(ERROR,
2939-
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
2940-
errmsg("child column \"%s\" specifies generation expression",
2941-
def->colname),
2942-
errhint("Omit the generation expression in the definition of the child table column to inherit the generation expression from the parent table.")));
29432944
if (newdef->raw_default && !newdef->generated)
29442945
ereport(ERROR,
29452946
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
@@ -2951,15 +2952,14 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
29512952
errmsg("column \"%s\" inherits from generated column but specifies identity",
29522953
def->colname)));
29532954
}
2954-
2955-
/*
2956-
* If the parent column is not generated, then take whatever
2957-
* the child column definition says.
2958-
*/
29592955
else
29602956
{
29612957
if (newdef->generated)
2962-
def->generated = newdef->generated;
2958+
ereport(ERROR,
2959+
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
2960+
errmsg("child column \"%s\" specifies generation expression",
2961+
def->colname),
2962+
errhint("A child table column cannot be generated unless its parent column is.")));
29632963
}
29642964

29652965
/* If new def has a default, override previous default */
@@ -2994,8 +2994,8 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
29942994
/*
29952995
* Now that we have the column definition list for a partition, we can
29962996
* check whether the columns referenced in the column constraint specs
2997-
* actually exist. Also, we merge NOT NULL and defaults into each
2998-
* corresponding column definition.
2997+
* actually exist. Also, we merge parent's NOT NULL constraints and
2998+
* defaults into each corresponding column definition.
29992999
*/
30003000
if (is_partition)
30013001
{
@@ -3014,6 +3014,19 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
30143014
found = true;
30153015
coldef->is_not_null |= restdef->is_not_null;
30163016

3017+
/*
3018+
* As above, reject generated columns in partitions that
3019+
* are not generated in the parent.
3020+
*/
3021+
if (restdef->generated && !coldef->generated)
3022+
ereport(ERROR,
3023+
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
3024+
errmsg("child column \"%s\" specifies generation expression",
3025+
restdef->colname),
3026+
errhint("A child table column cannot be generated unless its parent column is.")));
3027+
/* Other way around should have been dealt with above */
3028+
Assert(!(coldef->generated && !restdef->generated));
3029+
30173030
/*
30183031
* Override the parent's default value for this column
30193032
* (coldef->cooked_default) with the partition's local
@@ -3058,7 +3071,8 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
30583071
ereport(ERROR,
30593072
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
30603073
errmsg("column \"%s\" inherits conflicting generation expressions",
3061-
def->colname)));
3074+
def->colname),
3075+
errhint("To resolve the conflict, specify a generation expression explicitly.")));
30623076
else
30633077
ereport(ERROR,
30643078
(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
@@ -15038,64 +15052,18 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel)
1503815052
attributeName)));
1503915053

1504015054
/*
15041-
* If parent column is generated, child column must be, too.
15055+
* Child column must be generated if and only if parent column is.
1504215056
*/
1504315057
if (attribute->attgenerated && !childatt->attgenerated)
1504415058
ereport(ERROR,
1504515059
(errcode(ERRCODE_DATATYPE_MISMATCH),
1504615060
errmsg("column \"%s\" in child table must be a generated column",
1504715061
attributeName)));
15048-
15049-
/*
15050-
* Check that both generation expressions match.
15051-
*
15052-
* The test we apply is to see whether they reverse-compile to the
15053-
* same source string. This insulates us from issues like whether
15054-
* attributes have the same physical column numbers in parent and
15055-
* child relations. (See also constraints_equivalent().)
15056-
*/
15057-
if (attribute->attgenerated && childatt->attgenerated)
15058-
{
15059-
TupleConstr *child_constr = child_rel->rd_att->constr;
15060-
TupleConstr *parent_constr = parent_rel->rd_att->constr;
15061-
char *child_expr = NULL;
15062-
char *parent_expr = NULL;
15063-
15064-
Assert(child_constr != NULL);
15065-
Assert(parent_constr != NULL);
15066-
15067-
for (int i = 0; i < child_constr->num_defval; i++)
15068-
{
15069-
if (child_constr->defval[i].adnum == childatt->attnum)
15070-
{
15071-
child_expr =
15072-
TextDatumGetCString(DirectFunctionCall2(pg_get_expr,
15073-
CStringGetTextDatum(child_constr->defval[i].adbin),
15074-
ObjectIdGetDatum(child_rel->rd_id)));
15075-
break;
15076-
}
15077-
}
15078-
Assert(child_expr != NULL);
15079-
15080-
for (int i = 0; i < parent_constr->num_defval; i++)
15081-
{
15082-
if (parent_constr->defval[i].adnum == attribute->attnum)
15083-
{
15084-
parent_expr =
15085-
TextDatumGetCString(DirectFunctionCall2(pg_get_expr,
15086-
CStringGetTextDatum(parent_constr->defval[i].adbin),
15087-
ObjectIdGetDatum(parent_rel->rd_id)));
15088-
break;
15089-
}
15090-
}
15091-
Assert(parent_expr != NULL);
15092-
15093-
if (strcmp(child_expr, parent_expr) != 0)
15094-
ereport(ERROR,
15095-
(errcode(ERRCODE_DATATYPE_MISMATCH),
15096-
errmsg("column \"%s\" in child table has a conflicting generation expression",
15097-
attributeName)));
15098-
}
15062+
if (childatt->attgenerated && !attribute->attgenerated)
15063+
ereport(ERROR,
15064+
(errcode(ERRCODE_DATATYPE_MISMATCH),
15065+
errmsg("column \"%s\" in child table must not be a generated column",
15066+
attributeName)));
1509915067

1510015068
/*
1510115069
* OK, bump the child column's inheritance count. (If we fail

src/backend/parser/parse_utilcmd.c

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -740,11 +740,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
740740
ereport(ERROR,
741741
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
742742
errmsg("generated columns are not supported on typed tables")));
743-
if (cxt->partbound)
744-
ereport(ERROR,
745-
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
746-
errmsg("generated columns are not supported on partitions")));
747-
748743
if (saw_generated)
749744
ereport(ERROR,
750745
(errcode(ERRCODE_SYNTAX_ERROR),

src/bin/pg_dump/common.c

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -452,14 +452,15 @@ flagInhIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
452452
* that we'll correctly emit the necessary DEFAULT NULL clause; otherwise
453453
* the backend will apply an inherited default to the column.
454454
*
455-
* - Detect child columns that have a generation expression when their parents
456-
* also have one. Generation expressions are always inherited, so there is
457-
* no need to set them again in child tables, and there is no syntax for it
458-
* either. Exceptions: If it's a partition or we are in binary upgrade
459-
* mode, we dump them because in those cases inherited tables are recreated
460-
* standalone first and then reattached to the parent. (See also the logic
461-
* in dumpTableSchema().) In that situation, the generation expressions
462-
* must match the parent, enforced by ALTER TABLE.
455+
* - Detect child columns that have a generation expression and all their
456+
* parents also have the same generation expression, and if so suppress the
457+
* child's expression. The child will inherit the generation expression
458+
* automatically, so there's no need to dump it. This improves the dump's
459+
* compatibility with pre-v16 servers, which didn't allow the child's
460+
* expression to be given explicitly. Exceptions: If it's a partition or
461+
* we are in binary upgrade mode, we dump such expressions anyway because
462+
* in those cases inherited tables are recreated standalone first and then
463+
* reattached to the parent. (See also the logic in dumpTableSchema().)
463464
*
464465
* modifies tblinfo
465466
*/
@@ -497,15 +498,17 @@ flagInhAttrs(DumpOptions *dopt, TableInfo *tblinfo, int numTables)
497498
{
498499
bool foundNotNull; /* Attr was NOT NULL in a parent */
499500
bool foundDefault; /* Found a default in a parent */
500-
bool foundGenerated; /* Found a generated in a parent */
501+
bool foundSameGenerated; /* Found matching GENERATED */
502+
bool foundDiffGenerated; /* Found non-matching GENERATED */
501503

502504
/* no point in examining dropped columns */
503505
if (tbinfo->attisdropped[j])
504506
continue;
505507

506508
foundNotNull = false;
507509
foundDefault = false;
508-
foundGenerated = false;
510+
foundSameGenerated = false;
511+
foundDiffGenerated = false;
509512
for (k = 0; k < numParents; k++)
510513
{
511514
TableInfo *parent = parents[k];
@@ -517,8 +520,19 @@ flagInhAttrs(DumpOptions *dopt, TableInfo *tblinfo, int numTables)
517520
if (inhAttrInd >= 0)
518521
{
519522
foundNotNull |= parent->notnull[inhAttrInd];
520-
foundDefault |= (parent->attrdefs[inhAttrInd] != NULL && !parent->attgenerated[inhAttrInd]);
521-
foundGenerated |= parent->attgenerated[inhAttrInd];
523+
foundDefault |= (parent->attrdefs[inhAttrInd] != NULL &&
524+
!parent->attgenerated[inhAttrInd]);
525+
if (parent->attgenerated[inhAttrInd])
526+
{
527+
/* these pointer nullness checks are just paranoia */
528+
if (parent->attrdefs[inhAttrInd] != NULL &&
529+
tbinfo->attrdefs[j] != NULL &&
530+
strcmp(parent->attrdefs[inhAttrInd]->adef_expr,
531+
tbinfo->attrdefs[j]->adef_expr) == 0)
532+
foundSameGenerated = true;
533+
else
534+
foundDiffGenerated = true;
535+
}
522536
}
523537
}
524538

@@ -561,8 +575,9 @@ flagInhAttrs(DumpOptions *dopt, TableInfo *tblinfo, int numTables)
561575
tbinfo->attrdefs[j] = attrDef;
562576
}
563577

564-
/* Remove generation expression from child */
565-
if (foundGenerated && !tbinfo->ispartition && !dopt->binary_upgrade)
578+
/* Remove generation expression from child if possible */
579+
if (foundSameGenerated && !foundDiffGenerated &&
580+
!tbinfo->ispartition && !dopt->binary_upgrade)
566581
tbinfo->attrdefs[j] = NULL;
567582
}
568583
}

src/bin/pg_dump/pg_dump.c

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8529,17 +8529,11 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
85298529
{
85308530
/*
85318531
* Column generation expressions cannot be dumped separately,
8532-
* because there is no syntax for it. The !shouldPrintColumn
8533-
* case below will be tempted to set them to separate if they
8534-
* are attached to an inherited column without a local
8535-
* definition, but that would be wrong and unnecessary,
8536-
* because generation expressions are always inherited, so
8537-
* there is no need to set them again in child tables, and
8538-
* there is no syntax for it either. By setting separate to
8532+
* because there is no syntax for it. By setting separate to
85398533
* false here we prevent the "default" from being processed as
85408534
* its own dumpable object, and flagInhAttrs() will remove it
8541-
* from the table when it detects that it belongs to an
8542-
* inherited column.
8535+
* from the table if possible (that is, if it can be inherited
8536+
* from a parent).
85438537
*/
85448538
attrdefs[j].separate = false;
85458539
}

0 commit comments

Comments
 (0)