Skip to content

Instantly share code, notes, and snippets.

@MarkusH
Last active March 11, 2025 23:24
Show Gist options
  • Save MarkusH/4c03d0867ba37b6b89379bced12cb3ae to your computer and use it in GitHub Desktop.
Save MarkusH/4c03d0867ba37b6b89379bced12cb3ae to your computer and use it in GitHub Desktop.
A script that outputs the raw SQL to convert Django's serial columns to identity columns.
WITH tab AS (
SELECT
a.attrelid::regclass::text AS t,
a.attname AS c,
pg_get_serial_sequence(a.attrelid::regclass::text, a.attname) AS s,
nextval(pg_get_serial_sequence(a.attrelid::regclass::text, a.attname)) AS v
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
WHERE
a.attrelid::regclass::text LIKE '%'
AND c.relkind IN ('r', 'p') /* regular and partitioned tables */
AND a.attnum > 0
AND NOT a.attisdropped
AND a.atttypid = ANY ('{int,int8,int2}'::regtype[])
AND EXISTS (
SELECT FROM pg_attrdef ad
WHERE
ad.adrelid = a.attrelid
AND ad.adnum = a.attnum
AND (
pg_get_expr(ad.adbin, ad.adrelid)
=
'nextval('''
|| (
pg_get_serial_sequence(a.attrelid::regclass::text, a.attname)
)::regclass
|| '''::regclass)'
)
)
ORDER BY a.attnum
)
SELECT
'BEGIN; '
|| 'LOCK TABLE ' || quote_ident(t) || ' IN ACCESS EXCLUSIVE MODE; '
|| 'ALTER TABLE ' || quote_ident(t) || ' ALTER COLUMN ' || quote_ident(c) || ' DROP DEFAULT; '
|| 'DROP SEQUENCE ' || s || '; '
|| 'ALTER TABLE ' || quote_ident(t) || ' ALTER ' || quote_ident(c) || ' ADD GENERATED BY DEFAULT AS IDENTITY (RESTART ' || v || '); '
|| 'COMMIT;'
FROM tab;
@johanneskares
Copy link

johanneskares commented Mar 11, 2025

This should execute directly

DO $$
DECLARE
    rec record;
    table_ident text;
    seq_ident text;
    max_val bigint;
BEGIN
    FOR rec IN
        SELECT
            a.attrelid::regclass::text AS t,
            a.attname AS c,
            pg_get_serial_sequence(a.attrelid::regclass::text, a.attname) AS s
        FROM pg_attribute a
        JOIN pg_class c ON c.oid = a.attrelid
        WHERE
            a.attrelid::regclass::text LIKE '%'
            AND c.relkind IN ('r', 'p')  -- regular and partitioned tables
            AND a.attnum > 0
            AND NOT a.attisdropped
            AND a.atttypid = ANY ('{int,int8,int2}'::regtype[])
            AND EXISTS (
                SELECT 1
                FROM pg_attrdef ad
                WHERE
                    ad.adrelid = a.attrelid
                    AND ad.adnum = a.attnum
                    AND (
                        pg_get_expr(ad.adbin, ad.adrelid)
                        =
                        'nextval('''
                        || (pg_get_serial_sequence(a.attrelid::regclass::text, a.attname))::regclass
                        || '''::regclass)'
                    )
            )
        ORDER BY a.attnum
    LOOP
        -- Handle schema-qualified table names
        table_ident := CASE 
                         WHEN position('.' in rec.t) > 0 
                           THEN quote_ident(split_part(rec.t, '.', 1)) || '.' || quote_ident(split_part(rec.t, '.', 2))
                         ELSE quote_ident(rec.t)
                       END;
        -- Handle schema-qualified sequence names
        seq_ident := CASE 
                       WHEN position('.' in rec.s) > 0 
                         THEN quote_ident(split_part(rec.s, '.', 1)) || '.' || quote_ident(split_part(rec.s, '.', 2))
                       ELSE quote_ident(rec.s)
                     END;
        
        -- Lock the table to prevent concurrent modifications
        EXECUTE 'LOCK TABLE ' || table_ident || ' IN ACCESS EXCLUSIVE MODE';
        
        -- Get the max value of the column to set the new identity restart value
        EXECUTE 'SELECT COALESCE(MAX(' || quote_ident(rec.c) || '), 0) + 1 FROM ' || table_ident INTO max_val;
        
        -- Drop the default and sequence
        EXECUTE 'ALTER TABLE ' || table_ident || ' ALTER COLUMN ' || quote_ident(rec.c) || ' DROP DEFAULT';
        EXECUTE 'DROP SEQUENCE ' || seq_ident;
        
        -- Convert the column to GENERATED ALWAYS AS IDENTITY
        EXECUTE 'ALTER TABLE ' || table_ident || ' ALTER COLUMN ' || quote_ident(rec.c)
                || ' ADD GENERATED ALWAYS AS IDENTITY (RESTART WITH ' || max_val || ')';
    END LOOP;
END $$;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment