patternsqlModerate
Postgres CRM associations: one-way "primary" label, required primary with deferred check, and auto-promotion on unlink
Viewed 0 times
deferred constraint triggerassociation labelprimary associationinverse association typeone-way label
Problem
In a CRM where every association type has an inverse row (A->B and B->A) and a labeled link implies the unlabeled one, a "primary company" label is often modeled symmetrically, so the label shows on both records. Rules like "first company becomes primary", "a contact with companies must always have exactly one primary" and "replace the primary" then break: immediate checks fail mid-transaction (remove old primary before adding the new one), and bulk operations such as record merges or imports that delete and reinsert links trip the rules in the wrong order.
Solution
Keep the inverse type (needed for symmetric deletes) but add a
visible boolean column and set it false on the inverse of a one-way label; filter every UI, filter vocabulary and lookup-by-label on visible types, and refuse inserting an invisible type directly (it only appears as an inverse). Implement the rules as three triggers on the associations table: (1) AFTER INSERT of the unlabeled object->company link: if the record has no primary yet, insert the primary row with ON CONFLICT DO NOTHING; (2) a CONSTRAINT TRIGGER ... DEFERRABLE INITIALLY DEFERRED on insert/delete that checks, at commit, that a non-deleted contact with at least one company has exactly one primary (so "remove old + set new" in one transaction passes); (3) AFTER DELETE of the unlabeled link, named to sort after the cascade trigger that removes labeled rows, that promotes the oldest remaining company when the primary was unlinked. Make (1) and (3) no-ops when a session setting says the write comes from a merge (the merge function sets the primary itself), and skip when the parent record is being deleted.Why
Primary rules span several rows and several statements; only a deferred check sees the final state, and bulk rewrite paths need an explicit opt-out or the auto-rules pick a different primary and then collide with the max-1 limit.
Gotchas
- Postgres fires same-timing triggers in name order: name the promote trigger so it runs after the trigger that cascades deletion of labeled rows
- Use ON CONFLICT DO NOTHING in auto-insert triggers: loaders that insert the labeled row first will otherwise hit the primary key
- Test with SET CONSTRAINTS ALL IMMEDIATE to make the deferred check fire inside a pgTAP transaction
Revisions (0)
No revisions yet.