serial not accepted as datatype in ALTER TABLE ... ALTER COLUMN
Hi All,
alter table t1 add column c serial;
ALTER TABLE
this works, but not
#alter table t1 add column c int;
ALTER TABLE
#alter table t1 alter column c type serial;
ERROR: type "serial" does not exist
Looking at the documentation [1]https://www.postgresql.org/docs/current/sql-altertable.html, the grammar for both mentions data_type
ADD [ COLUMN ] [ IF NOT EXISTS ] column_name data_type and
ALTER [ COLUMN ] column_name [ SET DATA ] TYPE data_type
data_type is described on that page as "Data type of the new column,
or new data type for an existing column." but CREATE TABLE
documentation [2]https://www.postgresql.org/docs/16/sql-createtable.html redirects data_type to [3]https://www.postgresql.org/docs/16/datatype.html, which mentions serial.
The impression created by the documentation is the second statement
above is a valid statement as should not throw an error; instead
change the data type of the column (and create required sequence).
In code ATPrepAlterColumnType() calls typenameTypeIdAndMod(), whereas
transformColumnDefinition() (called for ALTER TABLE ... ADD COLUMN and
CREATE TABLE) handles "serial" data type separately. Looks like we are
missing a call to transformColumnDefinition() in
transformAlterTableStmt() under case AT_AlterColumnType.
[1]: https://www.postgresql.org/docs/current/sql-altertable.html
[2]: https://www.postgresql.org/docs/16/sql-createtable.html
[3]: https://www.postgresql.org/docs/16/datatype.html
--
Best Wishes,
Ashutosh Bapat
Hi Ashutosh,
data_type is described on that page as "Data type of the new column,
or new data type for an existing column." but CREATE TABLE
documentation [2] redirects data_type to [3], which mentions serial.
The impression created by the documentation is the second statement
above is a valid statement as should not throw an error; instead
change the data type of the column (and create required sequence).
I didn't find out a reason to not support it, if have any reason, I
think it is better have some explaination in the document.
In code ATPrepAlterColumnType() calls typenameTypeIdAndMod(), whereas
transformColumnDefinition() (called for ALTER TABLE ... ADD COLUMN and
CREATE TABLE) handles "serial" data type separately. Looks like we are
missing a call to transformColumnDefinition() in
transformAlterTableStmt() under case AT_AlterColumnType.
I tried your idea with the attatchment, it is still in a drafted state
but it can be used as a prove-of-concept and for better following
communicating. Just one point needs to metion is serial implies
"default value" + "not null" constaint. So when we modify a column into
serial, we need to modify the 'NULL value' and only to the default value
at the RewriteTable stage.
--
Best Regards
Andy Fan
Attachments:
v0-0001-POC-Support-ALTER-TABLE-tableName-ALERT-COLUMN-co.patchtext/x-diffDownload
From 4485a9af33c9c7d493e23c2804bde4c15df0b79a Mon Sep 17 00:00:00 2001
From: "yizhi.fzh" <yizhi.fzh@alibaba-inc.com>
Date: Sun, 18 Feb 2024 16:14:29 +0800
Subject: [PATCH v0 1/1] POC: Support ALTER TABLE tableName ALERT COLUMN col
type serial.
---
src/backend/commands/tablecmds.c | 60 +++++++++++++++++--
src/backend/parser/gram.y | 1 +
src/backend/parser/parse_utilcmd.c | 41 ++++++++++++-
src/include/nodes/parsenodes.h | 1 +
src/include/parser/parse_utilcmd.h | 4 +-
src/test/regress/expected/alter_table.out | 70 +++++++++++++++++++++--
src/test/regress/sql/alter_table.sql | 20 +++++--
7 files changed, 180 insertions(+), 17 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 46ece07338..7e13b04efa 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -231,6 +231,8 @@ typedef struct NewColumnValue
Expr *expr; /* expression to compute */
ExprState *exprstate; /* execution state */
bool is_generated; /* is it a GENERATED expression? */
+ bool new_on_null; /* set the new value only when the old value
+ * is NULL. */
} NewColumnValue;
/*
@@ -5594,7 +5596,8 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
atstmt,
context->queryString,
&beforeStmts,
- &afterStmts);
+ &afterStmts,
+ context);
/* Execute any statements that should happen before these subcommand(s) */
foreach(lc, beforeStmts)
@@ -6230,10 +6233,11 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
if (ex->is_generated)
continue;
- newslot->tts_values[ex->attnum - 1]
- = ExecEvalExpr(ex->exprstate,
- econtext,
- &newslot->tts_isnull[ex->attnum - 1]);
+ if (!ex->new_on_null || oldslot->tts_isnull[ex->attnum - 1])
+ newslot->tts_values[ex->attnum - 1]
+ = ExecEvalExpr(ex->exprstate,
+ econtext,
+ &newslot->tts_isnull[ex->attnum - 1]);
}
ExecStoreVirtualTuple(newslot);
@@ -13284,6 +13288,7 @@ ATPrepAlterColumnType(List **wqueue,
newval->attnum = attnum;
newval->expr = (Expr *) transform;
newval->is_generated = false;
+ newval->new_on_null = def->from_serial;
tab->newvals = lappend(tab->newvals, newval);
if (ATColumnChangeRequiresRewrite(transform, attnum))
@@ -13495,6 +13500,48 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
HeapTuple depTup;
ObjectAddress address;
+ if (def->from_serial)
+ {
+ /*
+ * Store the DEFAULT for the from_serial
+ */
+ /* XXX: copy from ATExecAddColumn */
+ RawColumnDefault *rawEnt;
+
+ heapTup = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
+ attTup = (Form_pg_attribute) GETSTRUCT(heapTup);
+
+ rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault));
+ rawEnt->attnum = attTup->attnum;
+ rawEnt->raw_default = copyObject(def->raw_default);
+
+ /*
+ * Attempt to skip a complete table rewrite by storing the specified
+ * DEFAULT value outside of the heap. This may be disabled inside
+ * AddRelationNewConstraints if the optimization cannot be applied.
+ */
+ rawEnt->missingMode = (!def->generated);
+
+ rawEnt->generated = def->generated;
+
+ /*
+ * This function is intended for CREATE TABLE, so it processes a
+ * _list_ of defaults, but we just do one.
+ */
+ AddRelationNewConstraints(rel, list_make1(rawEnt), NIL,
+ false, true, false, NULL);
+
+ /* Make the additional catalog changes visible */
+ CommandCounterIncrement();
+
+ /*
+ * Did the request for a missing value work? If not we'll have to do a
+ * rewrite
+ */
+ if (!rawEnt->missingMode)
+ tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+ }
+
/*
* Clear all the missing values if we're rewriting the table, since this
* renders them pointless.
@@ -14368,7 +14415,8 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
(AlterTableStmt *) stmt,
cmd,
&beforeStmts,
- &afterStmts);
+ &afterStmts,
+ NULL);
querytree_list = list_concat(querytree_list, beforeStmts);
querytree_list = lappend(querytree_list, stmt);
querytree_list = list_concat(querytree_list, afterStmts);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 60b31d9f85..0aea1673c1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2599,6 +2599,7 @@ alter_table_cmd:
def->collClause = (CollateClause *) $7;
def->raw_default = $8;
def->location = @3;
+ def->colname = $3;
$$ = (Node *) n;
}
/* ALTER FOREIGN TABLE <name> ALTER [COLUMN] <colname> OPTIONS */
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 8dcf794ca2..622e7f629a 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -28,6 +28,7 @@
#include "access/reloptions.h"
#include "access/table.h"
#include "access/toast_compression.h"
+#include "access/xact.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
#include "catalog/index.h"
@@ -59,6 +60,7 @@
#include "parser/parse_utilcmd.h"
#include "parser/parser.h"
#include "rewrite/rewriteManip.h"
+#include "tcop/utility.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
@@ -385,6 +387,8 @@ generateSerialExtraStmts(CreateStmtContext *cxt, ColumnDef *column,
/* Make a copy of this as we may end up modifying it in the code below */
seqoptions = list_copy(seqoptions);
+ column->from_serial = true;
+
/*
* Determine namespace and name to use for the sequence.
*
@@ -3423,11 +3427,15 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString,
* To avoid race conditions, it's important that this function rely only on
* the passed-in relid (and not on stmt->relation) to determine the target
* relation.
+ *
+ * Note: context is used for creating the beforeStmt earlier, maybe NULL
+ * sometimes.
*/
AlterTableStmt *
transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
const char *queryString,
- List **beforeStmts, List **afterStmts)
+ List **beforeStmts, List **afterStmts,
+ struct AlterTableUtilityContext *context)
{
Relation rel;
TupleDesc tupdesc;
@@ -3440,6 +3448,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
bool skipValidation = true;
AlterTableCmd *newcmd;
ParseNamespaceItem *nsitem;
+ bool beforeStmtExecuted = false;
/* Caller is responsible for locking the relation */
rel = relation_open(relid, NoLock);
@@ -3540,6 +3549,30 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
ColumnDef *def = castNode(ColumnDef, cmd->def);
AttrNumber attnum;
+ /*
+ * transform the serial data type, including 1. generate
+ * the seq cmd as before stmt. 2. change the serial type
+ * into intX 3. add not null constraint.
+ */
+ transformColumnDefinition(&cxt, def);
+ if (def->from_serial)
+ {
+ /*
+ * XXX: we have to execute this sooner for the later
+ * transformExpr(.., raw_default) works.
+ */
+ ListCell *lc;
+
+ foreach(lc, cxt.blist)
+ {
+ Node *stmt = (Node *) lfirst(lc);
+
+ ProcessUtilityForAlterTable(stmt, context);
+ CommandCounterIncrement();
+ }
+ beforeStmtExecuted = true;
+ }
+
/*
* For ALTER COLUMN TYPE, transform the USING clause if
* one was specified.
@@ -3776,7 +3809,11 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
*/
stmt->cmds = newcmds;
- *beforeStmts = cxt.blist;
+ if (!beforeStmtExecuted)
+ *beforeStmts = cxt.blist;
+ else
+ *beforeStmts = NIL;
+
*afterStmts = list_concat(cxt.alist, save_alist);
return stmt;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d5b08ded44..7ff2abfc53 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -729,6 +729,7 @@ typedef struct ColumnDef
List *constraints; /* other constraints on column */
List *fdwoptions; /* per-column FDW options */
int location; /* parse location, or -1 if none/unknown */
+ bool from_serial; /* transform from type serial. */
} ColumnDef;
/*
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 1406589477..7b70c785fb 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -18,12 +18,14 @@
struct AttrMap; /* avoid including attmap.h here */
+struct AlterTableUtilityContext;
extern List *transformCreateStmt(CreateStmt *stmt, const char *queryString);
extern AlterTableStmt *transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
const char *queryString,
List **beforeStmts,
- List **afterStmts);
+ List **afterStmts,
+ struct AlterTableUtilityContext *context);
extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt,
const char *queryString);
extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt,
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 7666c76238..0daf296e13 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -930,10 +930,25 @@ ERROR: duplicate key value violates unique constraint "atacc_test1"
DETAIL: Key (test)=(2) already exists.
-- should succeed
insert into atacc1 (test) values (4);
+-- insert a NULL.
+insert into atacc1 select;
-- try to create duplicates via alter table using - should fail
alter table atacc1 alter column test type integer using 0;
ERROR: could not create unique index "atacc_test1"
DETAIL: Key (test)=(0) is duplicated.
+alter table atacc1 alter column test type serial;
+select * from atacc1 order by 1;
+ test
+------
+ 1
+ 2
+ 4
+(3 rows)
+
+-- fail since the nextval of seq is 2 and 2 exist already.
+insert into atacc1 select;
+ERROR: duplicate key value violates unique constraint "atacc_test1"
+DETAIL: Key (test)=(2) already exists.
drop table atacc1;
-- let's do one where the unique constraint fails when added
create table atacc1 ( test int );
@@ -2086,11 +2101,13 @@ alter table at_tab1 alter column b type varchar; -- fails
ERROR: cannot alter table "at_tab1" because column "at_tab2.y" uses its row type
drop table at_tab1, at_tab2;
-- Alter column type that's part of a partitioned index
-create table at_partitioned (a int, b text) partition by range (a);
+create table at_partitioned (a int, b text, c int) partition by range (a);
create table at_part_1 partition of at_partitioned for values from (0) to (1000);
-insert into at_partitioned values (512, '0.123');
-create table at_part_2 (b text, a int);
-insert into at_part_2 values ('1.234', 1024);
+insert into at_partitioned values (512, '0.123', 1);
+insert into at_partitioned values (512, '0.123', NULL);
+create table at_part_2 (b text, c int, a int);
+insert into at_part_2 values ('1.234', 2, 1024);
+insert into at_part_2 values ('1.234', NULL, 1024);
create index on at_partitioned (b);
create index on at_partitioned (a);
\d at_part_1
@@ -2099,6 +2116,7 @@ create index on at_partitioned (a);
--------+---------+-----------+----------+---------
a | integer | | |
b | text | | |
+ c | integer | | |
Partition of: at_partitioned FOR VALUES FROM (0) TO (1000)
Indexes:
"at_part_1_a_idx" btree (a)
@@ -2109,6 +2127,7 @@ Indexes:
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
b | text | | |
+ c | integer | | |
a | integer | | |
alter table at_partitioned attach partition at_part_2 for values from (1000) to (2000);
@@ -2117,6 +2136,7 @@ alter table at_partitioned attach partition at_part_2 for values from (1000) to
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
b | text | | |
+ c | integer | | |
a | integer | | |
Partition of: at_partitioned FOR VALUES FROM (1000) TO (2000)
Indexes:
@@ -2130,6 +2150,7 @@ alter table at_partitioned alter column b type numeric using b::numeric;
--------+---------+-----------+----------+---------
a | integer | | |
b | numeric | | |
+ c | integer | | |
Partition of: at_partitioned FOR VALUES FROM (0) TO (1000)
Indexes:
"at_part_1_a_idx" btree (a)
@@ -2140,12 +2161,53 @@ Indexes:
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
b | numeric | | |
+ c | integer | | |
a | integer | | |
Partition of: at_partitioned FOR VALUES FROM (1000) TO (2000)
Indexes:
"at_part_2_a_idx" btree (a)
"at_part_2_b_idx" btree (b)
+alter table at_partitioned alter column c type serial;
+select * from at_partitioned order by 1;
+ a | b | c
+------+-------+---
+ 512 | 0.123 | 1
+ 512 | 0.123 | 1
+ 1024 | 1.234 | 2
+ 1024 | 1.234 | 2
+(4 rows)
+
+\d+ at_part_1
+ Table "public.at_part_1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+-------------------------------------------+---------+--------------+-------------
+ a | integer | | | | plain | |
+ b | numeric | | | | main | |
+ c | integer | | not null | nextval('at_partitioned_c_seq'::regclass) | plain | |
+Partition of: at_partitioned FOR VALUES FROM (0) TO (1000)
+Partition constraint: ((a IS NOT NULL) AND (a >= 0) AND (a < 1000))
+Indexes:
+ "at_part_1_a_idx" btree (a)
+ "at_part_1_b_idx" btree (b)
+Not-null constraints:
+ "at_partitioned_c_not_null" NOT NULL "c" (inherited)
+
+\d+ at_part_2
+ Table "public.at_part_2"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+-------------------------------------------+---------+--------------+-------------
+ b | numeric | | | | main | |
+ c | integer | | not null | nextval('at_partitioned_c_seq'::regclass) | plain | |
+ a | integer | | | | plain | |
+Partition of: at_partitioned FOR VALUES FROM (1000) TO (2000)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1000) AND (a < 2000))
+Indexes:
+ "at_part_2_a_idx" btree (a)
+ "at_part_2_b_idx" btree (b)
+Not-null constraints:
+ "at_partitioned_c_not_null" NOT NULL "c" (inherited)
+
drop table at_partitioned;
-- Alter column type when no table rewrite is required
-- Also check that comments are preserved
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 9df5a63bdf..876e6b1fa4 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -680,8 +680,14 @@ insert into atacc1 (test) values (2);
insert into atacc1 (test) values (2);
-- should succeed
insert into atacc1 (test) values (4);
+-- insert a NULL.
+insert into atacc1 select;
-- try to create duplicates via alter table using - should fail
alter table atacc1 alter column test type integer using 0;
+alter table atacc1 alter column test type serial;
+select * from atacc1 order by 1;
+-- fail since the nextval of seq is 2 and 2 exist already.
+insert into atacc1 select;
drop table atacc1;
-- let's do one where the unique constraint fails when added
@@ -1431,11 +1437,13 @@ alter table at_tab1 alter column b type varchar; -- fails
drop table at_tab1, at_tab2;
-- Alter column type that's part of a partitioned index
-create table at_partitioned (a int, b text) partition by range (a);
+create table at_partitioned (a int, b text, c int) partition by range (a);
create table at_part_1 partition of at_partitioned for values from (0) to (1000);
-insert into at_partitioned values (512, '0.123');
-create table at_part_2 (b text, a int);
-insert into at_part_2 values ('1.234', 1024);
+insert into at_partitioned values (512, '0.123', 1);
+insert into at_partitioned values (512, '0.123', NULL);
+create table at_part_2 (b text, c int, a int);
+insert into at_part_2 values ('1.234', 2, 1024);
+insert into at_part_2 values ('1.234', NULL, 1024);
create index on at_partitioned (b);
create index on at_partitioned (a);
\d at_part_1
@@ -1445,6 +1453,10 @@ alter table at_partitioned attach partition at_part_2 for values from (1000) to
alter table at_partitioned alter column b type numeric using b::numeric;
\d at_part_1
\d at_part_2
+alter table at_partitioned alter column c type serial;
+select * from at_partitioned order by 1;
+\d+ at_part_1
+\d+ at_part_2
drop table at_partitioned;
-- Alter column type when no table rewrite is required
--
2.34.1
On Sun, Feb 18, 2024 at 1:59 PM Andy Fan <zhihuifan1213@163.com> wrote:
Hi Ashutosh,
data_type is described on that page as "Data type of the new column,
or new data type for an existing column." but CREATE TABLE
documentation [2] redirects data_type to [3], which mentions serial.
The impression created by the documentation is the second statement
above is a valid statement as should not throw an error; instead
change the data type of the column (and create required sequence).I didn't find out a reason to not support it, if have any reason, I
think it is better have some explaination in the document.In code ATPrepAlterColumnType() calls typenameTypeIdAndMod(), whereas
transformColumnDefinition() (called for ALTER TABLE ... ADD COLUMN and
CREATE TABLE) handles "serial" data type separately. Looks like we are
missing a call to transformColumnDefinition() in
transformAlterTableStmt() under case AT_AlterColumnType.I tried your idea with the attatchment, it is still in a drafted state
but it can be used as a prove-of-concept and for better following
communicating. Just one point needs to metion is serial implies
"default value" + "not null" constaint. So when we modify a column into
serial, we need to modify the 'NULL value' and only to the default value
at the RewriteTable stage.
I am surprised that this requires changes in ReWrite. I thought adding
NOT NULL constraint and default value commands would be done by
transformColumnDefinition(). But I haven't looked at the patch close
enough.
--
Best Wishes,
Ashutosh Bapat
Ashutosh Bapat <ashutosh.bapat.oss@gmail.com> writes:
On Sun, Feb 18, 2024 at 1:59 PM Andy Fan <zhihuifan1213@163.com> wrote:
I tried your idea with the attatchment, it is still in a drafted state
but it can be used as a prove-of-concept and for better following
communicating. Just one point needs to metion is serial implies
"default value" + "not null" constaint. So when we modify a column into
serial, we need to modify the 'NULL value' and only to the default value
at the RewriteTable stage.I am surprised that this requires changes in ReWrite. I thought adding
NOT NULL constraint and default value commands would be done by
transformColumnDefinition(). But I haven't looked at the patch close
enough.
Hmm, I think this depends on how to handle the NULL values before the
RewriteTable.
Consider the example like this:
\pset null (null)
create table t(a int);
insert into t select 1;
insert into t select;
postgres=# select * from t;
a
--------
1
(null)
(2 rows)
since serial type implies "not null" + "default value", shall we raise error
or fill the value with the "default" value? The patch choose the later
way which needs changes in RewirteTable stage, but now I think the raise
error directly is an option as well.
--
Best Regards
Andy Fan