Delete form table doesn't work psql - postgresql

So I'm currently working on PostgreSQL (10.4) and need to simulate a "file system" within my database. In order to achieve this goal I have two tables :
-- represent a directory
CREATE TABLE USERS_DIRECTORIES
(
id SERIAL NOT NULL,
users_id INTEGER NOT NULL,
name VARCHAR(64) NOT NULL,
description VARCHAR(64) NOT NULL DEFAULT '',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(users_id, name),
PRIMARY KEY (id),
FOREIGN KEY (users_id) REFERENCES USERS(id)
);
-- represent links between a directory and its sub-directories
CREATE TABLE SUB_DIRECTORIES
(
id SERIAL NOT NULL,
-- parent directory
parent_id INTEGER,
-- sub directory
child_id INTEGER NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES USERS_DIRECTORIES(id),
FOREIGN KEY (child_id) REFERENCES USERS_DIRECTORIES(id) ON DELETE CASCADE
);
The issue occur when I need to delete a directory. In order to do it correctly I need to delete all sub directories.
CURRENT SOLUTION
To delete a sub directory I need to delete its reference in the 'SUB_DIRECTORIES' tables to not trigger the foreign key exception. to solve this problem I use two triggered procedures.
-- triggered when I delete a directory to delete a reference
CREATE OR REPLACE FUNCTION delete_reference()
RETURNS TRIGGER AS
$$
BEGIN
raise notice 'OLD: %', OLD.id;
DELETE FROM sub_directories WHERE parent_id = OLD.id;
IF FOUND THEN
raise notice 'SUCESS id : %', OLD.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_delete_sub_references BEFORE DELETE ON users_directories
FOR EACH ROW
EXECUTE PROCEDURE delete_reference();
-- triggered when I delete a reference to delete all sub directories
CREATE OR REPLACE FUNCTION delete_refered()
RETURNS TRIGGER AS
$$
BEGIN
raise notice 'OLD child: %', OLD.child_id;
DELETE FROM users_directories WHERE id = OLD.child_id;
IF FOUND THEN
raise notice 'SUCESS child_id : %', OLD.child_id;
END IF;
RETURN NEW;
EXCEPTION
WHEN OTHERS THEN
raise notice 'EXCEPTION';
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_delete_sub_refered AFTER DELETE ON sub_directories
FOR EACH ROW
EXECUTE PROCEDURE delete_refered();
Whenever I delete a directory it triggers 'delete_reference()' function which remove all references of the directory from the tables 'SUB_DIRECTORIES' and when I delete a references from the very same table it triggers the 'delete_refered()' functions which delete the sub_directories and it goes on and on recursively.
PROBLEM
Evertything works fine except that no data is deleted in the table 'USERS_DIRECTORIES'. The references are deleted correctly in the 'SUB_DIRECTORIES' table tho.
Eventhough I try to delete 'by hand' a specific row in the table 'USERS_DIRECTORIES' ('delete from ... where id = ..') it ends up with 'DELETE 0' with a correct query and id.
After much time asking Google for solutions I don't find anything about my issue. Moreover I'm not a native english speaker so it is hard to find good key words defining my problem. If anyone can help me on this one I'll be very grateful.
PS : I can speak french too if it helps.

RETURN OLD; for BEFORE DELETE trigger (this you have wrong), RETURN NEW; for AFTER DELETE.
You can also use OLD in AFTER DELETE triggers, so safest is to always return OLD in DELETE trigger.

Related

Postgres Trigger for Timestamp in related table

I want to use a trigger to auto-update a table's updated_at date, but the problem is that the actual "update" happens in a separate table. I'll water-down the tables to make it more clear:
-- Trigger
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Table to update
CREATE TABLE bug (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
);
CREATE TRIGGER set_timestamp
BEFORE UPDATE ON bug
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();
Fairly standard, but there's no column to actually update. The real "update" is coming from a comment thread table:
CREATE TABLE comment_thread (
bug_id INTEGER REFERENCES bug(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
comment VARCHAR(2000),
PRIMARY KEY (bug_id, user_id)
);
So my question is if there's a way to use that trigger (or some variation of) to update the updated_at field in the related row when creating a comment?
Thanks!

Creating a SQL trigger to update tables when entering data in a view

If I have this setup:
CREATE TABLE category(
category_id serial PRIMARY KEY,
category_name text UNIQUE NOT NULL -- must be UNIQUE
);
CREATE TABLE parts (
part_id serial PRIMARY KEY,
category_id int REFERENCES product,
part_name text
);
CREATE VIEW partview AS
SELECT com.part_id, cat.category_name, com.part_name
FROM parts com
LEFT JOIN category cat USING (category_id);
How do I create a trigger so that when I insert data into the view, the source tables are updated?
I tried this... but it doesn't work :(
CREATE FUNCTION insert_view_func()
RETURNS trigger as
$func$
BEGIN
INSERT INTO parts (category_name)
select (select category_id from category where category_name = category.category_name)
RETURNING category_id as id
into new.componentid;
return new;
END
$func$ language plpgsql;
create trigger insert_view_trig
INSTEAD of insert on partview
for each row execute procedure insert_view_func();
The big issue with view insert triggers on non-simple views is you do not know what is be inserted.
In this case that could be Category or Parts or both. Your trigger has to handle both. Here that is not much a issue here:
create or replace function insert_view_func()
returns trigger
language plpgsql
as $$
begin
insert into category (category_name) values(new.category_name)
on conflict do nothing;
insert into parts (category_id, part_name)
select category_id, new.part_name
from category
where category_name = new.category_name;
return new;
end ;
$$ ;
That, however, is not the major issue here. Your data model setup a 1:M relationship between Category:Parts.
Not a problem if that is what you really want, but it does open a potential problem. Since the Part_Name is not unique it opens
the possibility of multiple parts with the same name (see fiddle), but each associated to a separate category.
This could become quite confusing. To avoid this you may want to consider a M:M relationship and creating a resolution table. The other option would be modifying the trigger function to check for existing part_name. Better yet make Part_Name unique.

postgres update NEW variable before INSERT in a TRIGGER

I've two tables accounts and projects:
create table accounts (
id bigserial primary key,
slug text unique
);
create table projects (
id bigserial primary key,
account_id bigint not null references accounts (id),
name text
);
I want to be able to insert a new row into projects by specifying only account.slug (not account.id). What I'm trying to achieve is something like:
INSERT into projects (account_slug, name) values ('account_slug', 'project_name');
I thought about using a trigger (unfortunately it doesn't work):
create or replace function trigger_projects_insert() returns trigger as $$
begin
if TG_OP = 'INSERT' AND NEW.account_slug then
select id as account_id
from accounts as account
where account.slug = NEW.account_slug;
NEW.account_id = account_id;
-- we should also remove NEW.account_slug but don't know how
end if;
return NEW;
end;
$$ LANGUAGE plpgsql;
create trigger trigger_projects_insert before insert on projects
for each row execute procedure trigger_projects_insert();
What is the best way to achieve what I'm trying to do?
Is a trigger a good idea?
Is there any other solution?
WITH newacc AS (
INSERT INTO accounts (slug)
VALUES ('account_slug')
RETURNING id
)
INSERT INTO projects (account_id, name)
SELECT id, 'project_name'
FROM newacct;
If you are limited in the SQL you can use, another idea might be to define a view over both tables and create an INSTEAD OF INSERT trigger on the view that performs the two INSERTs on the underlying tables. Then an INSERT statement like the one in your question would work.

How to properly emulate statement level triggers with access to data in postgres

I am using PostgreSQL as my database for a project at work. We use triggers in quite a few places to either maintain computed columns, or tables that essentially act as a materialized view.
All this worked just fine when simply utilizing row level triggers to keep all this in sync. However when we wrote scripts to periodically import our customers data into the database, we ran into issues with either performance or problems with number of locks in a single transaction.
To alleviate this I wanted to create a statement-level trigger with access to the modified rows (inserted, updated or deleted). However as this is not possible I instead created a BEFORE statement-level trigger that would create a temporary table. Then an AFTER row-level trigger that would insert the changed data into the temporary table. At last an AFTER statement-level trigger that would read the changes and perform necessary updates, and then drop the temporary table.
All this works just fine, assuming that within the triggers, no one would re-trigger the same flow again (as the temporary table would then already exist).
However I then learned that when using foreign key constraints with ON DELETE SET NULL, it is simply implemented with a system trigger that sets the column to NULL. This of course is not a problem at all, except for the fact that when you have several foreign key constraints like this on a single table, all referencing the same table (let's just call this files). When deleting a row from the files table, all these system level triggers to handle the ON DELETE SET NULL clause all fire at the same time, that is in parallel. Which presents a serious issue for me.
How would I go about implementing something like this? Here is a short SQL script to illustrate the problem:
CREATE TABLE files (
id serial PRIMARY KEY,
"name" TEXT NOT NULL
);
CREATE TABLE profiles (
id serial PRIMARY KEY,
NAME TEXT NOT NULL,
cv_file_id INT REFERENCES files(id) ON DELETE SET NULL,
photo_file_id INT REFERENCES files(id) ON DELETE SET NULL
);
CREATE TABLE profile_audit (
profile_id INT NOT NULL,
modified_at timestamptz NOT NULL
);
CREATE FUNCTION pre_stmt_create_temp_table()
RETURNS TRIGGER
AS $$
BEGIN
CREATE TEMPORARY TABLE tmp_modified_profiles (
id INT NOT NULL
) ON COMMIT DROP;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
CREATE FUNCTION insert_modified_profile_to_temp_table()
RETURNS TRIGGER
AS $$
BEGIN
INSERT INTO tmp_modified_profiles(id) VALUES (NEW.id);
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
CREATE FUNCTION post_stmt_insert_rows_and_drop_temp_table()
RETURNS TRIGGER
AS $$
BEGIN
INSERT INTO profile_audit (id, modified_at)
SELECT t.id, CURRENT_TIMESTAMP FROM tmp_modified_profiles t;
DROP TABLE tmp_modified_profiles;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER tr_create_working_table BEFORE UPDATE ON profiles FOR EACH STATEMENT EXECUTE PROCEDURE pre_stmt_create_temp_table();
CREATE TRIGGER tr_insert_row_to_working_table AFTER UPDATE ON profiles FOR EACH ROW EXECUTE PROCEDURE insert_modified_profile_to_temp_table();
CREATE TRIGGER tr_insert_modified_rows_and_drop_working_table AFTER UPDATE ON profiles FOR EACH STATEMENT EXECUTE PROCEDURE post_stmt_insert_rows_and_drop_temp_table();
INSERT INTO files ("name") VALUES ('photo.jpg'), ('my_cv.pdf');
INSERT INTO profiles ("name") VALUES ('John Doe');
DELETE FROM files WHERE "name" = 'photo.jpg';
It would be a serious hack, but meanwhile, until PostgreSQL 9.5 is out, I would try to use CONSTRAINT triggers deferred to the end of the transaction. I am not really sure this will work, but might be worth trying.
You could use a status column to track inserts and updates for your statement-level triggers.
In a BEFORE INSERT OR UPDATE row-level trigger:
SET NEW.status = TG_OP;
Now you can use statement-level AFTER triggers:
BEGIN
DO FUNNY THINGS
WHERE status = 'INSERT';
-- reset the status
UPDATE mytable
SET status = NULL
WHERE status = 'INSERT';
END;
However, if you want to deal with deletes as well, you'll need something like this in your row-level trigger:
INSERT INTO status_table (table_name, op, id) VALUES (TG_TABLE_NAME, TG_OP, OLD.id);
Then, in your statement-level AFTER trigger, you can go like:
BEGIN
DO FUNNY THINGS
WHERE id IN (SELECT id FROM status_table
WHERE table_name = TG_TABLE_NAME AND op = TG_OP); -- just an example
-- reset the status
DELETE FROM status_table
WHERE table_name = TG_TABLE_NAME AND op = TG_OP;
END;

Insert trigger to Update another table using PostgreSQL

I have a table named awards. How can I mount a Trigger in PostgreSQL where each insert in the table awards updates a different table?
Here we have two tables named table1 and table2. Using a trigger I'll update table2 on insertion into table1.
Create the tables
CREATE TABLE table1
(
id integer NOT NULL,
name character varying,
CONSTRAINT table1_pkey PRIMARY KEY (id)
)
CREATE TABLE table2
(
id integer NOT NULL,
name character varying
)
The Trigger Function
CREATE OR REPLACE FUNCTION function_copy() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO
table2(id,name)
VALUES(new.id,new.name);
RETURN new;
END;
$BODY$
language plpgsql;
The Trigger
CREATE TRIGGER trig_copy
AFTER INSERT ON table1
FOR EACH ROW
EXECUTE PROCEDURE function_copy();
You want the documenation for PL/PgSQL triggers, which discusses just this case among others. The general documentation on triggers may also be useful.
You can use either a BEFORE or AFTER trigger for this. I'd probably use an AFTER trigger so that my trigger saw the final version of the row being inserted, though. You want FOR EACH ROW, of course.