Postgres insert procedure throws foreign key exception - postgresql

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

Related

Do statements within a PL/pgSQL function run sequentially?

I'd like to run the create_company_maybe_race_condition() function below, but think it might create a race condition if plpgsql functions don't run sequentially. I'm on Postgres 12.
My real world scenario is that I have Row Level Security turned on for all of my tables. The RLS check relies on an access control list in a permissions table. When I add a row to a table, I can't use the RETURNING clause since there isn't a row in the permissions table yet that'll allow the user to read the object.
CREATE TABLE companies (
PRIMARY KEY(company_id),
company_id uuid,
);
CREATE TABLE departments (
PRIMARY KEY (department_id),
department_id uuid DEFAULT gen_random_uuid(),
name text,
company_id uuid REFERENCES companies
);
/**********/
CREATE FUNCTION create_company_maybe_race_condition ()
RETURNS void AS $$
DECLARE
v_company_id := gen_random_uuid();
BEGIN
INSERT INTO companies (company_id)
VALUES (v_company_id);
INSERT INTO departments (company_id, name)
VALUES (v_company_id, 'My Department'); -- Postgres doesn't know that it depends on companies
END;
$$ LANGUAGE plpgsql;
/**********/
CREATE FUNCTION create_company_no_race_condition ()
RETURNS void AS $$
DECLARE
v_company_id uuid;
BEGIN
INSERT INTO companies (company_id)
VALUES (DEFAULT)
RETURN company_id INTO v_company_id;
INSERT INTO departments (company_id, name)
VALUES (v_company_id, 'My Department');
END;
$$ LANGUAGE plpgsql;

How can I pass table data into a function using a PostgresSQL composite type?

I am tying to pass data into a function using a composite type. However, I can't see how to call the function. Consider the following code:
drop table if exists ids cascade;
create table ids
(
id bigint primary key
);
drop table if exists data;
create table data
(
id bigint generated always as identity primary key references ids(id) deferrable initially deferred,
name text
);
drop type if exists raw_type cascade;
create type raw_type as (name text);
create table raw2 of raw_type;
insert into raw2(name)
values ('test1'),
('test2'),
('test3'),
('test4'),
('test5'),
('test6'),
('test7')
;
create or replace function special_insert(data_to_insert raw_type) returns void as
$func$
begin
with x as (insert into data(name) select name from data_to_insert returning id)
insert into ids(id)
select id from x;
end;
$func$ language plpgsql;
Running this:
begin transaction ;
select special_insert(raw2);
commit ;
I get the following error:
ERROR: column "raw2" does not exist
Running this:
begin transaction ;
select special_insert(name::raw_type) from raw2;
commit ;
I get
[2019-04-10 15:41:18] [22P02] ERROR: malformed record literal: "test1"
[2019-04-10 15:41:18] Detail: Missing left parenthesis.
What am I doing wrong?
The second function call is correct, however there is a bug in your function definition.
You make the mistake of treating data_to_insert as a table, but it is a single value. Use the following notation to get an individual field from a composite type:
INSERT INTO data (name)
VALUES (data_to_insert.name)
RETURNING id

SELECT in INSERTING in postgreSQL

I have two table Documents and RegCard (name uses a name of current_user)
Table Documents
CREATE TABLE public.document_dimauser
(
documentid uuid NOT NULL,
documentname character varying(100),
author character varying(100),
contents bytea,
CONSTRAINT document_dimauser_pkey PRIMARY KEY (documentid)
)
Table RegCard
CREATE TABLE public.regcard_dimauser
(
regcardid uuid NOT NULL,
documentid uuid,
documentintronumber character varying(100),
documentexternnumber character varying(100),
dateintro date,
dateextern date,
CONSTRAINT regcard_dimauser_pkey PRIMARY KEY (regcardid),
CONSTRAINT regcard_dimauser_documentid_fkey FOREIGN KEY (documentid)
REFERENCES public.document_dimauser (documentid) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
This two tables are connected one row 'documentid'
Also, i have a function, which inserting data in this two tables. Here i try selecting last rec of documentid from first table and transer him in second table
CREATE OR REPLACE FUNCTION public.addrecuserdocuments(
documentname character varying,
contents bytea,
documentintronumber character varying)
RETURNS void AS
$BODY$
DECLARE
comm VARCHAR;
nameuser VARCHAR;
currdate DATE;
iddoc uuid;
BEGIN
SELECT CURRENT_USER INTO STRICT nameuser;
SELECT CURRENT_DATE INTO STRICT currdate;
comm:='INSERT INTO Document_'||nameuser||' VALUES ('||quote_literal(uuid_generate_v4())||', '||quote_literal(documentname)||','||quote_literal(nameuser)||','||quote_literal(contents)||');
SELECT documentid INTO STRICT '||quote_literal(iddoc)||' FROM Document_'||nameuser||' order by documentid DESC LIMIT 1;
INSERT INTO Regcard_'||nameuser||' (regcardid, documentid, documentintronumber, dateintro) VALUES ('||quote_literal(uuid_generate_v4())||' , '||quote_literal(iddoc)||', '||quote_literal(documentintronumber)||', '||quote_literal(currdate)||');';
EXECUTE comm;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
But i have a error
ERROR: query string argument of EXECUTE is null
SQL-состояние: 22004
Контекст: PL/pgSQL function addrecuserdocuments(character varying,bytea,character varying) line 13 at EXECUTE
Please, tell me, what i do wrong?
you concat with || operator - if any value is null the whole string is null...
you can instead do smth like :
comm := format('INSERT INTO %I VALUES(%L)',concat(Document_',nameuser),uuid_generate_v4());
and so on...
Don't concatenate input values if you don't need that, use parameters in the string and use format() to build it. You also don't need variables to store the results of built-in functions and you don't need to "select" the UUID you generated inside your code.
Putting all that together you can simplify your code to:
BEGIN
iddoc := uuid_generate_v4();
comm : = format('INSERT INTO Document_%s VALUES ($1, $2, $3, $4)', current_user);
execute comm
using iddoc, documentname, current_user, contents;
comm := format('insert into regcard_%s (regcardid, documentid, documentintronumber, dateintro) values ($1, $2, $3, $4)', current_user);
execute comm
using uuid_generate_v4(), iddoc, documentintronumber, current_date;
END;
This not only makes the code much more readable it is also a way to prevent SQL injection through that function.
Unrelated, but: I find a design that uses a table with the user name appended highly questionable - especially if that user name is also stored inside the table. If you don't do that you can avoid all that dynamic SQL.

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

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.

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