I have a function which receives some parameters, does a SELECT to see if a table row exists and returns FALSE if so. If not, it does an INSERT but currently always fails with a 'duplicate key'. Here's a pseudo-code version...
CREATE OR REPLACE FUNCTION bob_function (
p_work_id INTEGER,
p_location_id VARCHAR,
p_area_id VARCHAR,
p_scheduled_work_no INTEGER,
p_start_date_time TIMESTAMPTZ,
p_work_date_time TIMESTAMPTZ,
p_user_id INTEGER,
p_comments TEXT,
p_work_stat_code CHAR(1)
)
RETURNS BOOLEAN AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM work_table
WHERE location_id = p_location_id
AND area_id = p_area_id
AND work_id = p_work_id
AND scheduled_work_no = p_scheduled_work_no
AND start_date_time = p_start_date_time
AND user_work_id = p_user_id
AND work_date_time = p_work_date_time
)
THEN
RAISE NOTICE 'Work already exists - SKIPPING';
RETURN FALSE;
END IF;
INSERT INTO work_table (
location_id,
area_id,
work_id,
scheduled_work_no,
start_date_time,
user_work_id,
work_date_time,
stat_code,
comment
)
VALUES (
p_location_id,
p_area_id,
p_work_id,
p_scheduled_work_no,
p_start_date_time,
p_user_id,
p_work_date_time,
v_work_stat_code,
p_comments
);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
The primary key is defined thus...
myDb=# \d task_work_pk
Index "schema1.task_work_pk"
Column | Type | Key? | Definition
-------------------+-----------------------------+------+-------------------
location_id | character varying(8) | yes | location_id
area_id | character varying(3) | yes | area_id
work_id | integer | yes | work_id
scheduled_work_no | integer | yes | scheduled_work_no
start_date_time | timestamp(0) with time zone | yes | start_date_time
user_work_id | integer | yes | user_work_id
work_date_time | timestamp(0) with time zone | yes | work_date_time
primary key, btree, for table "schema1.work_table"
Currently I get the following error every time I run this function...
ERROR: 23505: duplicate key value violates unique constraint "task_work_pk"
DETAIL: Key (location_id, area_id, work_id, scheduled_work_no, start_date_time, user_work_id, work_date_time)=(SITE_1, BOB, 218, 5, 2021-07-09 00:28:00+10, 1, 2021-07-09 21:00:15+10) already exists.
There are no rows whatsoever with work_id = 218 and this is the only place in the entire database where this table is written to. The function is only called no more frequently than once a minute and I'm 99% sure I've not got any race condition.
EDIT: updated to remove errors
I'm ignoring your PLPGSQL code because it is not real code and has obvious flaws.
Given that 218 doesn't exist the only way to cause that error without 218 pre-existing is to insert the same record twice in a single transaction.
Related
My Table structure is
CREATE TABLE dev.clbk_logs
(
id bigint NOT NULL,
clbk_typ character varying(255) COLLATE pg_catalog."default",
clbk_json json,
cre_dte timestamp without time zone,
ld_id bigint,
ld_num character varying(255) COLLATE pg_catalog."default",
mod_dte timestamp without time zone,
CONSTRAINT clbk_logs_pkey PRIMARY KEY (id)
)
WITH (
OIDS = FALSE
)
And my stored procedure as follows
CREATE OR REPLACE FUNCTION dev.get_ranged_loads(p_callback_types TEXT[],p_loads TEXT[], p_days_ago_1 INT, p_days_ago_2 INT)
RETURNS table(
clbk_json json,
ld_id character varying,
ld_num character varying,
days_ago int)
AS $BODY$
BEGIN
return query
SELECT a.clbk_json,a.ld_id,a.ld_num,(current_date - a.cre_dte::date) as _days_ago
FROM dev.clbk_logs a
WHERE exists (
SELECT * from json_array_elements_text(a.clbk_json -> 'ReferenceNumbers') as x(item)
WHERE x.item = any(p_loads)
AND a.clbk_typ = any(p_callback_types)
AND ((current_date - a.cre_dte::date)=p_days_ago_1 OR (current_date - a.cre_dte::date)<p_days_ago_2)
) order by a.cre_dte desc;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Currently this is giving me an error
ERROR: cannot change return type of existing function
DETAIL: Row type defined by OUT parameters is different.
HINT: Use DROP FUNCTION dev.get_ranged_loads(text[],text[],integer,integer) first.
SQL state: 42P13
What is getting wrong in above code
I am expecting the result set as
|---------|---------|------------|----------------|----------------|----------------|----------------|------------|
| id |clbk_typ |clbk_json |cre_dte | ld_id |ld_num |mod_dte |days_ago |
|---------|---------|------------|----------------|----------------|----------------|----------------|------------|
| 1172 |ADD | {} | 0 | 12 | | | |
| 1172 |UPDATE | {} | 40 | 45 | | | |
Thanks in advance
The error message is pretty clear. You can't change the return type. Run:
DROP FUNCTION dev.get_ranged_loads(TEXT[],TEXT[],INT,INT);
and then you'll be able to run CREATE OR REPLACE as usual.
I have the following tables in a Postgres 9.5 database:
product
Column | Type | Modifiers
----------------+-----------------------------+-----------------------------------------------------
id | integer | not null default nextval('product_id_seq'::regclass)
name | character varying(100) |
number_of_items | integer |
created_at | timestamp without time zone | default now()
updated_at | timestamp without time zone | default now()
total_number | integer |
provider_id | integer |
Indexes:
"pk_product" PRIMARY KEY, btree (id)
Foreign-key constraints:
"fk_product_provider" FOREIGN KEY (provider_id) REFERENCES provider(id)
And we also have
provider
Column | Typ | Modifiers
-------------+------------------------+------------------------------
id | integer | not null default nextval('property_id_seq'::regclass)
name | text |
description | text |
created_at | timestamp without time zone | default now()
updated_at | timestamp without time zone | default now()
Indexes:
"pk_provider" PRIMARY KEY, btree (id)
I am implelemtnig a plpgsql function which is supposed to find some specific products of a provider and loop through them
products = select u_id, number_of_items from product
where provider_id = p_id and total_number > limit;
loop
//here I need to loop through the products
end loop;
Question
what kind of data type should I declare for the products variables in order to store queried products into it? and also how should I have later on access to its columns like id or number_of_items?
In PostgreSQL, creating table also defines a composite data type with the same name as the table.
You can either use a variable of that type:
DECLARE
p product;
BEGIN
FOR p IN SELECT product FROM product WHERE ...
LOOP
[do something with "p.id" and "p.val"]
END LOOP;
END;
Or you can use several variables for the individual fields you need (probably better):
DECLARE
v_id integer;
v_val text;
BEGIN
FOR v_id, v_val IN SELECT id, val FROM product WHERE ...
LOOP
[do something with "v_id" and "v_val"]
END LOOP;
END;
I have a table Table_A:
\d "Table_A";
Table "public.Table_A"
Column | Type | Modifiers
----------+---------+-------------------------------------------------------------
id | integer | not null default nextval('"Table_A_id_seq"'::regclass)
field1 | bigint |
field2 | bigint |
and now I want to add a new column. So I run:
ALTER TABLE "Table_A" ADD COLUMN "newId" BIGINT DEFAULT NULL;
now I have:
\d "Table_A";
Table "public.Table_A"
Column | Type | Modifiers
----------+---------+-------------------------------------------------------------
id | integer | not null default nextval('"Table_A_id_seq"'::regclass)
field1 | bigint |
field2 | bigint |
newId | bigint |
And I want newId to be filled with the same value as id for new/updated rows.
I created the following function and trigger:
CREATE OR REPLACE FUNCTION autoFillNewId() RETURNS TRIGGER AS $$
BEGIN
NEW."newId" := NEW."id";
RETURN NEW;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER "newIdAutoFill" AFTER INSERT OR UPDATE ON "Table_A" EXECUTE PROCEDURE autoFillNewId();
Now if I insert something with:
INSERT INTO "Table_A" values (97, 1, 97);
newId is not filled:
select * from "Table_A" where id = 97;
id | field1 | field2 | newId
----+----------+----------+-------
97 | 1 | 97 |
Note: I also tried with FOR EACH ROW from some answer here in SO
What's missing me?
You need a BEFORE INSERT OR UPDATE ... FOR EACH ROW trigger to make this work:
CREATE TRIGGER "newIdAutoFill"
BEFORE INSERT OR UPDATE ON "Table_A"
FOR EACH ROW EXECUTE PROCEDURE autoFillNewId();
A BEFORE trigger takes place before the new row is inserted or updated, so you can still makes changes to the field values. An AFTER trigger is useful to implement some side effect, like auditing of changes or cascading changes to other tables.
By default, triggers are FOR EACH STATEMENT and then the NEW parameter is not defined (because the trigger does not operate on a row). So you have to specify FOR EACH ROW.
I have a table with a definition similar to the following (condensed for clarity):
CREATE TABLE fns(
id serial,
start_date timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_date timestamptz,
name text NOT NULL,
parent_id integer,
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES fns(id),
UNIQUE(name)
);
When an UPDATE takes place I would like the row being 'updated' to have its end_date set to CURRENT_TIMESTAMP and to have a new row created (based on the old one) with its start_date set to CURRENT_TIMESTAMP. For example:
Before UPDATE
| id | start_date | end_date | name | parent_id |
|----|-------------------------|----------|-------|-----------|
| 1 | April, 01 2015 00:00:00 | (null) | fns_a | (null) |
Desired state after UPDATE
| id | start_date | end_date | name | parent_id |
|----|-------------------------|-------------------------|-------------|-----------|
| 1 | April, 01 2015 00:00:00 | April, 02 2015 00:00:00 | fns_a [old] | (null) |
| 2 | April, 02 2015 00:00:00 | (null) | fns_a | 1 |
I'm running into issues with the unique constraint for the name column. Here is the current state of my trigger:
CREATE OR REPLACE FUNCTION enfore_fns_immutability() RETURNS trigger AS $func$
BEGIN
-- 'Turn off' old record.
OLD.end_date = CURRENT_TIMESTAMP;
OLD.name = OLD.name || ' [old]';
-- Create the new record.
INSERT INTO fns(start_date, name, parent_id)
VALUES(CURRENT_TIMESTAMP, NEW.name, OLD.id); -- <-- unique violation
RETURN OLD;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER tg_fns_bi
BEFORE UPDATE ON fns
FOR EACH ROW
EXECUTE PROCEDURE enforce_fns_immutability();
As far as I understand it this is failing because the update to OLD.name has not yet happened as the containing transaction has not committed. I'm struggling to think of a way around it but it feels like there must be an elegant solution for this! Some solutions I've considered:
Temporary table (feels like this is too heavyweight for this use case).
Use of an AFTER UPDATE trigger (same issue as the transaction has obviously not yet been committed).
I'm using Postgres 9.4.1.
You can create the unique constraint as deferred, in that case it will be checked when you commit your transaction, not when the insert is executed:
CREATE TABLE fns
(
id serial,
start_date timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_date timestamptz,
name text NOT NULL,
parent_id integer,
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES fns(id),
UNIQUE(name) deferrable initially deferred --<< here
);
An INSERT on a table triggers a stored proc where the following error occurs.
ERROR: column "targetedfamily" is of type boolean but expression is of type character varying
Hint: You will need to rewrite or cast the expression.
Where: PL/pgSQL function "fn_family_audit" line 19 at SQL statement
And here's the ERRING stored proc (notice that my attempt to fix the problem by doing CAST(NEW.targetedfamily AS BOOLEAN) does NOT seem to work)
CREATE OR REPLACE FUNCTION fn_family_audit() RETURNS TRIGGER AS $tr_family_audit$
BEGIN
--
-- Create a row in family_audit to reflect the operation performed on family,
-- make use of the special variable TG_OP to work out the operation.
--
IF (TG_OP = 'DELETE') THEN
INSERT INTO public.family_audit values (
DEFAULT, 'D', OLD.family_id, OLD.familyserialno, OLD.node_id, OLD.sourcetype, OLD.familyname,
OLD.familynamelocallang, OLD.hofname, OLD.hofnamelocallang, OLD.targetedfamily, OLD.homeless,
OLD.landless, OLD.dependentonlabour, OLD.womenprimaryearner, OLD.landlinenumber, OLD.username , now());
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO public.family_audit values(
DEFAULT, 'U',NEW.family_id, NEW.familyserialno, NEW.node_id, NEW.sourcetype, NEW.familyname,
NEW.familynamelocallang, NEW.hofname, NEW.hofnamelocallang, NEW.targetedfamily, NEW.homeless,
NEW.landless, NEW.dependentonlabour, NEW.womenprimaryearner, NEW.landlinenumber, NEW.username , now());
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO public.family_audit values(
DEFAULT, 'I',NEW.family_id, NEW.familyserialno, NEW.node_id, NEW.sourcetype, NEW.familyname,
NEW.familynamelocallang, NEW.hofname, NEW.hofnamelocallang, CAST(NEW.targetedfamily AS BOOLEAN), NEW.homeless,
NEW.landless, NEW.dependentonlabour, NEW.womenprimaryearner, NEW.landlinenumber, NEW.username , now());
RETURN NEW;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$tr_family_audit$ LANGUAGE plpgsql;
Here's the table definition
nucleus4=# \d family;
Table "public.family"
Column | Type | Modifiers
---------------------+-----------------------------+------------------------------------------------------------
family_id | integer | not null default nextval('family_family_id_seq'::regclass)
familyserialno | integer | not null
sourcetype | character varying(20) | not null
familyname | character varying(100) |
familynamelocallang | character varying(255) |
hofname | character varying(100) | not null
hofnamelocallang | character varying(255) | not null
targetedfamily | boolean |
homeless | boolean |
landless | boolean |
dependentonlabour | boolean |
womenprimaryearner | boolean |
landlinenumber | character varying(20) |
username | character varying(20) | not null
adddate | timestamp without time zone | not null default now()
updatedate | timestamp without time zone | not null default now()
node_id | integer | not null
Indexes:
"PK_family" PRIMARY KEY, btree (family_id)
"family_idx" UNIQUE, btree (familyserialno, node_id)
Foreign-key constraints:
"family_fk" FOREIGN KEY (node_id) REFERENCES hierarchynode_master(node_id)
Referenced by:
TABLE "agriland" CONSTRAINT "FK_agriland_family" FOREIGN KEY (family_id) REFERENCES family(family_id) ON UPDATE RESTRICT ON DELETE RESTRICT
TABLE "currentloans" CONSTRAINT "FK_currentloans_family" FOREIGN KEY (family_id) REFERENCES family(family_id) ON UPDATE RESTRICT ON DELETE RESTRICT
TABLE "family_address" CONSTRAINT "FK_family_address_family" FOREIGN KEY (family_id) REFERENCES family(family_id) ON UPDATE RESTRICT ON DELETE RESTRICT
TABLE "family_basic_info" CONSTRAINT "FK_family_basic_info_family" FOREIGN KEY (family_id) REFERENCES family(family_id) ON UPDATE RESTRICT ON DELETE RESTRICT
TABLE "family_entitlement" CONSTRAINT "FK_family_entitlement_family" FOREIGN KEY (family_id) REFERENCES family(family_id) ON UPDATE RESTRICT ON DELETE RESTRICT
TABLE "livestock" CONSTRAINT "FK_livestock_family" FOREIGN KEY (family_id) REFERENCES family(family_id) ON UPDATE RESTRICT ON DELETE RESTRICT
TABLE "member" CONSTRAINT "FK_member_family" FOREIGN KEY (family_id) REFERENCES family(family_id) ON UPDATE RESTRICT ON DELETE RESTRICT
TABLE "otherassets" CONSTRAINT "FK_otherassets_family" FOREIGN KEY (family_id) REFERENCES family(family_id) ON UPDATE RESTRICT ON DELETE RESTRICT
Triggers:
tr_family_audit AFTER INSERT OR DELETE OR UPDATE ON family FOR EACH ROW EXECUTE PROCEDURE fn_family_audit()
tr_family_updatedate BEFORE UPDATE ON family FOR EACH ROW EXECUTE PROCEDURE fn_modify_updatedate_column()
nucleus4=#
Here's family_audit
nucleus4=# \d family_audit;
Table "public.family_audit"
Column | Type | Mod
---------------------+-----------------------------+----------------------------------
familyaudit_id | integer | not null default nextval('family_
operation | character(1) | not null
family_id | integer | not null
familyserialno | integer | not null
sourcetype | character varying(20) | not null
familyname | character varying(100) |
familynamelocallang | character varying(255) |
hofname | character varying(100) | not null
hofnamelocallang | character varying(255) | not null
targetedfamily | boolean |
homeless | boolean |
landless | boolean |
dependentonlabour | boolean |
womenprimaryearner | boolean |
landlinenumber | character varying(20) |
username | character varying(20) | not null
adddate | timestamp without time zone | not null default now()
node_id | integer | not null
Indexes:
"PK_family_audit" PRIMARY KEY, btree (familyaudit_id)
nucleus4=#
Here's the trigger
CREATE TRIGGER tr_family_audit
AFTER INSERT OR UPDATE OR DELETE ON public.family
FOR EACH ROW EXECUTE PROCEDURE fn_family_audit();
I would appreciate any hints.
Thank you,
BR,
~A
Your problem is here:
NEW.hofnamelocallang
Your insert has one extra column (apparently NEW.node_id). Try changing your insert to:
INSERT INTO public.family_audit values(
DEFAULT, 'I',NEW.family_id, NEW.familyserialno,
NEW.sourcetype, NEW.familyname,
NEW.familynamelocallang, NEW.hofname, NEW.hofnamelocallang,
NEW.targetedfamily, NEW.homeless,
NEW.landless, NEW.dependentonlabour, NEW.womenprimaryearner,
NEW.landlinenumber, NEW.username , now()
);
The error you are getting is basically saying that you were trying to insert NEW.hofnamelocallang into targetedfamily column (which is boolean, not varchar) because of the extra column you were putting in the insert sentence.
I would advice that, when you are performing an insert, for sanity reasons, always enumerate the columns you are putting values into. Something like this:
insert into table foo
(col1, col2, col3) -- column enumeration here
values
(1, 2, 3);