Migration to identity columns on PostgreSQL
As of version 4, the DBAL uses identity columns to implement the autoincrement
behavior on PostgreSQL instead of SERIAL* column types.
If you have a database with autoincrement columns created using DBAL 3 or earlier,
you will need to perform the following schema migration before being able to continue managing
the schema with the DBAL:
- Identify all
autoincrementcolumns and their tables. -
Create the
upgrade_serial_to_identity()function in the database as described in PostgreSQL 10 identity columns explained:CREATE OR REPLACE FUNCTION upgrade_serial_to_identity(tbl regclass, col name)RETURNS voidLANGUAGE plpgsqlAS $$DECLAREcolnum smallint;seqid oid;count int;BEGIN-- find column numberSELECT attnum INTO colnum FROM pg_attribute WHERE attrelid = tbl AND attname = col;IF NOT FOUND THENRAISE EXCEPTION 'column does not exist';END IF;-- find sequenceSELECT INTO seqid objidFROM pg_dependWHERE (refclassid, refobjid, refobjsubid) = ('pg_class'::regclass, tbl, colnum)AND classid = 'pg_class'::regclass AND objsubid = 0AND deptype = 'a';GET DIAGNOSTICS count = ROW_COUNT;IF count < 1 THENRAISE EXCEPTION 'no linked sequence found';ELSIF count > 1 THENRAISE EXCEPTION 'more than one linked sequence found';END IF;-- drop the defaultEXECUTE 'ALTER TABLE ' || tbl || ' ALTER COLUMN ' || quote_ident(col) || ' DROP DEFAULT';-- change the dependency between column and sequence to internalUPDATE pg_dependSET deptype = 'i'WHERE (classid, objid, objsubid) = ('pg_class'::regclass, seqid, 0)AND deptype = 'a';-- mark the column as identity columnUPDATE pg_attributeSET attidentity = 'd'WHERE attrelid = tblAND attname = col;END;$$; -
Run the function for each table like this:
SELECT upgrade_serial_to_identity('article', 'id');
Without this migration, next time when DBAL 4 is used to manage the schema, it will perform a similar migration
but instead of reusing the existing sequence, it will drop it and create a new one. As a result,
all new sequence numbers will be generated from 1, which is most likely undesired.
Migrating on managed PostgreSQL services (AWS RDS, Cloud SQL, Azure, Supabase, Neon)
The function above writes directly to pg_depend and pg_attribute. These
catalog updates require PostgreSQL superuser privileges, which managed services
typically do not grant to the database's master / admin user. On those services
the function fails with:
ERROR: permission denied for table pg_depend
Use the variant below instead. It produces the same end state - the column is
GENERATED BY DEFAULT AS IDENTITY, an attached sequence positioned at the next
value the original sequence would have emitted - but uses only standard DDL
(DROP SEQUENCE + ALTER TABLE … ADD GENERATED BY DEFAULT AS IDENTITY).
The trade-off is that the original sequence object is dropped and PostgreSQL
creates a fresh internal sequence; only ownership of the table and sequence is
required, no superuser.
CREATE OR REPLACE FUNCTION upgrade_serial_to_identity(tbl regclass, col name)RETURNS voidLANGUAGE plpgsqlAS $$DECLARE seq_name regclass; next_val bigint;BEGIN -- already identity? nothing to do IF EXISTS ( SELECT 1 FROM pg_attribute WHERE attrelid = tbl AND attname = col AND attidentity IN ('a', 'd') ) THEN RETURN; END IF; -- find the sequence linked via the column's DEFAULT SELECT pg_get_serial_sequence(tbl::text, col)::regclass INTO seq_name; IF seq_name IS NULL THEN RAISE EXCEPTION 'no linked sequence found for %.%', tbl, col; END IF; -- capture the next value the sequence would emit EXECUTE format('SELECT CASE WHEN is_called THEN last_value + 1 ELSE last_value END FROM %s', seq_name) INTO next_val; -- swap SERIAL for IDENTITY via standard DDL EXECUTE format('ALTER TABLE %s ALTER COLUMN %I DROP DEFAULT', tbl, col); EXECUTE format('DROP SEQUENCE %s', seq_name); EXECUTE format( 'ALTER TABLE %s ALTER COLUMN %I ADD GENERATED BY DEFAULT AS IDENTITY (START WITH %s)', tbl, col, next_val );END;$$;
