Upsert always updates postgresql even not matching where clause - postgresql

I have sql query:
INSERT INTO books VALUES (12, 0, CURRENT_TIMESTAMP)
ON CONFLICT (id)
WHERE version IS NULL OR updated + INTERVAL '2min' < CURRENT_TIMESTAMP
DO UPDATE SET version = books.version + 1, updated = CURRENT_TIMESTAMP;
however even if the where clause is not true, the row is updated. Here's example: https://dbfiddle.uk/CPHvZDm3
I can't understand what is wrong here.

The location of the WHERE clause is the issue. Corrected statement below.
CREATE TABLE books (
id int4 NOT NULL,
version int8 NOT NULL,
updated timestamp NULL,
CONSTRAINT books_pkey PRIMARY KEY (id)
);
INSERT INTO books VALUES (12, 0, CURRENT_TIMESTAMP)
ON CONFLICT (id)
DO UPDATE
SET version = books.version + 1, updated = CURRENT_TIMESTAMP
WHERE books.version IS NULL OR books.updated + INTERVAL '2min' < CURRENT_TIMESTAMP;
select *, CURRENT_TIMESTAMP, updated + INTERVAL '2min' < CURRENT_TIMESTAMP from books where id = 12;
id | version | updated | current_timestamp | ?column?
----+---------+----------------------------+--------------------------------+----------
12 | 0 | 11/13/2022 10:34:06.028222 | 11/13/2022 10:34:06.055526 PST | f
INSERT INTO books VALUES (12, 0, CURRENT_TIMESTAMP)
ON CONFLICT (id)
DO UPDATE
SET version = books.version + 1, updated = CURRENT_TIMESTAMP
WHERE books.version IS NULL OR books.updated + INTERVAL '2min' < CURRENT_TIMESTAMP;
select *, CURRENT_TIMESTAMP, updated + INTERVAL '2min' < CURRENT_TIMESTAMP from books where id = 12
id | version | updated | current_timestamp | ?column?
----+---------+----------------------------+--------------------------------+----------
12 | 0 | 11/13/2022 10:34:06.028222 | 11/13/2022 10:34:08.668121 PST | f
From docs INSERT:
and conflict_action is one of:
DO NOTHING
DO UPDATE SET { column_name = { expression | DEFAULT } |
( column_name [, ...] ) = [ ROW ] ( { expression | DEFAULT } [, ...] ) |
( column_name [, ...] ) = ( sub-SELECT )
} [, ...]
[ WHERE condition ]

Related

PostgreSQL recursive parent/child query

I'm having some trouble working out the PostgreSQL documentation for recursive queries, and wonder if anyone might be able to offer a suggestion for the following.
Here's the data:
Table "public.subjects"
Column | Type | Collation | Nullable | Default
-------------------+-----------------------------+-----------+----------+--------------------------------------
id | bigint | | not null | nextval('subjects_id_seq'::regclass)
name | character varying | | |
Table "public.subject_associations"
Column | Type | Collation | Nullable | Default
------------+-----------------------------+-----------+----------+--------------------------------------------------
id | bigint | | not null | nextval('subject_associations_id_seq'::regclass)
parent_id | integer | | |
child_id | integer | | |
Here, a "subject" may have many parents and many children. Of course, at the top level a subject has no parents and at the bottom no children. For example:
parent_id | child_id
------------+------------
2 | 3
1 | 4
1 | 3
4 | 8
4 | 5
5 | 6
6 | 7
What I'm looking for is starting with a child_id to get all the ancestors, and with a parent_id, all the descendants. Therefore:
parent_id 1 -> children 3, 4, 5, 6, 7, 8
parent_id 2 -> children 3
child_id 3 -> parents 1, 2
child_id 4 -> parents 1
child_id 7 -> parents 6, 5, 4, 1
Though there seem to be a lot of examples of similar things about I'm having trouble making sense of them, so any suggestions I can try out would be welcome.
To get all children for subject 1, you can use
WITH RECURSIVE c AS (
SELECT 1 AS id
UNION ALL
SELECT sa.child_id
FROM subject_associations AS sa
JOIN c ON c.id = sa. parent_id
)
SELECT id FROM c;
CREATE OR REPLACE FUNCTION func_finddescendants(start_id integer)
RETURNS SETOF subject_associations
AS $$
DECLARE
BEGIN
RETURN QUERY
WITH RECURSIVE t
AS
(
SELECT *
FROM subject_associations sa
WHERE sa.id = start_id
UNION ALL
SELECT next.*
FROM t prev
JOIN subject_associations next ON (next.parentid = prev.id)
)
SELECT * FROM t;
END;
$$ LANGUAGE PLPGSQL;
Try this
--- Table
-- DROP SEQUENCE public.data_id_seq;
CREATE SEQUENCE "data_id_seq"
INCREMENT 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 1
CACHE 1;
ALTER TABLE public.data_id_seq
OWNER TO postgres;
CREATE TABLE public.data
(
id integer NOT NULL DEFAULT nextval('data_id_seq'::regclass),
name character varying(50) NOT NULL,
label character varying(50) NOT NULL,
parent_id integer NOT NULL,
CONSTRAINT data_pkey PRIMARY KEY (id),
CONSTRAINT data_name_parent_id_unique UNIQUE (name, parent_id)
)
WITH (
OIDS=FALSE
);
INSERT INTO public.data(id, name, label, parent_id) VALUES (1,'animal','Animal',0);
INSERT INTO public.data(id, name, label, parent_id) VALUES (5,'birds','Birds',1);
INSERT INTO public.data(id, name, label, parent_id) VALUES (6,'fish','Fish',1);
INSERT INTO public.data(id, name, label, parent_id) VALUES (7,'parrot','Parrot',5);
INSERT INTO public.data(id, name, label, parent_id) VALUES (8,'barb','Barb',6);
--- Function
CREATE OR REPLACE FUNCTION public.get_all_children_of_parent(use_parent integer) RETURNS integer[] AS
$BODY$
DECLARE
process_parents INT4[] := ARRAY[ use_parent ];
children INT4[] := '{}';
new_children INT4[];
BEGIN
WHILE ( array_upper( process_parents, 1 ) IS NOT NULL ) LOOP
new_children := ARRAY( SELECT id FROM data WHERE parent_id = ANY( process_parents ) AND id <> ALL( children ) );
children := children || new_children;
process_parents := new_children;
END LOOP;
RETURN children;
END;
$BODY$
LANGUAGE plpgsql VOLATILE COST 100;
ALTER FUNCTION public.get_all_children_of_parent(integer) OWNER TO postgres
--- Test
SELECT * FROM data WHERE id = any(get_all_children_of_parent(1))
SELECT * FROM data WHERE id = any(get_all_children_of_parent(5))
SELECT * FROM data WHERE id = any(get_all_children_of_parent(6))

How to build a rules table in sql server

There are quite a few business rules which are currently hardcoded within a stored procedure. Wanted to explore the option of setting up a rules table where in we intend to key-in all business rules and based on it execute the stored procedure.
Though the system is little complicated have provided a simple version here.
Create table tblTest
(
TranID int primary key not null,
FName varchar(20) not null,
Age int not null,
Salary money not null,
MaritalStatus char(1) not null
)
Insert into tblTest values (1, 'Alex', 26, '25000.00','Y')
Insert into tblTest values (2, 'Brenda', 25, '14500.00','Y')
Insert into tblTest values (3, 'Peter', 69, '50000.00','N')
Insert into tblTest values (4, 'Paul', 64, '74500.00','Y')
Now to keep the example simple lets assume the business rules to be the following:
1. Age >=25,
2. Age < 65 and
3. Salary > 15K
Create table tblBusRule
(
RuleID int Primary key not null,
ColName varchar(20) not null,
Operator varchar(2) not null,
ColValue varchar(10) not null,
RuleOrder int not null
)
Insert into tblBusRule values (1, 'Age', '>=', '25', 1)
Insert into tblBusRule values (2, 'Age', '<', '65', 2)
Insert into tblBusRule values (3, 'Salary', '>', '15000.00', 3)
The direct query would be something like this which would output the record 1 (Alex) and 4 (Paul) alone.
Select * from tblTest
where
age >=25 and
age < 65 and
salary > '15000.00'
Now how to make this dynamic based on the rules mentioned in tblBusRule?
Using the stuff() with select ... for xml path ('') method of string concatenation and sp_executesql
declare #sql nvarchar(max), #where nvarchar(max);
set #where = stuff((
select ' and '+colname +' '+operator +' ' + colvalue+char(10)
from tblBusRule
order by RuleOrder
for xml path (''), type).value('.','nvarchar(max)')
,1,6,'');
set #sql = 'select * ' +char(10)+'from tblTest'+char(10)+'where '+#where;
select #sql as CodeGenerated;
exec sp_executesql #sql;
rextester demo: http://rextester.com/CGRF91788
returns:
+-------------------------+
| CodeGenerated |
+-------------------------+
| select * |
| from tblTest |
| where Age >= 25 |
| and Age < 65 |
| and Salary > 15000.00 |
+-------------------------+
+--------+-------+-----+------------+---------------+
| TranID | FName | Age | Salary | MaritalStatus |
+--------+-------+-----+------------+---------------+
| 1 | Alex | 26 | 25000,0000 | Y |
| 4 | Paul | 64 | 74500,0000 | Y |
+--------+-------+-----+------------+---------------+
Reference:
- The curse and blessings of dynamic SQL - Erland Sommarskog

Need row returned from upsert

I have a table that I need to upsert. If the row already exists then I want to update and return the row. If the row doesn't already exist then I need to insert and return the row. With the query I have below I get the row returned on insert, but not on update.
Table "main.message_account_seen"
Column | Type | Modifiers
----------------+--------------------------+-------------------------------------------------------------------
id | integer | not null default nextval('message_account_seen_id_seq'::regclass)
field_config_id | integer | not null
edit_stamp | timestamp with time zone | not null default now()
audit_stamp | timestamp with time zone |
message_id | integer | not null
account_id | integer |
Here's the sql.
with upsert as (
update message_account_seen set (message_id, account_id, field_config_id ) = (1, 60, 980)
where message_id = 1 and account_id = 60 and field_config_id = 980 returning *
)
insert into message_account_seen (message_id, account_id, field_config_id)
select 1, 60, 980
where not exists (select message_id, account_id, field_config_id from upsert) returning *;
I can't do a postgres function, it needs to be handled in a regular sql query. Also, there is no constraint on the table for uniqueness of row otherwise I would use on conflict. But I'm willing to scrap this query and go with something else if need be.
These are the results when I run the query, and then run it again. You can see that on the insert or first run I get the row returned. However on subsequent runs of the query I get 0 rows returned. I know that it's working because the edit_stamp increases in time. That's a good thing.
# with upsert as (
update message_account_seen set (message_id, account_id, field_config_id ) = (1, 60, 980)
where message_id = 1 and account_id = 60 and field_config_id = 980 returning *
)
insert into message_account_seen (message_id, account_id, field_config_id)
select 1, 60, 980
where not exists (select message_id, account_id, field_config_id from upsert) returning *;
id | field_config_id | edit_stamp | audit_stamp | message_id | account_id
--+-----------------+--------------------------------+-------------+------------+------------
38 | 980 | 09/27/2016 11:43:22.153908 MDT | | 1 | 60
(1 row)
INSERT 0 1
# with upsert as (
update message_account_seen set (message_id, account_id, field_config_id ) = (1, 60, 980)
where message_id = 1 and account_id = 60 and field_config_id = 980 returning *
)
insert into message_account_seen (message_id, account_id, field_config_id)
select 1, 60, 980
where not exists (select message_id, account_id, field_config_id from upsert) returning *;
id | field_config_id | edit_stamp | audit_stamp | message_id | account_id
----+-----------------+------------+-------------+------------+------------
(0 rows)
INSERT 0 0
When the update succeeds its result is not returned in your query. This does it:
with upsert as (
update message_account_seen
set (message_id, account_id, field_config_id ) = (1, 60, 980)
where (message_id, account_id, field_config_id) = (1, 60, 980)
returning *
), ins as (
insert into message_account_seen (message_id, account_id, field_config_id)
select 1, 60, 980
where not exists (select 1 from upsert)
returning *
)
select * from upsert
union all
select * from ins
;
The best option here is to use the new upsert that postgres 9.5 offers, but this requires a unique index on (message_id, account_id, field_config_id). It can be used like this:
INSERT INTO message_account_seen(message_id, account_id, field_config_id)
VALUES (1, 60, 980)
ON CONFLICT (message_id, account_id, field_config_id)
DO UPDATE
SET edit_stamp=now() -- adjust here
RETURNING *;
This is probably the fastest way to do this and guarantees that nothing unexpected will happen if two processes try to upsert into the same table at the same time (your approach doesn't guarantee that).

Using JSONB_ARRAY_ELEMENTS with WHERE ... IN condition

Online poker players can optionally purchase access to playroom 1 or playroom 2.
And they can be temporarily banned for cheating.
CREATE TABLE users (
uid SERIAL PRIMARY KEY,
paid1_until timestamptz NULL, -- may play in room 1
paid2_until timestamptz NULL, -- may play in room 2
banned_until timestamptz NULL, -- punished for cheating etc.
banned_reason varchar(255) NULL
);
Here the above table is filled with 4 test records:
INSERT INTO users (paid1_until, paid2_until, banned_until, banned_reason)
VALUES (NULL, NULL, NULL, NULL),
(current_timestamp + interval '1 month', NULL, NULL, NULL),
(current_timestamp + interval '2 month', current_timestamp + interval '4 month', NULL, NULL),
(NULL, current_timestamp + interval '8 month', NULL, NULL);
All 4 records belong to the same person - who has authenticated herself via different social networks (for example through Facebook, Twitter, Apple Game Center, etc.)
I am trying to create a stored function, which would take a list of numeric user ids (as a JSON array) and merge records belonging to same person into a single record - without losing her payments or punishments:
CREATE OR REPLACE FUNCTION merge_users(
IN in_users jsonb,
OUT out_uid integer)
RETURNS integer AS
$func$
DECLARE
new_paid1 timestamptz;
new_paid2 timestamptz;
new_banned timestamptz;
new_reason varchar(255);
BEGIN
SELECT min(uid),
current_timestamp + sum(paid1_until - current_timestamp),
current_timestamp + sum(paid2_until - current_timestamp),
max(banned_until)
INTO
out_uid, new_paid1, new_paid2, new_banned
FROM users
WHERE uid IN (SELECT JSONB_ARRAY_ELEMENTS(in_users));
IF out_uid IS NOT NULL THEN
SELECT banned_reason
INTO new_reason
FROM users
WHERE new_banned IS NOT NULL
AND banned_until = new_banned
LIMIT 1;
DELETE FROM users
WHERE uid IN (SELECT JSONB_ARRAY_ELEMENTS(in_users))
AND uid <> out_uid;
UPDATE users
SET paid1_until = new_paid1,
paid2_until = new_paid2,
banned_until = new_banned,
banned_reason = new_reason
WHERE uid = out_uid;
END IF;
END
$func$ LANGUAGE plpgsql;
Unfortunately, its usage results in the following error:
# TABLE users;
uid | paid1_until | paid2_until | banned_until | banned_reason
-----+-------------------------------+-------------------------------+--------------+---------------
1 | | | |
2 | 2016-03-27 19:47:55.876272+02 | | |
3 | 2016-04-27 19:47:55.876272+02 | 2016-06-27 19:47:55.876272+02 | |
4 | | 2016-10-27 19:47:55.876272+02 | |
(4 rows)
# select merge_users('[1,2,3,4]'::jsonb);
ERROR: operator does not exist: integer = jsonb
LINE 6: WHERE uid IN (SELECT JSONB_ARRAY_ELEMENTS(in_users))
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
QUERY: SELECT min(uid),
current_timestamp + sum(paid1_until - current_timestamp),
current_timestamp + sum(paid2_until - current_timestamp),
max(banned_until)
FROM users
WHERE uid IN (SELECT JSONB_ARRAY_ELEMENTS(in_users))
CONTEXT: PL/pgSQL function merge_users(jsonb) line 8 at SQL statement
Please help me to solve the problem.
Here is a gist with SQL code for your convenience.
Result of jsonb_array_elements() is a set of jsonb elements, therefore you need add explicit cast of uid to jsonb with to_jsonb() function, IN will be replaced with <# operator:
WITH t(val) AS ( VALUES
('[1,2,3,4]'::JSONB)
)
SELECT TRUE
FROM t,jsonb_array_elements(t.val) element
WHERE to_jsonb(1) <# element;
For your case, snippet should be adjusted to something like:
...SELECT ...,JSONB_ARRAY_ELEMENTS(in_users) user_ids
WHERE to_jsonb(uid) <# user_ids...

Strange date difference problem in PL/pgSQL

I have written a procedure that should increase salary for employee table according to days of each persons experience. The values for increase are in another table. Could someone tell me why it doesn't increase salary for employees working more than 3650 days?
DECLARE
row record;
row2 record;
dateDiff int;
BEGIN
FOR row IN EXECUTE 'SELECT * FROM employee'
LOOP
FOR row2 IN SELECT * FROM increases
LOOP
dateDiff := now()::date - row.empjoindate;
IF dateDiff> 3650 THEN
RAISE NOTICE '%', dateDiff;
END IF;
IF dateDiff >= row2.employment_length_from
AND dateDiff < row2.employment_length_to THEN
UPDATE employee SET empsalary = empsalary + row2.pay_rise WHERE empid = row.empid;
END IF;
END LOOP;
END LOOP;
END;
Table for increasing salaries looks like this:
id | employment_length_from | employment_length_to | pay_rise
----+------------------------+----------------------+----------
2 | 3650 | 7300 | 200
3 | 7300 | 10950 | 400
4 | 10950 | 14600 | 600
5 | 14600 | 18250 | 800
6 | 18250 | 21900 | 1000
1 | 0 | 3650 | 100
If something is not clear just ask me questions.
Edit
The table definitions are:
For Employee:
Column | Type | Modifiers
-----------------+-----------------------------+-----------
empid | integer | not null
empemailaddress | character varying(255) | not null
empjoindate | date |
emplastname | character varying(255) |
emplogintime | timestamp without time zone |
empname | character varying(255) |
ispermanent | boolean | not null
empsalary | double precision |
Indexes:
"employee_pkey" PRIMARY KEY, btree (empid)
For increases:
Column | Type | Modifiers
------------------------+------------------+-----------
id | integer | not null
employment_length_from | integer |
employment_length_to | integer |
pay_rise | double precision |
Indexes:
"increases_pkey" PRIMARY KEY, btree (id)
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp;
CREATE TABLE tmp.increases
(id INTEGER NOT NULL PRIMARY KEY
, employment_length_from INTEGER NOT NULL
, employment_length_to INTEGER NOT NULL
, pay_rise double precision
);
INSERT INTO tmp.increases(id
,employment_length_from,employment_length_to,pay_rise)
VALUES
(1 , 0 , 3650 , 100)
,(2 , 3650 , 7300 , 200)
,(3 , 7300 , 10950 , 400)
,(4 , 10950 , 14600 , 600)
,(5 , 14600 , 18250 , 800)
,(6 , 18250 , 21900 , 1000)
;
CREATE TABLE tmp.employee
( empid INTEGER NOT NULL
, empemailaddress VARCHAR (255) not null
, empjoindate DATE
, emplastname VARCHAR (255)
, emplogintime TIMESTAMP WITHOUT TIME ZONE
, empname VARCHAR(255)
, ispermanent BOOLEAN NOT NULL
, empsalary DOUBLE PRECISION
);
INSERT INTO tmp.employee(empid,empemailaddress,empjoindate,emplastname,emplogintime,empname,ispermanent,empsalary)
VALUES
(1,'lutser#nocorp.com' , '1939-01-01', 'Lutser', '2011-09-30' , 'Kleine' , True, 100.0 )
, (2,'lutser#nocorp.com' , '1949-01-01', 'Prutser', '2011-10-01' , 'Grote' , True, 200.0 )
, (3,'lutser#nocorp.com' , '1959-01-01', 'Klutser', '2011-10-01' , 'Grote' , True, 200.0 )
, (4,'lutser#nocorp.com' , '1969-01-01', 'Glutser', '2011-10-01' , 'Grote' , True, 200.0 )
, (5,'lutser#nocorp.com' , '1979-01-01', 'Brutser', '2011-10-01' , 'Grote' , True, 200.0 )
, (6,'lutser#nocorp.com' , '1989-01-01', 'Mutser', '2011-10-01' , 'Grote' , True, 200.0 )
;
SELECT * FROM tmp.employee ;
-- EXPLAIN ANALYZE
UPDATE tmp.employee emp
SET empsalary = empsalary + inc.pay_rise
FROM tmp.increases inc
WHERE (now() - emp.empjoindate)
>= inc.employment_length_from * '1 day'::interval
AND (now() - emp.empjoindate)
< inc.employment_length_to * '1 day'::interval
;
SELECT * FROM tmp.employee ;
Casting between intervals and integers can be painful. Above I solved this by multiplying the int with a 1day interval. Now it is up to you to embed this fragment in a procedure / function.
A simple UPDATE should do:
UPDATE employee e
SET empsalary = empsalary + i.pay_rise
FROM increases i
WHERE (now()::date - e.empjoindate) >= i.employment_length_from
AND (now()::date - e.empjoindate) < i.employment_length_to;
You don't need a plpgsql function for this.
I would advise you mark rows when they get their raise (in the same query) so you don't raise multiple times by accident.
Edit:
Here is a plpgsql function doing the same, as you asked for it. It returns the number of employees who got a raise.
CREATE OR REPLACE FUNCTION f_raise(OUT happy_employees integer) AS
$BODY$
BEGIN
UPDATE employee e
SET empsalary = empsalary + i.pay_rise
FROM increases i
WHERE (now()::date - e.empjoindate) >= i.employment_length_from
AND (now()::date - e.empjoindate) < i.employment_length_to;
GET DIAGNOSTICS happy_employees = ROW_COUNT;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
COMMENT ON FUNCTION f_raise() IS 'Gives employees who deserve it a raise.
Returns number of happy employees.'