Simulate a primary key for a nullable column - postgresql

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?

Related

PostgreSQL how to build a query which updates data to an existing table but to avoid duplicate value in a specific column

I'm trying to build a very specific PSQL query that has to update a table data adding to a column called sign_order a number that identifies an order of an entity called recipient to sign a document.
To be clear but out of scope is that there is a document that has recipients and the recipients they have to sign that order on a specific order.
I tried initially the following query but the issue is that works if we have no data on the table but soon as it runs on a table filled it generates duplicates for the same type
UPDATE
recipients
SET
sign_order = CASE
"role"
WHEN 'CONSENTEE' THEN 1
WHEN 'GUARDIAN' THEN 2
WHEN 'ASSENTEE' THEN 3
WHEN 'COUNTERSIGNEE' THEN 4
END
WHERE
"role" IN ('CONSENTEE', 'GUARDIAN', 'ASSENTEE', 'COUNTERSIGNEE');
ALTER TABLE recipients ALTER COLUMN sign_order SET NOT NULL;
So what happening here is that is adding the signed order when it finds the case but creates a duplicate for example if that finds already a Guardian it ads as 2 but for the same document we can have multiple Guardian and we have an issue as that is added as Guardian 2 then Guardian 2 but should be Guardian 2 Guardian 3 and so on.
The same affects the rest.
A view of the issue where the recipients under the same document are assigned with the same signing order:
select consentee_id, "role" , count(*)
from recipients
group by "role", consentee_id
order by "count" desc;
Result
The base case order is as in the query;
1 CONSENTEE
2 GUARDIAN
3 ASSENTEE
4 COUNTERSIGNEE
This order needs to be maintained and as an example of the right output running the query should be:
recipient
sign_order
CONSENTEE
1
GUARDIAN
2
GUARDIAN
3
GUARDIAN
4
ASSENTEE
5
COUNTERSIGNEE
6
COUNTERSIGNEE
7
COUNTERSIGNEE
8
I need to add 2 constraints as unique for these consentee_id and sign_order and then make the query more complex to solve the problem described.
The ALTER part I believe with the constraints will look as it is
ALTER TABLE recipients ALTER COLUMN sign_order SET NOT NULL
AND
ADD CONSTRAINT unique_sign_order UNIQUE (consentee_id, sign_order);
However, need some help to achieve the goal of this solving problem issue.
Update to give a better view of the table
Table properties
DDL
CREATE TABLE public.recipients (
id text NOT NULL,
name text NOT NULL,
email text NULL,
phone text NULL,
locale text NULL,
consentee_id text NULL,
is_primary bool NULL DEFAULT false,
sign_order int4 NOT NULL,
"role" text NOT NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at timestamptz NULL,
CONSTRAINT recipients_id_unique UNIQUE (id),
CONSTRAINT recipients_pkey PRIMARY KEY (id),
CONSTRAINT recipients_consentee_id_foreign FOREIGN KEY (consentee_id) REFERENCES public.consentees(id) ON DELETE CASCADE
);
Assuming you have some as yet unmentioned unique column (PK perhaps) you can assign the sign_order value in a CTE, then the main portion of the query updates the column for the PK. I've also assumed your table also contains more that 1 document to be signed (if not it a pretty limited table) with its own signing order. In the following these column are id and doc_id respectively. If those assumptions are invalid, well do not know if this will work.
The big change from what you had was to move the CASE expression into an order by clause of a row_number window function. Then assign sort_order from the row_number value and that is a CTE. (see demo)
with assign_sign_order ( rid, sorder) as
( select id, row_number() over (partition by doc_id
order by
case "role"
when 'CONSENTEE' then 1
when 'GUARDIAN' then 2
when 'ASSENTEE' then 3
when 'COUNTERSIGNEE' then 4
end
) so
, "role", doc_id
from recipients order by id
)
update recipients r
set sign_order = s.sorder
from assign_sign_order s
where r.id = s.rid ;
An alternative would be to create a ENUM for the "role", this simplifies the above query, and would reduce maintenance to just updating the ENUM (no change to the query). Providing you do not want to remove one. It is also in the demo.

Cross table constraints in PostgreSQL

Using PostgreSQL 9.2.4, I have a table users with a 1:many relation to the table user_roles. The users table stores both employees and other kinds of users.
Table "public.users"
Column | Type | Modifiers
-----------------+-------------------+-----------------------------------------------------
uid | integer | not null default nextval('users_uid_seq'::regclass)
employee_number | character varying |
name | character varying |
Indexes:
"users_pkey" PRIMARY KEY, btree (uid)
Referenced by:
TABLE "user_roles" CONSTRAINT "user_roles_uid_fkey" FOREIGN KEY (uid) REFERENCES users(uid)
Table "public.user_roles"
Column | Type | Modifiers
-----------+-------------------+------------------------------------------------------------------
id | integer | not null default nextval('user_roles_id_seq'::regclass)
uid | integer |
role | character varying | not null
Indexes:
"user_roles_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"user_roles_uid_fkey" FOREIGN KEY (uid) REFERENCES users(uid)
I want to ensure that the column users.employee_number cannot be NULL if there is a related row where user_roles.role_name contains an employee role name. That is, I want the database to enforce the constraint that for some roles, users.employee_number must have a value, but not for others.
How can I accomplish this, preferably without user-defined functions or triggers? I found (blog post, SO Answer) that SQL Server supports indexed views, which sounds like it would serve my purpose. However, I assume that materialized views will not work in my case, since they are not dynamically updated.
Clarifications
The formulation of this requirement leaves room for interpretation:
where UserRole.role_name contains an employee role name.
My interpretation:
with an entry in UserRole that has role_name = 'employee'.
Your naming convention is was problematic (updated now). User is a reserved word in standard SQL and Postgres. It's illegal as identifier unless double-quoted - which would be ill-advised. User legal names so you don't have to double-quote.
I am using trouble-free identifiers in my implementation.
The problem
FOREIGN KEY and CHECK constraint are the proven, air-tight tools to enforce relational integrity. Triggers are powerful, useful and versatile features but more sophisticated, less strict and with more room for design errors and corner cases.
Your case is difficult because a FK constraint seems impossible at first: it requires a PRIMARY KEY or UNIQUE constraint to reference - neither allows NULL values. There are no partial FK constraints, the only escape from strict referential integrity are NULL values in the referencing columns due to the default MATCH SIMPLE behavior of FK constraints. Per documentation:
MATCH SIMPLE allows any of the foreign key columns to be null; if any
of them are null, the row is not required to have a match in the referenced table.
Related answer on dba.SE with more:
Two-column foreign key constraint only when third column is NOT NULL
The workaround is to introduce a boolean flag is_employee to mark employees on both sides, defined NOT NULL in users, but allowed to be NULL in user_role:
Solution
This enforces your requirements exactly, while keeping noise and overhead to a minimum:
CREATE TABLE users (
users_id serial PRIMARY KEY
, employee_nr int
, is_employee bool NOT NULL DEFAULT false
, CONSTRAINT role_employee CHECK (employee_nr IS NOT NULL = is_employee)
, UNIQUE (is_employee, users_id) -- required for FK (otherwise redundant)
);
CREATE TABLE user_role (
user_role_id serial PRIMARY KEY
, users_id int NOT NULL REFERENCES users
, role_name text NOT NULL
, is_employee bool CHECK(is_employee)
, CONSTRAINT role_employee
CHECK (role_name <> 'employee' OR is_employee IS TRUE)
, CONSTRAINT role_employee_requires_employee_nr_fk
FOREIGN KEY (is_employee, users_id) REFERENCES users(is_employee, users_id)
);
That's all.
These triggers are optional but recommended for convenience to set the added tags is_employee automatically and you don't have to do anything extra:
-- users
CREATE OR REPLACE FUNCTION trg_users_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = (NEW.employee_nr IS NOT NULL);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF employee_nr ON users
FOR EACH ROW
EXECUTE PROCEDURE trg_users_insup_bef();
-- user_role
CREATE OR REPLACE FUNCTION trg_user_role_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = true;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF role_name ON user_role
FOR EACH ROW
WHEN (NEW.role_name = 'employee')
EXECUTE PROCEDURE trg_user_role_insup_bef();
Again, no-nonsense, optimized and only called when needed.
SQL Fiddle demo for Postgres 9.3. Should work with Postgres 9.1+.
Major points
Now, if we want to set user_role.role_name = 'employee', then there has to be a matching user.employee_nr first.
You can still add an employee_nr to any user, and you can (then) still tag any user_role with is_employee, irregardless of the actual role_name. Easy to disallow if you need to, but this implementation does not introduce any more restrictions than required.
users.is_employee can only be true or false and is forced to reflect the existence of an employee_nr by the CHECK constraint. The trigger keeps the column in sync automatically. You could allow false additionally for other purposes with only minor updates to the design.
The rules for user_role.is_employee are slightly different: it must be true if role_name = 'employee'. Enforced by a CHECK constraint and set automatically by the trigger again. But it's allowed to change role_name to something else and still keep is_employee. Nobody said a user with an employee_nr is required to have an according entry in user_role, just the other way round! Again, easy to enforce additionally if needed.
If there are other triggers that could interfere, consider this:
How To Avoid Looping Trigger Calls In PostgreSQL 9.2.1
But we need not worry that rules might be violated because the above triggers are only for convenience. The rules per se are enforce with CHECK and FK constraints, which allow no exceptions.
Aside: I put the column is_employee first in the constraint UNIQUE (is_employee, users_id) for a reason. users_id is already covered in the PK, so it can take second place here:
DB associative entities and indexing
First, you can solve this using a trigger.
But, I think you can solve this using constraints, with just a little weirdness:
create table UserRoles (
UserRoleId int not null primary key,
. . .
NeedsEmployeeNumber boolean not null,
. . .
);
create table Users (
. . .
UserRoleId int,
NeedsEmployeeNumber boolean,
EmployeeNumber,
foreign key (UserRoleId, NeedsEmployeeNumber) references UserRoles(UserRoleId, NeedsEmployeeNumber),
check ((NeedsEmployeeNumber and EmployeeNumber is not null) or
(not NeedsEmployeeNumber and EmployeeNumber is null)
)
);
This should work, but it is an awkward solution:
When you add a role to an employee, you need to add the flag along with the role.
If a role is updated to change the flag, then this needs to be propagated to existing records -- and the propagation cannot be automatic because you also need to potentially set EmployeeNumber.
New Answer:
This( SQL Sub queries in check constraint ) seems to answer your question, and the language is still in the 9.4 documentation( http://www.postgresql.org/docs/9.4/interactive/sql-createtable.html ).
Old Answer:
SELECT
User.*
, UserRole1.*
FROM
User
LEFT JOIN UserRole UserRole1
ON User.id = UserRole1.UserId
AND (
(
User.employee_number IS NOT NULL AND UserRole1.role_name IN (enumerate employee role names here)
)
OR
(User.employee_number IS NULL)
)
The above query selects all fields from User and all fields from UserRole(aliased as UserRole1). I assumed that the key field between the two fields is known as User.id and UserRole1.UserId, please change these to whatever the real values are.
In the JOIN part of the query there is an OR that on the left side requires an employee number not be NULL in the user table and that UserRole1.role_name be in a list that you must supply to the IN () operator.
The right part of the JOIN is the opposite, it requires that User.employee_number be NULL(this should be your non-employee set).
If you require a more exact solution then please provide more details on your table structures and what roles must be selected for employees.

Multiple ID's in a field. 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?

How to AUTO_INCREMENT in db2?

I thought this would be simple, but I can't seem to use AUTO_INCREMENT in my db2 database. I did some searching and people seem to be using "Generated by Default", but this doesn't work for me.
If it helps, here's the table I want to create with the sid being auto incremented.
create table student(
sid integer NOT NULL <auto increment?>
sname varchar(30),
PRIMARY KEY (sid)
);
Any pointers are appreciated.
You're looking for is called an IDENTITY column:
create table student (
sid integer not null GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1)
,sname varchar(30)
,PRIMARY KEY (sid)
);
A sequence is another option for doing this, but you need to determine which one is proper for your particular situation. Read this for more information comparing sequences to identity columns.
You will have to create an auto-increment field with the sequence object (this object generates a number sequence).
Use the following CREATE SEQUENCE syntax:
CREATE SEQUENCE seq_person
MINVALUE 1
START WITH 1
INCREMENT BY 1
CACHE 10
The code above creates a sequence object called seq_person, that starts with 1 and will increment by 1. It will also cache up to 10 values for performance. The cache option specifies how many sequence values will be stored in memory for faster access.
To insert a new record into the "Persons" table, we will have to use the nextval function (this function retrieves the next value from seq_person sequence):
INSERT INTO Persons (P_Id,FirstName,LastName)
VALUES (seq_person.nextval,'Lars','Monsen')
The SQL statement above would insert a new record into the "Persons" table. The "P_Id" column would be assigned the next number from the seq_person sequence. The "FirstName" column would be set to "Lars" and the "LastName" column would be set to "Monsen".
hi If you are still not able to make column as AUTO_INCREMENT while creating table. As a work around first create table that is:
create table student(
sid integer NOT NULL
sname varchar(30),
PRIMARY KEY (sid)
);
and then explicitly try to alter column bu using the following
alter table student alter column sid set GENERATED BY DEFAULT AS
IDENTITY
Or
alter table student alter column sid set GENERATED BY DEFAULT
AS IDENTITY (start with 100)
Added a few optional parameters for creating "future safe" sequences.
CREATE SEQUENCE <NAME>
START WITH 1
INCREMENT BY 1
NO MAXVALUE
NO CYCLE
CACHE 10;

Unique constraint with soft deleted rows excluded

We have a table with a unique constraint on it, for feedback left from one user, for another, in relation to a sale.
ALTER TABLE feedback
ADD CONSTRAINT unique_user_subject_and_sale
UNIQUE (user_id, subject_id, sale_id)
This ensures we don't accidentally get duplicated rows of feedback.
Currently we sometimes hard-delete feedback left in error and left the user leave it again. We want to change to soft-delete:
ALTER TABLE feedback
ADD COLUMN deleted_at timestamptz
If deleted_at IS NOT NULL, consider the feedback deleted, though we still have the audit trail in our DB (and will probably show it ghosted out to site admins).
How can we keep our unique constraint when we're using soft-delete like this? Is it possible without using a more general CHECK() constraint the does an aggregate check (I've never tried using check constraint like this).
It's like I need to append a WHERE clause to the constraint.
Your unique index, later edited out.
CREATE UNIQUE INDEX feedback_unique_user_subject_and_sale_null
ON feedback(user_id, subject_id, sale_id)
WHERE deleted_at IS NULL
Your unique index has at least two side effects that might cause you some trouble.
In other tables, you can't set a foreign key constraint that references "feedback". A foreign key reference requires some combination of columns to be declared as either primary key or unique.
Your unique index allows multiple rows that differ only in the "deleted_at" timestamp. So it's possible to end up with rows that look like the example below. Whether this is a problem is application-dependent.
Example
user_id subject_id sale_id deleted_at
--
1 1 1 2012-01-01 08:00:01.33
1 1 1 2012-01-01 08:00:01.34
1 1 1 2012-01-01 08:00:01.35
PostgreSQL documents this kind of index as a partial index, should you need to Google it sometime. Other platforms use different terms for it--filtered index is one. You can limit the problems to a certain extent with a pair of partial indexes.
CREATE UNIQUE INDEX feedback_unique_user_subject_and_sale_null
ON feedback(user_id, subject_id, sale_id)
WHERE deleted_at IS NULL
CREATE UNIQUE INDEX feedback_unique_user_subject_and_sale_not_null
ON feedback(user_id, subject_id, sale_id)
WHERE deleted_at IS NOT NULL
But I see no reason to go to this much trouble, especially given the potential problems with foreign keys. If your table looks like this
create table feedback (
feedback_id integer primary key,
user_id ...
subject_id ...
sale_id ...
deleted_at ...
constraint unique_user_subj_sale
unique (user_id, subject_id, sale_id)
);
then all you need is that unique constraint on {user_id, subject_id, sale_id}. You might further consider making all deletes use the "deleted_at" column instead of doing a hard delete.
Despite the fact the PostgreSQL documentation advises against using a unique index instead of a constraint (if the point is to have a constraint), it appears you can do
CREATE UNIQUE INDEX feedback_unique_user_subject_and_sale
ON feedback(user_id, subject_id, sale_id)
WHERE deleted_at IS NULL
You could create a constraint with deleted_at signature.
ALTER TABLE feedback
ADD CONSTRAINT unique_user_subject_and_sale
UNIQUE (user_id, subject_id, sale_id, deleted_at)
This does not create problems like a unique index.
The only problem would be removing user feedback, creating a new one, and then deleting it again in the same time.
And considering the accuracy of the column, the user would have to fit in 1 microsecond. Which, let's face it, is impossible, even if he did it through, the time between these requests will definitely be greater than 1 microsecond.
The only problem is that null value isn't checking in unique. So it won't works.
But you could add a default value for not deleted rows as the lowest value you could store in that column.
If you cannot accept the default value for deleted_at, if the object is not deleted, you can consider adding a deleted_token column and generating a key for it if you delete a value, and for an undeleted object, keep 0 or whatever.