I want to have an ordinal column in which values always start from 1 and have no gaps. I have devised a solution with triggers, but I'd like to know if there is a better or more elegant way.
BEFORE INSERT trigger renumbers the rows that come after the inserted value. If value is not provided or too high, it is set to row count + 1. Similarly, AFTER DELETE trigger renumbers the rows that come after the deleted value. Both triggers lock rows before changing the value.
CREATE OR REPLACE FUNCTION ids_insert() RETURNS trigger AS $BODY$
DECLARE
_lock_sql text;
_id bigint;
BEGIN
IF TG_OP = 'INSERT' THEN
IF NEW.id < 1 THEN
RAISE EXCEPTION 'ID must be greater than zero.';
END IF;
EXECUTE format('SELECT COUNT(*) + 1 FROM %I', TG_TABLE_NAME)
INTO _id;
IF NEW.id IS NULL OR NEW.id > _id THEN
NEW.id := _id;
ELSE
_lock_sql := format(
'SELECT id FROM %I '
'WHERE id >= %s '
'ORDER BY id DESC '
'FOR UPDATE', TG_TABLE_NAME, NEW.id
);
FOR _id IN EXECUTE _lock_sql LOOP
EXECUTE format('UPDATE %I SET id = id + 1 WHERE id = %s', TG_TABLE_NAME, _id);
END LOOP;
END IF;
ELSE
IF NEW.id != OLD.id THEN
RAISE EXCEPTION 'Changing the ID directly is not allowed.';
END IF;
END IF;
RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION ids_delete() RETURNS trigger AS $BODY$
DECLARE
_lock_sql text;
_id bigint;
BEGIN
_lock_sql := format(
'SELECT id FROM %I '
'WHERE id > %s '
'ORDER BY id '
'FOR UPDATE', TG_TABLE_NAME, OLD.id
);
FOR _id IN EXECUTE _lock_sql LOOP
EXECUTE format('UPDATE %I SET id = id - 1 WHERE id = %s', TG_TABLE_NAME, _id);
END LOOP;
RETURN OLD;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TABLE test (
id bigint PRIMARY KEY,
...
)
CREATE TRIGGER test_insert BEFORE INSERT OR UPDATE OF id ON test
FOR EACH ROW WHEN (pg_trigger_depth() < 1) EXECUTE PROCEDURE ids_insert();
CREATE TRIGGER test_delete AFTER DELETE ON test
FOR EACH ROW EXECUTE PROCEDURE ids_delete();