Multiple ID's in a field. Postgresql - postgresql

So, i'm currently working with a database system where a user can register, login and update his/her details whenever.
The database includes 5 roles:
1. Public
2. Member
3. Moderator
4. Coordinator
5. Admin
I want to be able to assign multiple roles to my users. For example, the Admin can also be a member. Therefore in the database it should show:
User_id | Role_ID
------------------------
user1 | 2, 5
^ is it possible to add multivalued id's in postgresql?

You can use filed of type array to store list of values.
However I think that there is much better way to organize what you want.
Make one table: role_names and another roles, like that:
CREATE TABLE role_names
(
id serial NOT NULL,
name text NOT NULL,
CONSTRAINT role_names_pkey PRIMARY KEY (id),
CONSTRAINT role_names_name_key UNIQUE (name)
);
CREATE TABLE roles
(
user_id bigint NOT NULL,
role_id bigint NOT NULL,
CONSTRAINT roles_pkey PRIMARY KEY (user_id, role_id),
CONSTRAINT roles_role_id_fkey FOREIGN KEY (role_id)
REFERENCES role_names (id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE RESTRICT
);
Now in the table role_names you put all the roles that you want to have.
In the table roles, you can assign or delete any number of roles to any user.
Also you can search in table roles for specific users or specific roles - much neat and faster than searching into arrays I think.
Feel free to add FK constraint for the user_id field too.

Yes, you can use int array to store list of roles.
Here's related question -Junction tables vs foreign key arrays?

Related

How do I define the main/primary record of a one-to-many relationship?

I'm making a service with an account table and a profile table. An account can own multiple profiles, but accounts always have a primary profile. This would initially be the first profile created alongside the account upon registration, but the user could add profiles and set one as the primary profile later.
I've tried the following setup:
create table account (
id uuid primary key,
email text unique,
phone text unique,
created_at timestamptz,
primary_profile_id uuid references profile on delete restrict,
);
create table profile (
id uuid primary key,
account_id uuid references account on delete cascade,
username text unique,
about text,
created_at timestamptz
);
This doesn't work because:
You can't even run that to create the tables since they both rely on the other existing beforehand. A workaround would be to create the account table without primary_profile_id and alter the table to add that column after the creation of the profile table, however...
Even if you can create the tables, you can't add records to them because you would need the other to exist first to reference it. It is technically possible if you leave primary_profile_id as NULL and change it after the profile has been created, but the columns aren't supposed to be nullable, so it's not really ideal.
Is there any good solution to this? I've also considered having a primary boolean on the profile table, but then there's nothing on the database side preventing that being true for multiple profiles under a single account.
Thanks for any help :)
Your idea of enforcing your requirement with a foreign key is good.
Creating the tables is no problem; you can simply run
create table account (
id uuid primary key,
email text unique,
phone text unique,
created_at timestamptz,
primary_profile_id uuid,
);
create table profile (
id uuid primary key,
account_id uuid references account on delete cascade,
username text unique,
about text,
created_at timestamptz
);
ALTER TABLE account
ADD FOREIGN KEY (primary_profile_id) REFERENCES profile
DEFERRABLE INITIALLY DEFERRED;
A deferred foreign key constraint like that is not checked right when a row is inserted, but at the end of the transaction. So you can add account first and then the matching profile, as long as you Insert both in the same transaction.
I recommend using NOT NULL in your column definitions wherever possible.

Simulate a primary key for a nullable column

I handle members' roles in a table with this structure:
id: id of the row
id_member: integer, foreign key is 'id' column in 'members' table
id_role: integer, foreign key is 'id' column in 'roles' table
date_start: timestamp when this user gets the role
date_end: timestamp when this user loses the role
When I add a role, the date_start is set with current_timestamp, and date_end is null.
When I remove a role, the date_end is set with current_timestamp.
I don't want a user to have several roles at the same time, so initially I thought about setting a triple primary key: id_member, id_role and date_end, but it appears I can't put a nullable column as primary key.
How could I change the structure of the table so that I can prevent a user having 2 active roles? I thought about adding a active column but not only would it overcharge the structure, but also I won't be able to save 2 historical roles (if a user was ROLE3 during 4 different periods, for example).
Thanks in advance.
I don't want a user to have several roles at the same time
Partial UNIQUE index
So, each member can only have a single active role (date_end IS NULL).
A partial UNIQUE index will enforce that:
CREATE UNIQUE INDEX tbl_member_active_role_uni ON tbl (id_member)
WHERE date_end IS NULL; -- active role
See:
Create unique constraint with null columns
PostgreSQL multi-column unique constraint and NULL values
EXCLUDE
The above still allows to add historic entries that overlap. To disallow that, too, use an exclusion constraint. You'll need the additional module btree_gist for your integer column. See:
PostgreSQL EXCLUDE USING error: Data type integer has no default operator class
Then:
ALTER TABLE tbl ADD CONSTRAINT tbl_member_no_overlapping_role
EXCLUDE USING gist (id_member with =, tsrange(date_start, date_end) WITH &&);
NULL values for date_end happen to work perfectly. In a range types, NULL as upper bound signifies "unbounded".
See:
How to ensure entries with non-overlapping time ranges?

Composite FK referencing atomic PK + non unique attribute

I am trying to create the following tables in Postgres 13.3:
CREATE TABLE IF NOT EXISTS accounts (
account_id Integer PRIMARY KEY NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
user_id Integer PRIMARY KEY NOT NULL,
account_id Integer NOT NULL REFERENCES accounts(account_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS calendars (
calendar_id Integer PRIMARY KEY NOT NULL,
user_id Integer NOT NULL,
account_id Integer NOT NULL,
FOREIGN KEY (user_id, account_id) REFERENCES users(user_id, account_id) ON DELETE CASCADE
);
But I get the following error when creating the calendars table:
ERROR: there is no unique constraint matching given keys for referenced table "users"
Which does not make much sense to me since the foreign key contains the user_id which is the PK of the users table and therefore also has a uniqueness constraint. If I add an explicit uniqueness constraint on the combined user_id and account_id like so:
ALTER TABLE users ADD UNIQUE (user_id, account_id);
Then I am able to create the calendars table. This unique constraint seems unnecessary to me as user_id is already unique. Can someone please explain to me what I am missing here?
Postgres is so smart/dumb that it doesn't assume the designer to do stupid things.
The Postgres designers could have taken different strategies:
Detect the transitivity, and make the FK not only depend on users.id, but also on users.account_id -> accounts.id. This is doable but costly. It also involves adding multiple dependency-records in the catalogs for a single FK-constraint. When imposing the constraint(UPDATE or DELETE in any of the two referred tables), it could get very complex.
Detect the transitivity, and silently ignore the redundant column reference. This implies: lying to the programmer. It would also need to be represented in the catalogs.
cascading DDL operations would get more complex, too. (remember: DDL is already very hard w.r.t. concurrency/versioning)
From the execution/performance point of view: imposing the constraints currently involves "pseudo triggers" on the referred table's indexes. (except DEFERRED, which has to be handled specially)
So, IMHO the Postgres developers made the sane choice of refusing to do stupid complex things.

Postgres conditional unique constraint

Pretend I have a users table where users are members of a specific tenant, and their e-mails are uniquely indexed to their tenant, like this:
User
id | tenant_id | email
1 1 person1#example.com
2 1 person2#example.com
This user is allowed because despite a duplicate e-mail, they are at a different tenant:
3 2 person1#example.com
This user is prevented because the e-mail is a duplicate at the same tenant:
4 2 person1#example.com <--- will throw an error
We have this much covered with a unique index -- that part is easy.
Now pretend that I want to be able to add a global user that can access all tenants, but only if the e-mail doesn't already exist in the table at all. Additionally, once the record exists, nobody else -- whether tenanted or not -- will be able to use the same e-mail.
For clarity, the global users could simply have a null tenant ID but we would likely also add a global boolean.
Is there a way to write constraints for this logic? You can't simply make e-mails globally uniquely constrained because they won't be able to be repeated across tenants, and if you index with a null tenant ID, postgres will allow an untenanted user if there are tenanted users with the same e-mail.
I've looked at exclusion constraints and checks but couldn't figure out how to combine them (uniquely constrain e-mail globally if tenant_id is null, and check for records with null tenant ID and matching e-mail when inserting any record).
Please don't ask why I'm doing things this way -- my table isn't actually users and we've considered and dismissed other architectures :)
Thanks in advance!
According to PostgreSQL Documentation you can create unique partial index which will be effectively the same as creating unique partial constraint on your table:
CREATE UNIQUE INDEX some_index ON some_table (col_a) WHERE (col_b is null);
Using this technique you can create 2 separate unique indexes for admin and non-admin users.
You can use a UNIQUE constraint for both fields:
create table myUsers
(
id int not null,
tenant int not null,
email varchar(200) not null,
UNIQUE(email, tenant)
);
insert into myUsers values
(1, 1, 'person1#example.com'),
(2, 1, 'person2#example.com');
insert into myUsers values
(3, 2, 'person1#example.com');
Next insert will throw an error:
insert into myUsers values
(4, 2, 'person1#example.com');
Error(s), warning(s):
23505: duplicate key value violates unique constraint
"myusers_email_tenant_key"
Check it here: http://rextester.com/AJZVI34616
For the second part of the question:
Now pretend that I want to be able to add a global user that can access all tenants, but only if the e-mail doesn't already exist in the table at all.
One solution could be to reserve a tenant for admin users:
tenant = 0 <-- admin users
But the UNIQUE constraint allow duplicated emails, I recommend you to add a rol field to this table, or have another table of admin users for this purpose.
In my case, we use two tables, and both have a rol field.

Multiple entries in PostgreSQL

I'm using a PostgreSQL 9.3 Database to create a small Userdatabase. One user can be a part of different groups.
There are 2 Tables in the Database:
user and group
The data type of all columns is text
Every row in user is a single user, every row in group is a single group.One column in user should be groups
How may i configure the groups-column to add multiple entries of group to a single user
What you are describing is a standard many-to-many relationship in a relational database.
You solve this problem by creating a mapping table between users and groups:
If you have something like this currently:
create table users (id integer primary key, user_name text);
create table groups (id integer primary key, group_name text);
The mapping table would look something like this:
create table user_groups
(
user_id not null references users,
group_id not null references groups,
primary key (user_id, group_id)
);
By using (user_id, group_id) as the primary key, you make sure that a user can only be mapped to the same group once.