Adding constraint in PostgreSQL that requires information from another table - sql

I have the following schema in PostgreSQL
CREATE TABLE exam (
exam_id INT,
exam_name TEXT,
total_marks INT,
PRIMARY KEY(exam_id)
);
CREATE TABLE studentmarks (
studentmarks_id INT,
student_id INT,
exam_id INT,
marks_scored INT,
PRIMARY KEY(studentmarks_id),
FOREIGN KEY(exam_id) REFERENCES exam ON DELETE SET NULL,
);
How can I enforce the constraint that studentmarks.marks_scored <= exam.total_marks such that the behaviour is just like the CHECK constraint?

Use trigger.
You need to create trigger function first.
-- FUNCTION: public.check_marks_calculation()
-- DROP FUNCTION public.check_marks_calculation();
CREATE FUNCTION public.check_marks_calculation()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS $BODY$
DECLARE
_exam_id integer;
_total_marks integer;
BEGIN
IF (TG_OP = 'INSERT') THEN
_exam_id = NEW.exam_id;
_total_marks = (select total_marks from exam where exam_id=_exam_id);
if(NEW.marks_scored <= _total_marks) then
RETURN NEW;
else
raise exception 'Student Marks greater than exam Total Marks.';
end if;
end if;
END;
$BODY$;
ALTER FUNCTION public.check_marks_calculation()
OWNER TO postgres;
Then create trigger.
CREATE TRIGGER check_toal_marks
BEFORE INSERT
ON public.studentmarks
FOR EACH ROW
EXECUTE FUNCTION public.check_marks_calculation();
NOTE I have tested in postgres

Related

How to put argument (table name) of function?

I am using PostgreSQL 14.4 . My script
-- 0.
DROP TABLE IF EXISTS tenant;
CREATE TABLE tenant
(
id smallint primary key,
company_tax_code character varying(14),
period character varying(16), -- 2021070420220705
created timestamp with time zone
);
CREATE OR REPLACE FUNCTION set_id_tenant()
RETURNS trigger AS
$$
DECLARE
BEGIN
new.id = (select coalesce(max(id), -32769) from tenant) + 1;
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER trigger_insert_without_id_tenant
BEFORE INSERT
ON tenant
FOR EACH ROW
EXECUTE PROCEDURE set_id_tenant();
CREATE INDEX tenant_idx ON tenant (id);
COMMIT;
--------------------------------------------------------------------------------
-- 4.
DROP TABLE IF EXISTS account_object_bank_account;
CREATE TABLE account_object_bank_account
(
id smallint,
account_object_id smallint not null,
bank_account character varying(64),
bank_name character varying(128),
bank_id smallint,
sort_order smallint,
bank_branch_name character varying(256),
province character varying(128),
tenant_id smallint,
PRIMARY KEY (id, tenant_id),
CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES tenant (id)
);
CREATE OR REPLACE FUNCTION account_object_bank_account_setId()
RETURNS trigger AS
$$
DECLARE
BEGIN
new.id = (select coalesce(max(id), -32769) from account_object_bank_account where tenant_id = new.tenant_id) + 1;
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER account_object_bank_account_trig_insertWithoutId
BEFORE INSERT
ON account_object_bank_account
FOR EACH ROW
EXECUTE PROCEDURE account_object_bank_account_setId();
CREATE INDEX account_object_bank_account_idx ON account_object_bank_account (id, tenant_id);
COMMENT ON TABLE public.account_object_bank_account IS 'Bảng lưu tài khoản ngân hàng của khách hàng';
COMMENT ON COLUMN public.account_object_bank_account.id IS 'PK';
COMMENT ON COLUMN public.account_object_bank_account.account_object_id IS 'FK';
COMMENT ON COLUMN public.account_object_bank_account.bank_account IS 'Số TK ngân hàng';
COMMENT ON COLUMN public.account_object_bank_account.bank_name IS 'Tên ngân hàng';
COMMIT;
--------------------------------------------------------------------------------
-- 5.
DROP TABLE IF EXISTS account_object_belong_to_group;
CREATE TABLE account_object_belong_to_group
(
id smallint,
account_object_id smallint,
account_object_group_id smallint,
tenant_id smallint,
PRIMARY KEY (id, tenant_id),
CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES tenant (id)
);
CREATE OR REPLACE FUNCTION account_object_belong_to_group_setId()
RETURNS trigger AS
$$
DECLARE
BEGIN
new.id = (select coalesce(max(id), -32769) from account_object_belong_to_group where tenant_id = new.tenant_id) + 1;
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER account_object_belong_to_group_trig_insertWithoutId
BEFORE INSERT
ON account_object_belong_to_group
FOR EACH ROW
EXECUTE PROCEDURE account_object_belong_to_group_setId();
CREATE INDEX account_object_belong_to_group_idx ON account_object_belong_to_group (id, tenant_id);
COMMIT;
--------------------------------------------------------------------------------
I want re-use this custom function, then put argument of function is table name:
account_object_bank_account_setId()
account_object_belong_to_group_setId()
to
set_id( table_name )
argument sample: account_object_bank_account, account_object_belong_to_group.
How to do?
update your 2 triggers with set_id('table_name')
and use TG_ARGV[] to retrieve the parameter in the trigger function
https://www.postgresql.org/docs/14/plpgsql-trigger.html

Postgres string formatting for record ID

I am trying to create a table to keep an archive of dad jokes in Postgres. For the title record, I would like the value to by-default be the joke ID, but formatted in a way where if the id is 7, the record's title is Joke #7. Here is my query to create the table:
CREATE TABLE public.jokes (
id int NOT null primary KEY,
user_id int NOT NULL DEFAULT 1,
title varchar NULL DEFAULT FORMAT("Joke #%s", ), -- the title that I would like to be formatted
body varchar NOT NULL,
CONSTRAINT jokes_fk FOREIGN KEY (user_id) REFERENCES public."Users"(id)
);
You need to create a trigger function that will change the title if it is not set
create function change_title() returns trigger as $$
begin
if new.title is null then
new.title := format('joke #%s', new.id);
end if;
return new;
end; $$ language plpgsql;
create trigger change_title_jokes before insert on jokes for each row execute procedure change_title();
Demo in sqldaddy.io

Is it possible to create a cross relationship constraint in postgresql? [duplicate]

I would like to add a constraint that will check values from related table.
I have 3 tables:
CREATE TABLE somethink_usr_rel (
user_id BIGINT NOT NULL,
stomethink_id BIGINT NOT NULL
);
CREATE TABLE usr (
id BIGINT NOT NULL,
role_id BIGINT NOT NULL
);
CREATE TABLE role (
id BIGINT NOT NULL,
type BIGINT NOT NULL
);
(If you want me to put constraint with FK let me know.)
I want to add a constraint to somethink_usr_rel that checks type in role ("two tables away"), e.g.:
ALTER TABLE somethink_usr_rel
ADD CONSTRAINT CH_sm_usr_type_check
CHECK (usr.role.type = 'SOME_ENUM');
I tried to do this with JOINs but didn't succeed. Any idea how to achieve it?
CHECK constraints cannot currently reference other tables. The manual:
Currently, CHECK expressions cannot contain subqueries nor refer to
variables other than columns of the current row.
One way is to use a trigger like demonstrated by #Wolph.
A clean solution without triggers: add redundant columns and include them in FOREIGN KEY constraints, which are the first choice to enforce referential integrity. Related answer on dba.SE with detailed instructions:
Enforcing constraints “two tables away”
Another option would be to "fake" an IMMUTABLE function doing the check and use that in a CHECK constraint. Postgres will allow this, but be aware of possible caveats. Best make that a NOT VALID constraint. See:
Disable all constraints and table checks while restoring a dump
A CHECK constraint is not an option if you need joins. You can create a trigger which raises an error instead.
Have a look at this example: http://www.postgresql.org/docs/9.1/static/plpgsql-trigger.html#PLPGSQL-TRIGGER-EXAMPLE
CREATE TABLE emp (
empname text,
salary integer,
last_date timestamp,
last_user text
);
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- Check that empname and salary are given
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname cannot be null';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
END IF;
-- Who works for us when she must pay for it?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
END IF;
-- Remember who changed the payroll when
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;
CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
...i did it so (nazwa=user name, firma = company name) :
CREATE TABLE users
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
nazwa character varying(20),
firma character varying(50)
);
CREATE TABLE test
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
firma character varying(50),
towar character varying(20),
nazwisko character varying(20)
);
ALTER TABLE public.test ENABLE ROW LEVEL SECURITY;
CREATE OR REPLACE FUNCTION whoIAM3() RETURNS varchar(50) as $$
declare
result varchar(50);
BEGIN
select into result users.firma from users where users.nazwa = current_user;
return result;
END;
$$ LANGUAGE plpgsql;
CREATE POLICY user_policy ON public.test
USING (firma = whoIAM3());
CREATE FUNCTION test_trigger_function()
RETURNS trigger AS $$
BEGIN
NEW.firma:=whoIam3();
return NEW;
END
$$ LANGUAGE 'plpgsql'
CREATE TRIGGER test_trigger_insert BEFORE INSERT ON test FOR EACH ROW EXECUTE PROCEDURE test_trigger_function();

PostgreSQL Trigger Function get stack and does not update the table

I have a trigger that calls a function. It basically must update the same table after update. But it get stacks and does not update anything.
This is my trigger:
CREATE OR REPLACE FUNCTION invtransferences_products_after()
RETURNS TRIGGER AS
$BODY$
DECLARE
TR invtransferences_products%ROWTYPE;
v_transfer_cost NUMERIC;
BEGIN
IF(TG_OP='INSERT') THEN
TR := NEW;
RAISE NOTICE 'INVTRANSFERENCE PRODUCT ADDED %',TR.id;
UPDATE invtransferences_products
SET product_cost = (get_product_composition_cost(product_id, 0)*quantity )
WHERE invtransferences_products.id=TR.id;
ELSE
IF (TG_OP='UPDATE') THEN
TR := NEW;
RAISE NOTICE 'INVTRANSFERENCE PRODUCTS UPDATED %',TR.id;
UPDATE invtransferences_products
SET product_cost = (get_product_composition_cost(product_id, 0)*quantity )
WHERE invtransferences_products.id=TR.id;
END IF;
END IF;
RETURN TR;
END
$BODY$
LANGUAGE plpgsql;
This is my table invtransferences_products:
CREATE TABLE invtransferences_products
(
id serial NOT NULL,
invtransference_id bigint NOT NULL,
product_id bigint NOT NULL,
quantity numeric DEFAULT 1 NOT NULL,
created timestamp DEFAULT now() NOT NULL,
modified timestamp,
rcv_quantity numeric DEFAULT 0 NOT NULL,
pnd_quantity numeric DEFAULT 0 NOT NULL,
product_cost numeric
);
ALTER TABLE invtransferences_products
ADD CONSTRAINT invtransferences_products_pkey
PRIMARY KEY (id);
ALTER TABLE invtransferences_products
ADD CONSTRAINT invtransferences_products_invtransference_id_fkey FOREIGN KEY (invtransference_id)
REFERENCES invtransferences (id)
ON UPDATE CASCADE
ON DELETE CASCADE;
COMMIT;
What's wrong?? Help please.
The problem is that the UPDATE statement in the trigger function causes the trigger to fire again.
Instead of issuing a separate update, you should manipulate the data in NEW.
Something like:
CREATE OR REPLACE FUNCTION invtransferences_products_after()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF(TG_OP='INSERT') THEN
RAISE NOTICE 'INVTRANSFERENCE PRODUCT ADDED %',NEW.id;
ELSE
IF (TG_OP='UPDATE') THEN
RAISE NOTICE 'INVTRANSFERENCE PRODUCTS UPDATED %',NEW.id;
END IF;
END IF;
NEW.product_cost := get_product_composition_cost(NEW.product_id,0)*NEW.quantity ;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
Fiddle at: SQLFiddle
This trigger will cause infinite recursion ending up with a stack depth limit exceeded error, because it issues another UPDATE of the table each time an INSERT/UPDATE occurs on the same table.
The solution is, instead of this:
UPDATE invtransferences_products
SET product_cost = (get_product_composition_cost(product_id, 0)*quantity )
WHERE invtransferences_products.id=TR.id;
It should do that:
NEW.product_cost := get_product_composition_cost(NEW.product_id, 0)*NEW.quantity;
and declare the trigger as running BEFORE UPDATE or INSERT (not AFTER).
That's the more logical approach.
As a workaround, recursion can also be blocked outside of the trigger. This is answered in Prevent recursive trigger in PostgreSQL.

Trigger to delete rows from related tables before deleting rows from actual table

I have the following tables:
CREATE TABLE QUESTION(
id varchar(10) NOT NULL PRIMARY KEY,
que_type numeric(1));
CREATE TABLE ESSAY(
que_id varchar(10) NOT NULL PRIMARY KEY,
ans varchar(2000),
FOREIGN KEY (que_id) REFERENCES QUESTION (id));
CREATE TABLE TFFB(
que_id varchar(10) NOT NULL PRIMARY KEY,
ans varchar(50),
FOREIGN KEY (que_id) REFERENCES QUESTION (id));
CREATE TABLE MCQ(
que_id varchar(10) NOT NULL PRIMARY KEY,
ans varchar(200),
FOREIGN KEY (que_id) REFERENCES QUESTION (id));
and try to create trigger so that when I delete from the main table, it will delete related rows from other tables:
CREATE OR REPLACE FUNCTION delete_question()
RETURNS trigger AS $delete_question$
DECLARE
BEGIN
IF ( (OLD).que_type = '1' ) THEN
IF EXISTS (SELECT 1 FROM mcq WHERE person_id = (OLD).id) THEN
DELETE FROM mcq WHERE que_id='(OLD).id';
END IF;
ELSIF ( (OLD).que_type = '2' OR OLD.que_type = '3' ) THEN
IF EXISTS (SELECT 1 FROM tffb WHERE person_id = (OLD).id) THEN
DELETE FROM tffb WHERE que_id='(OLD).id';
END IF;
ELSIF ( (OLD).que_type = '4' ) THEN
IF EXISTS (SELECT 1 FROM essay WHERE person_id = (OLD).id) THEN
DELETE FROM essay WHERE que_id='(OLD).id';
END IF;
END IF;
RETURN NULL;
END;
$delete_question$ LANGUAGE plpgsql;
CREATE TRIGGER delete_question
BEFORE DELETE ON question
FOR EACH ROW EXECUTE PROCEDURE delete_question();
When I delete data from question, the row disappear for a while. But when I refresh, it still there.
I tried to put RETURN OLD; but it failed because of the constrain relation.
What is wrong with this?
Some more advice on your trigger function:
CREATE OR REPLACE FUNCTION delete_question()
RETURNS trigger AS
$func$
BEGIN
CASE OLD.que_type
WHEN 1 THEN
DELETE FROM mcq WHERE que_id=OLD.id;
WHEN 2, 3 THEN
DELETE FROM tffb WHERE que_id=OLD.id;
WHEN 4 THEN
DELETE FROM essay WHERE que_id=OLD.id;
-- ELSE
-- Do something?
END CASE;
RETURN OLD;
END
$func$ LANGUAGE plpgsql;
Major points
Your check for existence with a SELECT statement doubles the cost. Just run the DELETE, if no matching row is found, nothing is deleted.
Use a CASE statement here. Shorter, faster. Note that plpgsql CASE is slightly different from SQL CASE statement. For instance, you can list several cases at once.
You don't need the DECLARE keyword, unless you actually declare variables.
Alternative design
You could avoid the problem altogether by cascading deletes via foreign key, as #a_horse mentioned in the comment. My schema layout would look like this:
CREATE TABLE question (
question_id serial NOT NULL PRIMARY KEY
,que_type int -- this may be redundant as well
);
CREATE TABLE essay (
que_id int NOT NULL PRIMARY KEY
REFERNECES question(question_id) ON UPDATE CASCADE
ON DELETE CASCADE
,ans text
);
...
About serial:
Auto increment SQL function
i then try solve it again.... IT WORKS PERFECTLY. YEAY.
CREATE OR REPLACE FUNCTION delete_question()
RETURNS trigger AS $delete_question$
DECLARE
BEGIN
IF ( OLD.que_type=1 ) THEN
IF EXISTS (SELECT 1 FROM mcq WHERE que_id=OLD.id) THEN
DELETE FROM mcq WHERE que_id=OLD.id;
END IF;
ELSIF ( OLD.que_type=2 OR OLD.que_type=3) THEN
IF EXISTS (SELECT 1 FROM tffb WHERE que_id=OLD.id) THEN
DELETE FROM tffb WHERE que_id=OLD.id;
END IF;
ELSIF ( OLD.que_type=4 ) THEN
IF EXISTS (SELECT 1 FROM essay WHERE que_id=OLD.id) THEN
DELETE FROM essay WHERE que_id=OLD.id;
END IF;
END IF;
RETURN OLD;
END;
$delete_question$ LANGUAGE plpgsql;
CREATE TRIGGER delete_question
BEFORE DELETE ON question
FOR EACH ROW EXECUTE PROCEDURE delete_question();
there is no ' ' on OLD.id or even (OLD).id and use RETURN OLD;