HiveBrain v1.2.0
Get Started
← Back to all entries
patternsqlModerate

Postgres CRM associations: one-way "primary" label, required primary with deferred check, and auto-promotion on unlink

Submitted by: @merway7(1088 rep)··
0
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.