PostgreSQL: cannot alter type of a column used by a view or rule
In a lectur on PostgreSQL a participant came up with a puzzling question: how to cascade anALTER COLUMN
from a table to a view? There are two main approaches: using the power of DDL transactionable commands or alter the system catalog. In this post I explain how to do both.
Imagine you have a table and a view (either dynamic or materialized) built on top of it:
> CREATE TABLE t( pk smallint, t char(2) );
> CREATE VIEW vw_t AS SELECT * FROM t;
> \d t
Table "public.t"
Column | Type | Collation | Nullable | Default
--------|--------------|-----------|----------|---------
pk | smallint | | |
t | character(2) | | |
> \d vw_t
View "public.vw_t"
Column | Type | Collation | Nullable | Default
--------|--------------|-----------|----------|---------
pk | smallint | | |
t | character(2) | | |
t
table changes the structure? PostgreSQL does not allow this since there is a dependency from vw_t
over t
:
> ALTER TABLE t ALTER COLUMN t TYPE char(10);
ERROR: cannot alter type of a column used by a view or rule
DETAIL: rule _RETURN on view vw_t depends on column "t"
> ALTER TABLE t ALTER COLUMN pk TYPE bigint;
ERROR: cannot alter type of a column used by a view or rule
DETAIL: rule _RETURN on view vw_t depends on column "pk"
DETAIL
message provides an hint on the problem: the "_RETURN"
rule is the special way PostgreSQL handles views: it does bounce the `SELECT** statement to the underlying table.
There is only a correct approach to solve the problem: since PostgreSQL allows any DDL command to be included in a transaction, a transaction can be used to drop the view, alter the table and recreate the view:
> BEGIN;
> DROP VIEW vw_t;
> ALTER TABLE t ALTER COLUMN pk TYPE bigint;
> ALTER TABLE t ALTER COLUMN t TYPE char(10);
> CREATE VIEW vw_t AS SELECT * FROM t;
> COMMIT;
pg_attribute
system catalog shows how the table has been defined:
> SELECT attname, atttypmod, attlen
FROM pg_attribute
WHERE attname IN ( 'pk', 't' )
AND attrelid = 't'::regclass;
attname | atttypmod | attlen
---------|-----------|--------
pk | -1 | 2
t | 6 | -1
pk
column being a number has no type modifiers (atttypmod
) and a total length (attlen
) of 2
bytes (it is a smallint
); on the other hand, the t
column has no specific length but a modifier of 6
, in particular 4
added by the length itself (in this case, being a char(2)
is 2 + 4 = 6
).
Let’s say the table must to be changed so that the
pk
becomes a bigint
and the t
column a char(10)
: it is possible to force it on the catalog itself. While it is possible to modify the t
column directly on the catalog increasing the type modifier length, this is not possible on the pk
column because the pg_attribute.attlen
is not a real value rather a copy of pg_type.typlen
as shown in the pg_attribute
documentation.
Therefore, the update of the t
table has to be done as follows:
1) change the type of the numeric column pk
;
2) change the type modifier length of the column t
, adding to the desired value 4 bytes required by PostgreSQL bookeping.
The above two steps must be performed as a superuser, so if you cannot gain superuser privileges, you cannot update the table structure via the system catalog. It is also important to note that the names in
pg_type
are not SQL names, but PostgreSQL internal names; in other words the bigint
type is named int8
.
Let’s see the procedure in action:
# BEGIN;
# UPDATE pg_attribute
SET atttypid =
( SELECT oid
FROM pg_type
WHERE typname = 'int8' )
WHERE attname = 'pk'
AND attrelid = 't'::regclass;
# UPDATE pg_attribute SET atttypmod = 14
WHERE attname = 't'
AND attrelid = 't'::regclass;
# \d t
Table "public.t"
Column | Type | Collation | Nullable | Default
--------|---------------|-----------|----------|---------
pk | bigint | | |
t | character(10) | | |
# \d vw_t
View "public.vw_t"
Column | Type | Collation | Nullable | Default
--------|--------------|-----------|----------|---------
pk | smallint | | |
t | character(2) | | |
-- if ready commit changes ...
vw_t
). If the view remains at its old structure odd behaviors can arise, since extracted values can be regularly read (i.e., the t
column will be limited from the underlying table, so a char(10)
) but updates of the view can result in data truncation.
Again, in PostgreSQL the correct way to perform such structure change is using an ordinary transaction, dropping the dependent objects and recreating them within the same transaction once the table has changed.
Take extreme care when working with the system catalog, and ensure you have a valid backup of your data before changing it, since this is not the intended way to let PostgreSQL protect your data!