Yesterday I ran into something rather weird with PostgreSQL (9.1rc).
A bit of context first: the database in which the problem occurred contains tables which represent an ordered tree, something quite similar to what depesz's post describes; in addition, most of the operations on the structure of the tree are done through stored procedures, and these stored procedures usually re-order the tree's items when they're done.
The main table had a trigger that basically looked like the following:
CREATE OR REPLACE FUNCTION objects_bu( ) RETURNS TRIGGER SECURITY DEFINER AS $objects_bu$ BEGIN -- Various other checks here IF OLD.object_ordering = NEW.object_ordering AND OLD.object_id_parent IS NOT DISTINCT FROM NEW.object_id_parent THEN RETURN NEW; END IF; IF NEW.object_id_parent IS NULL THEN NEW.object_ordering_path := to_char( NEW.object_ordering , '000000000000' ); ELSE SELECT object_ordering_path || '/' || to_char( NEW.object_ordering , '000000000000') INTO NEW.object_ordering_path FROM objects WHERE object_id = NEW.object_id_parent; END IF; UPDATE objects SET object_ordering_path = regexp_replace( object_ordering_path , '^' || OLD.object_ordering_path, NEW.object_ordering_path ) WHERE object_id IN ( SELECT object_id_child FROM objects_tree WHERE object_id_parent = NEW.object_id AND ot_depth > 0 ); RETURN NEW; END; $objects_bu$ LANGUAGE plpgsql; CREATE TRIGGER objects_bu BEFORE UPDATE ON objects FOR EACH ROW EXECUTE PROCEDURE objects_bu( );
In most cases, this trigger works: it does what it's supposed to do, and the ordering path column is updated just fine. However, if an operation that causes this trigger to execute is followed by another update in the same transaction (for example calling the function that resets the ordering indexes), things get really weird.
For example, if the table contains 3 entries, two of which are children of another, and you try to invert both items (by setting the first item's ordering index to the second item's, plus one) then reorder the tree, it will look like the inversion of the child items did not work. This will not happen systematically - sometimes it will work, but in most cases it won't. Upon closer examination, it appears that:
- the inversion itself works,
- the first update of the re-ordering code sometimes updates all 3 rows (which is what it is supposed to do), but in many cases only changes the rows that have not been updated,
- because that first update "failed" silently, the second update of the re-ordering query will do exactly what it's supposed to do, but since the first child still has its low, non-reordered index, it will stay where it was before the operation.
The whole mess is caused by the last update in the trigger above. It somehow makes PostgreSQL a little confused. To fix it, moving that update to another trigger, which is executed after the update, suffices - and is more logical anyway.
What I am unhappy about here is not the fact that doing updates on the table in a "before update" trigger on the same table does not work. That was a rather silly idea to begin with. But it seems to me that PostgreSQL should behave consistently when that happens - either make it work, or cause an error - instead of going Hannibal Lecter and randomly failing in silence.
I have not reported this bug yet, because it seems related to bug #6123 and I know there is some work in progress on that. Still, this bug results in a behaviour that's weird enough to be worth mentioning.