Postgresql aggregate related values using triggers - postgresql

I have a database representing a payroll system.
Each Payroll is related to several PayrollRows (1 to M relationship), and includes a field summarizing its related payrollRows fields.
Simply said, Payroll includes a field called "amountEurToPay" which is the sum of the "paybackEur" fields of its related PayrollRow records.
I am trying to create a trigger function that automatically completes the amountEurToPay field when a Payroll is created. (indeed it will be created directly with its payrollRow)
I have done this :
-- Generate the trigger function
CREATE OR REPLACE TRIGGER new_payroll_creation
AFTER INSERT
ON "Payroll"
FOR EACH ROW
EXECUTE PROCEDURE populate_payroll_amountEur();
For the function, I tried this, but without any success.
CREATE OR REPLACE FUNCTION populate_payroll_amounteur()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
UPDATE "Payroll"
SET "amountEurToPay" = ( SELECT SUM("paybackEur")
FROM "PayrollRow"
WHERE "payrollId" = NEW."id")
WHERE ("id" = NEW."id");
RETURN NEW;
END;
$$
For extra information, my schema looks like this
model PayrollRow {
id Int #id #default(autoincrement())
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
paybackEur Decimal #default(0) #db.Decimal(10, 2)
payroll Payroll #relation(fields: [payrollId], references: [id])
payrollId Int
}
model Payroll {
id Int #id #default(autoincrement())
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
amountEurToPay Decimal #default(0) #db.Decimal(10, 2)
payrollRow PayrollRow[]
}
Could you please provide me some support to achieve that? :)
UPDATE--
It seems that that this works (with a preexisting payroll with id 2) for example, I am a bit confused :
CREATE OR REPLACE FUNCTION populate_payroll_amounteur()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
UPDATE "Payroll"
SET "amountEurToPay" = ( SELECT SUM("paybackEur")
FROM "PayrollRow"
WHERE "payrollId" = 2)
WHERE ("id" = NEW."id");
RETURN NEW;
END;
$$
---- UPDATE for MiTKo
If you request the schema definition , actually I use prisma.io ORM , which simplified for me the creation of the schema.
Some reverse engineering would give something like this :
-- CreateTable
CREATE TABLE "Payroll" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"amountEurToPay" DECIMAL(10,2) NOT NULL DEFAULT 0,
CONSTRAINT "Payroll_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PayrollRow" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"paybackEur" DECIMAL(10,2) NOT NULL DEFAULT 0,
"payrollId" INTEGER NOT NULL,
CONSTRAINT "PayrollRow_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "PayrollRow" ADD CONSTRAINT "PayrollRow_payrollId_fkey" FOREIGN KEY ("payrollId") REFERENCES "Payroll"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

Try this (it's working for update and delete as well)
-- Generate the trigger function
CREATE OR REPLACE TRIGGER new_payroll_creation
AFTER INSERT or UPDATE or delete
ON "PayrollRow"
FOR EACH ROW
EXECUTE PROCEDURE populate_payroll_amountEur();
CREATE OR REPLACE FUNCTION populate_payroll_amounteur()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
UPDATE "Payroll"
SET "amountEurToPay" =
(
SELECT COALESCE(SUM("paybackEur") ,0)
FROM "PayrollRow"
WHERE "payrollId" = COALESCE( NEW."payrollId",OLD."payrollId")
)
WHERE ("id" = (COALESCE( NEW."payrollId",OLD."payrollId")));
RETURN NEW;
END;
$$

Related

I'm having an issue with this code when I try to input values into the transactions table

So I'm setting up a schema in which I can input transactions of a journal entry independent of each other but also that rely on each other (mainly to ensure that debits = credits). I set up the tables, function, and trigger. Then, when I try to input values into the transactions table, I get the error below. I'm doing all of this in pgAdmin4.
CREATE TABLE transactions (
transactions_id UUID PRIMARY KEY DEFAULT uuid_generate_v1(),
entry_id INTEGER NOT NULL,
post_date DATE NOT NULL,
account_id INTEGER NOT NULL,
contact_id INTEGER NULL,
description TEXT NOT NULL,
reference_id UUID NULL,
document_id UUID NULL,
amount NUMERIC(12,2) NOT NULL
);
CREATE TABLE entries (
id UUID PRIMARY KEY,
test_date DATE NOT NULL,
balance NUMERIC(12,2)
CHECK (balance = 0.00)
);
CREATE OR REPLACE FUNCTION transactions_biut()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
EXECUTE 'INSERT INTO entries (id,test_date,balance)
SELECT
entry_id,
post_date,
SUM(amount) AS ''balance''
FROM
transactions
GROUP BY
entry_id;';
END;
$$;
CREATE TRIGGER transactions_biut
BEFORE INSERT OR UPDATE ON transactions
FOR EACH ROW EXECUTE PROCEDURE transactions_biut();
INSERT INTO transactions (
entry_id,
post_date,
account_id,
description,
amount
)
VALUES
(
'1',
'2019-10-01',
'101',
'MISC DEBIT: PAID FOR FACEBOOK ADS',
-200.00
),
(
'1',
'2019-10-01',
'505',
'MISC DEBIT: PAID FOR FACEBOOK ADS',
200.00
);
After I execute this input, I get the following error:
ERROR: column "id" of relation "entries" does not exist
LINE 1: INSERT INTO entries (id,test_date,balance)
^
QUERY: INSERT INTO entries (id,test_date,balance)
SELECT
entry_id,
post_date,
SUM(amount) AS "balance"
FROM
transactions
GROUP BY
entry_id;
CONTEXT: PL/pgSQL function transactions_biut() line 2 at EXECUTE
SQL state: 42703
There are a few problems here:
You're not returning anything from the trigger function => should probably be return NEW or return OLD since you're not modifying anything
Since you're executing the trigger before each row, it's bound to fail for any transaction that isn't 0 => maybe you want a deferred constraint trigger?
You're not grouping by post_date, so your select should fail
You've defined entry_id as INTEGER, but entries.id is of type UUID
Also note that this isn't really going to scale (you're summing up all transactions of all days, so this will get slower and slower...)
#chirs I was able to figure out how to create a functioning solution using statement-level triggers:
CREATE TABLE transactions (
transactions_id UUID PRIMARY KEY DEFAULT uuid_generate_v1(),
entry_id INTEGER NOT NULL,
post_date DATE NOT NULL,
account_id INTEGER NOT NULL,
contact_id INTEGER NULL,
description TEXT NOT NULL,
reference_id UUID NULL,
document_id UUID NULL,
amount NUMERIC(12,2) NOT NULL
);
CREATE TABLE entries (
entry_id INTEGER PRIMARY KEY,
post_date DATE NOT NULL,
balance NUMERIC(12,2),
CHECK (balance = 0.00)
);
CREATE OR REPLACE FUNCTION transactions_entries() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO entries
SELECT o.entry_id, o.post_date, SUM(o.amount) FROM old_table o GROUP BY o.entry_id, o.post_date;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO entries
SELECT o.entry_id, n.post_date, SUM(n.amount) FROM new_table n, old_table o GROUP BY o.entry_id, n.post_date;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO entries
SELECT n.entry_id,n.post_date, SUM(n.amount) FROM new_table n GROUP BY n.entry_id, n.post_date;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER transactions_ins
AFTER INSERT ON transactions
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT EXECUTE PROCEDURE transactions_entries();
CREATE TRIGGER transactions_upd
AFTER UPDATE ON transactions
REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
FOR EACH STATEMENT EXECUTE PROCEDURE transactions_entries();
CREATE TRIGGER transactions_del
AFTER DELETE ON transactions
REFERENCING OLD TABLE AS old_table
FOR EACH STATEMENT EXECUTE PROCEDURE transactions_entries();
Any thoughts on optimization?

Partial update on an postgres upsert violates constraint

I want to be able to upsert partially inside postgres (9.5), but it seems that a partial upsert fails when not all of the constraint is fulfilled (such as the not null constraint)
Here is an example of the scenario and error
CREATE TABLE jobs (
id integer PRIMARY KEY,
employee_name TEXT NOT NULL,
address TEXT NOT NULL,
phone_number TEXT
);
CREATE OR REPLACE FUNCTION upsert_job(job JSONB)
RETURNS VOID AS $$
BEGIN
INSERT INTO jobs AS origin VALUES(
(job->>'id')::INTEGER,
job->>'employee_name'::TEXT,
job->>'address'::TEXT,
job->>'phone_number'::TEXT
) ON CONFLICT (id) DO UPDATE SET
employee_name = COALESCE(EXCLUDED.employee_name, origin.employee_name),
address = COALESCE(EXCLUDED.address, origin.address),
phone_number = COALESCE(EXCLUDED.phone_number, origin.phone_number);
END;
$$ LANGUAGE PLPGSQL SECURITY DEFINER;
--Full insert (OK)
SELECT upsert_job('{"id" : 1, "employee_name" : "AAA", "address" : "City, x street no.y", "phone_number" : "123456789"}'::jsonb);
--Partial update that fulfills constraint (Ok)
SELECT upsert_job('{"id" : 1, "employee_name" : "BBB", "address" : "City, x street no.y"}'::jsonb);
--Partial update that doesn't fulfill constraint (FAILS)
SELECT upsert_job('{"id" : 1, "phone_number" : "12345"}'::jsonb);
--ERROR: null value in column "employee_name" violates not-null constraint
--DETAIL: Failing row contains (1, null, null, 12345).
How do I go around approaching this ?
To think of it another way, what if the id didn't already exist? You can't insert just a phone number as it would have no name/address but that's exactly what you are telling it to do. So the constraint gets mad and it fails because an upsert tries to insert first and then updates if the insert fails. But your insert didn't get past the constraint check to see if it already existed.
What you can do instead if you want partials is tell it how to handle partials that would violate the constraints. Something like this (this is NOT complete and doesn't handle all partial data scenarios):
CREATE OR REPLACE FUNCTION upsert_job(job JSONB)
RETURNS VOID AS $$
BEGIN
IF (job->>'phone_number' IS NOT NULL
AND job->>'employee_name' IS NOT NULL
AND job->>'address' IS NOT NULL) THEN
INSERT INTO jobs AS origin VALUES(
(job->>'id')::INTEGER,
job->>'employee_name'::TEXT,
job->>'address'::TEXT,
job->>'phone_number'::TEXT
) ON CONFLICT (id) DO UPDATE SET
employee_name = COALESCE(EXCLUDED.employee_name, origin.employee_name),
address = COALESCE(EXCLUDED.address, origin.address),
phone_number = COALESCE(EXCLUDED.phone_number, origin.phone_number);
ELSIF (job->>'phone_number' IS NOT NULL AND (job->>'employee_name' IS NULL AND job->>'address' IS NULL)) THEN
UPDATE jobs SET phone_number=job->>'phone_number'::TEXT
WHERE id=(job->>'id')::INTEGER;
END IF;
END;
$$ LANGUAGE PLPGSQL SECURITY DEFINER;

Functions and Triggers in PostgreSql

As I am new to DBs, I am learning PostgreSql and for a sample, I am trying to small scenario on Mobile Recharge Database System. The below is the query I have. I want to know what is the problem with the function I have written which should only return balance amount of an account number which is nothing but the customer id.
And, I also want to add the value in the wallet table when we add some value (that's topup kind of).
Please help me in this. Thanks.
The below is the complete query:
CREATE DATABASE "RECHARGESYS"
WITH OWNER = postgres
ENCODING = 'UTF8'
TABLESPACE = pg_default
LC_COLLATE = 'English_United States.1252'
LC_CTYPE = 'English_United States.1252'
CONNECTION LIMIT = -1;
--SERVICEPROVIDER TABLE:
DROP TABLE SERVICE_PROVIDERS;
CREATE TABLE SERVICE_PROVIDERS
(
SPID VARCHAR(5) PRIMARY KEY CHECK(SPID LIKE 'S%'),
SPNAME VARCHAR(50)
);
--CUSTOMER TABLE:
DROP TABLE CUSTOMER;
CREATE TABLE CUSTOMER
(
CID INT PRIMARY KEY,
CNAME VARCHAR(50)
);
--RECHARGE TABLE:
DROP TABLE RECHARGE;
CREATE TABLE RECHARGE
(
RID INT PRIMARY KEY,
CID INT REFERENCES CUSTOMER(CID),
SPID VARCHAR(5) REFERENCES SERVICE_PROVIDERS(SPID) CHECK(SPID LIKE 'S%'),
RENUMBER BIGINT,
AMOUNT INT
);
--TRANSACTION TABLE:
DROP TABLE TRANSACTION;
CREATE TABLE TRANSACTION
(
TID INT PRIMARY KEY,
SPID VARCHAR(5) REFERENCES SERVICE_PROVIDERS(SPID) CHECK(SPID LIKE('S%')),
RID INT REFERENCES RECHARGE(RID)
);
--WALLET TABLE:
DROP TABLE WALLET;
CREATE TABLE WALLET
(
WID INT PRIMARY KEY,
CID INT REFERENCES CUSTOMER(CID),
WAMOUNT INT
);
INSERT INTO SERVICE_PROVIDERS VALUES ('S1001', 'AIRTEL');
INSERT INTO SERVICE_PROVIDERS VALUES ('S1002', 'AIRCEL');
INSERT INTO SERVICE_PROVIDERS VALUES ('S1003', 'TATA DOCOMO');
INSERT INTO SERVICE_PROVIDERS VALUES ('S1004', 'IDEA');
INSERT INTO SERVICE_PROVIDERS VALUES ('S1005', 'VODAFONE');
SELECT * FROM SERVICE_PROVIDERS;
INSERT INTO CUSTOMER VALUES('20001','AHMED');
INSERT INTO CUSTOMER VALUES('20002','ASIF');
INSERT INTO CUSTOMER VALUES('20003','AHSRAF');
INSERT INTO CUSTOMER VALUES('20004','MAHESH');
INSERT INTO CUSTOMER VALUES('20005','ARUN');
SELECT * FROM CUSTOMER;
INSERT INTO WALLET VALUES('30001','20001','1000');
INSERT INTO WALLET VALUES('30002','20002','1000');
INSERT INTO WALLET VALUES('30003','20003','1000');
INSERT INTO WALLET VALUES('30004','20004','1000');
INSERT INTO WALLET VALUES('30005','20005','1000');
SELECT * FROM WALLET;
--IN THIS FUNCTION I WANT TO CHECK THE BALANCE ONCE I GIVE THE ACCOUNT NUMBER / CUSTOMER ID (ID):
CREATE OR REPLACE FUNCTION BALANCE(ACCNO INT)
RETURNS INT AS $BAL$
BEGIN
SELECT WAMOUNT INTO BAL FROM WALLET WHERE CID=ACCNO;
RETURN(BAL);
END;
$BAL$ LANGUAGE plpgsql;
SELECT BALANCE('20001') FROM WALLET;
--ALSO I WANT TRIGGER THAT CAN ADD AMOUNT TO THE WALLET.
The problem with your function is that you try to use the variable BAL without declaring it:
CREATE OR REPLACE FUNCTION BALANCE(ACCNO INT)
RETURNS INT AS $BAL$
declare BAL integer; --<--- Add this line
.......
But for a simple select query you don't need a plpgsql function. A sql function will do the job:
CREATE OR REPLACE FUNCTION balance(ACCNO INT)
RETURNS INT AS $$
SELECT wamount FROM wallet WHERE cid=ACCNO;
$$ LANGUAGE sql;

Trigger after insert, update a related model Postgresql

This is the first time I'm creating a trigger, so I've got a little bit confused. I'm following this guide.
Here is what I've done so far:
DROP TRIGGER IF EXISTS "update_metas" ON "post";
CREATE TRIGGER "update_metas"
AFTER INSERT ON "post"
FOR EACH ROW EXECUTE PROCEDURE update_post_count();
I have two tables: user and post. What I need to do is to increment the column user.postCount for each new post created. The foreign key is post.user_id
The procedure I'm creating is the following:
CREATE FUNCTION update_post_count() RETURNS TRIGGER AS $updates_user_postCount$
BEGIN
-- I know that NEW contains the new post info, so I
-- can gather the user_id by doing NEW.post_id.
--
-- What exactly should I do here?
RETURN NEW;
END;
$updates_user_postCount$ LANGUAGE plpgsql;
How should I structure this procedure? Can I just use a direct SQL query, something like:
UPDATE "user"
SET "user"."post_count" = "user"."post_count" + 1
WHERE "user"."_id" = NEW.idol_id;
UPDATE
I've tried using that SQL statement inside the procedure, but it returns the error error: column "user" of relation "user" does not exist.
Here is the SQL statement that I used to create both user and post tables:
CREATE TABLE IF NOT EXISTS "user" (
_id BIGSERIAL UNIQUE,
__id TEXT UNIQUE,
fbid VARCHAR(100),
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(512) NOT NULL,
profile_picture VARCHAR(512),
profile_cover VARCHAR(512),
profile_about TEXT,
profile_birth_date BIGINT,
social_facebook VARCHAR(256),
social_twitter VARCHAR(256),
social_instagram VARCHAR(256),
post_count BIGINT,
highlighted BOOLEAN,
idol BOOLEAN,
free BOOLEAN,
blocked BOOLEAN
);
CREATE TABLE IF NOT EXISTS "post" (
_id BIGSERIAL UNIQUE,
__id TEXT UNIQUE,
idol_id BIGINT,
removed BOOLEAN,
free BOOLEAN,
created_at BIGINT,
hashtags VARCHAR(1024),
audio_src VARCHAR(512),
audio_size INTEGER,
audio_length INTEGER,
FOREIGN KEY ("idol_id") REFERENCES "user"("_id")
);
Your trigger function is largely correct. The only problem is that an UPDATE statement cannot use the table.column notation.
From the documentation: Do not include the table's name in the specification of a target column — for example, UPDATE tab SET tab.col = 1 is invalid.
CREATE FUNCTION update_post_count() RETURNS TRIGGER AS $updates_user_postCount$
BEGIN
UPDATE "user"
SET "post_count" = "post_count" + 1
WHERE "_id" = NEW.idol_id;
RETURN NEW;
END;
$updates_user_postCount$ LANGUAGE plpgsql;

Need foreign key as array

CREATE TABLE test ( id int PRIMARY KEY , name );
CREATE TABLE test1 ( id integer[] REFERENCES test , rollid int );
ERROR: foreign key constraint "test3_id_fkey" cannot be implemented
DETAIL: Key columns "id" and "id" are of incompatible types: integer[] and integer.
after that I try to another way also
CREATE TABLE test1 ( id integer[] , rollid int);
ALTER TABLE test1 ADD CONSTRAINT foreignkeyarray FOREIGN KEY (id) REFERENCES test;
ERROR: foreign key constraint "fkarray" cannot be implemented
DETAIL: Key columns "id" and "id" are of incompatible types: integer[] and integer.
so I try create a foreign key array means it say error. please tell me anyone?
postgresql version is 9.1.
What you're trying to do simply can't be done. At all. No ifs, no buts.
Create a new table, test1_test, containing two fields, test1_id, test_id. Put the foreign keys as needed on that one, and make test1's id an integer.
Using arrays with foreign element keys is usually a sign of incorrect design. You need to do separate table with one to many relationship.
But technically it is possible. Example of checking array values without triggers. One reusable function with paramethers and dynamic sql. Tested on PostgreSQL 10.5
create schema if not exists test;
CREATE OR REPLACE FUNCTION test.check_foreign_key_array(data anyarray, ref_schema text, ref_table text, ref_column text)
RETURNS BOOL
RETURNS NULL ON NULL INPUT
LANGUAGE plpgsql
AS
$body$
DECLARE
fake_id text;
sql text default format($$
select id::text
from unnest($1) as x(id)
where id is not null
and id not in (select %3$I
from %1$I.%2$I
where %3$I = any($1))
limit 1;
$$, ref_schema, ref_table, ref_column);
BEGIN
EXECUTE sql USING data INTO fake_id;
IF (fake_id IS NOT NULL) THEN
RAISE NOTICE 'Array element value % does not exist in column %.%.%', fake_id, ref_schema, ref_table, ref_column;
RETURN false;
END IF;
RETURN true;
END
$body$;
drop table if exists test.t1, test.t2;
create table test.t1 (
id integer generated by default as identity primary key
);
create table test.t2 (
id integer generated by default as identity primary key,
t1_ids integer[] not null check (test.check_foreign_key_array(t1_ids, 'test', 't1', 'id'))
);
insert into test.t1 (id) values (default), (default), (default); --ok
insert into test.t2 (id, t1_ids) values (default, array[1,2,3]); --ok
insert into test.t2 (id, t1_ids) values (default, array[1,2,3,555]); --error
If you are able to put there just values from test.id, then you can try this:
CREATE OR REPLACE FUNCTION test_trigger() RETURNS trigger
LANGUAGE plpgsql AS $BODY$
DECLARE
val integer;
BEGIN
SELECT id INTO val
FROM (
SELECT UNNEST(id) AS id
FROM test1
) AS q
WHERE id = OLD.id;
IF val IS NULL THEN RETURN OLD;
ELSE
RAISE 'Integrity Constraint Violation: ID "%" in Test1', val USING ERRCODE = '23000';
RETURN NULL;
END IF;
END; $BODY$;
-- DROP TRIGGER test_delete_trigger ON test;
CREATE TRIGGER test_delete_trigger BEFORE DELETE OR UPDATE OF id ON test
FOR EACH ROW EXECUTE PROCEDURE test_trigger();