Skip to content
Advertisement

Postgresql unique constraint for colums of multiple related tables

I have the following tables:

server (server_id as PK)

server_id |  server_name
----------+---------------
 3251623  |  ServerOfDoom
 5578921  |  BestServerEU

server_groups
Each server can have multiple server groups, every group name within a server must be unique.

 id | server_id |  group_name
----+-----------+---------------
  1 |  3251623  | mods
  2 |  5578921  | admins
  3 |  5578921  | anothergroup

group_tags
Each group can have multiple tags, a tag can be connected to multiple groups. Each tag name must be unique.

 id | tag_name
----+--------------
  1 | mods
  2 | admins
  3 | ag

group_tag_relations

 group_id | tag_id
----------+--------
        2 |      1
        3 |      3
        1 |      2

Problem:

I need group tag names and server group names to be unique within one server. So a tag can be connected to multiple groups, but only to one group per server. Or the tag "foo" cannot be connected to the group "bar" of server z because server z already has a group "foo". The same goes for the other way around, so if the group "foo" had the tag "hey" you shouldn’t be able to add a group "hey" to the same server.

Basically names should not appear multiple times on a server, no matter if tag or group name.

How would I implement such a Constraint?

EDIT

So basically I’m trying to convert this JSON format into SQL tables:

{
    "5578921": {
        "Server Name": "Server1",
        ...
        "Groups": {
            "Game1": {
                "Tags": [
                    "g1",
                    "gameone"
                ], ...
            },
            "Game2": {
                "Tags": [
                    "g2",
                    "gametwo"
                ], ...
            }
        }
    },
    "3251623": ...
}

The id’s 5578921 and 3251623 should just represent Discord server id’s, so every id is for one server my bot is on. The Json file is all the information of my bot for each server, but it’s not as reliable and scalable as a database, so I wanted to convert it.

So the upper tables are what I came up with:

A One-To-Many relation between server and server_groups and a Many-To-Many relation between server_groups and group_tags (so instead of storing duplicate tags I can just assign them to different groups).

I just want to make sure there are no duplicate names on one server and asking how to do so for my current tables.

Advertisement

Answer

After hours of suffering I finally got what I wanted:

Get all Tags of a server

CREATE OR REPLACE FUNCTION get_server_tags(serverid BIGINT) 
RETURNS TABLE(group_name VARCHAR(100), tag_name VARCHAR(100), group_id BIGINT, tag_id BIGINT)
AS
$$
    SELECT group_name, tag_name, group_id, tag_id FROM group_tag_relations 
    JOIN server_groups 
    ON server_groups.server_id = serverid
    AND server_groups.id = group_tag_relations.group_id
    JOIN group_tags
    ON group_tags.id = group_tag_relations.tag_id
$$
language sql
stable;

Get all Groups of a server

CREATE OR REPLACE FUNCTION get_server_group(serverid BIGINT, groupname VARCHAR(100)) 
RETURNS TABLE(group_name VARCHAR(100), group_id BIGINT)
AS
$$
    SELECT group_name, id
    FROM server_groups 
    WHERE server_id = serverid 
    AND lower(group_name) = lower(groupname);
$$
language sql
stable;

Get a Group by Name (Calling both Functions above)

CREATE OR REPLACE FUNCTION get_group_by_name(serverid BIGINT, groupname VARCHAR(100))
RETURNS TABLE(group_name VARCHAR(100), group_id BIGINT)
AS
$$
BEGIN
    RETURN QUERY SELECT get_server_group.group_name, get_server_group.group_id
    FROM get_server_group(serverid, groupname);

    IF NOT found THEN 
        RETURN QUERY SELECT get_server_tags.group_name, get_server_tags.group_id
        FROM get_server_tags(serverid)
        WHERE lower(tag_name) = lower(groupname);
    END IF;
END;
$$
language plpgsql
stable;

Update Trigger for server_groups table, checking wether the name is already taken on a server

CREATE OR REPLACE FUNCTION group_name_update() 
RETURNS TRIGGER
AS
$$ 
BEGIN
    PERFORM get_group_by_name(OLD.server_id, NEW.group_name);
    
    IF lower(OLD.group_name) = lower(NEW.group_name) THEN
        RETURN NEW;
    ELSIF found THEN
        RETURN OLD;
    ELSE
        RETURN NEW;
    END IF;
END;
$$
language plpgsql
volatile;


CREATE TRIGGER group_name_update_trigger 
BEFORE UPDATE ON server_groups
FOR EACH ROW EXECUTE PROCEDURE group_name_update();

Insert Trigger for server_groups table, checking wether the name is already taken on a server

CREATE OR REPLACE FUNCTION group_name_insert() 
RETURNS TRIGGER
AS
$$ 
BEGIN
    PERFORM get_group_by_name(NEW.server_id, NEW.group_name);

    IF found THEN
        RETURN OLD;
    ELSE
        RETURN NEW;
    END IF;
END;
$$
language plpgsql
volatile;

CREATE TRIGGER group_name_insert_trigger 
BEFORE INSERT ON server_groups
FOR EACH ROW EXECUTE PROCEDURE group_name_insert();

Insert Trigger for group_tag_relation table, checking wether the tag is already taken on a server (as the tags wont get updated there’s no need for an update trigger)

CREATE OR REPLACE FUNCTION group_tag_relation_insert()
RETURNS TRIGGER
AS
$$ 
BEGIN
    PERFORM get_group_by_name((SELECT server_id FROM server_groups WHERE id = NEW.group_id), (SELECT tag_name FROM group_tags WHERE id = tag_id));

    IF found THEN
        RETURN OLD;
    ELSE
        RETURN NEW;
    END IF;
END;
$$
language plpgsql
volatile;

CREATE TRIGGER group_tag_relation_insert_trigger 
BEFORE INSERT ON group_tag_relations
FOR EACH ROW EXECUTE PROCEDURE group_tag_relation_insert();
User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement