1

I would like to have a sequential position column partition by another column. The position should be calculated if a new row get's inserted, deleted or updated. I know it can be done in application code. However, I would like to have it in postgres. Example:

id    position   category_id
5     0          1
8     1          1
26    2          1
32    3          1
120   0          2

Related Question I looked up:

I have already a working solution:

create table items
(
    id int primary key generated always as identity,
    category_id int NOT NULL,
    position int NOT NULL CHECK(position >= 0),
    name text not NULL CHECK (name <> ''),
    UNIQUE (category_id, position) DEFERRABLE INITIALLY IMMEDIATE
);


CREATE OR REPLACE FUNCTION items_reposition()
    RETURNS TRIGGER
    LANGUAGE PLPGSQL
AS '
DECLARE
    _count int;
    _current_position int;
BEGIN
  IF (TG_OP = ''DELETE'') THEN
     UPDATE items i SET position = o.position 
       FROM (SELECT ROW_NUMBER() OVER (ORDER BY position) - 1 as position, id 
             FROM items WHERE category_id = OLD.category_id ORDER BY position) o 
      WHERE i.id = o.id;
    RETURN OLD;
  END IF;
 
  
  SELECT COUNT(1)
  FROM   items
  WHERE  category_id = NEW.category_id
  INTO   _count;
   
  -- insert as last one if position is not given
  IF NEW.position IS NULL THEN
    NEW.position = _count;
  END IF;
  
  IF (OLD IS NOT NULL AND NEW.position >= _count) OR NEW.position > _count THEN
    RAISE EXCEPTION ''Tried to set row with id % to position %, but last position is %.'', NEW.id, NEW.position, _count;
  END IF;
  
  IF OLD IS NULL THEN 
    _current_position = _count;
  ELSE 
    _current_position = OLD.position;
  END IF;

  SET CONSTRAINTS ALL DEFERRED; -- so we can set the position on update
  UPDATE items
  SET position = CASE
    WHEN NEW.position < _current_position THEN position + 1 ELSE position - 1
    END
  WHERE category_id = NEW.category_id
    AND position >= LEAST(_current_position, NEW.position)::int
    AND position <= GREATEST(_current_position, NEW.position)::int 
    AND position != _current_position;

  RETURN NEW;
END;
';


CREATE TRIGGER items_position_items_reposition_trigger
    AFTER DELETE
    ON items
    FOR EACH ROW
    WHEN (pg_trigger_depth() = 0)
EXECUTE PROCEDURE items_reposition();

CREATE TRIGGER items_position_items_before_trigger
    BEFORE INSERT OR UPDATE OF position
    ON items 
    FOR EACH ROW
    WHEN (pg_trigger_depth() = 0)
EXECUTE PROCEDURE items_reposition();

db-fiddle

It looks like it works fine.

My Question is: What can possible go wrong and will be any side-effect with this trigger?

Dantio
  • 1,799
  • 1
  • 10
  • 13
  • Probably this code will be run as a critical section. Can you actually do this and serialize it, or do you need to support high volume of data changes that cannot wait? If they can't wait, then you probably want to schedule the renumbering periodically (every 10 mis, every hour, etc.) or on-demand (triggered after some delay when data changes). – The Impaler Oct 13 '21 at 16:37
  • Also, you can do the renumbering in a single SQL statement. No need for a complex procedure. – The Impaler Oct 13 '21 at 16:37
  • @TheImpaler no high volume of data changes is expected. It will be used for a drag'n drop table in the frontend. Could you give me a hint how the renumbering in single SQL statement will look like? – Dantio Oct 13 '21 at 16:41
  • Actually I take that back. You can typically use `ROW_NUMBER() OVER(PARTITION BY category_id ORDER BY id)` to produce the `position` you want, but since this is a trigger I don't know how the engine will like the access and change of the rows on the same table. – The Impaler Oct 13 '21 at 16:47

0 Answers0