postgres updatable view and unique constraints - sql

in my simple application I would like to create a view in order to allow users filling data of my db.
Here a little example of my data
CREATE TABLE specie
(
specie_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
nome_comune TEXT UNIQUE,
nome_scientifico TEXT UNIQUE
);
CREATE TABLE rilevatore
(
rilevatore_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
nome_cognome TEXT UNIQUE,
telefono INTEGER,
email TEXT,
ente_appartenenza TEXT
);
CREATE TABLE evento_investimento
(
evento_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
data DATE,
ora TIME WITHOUT TIME ZONE,
rilevatore_id INT REFERENCES rilevatore (rilevatore_id),
specie_id INT REFERENCES specie(specie_id),
);
This is the VIEW I created
CREATE VIEW investimenti_vista AS
SELECT
evento_investimento.evento_id,
evento_investimento.ora,
evento_investimento.data,
rilevatore.nome_cognome,
rilevatore.telefono,
rilevatore.email,
rilevatore.ente_appartenenza,
specie.nome_comune,
specie.nome_scientifico
from
evento_investimento
JOIN specie ON evento_investimento.specie_id = specie.specie_id
JOIN rilevatore ON evento_investimento.rilevatore_id = rilevatore.rilevatore_id;
When I attempt to fill the data I receive an error from postgres since view generated from different tables aren't updatable by default.
Thus, I implemetend the following trigger to overcome this issue.
CREATE OR REPLACE FUNCTION inserimento_vista() RETURNS trigger AS $$
BEGIN
INSERT INTO evento_investimento (data,ora)
VALUES (NEW.data,NEW.ora);
INSERT INTO rilevatore (nome_cognome, telefono, email, ente_appartenenza)
VALUES (NEW.nome_cognome, NEW.telefono, NEW.email, NEW.ente_appartenenza);
INSERT INTO specie (nome_comune, nome_scientifico)
VALUES (NEW.nome_comune, NEW.nome_scientifico);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
create trigger inserimento_vista_trg
instead of insert on investimenti_vista for each row EXECUTE procedure inserimento_vista();
However this is not working due to unique contraints I have in the rilevatore and specie tables. How I can solve this?
Thanks

You might try to check for the existence of the conflicting values like this:
CREATE OR REPLACE FUNCTION inserimento_vista() RETURNS trigger AS $$
BEGIN
INSERT INTO evento_investimento (data,ora)
VALUES (NEW.data,NEW.ora);
if not exists(select * from rilevatore where rilevatore.nome_cognome=new.nome_cognome) then
INSERT INTO rilevatore (nome_cognome, telefono, email, ente_appartenenza)
VALUES (NEW.nome_cognome, NEW.telefono, NEW.email, NEW.ente_appartenenza);
end if;
if not exists(select * from specie where specie.nome_comune=new.nome_comune) then
INSERT INTO specie (nome_comune, nome_scientifico)
VALUES (NEW.nome_comune, NEW.nome_scientifico);
end if;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
You might want to add to this an update the specie and/or rilevatore tables with the non-conflicting values but that's up to you :-)

Related

Oracle masking data using a trigger

Im trying to create a process that masks data. When I create the trigger I'm getting the error.
ORA-04072: invalid trigger type
I'm unsure why and was hoping someone can explain what the problem is and how to fix it.
The end result is when a user queries cards they should see the masked data and WHEN they query CARDS_TBL they should see all the data (unmasked)
Original implementation
CREATE TABLE CARDS (
CARD_ID NUMBER
GENERATED BY DEFAULT AS IDENTITY,
CARD_STR VARCHAR2(16) NOT NULL,
PRIMARY KEY (CARD_ID)
);
INSERT INTO CARDS(CARD_STR) VALUES('4024007187788590');
INSERT INTO CARDS(CARD_STR) VALUES('5432223398564536');
INSERT INTO CARDS(CARD_STR) VALUES('5430445512530934');
INSERT INTO CARDS(CARD_STR) VALUES('4020156755227854');
INSERT INTO CARDS(CARD_STR) VALUES('5431248766892318');
CREATE OR REPLACE VIEW CARDS_V AS
SELECT
CARD_ID,
REGEXP_REPLACE(CARD_STR, '(^\d{3})(.*)(\d{4}$)', '\1**********\3') AS CARD_STR
FROM CARDS;
CREATE OR REPLACE TRIGGER CARDS_TRG_INSERT INSTEAD OF
INSERT ON CARDS_V
FOR EACH ROW
BEGIN
INSERT INTO CARDS (CARD_STR) VALUES (:NEW.CARD_STR);
END;
INSERT INTO CARDS_V (CARD_STR) VALUES ('4011589733550908');
CREATE OR REPLACE TRIGGER CARDS_TRG_UPDATE INSTEAD OF
UPDATE ON CARDS_V
FOR EACH ROW
BEGIN
UPDATE CARDS
SET CARD_STR = :NEW.CARD_STR
WHERE CARD_ID = :OLD.CARD_ID;
END;
CREATE TABLE CARDS_TBL (
CARD_ID NUMBER
GENERATED BY DEFAULT AS IDENTITY,
CARD_STR VARCHAR2(16) NOT NULL,
PRIMARY KEY (CARD_ID)
);
INSERT INTO CARDS_TBL(CARD_STR) VALUES('4024007187788590');
INSERT INTO CARDS_TBL(CARD_STR) VALUES('5432223398564536');
INSERT INTO CARDS_TBL(CARD_STR) VALUES('5430445512530934');
INSERT INTO CARDS_TBL(CARD_STR) VALUES('4020156755227854');
INSERT INTO CARDS_TBL(CARD_STR) VALUES('5431248766892318');
CREATE OR REPLACE VIEW CARDS AS
SELECT
CARD_ID,
REGEXP_REPLACE(CARD_STR, '(^\d{3})(.*)(\d{4}$)', '\1**********\3') AS CARD_STR
FROM CARDS_TBL;
CREATE OR REPLACE TRIGGER CARDS_TBL_TRG_UPDATE BEFORE UPDATE ON CARDS_TBL
FOR EACH ROW
BEGIN
UPDATE CARDS_TBL
SET CARD_STR = :NEW.CARD_STR
WHERE CARD_ID = :OLD.CARD_ID;
END;
/
CREATE OR REPLACE TRIGGER CARDS_TBL_TRG_INSERT BEFORE INSERT ON CARDS
FOR EACH ROW
BEGIN
INSERT INTO CARDS_TBL (CARD_STR) VALUES (:NEW.CARD_STR);
END;
INSERT INTO CARDS_TBL (CARD_STR) VALUES ('2222333344445555');
SELECT * FROM CARDS_TBL;
UPDATE CARDS_TBL
SET CARD_STR = '2222333344445566'
WHERE CARD_ID = 6;
/
SELECT * FROM CARDS;
In this particular example, at least, the trigger CARDS_TBL_TRG_UPDATE doesn't do anything (except raise a MUTATING TABLE exception) and can be dispensed with. Get rid of it and your example runs as expected. See this db<>fiddle

SQL Trigger Wont Generate Primary Key

I am trying to create a table that logs all inserts in the author table. Here is the author table, and the Audit_log Table:
CREATE TABLE Author(AuthorID INT PRIMARY KEY NOT NULL,
last_name CHAR(20),
first_name CHAR(20));
CREATE TABLE Audit_Log(Action_ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
table_name Char(40),
action_name Char(6),
Date_Time DATETIME DEFAULT CURRENT_TIMESTAMP);
Here is the simple trigger.
DELIMITER $$
CREATE TRIGGER Author_Trigger AFTER INSERT
ON Author
FOR EACH ROW BEGIN
INSERT INTO Audit_Log VALUES('Author', 'INSERT', CURRENT_TIMESTAMP);
END $$
DELIMITER ;
However, when I cause the trigger to occur by inserting into the author table, it says that the columns do not match row 1. How come my Primary key does not get auto generated, despite having the AUTO_INCREMENT Constraint?
How can I get this trigger to generate the primary key?
Basically MySQL thinks that 'Author' is being inserted as the PRIMARY KEY (Action_ID). What you can do to avoid this is specify wich attribute correspond to wich element of the INSERT query. This should work :)
DELIMITER $$
CREATE TRIGGER Author_Trigger AFTER INSERT
ON Author
FOR EACH ROW BEGIN
INSERT INTO Audit_Log(table_name, action_name, Date_Time)
VALUES('Author', 'INSERT', CURRENT_TIMESTAMP);
END $$
DELIMITER ;
For the value to autoincrement, since you do not have defined the data you must insert and place the autoincremental, it does not take it, so the first value is null and through this null it will autoincrement.
DELIMITER $$
CREATE TRIGGER Author_Trigger AFTER INSERT
ON Author
FOR EACH ROW BEGIN
INSERT INTO Audit_Log VALUES(null,'Author', 'INSERT', CURRENT_TIMESTAMP);
END $$
DELIMITER ;

How to insert a newly generated id into another table with a trigger in postgresql?

Basically, users when they create a new record in mytable1, there is an id field that needs to be the same across multiple tables. I achieve this by having mytable2 with the s_id as primary key
My current function looks like
CREATE OR REPLACE FUNCTION test.new_record()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
case when new.s_id in (select s_id from mytable1) then
insert into mytable2 (sprn, date_created) select max(s_id) +1, now() from mytable2 ;
update mytable1 set new.s_id = (select max(b.s_id) from mytable2 b);
end case;
RETURN new;
END;
$function$;
Intended was when the s_id is replicated then it would create a new entry on mytable2. This new entry would then be updated onto mytable1
Problem with this function is that right now it does not recognise the new on the update part of the function.
How to keep the s_id take the value on every new insert ?
If you want to have one "generator" across multiple tables, create one sequence that is used across all those tables for the default value:
create sequence the_id_sequence;
create table one
(
id integer primary key default nextval('the_id_sequence')
.... other columns
);
create table two
(
id integer primary key default nextval('the_id_sequence')
.... other columns ...
);
If you want to replicate an ID from one table to another during insert, you only need one sequence:
create table one
(
-- using identity is the preferred over "serial" to auto-generate PK values
id integer primary key generated always as identity
);
create table two
(
id integer primary key
);
create or replace function insert_two()
returns trigger
as
$$
begin
insert into two (id) values (new.id);
return new;
end;
$$
language plpgsql;
create trigger replicate_id
before insert on one
for each row
execute procedure insert_two();
Then if you run:
insert into one (id) values (default);
A row with exactly the same id value will be inserted into table two.
If you don't have a generated ID column so far, use the following syntax:
alter table one
add testidcolumn bigint generated always as identity;

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

Postgres trigger-based insert redirection without breaking RETURNING

I'm using table inheritance in postgres, but the trigger I'm using to partition data into the child tables isn't quite behaving right. For example, this query returns nil, but I would like it to return the id of the new record.
INSERT INTO flags (flaggable_id, flaggable_type)
VALUES (233, 'Thank')
RETURNING id;
If I change the return value of the trigger function from NULL to NEW, I get the desired RETURNING behavior, but then two identical rows are inserted in the database. This makes sense, since a non-null return value from the trigger function causes the original INSERT statement execute, whereas returning NULL causes the statement to halt execution. A unique index might halt the second insertion, but would probably raise an error.
Any ideas how to make the INSERT with RETURNING work properly with a trigger like this?
CREATE TABLE flags (
id integer NOT NULL,
flaggable_type character varying(255) NOT NULL,
flaggable_id integer NOT NULL,
body text
);
ALTER TABLE ONLY flags
ADD CONSTRAINT flags_pkey PRIMARY KEY (id);
CREATE TABLE "comment_flags" (
CHECK ("flaggable_type" = 'Comment'),
PRIMARY KEY ("id"),
FOREIGN KEY ("flaggable_id") REFERENCES "comments"("id")
) INHERITS ("flags");
CREATE TABLE "profile_flags" (
CHECK ("flaggable_type" = 'Profile'),
PRIMARY KEY ("id"),
FOREIGN KEY ("flaggable_id") REFERENCES "profiles"("id")
) INHERITS ("flags");
CREATE OR REPLACE FUNCTION flag_insert_trigger_fun() RETURNS TRIGGER AS $BODY$
BEGIN
IF (NEW."flaggable_type" = 'Comment') THEN
INSERT INTO comment_flags VALUES (NEW.*);
ELSIF (NEW."flaggable_type" = 'Profile') THEN
INSERT INTO profile_flags VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Wrong "flaggable_type"="%", fix flag_insert_trigger_fun() function', NEW."flaggable_type";
END IF;
RETURN NULL;
END; $BODY$ LANGUAGE plpgsql;
CREATE TRIGGER flag_insert_trigger
BEFORE INSERT ON flags
FOR EACH ROW EXECUTE PROCEDURE flag_insert_trigger_fun();
The only workaround I found, is to create a view for the base table & use INSTEAD OF triggers on that view:
CREATE TABLE flags_base (
id integer NOT NULL,
flaggable_type character varying(255) NOT NULL,
flaggable_id integer NOT NULL,
body text
);
ALTER TABLE ONLY flags_base
ADD CONSTRAINT flags_base_pkey PRIMARY KEY (id);
CREATE TABLE "comment_flags" (
CHECK ("flaggable_type" = 'Comment'),
PRIMARY KEY ("id")
) INHERITS ("flags_base");
CREATE TABLE "profile_flags" (
CHECK ("flaggable_type" = 'Profile'),
PRIMARY KEY ("id")
) INHERITS ("flags_base");
CREATE OR REPLACE VIEW flags AS SELECT * FROM flags_base;
CREATE OR REPLACE FUNCTION flag_insert_trigger_fun() RETURNS TRIGGER AS $BODY$
BEGIN
IF (NEW."flaggable_type" = 'Comment') THEN
INSERT INTO comment_flags VALUES (NEW.*);
ELSIF (NEW."flaggable_type" = 'Profile') THEN
INSERT INTO profile_flags VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Wrong "flaggable_type"="%", fix flag_insert_trigger_fun() function', NEW."flaggable_type";
END IF;
RETURN NEW;
END; $BODY$ LANGUAGE plpgsql;
CREATE TRIGGER flag_insert_trigger
INSTEAD OF INSERT ON flags
FOR EACH ROW EXECUTE PROCEDURE flag_insert_trigger_fun();
But this way you must supply the id field on each insertion (even if flags_base's primary key has a default value / is a serial), so you must prepare your insert trigger to fix NEW.id if it is a NULL.
UPDATE: It seems views' columns can have a default values too, set with
ALTER VIEW [ IF EXISTS ] name ALTER [ COLUMN ] column_name SET DEFAULT expression
which is only used in views have an insert/update rule/trigger.
http://www.postgresql.org/docs/9.3/static/sql-alterview.html
#pozs provided a correct answer but didn't quite provide the code for a full working implementation. I tried to include the code in an edit on his question, but it was not accepted. He instead suggested yet another approach, which looks cleaner, but may have some drawbacks (in the case where you re-use your trigger function elsewhere).
Including my solution here for reference:
CREATE TABLE base_flags (
id integer NOT NULL,
flaggable_type character varying(255) NOT NULL,
flaggable_id integer NOT NULL,
body text
);
ALTER TABLE ONLY base_flags
ADD CONSTRAINT base_flags_pkey PRIMARY KEY (id);
CREATE SEQUENCE base_flags_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE base_flags_id_seq OWNED BY base_flags.id;
CREATE OR REPLACE VIEW flags AS SELECT * FROM base_flags;
CREATE TABLE "comment_flags" (
CHECK ("flaggable_type" = 'Comment'),
PRIMARY KEY ("id"),
FOREIGN KEY ("flaggable_id") REFERENCES "comments"("id")
) INHERITS ("flags");
CREATE TABLE "profile_flags" (
CHECK ("flaggable_type" = 'Profile'),
PRIMARY KEY ("id"),
FOREIGN KEY ("flaggable_id") REFERENCES "profiles"("id")
) INHERITS ("flags");
CREATE OR REPLACE FUNCTION flag_insert_trigger_fun() RETURNS TRIGGER AS $BODY$
BEGIN
IF NEW.id IS NULL THEN
NEW.id := nextval('base_flags_id_seq');
END IF;
IF (NEW."flaggable_type" = 'Comment') THEN
INSERT INTO comment_flags VALUES (NEW.*);
ELSIF (NEW."flaggable_type" = 'Profile') THEN
INSERT INTO profile_flags VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Wrong "flaggable_type"="%", fix flag_insert_trigger_fun() function', NEW."flaggable_type";
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER flag_insert_trigger
INSTEAD OF INSERT ON base_flags
FOR EACH ROW EXECUTE PROCEDURE flag_insert_trigger_fun();