Trigger after insert, update a related model Postgresql - 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;

Related

Postgresql aggregate related values using triggers

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;
$$

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;

Access database, Sql query , Error "Syntax error in DROP TABLE or DROP INDEX."

This is the query , running this in C#.
n getting above error
"DROP TABLE IF EXISTS `NATIONAL_ID_ISSUANCE_CENTER`;
CREATE TABLE `NATIONAL_ID_ISSUANCE_CENTER` (
`ID` INTEGER NOT NULL AUTO_INCREMENT,
`NAME` VARCHAR(100),
`APPLICATION_ID` INTEGER,
`STATUS` INTEGER,
`CREATED_BY` INTEGER,
`UPDATED_BY` INTEGER,
`CREATED_DATE` DATETIME,
`UPDATED_DATE` DATETIME,
`THIRD_PARTY_ID` INTEGER,
`PROVINCE_ID` INTEGER,
INDEX (`APPLICATION_ID`),
PRIMARY KEY (`ID`),
INDEX (`PROVINCE_ID`),
INDEX (`THIRD_PARTY_ID`)
)"
You can't put an IF statement inside Drop and Create statements. Anytime you want to drop a table that you're not sure exists, use the following:
IF(OBJECT_ID('[Database].[Schema].[TableName]') is not null)
BEGIN
DROP TABLE [Database].[Schema].[TableName];
END;
Please note you should replace [Database], [Schema], and [TableName] with the appropriate database, schema, and table names, respectively.

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();

Get row to swap tables on a certain condition

I currently have a parent table:
CREATE TABLE members (
member_id SERIAL NOT NULL, UNIQUE, PRIMARY KEY
first_name varchar(20)
last_name varchar(20)
address address (composite type)
contact_numbers varchar(11)[3]
date_joined date
type varchar(5)
);
and two related tables:
CREATE TABLE basic_member (
activities varchar[3])
INHERITS (members)
);
CREATE TABLE full_member (
activities varchar[])
INHERITS (members)
);
If the type is full the details are entered to the full_member table or if type is basic into the basic_member table. What I want is that if I run an update and change the type to basic or full the tuple goes into the corresponding table.
I was wondering if I could do this with a rule like:
CREATE RULE tuple_swap_full
AS ON UPDATE TO full_member
WHERE new.type = 'basic'
INSERT INTO basic_member VALUES (old.member_id, old.first_name, old.last_name,
old.address, old.contact_numbers, old.date_joined, new.type, old.activities);
... then delete the record from the full_member
Just wondering if my rule is anywhere near or if there is a better way.
You don't need
member_id SERIAL NOT NULL, UNIQUE, PRIMARY KEY
A PRIMARY KEY implies UNIQUE NOT NULL automatically:
member_id SERIAL PRIMARY KEY
I wouldn't use hard coded max length of varchar(20). Just use text and add a check constraint if you really must enforce a maximum length. Easier to change around.
Syntax for INHERITS is mangled. The key word goes outside the parens around columns.
CREATE TABLE full_member (
activities text[]
) INHERITS (members);
Table names are inconsistent (members <-> member). I use the singular form everywhere in my test case.
Finally, I would not use a RULE for the task. A trigger AFTER UPDATE seems preferable.
Consider the following
Test case:
Tables:
CREATE SCHEMA x; -- I put everything in a test schema named "x".
-- DROP TABLE x.members CASCADE;
CREATE TABLE x.member (
member_id SERIAL PRIMARY KEY
,first_name text
-- more columns ...
,type text);
CREATE TABLE x.basic_member (
activities text[3]
) INHERITS (x.member);
CREATE TABLE x.full_member (
activities text[]
) INHERITS (x.member);
Trigger function:
Data-modifying CTEs (WITH x AS ( DELETE ..) are the best tool for the purpose. Requires PostgreSQL 9.1 or later.
For older versions, first INSERT then DELETE.
CREATE OR REPLACE FUNCTION x.trg_move_member()
RETURNS trigger AS
$BODY$
BEGIN
CASE NEW.type
WHEN 'basic' THEN
WITH x AS (
DELETE FROM x.member
WHERE member_id = NEW.member_id
RETURNING *
)
INSERT INTO x.basic_member (member_id, first_name, type) -- more columns
SELECT member_id, first_name, type -- more columns
FROM x;
WHEN 'full' THEN
WITH x AS (
DELETE FROM x.member
WHERE member_id = NEW.member_id
RETURNING *
)
INSERT INTO x.full_member (member_id, first_name, type) -- more columns
SELECT member_id, first_name, type -- more columns
FROM x;
END CASE;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Trigger:
Note that it is an AFTER trigger and has a WHEN condition.
WHEN condition requires PostgreSQL 9.0 or later. For earlier versions, you can just leave it away, the CASE statement in the trigger itself takes care of it.
CREATE TRIGGER up_aft
AFTER UPDATE
ON x.member
FOR EACH ROW
WHEN (NEW.type IN ('basic ','full')) -- OLD.type cannot be IN ('basic ','full')
EXECUTE PROCEDURE x.trg_move_member();
Test:
INSERT INTO x.member (first_name, type) VALUES ('peter', NULL);
UPDATE x.member SET type = 'full' WHERE first_name = 'peter';
SELECT * FROM ONLY x.member;
SELECT * FROM x.basic_member;
SELECT * FROM x.full_member;