Getting a violates foreign key constraint from PostgreSQL in a AFTER INSERT OR UPDATE trigger - postgresql

I have two tables, documents and events. When a document is inserted or updated I would like to update the events table.
-- Create the document store table
CREATE TABLE documents (
id TEXT PRIMARY KEY,
parent TEXT REFERENCES documents,
data JSONB,
created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Create the normalized event table
CREATE TABLE events (
document TEXT REFERENCES documents,
type TEXT NOT NULL,
eventDate date NOT NULL,
title TEXT,
subtitle TEXT,
description TEXT,
PRIMARY KEY(document, type, eventDate)
);
I have a helper function and trigger:
-- Keep the update documentDB column valid
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated = now();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER trigger_patents_updated BEFORE UPDATE ON documents
FOR EACH ROW EXECUTE PROCEDURE update_modified_column();
-- Put a document
CREATE OR REPLACE FUNCTION PutDocument(in_data JSON)
RETURNS TEXT AS $$
DECLARE
r_id TEXT;
BEGIN
INSERT INTO documents (id, parent, data) VALUES (in_data::JSONB->>'id',
in_data::JSONB->>'parent', in_data::JSONB)
ON CONFLICT (id) DO UPDATE SET data=in_data::JSONB RETURNING id
INTO r_id;
RETURN r_id;
END;
$$ language 'plpgsql';
Then I have the trigger that should update the events table
-- Create birthday events
CREATE OR REPLACE FUNCTION document_to_event_birthday()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO events (document, type, eventDate, title, subtitle, description)
VALUES(NEW.data->'id', 'BIRTH', to_date(NEW.data->'birth'->>'date', 'YYYY-MM-DD'), 'title', 'subtitle', 'description')
ON CONFLICT ON CONSTRAINT events_pkey DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER document_to_event_birthday
AFTER INSERT OR UPDATE ON documents
FOR EACH ROW EXECUTE PROCEDURE document_to_event_birthday();
When I try to insert a document it errors in the document_to_event_birthday function.
PutDocument('{"id": "ham", "test": "value", "birth": { "date": "2011-06-02" }}');
Gives me
ERROR: insert or update on table "events" violates foreign key constraint "events_document_fkey"
DETAIL: Key (document)=("ham") is not present in table "documents".
CONTEXT: SQL statement "INSERT INTO events (document, type, eventDate, title, subtitle, description)
VALUES(NEW.data->'id', 'BIRTH', to_date(NEW.data->'birth'->>'date', 'YYYY-MM-DD'), 'title', 'subtitle', 'description')
ON CONFLICT ON CONSTRAINT events_pkey DO NOTHING"
PL/pgSQL function document_to_event_birthday() line 3 at SQL statement
SQL statement "INSERT INTO documents (id, parent, data) VALUES (in_data::JSONB->>'id',
in_data::JSONB->>'parent', in_data::JSONB)
ON CONFLICT (id) DO UPDATE SET data=in_data::JSONB RETURNING id"
PL/pgSQL function putdocument(json) line 5 at SQL statement
If I look at the tables after the error, they are both empty. So my trigger is not happening after the insert/update on the document table.
How do I go about fixing this?

You made a typo in the function document_to_event_birthday(): In
INSERT INTO events (document, ...)
VALUES (NEW.data->'id', ...)
you should have used
NEW.data->>'id'
The way the function is coded now, the function tries to insert a jsonb value, which implicitly gets converted to text. But then the value is "ham" rather than ham with additional double quotes, which violates the foreign key constraint.

Related

Postgres update view with NOT NULL constraints on its base table

#PostgreSQL 10.22
Lets say I have a table and a view of it such as in:
CREATE TABLE item
(
id integer Not Null,
name varchar(50) Not Null
);
CREATE OR REPLACE VIEW all_items AS(
SELECT i.id
FROM item i
)
WITH CHECK OPTION
When I try to insert a tuple to all_items view I get an error because of the 'Not Null' constraint on the base table. Eg:
INSERT INTO all_items
VALUES (999)
ERROR: null value in column "name" violates not-null constraint
DETAIL: Failing row contains (999, null).
I tried to use triggers in order for it to work but it didn't:
CREATE OR REPLACE FUNCTION fill_NULL_attributes()
RETURNS trigger AS $$
BEGIN
IF NEW.name IS NULL THEN
NEW.name := 'X'; -- fills empty attribute with some value
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql
CREATE TRIGGER all_items_insert_fix
INSTEAD OF INSERT ON all_items
FOR EACH ROW
EXECUTE PROCEDURE fill_NULL_attributes();
This trigger solution didn't work because NEW doesn't have the "name" attribute.
Is there any way of doing this?
Your trigger function would be correct in a BEFORE trigger, since it modifies the row you are about to insert. But that is not correct in an INSTEAD OF trigger: there, you have to perform the INSERT into the base table yourself.

Postgresql update timestamp trigger in inherited table

I used this example and it worked well when all my tables were in public schema.
But trigger hasn't been working since I separated tables into different schemas and applied inheriting.
Here is example of my structure:
CREATE SCHEMA common;
CREATE SCHEMA video;
CREATE TABLE common.file (
file_id SERIAL PRIMARY KEY,
url VARCHAR(255) NOT NULL,
mime_type VARCHAR(31) DEFAULT '' NOT NULL,
size INTEGER NOT NULL,
modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE TABLE video.file (
width INTEGER NOT NULL,
height INTEGER NOT NULL,
local_path VARCHAR(255) DEFAULT '' NOT NULL
)
INHERITS (common.file);
CREATE FUNCTION common.update_modified()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.modified = now();
RETURN NEW;
END;
CREATE TRIGGER update_modified
BEFORE UPDATE ON common.file
FOR EACH ROW EXECUTE PROCEDURE common.update_modified();
When I do UPDATE common.file ... or UPDATE video.file ... field common.file.modified doesn't change itself. It seems trigger doesn't run, but I don't understand why.
What should I do to repair the behavior?
In described issue trigger is set only on common.file, so UPDATE common.file ... doesn't work if row inserted in video.file
Documentation says: INSERT always inserts into exactly the table specified
So trigger should be applied to both common.file and video.file.
-- Creating schemas and tables the same
-- Let function be in public scheme
CREATE FUNCTION update_modified()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.modified = now();
RETURN NEW;
END;
CREATE TRIGGER update_modified
BEFORE UPDATE ON common.file
FOR EACH ROW EXECUTE PROCEDURE update_modified();
CREATE TRIGGER update_modified
BEFORE UPDATE ON video.file
FOR EACH ROW EXECUTE PROCEDURE update_modified();
In that case when we update rows inserted either in common.file or in video.file corresponding trigger will call.

Postgres insert procedure throws foreign key exception

I have two tables vendor_service_place and places with places.id as a foreign key in vendor_service_table. I have created a procedure which first inserts an entry into places table and then takes that id from LASTVAL() and insert an entry into vendor_service_table. But when I am executing this function I am getting
insert or update on table "vendor_service_place" violates foreign key
constraint "fk_places"
DETAIL: Key (place_id)=(2057) is not present in table "places".
CONTEXT: SQL function "insert_data" statement 2
This is my insert procedure:
CREATE FUNCTION insert_data(vendorid integer,
serviceid integer,
name text,
address text,
latitude text,
longitude text,
contact_info text,
rating numeric,
description text) RETURNS bigint AS $$
INSERT INTO places(name,address,latitude,longitude,contact_info,created_at)
VALUES (name,address,latitude,longitude,contact_info,current_timestamp);
INSERT INTO vendor_service_place(vendor_id,service_id,place_id,description,rating,created_at)
VALUES (vendorid,serviceid,LASTVAL(),description,rating,current_timestamp);
SELECT LASTVAL() as result;
$$ LANGUAGE SQL;
I am suspecting that Postgres performs some kind of batching where it executes both these statements together, that's probably why Its not able to find the id in places table. Any ideas on how to do it properly?
Seems using lastval() to get last insert id is not recommend if you are doing multiple inserts. Postgres not returning lastval() properly.
Procedure is working fine after replacing LastVal() with return id statement.
DECLARE
insert_id bigint;
BEGIN
INSERT INTO places(name,address,latitude,
longitude,contact_info,
created_at,coordinates)
VALUES (name,address,latitude,
longitude,contact_info,
current_timestamp,
ST_SetSRID(ST_MakePoint(cast (longitude as numeric),cast (latitude as numeric)),4326))
returning id into insert_id;
INSERT INTO vendor_service_place(vendor_id,service_id,place_id,
description,rating,created_at)
VALUES (vendorid,serviceid,insert_id,
description,rating,current_timestamp);
return insert_id;
END

ERROR: record "old" is not assigned yet

I'm having difficulties in this simple trigger. My purpose is to verify before inserting a new register if is there's a register with the same field content which is "tag_id". If NEW tag_id is the same tag_id of any register on my table "coordenadas", then it updates, if not, it inserts a new one. When I try to insert sth, I get the error:
ERROR: record "old" is not assigned yet
DETAIL: The tuple structure of a not-yet-assigned record is indeterminate.
CONTEXT: PL/pgSQL function verifica_coo() line 7 at IF
I have this table:
CREATE TABLE public.coordenadas
(
id bigint NOT NULL,
pos_data timestamp without time zone,
pos_latitude double precision,
pos_longitude double precision,
tag_id bigint NOT NULL,
gado_id bigint NOT NULL,
CONSTRAINT coordenadas_pkey PRIMARY KEY (id),
CONSTRAINT coordenadas_gado_id_fkey FOREIGN KEY (gado_id)
REFERENCES public.gado (gado_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT fkj14dwmpa6g037ardymqc2q4lj FOREIGN KEY (tag_id)
REFERENCES public.tag (tag_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT fktawrw6tlliq4ace5p7io87c5p FOREIGN KEY (gado_id)
REFERENCES public.gado (gado_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
This trigger:
CREATE TRIGGER verifica_coo
BEFORE INSERT OR UPDATE ON coordenadas
FOR EACH ROW EXECUTE PROCEDURE verifica_coo();
This function:
CREATE OR REPLACE FUNCTION public.verifica_coo()
RETURNS trigger AS $verifica_coo$
BEGIN
--
-- Verifica se é para inserir ou atualizar os dados na tabela.
--
IF (NEW.tag_id != OLD.tag_id ) THEN
INSERT INTO coordenadas (pos_data,pos_latitude,pos_longitude,tag_id,gado_id)
VALUES (NEW.pos_data,NEW.pos_latitude,NEW.pos_longitude,NEW.tag_id,NEW.gado_id);
ELSE
UPDATE coordenadas SET pos_data = NEW.pos_data, pos_latitude = NEW.pos_latitude, pos_longitude = NEW.pos_longitude WHERE tag_id = NEW.tag_id;
END IF;
RETURN NEW;
END;
$verifica_coo$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION public.verifica_coo()
OWNER TO postgres;
My insert:
INSERT INTO coordenadas (pos_data,pos_latitude,pos_longitude,tag_id,gado_id) VALUES ('21/08/2016', '-23.563844' ,'-46.322525', '2','2');
This is because:
OLD
Data type RECORD; variable holding the old database row for
UPDATE/DELETE operations in row-level triggers. This variable is
unassigned in statement-level triggers and for INSERT operations.
So you first need to check if you are doing an insert or update. That information is available in TG_OP
IF TG_OP = 'UPDATE' THEN
-- some code involving OLD
ELSE
-- other code

Insert same UUID into two tables (once as primary key, once as foreign key) in one statement

Consider the postgres view which joins together two tables table_geom and table_data by the field id_data (id_data being the primary key of table_data and a foreign key in table_geom):
CREATE OR REPLACE VIEW myschema.view AS
SELECT table_geom.geom, table_geom.id_geom, table_geom.id_data,
table_data.id_data, table_data.data
FROM myschema.table_geom, myschema.table_data
WHERE table_geom.id_data = table_data.id_data;
id_geom and id_data are UUIDs. I'd like to autogenerate them on insert using uuid_generate_v4() with a rule such as
CREATE OR REPLACE RULE view_insert_rule AS
ON INSERT TO myschema.view DO INSTEAD (
INSERT INTO myschema.table_geom (geom, id_geom, id_data) VALUES (new.geom, (select uuid_generate_v4()), $ID_DATA$);
INSERT INTO myschema.table_data (id_data, data) VALUES ($ID_DATA$, new.data);
);
Problem: $ID_DATA$ needs to be the same UUID when inserting into the two tables.
One attempt was
CREATE OR REPLACE RULE view_insert_rule AS
ON INSERT TO myschema.view DO INSTEAD (
WITH ins_data as (
INSERT INTO myschema.table_data (id_data, data) VALUES ((select uuid_generate_v4()), new.data) RETURNING id_data
)
INSERT INTO myschema.table_geom (geom, id_geom, id_data) VALUES (new.geom, (select uuid_generate_v4()), ins_data.id_data);
);
which however does not work due to ERROR: cannot refer to NEW within WITH query.
Any idea how write such an insert rule?
Since you are doing an INSERT on a view, the recommended procedure is an INSTEAD OF INSERT trigger on the view. In the trigger function you rewrite the insert on the view into two inserts on the underlying tables:
CREATE FUNCTION insert_new_uuids() RETURNS trigger AS $$
DECLARE
new_id uuid;
BEGIN
new_id := uuid_generate_v4();
INSERT INTO myschema.table_data (id_data, data) VALUES (new_id, NEW.data);
INSERT INTO myschema.table_geom (geom, id_geom, id_data) VALUES (NEW.geom, uuid_generate_v4(), new_id);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER ins_view
INSTEAD OF INSERT ON myschema."view"
FOR EACH ROW EXECUTE PROCEDURE insert_new_uuids();